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: [email protected]
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":"[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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!