Search code examples
postgresqlgraphqlapolloapollo-servertypeorm

How to implement a node query resolver with apollo / graphql


I am working on implementing a node interface for graphql -- a pretty standard design pattern.

Looking for guidance on the best way to implement a node query resolver for graphql

node(id ID!): Node

The main thing that I am struggling with is how to encode/decode the ID the typename so that we can find the right table/collection to query from.

Currently I am using postgreSQL uuid strategy with pgcrytpo to generate ids.

Where is the right seam in the application to do this?:

  1. could be done in the primary key generation at the database
  2. could be done at the graphql seam (using a visitor pattern maybe)

And once the best seam is picked:

  1. how/where do you encode/decode?

Note my stack is:

  • ApolloClient/Server (from graphql-yoga)
  • node
  • TypeORM
  • PostgreSQL

Solution

  • The id exposed to the client (the global object id) is not persisted on the backend -- the encoding and decoding should be done by the GraphQL server itself. Here's a rough example based on how relay does it:

    import Foo from '../../models/Foo'
    
    function encode (id, __typename) {
      return Buffer.from(`${id}:${__typename}`, 'utf8').toString('base64');
    }
    
    function decode (objectId) {
      const decoded = Buffer.from(objectId, 'base64').toString('utf8')
      const parts = decoded.split(':')
      return {
        id: parts[0],
        __typename: parts[1],
      }
    }
    
    const typeDefs = `
      type Query {
        node(id: ID!): Node
      }
      type Foo implements Node {
        id: ID!
        foo: String
      }
      interface Node {
        id: ID!
      }
    `;
    
    // Just in case model name and typename do not always match
    const modelsByTypename = {
      Foo,
    }
    
    const resolvers = {
      Query: {
        node: async (root, args, context) => {
          const { __typename, id } = decode(args.id)
          const Model = modelsByTypename[__typename]
          const node = await Model.getById(id)
          return {
            ...node,
            __typename,
          };
        },
      },
      Foo: {
        id: (obj) => encode(obj.id, 'Foo')
      }
    };
    

    Note: by returning the __typename, we're letting GraphQL's default resolveType behavior figure out which type the interface is returning, so there's no need to provide a resolver for __resolveType.

    Edit: to apply the id logic to multiple types:

    function addIDResolvers (resolvers, types) {
      for (const type of types) {
        if (!resolvers[type]) {
          resolvers[type] = {}
        }
        resolvers[type].id = (obj) => encode(obj.id, type)
      }
    }
    
    addIDResolvers(resolvers, ['Foo', 'Bar', 'Qux'])