Skip to content
KeystoneJS LogoKeystoneJSv5

Custom Mutations

Out of the box Keystone provides predictable CRUD operations (Create, Read, Update and Delete) for Lists. The generated graphQL queries and mutations are the primary method for updating data in a List. These should be enough for most application requirements.

Custom Queries and Mutations may be required if you wish to preform non-CRUD operations or actions that don't relate to a specific List.

See the GraphQL Philosophy for more information on how Keystone implements CRUD operations in GraphQL and when Custom Queries and Mutations may be required.

You can add to Keystone's generated schema with custom types, queries, and mutations using the keystone.extendGraphQLSchema() method.

Creating an Custom Mutation

A common example where a custom mutation might be beneficial is if you want to increment a value.

Like any problem there are multiple solutions. You can implement an incrementing value with Hooks but in this example we're going to look at how to do this with a custom mutation.

First let's define a Page list. For the sake of simplicity, we'll give it only two fields: title and views.

const { Text, Integer } = require('@keystonejs/fields');
const Page = {
  fields: {
    title: { type: Text },
    views: { type: Integer },
  },
};

If we had a front-end application that updated the view count everytime someone visits the page it would require multiple CRUD operations.

Using CRUD we'd first have to fetch the current view count:

query Page($id: ID!){
  Page(where:{id: $id}) {
    views
  }
}

Once we have the view count we could increment this value with JavaScript and send an updatePage mutation to save the new value:

mutation updatePageViews($id: ID!, $views: Int!) {
  updatePage(id: $id, data: { views: $views }) {
    views
  }
}

The problem with this approach is the client can update the views with any arbitrary value. And even if there is no deliberate manipulation of the value, the GraphQL server is trusting that the update mutation is received from the same client, immediately after the view query. On heavily trafficked sites this will not always be the case.

Read and Update requests from multiple clients can be received in any order depending on the speed of the their internet connections. This means updates can override each other. One solution to this problem is a custom mutation.

You can add to Keystone's generated schema using keystone.extendGraphQLSchema(). This method accepts an array of types, queries and mutations. For our example we are going to add a mutation array with a single item.

Each item in the mutation array requires a schema and a resolver.

The Schema defines the input and return types of the mutation. You can learn more about schemas and types on https://graphql.org.

Our Schema is simple: incrementPageViews(id: ID!): Page. It's called incrementPageViews and requires an id parameter that must be of an ID type. It returns a Page type.

ID is an internal GraphQL type and Page is a type that has been generated automatically by Keystone when we created the Page list.

The resolver is a function that returns an object matching the Page type. You can see all the type generated by Keystone by looking at the GraphQL Playground documentation that is accessible via the admin UI.

All together our custom mutation looks like this:

keystone.extendGraphQLSchema({
  mutations: [
    {
      schema: `incrementPageViews(id: ID!): Page`,
      resolver: incrementPageViews,
    },
  ],
});

In this mutation we want to access an existing item in the list. We don't care about access control, in-fact we will make the field read-only so that it cannot be updated with normal mutations or in the AdminUI. Our new Page definition is:

const Page = {
  fields: {
    title: { type: Text },
    views: {
      type: Integer,
      access: {
        create: false,
        read: true,
        update: false,
      },
    },
  },
};

The last step is to define the resolver function incrementPageViews.

Our function will by-pass access control and update the value directly by getting the list item and then by calling findById on the list adapter. Once we have the old item we can call update with the new values.

const incrementPageViews = async (_, { id }) => {
  const list = keystone.lists.Page;
  const oldItem = await list.adapter.findById(id);
  const newItem = await list.adapter.update(id, {
    ...oldItem,
    views: (oldItem.views || 0) + 1,
  });
  return newItem;
};

Note: The value of views may be undefined initially, so before we increment it we make sure to convert any falsey values to 0.

Our custom mutation is now available to the client that can use it to increment page views like this:

mutation incrementPageViews($id: ID!) {
  mutation {
    incrementPageViews(id: $id) {
      views
  }
}

Custom mutations can be used for actions like increment that require a single operation that should not rely on data from the client but can also be used for operations that have side effects not related to updating lists.

Note: We used a custom mutation to increase the reliability of operations like increment because client requests can be received out of order. Whilst a custom mutation is a huge improvement, it is not a completely transactional in every situation. The incrementPageViews function is asynchronous. This means it awaits database operations like findById and update. Depending on you server environment database operations, just like like http requests, can be returned in a different order than executed in JavaScript. In this example we've reduced the window this can occur in from seconds to milliseconds. It's not likely a problem for simple page views but you may want to consider implementing database transactions where accuracy is absolutely critical.

Have you found a mistake, something that is missing, or could be improved on this page? Please edit the Markdown file on GitHub and submit a PR with your changes.

Edit Page