后端定制

Routing 路由

./api/**/config/routes.json 文件定义客户机的所有可用端点.

默认情况下,Strapi 为所有的 Content Types 生成端点,更多内容参考 Content API

如何创建一条路由?

您必须在您的一个 api 文件夹中编辑 routes.json 文件(./api/**/config/routes.json)并手动向 routes 数组中添加一个新的 route array 对象。

Path — ./api/**/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": []
  9. }
  10. },
  11. {
  12. "method": "PUT",
  13. "path": "/restaurants/bulkUpdate",
  14. "handler": "Restaurant.bulkUpdate",
  15. "config": {
  16. "policies": []
  17. }
  18. },
  19. {
  20. "method": "POST",
  21. "path": "/restaurants/:id/reservation",
  22. "handler": "Restaurant.reservation",
  23. "config": {
  24. "policies": ["isAuthenticated", "hasCreditCard"]
  25. }
  26. }
  27. ]
  28. }
  • method (string): 命中路由的方法的方法或数组 (e.g. GET, POST, PUT, HEAD, DELETE, PATCH).
  • path (string): 启动路径 URL / (e.g. /restaurants).
  • handler (string): 当按照此语法命中路由时执行的操作 <Controller>.<action>.
  • config
    • policies (array): 策略名称或路径的数组 (更多查看)

::: tip 如果你不想让 用户&权限插件 检查路由,你可以排除整个 config 对象。 :::

动态参数

Strapi 使用的路由器允许您创建可以使用参数和简单正则表达式的动态路由。这些参数将在 ctx.params 对象中公开。有关更多细节,请参考 PathToRegex 文档。

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants/:category/:id",
  6. "handler": "Restaurant.findOneByCategory",
  7. "config": {
  8. "policies": []
  9. }
  10. },
  11. {
  12. "method": "GET",
  13. "path": "/restaurants/:region(\\d{2}|\\d{3})/:id", // Only match when the first parameter contains 2 or 3 digits.
  14. "handler": "Restaurant.findOneByRegion",
  15. "config": {
  16. "policies": []
  17. }
  18. }
  19. ]
  20. }

例子

带有 URL 参数的路由定义

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants/:id",
  6. "handler": "Restaurant.findOne",
  7. "config": {
  8. "policies": []
  9. }
  10. }
  11. ]
  12. }

获取控制器中的 URL 参数

  1. module.exports = {
  2. findOne: async ctx => {
  3. // const id = ctx.params.id;
  4. const { id } = ctx.params;
  5. return id;
  6. },
  7. };

Policies 策略

策略是能够在每个请求到达控制器的操作之前对其执行特定逻辑的函数。它们主要用于简单地保护业务逻辑。项目的每个路由都可以关联到一组策略。例如,您可以创建名为 isAdmin 的策略,该策略显然检查请求是否由管理员用户发送,并将其用于关键路由。

策略定义在了 ./api/**/config/policies/ 目录中. 它们分别通过 strapi.api.**.config.policiesstrapi.plugins.**.config.policies 配置策略。全局策略定义在 ./config/policies/ ,可通过 strapi.config.policies 访问。

如何制定策略 policy?

有几种方法可以创建一个策略 policy.

  • 使用 CLI strapi generate:policy isAuthenticated.
    阅读 CLI 文章 了解详情.
  • 手动创建一个 JavaScript 文件,名字 isAuthenticated.js 放在 ./config/policies/ 目录下.

Path — ./config/policies/isAuthenticated.js.

  1. module.exports = async (ctx, next) => {
  2. if (ctx.state.user) {
  3. // Go to next policy or will reach the controller's action.
  4. return await next();
  5. }
  6. ctx.unauthorized(`You're not logged in!`);
  7. };

在这个示例中,我们正在验证一个会话是否打开。如果是这种情况,我们调用 next() 方法来执行下一个策略或控制器的操作。否则,返回一个 401 错误。

401 HTTP 状态码表示没有登录认证

用法

要将策略应用于路由,需要将一组策略与其关联。有两种策略: global 策略和 scoped 策略。

::: warning

要使用 GraphQL 应用策略,请参阅 以下指南。 :::

Global 策略

全局策略可以关联到项目中的任何路由。

Path — ./api/restaurant/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": ["global::isAuthenticated"]
  9. }
  10. }
  11. ]
  12. }

Restaurant.js 控制器中执行 find 操作之前,将调用位于 ./config/policies/isAuthenticated.js 中的全局策略 isAuthenticated

::: tip 您可以在这个数组中放置任意多的策略,但是要注意对性能的影响。 :::

插件策略

插件可以在你的应用程序中添加和公开策略。例如,插件 Users & Permissions 附带了有用的策略,以确保用户得到良好的身份验证或拥有执行操作的权限。

Path — ./api/restaurant/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": ["plugins::users-permissions.isAuthenticated"]
  9. }
  10. }
  11. ]
  12. }

位于 users-permissions 插件中的策略 isAuthenticated 将在 Restaurant.js 控制器中的 find 操作之前执行。

插件策略

API 策略可以与在 API 中定义的路由相关联,这些路由是在 API 中声明的。

Path — ./api/restaurant/config/policies/isAdmin.js.

  1. module.exports = async (ctx, next) => {
  2. if (ctx.state.user.role.name === 'Administrator') {
  3. // Go to next policy or will reach the controller's action.
  4. return await next();
  5. }
  6. ctx.unauthorized(`You're not allowed to perform this action!`);
  7. };

Path — ./api/restaurant/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": ["isAdmin"]
  9. }
  10. }
  11. ]
  12. }

位于 ./api/restaurant/config/policies/isAdmin.js 中的策略 isAdmin 将在 Restaurant.js 控制器中的 find 操作之前执行。

使用 api 之外的策略

要在另一个 api 中使用策略,可以使用以下语法引用它: {apiName}.{policyName}

Path — ./api/category/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/categories",
  6. "handler": "Category.find",
  7. "config": {
  8. "policies": ["restaurant.isAdmin"]
  9. }
  10. }
  11. ]
  12. }

高级使用

如上所述,策略在控制器的操作之前执行。它看起来像是在控制器动作 before 可以执行的动作。您还可以在 after

Path — ./config/policies/custom404.js.

  1. module.exports = async (ctx, next) => {
  2. // Indicate to the server to go to
  3. // the next policy or to the controller's action.
  4. await next();
  5. // The code below will be executed after the controller's action.
  6. if (ctx.status === 404) {
  7. ctx.body = 'We cannot find the resource.';
  8. }
  9. };

Controllers 控制器

控制器是 JavaScript 文件,其中包含一组方法,称为客户机根据请求的路由到达的动作。这意味着每次客户机请求路由时,操作都执行业务逻辑编码并发送回响应。它们在 MVC 模式中表示 c。在大多数情况下,控制器将包含项目的大部分业务逻辑。

  1. module.exports = {
  2. // GET /hello
  3. async index(ctx) {
  4. return 'Hello World!';
  5. },
  6. };

在这个例子中,任何时候浏览器指向应用程序上的 /hello URL,页面都会显示文本: Hello World!.

控制器定义在每个 ./api/**/controllers/ .放在这些文件夹中的每个 JavaScript 文件都将作为控制器加载。它们也可以通过 strapi.controllersstrapi.api.**.controllers 全局变量。

核心控制器

当您创建一个新的 Content Type 时,您将看到一个新的空控制器已被创建。这是因为 Strapi 默认为您的模型构建了一个通用控制器,并允许您在生成的文件中覆盖和扩展它。

扩展模型控制器

以下是核心方法(及其当前实现)。您只需将此代码复制并粘贴到自己的控制器文件中,即可自定义方法。

::: warning 在下面的示例中,我们将假设您的控制器、服务和模型命名为 restaurant :::

Utils 工具类

首先引入 ‘strapi-utils’ 包

  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  • parseMultipartData: 此函数解析 Strapi 的 formData 格式.
  • sanitizeEntity: 此函数从模型及其关系中删除所有私有字段.
Collection Type 集合类型

:::: tabs

::: tab find

find
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Retrieve records.
  5. *
  6. * @return {Array}
  7. */
  8. async find(ctx) {
  9. let entities;
  10. if (ctx.query._q) {
  11. entities = await strapi.services.restaurant.search(ctx.query);
  12. } else {
  13. entities = await strapi.services.restaurant.find(ctx.query);
  14. }
  15. return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.restaurant }));
  16. },
  17. };

:::

::: tab findOne

findOne
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Retrieve a record.
  5. *
  6. * @return {Object}
  7. */
  8. async findOne(ctx) {
  9. const { id } = ctx.params;
  10. const entity = await strapi.services.restaurant.findOne({ id });
  11. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  12. },
  13. };

:::

::: tab count

count
  1. module.exports = {
  2. /**
  3. * Count records.
  4. *
  5. * @return {Number}
  6. */
  7. count(ctx) {
  8. if (ctx.query._q) {
  9. return strapi.services.restaurant.countSearch(ctx.query);
  10. }
  11. return strapi.services.restaurant.count(ctx.query);
  12. },
  13. };

:::

::: tab create

create
  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Create a record.
  5. *
  6. * @return {Object}
  7. */
  8. async create(ctx) {
  9. let entity;
  10. if (ctx.is('multipart')) {
  11. const { data, files } = parseMultipartData(ctx);
  12. entity = await strapi.services.restaurant.create(data, { files });
  13. } else {
  14. entity = await strapi.services.restaurant.create(ctx.request.body);
  15. }
  16. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  17. },
  18. };

:::

::: tab update

update
  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Update a record.
  5. *
  6. * @return {Object}
  7. */
  8. async update(ctx) {
  9. const { id } = ctx.params;
  10. let entity;
  11. if (ctx.is('multipart')) {
  12. const { data, files } = parseMultipartData(ctx);
  13. entity = await strapi.services.restaurant.update({ id }, data, {
  14. files,
  15. });
  16. } else {
  17. entity = await strapi.services.restaurant.update({ id }, ctx.request.body);
  18. }
  19. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  20. },
  21. };

:::

::: tab delete

delete
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Delete a record.
  5. *
  6. * @return {Object}
  7. */
  8. async delete(ctx) {
  9. const { id } = ctx.params;
  10. const entity = await strapi.services.restaurant.delete({ id });
  11. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  12. },
  13. };

:::

::::

Single Type 单一类型

:::: tabs

::: tab find

find
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Retrieve the record.
  5. *
  6. * @return {Object}
  7. */
  8. async find(ctx) {
  9. const entity = await strapi.services.restaurant.find();
  10. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  11. },
  12. };

:::

::: tab update

update
  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Update the record.
  5. *
  6. * @return {Object}
  7. */
  8. async update(ctx) {
  9. let entity;
  10. if (ctx.is('multipart')) {
  11. const { data, files } = parseMultipartData(ctx);
  12. entity = await strapi.services.restaurant.createOrUpdate(data, {
  13. files,
  14. });
  15. } else {
  16. entity = await strapi.services.restaurant.createOrUpdate(ctx.request.body);
  17. }
  18. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  19. },
  20. };

:::

::: tab delete

delete
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Delete the record.
  5. *
  6. * @return {Object}
  7. */
  8. async delete(ctx) {
  9. const entity = await strapi.services.restaurant.delete();
  10. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  11. },
  12. };

:::

::::

自定义控制器

您还可以创建自定义控制器来构建自己的业务逻辑和 API Endpoint 端点。

创建控制器有两种方法:

  • 使用 CLI strapi generate:controller restaurant.
    阅读 CLI 文章 了解详情.
  • 手动创建 JavaScript 文件 ./api/**/controllers.

添加 Endpoints

每个控制器的操作必须是一个 async 异步函数。

每个操作都接收一个 context (ctx) 对象作为第一个参数,其中包含 request contextresponse context

例子

在这个例子中, 我们定义了一个路由 route 文件 ./api/hello/config/routes.json, 处理程序 Hello.index. 有关路由的详细信息,请参阅 Routing 文章

这意味着每次向服务器发送请求 GET /hello 时,Strapi 将调用 Hello.js 控制器中的 index 操作。我们的索引动作将返回 Hello World!. 您还可以返回一个 JSON 对象。

Path — ./api/hello/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/hello",
  6. "handler": "Hello.index",
  7. "config": {
  8. "policies": []
  9. }
  10. }
  11. ]
  12. }

Path — ./api/hello/controllers/Hello.js.

  1. module.exports = {
  2. // GET /hello
  3. async index(ctx) {
  4. ctx.send('Hello World!');
  5. },
  6. };

::: tip 路由处理程序只能访问 ./api/**/controllers 文件夹中定义的控制器。 :::

Requests & Responses 请求 & 回应

Requests 请求

上下文对象(ctx)包含所有与请求相关的信息,可以通过 ctx.request控制器策略 访问它们。

Strapi 通过 ctx.request.bodyctx.request.files 传递 body

有关更多信息,请参考 Koa 请求文档

Responses 回应

上下文对象(ctx)包含一系列用于管理服务器响应的值和函数。可以通过 ctx.responsecontrollerspolicies 访问它们。

详细信息, 访问 Koa response 文章.

Services 服务

服务是一组可重用的函数。它们在尊重 DRY (不要重复自己) 编程概念和简化 controllers 逻辑方面特别有用。

核心服务

当您创建一个新的 Content Type 或者一个新的模型时,您将看到一个新的空服务已经被创建。这是因为 Strapi 默认为您的模型构建了一个通用服务,并允许您在生成的文件中覆盖和扩展它。

扩展模型服务

以下是核心方法(及其当前实现)。您只需将此代码复制并粘贴到您自己的服务文件中,即可自定义方法。

你可以在 这里 阅读 strapi.query 调用。

::: tip 在下面的示例中,您的控制器、服务和模型被命名为 restaurant。 :::

Utils 工具函数

如果你正在扩展 create 或者 update 服务,首先需要下面的工具函数:

  1. const { isDraft } = require('strapi-utils').contentTypes;
  • isDraft: 此函数检查条目是否为草稿.
Collection Type 集合类型

:::: tabs

::: tab find

find
  1. module.exports = {
  2. /**
  3. * Promise to fetch all records
  4. *
  5. * @return {Promise}
  6. */
  7. find(params, populate) {
  8. return strapi.query('restaurant').find(params, populate);
  9. },
  10. };
  • params (object): 这表示用于查找请求的过滤器.
    对象遵循 URL 查询格式, 请参阅这份文件..
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "_limit": 20,
  7. "name_contains": "sushi"
  8. }
  • populate (array): 你必须提到你想要填充的数据 ["author", "author.name", "comment", "comment.content"]

:::

::: tab findOne

findOne
  1. module.exports = {
  2. /**
  3. * Promise to fetch record
  4. *
  5. * @return {Promise}
  6. */
  7. findOne(params, populate) {
  8. return strapi.query('restaurant').findOne(params, populate);
  9. },
  10. };
  • params (object): 这表示用于查找请求的过滤器.
    对象遵循 URL 查询格式, 请参阅这份文件..
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }
  • populate (array): 你必须提到你想要填充的数据 ["author", "author.name", "comment", "comment.content"]

:::

::: tab count

count
  1. module.exports = {
  2. /**
  3. * Promise to count record
  4. *
  5. * @return {Promise}
  6. */
  7. count(params) {
  8. return strapi.query('restaurant').count(params);
  9. },
  10. };
  • params (object): 这表示用于查找请求的过滤器.
    对象遵循 URL 查询格式, 请参阅这份文件..
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }

:::

::: tab create

create
  1. const { isDraft } = require('strapi-utils').contentTypes;
  2. module.exports = {
  3. /**
  4. * Promise to add record
  5. *
  6. * @return {Promise}
  7. */
  8. async create(data, { files } = {}) {
  9. const isDraft = isDraft(data, strapi.models.restaurant);
  10. const validData = await strapi.entityValidator.validateEntityCreation(
  11. strapi.models.restaurant,
  12. data,
  13. { isDraft }
  14. );
  15. const entry = await strapi.query('restaurant').create(validData);
  16. if (files) {
  17. // automatically uploads the files based on the entry and the model
  18. await strapi.entityService.uploadFiles(entry, files, {
  19. model: 'restaurant',
  20. // if you are using a plugin's model you will have to add the `source` key (source: 'users-permissions')
  21. });
  22. return this.findOne({ id: entry.id });
  23. }
  24. return entry;
  25. },
  26. };

:::

::: tab update

update
  1. const { isDraft } = require('strapi-utils').contentTypes;
  2. module.exports = {
  3. /**
  4. * Promise to edit record
  5. *
  6. * @return {Promise}
  7. */
  8. async update(params, data, { files } = {}) {
  9. const existingEntry = await db.query('restaurant').findOne(params);
  10. const isDraft = isDraft(existingEntry, strapi.models.restaurant);
  11. const validData = await strapi.entityValidator.validateEntityUpdate(
  12. strapi.models.restaurant,
  13. data,
  14. { isDraft }
  15. );
  16. const entry = await strapi.query('restaurant').update(params, validData);
  17. if (files) {
  18. // automatically uploads the files based on the entry and the model
  19. await strapi.entityService.uploadFiles(entry, files, {
  20. model: 'restaurant',
  21. // if you are using a plugin's model you will have to add the `source` key (source: 'users-permissions')
  22. });
  23. return this.findOne({ id: entry.id });
  24. }
  25. return entry;
  26. },
  27. };
  • params (object): it should look like this {id: 1}

:::

::: tab delete

delete
  1. module.exports = {
  2. /**
  3. * Promise to delete a record
  4. *
  5. * @return {Promise}
  6. */
  7. delete(params) {
  8. return strapi.query('restaurant').delete(params);
  9. },
  10. };
  • params (object): it should look like this {id: 1}

:::

::: tab search

search
  1. module.exports = {
  2. /**
  3. * Promise to search records
  4. *
  5. * @return {Promise}
  6. */
  7. search(params) {
  8. return strapi.query('restaurant').search(params);
  9. },
  10. };
  • params (object): 这表示用于查找请求的过滤器.
    对象遵循 URL 查询格式, 请参阅这份文件..
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }

:::

::: tab countSearch

countSearch
  1. module.exports = {
  2. /**
  3. * Promise to count searched records
  4. *
  5. * @return {Promise}
  6. */
  7. countSearch(params) {
  8. return strapi.query('restaurant').countSearch(params);
  9. },
  10. };
  • params (object): 这表示用于查找请求的过滤器.
    对象遵循 URL 查询格式, 请参阅这份文件..
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }

:::

::::

Single Type 单一类型

:::: tabs

::: tab find

find
  1. const _ = require('lodash');
  2. module.exports = {
  3. /**
  4. * Promise to fetch the record
  5. *
  6. * @return {Promise}
  7. */
  8. async find(populate) {
  9. const results = await strapi.query('restaurant').find({ _limit: 1 }, populate);
  10. return _.first(results) || null;
  11. },
  12. };
  • populate (array): 你必须提到你想要填充的数据 ["author", "author.name", "comment", "comment.content"]

:::

::: tab createOrUpdate

createOrUpdate
  1. const _ = require('lodash');
  2. module.exports = {
  3. /**
  4. * Promise to add/update the record
  5. *
  6. * @return {Promise}
  7. */
  8. async createOrUpdate(data, { files } = {}) {
  9. const results = await strapi.query('restaurant').find({ _limit: 1 });
  10. const entity = _.first(results) || null;
  11. let entry;
  12. if (!entity) {
  13. entry = await strapi.query('restaurant').create(data);
  14. } else {
  15. entry = await strapi.query('restaurant').update({ id: entity.id }, data);
  16. }
  17. if (files) {
  18. // automatically uploads the files based on the entry and the model
  19. await strapi.entityService.uploadFiles(entry, files, {
  20. model: 'restaurant',
  21. // if you are using a plugin's model you will have to add the `plugin` key (plugin: 'users-permissions')
  22. });
  23. return this.findOne({ id: entry.id });
  24. }
  25. return entry;
  26. },
  27. };

:::

::: tab delete

delete
  1. module.exports = {
  2. /**
  3. * Promise to delete a record
  4. *
  5. * @return {Promise}
  6. */
  7. delete() {
  8. const results = await strapi.query('restaurant').find({ _limit: 1 });
  9. const entity = _.first(results) || null;
  10. if (!entity) return;
  11. return strapi.query('restaurant').delete({id: entity.id});
  12. },
  13. };

:::

::::

自定义服务

您还可以创建自定义服务来构建自己的业务逻辑。

创建服务有两种方法。

  • 使用 CLI strapi generate:service restaurant.
    阅读 CLI 文章 了解详情.
  • 手动创建 JavaScript 文件 ./api/**/services/.

例子

服务的目标是存储可重用的函数。email 服务可以从我们的代码库中的不同功能发送电子邮件:

Path — ./api/email/services/Email.js.

  1. const nodemailer = require('nodemailer');
  2. // Create reusable transporter object using SMTP transport.
  3. const transporter = nodemailer.createTransport({
  4. service: 'Gmail',
  5. auth: {
  6. user: 'user@gmail.com',
  7. pass: 'password',
  8. },
  9. });
  10. module.exports = {
  11. send: (from, to, subject, text) => {
  12. // Setup e-mail data.
  13. const options = {
  14. from,
  15. to,
  16. subject,
  17. text,
  18. };
  19. // Return a promise of the function that sends the email.
  20. return transporter.sendMail(options);
  21. },
  22. };

::: tip 请确保为此示例安装了 nodemailer (npm install nodemailer)。 :::

现在可以通过 strapi.services 全局变量使用该服务。我们可以在代码库的另一部分使用它。例如下面这个控制器:

Path — ./api/user/controllers/User.js.

  1. module.exports = {
  2. // GET /hello
  3. signup: async ctx => {
  4. // Store the new user in database.
  5. const user = await User.create(ctx.query);
  6. // Send an email to validate his subscriptions.
  7. strapi.services.email.send('welcome@mysite.com', user.email, 'Welcome', '...');
  8. // Send response to the server.
  9. ctx.send({
  10. ok: true,
  11. });
  12. },
  13. };

Queries 查询

Strapi 提供了一个实用函数 strapi.query 来进行数据库查询。

您只需调用 strapi.query('modelName', 'pluginName') 就可以访问任何模型的查询 API。

这些查询为您处理特定的 Strapi 功能,如 components, dynamic zones, filterssearch

API 参考

:::: tabs

::: tab find

find

此方法返回匹配 Strapi 筛选器的条目列表。您还可以传递一个 populate 选项来指定要填充的关系。

例子

Find by id:

  1. strapi.query('restaurant').find({ id: 1 });

Find by in IN, with a limit:

  1. strapi.query('restaurant').find({ _limit: 10, id_in: [1, 2] });

Find by date orderBy name:

  1. strapi.query('restaurant').find({ date_gt: '2019-01-01T00:00:00Z', _sort: 'name:desc' });

Find by id not in and populate a relation. Skip the first ten results

  1. strapi.query('restaurant').find({ id_nin: [1], _start: 10 }, ['category', 'category.name']);

:::

::: tab findOne

findOne

此方法返回与某些基本参数匹配的第一个条目。您还可以传递一个 populate 选项来指定要填充的关系。

例子

Find one by id:

  1. strapi.query('restaurant').findOne({ id: 1 });

Find one by name:

  1. strapi.query('restaurant').findOne({ name: 'restaurant name' });

Find one by name and creation_date:

  1. strapi.query('restaurant').findOne({ name: 'restaurant name', date: '2019-01-01T00:00:00Z' });

Find one by id and populate a relation

  1. strapi.query('restaurant').findOne({ id: 1 }, ['category', 'category.name']);

:::

::: tab create

create

在数据库中创建一个条目并返回该条目。

例子
  1. strapi.query('restaurant').create({
  2. name: 'restaurant name',
  3. // this is a dynamiczone 字段 the order is persisted in db.
  4. content: [
  5. {
  6. __component: 'blog.rich-text',
  7. title: 'Some title',
  8. subTitle: 'Some sub title',
  9. },
  10. {
  11. __component: 'blog.quote',
  12. quote: 'Some interesting quote',
  13. author: 1,
  14. },
  15. ],
  16. // this is a component 字段 the order is persisted in db.
  17. opening_hours: [
  18. {
  19. day_interval: 'Mon',
  20. opening_hour: '7:00 PM',
  21. closing_hour: '11:00 PM',
  22. },
  23. {
  24. day_interval: 'Tue',
  25. opening_hour: '7:00 PM',
  26. closing_hour: '11:00 PM',
  27. },
  28. ],
  29. // pass the id of a media to link it to the entry
  30. cover: 1,
  31. // automatically creates the relations when passing the ids in the field
  32. reviews: [1, 2, 3],
  33. });

:::

::: tab update

update

更新数据库中的条目并返回该条目。

例子

Update by id

  1. strapi.query('restaurant').update(
  2. { id: 1 },
  3. {
  4. name: 'restaurant name',
  5. content: [
  6. {
  7. __component: 'blog.rich-text',
  8. title: 'Some title',
  9. subTitle: 'Some sub title',
  10. },
  11. {
  12. __component: 'blog.quote',
  13. quote: 'Some interesting quote',
  14. author: 1,
  15. },
  16. ],
  17. opening_hours: [
  18. {
  19. day_interval: 'Mon',
  20. opening_hour: '7:00 PM',
  21. closing_hour: '11:00 PM',
  22. },
  23. {
  24. day_interval: 'Tue',
  25. opening_hour: '7:00 PM',
  26. closing_hour: '11:00 PM',
  27. },
  28. ],
  29. // pass the id of a media to link it to the entry
  30. cover: 1,
  31. // automatically creates the relations when passing the ids in the field
  32. reviews: [1, 2, 3],
  33. }
  34. );

在使用组件或动态区域更新条目时,请注意,如果发送的组件没有任何 id,则前面的组件将被删除和替换。你可以通过发送它们的 id 和其他字段来更新组件:

通过 id 进行更新并更新以前的组件

  1. strapi.query('restaurant').update(
  2. { id: 1 },
  3. {
  4. name: 'Mytitle',
  5. content: [
  6. {
  7. __component: 'blog.rich-text',
  8. id: 1,
  9. title: 'Some title',
  10. subTitle: 'Some sub title',
  11. },
  12. {
  13. __component: 'blog.quote',
  14. id: 1,
  15. quote: 'Some interesting quote',
  16. author: 1,
  17. },
  18. ],
  19. opening_hours: [
  20. {
  21. id: 2,
  22. day_interval: 'Mon',
  23. opening_hour: '7:00 PM',
  24. closing_hour: '11:00 PM',
  25. },
  26. {
  27. id: 1,
  28. day_interval: 'Tue',
  29. opening_hour: '7:00 PM',
  30. closing_hour: '11:00 PM',
  31. },
  32. ],
  33. // pass the id of a media to link it to the entry
  34. cover: 1,
  35. // automatically creates the relations when passing the ids in the field
  36. reviews: [1, 2, 3],
  37. }
  38. );

按名称部分更新

  1. strapi.query('restaurant').update(
  2. { title: 'specific name' },
  3. {
  4. title: 'restaurant name',
  5. }
  6. );

:::

::: tab delete

delete

在删除之前删除一个条目并返回其值。您可以使用传递的参数一次删除多个条目。

例子

按 id 删除一个

  1. strapi.query('restaurant').delete({ id: 1 });

按字段删除多个

  1. strapi.query('restaurant').delete({ district: '_18th' });

:::

::: tab count

count

返回匹配 Strapi 筛选器的条目数。

例子

按地区计算

  1. strapi.query('restaurant').count({ district: '_1st' });

按名称计数包含

  1. strapi.query('restaurant').count({ name_contains: 'food' });

按日期计数小于

  1. strapi.query('restaurant').count({ date_lt: '2019-08-01T00:00:00Z' });

:::

::: tab search

search

基于对所有字段的搜索返回条目(此特性将返回 sqlite 中的所有条目)。

例子

首先从 20 开始搜索 10

  1. strapi.query('restaurant').search({ _q: 'my search query', _limit: 10, _start: 20 });

搜索和排序

  1. strapi.query('restaurant').search({ _q: 'my search query', _limit: 100, _sort: 'date:desc' });

:::

::: tab countSearch

countSearch

返回基于搜索的条目总数(此特性将返回 sqlite 上的所有条目)。

例子
  1. strapi.query('restaurant').countSearch({ _q: 'my search query' });

:::

::::

自定义查询

当您希望自定义服务或创建新服务时,必须使用底层 ORM 模型构建查询。

要访问底层模型:

  1. strapi.query(modelName, plugin).model;

然后您可以在模型上运行任何可用的查询。有关更多细节,您应该参考特定 ORM 文档:

:::: tabs

::: tab SQL

Bookshelf

文档: https://bookshelfjs.org/

例子

  1. const result = await strapi
  2. .query('restaurant')
  3. .model.query(qb => {
  4. qb.where('id', 1);
  5. })
  6. .fetch();
  7. const fields = result.toJSON();

Knex

可以用 Knex.js 直接对数据库进行构建和自定义查询。

文档: http://knexjs.org/#Builder

您可以使用以下命令访问 Knex 实例:

  1. const knex = strapi.connections.default;

然后可以使用 Knex 构建自己的定制查询。您将失去模型的所有功能,但是如果您正在构建一个更加自定义的模式,这可能会很方便。请注意,如果您使用的是 draft system ,那么在发布 Draft 列之前,Strapi 将使其为空。

例子

  1. const _ = require('lodash');
  2. const knex = strapi.connections.default;
  3. const result = await knex('restaurants')
  4. .where('cities', 'berlin')
  5. .whereNot('cities.published_at', null)
  6. .join('chefs', 'restaurants.id', 'chefs.restaurant_id')
  7. .select('restaurants.name as restaurant')
  8. .select('chef.name as chef')
  9. // Lodash's groupBy method can be used to
  10. // return a grouped key-value object generated from
  11. // the response
  12. return (_.groupBy(result, 'chef');

我们强烈建议在对 DB 进行查询之前对任何字符串进行净化。

千万不要试图对直接来自前端的数据进行原始查询; 如果要查找 raw 原始查询,请参阅文档的 这一部分

:::

::: tab MongoDB

Mongoose

文档: https://mongoosejs.com/

例子

  1. const result = strapi.query('restaurant').model.find({
  2. date: { $gte: '2019-01-01T00.00.00Z' },
  3. });
  4. const fields = result.map(entry => entry.toObject());

:::

::::

Models 模型

概念

内容类型的模型

模型是数据库结构的表示。它们被分成两个独立的文件。一个包含模型选项(例如: lifecycle hooks)的 JavaScript 文件,以及一个表示存储在数据库中的数据结构的 JSON 文件。

Path — ./api/restaurant/models/Restaurant.js.

  1. module.exports = {
  2. lifecycles: {
  3. // Called before an entry is created
  4. beforeCreate(data) {},
  5. // Called after an entry is created
  6. afterCreate(result) {},
  7. },
  8. };

Path — ./api/restaurant/models/Restaurant.settings.json.

  1. {
  2. "kind": "collectionType",
  3. "connection": "default",
  4. "info": {
  5. "name": "restaurant",
  6. "description": "This represents the Restaurant Model"
  7. },
  8. "attributes": {
  9. "cover": {
  10. "collection": "file",
  11. "via": "related",
  12. "plugin": "upload"
  13. },
  14. "name": {
  15. "default": "",
  16. "type": "string"
  17. },
  18. "description": {
  19. "default": "",
  20. "type": "text"
  21. }
  22. }
  23. }

在这个示例中,有一个 Restaurant 模型,其中包含了 covernamedescription 属性。

组件的模型

另一种类型的模型是命名 components 。组件是一种数据结构,可以在一个或多个其他 API 模型中使用。没有与生命周期相关的,只有一个 JSON 文件定义。

Path — ./components/default/simple.json

  1. {
  2. "connection": "default",
  3. "collectionName": "components_default_simples",
  4. "info": {
  5. "name": "simple",
  6. "icon": "arrow-circle-right"
  7. },
  8. "options": {},
  9. "attributes": {
  10. "name": {
  11. "type": "string"
  12. }
  13. }
  14. }

在这个示例中,有一个包含属性 nameSimple 组件。并且组件是 default 类别。

模型在哪里定义?

Content Types 内容类型模型在每个中定义 ./api/**/models/ 这些文件夹中的每个 JavaScript 或 JSON 文件都将作为模型加载。它们也可以通过 strapi.modelsstrapi.api.**.models 建立全局变量模型。在项目的任何地方都可以使用,它们包含它们所引用的 ORM 模型对象。按照惯例,模型的名称应该用小写字母书写。

Components 模型定义在 ./components 文件夹。每个组件都必须位于子文件夹(组件的类别名称)中。

如何创建一个模型?

::: tip 如果您刚刚开始,在管理界面中直接使用 Content Type Builder 生成一些模型是非常方便的。然后,您可以在代码级别查看生成的模型映射。用户界面接管了许多验证任务,让您对可用的特性有一种感觉。 :::

对于内容类型模型

使用 CLI 并运行以下命令 strapi generate:model restaurant name:string description:text

阅读 CLI 文章 了解详情。

这将创建两个位于 ./api/restaurant/models 的文件:

  • Restaurant.settings.json: 包含属性和设置列表。 JSON 格式使文件易于编辑.
  • Restaurant.js: 导入 Restaurant.settings.json 并通过附加设置和生命周期回调对其进行扩展.

::: tip 当您使用 CLI (strapi generate:api <name>)创建一个新的 API 时,会自动创建一个模型。 :::

对于组件模型

要创建组件,必须使用管理面板中的 Content Type Builder,因为没有组件的 cli 生成器。

或者,您可以通过遵循前面描述的文件路径并遵循下面描述的文件结构来手动创建组件。

模型设置

可以在模型上设置其他设置:

  • kind (string) - 定义模型是否为集合类型 (collectionType) 的单一类别 (singleType) - 只适用于内容类型
  • connection (string) - 必须使用的连接名称. 默认值: default.
  • collectionName (string) - 存储数据的集合名称(或表名称).
  • globalId (string) - 此模型的全局变量名 (case-sensitive) - 只适用于内容类型
  • attributes (object) - 定义模型的数据结构. 查找可用的选项 below.

Path — Restaurant.settings.json.

  1. {
  2. "kind": "collectionType",
  3. "connection": "mongo",
  4. "collectionName": "Restaurants_v1",
  5. "globalId": "Restaurants",
  6. "attributes": {}
  7. }

在本例中,可以通过 Restaurants 全局变量访问模型 Restaurant。数据将存储在 Restaurants_v1 mongo 集合或表中,模型将使用 ./config/database.js

::: warning 如果没有在 JSON 文件中手动设置,Strapi 将采用文件名 globalIdglobalId 作为关系和 Strapi api 中模型的参考。如果您选择重命名它(通过重命名文件或更改 globalId 的值) ,则必须手动迁移表并更新引用。请注意,您不应该更改 Strapi 的模型 globalId (插件和核心模型) ,因为它们直接用于 Strapi api 和其他模型的关系中。 :::

::: tip 可以随时更改 connection 值,但是应该注意,没有自动数据迁移过程。另外,如果新连接不使用相同的 ORM,那么您将不得不重写查询。 :::

模型资料

model-json 表示关于模型的信息。当显示模型时,此信息用于管理界面。

  • name: 模型的名称,如管理界面中所示.
  • description: 模型的描述.
  • icon: fontawesome V5 的名字 - 只适用于组件

Path — Restaurant.settings.json.

  1. {
  2. "info": {
  3. "name": "restaurant",
  4. "description": ""
  5. }
  6. }

模型选项

模型上的选项键 model-json 状态。

  • timestamps: 时间戳, 这告诉模型使用哪些属性作为时间戳。接受 boolean 或字符串 Array ,其中第一个元素为 create date,第二个元素为 update date。对于 Bookshelf 设置为 true 时的默认值是 ["created_at", "updated_at"] ,对于 MongoDB 设置为 ["createdAt", "updatedAt"]
  • privateAttributes: 这种配置允许将一组属性视为私有属性,即使它们实际上没有在模型中定义为属性。接受字符串 Array 。当使用 MongoDB 时,它可以用来从 API 响应中删除时间戳或 _v 。模型中定义的 privateAttributes 集与全局 Strapi 配置中定义的 privateAttributes 合并。
  • populateCreatorFields: 配置 API 响应是否应该包含 created_byupdated_by 字段。接受一个布尔值。默认值为 false
  • draftAndPublish: 启用草稿和发布特性。接受一个 boolean。默认值为 false

Path — Restaurant.settings.json.

  1. {
  2. "options": {
  3. "timestamps": true,
  4. "privateAttributes": ["id", "created_at"],
  5. "populateCreatorFields": true,
  6. "draftAndPublish": false
  7. }
  8. }

定义属性

目前有以下几种类型:

  • string
  • text
  • richtext
  • email
  • password
  • integer
  • biginteger
  • float
  • decimal
  • date
  • time
  • datetime
  • boolean
  • enumeration
  • json
  • uid

Validations 验证

您可以对属性应用基本的验证。以下支持的验证只有 MongoDB 数据库连接支持。如果使用 SQL 数据库,应该使用本地 SQL 约束来应用它们。

  • required (boolean) — 如果为真,则为此属性添加一个必需的验证器.
  • unique (boolean) — 是否在此属性上定义唯一索引.
  • index (boolean) — 在此属性上添加索引,这将创建一个 single field index 可以在后台运行. 只支持 MongoDB.
  • max (integer) — 检查值是否大于或等于给定的最大值.
  • min (integer) — 检查值是否小于或等于给定的最小值.

安全验证

为了改善开发人员在开发或使用管理面板时的体验,该框架通过以下这些“安全验证”来增强属性:

  • private (boolean) — 如果为真,属性将从服务器响应中删除. (这对隐藏敏感数据很有用).
  • configurable (boolean) - 如果属性为 false,则无法从 Content Type Builder 插件配置该属性.
  • autoPopulate (boolean) - 如果为 false,相关数据将不会在 REST 响应中填充。(这不会停止查询 GraphQL 上的关系数据)

例外

uid

  • targetField(string) — The value is the name of an attribute that has string of the text type.
  • options (string) — 该值是传递给 the underlying uid generator. 一个警告是 uid 必须遵守以下正则表达式 /^[A-Za-z0-9-_.~]*$.

例子

Path — Restaurant.settings.json.

  1. {
  2. ...
  3. "attributes": {
  4. "title": {
  5. "type": "string",
  6. "min": 3,
  7. "max": 99,
  8. "unique": true
  9. },
  10. "description": {
  11. "default": "My description",
  12. "type": "text",
  13. "required": true
  14. },
  15. "slug": {
  16. "type": "uid",
  17. "targetField": "title"
  18. }
  19. ...
  20. }
  21. }

Relations 关系

关系允许您在内容类型之间创建链接(关系)。

:::: tabs

::: tab One-Way

单向关系有助于将一个条目链接到另一个条目。但是,只能查询其中一个模型的链接项。

例子

A pet can be owned by someone (a user).

Path — ./api/pet/models/Pet.settings.json.

  1. {
  2. "attributes": {
  3. "owner": {
  4. "model": "user"
  5. }
  6. }
  7. }

例子

  1. // Create a pet
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/pets', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. owner: '5c151d9d5b1d55194d3209be', // The id of the user you want to link
  8. })
  9. );

:::

::: tab Many-way

多向关系有助于将一个条目链接到许多其他条目。但是,只能查询其中一个模型的链接项。

例子

A pet can be owned by many people (multiple users).

Path — ./api/pet/models/Pet.settings.json.

  1. {
  2. "attributes": {
  3. "owners": {
  4. "collection": "user"
  5. }
  6. }
  7. }

例子

  1. // Create a pet
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/pets', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. owners: ['5c151d9d5b1d55194d3209be', '5fc666a5bf16f48ed050ef5b'], // The id of the users you want to link
  8. })
  9. );

:::

::: tab One-to-One

当一个实体只能链接到另一个实体时,一对一关系非常有用。反之亦然

例子

A user can have one address. And this address is only related to this user.

Path — ./api/user/models/User.settings.json.

  1. {
  2. "attributes": {
  3. "address": {
  4. "model": "address",
  5. "via": "user"
  6. }
  7. }
  8. }

Path — ./api/address/models/Address.settings.json.

  1. {
  2. "attributes": {
  3. "user": {
  4. "model": "user"
  5. }
  6. }
  7. }

例子

  1. // Create an address
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/addresses', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. user: '5c151d9d5b1d55194d3209be', // The id of the user you want to link
  8. })
  9. );

:::

::: tab One-to-Many

当一个条目可以链接到另一个 Content Type 的多个条目时,一对多关系非常有用。另一个 Content Type 条目只能链接到一个条目。

例子

A user can have many articles, and an article can be related to only one user (author).

Path — ./api/user/models/User.settings.json.

  1. {
  2. "attributes": {
  3. "articles": {
  4. "collection": "article",
  5. "via": "author"
  6. }
  7. }
  8. }

Path — ./api/article/models/Article.settings.json.

  1. {
  2. "attributes": {
  3. "author": {
  4. "model": "user"
  5. }
  6. }
  7. }

例子

  1. // Create an article
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/articles', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. author: '5c151d9d5b1d55194d3209be', // The id of the user you want to link
  8. })
  9. );
  10. // Update an article
  11. const xhr = new XMLHttpRequest();
  12. xhr.open('PUT', '/users/5c151d9d5b1d55194d3209be', true);
  13. xhr.setRequestHeader('Content-Type', 'application/json');
  14. xhr.send(
  15. JSON.stringify({
  16. articles: ['5c151d51eb28fd19457189f6', '5c151d51eb28fd19457189f8'], // Set of ALL articles linked to the user (existing articles + new article or - removed article)
  17. })
  18. );

:::

::: tab Many-to-Many

当一个条目可以链接到另一个内容类型的多个条目时,多对多关系非常有用。另一个 Content Type 的条目可以链接到许多条目。

例子

A product can be related to many categories and a category can have many products.

Path — ./api/product/models/Product.settings.json.

  1. {
  2. "attributes": {
  3. "categories": {
  4. "collection": "category",
  5. "via": "products",
  6. "dominant": true,
  7. "collectionName": "products_categories__categories_products" // optional
  8. }
  9. }
  10. }

NOTE: (NoSQL databases only) The dominant key defines which table/collection should store the array that defines the relationship. Because there are no join tables in NoSQL, this key is required for NoSQL databases (e.g. MongoDB).

NOTE: (NoSQL databases only) The collectionName key defines the name of the join table. It has to be specified once, in the dominant attribute of the relation. If it is not specified, Strapi will use a generated default one. It is useful to define the name of the join table when the name generated by Strapi is too long for the database you use.

Path — ./api/category/models/Category.settings.json.

  1. {
  2. "attributes": {
  3. "products": {
  4. "collection": "product",
  5. "via": "categories"
  6. }
  7. }
  8. }

例子

  1. // Update a product
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('PUT', '/products/5c151d9d5b1d55194d3209be', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. categories: ['5c151d51eb28fd19457189f6', '5c151d51eb28fd19457189f8'], // Set of ALL categories linked to the product (existing categories + new category or - removed category).
  8. })
  9. );

:::

::: tab Polymorphic

当您不知道哪种类型的模型将与您的条目相关联,或者当您希望将不同类型的模型连接到一个模型时,多态关系是一种解决方案。常见的用例是可以与不同类型的模型(文章、产品、用户等)相关联的 Image 模型。

Single vs Many

Let’s stay with our Image model which might belong to a single Article or Product entry.

NOTE: In other words, it means that an Image entry can be associated to one entry. This entry can be a Article or Product entry.

Also our Image model might belong to many Article or Product entries.

NOTE: In other words, it means that an Article entry can relate to the same image as a Product entry.

Path — ./api/image/models/Image.settings.json.

  1. {
  2. "attributes": {
  3. "related": {
  4. "collection": "*",
  5. "filter": "field"
  6. }
  7. }
  8. }
Filter

filter 属性是可选的(但我们强烈建议每次都使用它)。如果提供了,则添加一个新的匹配级别来检索相关数据。

例如,Product 模型可能有两个与 Image 模型相关联的属性。为了区分哪些图像附加到封面字段和哪些图像附加到 pictures 字段,我们需要保存并提供给数据库。

Path — ./api/article/models/Product.settings.json.

  1. {
  2. "attributes": {
  3. "cover": {
  4. "model": "image",
  5. "via": "related"
  6. },
  7. "pictures": {
  8. "collection": "image",
  9. "via": "related"
  10. }
  11. }
  12. }

filter 属性的值是存储信息的列的名称。

例子

An Image model might belong to many Article models or Product models.

Path — ./api/image/models/Image.settings.json.

  1. {
  2. "attributes": {
  3. "related": {
  4. "collection": "*",
  5. "filter": "field"
  6. }
  7. }
  8. }

Path — ./api/article/models/Article.settings.json.

  1. {
  2. "attributes": {
  3. "avatar": {
  4. "model": "image",
  5. "via": "related"
  6. }
  7. }
  8. }

Path — ./api/article/models/Product.settings.json.

  1. {
  2. "attributes": {
  3. "pictures": {
  4. "collection": "image",
  5. "via": "related"
  6. }
  7. }
  8. }

:::

::::

Components 组件

组件字段允许您在内容类型和组件结构之间创建关系。

例子

让我们说,我们创建了一个 restaurant 类的 openinghours 组件。

Path — ./api/restaurant/models/Restaurant.settings.json.

  1. {
  2. "attributes": {
  3. "openinghours": {
  4. "type": "component",
  5. "repeatable": true,
  6. "component": "restaurant.openinghours"
  7. }
  8. }
  9. }
  • repeatable (boolean): Could be true or false that let you create a list of data.
  • component (string): It follows this format <category>.<componentName>.

:::: tabs

::: tab Create

Create a restaurant with non-repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('POST', '/restaurants', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghour: {
  7. opening_at: '10am',
  8. closing_at: '6pm',
  9. day: 'monday',
  10. },
  11. })
  12. );

Create a restaurant with repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('POST', '/restaurants', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghours: [
  7. {
  8. opening_at: '10am',
  9. closing_at: '6pm',
  10. day: 'monday',
  11. },
  12. {
  13. opening_at: '10am',
  14. closing_at: '6pm',
  15. day: 'tuesday',
  16. },
  17. ],
  18. })
  19. );

:::

::: tab Update

Update a restaurant with non-repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/1', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghour: {
  7. id: 1, // the ID of the entry
  8. opening_at: '11am',
  9. closing_at: '7pm',
  10. day: 'wednesday',
  11. },
  12. })
  13. );

Update a restaurant with repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghours: [
  7. {
  8. "id": 1 // the ID of the entry you want to update
  9. "opening_at": "10am",
  10. "closing_at": "6pm",
  11. "day": "monday"
  12. },
  13. {
  14. "id": 2, // you also have to put the ID of entries you don't want to update
  15. "opening_at": "10am",
  16. "closing_at": "6pm",
  17. "day": "tuesday"
  18. }
  19. ]
  20. })
  21. );

NOTE if you don’t specify the ID it will delete and re-create the entry and you will see the ID value change.

:::

::: tab Delete

Delete a restaurant with non-repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/1', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghour: null,
  7. })
  8. );

Delete a restaurant with repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghours: [
  7. {
  8. "id": 1 // the ID of the entry you want to keep
  9. "opening_at": "10am",
  10. "closing_at": "6pm",
  11. "day": "monday"
  12. }
  13. ]
  14. })
  15. );

:::

::::

Dynamic Zone

Dynamic Zone 字段允许您创建一个灵活的空间,以便根据组件的混合列表编写内容。

例子

假设我们在 article 类别中创建了一个 slidercontent 组件。

Path — ./api/article/models/Article.settings.json.

  1. {
  2. "attributes": {
  3. "body": {
  4. "type": "dynamiczone",
  5. "components": ["article.slider", "article.content"]
  6. }
  7. }
  8. }
  • components (array): 遵循此格式的组件数组 <category>.<componentName>.

:::: tabs

::: tab Create

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('POST', '/articles', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. body: [
  7. {
  8. __component: 'article.content',
  9. content: 'This is a content',
  10. },
  11. {
  12. __component: 'article.slider',
  13. name: 'Slider name',
  14. },
  15. ],
  16. })
  17. );

:::

::: tab Update

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurant/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. body: [
  7. {
  8. "id": 1 // the ID of the entry you want to update
  9. "__component": "article.content",
  10. "content": "This is an updated content",
  11. },
  12. {
  13. "id": 2, // you also have to put the ID of entries you don't want to update
  14. "__component": "article.slider",
  15. "name": "Slider name",
  16. }
  17. ]
  18. })
  19. );

NOTE if you don’t specify the ID it will delete and re-create the entry and you will see the ID value change.

:::

::: tab Delete

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurant/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. body: [
  7. {
  8. "id": 1 // the ID of the entry you want to keep
  9. "__component": "article.content",
  10. "content": "This is an updated content",
  11. }
  12. ]
  13. })
  14. );

:::

::::

生命周期挂钩

生命周期钩子是在调用 Strapi queries 时触发的函数。当您在管理面板中管理内容或使用 queries 开发自定义代码时,它们将自动触发

要配置 ContentType 生命周期钩子,可以在 {modelName}.js 中设置 lifecycles 键。文件位于 ./api/{apiName}/models 文件夹。

可用的生命周期挂钩

:::: tabs

::: tab find

beforeFind(params, populate)

Parameters:

Name Type Description
params Object Find params (e.g: limit, filters)

afterFind(results, params, populate)

Parameters:

Name Type Description
results Array{Object} The results found for the find query
params Object Find params (e.g: limit, filters)
populate Array{string} Populate specific relations

:::

::: tab findOne

beforeFindOne(params, populate)

Parameters:

Name Type Description
params Object Find params (e.g: filters)

afterFindOne(result, params, populate)

Parameters:

Name Type Description
result Object The results found for the findOne query
params Object Find params (e.g: filters)
populate Array{string} Populate specific relations

:::

::: tab create

beforeCreate(data)

Parameters:

Name Type Description
data Object Input data to the entry was created with

afterCreate(result, data)

Parameters:

Name Type Description
result Object Created entry
data Object Input data to the entry was created with

:::

::: tab update

beforeUpdate(params, data)

Parameters:

Name Type Description
params Object Find params (e.g: filters)
data Object Input data to the entry was created with

afterUpdate(result, params, data)

Parameters:

Name Type Description
result Object Updated entry
params Object Find params (e.g: filters)
data Object Input data to the entry was created with

:::

::: tab delete

beforeDelete(params)

Parameters:

Name Type Description
params Object Find params (e.g: filters)

afterDelete(result, params)

Parameters:

Name Type Description
result Object Deleted entry
params Object Find params (e.g: filters)

:::

::: tab count

beforeCount(params)

Parameters:

Name Type Description
params Object Find params (e.g: filters)

afterCount(result, params)

Parameters:

Name Type Description
result Integer The count matching entries
params Object Find params (e.g: filters)

:::

::: tab search

beforeSearch(params, populate)

Parameters:

Name Type Description
params Object Find params (e.g: filters)
populate Array{string} Populate specific relations

afterSearch(result, params)

Parameters:

Name Type Description
results Array{Object} The entries found
params Object Find params (e.g: filters)
populate Array{string} Populate specific relations

:::

::: tab countSearch

beforeCountSearch(params)

Parameters:

Name Type Description
params Object Find params (e.g: filters)

afterCountSearch(result, params)

Parameters:

Name Type Description
result Integer The count matching entries
params Object Find params (e.g: filters)

:::

::::

例子

Path — ./api/restaurant/models/Restaurant.js.

  1. module.exports = {
  2. /**
  3. * Triggered before user creation.
  4. */
  5. lifecycles: {
  6. async beforeCreate(data) {
  7. data.isTableFull = data.numOfPeople === 4;
  8. },
  9. },
  10. };

::: tip 您可以改变其中一个参数来改变它的属性。确保不要重新分配参数,因为它不会产生任何效果:

This will Work

  1. module.exports = {
  2. lifecycles: {
  3. beforeCreate(data) {
  4. data.name = 'Some fixed name';
  5. },
  6. },
  7. };

This will NOT Work

  1. module.exports = {
  2. lifecycles: {
  3. beforeCreate(data) {
  4. data = {
  5. ...data,
  6. name: 'Some fixed name',
  7. };
  8. },
  9. },
  10. };

:::

定制使用

当您构建定制 ORM 特定查询时,生命周期将不会被触发。然而,如果您愿意,您可以直接调用生命周期函数。

Bookshelf example

Path - ./api/{apiName}/services/{serviceName}.js

  1. module.exports = {
  2. async createCustomEntry() {
  3. const ORMModel = strapi.query(modelName).model;
  4. const newCustomEntry = await ORMModel.forge().save();
  5. // trigger manually
  6. ORMModel.lifecycles.afterCreate(newCustomEntry.toJSON());
  7. },
  8. };

::: tip 当直接调用生命周期函数时,您需要确保使用预期的参数调用它。 :::

Webhooks

Webhook 是应用程序用于通知其他应用程序发生事件的构造。更准确地说,webhook 是一个用户定义的 HTTP 回调。使用 webhook 是告诉第三方提供者开始某些处理(CI、构建、部署…)的好方法。

Webhook 的工作方式是通过 HTTP 请求(通常是 POST 请求)将信息传递给接收应用程序。

用户内容类型的网钩

为了防止无意中将任何用户信息发送到其他应用程序,Webhooks 不适用于 User 内容类型。如果需要通知其他应用程序用户集合中的更改,可以通过在文件中创建 Lifecycle hooks 来实现 ./extensions/users-permissions/models/User.js .

可用配置

您可以在文件 ./config/server.js 中设置 webhook 配置。

  • webhooks
    • defaultHeaders: 你可以设置默认的标题来使用你的 webhook 请求。此选项被 webhook 本身中设置的头部覆盖.

示例 configuration

  1. module.exports = {
  2. webhooks: {
  3. defaultHeaders: {
  4. 'Custom-Header': 'my-custom-header',
  5. },
  6. },
  7. };

安全 webhooks

大多数时候,webhooks 对公共 URL 发出请求,因此有可能有人发现这个 URL 并发送错误的信息。

为了防止这种情况发生,您可以使用认证标记发送标头。使用管理面板,你必须为每一个 webhook 做到这一点。另一种方法是定义 defaultHeaders 来添加到每个 webhook 请求中。

您可以通过更新 ./config/server.js 文件来配置这些全局头文件:

:::: tabs

::: tab Simple token

  1. module.exports = {
  2. webhooks: {
  3. defaultHeaders: {
  4. Authorization: 'Bearer my-very-secured-token',
  5. },
  6. },
  7. };

:::

::: tab Environment variable

  1. module.exports = {
  2. webhooks: {
  3. defaultHeaders: {
  4. Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`,
  5. },
  6. },
  7. };

::::

如果您正在自己开发 webhook 处理程序,现在可以通过读取标头来验证标记。

可用事件

默认情况下,Strapi webhooks 可以由以下事件触发:

Name Description
entry.create Triggered when a Content Type entry is created.
entry.update Triggered when a Content Type entry is updated.
entry.delete Triggered when a Content Type entry is deleted.
entry.publish Triggered when a Content Type entry is published.*
entry.unpublish Triggered when a Content Type entry is unpublished.*
media.create Triggered when a media is created.
media.update Triggered when a media is updated.
media.delete Triggered when a media is deleted.

* 仅当在此内容类型上启用 draftAndPublish 时。

Payloads

:::tip NOTE 私有字段和密码不会在有效负载中发送。 :::

Headers

当有效负载传递到你的 webhook 的 URL 时,它将包含特定的头部:

Header Description
X-Strapi-Event Name of the event type that was triggered.

entry.create

创建新条目时触发此事件。

示例 payload

  1. {
  2. "event": "entry.create",
  3. "created_at": "2020-01-10T08:47:36.649Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:47:36.264Z",
  14. "cover": null,
  15. "images": []
  16. }
  17. }

entry.update

更新条目时触发此事件。

示例 payload

  1. {
  2. "event": "entry.update",
  3. "created_at": "2020-01-10T08:58:26.563Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "cover": null,
  15. "images": []
  16. }
  17. }

entry.delete

删除条目时触发此事件。

示例 payload

  1. {
  2. "event": "entry.delete",
  3. "created_at": "2020-01-10T08:59:35.796Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "cover": null,
  15. "images": []
  16. }
  17. }

entry.publish

发布条目时触发此事件。

示例 payload

  1. {
  2. "event": "entry.publish",
  3. "created_at": "2020-01-10T08:59:35.796Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "published_at": "2020-08-29T14:20:12.134Z",
  15. "cover": null,
  16. "images": []
  17. }
  18. }

entry.unpublish

未发布条目时触发此事件。

示例 payload

  1. {
  2. "event": "entry.unpublish",
  3. "created_at": "2020-01-10T08:59:35.796Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "published_at": null,
  15. "cover": null,
  16. "images": []
  17. }
  18. }

media.create

当您在创建条目时或通过媒体界面上传文件时,将触发此事件。

示例 payload

  1. {
  2. "event": "media.create",
  3. "created_at": "2020-01-10T10:58:41.115Z",
  4. "media": {
  5. "id": 1,
  6. "name": "image.png",
  7. "hash": "353fc98a19e44da9acf61d71b11895f9",
  8. "sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc",
  9. "ext": ".png",
  10. "mime": "image/png",
  11. "size": 228.19,
  12. "url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png",
  13. "provider": "local",
  14. "provider_metadata": null,
  15. "created_at": "2020-01-10T10:58:41.095Z",
  16. "updated_at": "2020-01-10T10:58:41.095Z",
  17. "related": []
  18. }
  19. }

media.update

当您替换媒体或通过媒体界面更新媒体的元数据时,将触发此事件。

示例 payload

  1. {
  2. "event": "media.update",
  3. "created_at": "2020-01-10T10:58:41.115Z",
  4. "media": {
  5. "id": 1,
  6. "name": "image.png",
  7. "hash": "353fc98a19e44da9acf61d71b11895f9",
  8. "sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc",
  9. "ext": ".png",
  10. "mime": "image/png",
  11. "size": 228.19,
  12. "url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png",
  13. "provider": "local",
  14. "provider_metadata": null,
  15. "created_at": "2020-01-10T10:58:41.095Z",
  16. "updated_at": "2020-01-10T10:58:41.095Z",
  17. "related": []
  18. }
  19. }

media.delete

只有在通过媒体界面删除媒体时才会触发此事件。

示例 payload

  1. {
  2. "event": "media.delete",
  3. "created_at": "2020-01-10T11:02:46.232Z",
  4. "media": {
  5. "id": 11,
  6. "name": "photo.png",
  7. "hash": "43761478513a4c47a5fd4a03178cfccb",
  8. "sha256": "HrpDOKLFoSocilA6B0_icA9XXTSPR9heekt2SsHTZZE",
  9. "ext": ".png",
  10. "mime": "image/png",
  11. "size": 4947.76,
  12. "url": "/uploads/43761478513a4c47a5fd4a03178cfccb.png",
  13. "provider": "local",
  14. "provider_metadata": null,
  15. "created_at": "2020-01-07T19:34:32.168Z",
  16. "updated_at": "2020-01-07T19:34:32.168Z",
  17. "related": []
  18. }
  19. }