1

I am using graphql-tools@v6 and I have implemented two directives @map and @filter. My goal is to use them like a map and filter pipeline. In some cases, I want to map before filtering and in other cases, vice-versa. The directives are implemented using the Schema Directives API and they work as expected when only one directive is applied.

However, if I use them together, then they always execute in one specific order which doesn't match how they are declared in the schema.

For example

directive @map on FIELD_DEFINITION
directive @filter on FIELD_DEFINITION

# usage
type MyType {
    list1: [String!]! @map @filter
    list2: [String!]! @filter @map
}

In this case, either both fields are mapped and then filtered or vice-versa. The order is controlled by how I pass them in schemaTransforms property.

const schema = makeExecutableSchema({
  schemaTransforms: [mapDirective, filterDirective] # vs [filterDirective, mapDirective]
});

I believe since these transforms are passed as an array, so their order of execution depends on the ordering of array. I can replace them with directiveResolvers but they are limited in what they can do.

But what throws me off is the following statement from the documentation

Existing code that uses directiveResolvers could consider migrating to direct usage of mapSchema

Because they have different behavior when it comes to order of execution, I don't see how they are interchangeable.

Can someone explain if there is a way to guarantee that the Schema Directives execute in the order they are used in the schema for a particular field?

drcocoa
  • 1,155
  • 1
  • 14
  • 23

1 Answers1

0

Please see this github issue for in depth discussion.

The new API doesn't work the same way as directiveResolvers or schemaDirectives. A schemaTransform is applied to the entire schema before the next one contrary to the other two in which the all the transforms are applied to a particular field before visiting the next field node. There are two approaches to this in my opinion:

  1. Create a new @pipeline directive which takes a list of names of other directives and then applies them in the order like directiveResolvers.

  2. I took a bit different route where I created a new function attachSchemaTransforms just like attachDirectiveResolvers which visits each node and applies all the directives in order.

export function attachSchemaTransforms(
  schema: GraphQLSchema,
  schemaTransforms: Record<string, FieldDirectiveConfig>, // a custom config object which contains the transform and the directive name
): GraphQLSchema {
  if (typeof schemaTransforms !== 'object') {
    throw new Error(`Expected schemaTransforms to be of type object, got ${typeof schemaTransforms}`);
  }

  if (Array.isArray(schemaTransforms)) {
    throw new Error('Expected schemaTransforms to be of type object, got Array');
  }

  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: oldFieldConfig => {
      const fieldConfig = { ...oldFieldConfig };

      const directives = getDirectives(schema, fieldConfig);

      Object.keys(directives).forEach(directiveName => {
        const config = schemaTransforms[directiveName];
        if (config) {
          const { apply, name } = config;
          const directives = getDirectives(schema, fieldConfig);
          if (directives[name]) {
            const directiveArgs: unknown = directives[name]
            apply(fieldConfig, directiveArgs);
            return fieldConfig;
          }
        }
      });

      return fieldConfig;
    },
  });
}
drcocoa
  • 1,155
  • 1
  • 14
  • 23