В этой статье объясняется, как расширять и создавать собственные револьверы с помощью Strapi v4.

Автор: Павел Брацлавский

В последние годы наблюдается устойчивый рост спроса на безголовые решения, от электронной коммерции до управления контентом. Мы сосредоточимся на Strapi, безголовой CMS с открытым исходным кодом и расскажем, как быстро создавать и настраивать индивидуальные решения безголовой CMS.

В этой статье вы узнаете:

  • Как установить и настроить плагин Strapi GraphQL,
  • Такие понятия, как распознаватели, мутации и запросы в контексте GraphQL, и
  • Как настроить серверную часть GraphQL Strapi с помощью пользовательских распознавателей для запросов и мутаций.

Безголовая CMS

Термин безголовый происходит от идеи отделения головы (внешней части) от тела (внутренней части). Безголовая CMS ориентирована на хранение и доставку структурированного контента — ей все равно, где и как этот контент отображается.

Системы Headless CMS имеют множество применений, в том числе:

  • Создание веб-сайтов и приложений с использованием любого JavaScript-фреймворка (Next.js, React, Vue, Angular),
  • Предоставление контента для генераторов статических сайтов (Gatsby, Jekyll, Hugo),
  • Мобильные приложения (iOS, Android, React Native) и
  • Обогащение информации о продукте на сайтах электронной коммерции.

Страпи

Strapi — это автономная CMS на основе Node.js с открытым исходным кодом, которая экономит время разработчиков, предоставляя им свободу использовать свои любимые инструменты и фреймворки. Strapi также позволяет редакторам контента оптимизировать доставку контента (текста, изображений, видео и т. д.) на любом устройстве. — Страпи | Что такое Страпи

Strapi предлагает следующие преимущества:

  • Открытый исходный код: доступен на GitHub и поддерживается сотнями участников.
  • Собственный хостинг: дает вам полный контроль над вашими данными и конфиденциальностью.
  • Настраиваемый: через панель администратора или напрямую с помощью плагинов и настроек.
  • Гибкость: используйте его из любого клиента, SPA или мобильного приложения, а также через REST или GraphQL.

ГрафQL

GraphQL — это язык запросов и обработки данных с открытым исходным кодом для API и среда выполнения для выполнения запросов с существующими данными. GraphQL был разработан Facebook внутри компании в 2012 году, а затем публично выпущен в 2015 году. — Википедия

В отличие от REST, GraphQL позволяет извлекать только необходимый контент. Это дает клиенту гораздо больше свободы, что приводит к гораздо более быстрой разработке по сравнению с REST.

Реализация базового решения

В этой статье давайте воспользуемся одним из многих Стартеров Strapi в качестве отправной точки. Затем вы настроите его в соответствии со своими потребностями, в данном случае с помощью NextJS Blog Starter. Начните с создания совершенно нового проекта:

npx create-strapi-starter graphql-blog next-blog --quickstart
        cd graphql-blog

Затем убедитесь, что установка Strapi работает правильно, запустив:

yarn develop

Strapi потребует от вас создать учетную запись администратора при первом запуске, например:

Затем вы должны увидеть полностью настроенный администратор Strapi в контексте блога:

В этом стартере по умолчанию должен быть установлен GraphQL, если нет. Вы можете легко включить поддержку GraphQL прямо из администратора Strapi:

  • Перейдите в Маркетплейс.
  • Найдите плагин GraphQL
  • Нажмите Копировать команду установки в подключаемом модуле GraphQL.

В терминале вставьте команду, установите и перезапустите. Вы можете вручную перезапустить сервер, чтобы убедиться, что плагин GraphQL полностью инициализирован — вы можете сделать это из терминала, как и раньше:

yarn develop

После перезапуска сервера вы можете протестировать новый API GraphQL, открыв игровую площадку GraphQL: localhost:1337/graphql.

Затем введите следующий запрос, чтобы убедиться, что вы можете получать статьи:

query {
  articles {
    data {
      id
      attributes {
        title
        description
      }
    }
  }
}

Вы должны увидеть результаты справа:

По умолчанию в плагине Strapi GraphQL включена функция Shadow CRUD, полезная функция, устраняющая необходимость указывать какие-либо определения, запросы, мутации или что-либо еще. Shadow CRUD автоматически сгенерирует все необходимое для начала использования GraphQL на основе ваших существующих моделей. Однако этой автоматически сгенерированной реализации может быть недостаточно для каждого варианта использования. Скорее всего, вам придется настроить свои запросы и мутации для вашего конкретного случая использования. Далее давайте посмотрим, как вы можете использовать пользовательские распознаватели для настройки как ваших запросов, так и мутаций.

Резольверы

Преобразователи — это функции, которые разрешают значение для типа или поля в схеме. Вы также можете определить пользовательские преобразователи для обработки пользовательских запросов и мутаций. В отличие от Strapi v3, где мы записали наши пользовательские распознаватели в файл schema.graphql.js, в v4 все выглядит немного иначе.

Сравнение V3/V4

В Strapi v3 преобразователи GraphQL либо автоматически привязываются к контроллерам REST (из основного API), либо настраиваются с помощью файлов ./api/<api-name>/config/schema.graphql.js. В Strapi v4 для основных операций CRUD для каждого API автоматически создаются выделенные основные преобразователи GraphQL. Дополнительные преобразователи можно настроить программно с помощью службы расширения GraphQL, доступной с помощью strapi.plugin(’graphql’).service(’extension’).

Подробнее о различиях можно узнать здесь. Резолверы Strapi GraphQl v3/v4 Давайте начнем с простого примера, чтобы узнать, как запрашивать статью через slug вместо id. На игровой площадке GraphQL localhost:1337/graphql выполните следующий запрос:

query {
                     article(id: "1") {
                data {
                  id
                  attributes {
                    title
                    description
                    content
                  }
                }
              }
            }

Как видите, мы запрашиваем нашу статью по идентификатору.

И вернуть следующие данные:

{
      "data": {
        "article": {
          "data": {
            "id": "1",
            "attributes": {
              "title": "What's inside a Black Hole",
              "description": "Maybe the answer is in this article, or not...",
              "content": "Well, we don't know yet..."
            }
          }
        }
      }
    }

Если мы запросим статью через slug, это не сработает, потому что наш текущий преобразователь еще не поддерживает эту функцию.

Давайте посмотрим, как мы можем расширить наш преобразователь статей, чтобы добавить эту функциональность.

Служба расширения GraphQL

Мы можем настроить наши преобразователи с помощью службы расширения GraphQL. Давайте заглянем внутрь нашего файла index.js по адресу backend/src/index.js. Обычно наш файл будет выглядеть так.

Но в нашем текущем стартовом проекте это должно выглядеть так, как показано на рисунке ниже.

Мы настроим наш GraphQl в функциях регистрации, поэтому давайте добавим его обратно.

register(/* { strapi } */) {},

Полный код должен выглядеть так:

"use strict";
    const boostrap = require("./bootstrap");
    
    module.exports = {
      async bootstrap() {
        await boostrap();
      },
    
      register(/* { strapi } */) {},
    };

Давайте воспользуемся сервисом расширения GraphQL, чтобы добавить наши пользовательские преобразователи, добавив следующее в наш файл index.js.

"use strict";
    const boostrap = require("./bootstrap");
    
    module.exports = {
      async bootstrap() {
        await boostrap();
      },
    
      register({ strapi }) {
        const extensionService = strapi.service("plugin::graphql.extension");
        extensionService.use(// add extension code here);
      },
    };

Расширение схемы

Схему, сгенерированную Content API, можно расширить, зарегистрировав расширение. Это расширение, определенное либо как объект, либо как функция, возвращающая объект, будет использоваться функцией use(), предоставляемой extension service, поставляемой с подключаемым модулем GraphQL. Вы можете прочитать больше здесь". Объект, описывающий расширение, принимает следующие параметры:

| Параметр | Тип | Описание | | — — — — — — — — | — — — — | — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — | | типы | Массив | Позволяет расширять типы схем с помощью определений типов на основе Nexus | | typeDefs | Строка | Позволяет расширять типы схемы с помощью GraphQL SDL | | плагины | Массив | Позволяет расширять схему с помощью плагинов Nexus | | резольверы | Объект | Определяет пользовательские распознаватели | | Конфигурация резольверов | Объект | Определяет параметры конфигурации для распознавателей, такие как авторизация, политики и промежуточное ПО |

Вы можете расширить типы с помощью Nexus или сделать это через typeDefs с помощью GraphQL SDL; это подход, который мы собираемся использовать здесь, поскольку мы можем написать целую статью об использовании Nexus.

Прежде чем заполнять логику, давайте передадим в метод use() следующую функцию.

({ strapi }) => ({
      typeDefs: ``,
      resolvers: {},
    });

Наш готовый код должен выглядеть так:

"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: ``,
          resolvers: {},
        }));
      },
    };

Мы передаем strapi, чтобы получить доступ к его методам.

  • typeDefs: позволяет нам переопределять или создавать новые типы def.
  • преобразователь:
  • Запрос: раздел для определения одного или нескольких пользовательских распознавателей запросов.
  • Мутация: раздел для определения одного или нескольких распознавателей мутаций клиентов.
  • resolverConfig: позволяет передавать дополнительные параметры конфигурации.

Теперь, когда у вас есть базовая схема, давайте добавим пользовательский запрос. Запросы

Запрос GraphQL используется для чтения или выборки значений, а мутация используется для записи или публикации значений. В любом случае операция представляет собой простую строку, которую сервер GraphQL может проанализировать и отправить в ответ данные в определенном формате. — Тьюториалпойнты

В этом примере мы переопределим наш запрос article, чтобы позволить нам использовать slug вместо id. запросить наши данные. В настоящее время наше определение запроса выглядит так:

article(id: ID): ArticleEntityResponse

Он принимает id и возвращает наш ArticleEntityResponse, который был автоматически сгенерирован для нас, когда мы создавали тип контента статьи. Давайте переопределим его, чтобы взять slug и id. В наш код добавьте этот фрагмент:

typeDefs: `
        type Query {
          article(slug: String!): ArticleEntityResponse
        }
      `,

Этот запрос указывает имя запроса, которое будут принимать параметры; в этом случае:

  • article — это имя нашего запроса, который мы переопределяем.
  • slug — это параметр типа string, который необходимо передать в нашем запросе.
  • ArticleEntityResponse — это данные, которые мы возвращаем.

Наш готовый код должен выглядеть так:

"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!): ArticleEntityResponse
            }
          `,
          resolvers: {},
        }));
      },
    };

Теперь на нашей игровой площадке GraphQl мы должны иметь возможность передавать slug вместо id в нашем запрос статьи:

Однако, если вы попытаетесь выполнить запрос прямо сейчас, он не сработает. Это имеет смысл, поскольку вы указали только новый тип запроса, который хотите переопределить, но не указали, как разрешить этот запрос и вернуть данные. Здесь в игру вступают резольверы.

Теперь нам нужно переопределить наш преобразователь, чтобы он ожидал slug в качестве параметра, и написать собственную логику, позволяющую нам возвращать правильные данные. Давайте создадим наш преобразователь, а затем просмотрим код и то, что он делает. При определении преобразователей у вас есть два варианта. Вы можете переопределить существующий контроллер или создать полностью настраиваемый. В этом случае мы переопределим наш преобразователь article.

Добавьте следующий код в свою настраиваемую схему.

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 },
                    });
    
                    const response = toEntityResponse(data.results[0]);
    
                    console.log("##################", response, "##################");
    
                    return response;
                  },
                },
              },
            },

Наш готовый код должен выглядеть так:

"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!): 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 },
                  });
    
                  const response = toEntityResponse(data.results[0]);
    
                  console.log("##################", response, "##################");
    
                  return response;
                },
              },
            },
          },
        }));
      },
    };

После того как вы сохранили изменения в своей схеме, перезапустите сервер и снова запустите yarn develop, чтобы убедиться, что изменения отражены, и выполните следующий запрос ниже.

query {
                     article(slug: "what-s-inside-a-black-hole") {
                data {
                  id
                  attributes {
                    title
                    description
                    content
                    slug
                  }
                }
              }
            }

Успех! Мы расширили преобразователь, и теперь ваш запрос возвращает данные на основе slug.

Давайте быстро рассмотрим, что делает каждый фрагмент нашего кода. Мы получаем метод toEntityResponse, позволяющий преобразовать наш ответ в соответствующий формат перед возвратом данных.

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

Вместо того, чтобы наши резолверы были привязаны к контроллерам, как это было в Strapi v3, в v4 мы вызываем наши сервисы напрямую. В этом случае мы вызываем службу, которая была автоматически сгенерирована для нас, когда мы создавали наш тип контента article, но мы можем создавать собственные службы, если захотим.

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

Наконец, мы вызываем наш toEntityResponse, чтобы преобразовать наш ответ в соответствующий формат перед возвратом данных.

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

Мы только что рассмотрели, как переопределить существующий преобразователь. Давайте теперь посмотрим, как мы можем создать собственный преобразователь запросов GraphQL с нуля.

Создайте собственный преобразователь GraphQL

Мы будем следовать аналогичным шагам, как и раньше. Давайте создадим объект схемы заполнителя, который будет включать следующее:

  • typeDefs: позволяет нам переопределять или создавать новые типы def.
  • преобразователь:
  • Запрос: раздел для определения одного или нескольких пользовательских распознавателей запросов.
  • resolverConfig: позволяет передавать дополнительные параметры конфигурации.

Вставьте следующее в свой код:

// Going to be our custom query resolver to get all authors and their details.
    extensionService.use(({ strapi }) => ({
      typeDefs: ``,
      resolvers: {},
      resolversConfig: {},
    }));

Наш готовый код должен выглядеть так:

"use strict";
    
    const boostrap = require("./bootstrap");
    
    module.exports = {
      async bootstrap() {
        await boostrap();
      },
    
      register({ strapi }) {
        const extensionService = strapi.service("plugin::graphql.extension");
    
        // Previous code from before
        extensionService.use(({ strapi }) => ({}));
    
        // Going to be our custom query resolver to get all authors and their details.
        extensionService.use(({ strapi }) => ({
          typeDefs: ``,
          resolvers: {},
          resolversConfig: {},
        }));
      },
    };

Давайте определим наш запрос и определения типов.

typeDefs: `
        type Query {
          authorsContacts: [AuthorContact]
        }
    
        type AuthorContact {
          id: ID
          name: String
          email: String
          articles: [Article]
        }
      `,

Давайте определим наш резольвер.

resolvers: {
        Query: {
          authorsContacts: {
            resolve: async (parent, args, context) => {
    
              const data = await strapi.services["api::writer.writer"].find({
                populate: ["articles"],
              });
    
              return data.results.map(author => ({
                id: author.id,
                name: author.name,
                email: author.email,
                articles: author.articles,
              }));
    
            }
          }
        },
      },

Давайте определим конфигурации, чтобы предоставить нам публичный доступ при выполнении запроса.

resolversConfig: {
        "Query.authorsContacts": {
          auth: false,
        },
      },

Наш готовый код должен выглядеть так:

"use strict";
    
    const boostrap = require("./bootstrap");
    
    module.exports = {
      async bootstrap() {
        await boostrap();
      },
    
      register({ strapi }) {
        const extensionService = strapi.service("plugin::graphql.extension");
    
        // Previous code from before
        extensionService.use(({ strapi }) => ({}));
    
        // Code we just added - custom graphql resolver
        extensionService.use(({ strapi }) => ({
          typeDefs: `
            
            type Query {
              authorsContacts: [AuthorContact]
            }
    
            type AuthorContact {
              id: ID
              name: String
              email: String
              articles: [Article]
            }
          `,
    
          resolvers: {
            Query: {
              authorsContacts: {
                resolve: async (parent, args, context) => {
                  const data = await strapi.services["api::writer.writer"].find({
                    populate: ["articles"],
                  });
    
                  return data.results.map((author) => ({
                    id: author.id,
                    name: author.name,
                    email: author.email,
                    articles: author.articles,
                  }));
                },
              },
            },
          },
    
          resolversConfig: {
            "Query.authorsContacts": {
              auth: false,
            },
          },
        }));
      },
    };

Давайте быстро рассмотрим, что делает каждый фрагмент нашего кода в нашем пользовательском распознавателе. Мы получаем services для извлечения данных нашего модуля записи из базы данных. Затем мы передаем наш флаг заполнения, чтобы позволить нам заполнить данные отношения статьи.

const data = await strapi.services["api::writer.writer"].find({
      populate: ["articles"],
    });

Прежде чем вернуть наши данные, мы преобразуем наш ответ, чтобы он соответствовал нашему определению типов AuthorContact, которое будет возвращено в нашем ответе GraphQl.

return data.results.map((author) => ({
      id: author.id,
      name: author.name,
      email: author.email,
      articles: author.articles,
    }));

Мы только что рассмотрели базовый способ создания пользовательского преобразователя GraphQl в Strapi v4. После того как вы сохранили изменения в своей схеме, перезапустите сервер и снова запустите yarn develop, чтобы убедиться, что изменения отражены, и выполните следующий запрос ниже.

query {
      authorsContacts {
        id
        name
        email
        articles {
          title
          description
          publishedAt
        }
      }
    }

Теперь вы должны увидеть результаты нашего пользовательского запроса.

Вы можете проверить наш только что созданный запрос, взглянув на схему GraphQL Playground:

Есть одна большая проблема

Глядя на этот код, может показаться, что все работает правильно, но здесь есть проблема, и она как-то связана с передачей populate нашему методу find().

const data = await strapi.services["api::writer.writer"].find({
      populate: ["articles"],
    });

Всякий раз, когда мы передаем populate,, мы всегда будем делать дополнительный вызов для извлечения данных articles из базы данных, даже если мы не запрашиваем это явно в нашем запросе. Что нам нужно сделать, так это создать цепочку распознавателя для запроса статей по отдельности.

Создайте дочерний преобразователь для извлечения отношений

Во-первых, давайте реорганизуем наш предыдущий код, удалив ссылку articles в AuthorContact:

type AuthorContact {
            id: ID
            name: String
            email: String
            articles: [Article] <-- REMOVE THIS
        }

Теперь давайте удалим аргумент populate, который мы здесь передаем:

resolvers: {
          Query: {
            authorsContacts: {
              resolve: async (parent, args, context) => {
                const data = await strapi.services["api::writer.writer"].find({
                  populate: ["articles"], <-- REMOVE THIS
                });
    
                return data.results.map((author) => ({
                  id: author.id,
                  name: author.name,
                  email: author.email,
                  articles: author.articles, <-- REMOVE THIS
                }));
              },
            },
          },
        },

Теперь ваш код должен выглядеть так:

extensionService.use(({ strapi }) => ({
      typeDefs: `
    
            type Query {
              authorsContacts: [AuthorContact]
            }
    
            type AuthorContact {
              id: ID
              name: String
              email: String
            }
    
          `,
    
      resolvers: {
        Query: {
          authorsContacts: {
            resolve: async (parent, args, context) => {
              const data = await strapi.services["api::writer.writer"].find();
    
              return data.results.map((author) => ({
                id: author.id,
                name: author.name,
                email: author.email,
              }));
            },
          },
        },
      },
    
      resolversConfig: {
        "Query.authorsContacts": {
          auth: false,
        },
      },
    }));

Теперь давайте поступим правильно и создадим дочерний преобразователь для извлечения статей, связанных с автором. Таким образом, если мы не будем запрашивать «статьи» в запросе, мы не будем извлекать данные, как в нашем предыдущем примере.

Давайте определим тип AuthorsArticles и обязательно добавим его в тип AuthorContact:

type AuthorsArticles {
      id: ID
      title: String
      slug: String
      description: String
    }
    
    type AuthorContact {
      id: ID
      name: String
      email: String
      articles: [AuthorsArticles]
    }

Теперь давайте создадим наш дочерний преобразователь для получения всех статей, связанных с автором:

AuthorContact: {
          articles: {
            resolve: async (parent, args, context) => {
    
              console.log("#############", parent.id, "#############");
    
              const data = await strapi.services["api::article.article"].find({
                filters: { author: parent.id },
              });
    
              return data.results.map((article) => ({
                id: article.id,
                title: article.title,
                slug: article.slug,
                description: article.description,
              }));
    
            },
          },
        },

Наш готовый код должен выглядеть так:

"use strict";
    const boostrap = require("./bootstrap");
    
    module.exports = {
      async bootstrap() {
        await boostrap();
      },
    
      register({ strapi }) {
        const extensionService = strapi.service("plugin::graphql.extension");
    
        // Overriding the default article GraphQL resolver
        extensionService.use(({ strapi }) => ({
          typeDefs: `
            type Query {
              article(slug: String!): 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 },
                  });
    
                  const response = toEntityResponse(data.results[0]);
    
                  console.log("##################", response, "##################");
    
                  return response;
                },
              },
            },
          },
        }));
    
        // Custom query resolver to get all authors and their details.
        extensionService.use(({ strapi }) => ({
          typeDefs: `
    
            type Query {
              authorsContacts: [AuthorContact]
            }
    
            type AuthorsArticles {
              id: ID
              title: String
              slug: String
              description: String
            }
    
            type AuthorContact {
              id: ID
              name: String
              email: String
              articles: [AuthorsArticles]
            }
    
          `,
    
          resolvers: {
            Query: {
              authorsContacts: {
                resolve: async (parent, args, context) => {
                  const data = await strapi.services["api::writer.writer"].find();
    
                  return data.results.map((author) => ({
                    id: author.id,
                    name: author.name,
                    email: author.email,
                  }));
                },
              },
            },
    
            AuthorContact: {
              articles: {
                resolve: async (parent, args, context) => {
    
                  console.log("#############", parent.id, "#############");
    
                  const data = await strapi.services["api::article.article"].find({
                    filters: { author: parent.id },
                  });
    
                  return data.results.map((article) => ({
                    id: article.id,
                    title: article.title,
                    slug: article.slug,
                    description: article.description,
                  }));
                },
              },
            },
          },
    
          resolversConfig: {
            "Query.authorsContacts": {
              auth: false,
            },
          },
        }));
      },
    };

Теперь у нас есть отдельный преобразователь для получения articles, связанных с автором. Идите вперед и запустите этот запрос:

query {
      authorsContacts {
        id
        name
        email
        articles {
          id
          title
          description
          slug
        }
      }
    }

Подводя итог, при работе с GraphQL вы должны создать преобразователь для каждого связанного элемента, который вы хотите заполнить. Окончательный код на GitHub Надеюсь, вам понравилось это введение в основы расширения и создания пользовательских распознавателей с помощью GralhQL в Strapi v4.

Заключение

Как видите, Strapi предоставляет очень гибкую среду, которую можно использовать для создания полнофункционального API контента за считанные минуты. Кроме того, Strapi позволяет полностью контролировать API и систему. Независимо от того, хотите ли вы создать простую безголовую систему контента для своего блога или полностью централизовать информацию о продуктах электронной коммерции, Strapi предлагает надежную серверную часть.

Я надеюсь, что вы нашли этот урок полезным. Если у вас есть дополнительные вопросы, присоединяйтесь к нам в нашем сообществе Discord, где вы можете задавать вопросы или помогать другим участникам с их.