How to Extend and Build Custom Resolvers with Strapi v4

In recent years, there has been a consistent rise in demand for headless solutions, from e-commerce to content management. We will focus on Strapi, an open-source headless CMS, and break down how to quickly build and customize tailored headless CMS solutions.


This is a companion discussion topic for the original entry at https://strapi.io/blog/extending-and-building-custom-resolvers-with-strapi-v4

This post is well written with a lot of code examples. I have read countless of Strapi’s posts, and not all authors showed step-by-step how the code was written. Please keep it up.

2 Likes

Thank you so much. I had help from @Convly with some technical details and questions. Will have more graphql content coming.

1 Like

Hello,
I’ve found this tutorial very useful, and I thank you for that.
Unfortunately, I can’t adapt the code to be compatible with multi languages (i18n). Here is my code :

"use strict";

module.exports = {
  register({ strapi }) {
    const extensionService = strapi.service("plugin::graphql.extension");
    extensionService.use(({ strapi }) => ({
      typeDefs: `
        type Query {
          article(slug: String!, locale: I18NLocaleCode): ArticleEntityResponse
        }
      `,
      resolvers: {
        Query: {
          article: {
            resolve: async (parent, args, context) => {
              const { toEntityResponse } = strapi.service(
                "plugin::graphql.format"
              ).returnTypes;

              const data = await strapi.services["api::article.article"].find({
                filters: { slug: args.slug, locale: args.locale },
              });

              const response = toEntityResponse(data.results[0]);

              console.log("##################", response, "##################");

              return response;
            },
          },
        },
      },
    }));
  },
};

When I make a GraphQL request, I am getting an empty response.

Thank you very much for your help!

1 Like

Sorry for Super Late response, recently saw this question. If you haven’t figure out the answer yet or for future folks here is what I came up with.

The Problem

When using enititi.service it was only returning data for the the selected default locale. And not all available options. You can see the video for explanation.:point_right:t3: Issue with getting locale - YouTube

To fix this I ended up using strapi.db.query. I beleave that the service has some logic that check default locale and only returns those items. Since db.query is more lower level and you directly querying the data you don’t have that issue.

Here is the refactored code.

"use strict";

const boostrap = require("./bootstrap");

module.exports = {
  async bootstrap() {
    await boostrap();
  },

  register({ strapi }) {
    const extensionService = strapi.service("plugin::graphql.extension");
    extensionService.use(({ strapi }) => ({
      typeDefs: `
        type Query {
          article(slug: String!, locale: I18NLocaleCode): ArticleEntityResponse
        }
      `,
      resolvers: {
        Query: {
          article: {
            resolve: async (parent, args, context) => {
              const { toEntityResponse } = strapi.service(
                "plugin::graphql.format"
              ).returnTypes;

              // const data = await strapi.services["api::article.article"].find({
              //   filters: { slug: args.slug, locale: args.locale },
              // });

              const data = await strapi.db.query("api::article.article").findMany({
                where: { locale: args.locale, slug: args.slug }
              });

              // console.log("##################", data.results, "##################");


              const response = toEntityResponse(data[0]);
              // const response = toEntityResponse(data.results[0]);

              console.log("##################", response, "##################");

              return response;
            },
          },
        },
      },
    }));
  },
};

I would recommend on diving deeper into the

1 Like

Hello,
Thank you very much for your response.
Fortunately, after two weeks of being stuck, I found a solution on my own. I just use filters parameter on a GraphQL multiple result query, and select the first result of the array result in the webpage.
But thank you very much for the YouTube video, I really appreciate your help!!
Have a good weekend.

1 Like

Finally I prefer to use your solution (for presentational reasons)

1 Like

Sorry it took me a while to respond but glad that you found a work around and that this solution was helpful.

QUESTION. This works great but what if we need to create custom query for many collections. This file would grow very big. Is there a way we can put the code in different files for each collection? Also are there any third party libraries to automatically generate and manage the code for us

1 Like

graphql always returns 200.

const ctx = strapi.requestContext.get();
ctx.throw(401, "error.");

or

throw new ForbiddenError("error.");

I have writed these in policy, rest api working as expected but graphql not. Any idea?

How do i perform amutation on the nested relation?

Yes, you can place the code in different files and organize it in a way that makes sense for your use case and them import into the file with the register function.

So you can have your types and resolvers live in different files.

Can you provide an example of what you are trying to achieve to give more context.

Sure. Thanks for responding.

I have two tables: web_conference which is the parent and conference_users which is the relational child table.

I want to create an entry to add a new web_conference and a link to all conference_users.

I am doing this:

try {
        const newParentObject = await strapi.entityService.create('api::web-conference.web-conference',{
            data:{
                ownerId:userUid,
                sessionId:sessionId,
                title:title,
                date:date,
                startTime:startTime,
                endTime:endTime,
                publishedAt: publishedAt,
                conference_users:[{userId:0,userId:"eeeeee",sessionId:"333-333"}],
                inviteRequired:inviteOnly}
        });           
    }
catch (error) {
    console.log(error)    
}

but I keep getting the error: “Undefined attribute level operator userId”.

The schema.json for web_conference and conference_users are:

{
  "kind": "collectionType",
  "collectionName": "conference_users",
  "info": {
    "singularName": "conference-user",
    "pluralName": "conference-users",
    "displayName": "ConferenceUser",
    "description": ""
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {},
  "attributes": {
    "userId": {
      "type": "string"
    },
    "sessionId": {
      "type": "string",
      "required": true
    },
    "web_conference": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::web-conference.web-conference",
      "inversedBy": "conference_users"
    }
  }
}
{
  "kind": "collectionType",
  "collectionName": "web_conferences",
  "info": {
    "singularName": "web-conference",
    "pluralName": "web-conferences",
    "displayName": "WebConference",
    "description": ""
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {},
  "attributes": {
    "sessionId": {
      "type": "string",
      "required": true,
      "unique": true
    },
    "title": {
      "type": "string",
      "required": true
    },
    "date": {
      "type": "string"
    },
    "startTime": {
      "type": "string"
    },
    "endTime": {
      "type": "string"
    },
    "inviteRequired": {
      "type": "boolean",
      "required": true,
      "default": false
    },
    "ownerId": {
      "type": "string"
    },
    "conference_users": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::conference-user.conference-user",
      "mappedBy": "web_conference"
    }
  }
}

Thanks for any assistance.
Can you please help?

Sorry for super late response. I think here you just have to pass the array of conference_user ids and not the whole object.

Hey Paul,
I’d like to add an extra field to a type, to do some date string formatting. I cannot find how to do so. Could you help point me in the right direction?

However, we still cannot replace the existing type with a new one. For example, if we have a custom component that has a JSON type and doesn’t provide access to its subfields, and we want to define our own type in such a scenario, we can only extend, but not replace the existing type.