New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Relay Global Object Identification support. #313
Conversation
Fetch Artwork Global ID{
artwork(id: "mr-brainwash-charlie-chaplin-red") {
__id
}
} {
"data": {
"artwork": {
"__id": "QXJ0d29yazptci1icmFpbndhc2gtY2hhcmxpZS1jaGFwbGluLXJlZA=="
}
}
} Fetch Artwork through Node interface{
node(__id: "QXJ0d29yazptci1icmFpbndhc2gtY2hhcmxpZS1jaGFwbGluLXJlZA==") {
__typename
... on Artwork {
__id
_id
id
title
}
}
} {
"data": {
"node": {
"__typename": "Artwork",
"__id": "QXJ0d29yazptci1icmFpbndhc2gtY2hhcmxpZS1jaGFwbGluLXJlZA==",
"_id": "575a0dcfcd530e4f0d00022e",
"id": "mr-brainwash-charlie-chaplin-red",
"title": "Charlie Chaplin (Red)"
}
}
} |
The ID is a base64 encoded combination of the type and its real ID, so that it can be deconstructed and resolved regardless of type.
|
Decided to try to get Relay to use a differently named ID field so we don’t have to update all clients, especially not Eigen, which uses it in 1 place for analytics facebook/relay#1061 (comment) |
Well, seems like trying to change that requirement in upstream Relay might take longer than I want, so for now I’ll just continue as-is, but if there’s a hint of things changing in Relay before this PR is finalise I’ll switch it over. |
@ashfurrow I see you’re using the |
If it turns out that there’s more already deployed iOS code that relies on (Although in the case I linked to in the previous comment the model is a |
@@ -54,8 +54,8 @@ describe('ArtistCarousel type', () => { | |||
const gravity = ArtistCarousel.__get__('gravity'); | |||
const query = ` | |||
{ | |||
artist(id: "foo-bar") { | |||
id | |||
artist(slug_or_id: "foo-bar") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on the name of this parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't just call it id
and it do the right thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:reads above:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gid
? key
?
We use the Please advise on next steps – it's possible to disable the native live experience and fall back to a mobile web view (indeed, we're planning on doing so already). If this is an infrastructure change that eigen needs to make, we can roll it into our existing update. |
@@ -1,5 +1,6 @@ | |||
{ | |||
"presets": ["es2015"], | |||
"plugins": ["transform-object-rest-spread"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank youuu
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh you had wanted this? I reverted it, because it seems like I won’t need it after all :/
Let me know if anyone wants this in anyways and I’ll re-apply the patches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, re-adding it after all.
Had to use an older version because we use an older version of the graphql package.
04427a8
to
3232b69
Compare
Ok, was using |
Paging @broskoski, now that @dzucconi has officially left. |
Oops, forgot that @broskoski is out for the week, paging @mzikherman, sir. |
Noticed I still need to add support to to Author and Partner. |
* https://facebook.github.io/relay/graphql/objectidentification.htm * We use a custom ID, so we vendor some code from graphql-relay. * facebook/relay#1232 * facebook/relay#1246
This makes the test suite green, otherwise it would throw errors like: Error: Schema must contain unique named types but contains multiple types named "BidIncrements".
const SupportedTypes = { | ||
types: ['Article', 'Artist', 'Artwork', 'Partner', 'PartnerShow'], | ||
// To prevent circular dependencies, when this file is loaded, the modules are lazily loaded. | ||
typeModule: _.memoize(type => require(`./${_.snakeCase(type)}`).default), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice makes sense
I changed nothing with regards to the NPM modules in the last commit, yet CI now fails on that 😭 |
I’m not even sure why it tries to install fsevents 1.0.5, the shrinkwrap file has it pegged to 1.0.12 |
If I ssh into CI and run |
Noo :( sorry for making you go back in for minor-ish changes |
Making the fsevents 1.0.12 dependency explicit does change something, but now it hits the following:
Like… I dunno… it’s trying to rename a path to the same path? What’s up with that? I give up, this seems very much a CI issue. I have no issues locally and also not when SSH-ing into CI. I’m going to revert the last commit and call it a day.
Nah, that change should be totally irrelevant. I would say, if you feel good about the code, go ahead and merge it and give CI some time to chill, if that’s something y’all do in some cases. |
I'll try to rebuild it tonight (explore Semaphore's dependency cache- maybe can be force updated?), and if not I'll merge and babysit the staging deploy. Either way you'll have it by (your) tmrw morning :) Thanks for the awesome PR. |
My naive attempt was based on this: https://semaphoreci.com/docs/caching-between-builds.html, but the
💃🙌 |
I expired the cache and seems to have worked! https://semaphoreci.com/artsy-it/metaphysics/settings/admin (need to be logged in as it@) |
Yay!! |
What do you guys think of using UUIDv5 as (Oh didn't mean to assign people, sorry.) |
@ermik It wouldn’t solve our issue, which is that the |
@alloy sounds interesting. It's almost like a case of a funky collision turned into an interesting pattern. Thanks for the insight. I'm trying to grasp the ontology of your system to advance my code — learning from mistakes (and smarts) of others. |
How do I use the object identification mechanism you have in this project on stitched schemas? For example, I have two separate services both supporting relay and exposing Node root field, how do I stitch them together and resolve the node query in a metaphysics-like app? I'd really appreciate any guidance or tips on this. Thank you! ❤️ |
@ermik We have not been doing this yet. My idea was that the different services would encode the IDs themselves but prepend the IDs with their service name. Then the stitched schema could have a resolver that delegates to the responsible backend service by just matching the start of the IDs with base64 encoded versions of the services names (be sure to take into account how base64 encoding works, an example can be found here). |
So, since I will need more answers from you guys 😉, I hope this will be interesting for you to see. I actually didn't get your response until just now and hacked it together the best i could. First off, an I might PR this in the future, but I found that this repo is, as far as I understand is missing something like the following: /**
* update-master-schema.js
* Grabs the stitched schema to allow Relay compiler and other tools
* dependent on schema definition files to work.
*/
import fs from "fs";
import { printSchema } from "graphql/utilities";
import path from "path";
import { mergeSchemas } from "../server/graphql/lib/stitching/mergedSchemas";
let schema;
async function magic() {
schema = await mergeSchemas();
fs.writeFileSync(
path.join(__dirname, "../server/graphql/master.graphql"),
printSchema(schema)
);
}
magic(); Then, mangling the beauty that is your code: /*
* megedSchemas.js
* Prepares the edge layer to resolve graphql requests to the internal APIs by
* using schema merge, delegation, and transformation tooling
*/
/* ... */
const mergedSchema = _mergeSchemas({
schemas: [
lloydSchema,
localSchema
/**/
],
resolvers: {
Query: {
// delegating directly, no subschemas or mergeInfo
node: (parent, args, context, info) => {
return info.mergeInfo.delegateToSchema({
schema: lloydSchema,
operation: "query",
fieldName: "node",
args: {
id: args.id
},
context,
info
});
}
}
}
/* ... */ Which works for a single service, but must not be hard (I assume) to make it work for multiple via the same mapping mechanism you use for loaders for example. |
Indeed we’re not persisting the stitched schema, we’ve started using tooling such as graphql-fetch-schema to fetch when the client needs it. Thanks for offering to PR, though! As for the delegation, my thought was to do something like: node: (parent, args, context, info) => {
let schema
// Encode schema name prefix in such a way that it takes into account Base64 encoding boundaries.
if (args.id.startsWith(base64("lloyd"))) {
schema = lloydSchema
}
return info.mergeInfo.delegateToSchema({
schema,
operation: "query",
fieldName: "node",
args: {
id: args.id
},
context,
info
});
} |
There is a need for a contract that adheres to the graphql/relay spec. The Based on these considerations I'd keep the decision of picking a schema it in a controlled format of a static map: /** @flow */
import { fromGlobalId } from "graphql-relay";
import type { GraphQLSchema } from "graphql";
type ObjectType = string;
type Service = string;
type NodeMap = { [ObjectType]: Service };
const nodeResolvers: NodeMap = {
["Vendor"]: "lloyd",
["ElasticSearch"]: "local"
};
/**
* @func schemaFromGlobalId retrieves delegate schema for a Relay Global ID
* @param {string} id — Relay standard base64 encoded tuple of shape "type:id"
* @desc instead of base64() cost if encoding servicename as part of type
* we will pay expected base64() cost associated with Relay services; it will
* also be paid at the destination, but that is also expected.
*/
const schemaFromGlobalId = (id: string): GraphQLSchema | void => {
try {
// fromGlobalId is unsafe, and generally this is flaky
switch (nodeResolvers[fromGlobalId(id).type]) {
case "lloyd":
return lloydSchema;
case "local":
return localSchema;
default:
throw new Error("UnknownService");
}
} catch (e) {
// sentry, etc.
throw IdentificationFailureError(); // catch and recover thread elsewhere
}
};
/* ... */
node: (parent, args, context, info) => {
const schema = schemaFromGlobalId(args.id);
return info.mergeInfo.delegateToSchema({
schema,
operation: "query",
fieldName: "node",
args: {
id: args.id
},
context,
info
});
} Introducing additional base64 data into an already clunky (imho) system of Another issue with encoding the service name as part of the
|
For Relay, the only requirement is that a Node ID is globally unique and that the GraphQL server can resolve back to a unique entity from it. A strong suggestion is to make this ID opaque to the client. One way to make the ID opaque but encode the required data, as is used by the |
This adds the Relay Node ID support that we need to make full use of Relay’s capabilities.
Node
interface, which identifies an object with the__id
field. From the__id
value it is able to deduce the actual type and its type-specific ID.Node
interface. Any other types that will be used with Relay in the future will need to get support added.id
field now also have a__id
field, because even though you can’t look all of those up through theNode
interface yet, the globally unique IDs does mean that Relay can already cache them efficiently.GraphQLID
for allid
fields instead ofGraphQLString
. I did not yet add the non-null type to all of those, but I have a feeling that there are lots of places where a value (such as an ID) cannot be null.