Schema Plugins - Graphile Engine

The PostGraphile GraphQL schema is constructed out of a number of Graphile Engine plugins. The core PG-related plugins can be found here:

https://github.com/graphile/graphile-engine/tree/master/packages/graphile-build-pg/src/plugins

These plugins introduce small amounts of functionality, and build upon each other. The order in which the plugins are loaded is significant, and can be found from the defaultPlugins export in src/index.ts of the graphile-build-pg module.

You can extend PostGraphile's GraphQL schema by adding plugins before or after the default plugins. You may even opt to replace the entire list of plugins used to build the schema. Graphile Engine plugins are built on top of the GraphQL reference JS implementation, so it is recommended that you have familiarity with that before attempting to write your own plugins.

Adding root query/mutation fields

A common request is to add additional root-level fields to your schema, for example to integrate external services. The easiest way to do this is to use makeExtendSchemaPlugin to generate a plugin that will extend your schema (this can be used to add fields anywhere, not just at the root-level):

// add-http-bin-plugin.js
const { makeExtendSchemaPlugin, gql } = require("graphile-utils");
const fetch = require("node-fetch");

module.exports = makeExtendSchemaPlugin({
  typeDefs: gql`
    extend type Query {
      httpBinHeaders: JSON
    }
  `,
  resolvers: {
    Query: {
      async httpBinHeaders() {
        const response = await fetch("https://httpbin.org/headers");
        return response.json();
      },
    },
  },
});

If you need to do this using the low-level plugins API for some reason (for example you want access to the look-ahead features, or you're defining the fields in a more automated way) then you can use a 'GraphQLObjectType:fields' hook and to add our new field:

// add-http-bin-plugin-raw.js
const fetch = require("node-fetch");

function AddHttpBinPlugin(builder, { pgExtendedTypes }) {
  builder.hook(
    "GraphQLObjectType:fields",
    (
      fields, // Input object - the fields for this GraphQLObjectType
      { extend, getTypeByName }, // Build object - handy utils
      { scope: { isRootQuery } } // Context object - used for filtering
    ) => {
      if (!isRootQuery) {
        // This isn't the object we want to modify:
        // return the input object unmodified
        return fields;
      }

      // We don't want to introduce a new JSON type as that will clash,
      // so let's find the JSON type that other fields use:
      const JSONType = getTypeByName("JSON");

      return extend(fields, {
        httpBinHeaders: {
          type: JSONType,
          async resolve() {
            const response = await fetch("https://httpbin.org/headers");
            if (pgExtendedTypes) {
              // This setting is enabled through postgraphile's
              // `--dynamic-json` option, if enabled return JSON:
              return response.json();
            } else {
              // If Dynamic JSON is not enabled, we want a JSON string instead
              return response.text();
            }
          },
        },
      });
    }
  );
}

module.exports = AddHttpBinPlugin;

(If you wanted to add a mutation you'd use isRootMutation rather than isRootQuery.)

We can then load our plugin into PostGraphile via:

postgraphile --append-plugins `pwd`/add-http-bin-plugin.js -c postgres:///mydb

Note that the types of added fields do not need to be implemented via Graphile Engine's newWithHooks - you can use standard GraphQL objects too, as we have demonstrated with the JSONType above. (However, if you do not use newWithHooks then the objects referenced cannot be extended via plugins.)

Wrapping an existing resolver

Sometimes you might want to override what an existing field does. Due to the way that PostGraphile works (where the root Query field resolvers are the only ones who perform SQL queries) this is generally most useful at the top level.

In PostGraphile version 4.1 and above, you can use makeWrapResolversPlugin to easily wrap a resolver:

module.exports = makeWrapResolversPlugin({
  User: {
    async email(resolve, source, args, context, resolveInfo) {
      const result = await resolve();
      return result.toLowerCase();
    },
  },
});

If you need to process the resolvers in a more powerful way than possible via makeWrapResolversPlugin, then you can drop down to the raw plugin API. The following example modifies the 'createLink' mutation so that it performs some additional validation (thrown an error if the link's title is too short) and performs an action after the link has been saved. You could use a plugin like this to achieve many different tasks, including emailing a user after their account is created or logging failed authentication attempts.

Previously we used GraphQLObjectType:fields to add a field, as that manipulates the list of fields. This time we are manipulating an individual field, so we will use the GraphQLObjectType:fields:field hook. This makes our intent clear, and also grants us access to the addArgDataGenerator function which we need to request the record id. The following example also uses an instance of queryBuilder. (Read more about the different hooks in the Graphile Engine docs.)

function performAnotherTask(linkId) {
  console.log(`We created link ${linkId}!`);
}

module.exports = function CreateLinkWrapPlugin(builder) {
  builder.hook(
    "GraphQLObjectType:fields:field",
    (
      field,
      { pgSql: sql },
      { scope: { isRootMutation, fieldName }, addArgDataGenerator }
    ) => {
      if (!isRootMutation || fieldName !== "createLink") {
        // The 'GraphQLObjectType:fields:field' hook runs for every field on
        // every object type in the schema. If it's not a field in the root
        // mutation type, or the field isn't named 'createLink', we don't want
        // to modify it in this hook - so return the input object unmodified.
        return field;
      }

      // We're going to need link.id for our `performAnotherTask`; so we're going
      // to abuse addArgDataGenerator to make sure that this field is ALWAYS
      // requested, even if the user doesn't specify it. We're careful to alias
      // the result to a field that begins with `__` as that's forbidden by
      // GraphQL and thus cannot clash with a user's fields.
      addArgDataGenerator(() => ({
        pgQuery: queryBuilder => {
          queryBuilder.select(
            // Select this value from the result of the INSERT:
            sql.query`${queryBuilder.getTableAlias()}.id`,
            // And give it this name in the result data:
            "__createdRecordId"
          );
        },
      }));

      // It's possible that `resolve` isn't specified on a field, so in that case
      // we fall back to a default resolver.
      const defaultResolver = obj => obj[fieldName];

      // Extract the old resolver from `field`
      const { resolve: oldResolve = defaultResolver, ...rest } = field;

      return {
        // Copy over everything except 'resolve'
        ...rest,

        // Add our new resolver which wraps the old resolver
        async resolve(...resolveParams) {
          // Perform some validation (or any other action you want to do before
          // calling the old resolver)
          const RESOLVE_ARGS_INDEX = 1;
          const {
            input: {
              link: { title },
            },
          } = resolveParams[RESOLVE_ARGS_INDEX];
          if (title.length < 3) {
            throw new Error("Title is too short!");
          }

          // Call the old resolver (you SHOULD NOT modify the arguments it
          // receives unless you also manipulate the AST it gets passed as the
          // 4th argument; which is quite a lot of effort) and store the result.
          const oldResolveResult = await oldResolve(...resolveParams);

          // Perform any tasks we want to do after the record is created.
          await performAnotherTask(oldResolveResult.data.__createdRecordId);

          // Finally return the result.
          return oldResolveResult;
        },
      };
    }
  );
};

Removing things from the schema

WARNING: removing things from your GraphQL schema this way may have unintended consequences - especially if you add back a field or type with the same name as that which you removed. It's advised that rather than removing things, you instead avoid them being generated in the first place.

If you're looking for an easy way to prevent certain tables, fields, functions or relations being added to your GraphQL schema, check out smart comments.

If you want to remove a class of things from the schema then you can remove the plugin that adds them; for example if you no longer wanted to allow ordering by all the columns of a table (i.e. only allow ordering by the primary key) you could omit PgOrderAllColumnsPlugin. If you didn't want computed columns added you could omit PgComputedColumnsPlugin.

However, sometimes you need more surgical precision, and you only want to remove one specific type of thing. To achieve this you need to add a hook to the thing that owns the thing you wish to remove - for example if you want to remove a field bar from an object type Foo you could hook GraphQLObjectType:fields and return the set of fields less the one you want removed.

Here's an example of a plugin generator you could use to generate plugins to remove individual fields. This is just to demonstrate how a plugin to do this might work, smart comments are likely a better approach.

const omit = require("lodash/omit");

function removeFieldPluginGenerator(objectName, fieldName) {
  const fn = function(builder) {
    builder.hook("GraphQLObjectType:fields", (fields, _, { Self }) => {
      if (Self.name !== objectName) return fields;
      return omit(fields, [fieldName]);
    });
  };
  // For debugging:
  fn.displayName = `RemoveFieldPlugin:${objectName}.${fieldName}`;
  return fn;
}

const RemoveFooDotBarPlugin = removeFieldPluginGenerator("Foo", "bar");

module.exports = RemoveFooDotBarPlugin;