Apollo-server: Proposal: Query directives

Created on 1 Aug 2019  路  4Comments  路  Source: apollographql/apollo-server

I'm aware that there may have been good reasons for not supporting query directives when schema directive support was rolled out. However, I think query directives could be a powerful tool and provide clients additional flexibility. They may also provide solutions for some more common problems. For example, @include and @skip are clunky to use for query building where any combination of fields might be needed -- a @selection directive that lets the client specify an array of field names to include would be more client-friendly. I imagine query directives would allow suitable workarounds for issues like this one too.

Here's an example implementation using a plugin that mirrors the SchemaDirectiveVisitor:

const { ApolloServer, makeExecutableSchema } = require("apollo-server")
const { Kind } = require('graphql')
const { getArgumentValues } = require('graphql/execution/values')

// Simple example schema
const typeDefs = `
  directive @selection(set: [String!]!) on FIELD

  type Query {
    foo(a: Int): Foo
  }

  type Foo {
    a: String
    b: String
    c: String
  }
`
const resolvers = {
  Query: {
    foo: () => ({ a: 'A', b: 'B', c: 'C' }),
  },
}
const schema = makeExecutableSchema({ typeDefs, resolvers })

class QueryDirectiveVisitor {
  constructor({ args, schema }) {
    this.args = args
    this.schema = schema
  }

  visitFragmentDefinition(fragmentDefinition) { }
  visitQuery(operation) { }
  visitMutation(operation) { }
  visitSubscription(operation) { }
  visitVariableDefinition(variableDefinition) { }
  visitField(field) { }
  visitFragmentSpread(fragmentSpread) { }
  visitInlineFragment(inlineFragmentNode) { }
}

const queryDirectives = {
  selection: class SelectionDirective extends QueryDirectiveVisitor {
    visitField(field) {
      field.selectionSet = {
        ...field.selectionSet,
        selections: field.selectionSet.selections.filter(selection => {
          return selection.kind !== Kind.FIELD || this.args.set.includes(selection.name.value)
        })
      }
    }
  }
}

const applyDirective = (node, directive, variables) => {
  const name = directive.name.value
  const ClientDirective = queryDirectives[name]
  if (ClientDirective) {
    const directiveDef = schema.getDirective(name)
    const args = getArgumentValues(directiveDef, directive, variables)
    const clientDirective = new ClientDirective({ args, schema })
    const visitorTarget = node.kind === Kind.OPERATION_DEFINITION
      ? node.operation.charAt(0).toUpperCase() + node.operation.slice(1)
      : node.kind
    clientDirective[`visit${visitorTarget}`](node)
  }
}
const walkAST = (ast, variables) => {
  if (!ast.selectionSet) {
    return
  }

  ast.selectionSet.selections.forEach((selectionNode) => {
    selectionNode.directives.forEach(directive => applyDirective(selectionNode, directive, variables))
    walkAST(selectionNode, variables)
  })
}

const server = new ApolloServer({
  schema,
  plugins: [
    {
      requestDidStart: () => ({
        didResolveOperation({ operation, request: { variables }, document }) {
          // Apply fragment definition directives
          document.definitions.forEach(definition => {
            if (definition.kind === Kind.FRAGMENT_DEFINITION) {
              definition.directives.forEach(directive => applyDirective(definition, directive, variables))
            }
          })

          // Apply operation definition directives
          operation.directives.forEach(directive => applyDirective(operation, directive, variables))

          // Apply variable definition directives
          operation.variableDefinitions.forEach(variableDef => {
            variableDef.directives.forEach(directive => applyDirective(variableDef, directive, variables))
          })

          // Walk the operation AST and apply any remaining directives
          walkAST(operation, variables)
        },
      }),
    }
  ]
})

I'm considering publishing a plugin along these lines, but I also think this might be a good fit for the base library. @martijnwalraven @abernix has there been any more conversation around adding this feature? Would you entertain a PR to this effect?

Most helpful comment

@bennypowers This has been open for 10 months with no response from the maintainers. Like I mentioned above, the functionality can be implemented via a plugin.

All 4 comments

@danielrearden I'm curious why you closed this? the use case is still compelling

@bennypowers This has been open for 10 months with no response from the maintainers. Like I mentioned above, the functionality can be implemented via a plugin.

Did you publish said plugin 馃槈 ?

Not yet. The overall concept needs to be fleshed out more, with all common use cases covered. I still want to circle back to this, but I'm probably going to first focus on somehow adding that functionality to express-graphql

Was this page helpful?
0 / 5 - 0 ratings