Skip to content
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

The Node interface and globally unique IDs #112

Open
zth opened this issue Jun 12, 2020 · 1 comment
Open

The Node interface and globally unique IDs #112

zth opened this issue Jun 12, 2020 · 1 comment
Labels
enhancement New feature or request

Comments

@zth
Copy link

zth commented Jun 12, 2020

Hi!

I'm interested in exploring and discussing what it'd take to implement the Node interface (and with that globally unique IDs) in Sqlmancer. This + connection based pagination would ultimately mean that Sqlmancer will be compatible with Relay, which would be a very nice thing 😀

Below are some initial thoughts from me (https://graphql.org/learn/global-object-identification/ is recommended reading if the reader is unfamiliar with globally unique IDs and the Node interface in GraphQL):

The Node interface

For illustrative purposes, let's say we're implementing the Node interface as a directive @nodeInterfaceyou can use on OBJECT. This is probably a bad idea (a better idea is likely some form of global setting to enable "Relay mode"), but let's use it as a way of exploring implementing the Node interface and globally unique IDs in Sqlmancer:

directive @nodeInterface on OBJECT

type Widget @model(...) @nodeInterface {
  id: ID! @hasDefault
  ...
}

Now, id defined in the model will be a database ID, which probably won't be globally unique, or contain enough information to decipher what type it's representing (which the Node interface needs). However, my thinking is that the @nodeInterface directive could do something like this;

type Widget @model(...) @nodeInterface {
  dbId: ID! # This is the old `id` field, just moved to this key so it's still accessible
  id: ID! # This is changed to be a globally unique ID with the type information needed by the Node interface
  ...
}

The id field that's changed could then be implemented to just change the resolution of itself to something Node-interface friendly:

// Manipulating the GraphQLObjectType with the `@nodeInterface` directive defined on it
id: {
  type: new GraphQLNonNull(GraphQLID),
  resolve(obj) {
    // obj.id below is the _old_, real database id at this point
    // base64.encode isn't really needed, but it's to show the clients that they shouldn't rely on the contents of the ID
    // This code below will allow us to take any `id`, decode it, and immediately know what type we need to fetch using what id. More on that below
    return base64.encode(`${typeNameFromGraphQLObject}:${obj.id}`);
  }
}

This could then be re-used in the node field on Query:

// Extending the root Query type
fields: () => ({ 
  ...rootQueryFields,
  node: {
    type: nodeInterfaceType,
   args: {...} // id: ID!
   resolve(ctx, args) {
     const [typename, id] = base64.decode(args.id).split(":");

    if (!typename || !id) {
      throw new Error("Malformed ID");
    }

   // Here we have a typename of a GraphQL type we want to resolve, and the id we want to use
   // Just need to resolve it. Made up pseudo-code below
   return ctx.models[typename].findOne({ id });
   }
  }
})

I believe this would accomplish what's needed for the Node interface and globally unique IDs.

Here's a few issues and things I've thought about that needs consideration:

  • id in input positions - if the IDs are now globally unique, they'll need decoding before being used in SQL etc.
  • id in relations and similar things - same as above, will need decoding.

This was a quick write up of my thoughts, I hope it's not terrible to read. What are your initial thoughts about something like this? Do you see any immediate blockers or issues?

I'm btw very willing to spend time implementing this if there's interest for it!

@danielrearden
Copy link
Owner

@zth Thanks for the above input. I think your suggested approach is very reasonable and wouldn't be too difficult to implement. If we were to use a directive, I think something like the @globalId used by Lighthouse would be good

"""
Converts between IDs/types and global IDs.
When used upon a field, it encodes,
when used upon an argument, it decodes.
"""
directive @globalId(
  """
  By default, an array of `[$type, $id]` is returned when decoding.
  You may limit this to returning just one of both.
  Allowed values: "ARRAY", "TYPE", "ID"
  """
  decode: String = "ARRAY"
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION

Customizing the id via the decode argument here is a bit overkill for me. However, I like the idea of reusing the same directive for encoding and decoding.

That said, I also kind of like your idea of having a global setting for this. That would make for better DX, and might also simplify the implementation a bit. I would imagine wanting to enable global ids for only some of the types in the schema would be quite an edge case.

@danielrearden danielrearden added the enhancement New feature or request label Jun 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants