diff --git a/docusaurus/docs/dev-docs/backend-customization/controllers.md b/docusaurus/docs/dev-docs/backend-customization/controllers.md index 23c92a181b..0db6e1bdd6 100644 --- a/docusaurus/docs/dev-docs/backend-customization/controllers.md +++ b/docusaurus/docs/dev-docs/backend-customization/controllers.md @@ -30,48 +30,75 @@ A new controller can be implemented: ```js title="./src/api/restaurant/controllers/restaurant.js" - -const { createCoreController } = require('@strapi/strapi').factories; - -module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ - // Method 1: Creating an entirely custom action - async exampleAction(ctx) { - try { - ctx.body = 'ok'; - } catch (err) { - ctx.body = err; - } - }, - - // Method 2: Wrapping a core action (leaves core logic in place) - async find(ctx) { - // some custom logic here - ctx.query = { ...ctx.query, local: 'en' } - - // Calling the default core action - const { data, meta } = await super.find(ctx); - - // some more custom logic - meta.date = Date.now() - - return { data, meta }; - }, - - // Method 3: Replacing a core action with proper sanitization - async find(ctx) { - // validateQuery (optional) - // to throw an error on query params that are invalid or the user does not have access to - await this.validateQuery(ctx); - - // sanitizeQuery to remove any query params that are invalid or the user does not have access to - // It is strongly recommended to use sanitizeQuery even if validateQuery is used - const sanitizedQueryParams = await this.sanitizeQuery(ctx); - const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); - const sanitizedResults = await this.sanitizeOutput(results, ctx); - - return this.transformResponse(sanitizedResults, { pagination }); - } -})); +const { createCoreController } = require("@strapi/strapi").factories; + +module.exports = createCoreController( + "api::restaurant.restaurant", + ({ strapi }) => ({ + /** + * Example 1: Modifying a Strapi controller function + * + * If you need to modify the input or output of a pre-defined Strapi controller method, + * write a method of the same name, and use `super` to call the parent method. + * */ + async find(ctx) { + // your custom logic for modifying the input + ctx.query = { ...ctx.query, locale: "en" }; // force ctx.query.locale to 'en' regardless of what was requested + + // Call the default parent controller action + const result = await super.find(ctx); + + // your custom logic for modifying the output + result.meta.date = Date.now(); // change the date that is returned + + return result; + }, + + /** + * Example 2: Replacing a Strapi controller function + * + * If you need to completely replace the behavior of a pre-defined Strapi controller method, + * you can do so by simply implementing a method of the same name. + * + * Caution: You will need to manage the security of the request and results on your own, + * as demonstrated in this example. + * */ + async find(ctx) { + // validateQuery throws an error if any of the query params used are inaccessible to ctx.user + // That is, trying to access private fields, fields they don't have permission for, wrong data type, etc + await this.validateQuery(ctx); + + // sanitizeQuery silently removes any query params that are invalid or the user does not have access to + // It is recommended to use sanitizeQuery even if validateQuery is used, as validateQuery allows + // a number of non-security-related cases such as empty objects in string fields to pass, while sanitizeQuery + // will remove them completely + const sanitizedQueryParams = await this.sanitizeQuery(ctx); + + // Perform whatever custom actions are needed + const { results, pagination } = await strapi + .service("api::restaurant.restaurant") + .find(sanitizedQueryParams); + + // sanitizeOutput removes any data that was returned by our query that the ctx.user should not have access to + const sanitizedResults = await this.sanitizeOutput(results, ctx); + + // transformResponse correctly formats the data and meta fields of your results to return to the API + return this.transformResponse(sanitizedResults, { pagination }); + }, + + /** + * Example 3: Writing your own new controller function + * If you need to create some new action that does not match one of the pre-configured Strapi methods, + * you can simply add the method with the desired name and implement whatever functionality you want. + * + * Caution: Similar to replacing a controller, you will need to manage the security of the request + * yourself, so remember to use sanitizers and validators as needed. + * */ + async healthCheck(ctx) { + ctx.body = "ok"; + }, + }) +); ``` @@ -79,50 +106,79 @@ module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) ```js title="./src/api/restaurant/controllers/restaurant.ts" - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ - // Method 1: Creating an entirely custom action - async exampleAction(ctx) { - try { - ctx.body = 'ok'; - } catch (err) { - ctx.body = err; - } - }, - - // Method 2: Wrapping a core action (leaves core logic in place) - async find(ctx) { - // some custom logic here - ctx.query = { ...ctx.query, local: 'en' } - - // Calling the default core action - const { data, meta } = await super.find(ctx); - - // some more custom logic - meta.date = Date.now() - - return { data, meta }; - }, - - // Method 3: Replacing a core action with proper sanitization - async find(ctx) { - // validateQuery (optional) - // to throw an error on query params that are invalid or the user does not have access to - await this.validateQuery(ctx); - - // sanitizeQuery to remove any query params that are invalid or the user does not have access to - // It is strongly recommended to use sanitizeQuery even if validateQuery is used - const sanitizedQueryParams = await this.sanitizeQuery(ctx); - const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); - - // sanitizeOutput to ensure the user does not receive any data they do not have access to - const sanitizedResults = await this.sanitizeOutput(results, ctx); - - return this.transformResponse(sanitizedResults, { pagination }); - } -})); +import { factories } from "@strapi/strapi"; + +export default factories.createCoreController( + "api::restaurant.restaurant", + ({ strapi }) => ({ + /** + * Example 1: Modifying a Strapi controller function + * + * If you need to modify the input or output of a pre-defined Strapi controller method, + * write a method of the same name, and use `super` to call the parent method. + * */ + async find(ctx) { + // your custom logic for modifying the input + ctx.query = { ...ctx.query, locale: "en" }; // force ctx.query.locale to 'en' regardless of what was requested + + // Call the default parent controller action + const result = await super.find(ctx); + + // your custom logic for modifying the output + result.meta.date = Date.now(); // change the date that is returned + + return result; + }, + + /** + * Example 2: Replacing a Strapi controller function + * + * If you need to completely replace the behavior of a pre-defined Strapi controller method, + * you can do so by simply implementing a method of the same name. + * + * Caution: You will need to manage the security of the request and results on your own, + * as demonstrated in this example. + * */ + async find(ctx) { + // validateQuery throws an error if any of the query params used are inaccessible to ctx.user + // That is, trying to access private fields, fields they don't have permission for, wrong data type, etc + await this.validateQuery(ctx); + + // sanitizeQuery silently removes any query params that are invalid or the user does not have access to + // It is recommended to use sanitizeQuery even if validateQuery is used, as validateQuery allows + // a number of non-security-related cases such as empty objects in string fields to pass, while sanitizeQuery + // will remove them completely + const sanitizedQueryParams = await this.sanitizeQuery(ctx); + + // Perform whatever custom actions are needed + const { results, pagination } = await strapi + .service("api::restaurant.restaurant") + .find(sanitizedQueryParams); + + // sanitizeOutput removes any data that was returned by our query that the ctx.user should not have access to + const sanitizedResults = await this.sanitizeOutput(results, ctx); + + // transformResponse correctly formats the data and meta fields of your results to return to the API + return this.transformResponse(sanitizedResults, { pagination }); + }, + + /** + * Example 3: Writing your own new controller function + * If you need to create some new action that does not match one of the pre-configured Strapi methods, + * you can simply add the method with the desired name and implement whatever functionality you want. + * + * Caution: Similar to replacing a controller, you will need to manage the security of the request + * yourself, so remember to use sanitizers and validators as needed. + * */ + async healthCheck(ctx) { + try { + ctx.body = "ok"; + } catch (err) { + ctx.body = err; + } + }, + }) +); ``` @@ -141,23 +197,22 @@ A specific `GET /hello` [route](/dev-docs/backend-customization/routes) is defin ```js "title="./src/api/hello/routes/hello.js" - module.exports = { routes: [ { - method: 'GET', - path: '/hello', - handler: 'hello.index', - } - ] -} + method: "GET", + path: "/hello", + handler: "hello.index", + }, + ], +}; ``` ```js "title="./src/api/hello/controllers/hello.js" - module.exports = { - async index(ctx, next) { // called by GET /hello - ctx.body = 'Hello World!'; // we could also send a JSON + async index(ctx, next) { + // called by GET /hello + ctx.body = "Hello World!"; // we could also send a JSON }, }; ``` @@ -167,23 +222,22 @@ module.exports = { ```js "title="./src/api/hello/routes/hello.ts" - export default { routes: [ { - method: 'GET', - path: '/hello', - handler: 'hello.index', - } - ] -} + method: "GET", + path: "/hello", + handler: "hello.index", + }, + ], +}; ``` ```js title="./src/api/hello/controllers/hello.ts" - export default { - async index(ctx, next) { // called by GET /hello - ctx.body = 'Hello World!'; // we could also send a JSON + async index(ctx, next) { + // called by GET /hello + ctx.body = "Hello World!"; // we could also send a JSON }, }; ``` @@ -213,12 +267,12 @@ It's strongly recommended you sanitize (v4.8.0+) and/or validate (v4.13.0+) your Within the Strapi factories the following functions are exposed that can be used for sanitization and validation: | Function Name | Parameters | Description | -|------------------|----------------------------|--------------------------------------------------------------------------------------| +| ---------------- | -------------------------- | ------------------------------------------------------------------------------------ | | `sanitizeQuery` | `ctx` | Sanitizes the request query | | `sanitizeOutput` | `entity`/`entities`, `ctx` | Sanitizes the output data where entity/entities should be an object or array of data | | `sanitizeInput` | `data`, `ctx` | Sanitizes the input data | | `validateQuery` | `ctx` | Validates the request query (throws an error on invalid params) | -| `validateInput` | `data`, `ctx` | (EXPERIMENTAL) Validates the input data (throws an error on invalid data) | +| `validateInput` | `data`, `ctx` | (EXPERIMENTAL) Validates the input data (throws an error on invalid data) | These functions automatically inherit the sanitization settings from the model and sanitize the data accordingly based on the content-type schema and any of the content API authentication strategies, such as the Users & Permissions plugin or API tokens. @@ -230,19 +284,23 @@ Because these methods use the model associated with the current controller, if y ```js title="./src/api/restaurant/controllers/restaurant.js" - -const { createCoreController } = require('@strapi/strapi').factories; - -module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ - async find(ctx) { - await this.validateQuery(ctx); - const sanitizedQueryParams = await this.sanitizeQuery(ctx); - const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); - const sanitizedResults = await this.sanitizeOutput(results, ctx); - - return this.transformResponse(sanitizedResults, { pagination }); - } -})); +const { createCoreController } = require("@strapi/strapi").factories; + +module.exports = createCoreController( + "api::restaurant.restaurant", + ({ strapi }) => ({ + async find(ctx) { + await this.validateQuery(ctx); + const sanitizedQueryParams = await this.sanitizeQuery(ctx); + const { results, pagination } = await strapi + .service("api::restaurant.restaurant") + .find(sanitizedQueryParams); + const sanitizedResults = await this.sanitizeOutput(results, ctx); + + return this.transformResponse(sanitizedResults, { pagination }); + }, + }) +); ``` @@ -250,34 +308,38 @@ module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) ```js title="./src/api/restaurant/controllers/restaurant.ts" - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ - async find(ctx) { - const sanitizedQueryParams = await this.sanitizeQuery(ctx); - const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); - const sanitizedResults = await this.sanitizeOutput(results, ctx); - - return this.transformResponse(sanitizedResults, { pagination }); - } -})); +import { factories } from "@strapi/strapi"; + +export default factories.createCoreController( + "api::restaurant.restaurant", + ({ strapi }) => ({ + async find(ctx) { + const sanitizedQueryParams = await this.sanitizeQuery(ctx); + const { results, pagination } = await strapi + .service("api::restaurant.restaurant") + .find(sanitizedQueryParams); + const sanitizedResults = await this.sanitizeOutput(results, ctx); + + return this.transformResponse(sanitizedResults, { pagination }); + }, + }) +); ``` -#### Sanitization and validation when building custom controllers {#sanitize-validate-custom-controllers} +#### Sanitization and validation when building custom controllers {#sanitize-validate-custom-controllers} Within custom controllers, there are 5 primary functions exposed via the `@strapi/utils` package that can be used for sanitization and validation: -| Function Name | Parameters | Description | -|------------------------------|--------------------|---------------------------------------------------------| -| `sanitize.contentAPI.input` | `data`, `schema`, `auth` | Sanitizes the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins | -| `sanitize.contentAPI.output` | `data`, `schema`, `auth` | Sanitizes the response output including restricted relations, private fields, passwords, and other nested "visitors" added by plugins | -| `sanitize.contentAPI.query` | `ctx.query`, `schema`, `auth` | Sanitizes the request query including filters, sort, fields, and populate | -| `validate.contentAPI.query` | `ctx.query`, `schema`, `auth` | Validates the request query including filters, sort, fields (currently not populate) | -| `validate.contentAPI.input` | `data`, `schema`, `auth` | (EXPERIMENTAL) Validates the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins | +| Function Name | Parameters | Description | +| ---------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sanitize.contentAPI.input` | `data`, `schema`, `auth` | Sanitizes the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins | +| `sanitize.contentAPI.output` | `data`, `schema`, `auth` | Sanitizes the response output including restricted relations, private fields, passwords, and other nested "visitors" added by plugins | +| `sanitize.contentAPI.query` | `ctx.query`, `schema`, `auth` | Sanitizes the request query including filters, sort, fields, and populate | +| `validate.contentAPI.query` | `ctx.query`, `schema`, `auth` | Validates the request query including filters, sort, fields (currently not populate) | +| `validate.contentAPI.input` | `data`, `schema`, `auth` | (EXPERIMENTAL) Validates the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins | :::note Depending on the complexity of your custom controllers, you may need additional sanitization that Strapi cannot currently account for, especially when combining the data from multiple sources. @@ -287,20 +349,30 @@ Depending on the complexity of your custom controllers, you may need additional ```js title="./src/api/restaurant/controllers/restaurant.js" - -const { sanitize, validate } = require('@strapi/utils'); +const { sanitize, validate } = require("@strapi/utils"); module.exports = { async findCustom(ctx) { - const contentType = strapi.contentType('api::test.test'); - await validate.contentAPI.query(ctx.query, contentType, { auth: ctx.state.auth }); - const sanitizedQueryParams = await sanitize.contentAPI.query(ctx.query, contentType, { auth: ctx.state.auth }); - - const entities = await strapi.entityService.findMany(contentType.uid, sanitizedQueryParams); - - return await sanitize.contentAPI.output(entities, contentType, { auth: ctx.state.auth }); - } -} + const contentType = strapi.contentType("api::test.test"); + await validate.contentAPI.query(ctx.query, contentType, { + auth: ctx.state.auth, + }); + const sanitizedQueryParams = await sanitize.contentAPI.query( + ctx.query, + contentType, + { auth: ctx.state.auth } + ); + + const entities = await strapi.entityService.findMany( + contentType.uid, + sanitizedQueryParams + ); + + return await sanitize.contentAPI.output(entities, contentType, { + auth: ctx.state.auth, + }); + }, +}; ``` @@ -308,21 +380,31 @@ module.exports = { ```js title="./src/api/restaurant/controllers/restaurant.ts" - -import { sanitize, validate } from '@strapi/utils'; +import { sanitize, validate } from "@strapi/utils"; export default { async findCustom(ctx) { - const contentType = strapi.contentType('api::test.test'); - - await validate.contentAPI.query(ctx.query, contentType, { auth: ctx.state.auth }); - const sanitizedQueryParams = await sanitize.contentAPI.query(ctx.query, contentType, { auth: ctx.state.auth }); - - const entities = await strapi.entityService.findMany(contentType.uid, sanitizedQueryParams); - - return await sanitize.contentAPI.output(entities, contentType, { auth: ctx.state.auth }); - } -} + const contentType = strapi.contentType("api::test.test"); + + await validate.contentAPI.query(ctx.query, contentType, { + auth: ctx.state.auth, + }); + const sanitizedQueryParams = await sanitize.contentAPI.query( + ctx.query, + contentType, + { auth: ctx.state.auth } + ); + + const entities = await strapi.entityService.findMany( + contentType.uid, + sanitizedQueryParams + ); + + return await sanitize.contentAPI.output(entities, contentType, { + auth: ctx.state.auth, + }); + }, +}; ``` @@ -474,9 +556,9 @@ Controllers are declared and attached to a route. Controllers are automatically ```js // access an API controller -strapi.controller('api::api-name.controller-name'); +strapi.controller("api::api-name.controller-name"); // access a plugin controller -strapi.controller('plugin::plugin-name.controller-name'); +strapi.controller("plugin::plugin-name.controller-name"); ``` :::tip