GraphQL custom resolver in V4

Hello.

I am trying to resolve a graphQL query. I want to share as much logic as possible between REST and graphQL, therefore I am trying to call a controller in my resolver.

First question - is it a good idea to resolve a query / mutation using a controller? Or should I rather use a service?

Second question - when I get the data from the controller, how does it need to be formatted before it’s returned in gQL response? I have tried following:

'use strict';

module.exports = {
  /**
   * An asynchronous register function that runs before
   * your application is initialized.
   *
   * This gives you an opportunity to extend code.
   */
   register: ({ strapi }) => {
    const { transformArgs, getContentTypeArgs } = strapi
      .plugin("graphql")
      .service("builders").utils;
    const extensionService = strapi.plugin("graphql").service("extension");

    const extension = ({ nexus }) => ({
      // Nexus
      types: [
        nexus.extendType({
          type: "Query",
          definition(t) {
            t.field("restaurants", {
              type: "RestaurantEntityResponseCollection",
              args: { slug: nexus.stringArg() },
              async resolve(parent, args, ctx) {
                const transformedArgs = transformArgs(args, {
                  contentType:
                    strapi.contentTypes["api::restaurant.restaurant"],
                  usePagination: false
                })

                const {data, meta} = await strapi.controller("api::restaurant.restaurant").find(ctx)
                if (data) {
                  return { restaurants: {data, meta}}

                } else {
                  throw new Error(ctx.koaContext.response.message);
                }
              }
            });
          }
        })
      ],

      resolversConfig: {
        "Query.restaurants": {
          auth: false
        }
      }
    });

    extensionService.use(extension);
  },

  /**
   * An asynchronous bootstrap function that runs before
   * your application gets started.
   *
   * This gives you an opportunity to set up your data model,
   * run jobs, or perform some special logic.
   */
  bootstrap(/*{ strapi }*/) {},
};

The problem is that my response always comes empty like this:

{
  "data": {
    "restaurants": {
      "data": []
    }
  }
}

I am pretty sure that I’m not formatting the response correctly. I have tried several formats like {data: {restaurants: {data, meta}}}, {restaurants: {data, meta}}, {data: {data, meta}} or even just {data, meta}, but none of them worked. So what is the correct format that needs to be returned from a gQL resolver?

Thanks to @Convly for his response under this GitHub issue:

Our goal with the V4 was to decouple REST & GraphQL. Previously GQL resolvers were calling REST controllers which caused a lot of troubles, now both REST controllers & GraphQL resolvers are using the entity service API internally.

That’s why I would advise against calling your REST controller directly from the resolver.
Instead what you could do is smth like

strapi.entityService.findMany("api::restaurant.restaurant", transformedArgs)

Then in the response, you could do:

const { toEntityResponseCollection } = strapi.plugin('graphql').service('format').returnTypes; 
if (data) { 
    return toEntityResponseCollection(data, { args: transformedArgs, resourceUID:"api::restaurant.restaurant" }) 
}  else { 
    throw NotFoundError(); 
}

On a side note: toEntityResponseCollection is basically just a wrapper that matches the expected format for the findMany output which is { value, args, resourceUID }. For a single resource you can use toEntityResponse.

3 Likes

I am trying to use this as described here Build Custom Resolvers with Strapi, but the query doesn’t shop up in /graphql

module.exports = {
  definition: `
    type Collection {
        title: String
    }
    `,
  query: `collectionBySlug(slug: String!): Collection`,
  type: {},
  resolver: {
    Query: {
      description: "Lorem ipsum",
      collectionBySlug: {
        async resolve(_, { slug }) {
          const entities = await strapi.entityService.findMany(
            "api::collection.collection",
            { filters: { slug } }
          );

          return entities[0] ?? null;
        },
      },
    },
  },
};
1 Like

@gvocale the example you are refering to is for V3 of Strapi. As far as i am aware that does not work with V4 anymore.

Limited documentation for V4, and how to extend the schema can be found here https://docs.strapi.io/developer-docs/latest/plugins/graphql.html#extending-the-schema

2 Likes

I wish Strapi was written in Typescript…then the compiler would tell me immediately if an option is valid or not…

6 Likes

The same problem for me. It didn’t work for v4

Detail documentation will be really helpful. I’m thinking now if I should switch back to the REST API

I have been having issues with how complicated graphql is to extend. I had hoped REST and graphql APIs would function the same but they require very different approaches it seems.

We are abandoning graphql for REST in our project, I hope it can be improved over time.

After a little digging, I found it very straightforward to set up a custom resolver on a GraphQL type. Here’s a loose example. Even though this topic is listed as solved. I’m replying here because it seems like people are having issues and this is the first result I found in Google.

Here is a sample src/index.js

'use strict';

module.exports = {
  /**
   * An asynchronous register function that runs before
   * your application is initialized.
   *
   * This gives you an opportunity to extend code.
   */
  register({ strapi }) {
    const extensionService = strapi.plugin('graphql').service('extension');

    const extension = ({}) => ({
      typeDefs: `
        type YourType {
          customResolver: [OtherType!]!
        }
      `,
      resolvers: {
        YourType: {
          customResolver: async (parent, args) => {
            const entries = await strapi.entityService.findMany('api::other-type.other-type', {});
            return entries;
          }
        }
      }
    })
    extensionService.use(extension);
  },

  /**
   * An asynchronous bootstrap function that runs before
   * your application gets started.
   *
   * This gives you an opportunity to set up your data model,
   * run jobs, or perform some special logic.
   */
  bootstrap(/*{ strapi }*/) {},
};

I hope this helps you out. Let me know if this doesn’t work for you or if you have any questions.

5 Likes

Could this resolver be placed within the plugin folder, i.e. /strapi/src/plugins/plugin-name/server/register.js. When I do it is not registered correctly?

How do I keep from repeating the code below in every single custom resolver?

const { toEntityResponseCollection } = strapi.plugin("graphql").service("format").returnTypes;

const { toEntityResponse } = strapi.plugin("graphql").service("format").returnTypes;

I have multiple custom resolvers and in each of them I have to include the logic above so my return type will display correctly; otherwise, I will get Data: null. I want to declare them once and use them over and over without repeating the logic.

Code with repeating logic:

const axios = require("axios");
const EXTERNAL_API_URL = "http://jsonplaceholder.typicode.com/users";

exports.Query = {
  endUsers: async () => {
    const { data } = await axios.get(EXTERNAL_API_URL);
    const { toEntityResponseCollection } = strapi
      .plugin("graphql")
      .service("format").returnTypes;

    if (data) {
      console.log(data);
      return toEntityResponseCollection(data);
    } else {
      throw NotFoundError();
    }
  },
  endUser: async (parent, args, context) => {
    console.log("id: ", args.id);

    const url = `${EXTERNAL_API_URL}/${args.id}`;

    const { toEntityResponse } = strapi
      .plugin("graphql")
      .service("format").returnTypes;
    const { data } = await axios.get(url);

    console.log("url", url);

    if (data) {
      console.log(data);
      return toEntityResponse(data);
    } else {
      throw NotFoundError();
    }
  },
};

Thank you.