SiSharp
SiSharp

Reputation: 121

Strapi - How to extend graphql type schema with custom property?

Let's say I have 'firstName' and 'lastName' properties on an Author type schema created as Strapi content type.

I am able to query them with graphql, but what if I want to query 'fullName' property without adding that field on my content type?

As field doesn't exist, now it says: Cannot query field \"fullName\" on type \"Author\".

How can I extend existing type schema with that additional "virtual" field?

Upvotes: 3

Views: 4344

Answers (4)

wavematt
wavematt

Reputation: 418

Just found this post and also found the appropriate solution. This example repo demonstrates how to use a service function with custom controller methods and a custom GraphQL schema to get what you want. I just implemented the same in my own project.

Your case would not need a service function. You just need to do 2 things:

  1. Define fullName property in /api/authors/config/schema.graphql.js like below:
module.exports = {
  definition:
    extend type Author {
      fullName: AuthorFullName
    }

    type AuthorFullName {
      firstName: String
      lastName: String
    }
  `,
};
  1. Next, you need to override the find and findOne controller methods for Author like below:
module.exports = {
  async find( ctx ) {
    let entities;

    if ( ctx.query._q ) {
      entities = await strapi.services.author.search( ctx.query );
    } else {
      entities = await strapi.services.author.find( ctx.query );
    }

    // Add computed field `fullName` to entities.
    entities.map( entity => {
      entity.fullName = `${entity.firstName} ${entity.lastName}`;

      return entity;
    } );

    return entities.map( entity => sanitizeEntity( entity, { model: strapi.models.author } ) );
  },

  async findOne( ctx ) {
    const { id } = ctx.params;
    let entity = await strapi.services.author.findOne( { id } );

    if ( ! entity ) {
      return ctx.notFound();
    }

    // Add computed field `fullName` to entity.
    entity.fullName = `${entity.firstName} ${entity.lastName}`;

    return sanitizeEntity( entity, { model: strapi.models.author } );
  },
};

This allows REST API calls to get the fullName returned and also tells GraphQL to include fullName in its schema as well, so find and findOne can pass it along to GraphQL properly.

I hope this helps because I feel like I just leveled up big time after learning this!

Upvotes: 2

SiSharp
SiSharp

Reputation: 121

I managed to do it with the following code in the schema.graphql file placed inside the api/author/config folder:

module.exports = {
  definition: `type AuthorOverride {
  firstName: String
  lastName: String
  fullName: String
}`,
  query: `
    authors: [AuthorOverride]
  `,
  type: {
    Author: false
  },
  resolver: {
    Query: {
      authors: {
        description: 'Return the authors',
        resolver: 'Author.find'
      }
    }
  }
};

The key was to define schema with additional field while using different type name (AuthorOverride) to avoid duplicate type error.

Also, to set type: { Author: false } so that original type won't be queriable.

Now, inside my resolver function 'Author.find' (placed in my Author.js controller) I can map fullName value.

If someone has a more appropriate solution for extending graphql schema in Strapi, feel free to post it.

Upvotes: 6

Jake
Jake

Reputation: 379

For Strapi v4 things have changed. See extending-the-schema

For example I extended UploadFile by doing:

"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 extension = ({ nexus }) => ({
      types: [
        nexus.extendType({
          type: "UploadFile",
          definition: (t) => {
            t.string("path", {
              description: "Path to file on its host.",
              resolve: (root, args, ctx) => (new URL(root.url).pathname),
            });
          },
        }),
      ],
    });

    strapi.plugin("graphql").service("extension").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 }*/) {},
};

Upvotes: 0

Will Squire
Will Squire

Reputation: 6595

None of these worked for me. From the docs, it looks like they've changed v4 to extend in a global context rather than per module/entity (src/api/...). Like so:

// src/index.js

"use strict";

module.exports = {
  register({ strapi }) {
    const extensionService = strapi.plugin("graphql").service("extension");

    const extension = () => ({
      typeDefs: `
      type Author {
        fullName: String
      }
      `,
      resolvers: {
        Author: {
          fullName(author) {
            return strapi.service("api::author.author").getFullName(author);
          },
        },
      },
    });

    extensionService.use(extension);
  },
};

This could be wrong and I hope it is. It is a step backend in my eyes, architecturally speaking. But, you could always add this logic to a file in each entity folder (such as src/api/graphql/index.js), then import it to this global file. A bit like what happening with the service logic above to keep the separation of concerns. It's just a bit 'manual'.

For those that want to see what strapi.service("api::author.author").getFullName(author) is calling:

// src/api/author/services/author.js

"use strict";

const { createCoreService } = require("@strapi/strapi").factories;

module.exports = createCoreService("api::author.author", () => ({
  getFullName(author) {
    return `${author.firstName} ${author.lastName}`;
  },
}));

Upvotes: 3

Related Questions