The Where Function

We of course don't want every single user in the database. We need a way to place a WHERE clause in the query.

You can define a where function on a field that generates the WHERE condition. Its parameters are the table alias (generated automatically by joinMonster), the GraphQL arguments on that field, the "context" mentioned earlier, and the field's "SQL AST node". The (Promise of) string returned is the WHERE condition. If a falsy value is returned, there will be no WHERE condition. We'll add another top-level field that just returns one user.

const QueryRoot = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    users: { /*...*/ },
    user: {
      type: User,
      // allow them to search for a specific  user
      args: {
        id: { type: new GraphQLNonNull(GraphQLInt) }
      },
      extensions: {
        joinMonster: {
          where: (usersTable, args, context) => {
            return `${usersTable}.id = ${args.id}`
          }
        }
      },
      resolve: (parent, args, context, resolveInfo) => {
        return joinMonster(resolveInfo, {}, sql => {
          return knex.raw(sql)
        })
      }
    }
  })
})

Now you can handle queries like this, which return a single user.

{
  user(id: 1) {
    id
    email
    fullName
  }
}

This where function directly interpolates user input into its clause. This if fine for integers, as the GraphQL validation will prevent malicious input. However, for strings, this is not recommended in production due to SQL injection risk. Instead, you should escape the input yourself or use an established library like sqlstring, pg-format, or pg-escape.

import escape from 'pg-escape'

const QueryRoot = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    user: {
      type: User,
      args: {
        lastName: GraphQLString
      },
      extensions: {
        joinMonster: {
          where: (usersTable, args, context) => {
            return escape(`${usersTable}.last_name = %L`, args.lastName)
          }
        }
      }
      // ...
    }
  })
})

Adding Context

Most often, we'll be asking for the logged-in user. The joinMonster function has a third parameter which is basically an arbitrary object with useful contextual information that your where functions might depend on. For example, you can pass in the ID of the logged in user to incorporate it into the WHERE condition.

{
  //...
  // there is a GraphQL context and a Join Monster context. these are separate!
  resolve: (parent, args, context, resolveInfo) => {
    // get some info off the HTTP request, like the cookie.
    const loggedInUserId = getHeaderAndParseCookie(context)
    return joinMonster(resolveInfo, { id: loggedInUserId }, sql => {
      return knex.raw(sql)
    })
  },
  extensions: {
    joinMonster: {
      where: (usersTable, args, context) => {
        return `${usersTable}.id = ${context.id}`
      }
    }
  }
}

Again, don't forget to double-quote case sensitive column names.

See API for more details on this callback.