Demo

Join Monster works well with graphql-relay-js. There is built-in support for Global IDs and the Node Interface. Check out the Relay-compliant version of the demo. Source code can be found here.

Global ID

These are Relay's unique identifiers used as cache keys. Implementing is relatively straight-forward. We can import the globalIdField helper and provide the id which we'll take from a column and voila.

import { globalIdField } from 'graphql-relay'

const User = new GraphQLObjectType({
  name: 'User',
  sqlTable: 'accounts',
  uniqueKey: 'id',
  fields: () => ({
    globalId: {
      description: 'The global ID for the Relay spec',
      ...globalIdField(),
      extensions: {
        joinMonster: {
          sqlDeps: ['id']
        }
      }
    }
    //...
  })
})

Node Interface

The Node Interface allows Relay to fetch an instance of any type. This could technically be useful without using Relay on the client. Join Monster provides a helper for easily fetching data in order to implement Relay's Node Interface. This is the getNode method.

import joinMonster from 'join-monster'
import {
  nodeDefinitions,
  fromGlobalId
} from 'graphql-relay'

const { nodeInterface, nodeField } = nodeDefinitions(
  // resolve the ID to an object
  (globalId, context, resolveInfo) => {
    // parse the globalID
    const { type, id } = fromGlobalId(globalId)

    // pass the type name and other info. `joinMonster` will find the type from the name and write the SQL
    return joinMonster.getNode(type, resolveInfo, context, parseInt(id),
      sql => knex.raw(sql)
    )
  },
  // determines the type. Join Monster places that type onto the result object on the "__type__" property
  obj => obj.__type__
)

const Query = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    // expose the Node type on the root-level
    node: nodeField,
    users: {...}
  })
})

The getNode method needs the type name, resolve info, a context object, the value of the primaryKey, and a function the receives the SQL and queries the database. If the primaryKey is composite, an array is needed for the fourth argument. See API for details

The Node interface also needs to resolve its type, which join monster figures out for you. It places the type on the "__type__" property of the resolved data. When you write the resolveType function, the second argument for nodeDefinitions, you can simply return the object on the "__type__" property.

Your global ID may not be the same as the uniqueKey. Or you might have more complex logic for retrieving the node from the global ID. For these cases you can pass a where function as your fourth argument instead of a value directly. This function generates the WHERE condition dynamically. Be sure to escape untrusted user input.

const { nodeInterface, nodeField } = nodeDefinitions(
  // resolve the ID to an object
  (globalId, context, resolveInfo) => {
    // parse the globalID
    const { type, id } = fromGlobalId(globalId)

    // pass a function to generate the WHERE condition, instead of simply passing a value
    return joinMonster.getNode(type, resolveInfo, context,
      table => `${table}.id = ${id}`,
      sql => knex.raw(sql)
    )
  },
  // determines the type. Join Monster places that type onto the result object on the "__type__" property
  obj => obj.__type__
)