Strapi Customize GraphQL Schema Example

June 17th 2020

Strapi is the leading Headless CMS that is open source and a complete Javascript framework. By default Strapi create REST endpoints for each of your content types. With the GraphQL plugin, you will be able to add a GraphQL endpoint to fetch and mutate your content. By default Strapi generates a GraphQL schema for each of your Content Types. For each model, the plugin auto-generates queries and mutations which reflect the REST create, read, update and delete API. Although this is a powerful toolset, you may come to the point in your project that you need to customize the GraphQL schema for one or more of your Content models. Today we’ll discuss how to customize the User Permissions plugin’s GraphQL schema in order to include a relationship with a Customer content type that we will be creating.

Strapi Quick Start

We will start by creating a new strapi project. We can use the yarn create command to quickstart our strapi project:

yarn create strapi-app strapi-graphql --quickstart

We’re naming our project strapi-graphql and when the project is finished installing we can create our admin user. We can also install the graphql plugin for strapi by running:

yarn strapi install graphql

We can insure our graphql server is running by navigating to http://localhost:1337/graphql to see our GraphiQL playground.

Strapi Customer Content Type

We will be creating a content type called Customer that will hold some additional fields outside of our User content type that is installed when you start the project. We’re creating the Customer content type because not all users will have a customer associated with them and some Customers will be “anonymous” or guest customers that will not need to create an account (a User) in our strapi application. We can navigate to our Content-Types Builder and add click on “Create new collection type”. We will be creating a collection called “Customer” and we can add and configure these fields:

For our relationship with our User model we can create a relationship that is “Customer has one User” where our Customer may have a relationship to a User but our User doesn’t need to know about the relationship to the Customer.

Now we can save the Customer Content type and wait for the server to restart. Let’s create our first user and our first customer and associate the customer with the user. This will simulate a user signing up on our application and then going through the checkout process to become a Customer of our application. Navigate to “Users” under our Collection Types and click “Add New User”. We can create the user with the username: jdoe and email: john.doe@gmail.com

We can create a Customer that reflects our User and create the relationship between our Customer and the User we created:

Retrieve a User with Customer

Although our User content type does not have a reciprocal relationship with our Customer we might want to get any Customer relationship on our User if a user is logged in to our application, otherwise if our User is not associated with a Customer we would expect the customer field to return null. The Strapi User Permissions plugin exposes an endpoint /users/me that returns the logged in User’s information. We will need to pass in the Authorization: Bearer <jwt> header with a valid jwt token of our logged in user. To get the token we can send a POST request to the http://localhost:1337/auth/local endpoint with our users credentials:

curl -d 'identifier=jdoe&password=john.doe' http://localhost:1337/auth/local

Make sure you replace the password with the password you created for the user. We can see that our endpoint returns our user object with a jwt string:

{
 "jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlZTk4MmZkOGJjNDlmNjc2ZDI5MTNiYSIsImlhdCI6MTU5MjQwMzk1MSwiZXhwIjoxNTk0OTk1OTUxfQ.uV7mSSu8IaM08yfRzH5KGKlWyTVccpZ31d_GDvtGFwI",
  "user":{
    "confirmed":true,
    "blocked":false,
    "_id":"5ee982fd8bc49f676d2913ba",
    "username":"jdoe",
    "email":"john.doe@gmail.com",
    "provider":"local",
    "createdAt":"2020-06-17T02:42:05.331Z",
    "updatedAt":"2020-06-17T02:42:05.345Z",
    "__v":0,
    "role":{
      "_id":"5ee98018efd1be5203f63b9e",
      "name":"Authenticated",
      "description":"Default role given to authenticated user.",
      "type":"authenticated",
      "createdAt":"2020-06-17T02:29:44.626Z",
      "updatedAt":"2020-06-17T02:29:44.626Z",
      "__v":0,
      "id":"5ee98018efd1be5203f63b9e"
    },
    "id":"5ee982fd8bc49f676d2913ba"
  }
}

We can copy the jwt token over to include in our Authorization: Bearer header to our GET request to http://localhost:1337/users/me and we will see our data returned from our /users/me endpoint

{
    "confirmed": true,
    "blocked": false,
    "_id": "5ee982fd8bc49f676d2913ba",
    "username": "jdoe",
    "email": "john.doe@gmail.com",
    "provider": "local",
    "createdAt": "2020-06-17T02:42:05.331Z",
    "updatedAt": "2020-06-17T02:42:05.345Z",
    "__v": 0,
    "role": {
        "_id": "5ee98018efd1be5203f63b9e",
        "name": "Authenticated",
        "description": "Default role given to authenticated user.",
        "type": "authenticated",
        "createdAt": "2020-06-17T02:29:44.626Z",
        "updatedAt": "2020-06-17T02:29:44.626Z",
        "__v": 0,
        "id": "5ee98018efd1be5203f63b9e"
    },
    "id": "5ee982fd8bc49f676d2913ba"
}

As you can see even though we created a Customer to User relationship our User does not know about the customer and thus does not return the field in our REST call. In order to populate our Customer on our user we’ll have to inspect the controller action that is handled by the request to /users/me endpoint which can be found at: strapi-graphql/node_modules/strapi-plugin-users-permissions/controllers/User.js and we can examine the me action:

  /**
   * Retrieve authenticated user.
   * @return {Object|Array}
   */
  async me(ctx) {
    const user = ctx.state.user;

    if (!user) {
      return ctx.badRequest(null, [{ messages: [{ id: 'No authorization header was found' }] }]);
    }

    const data = sanitizeUser(user);
    ctx.send(data);
  }

We can “shadow” or override this action by creating a file located at: strapi-graphql/extensions/users-permissions/controllers/User.js and adding our action that matches the action in the node_modules folder:

'use strict';

/**
 * User.js controller
 *
 * @description: A set of functions called "actions" for managing `User`.
 */

const { sanitizeEntity } = require('strapi-utils');

const sanitizeUser = user =>
  sanitizeEntity(user, {
    model: strapi.query('user', 'users-permissions').model,
  });

module.exports = {
  /**
   * Retrieve authenticated user.
   * @return {Object|Array}
   */
  async me(ctx) {
    const user = ctx.state.user;

    if (!user) {
      return ctx.badRequest(null, [{ messages: [{ id: 'No authorization header was found' }] }]);
    }

    const data = sanitizeUser(user);
    ctx.send(data);
  }
};

Inside this action we will need to query the Customer that has an association with the user. We do know the user’s id and we can use the strapi.query() to find the customer and merge it onto the user:

'use strict';

/**
 * User.js controller
 *
 * @description: A set of functions called "actions" for managing `User`.
 */

const { sanitizeEntity } = require('strapi-utils');

const sanitizeUser = user =>
  sanitizeEntity(user, {
    model: strapi.query('user', 'users-permissions').model,
  });

module.exports = {
  /**
   * Retrieve authenticated user.
   * @return {Object|Array}
   */
  async me(ctx) {
    const user = ctx.state.user;

    if (!user) {
      return ctx.badRequest(null, [{ messages: [{ id: 'No authorization header was found' }] }]);
    }

    const customer = await strapi.query('customer').findOne({ user: user.id }, []);
    const withCustomer = { ...user, customer }

    const data = sanitizeUser(withCustomer);
    ctx.send(data);
  }
};

We are using the strapi.query('customer').findOne({ user: user.id }, []) findOne query provided by strapi to get the customer based on the user.id. The second parameter passed in is an empty array to signify that we don’t need to populate the relationships on the returned customer object (because the only relationship we have is for the user). We should get back an object that looks like:

      {
        "id": "5ee9838d8bc49f676d2913bb",
        "first": "John",
        "last": "Doe",
        "address": {
          "street": "123 St",
          "city": "Austin",
          "state": "TX",
          "zip": 54321
        },
        "phone": "(555) 123-4567"
      }

Which we are going to merge onto the user object:

const withCustomer = { ...user, customer }

Finally, we’re sanitizing our data and returning it to the response object with ctx.send. We can confirm this is working by using the curl command in the terminal to GET the endpoint /users/me

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlZTk4MmZkOGJjNDlmNjc2ZDI5MTNiYSIsImlhdCI6MTU5MjQwMzk1MSwiZXhwIjoxNTk0OTk1OTUxfQ.uV7mSSu8IaM08yfRzH5KGKlWyTVccpZ31d_GDvtGFwI" http://localhost:1337/users/me

Making sure to replace the jwt token in the “Authorization: Bearer” header with the token for your user that you can get from running a POST to the /auth/local endpoint. You can see that in our returned response we have the Customer field populated with our customer data:

{
    "confirmed": true,
    "blocked": false,
    "_id": "5ee982fd8bc49f676d2913ba",
    "username": "jdoe",
    "email": "john.doe@gmail.com",
    "provider": "local",
    "createdAt": "2020-06-17T02:42:05.331Z",
    "updatedAt": "2020-06-17T02:42:05.345Z",
    "id": "5ee982fd8bc49f676d2913ba",
    "customer": {
        "_id": "5ee9838d8bc49f676d2913bb",
        "address": {
            "street": "123 St",
            "city": "Austin",
            "state": "TX",
            "zip": 54321
        },
        "first": "John",
        "last": "Doe",
        "email": "john.doe@gmail.com",
        "phone": "(555) 123-4567",
        "createdAt": "2020-06-17T02:44:29.955Z",
        "updatedAt": "2020-06-17T02:44:29.959Z",
        "__v": 0,
        "user": "5ee982fd8bc49f676d2913ba",
        "id": "5ee9838d8bc49f676d2913bb"
    }
}

Just as we had expected! But what if we want to use the GraphQL endpoint to access the /users/me information? Right now our GraphQL type definitions for the me query is set to resolve to our me action in the User.js file that we shadowed in extensions/users-permissions/controllers but the type definition for the query does not expect to return a customer field by looking at the Schema Definition in the GraphiQL explorer:

type Query {
  me: UsersPermissionsMe
}

type UsersPermissionsMe {
  id: ID!
  username: String!
  email: String!
  confirmed: Boolean
  blocked: Boolean
  role: UsersPermissionsMeRole
}

resolver: {
    Query: {
      me: {
        resolver: 'plugins::users-permissions.user.me',
      }
    }
}

Since we’re shadowing the action plugins::users-permissions.user.me by placing a file in strapi-graphql/extensions/users-permissions/controllers/User.js the resolver will use our action over the plugin’s action. But we do need to tell GraphQL that we want a customer field on our query definition. Unfortunately, we can’t override our me query as we will get the error:

Error: Field "Query.me" can only be defined once.

So instead we will need to create a new Query definition located in extensions/users-permissions/config/schema.graphql.js that we can use to populate our Customer on our user query:

module.exports = {
  definition: `
    type UserCustomer {
      id: ID!
      username: String!
      email: String!
      confirmed: Boolean
      blocked: Boolean
      role: UsersPermissionsMeRole
      customer: Customer
    }
  `,
  query: `
    userCustomer: UserCustomer 
  `,
  resolver: {
    Query: {
      userCustomer: {
        resolver: 'plugins::users-permissions.user.me'
      }
    }
  }
}

First, we’re created a new Type Definition called UserCustomer that expects a customer field of Type Customer. We are then definition a new query called userCustomer that is expecting to return the Type UserCustomer. Finally, we’re defining our resolver for userCustomer query which is the same resolver for the me query used by the Users Permissions plugin (which we have shadowed). Now we are able to go to our GraphiQL explorer and try out our new query:

Making sure we’re passing in the correct jwt token in our Authorization header we can see that we are getting back our user with the customer object associated.

We have successfully customized our GraphQL schema to associate our Customer on our User by using a custom query we defined and shadowing the User.me action of the User Permissions plugin!