FILEEM

POWER OF DREAM

如何将自定义 GraphQL 查询添加到 Strapi V4

原文在:https://www.theitsolutions.io/blog/how-to-add-custom-graphql-query-to-strapi-v4
翻译:
在 Strapi V4 中设置新查询、定义新类型或重用来自其他内容类型的现有类型时,您是否遇到过困难?您在使用 GraphQL 时是否对可用的自定义感到困惑?让我们帮助您并展示我们如何在我们的一个项目中解决这个问题!

虽然 Strapi 自己的文档有助于添加 GraphQL 支持,但当有人想要使用 GraphQL 时,它也会混淆哪些自定义可用。由于我们的项目使用的是 Strapi v4,与 v3 相比,我们必须学习如何创建此类查询。

官方 GraphQL 文档的自定义部分下的第一个示例是在没有提供太多上下文的情况下展示功能。该示例 – 在一些 ShadowCRUD 设置旁边 – 创建了两个对象类型(书籍和文章),一个自定义Nexus插件,覆盖现有内容类型(地址)的查询解析器并禁用相同内容类型(Query.address)的身份验证。

对我来说,很难理解如何设置新查询定义新类型以及如何重用来自其他内容类型的现有类型。由于我们的项目使用的是 Strapi v4,与 v3 相比,我们必须学习如何创建此类查询。

最终,我们可以探索文档、Strapi 代码库和官方插件(如用户和权限插件)的示例,并得出一种有效添加 GraphQL 自定义的方法。

这篇文章展示了我们的调查结果,旨在帮助需要使用 Strapi v4 实现类似目标的开发人员。

示例的域

假设我们正在开发一个销售产品的电子商务网站。有一天,我们想添加一项功能,以使用第三方解决方案跟踪每种产品的受欢迎程度。为了在服务器端保密 API 密钥,我们计划为 Strapi 提供的现有 GraphQL 模式定义一个新的 GraphQL 查询。

此查询将处理与第 3 方 API 的集成,并在请求时可选择返回产品数据。

初始化项目

为了简单起见,我们使用官方指南为本示例设置了一个新的 Strapi 项目,因此使用命令行中的 npx:

npx create-strapi-app@latest graphql-example --quickstart

创建初始管理帐户后,我们可以使用另一个终端窗口继续。

定义产品内容类型

为了添加Product具有 API 和单个属性的内容类型name,您可以使用命令行界面或启动的管理 Web 界面。

我将使用 CLI,以便包含它的输出。

 npm run strapi generate content-type
? Content type display name Product
? Content type singular name product
? Content type plural name products
? Please choose the model type Collection Type
? Use draft and publish? No
? Do you want to add attributes? Yes
? Name of attribute name
? What type of attribute string
? Do you want to add another attribute? No
? Where do you want to add this model? Add model to new API
? Name of the new API? product
? Bootstrap API related files? Yes
✔  ++ /api/product/content-types/product/schema.json
✔  +- /api/product/content-types/product/schema.json
✔  ++ /api/product/controllers/product.js
✔  ++ /api/product/services/product.js
✔  ++ /api/product/routes/product.js

添加 GraphQL 插件

Strapi 快速入门项目不包含 GraphQL 插件,因此我们必须安装它。您可以按照插件的文档进行操作,但是,运行此命令同样简单:

“` npm run strapi install graphql “`
当您的 Strapi 应用程序重新启动时,您可以通过 http://localhost:1337/graphql 访问 GraphQL 游乐场

让我们使用以下查询检查为内容类型创建的产品查询:

query Products {
  products {
    data {
      id
      attributes {
        name
        createdAt
        updatedAt
      }
    }
  }
}

第一次尝试失败,因为没有为未经身份验证的用户设置权限。

在 admin web 界面的 admin //位置中的角色添加内容类型find和findOne权限后,上面的查询应该返回空列表。ProductPublicSettingsUsers & Permissions PluginRolesPublic

如果您想查看一些结果,请手动添加新产品或(在提供角色create权限后Public)使用此 GraphQL 突变:

mutation CreateProduct {
  createProduct(data: {
    name: "The product"
  }) {
    data {
      id
    }
  }
} 

准备添加自定义 GraphQL 查询

我们可以在 Strapi 的生命周期钩子中为 GraphQL 插件添加自定义,该register钩子位于./src/index.js. 在一个相当大的项目中,这个文件可以包含很多代码,所以为了明确定义 GraphQL 相关代码的位置,我们将在./src/graphql.js. 这将负责收集自定义 GraphQL 查询和突变并将它们添加到 Strapi。

在我们更改之后,文件看起来像这样。

./src/index.js
'use strict';

const graphql = require('./graphql')

module.exports = {

  register({ strapi }) {
    graphql(strapi) // externalize all graphql related code to ./src/graphql.js
  },

  bootstrap(/*{ strapi }*/) {},
}
./src/graphql.js
'use strict';

const noop = (strapi) => ({nexus}) => ({})

const extensions = [noop]

module.exports = (strapi) => {
  const extensionService = strapi.plugin('graphql').service('extension')

  for (const extension of extensions) {
    extensionService.use(extension(strapi))
  }
}

稍后我们将用扩展替换noop扩展popularity,这也将类似于noop,即:它将是一个strapi作为参数的函数,返回另一个带有参数的函数,该函数有一个nexus字段并返回扩展定义。

从长远来看,这个复杂的功能是有帮助的,正如strapi我们的代码提供的那样,但nexus由 GraphQL 插件的extension服务提供。两者都可以在我们的扩展代码中用于访问其他 Strapi 服务、插件和使用nexus api。

使用 Nexus API

是时候定义我们的popularity扩展了。我会将noop扩展名复制到.src/api/popularity/graphql.js文件中,并将此非工作定义包含到扩展名中。

./src/api/popularity/graphql.js
'use strict';

module.exports = (strapi) => ({nexus}) => ({})
./src/graphql.js
'use strict';

const popularity = require('./api/popularity/graphql')
const extensions = [popularity]

module.exports = (strapi) => {
  const extensionService = strapi.plugin('graphql').service('extension')

  for (const extension of extensions) {
    extensionService.use(extension(strapi))
  }
}

如您所见,我已选择导入新模块并将其添加到扩展列表中。缺点是每当我们添加另一个 GraphQL 查询时,我们都必须手动将其添加到扩展列表中,否则将无法识别。

让我们继续实际的扩展,填写popularityGraphQL 定义。我们的第一种方法是使用nexus api。

.src/api/popularity/graphql.js
'use strict';

module.exports = (strapi) => ({nexus}) => ({
  types: [
    nexus.extendType({
      type: 'Query', // extending Query
      definition(t) {
        t.field('popularity', { // with this field
          type: 'PopularityResponse', // which has this custom type (defined below)
          args: {
            product: nexus.nonNull('ID') // and accepts a product id as argument
          },
          resolve: async (parent, args, context) => ({ // and resolves to the 3rd party popularity service logic
            stars: Math.floor(Math.random() * 5) + 1, // a simple mocked response
            product: args.product
          })
        })
      }
    }),
    nexus.objectType({
      name: 'PopularityResponse', // this is our custom object type
      definition(t) {
        t.nonNull.int('stars') // with a result of the 3rd party popularity service response

        t.field('product', { // and if requested, a field to query the product content type
          type: 'ProductEntityResponse',
          resolve: async (parent, args) => ({ // where we provide the resolver as Strapi does not know about relations of our new PopularityResponse type
            value: await strapi.entityService.findOne('api::product.product', parent.product, args)
          }) // but relations, components and dynamic zones of the Product will be handled by built-in resolvers
        })
      }
    }),
  ],
  resolversConfig: {
    'Query.popularity': {
      auth: {
        scope: ['api::product.product.findOne'] // we give permission to use the new query for those who has findOne permission of Product content type
      }
    }
  }
})

我已经包含了对 3rd 方调用的模拟响应,并让读者详细说明如何继续实施与任何 3rd 方或自定义逻辑的集成。

现在,您可以使用以下查询在 GraphQL Playground 中检查结果。

query GetPopularity {
  popularity(product: "1") {
    stars
    product {
      data {
        id
        attributes {
          name
        }
      }
    }
  }
}

这给出了结果:

{
  "data": {
    "popularity": {
      "stars": 3,
      "product": {
        "data": {
          "id": "1",
          "attributes": {
            "name": "The product"
          }
        }
      }
    }
  }
}

所以我们的实现按预期工作。

对我们来说,最困难的问题将自定义响应类型( PopularityResponse) 和Product 内容类型的解析器联系起来。最后一个技巧是为直接关系(product示例中的字段)提供解析器,并以 Strapi 的 graphql 插件接受的格式(带有value字段的对象)返回结果。此信息来自graphql 插件源代码,这意味着使用此技巧被认为是对 graphql 插件内部的依赖,并且可能会在升级期间中断。

使用 GraphQL 模式

从 Strapi v4.0.8 开始,ProductResponseEntity可以从 Strapi 中的 GraphQL SDL 类型定义中引用。如果您有兴趣,这里是相同的定义,但使用 GraphQL 模式并避免使用 nexus API。

.src/api/popularity/graphql.js
'use strict';

module.exports = (strapi) => ({nexus}) => ({
  typeDefs: `
    type PopularityResponse {
      stars: Int!
      product: ProductEntityResponse
    }

    extend type Query {
      popularity(product: ID!): PopularityResponse
    }
  `,
  resolvers: {
    Query: {
      popularity: {
        resolve: async (parent, args, context) => ({
          stars: Math.floor(Math.random() * 5) + 1,
          product: args.product
        })
      }
    },
    PopularityResponse: {
      product: {
        resolve: async (parent, args) => ({
          value: await strapi.entityService.findOne('api::product.product', parent.product, args)
        })
      }
    }
  },
  resolversConfig: {
    'Query.popularity': {
      auth: {
        scope: ['api::product.product.findOne']
      }
    }
  }
})

就个人而言,我更喜欢 GraphQL SDL 变体,因为它更紧凑,并且大多数开发人员都熟悉它。如果我们在 SDL 定义中犯了错误,它会在启动 Strapi 时失败,这在我们使用 Nexus API 时通常是相同的。

对于这两种情况,resolversConfig都可以添加授权、中间件和策略的配置。虽然可以覆盖现有的查询和突变resolversConfig,但您应该记住,活动resolversConfig将是合并 Strapi 的默认解析器配置和您的设置的结果。这使得解析器的配置覆盖依赖于原始设置和合并过程(两者都可以随着 Strapi 升级而改变)。

结论

正如我们所见,将自定义查询添加到 Strapi 并不难。但是,关于 GraphQL,Strapi v3 和 v4 之间存在重大差异。

我希望上面的示例可以节省您在 Strapi v4 中寻找实现 GraphQL 查询的方法所花费的时间。

点赞