Skip to content

Inflect DataID field from Node interface.#2249

Closed
alloy wants to merge 1 commit intofacebook:masterfrom
artsy:custom-node-id-redone
Closed

Inflect DataID field from Node interface.#2249
alloy wants to merge 1 commit intofacebook:masterfrom
artsy:custom-node-id-redone

Conversation

@alloy
Copy link
Copy Markdown
Contributor

@alloy alloy commented Dec 26, 2017

This PR makes it possible to inflect the DataID field to use from the user’s schema, rather than assuming it to be id.

How

  • The ID is either the single ID field of the Node interface in the user’s schema or otherwise falls back to id.
  • The ID field is selected using the __id alias.
  • At runtime the __id field in the response data is used as store DataID.

Example

If your schema looks like this:

interface Node {
  globalObjectID: ID!
}

type Artist implements Node {
  globalObjectID: ID!
  name: String!
}

…and you have a query like:

{
  artist(slug: "banksy") {
    name
  }
}

…then Relay will transform that to:

{
  artist(slug: "banksy") {
    __id: globalObjectID
    name
  }
}

…and use the value of __id as the key when writing the record to the store.


const nextID =
item.id ||
(field.idField && item[field.idField]) ||
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two changes are the only runtime changes.

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Dec 27, 2017

@josephsavona I got a couple of notification emails from GH with comments from you on this PR, but I don’t see them here. Is it a GH fluke or did you remove the comments?

@josephsavona
Copy link
Copy Markdown
Member

Sorry about that. I meant to start a code review and accidentally posted not fully thought out comments instead - will take another look when I have computer time (holidays, baby, etc).

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Dec 27, 2017

Oh no worries at all, I just got confused. Please enjoy your holidays and baby instead! 🙏 This can wait.

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Jan 30, 2018

Rebased on master.

node.idField = idFieldSelection && idFieldSelection.name;
}
return node;
},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josephsavona I think in your retracted initial review you said that this should become its own transformer, right? I think that makes sense, it’s kinda awkward here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this should be its own transform rather than happening at codegen time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do 👍

Copy link
Copy Markdown
Member

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave a real review to @kassens and others since I'm no longer working on Relay. Other than the comment about splitting out transform-like code from codegen into a transform, the other thing that occurs to me here is the perf impact: adding an extra property (that is almost always the same redundant "id" value) to tons of fields increases the size of query modules, and it also incurs an extra condition/branch at runtime when reading.

node.idField = idFieldSelection && idFieldSelection.name;
}
return node;
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this should be its own transform rather than happening at codegen time

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Jan 30, 2018

adding an extra property (that is almost always the same redundant "id" value) to tons of fields increases the size of query modules

Ah yes, good point. I hadn’t considered bundle size enough, probably because 99% of our Relay code thus far exists in our mobile app.

If that turns out to be a major concern, then we can just special-case id and revert to the previous behaviour of not emitting idField and hardcoding checks for id at runtime.

it also incurs an extra condition/branch at runtime when reading

Related to the previous item, I think that even when reverting to special-casing the id field, this extra branch would remain because we’d have to first determine if an idField is specified before falling back to the hardcoded id field, otherwise we end up selecting an id field that should not ben selected.

Copy link
Copy Markdown
Contributor

@leebyron leebyron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks cool! Some suggestions to continue:

  • This should be an extension of the existing transform which adds id fields

  • Rather than having to track idField on the resulting IR, you should use aliases, so that relay always knows to look for a specific response field that represents IDs.

  • I'd suggest looking for ID type in addition to ID!.

Also, a question. If a type has more than one fields with type ID!, what should be the right behavior?

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Feb 11, 2018

Rather than having to track idField on the resulting IR, you should use aliases, so that relay always knows to look for a specific response field that represents IDs.

I like the simplicity of that idea 👌 What name would you suggest, is this where we use a name like __id?

I'd suggest looking for ID type in addition to ID!.

The Global ID spec mentions that this field is supposed to be non-null, but that’s probably more because of how Relay Classic was supposed to be able to refetch using that ID, rather it being needed for cache denormalisation (indeed Relay Modern will generate a client ID if the ID is missing from the response)? If so, sounds good to me 👍

Also, a question. If a type has more than one fields with type ID!, what should be the right behavior?

A type may, but the Node interface shouldn’t, which is what this PR is about. If that were to occur then this code will raise an assertion error.

The logic in this PR is:

  • If a Node interface exists in the schema, assume it has a single ID! field and use that
  • If no Node interface exists or the type does not implement the Node interface, check if the type has an id field and use that
  • Otherwise generate client IDs

@alloy alloy force-pushed the custom-node-id-redone branch from 3e3decb to 6fb1d8d Compare February 14, 2018 20:33
@alloy alloy changed the title Inflect DataID field from Node interface. [WIP] Inflect DataID field from Node interface. Feb 14, 2018
alloy added a commit to alloy/relay-examples that referenced this pull request Feb 14, 2018
@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Feb 14, 2018

@leebyron Ok, I think I addressed all your feedback, I’ve updated the original explanation to reflect this. The overall implementation is definitely a lot simpler 👌

There’s a fork of the TODO example app that uses this here https://github.com/alloy/relay-examples/tree/custom-node-id/todo and we’re pulling this into our app for further testing as well artsy/emission#985.

@alloy alloy changed the title [WIP] Inflect DataID field from Node interface. Inflect DataID field from Node interface. Feb 14, 2018
@alloy alloy force-pushed the custom-node-id-redone branch 2 times, most recently from 4098931 to ee71c36 Compare February 19, 2018 12:54
@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Feb 19, 2018

Rebased on master.

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Mar 1, 2018

@leebyron FYI We’re now running this updated code in production in both our web and iOS app. You can see an example query here, but note that our schema uses __id for the GUID and thus most selections are just __id, only on line 150 will you find a __id: id selection.

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Mar 23, 2018

@leebyron Could I get another review?

@alloy alloy force-pushed the custom-node-id-redone branch from ee71c36 to b5e4b8f Compare April 9, 2018 03:42
@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Apr 9, 2018

I’ve gone ahead and squashed all the commits now to ease rebasing on my end. I’ve also rebased, but alas can’t run the tests at the moment due to the issue described in #2407

@alloy alloy force-pushed the custom-node-id-redone branch from b5e4b8f to 3ca8f8c Compare April 9, 2018 22:57
@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Apr 9, 2018

Tests pass again 👍

Copy link
Copy Markdown
Contributor

@jstejada jstejada left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alloy going to import this to test internally

Regarding this earlier comment:

If that turns out to be a major concern, then we can just special-case id and revert to the previous behaviour of not emitting idField and hardcoding checks for id at runtime.

Just want to double check, did you end up doing this? It looks like you did, but still always alias to __id?

## Comparison to Classic Relay

For users of classic Relay, note that the runtime makes as few assumptions as possible about GraphQL. Compared to earlier versions of Relay there is no concept of routes, there are no limitations on mutation input arguments or side-effects, arbitrary root fields just work, etc. At present, the main restriction from classic Relay that remains is the use of the `Node` interface and `id` field for object identification. However there is no fundamental reason that this restriction can't be relaxed (there is a single place in the codebase where object identity is determined), and we welcome feedback from the community about ways to support customizable object identity without negatively impacting performance.
For users of classic Relay, note that the runtime makes as few assumptions as possible about GraphQL. Compared to earlier versions of Relay there is no concept of routes, there are no limitations on mutation input arguments or side-effects, arbitrary root fields just work, etc. Like classic Relay, modern Relay still uses the `Node` interface for object identification purposes; however, rather than always assuming the globally unique field to be `id`, modern Relay will inflect it from the `Node` interface by searching for either an `ID` or `ID!` field.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we use a different term than inflect, or not use it at all? (same for the title of this PR) I might just be me as a non-native english speaker, but I didn't know what inflect means, and googling the definition didn't really help until I actually looked at what this PR was doing.

maybe something like: "Relay Modern will find a field of type ID or ID! defined in the Node interface and use it as a globally unique ID"

RelayRelayDirectiveTransform.transform,
RelayMaskTransform.transform,
RelayDeferrableFragmentTransform.transform,
RelayGenerateIDFieldTransform.transform,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I'm not super familiar with the compiler code. If we weren't using this transform here before, where were we using it?

* A transform that adds an `id` field on any type that has an id field but
* where there is no unaliased `id` selection.
* A transform that adds a `__id` field on any type that has a `Node` or `id`
* field but where there is no unaliased `__id` selection.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we'll always add an __id selection, unless there's already an unaliased __id selection.

what happens when there's already an aliased __id selection?, will we add another __id selection too?

// If the field already has an unaliased `id` field, do nothing
if (hasUnaliasedSelection(field, ID)) {
// If the field already has an `__id` selection, do nothing.
if (hasSelection(node, '__id')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

answering my own question from above, it seems that this won't add an __id selection if a selection for __id already exists, be it aliased or unaliased. Maybe update the comment above to reflect that?

return IRTransformer.transform(
context,
{
LinkedField: visitLinkedField,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I'm probably missing some context, why did we previously only apply this transform to LinkedFields, and now we do so for Fragments too?

// implement `Node`
// - If the field type is abstract, then generate a `... on Node { __id: id }`
// fragment if *any* concrete type implements `Node`. Then generate a
// `... on PossibleType { __id: id }` for every concrete type that does
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if PossibleType doesn't have an id field? can we add test coverage for this?

Copy link
Copy Markdown
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jstejada has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Aug 4, 2018

New TODOs

  • RelayDeferrableFragmentTransform hardcodes id
  • RelayViewerHandleTransform hardcodes id
  • RelayValidator disallows id alias
  • RelayQLPrinter may still be generating id fields

* The ID is either the single `ID` field of the `Node` interface in the
  user’s schema or otherwise falls back to `id`.
* The ID field is selected using the `__id` alias.
@alloy alloy force-pushed the custom-node-id-redone branch from 3ca8f8c to 20eeddb Compare August 4, 2018 17:33
@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Aug 4, 2018

This rebased version has a couple of failing tests, that I have left in as a good reminder of what to work on and I also need to address @jstejada’s feedback, but for now I want to first see if it actually works in our apps.

Copy link
Copy Markdown
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jstejada has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@clentfort
Copy link
Copy Markdown

What is the current progress on this issue? Is there any way one could help to advance this PR?

@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Jun 3, 2019

This patch has served us well, but we’ve decided that we are going to sunset this PR. The reasons are:

  • Redoing this PR for current master is non-trivial and was a constant source for us not being able to upgrade to latest Relay easily.
  • Since the creation of this patch, the GraphQL spec has decided to reserve fields starting with a dunderscore for internal use only, so even when/if we’d ever land this we at Artsy would still be left with a schema that all of our tooling has started complaining about.

In the end, we decided to start experimentation with a v2 of our schema in which we transform v1 to rename all of our ID fields to conform to the Node spec. Things are looking good thus far and we’ve been able to upgrade to Relay 4 easily.

@alloy alloy closed this Jun 3, 2019
@alloy
Copy link
Copy Markdown
Contributor Author

alloy commented Jun 3, 2019

@clentfort What a coincidence that you would ask that on the day that I was planning on sunsetting this PR 😞

If you are interested in taking a solution like this to the finish line, some work has been done to being able to provide a custom DataID value at runtime. However, unless your DataID would rely on id and __typename, which are already being queried for by default by relay, you would have to do some work to provide your own query transform that, at compile time, inserts the fields you need selected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants