diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 9ad25fa82b..b17166286e 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -3,29 +3,34 @@ about: Create a report to help us improve the Strapi documentation. title: "[Bug]: " labels: "type: bug, status: pending reproduction" issue_body: true -inputs: +body: - type: input attributes: label: Link to the documentation page or resource - required: true placeholder: https://strapi.io/documentation/developer-docs/latest/somepage.html + validations: + required: true - type: textarea attributes: label: Describe the bug - required: true placeholder: "A clear and concise description of what the bug is." + validations: + required: true - type: textarea attributes: label: Additional context - required: false placeholder: "Add any other context about the problem here." + validations: + required: false - type: textarea attributes: label: Suggested improvements or fixes - required: false placeholder: "A clear and concise description of what you want to happen." + validations: + required: false - type: textarea attributes: label: Related issue(s)/PR(s) - required: false placeholder: "Let us know if this is related to any issue/pull request." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/DOC_REQUEST.yml b/.github/ISSUE_TEMPLATE/DOC_REQUEST.yml index 95db9561a4..afd302ac60 100644 --- a/.github/ISSUE_TEMPLATE/DOC_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/DOC_REQUEST.yml @@ -3,24 +3,28 @@ about: Suggest a new part of the documentation we are missing! title: "[Request]: " labels: "type: doc request" issue_body: false -inputs: +body: - type: textarea attributes: label: Summary - required: true placeholder: Quick summary what's this documentation request about. + validations: + required: true - type: textarea attributes: label: Why is it needed? - required: true placeholder: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]" + validations: + required: true - type: textarea attributes: label: Suggested solution(s) - required: false placeholder: "A clear and concise description of what you want to happen." + validations: + required: false - type: textarea attributes: label: Related issue(s)/PR(s) - required: false placeholder: "Let us know if this is related to any issue/pull request." + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4a1acd5e69..4ab69ebe6d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ Hello πŸ‘‹ Thank you for submitting a pull request. To help us merge your PR, make sure to follow the instructions below: -- Create or update the documentation. (Should be made against the documentation branch) +- Create or update the documentation. (Should be made against the `main` branch) - Create or update the tests. - Refer to the issue you are closing in the PR description - fix #issue - Specify if the PR is in WIP (work in progress) state or ready to be merged diff --git a/.gitignore b/.gitignore index 4cedecc917..f60df091be 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,7 @@ dist # Other *.DS_Store + +# IDE +.idea +.vscode diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aaa24cb0be..1f0f2713da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ This project and everyone participating in it are governed by the [Strapi Code o ## Documentation -Pull requests relating to fixing documentation for the latest release should be directed towards the [documentation branch](https://github.com/strapi/documentation/tree/documentation) **not** towards the main branch. Any PRs made towards the main branch will not be released until the next Strapi version release. +Pull requests relating to fixing documentation for the latest release should be directed towards the `main` branch. ## Bugs @@ -28,28 +28,82 @@ We are using [GitHub Issues](https://github.com/strapi/documentation/issues) to --- -## Before Submitting a Pull Request +## Submitting a Pull Request The core team will review your pull request and will either merge it, request changes to it, or close it. -**Before submitting your pull request** make sure the following requirements are fulfilled: +### Contribution Prerequisites -- Fork the repository and create your branch from `documentation`. -- Run `yarn` and `yarn dev` in the specific documentation root. (Please use yarn, not npm as we maintain a yarn.lock file instead of a package-lock.json) - -## Contribution Prerequisites - -- You have [Node](https://nodejs.org/en/) at v12 only (Node v13+ are not supported) and [Yarn](https://yarnpkg.com/en/) at v1.2.0+. + +- You installed the following softwares: + - [Node](https://nodejs.org/en/) at v12 only (Node v13+ are not supported) + - [Yarn](https://yarnpkg.com/en/) at v1.2.0+. **Please use yarn**, not npm, as we maintain a `yarn.lock` file instead of `package-lock.json` - You are familiar with Git. -## Development Workflow +### Contribution Workflow -**Currently WIP** +#### Setting up + +1. Fork the `documentation` repository. +2. Clone your forked `documentation` repository. + + ```bash + # HTTPS + git clone https://github.com/yourgithubname/documentation.git + + # SSH + git clone git@github.com:yourgithubname/documentation.git + ``` + +3. Enter in the folder which contains either the user documentation or developer documentation (depending on what you want to contribute to). + + ```bash + #user documentation + cd documentation/user-docs + + #developer documentation + cd documentation/developer-docs + ``` + +4. From the `main` branch, retrieve the latest modifications to work on an up-to-date branch. + + ```bash + git checkout main + git pull + ``` -At the moment we are under a contribution freeze from the community, we will update this guide once we lift the freeze. +5. Create your own branch on which you will work on your documentation contribution. Make sure the name of your branch indicates both the type of documentation and the topic. + ```bash + #user documentation + git checkout -b + + #developer documentation + git checkout -b + ``` + +6. Make sure all dependencies are installed, then run the server: + + ```bash + yarn && yarn dev + ``` + +#### Writing +We can't provide you specific procedures with step-by-step instructions to write technical documentation. But if you have any question or need help, do feel free to reach us through [our forum](https://forum.strapi.io/). + +#### Sharing & reviewing + +1. Create a pull request from your forked `documentation` repository to the original `documentation` repository, to share your contribution to the documentation. +2. Wait for your pull request to be reviewed by a Strapi team member. +You do not need to assign anyone. Your pull request will be handled by our team in the next 48 hours (business days). +3. *(optional)* Once the PR has been reviewed, you may have additional fixes to make before it's validated. + +Congratulations, and thank you for your contribution! +Once your PR is validated, the Strapi team takes over and make sure your new content will soon be deployed! + +--- ## Miscellaneous ### Repository Organization @@ -64,11 +118,11 @@ We have various types of documentation that are maintained on their own: We have a few primary branches that are used for different purposes: - **main** - - This branch is used by the Strapi to push documentation on new features before they are released, it should not be used for PRs by the community unless instructed to do so by a Strapi team member -- **documentation** - - This branch is for the current released version of Strapi, all documentation requests, updates, and removals should be pointed here -- **docs/*** + - This branch is used to store the current version of the documentation, all PRs should target this branch +- **docs/\*** - This collection of branches is for current work in progress changes, Strapi team members push our changes here to make PRs against the documentation branch +- **feature/\*** + - This is a collection of branches used as documentation for features currently in development in the main [strapi/strapi](https://github.com/strapi/strapi) repo ### Reporting an issue diff --git a/developer-docs/latest/concepts/middlewares.md b/developer-docs/latest/concepts/middlewares.md new file mode 100644 index 0000000000..952782e426 --- /dev/null +++ b/developer-docs/latest/concepts/middlewares.md @@ -0,0 +1,270 @@ +# Middlewares + +The middlewares are functions which are composed and executed in a stack-like manner upon request. If you are not familiar with the middleware stack in Koa, we highly recommend you to read the [Koa's documentation introduction](http://koajs.com/#introduction). + +## Structure + +### File structure + +```js +module.exports = strapi => { + return { + // can also be async + initialize() { + strapi.app.use(async (ctx, next) => { + // await someAsyncCode() + + await next(); + + // await someAsyncCode() + }); + }, + }; +}; +``` + +- `initialize` (function): Called during the server boot. + +The middlewares are accessible through the `strapi.middleware` variable. + +### Node modules + +Every folder that follows this name pattern `strapi-middleware-*` in your `./node_modules` folder will be loaded as a middleware. + +A middleware needs to follow the structure below: + +``` +/middleware +└─── lib + - index.js +- LICENSE.md +- package.json +- README.md +``` + +The `index.js` is the entry point to your middleware. It should look like the example above. + +### Custom middlewares + +The framework allows the application to override the default middlewares and add new ones. You have to create a `./middlewares` folder at the root of your project and put the middlewares into it. + +``` +/project +└─── api +└─── config +└─── middlewares +β”‚ └─── responseTime // It will override the core default responseTime middleware. +β”‚ - index.js +β”‚ └─── views // It will be added into the stack of middleware. +β”‚ - index.js +└─── public +- favicon.ico +- package.json +- server.js +``` + +Every middleware will be injected into the Koa stack. To manage the load order, please refer to the [Middleware order section](#load-order). + +## Configuration and activation + +To configure the middlewares of your application, you need to create or edit the `./config/middleware.js` file in your Strapi app. + +By default this file doesn't exist, you will have to create it. + +**Available options** + +- `timeout` (integer): Defines the maximum allowed milliseconds to load a middleware. +- `load` (Object): Configuration middleware loading. See details [here](#load-order) +- `settings` (Object): Configuration of each middleware + - `{middlewareName}` (Object): Configuration of one middleware + - `enabled` (boolean): Tells Strapi to run the middleware or not + +### Settings + +**Example**: + +**Path β€”** `./config/middleware.js`. + +```js +module.exports = { + //... + settings: { + cors: { + origin: ['http://localhost', 'https://mysite.com', 'https://www.mysite.com'], + }, + }, +}; +``` + +### Load order + +The middlewares are injected into the Koa stack asynchronously. Sometimes it happens that some of these middlewares need to be loaded in a specific order. To define a load order, create or edit the file `./config/middleware.js`. + +**Path β€”** `./config/middleware.js`. + +```js +module.exports = { + load: { + before: ['responseTime', 'logger', 'cors', 'responses'], + order: [ + "Define the middlewares' load order by putting their name in this array in the right order", + ], + after: ['parser', 'router'], + }, +}; +``` + +- `load`: + - `before`: Array of middlewares that need to be loaded in the first place. The order of this array matters. + - `order`: Array of middlewares that need to be loaded in a specific order. + - `after`: Array of middlewares that need to be loaded at the end of the stack. The order of this array matters. + +## Core middleware configurations + +The core of Strapi embraces a small list of middlewares for performances, security and great error handling. + +- boom +- cors +- cron +- csp +- favicon +- gzip +- hsts +- ip +- language +- logger +- p3p +- parser +- public +- responses +- responseTime +- router +- session +- xframe +- xss + +::: tip +The following middlewares cannot be disabled: responses, router, logger and boom. +::: + +### Global middlewares + +- `favicon` + - `path` (string): Path to the favicon file. Default value: `favicon.ico`. + - `maxAge` (integer): Cache-control max-age directive in ms. Default value: `86400000`. +- `public` + - `path` (string): Path to the public folder. Default value: `./public`. + - `maxAge` (integer): Cache-control max-age directive in ms. Default value: `60000`. + - `defaultIndex` (boolean): Display default index page at `/` and `/index.html`. Default value: `true`. + +### Request middlewares + +- `session` + - `enabled` (boolean): Enable or disable sessions. Default value: `false`. +- `logger` + - `level` (string): Default log level. Default value: `debug`. + - `exposeInContext` (boolean): Expose logger in context so it can be used through `strapi.log.info(β€˜my log’)`. Default value: `true`. + - `requests` (boolean): Enable or disable requests logs. Default value: `false`. +- `parser` (See [koa-body](https://github.com/dlau/koa-body#options) for more information) + - `enabled`(boolean): Enable or disable parser. Default value: `true`. + - `multipart` (boolean): Enable or disable multipart bodies parsing. Default value: `true`. + - `jsonLimit` (string|integer): The byte (if integer) limit of the JSON body. Default value: `1mb`. + - `formLimit` (string|integer): The byte (if integer) limit of the form body. Default value: `56k`. + - `queryStringParser` (see [qs](https://github.com/ljharb/qs) for a full list of options). + - `arrayLimit` (integer): the maximum length of an array in the query string. Any array members with an index of greater than the limit will instead be converted to an object with the index as the key. Default value: `100`. + - `depth` (integer): maximum parsing depth of nested query string objects. Default value: `20`. + +::: tip +The session doesn't work with `mongo` as a client. The package that we should use is broken for now. +::: + +### Response middlewares + +- [`gzip`](https://en.wikipedia.org/wiki/Gzip) + - `enabled` (boolean): Enable or not GZIP response compression. + - `options` (Object): Allow passing of options from [koa-compress](https://github.com/koajs/compress#options). +- `responseTime` + - `enabled` (boolean): Enable or not `X-Response-Time header` to response. Default value: `false`. +- `poweredBy` + - `enabled` (boolean): Enable or not `X-Powered-By` header to response. Default value: `true`. + - `value` (string): The value of the header. Default value: `Strapi ` + +::: tip +`gzip` compression via `koa-compress` uses [Brotli](https://en.wikipedia.org/wiki/Brotli) by default, but is not configured with sensible defaults for most cases. If you experience slow response times with `gzip` enabled, consider disabling Brotli by passing `{br: false}` as an option. You may also pass more sensible params with `{br: { params: { // YOUR PARAMS HERE } }}` +::: + +### Security middlewares + +- [`csp`](https://en.wikipedia.org/wiki/Content_Security_Policy) + - `enabled` (boolean): Enable or disable CSP to avoid Cross Site Scripting (XSS) and data injection attacks. + - `policy` (string): Configures the `Content-Security-Policy` header. If not specified uses default value. Default value: `undefined`. +- [`p3p`](https://en.wikipedia.org/wiki/P3P) + - `enabled` (boolean): Enable or disable p3p. +- [`hsts`](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) + - `enabled` (boolean): Enable or disable HSTS. + - `maxAge` (integer): Number of seconds HSTS is in effect. Default value: `31536000`. + - `includeSubDomains` (boolean): Applies HSTS to all subdomains of the host. Default value: `true`. +- [`xframe`](https://en.wikipedia.org/wiki/Clickjacking) + - `enabled` (boolean): Enable or disable `X-FRAME-OPTIONS` headers in response. + - `value` (string): The value for the header, e.g. DENY, SAMEORIGIN or ALLOW-FROM uri. Default value: `SAMEORIGIN`. +- [`xss`](https://en.wikipedia.org/wiki/Cross-site_scripting) + - `enabled` (boolean): Enable or disable XSS to prevent Cross Site Scripting (XSS) attacks in older IE browsers (IE8). +- [`cors`](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) + - `enabled` (boolean): Enable or disable CORS to prevent your server to be requested from another domain. + - `origin` (string or array): Allowed URLs (`http://example1.com, http://example2.com`, `['http://www.example1.com', 'http://example1.com']` or allows everyone `*`). Default value: `*`. + - `expose` (array): Configures the `Access-Control-Expose-Headers` CORS header. If not specified, no custom headers are exposed. Default value: `["WWW-Authenticate", "Server-Authorization"]`. + - `maxAge` (integer): Configures the `Access-Control-Max-Age` CORS header. Default value: `31536000`. + - `credentials` (boolean): Configures the `Access-Control-Allow-Credentials` CORS header. Default value: `true`. + - `methods` (array)|String - Configures the `Access-Control-Allow-Methods` CORS header. Default value: `["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]`. + - `headers` (array): Configures the `Access-Control-Allow-Headers` CORS header. If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. Default value: `["Content-Type", "Authorization", "X-Frame-Options"]`. +- `ip` + - `enabled` (boolean): Enable or disable IP blocker. Default value: `false`. + - `whiteList` (array): Whitelisted IPs. Default value: `[]`. + - `blackList` (array): Blacklisted IPs. Default value: `[]`. + +## Example + +Create your custom middleware. + +**Path β€”** `./middlewares/timer/index.js` + +```js +module.exports = strapi => { + return { + initialize() { + strapi.app.use(async (ctx, next) => { + const start = Date.now(); + + await next(); + + const delta = Math.ceil(Date.now() - start); + + ctx.set('X-Response-Time', delta + 'ms'); + }); + }, + }; +}; +``` + +Enable the middleware in environments settings. + +Load a middleware at the very first place + +**Path β€”** `./config/middleware.js` + +```js +module.exports = { + load: { + before: ['timer', 'responseTime', 'logger', 'cors', 'responses', 'gzip'], + order: [ + "Define the middlewares' load order by putting their name in this array is the right order", + ], + after: ['parser', 'router'], + }, + settings: { + timer: { + enabled: true, + }, + }, +}; +``` diff --git a/developer-docs/latest/concepts/models.md b/developer-docs/latest/concepts/models.md new file mode 100644 index 0000000000..5f7e7caf25 --- /dev/null +++ b/developer-docs/latest/concepts/models.md @@ -0,0 +1,1143 @@ +# Models + +## Concept + +### Content Type's models + +Models are a representation of the database's structure. They are split into two separate files. A JavaScript file that contains the model options (e.g: lifecycle hooks), and a JSON file that represents the data structure stored in the database. + +**Path β€”** `./api/restaurant/models/Restaurant.js`. + +```js +module.exports = { + lifecycles: { + // Called before an entry is created + beforeCreate(data) {}, + // Called after an entry is created + afterCreate(result) {}, + }, +}; +``` + +**Path β€”** `./api/restaurant/models/Restaurant.settings.json`. + +```json +{ + "kind": "collectionType", + "connection": "default", + "info": { + "name": "restaurant", + "description": "This represents the Restaurant Model" + }, + "attributes": { + "cover": { + "collection": "file", + "via": "related", + "plugin": "upload" + }, + "name": { + "default": "", + "type": "string" + }, + "description": { + "default": "", + "type": "text" + } + } +} +``` + +In this example, there is a `Restaurant` model which contains the attributes `cover`, `name` and `description`. + +### Component's models + +Another type of model is named `components`. A component is a data structure that can be used in one or many other API's model. There is no lifecycle related, only a JSON file definition. + +**Path β€”** `./components/default/simple.json` + +```json +{ + "connection": "default", + "collectionName": "components_default_simples", + "info": { + "name": "simple", + "icon": "arrow-circle-right" + }, + "options": {}, + "attributes": { + "name": { + "type": "string" + } + } +} +``` + +In this example, there is a `Simple` component which contains the attribute `name`. And the component is in the category `default`. + +### Where are the models defined? + +The **Content Types** models are defined in each `./api/**/models/` folder. Every JavaScript or JSON file in these folders will be loaded as a model. They are also available through the `strapi.models` and `strapi.api.**.models` global variables. Usable everywhere in the project, they contain the ORM model object that they refer to. By convention, a model's name should be written in lowercase. + +The **Components** models are defined in the `./components` folder. Every component has to be inside a subfolder (the category name of the component). + +## How to create a model? + +::: tip +If you are just starting out it is very convenient to generate some models with the Content Type Builder directly in the admin interface. You can then review the generated model mappings on the code level. The UI takes over a lot of validation tasks and gives you a feeling for available features. +::: + +### For Content Types models + +Use the CLI and run the following command `strapi generate:model restaurant name:string description:text`.
Read the [CLI documentation](../cli/CLI.md) for more information. + +This will create two files located at `./api/restaurant/models`: + +- `Restaurant.settings.json`: contains the list of attributes and settings. The JSON format makes the file easily editable. +- `Restaurant.js`: imports `Restaurant.settings.json` and extends it with additional settings and life cycle callbacks. + +::: tip +When you create a new API using the CLI (`strapi generate:api `), a model is automatically created. +::: + +### For Components models + +To create a component you will have to use the Content Type Builder from the Admin panel, there is not a cli generator for components. + +Or you can create your component manually by following the file path described previously and by following the file structure described below. + +## Model settings + +Additional settings can be set on models: + +- `kind` (string) - Define if the model is a Collection Type (`collectionType`) of a Single Type (`singleType`) - _only for Content Types_ +- `connection` (string) - Connection name which must be used. Default value: `default`. +- `collectionName` (string) - Collection name (or table name) in which the data should be stored. +- `globalId` (string) - Global variable name for this model (case-sensitive) - _only for Content Types_ +- `attributes` (object) - Define the data structure of your model. Find available options [below](#define-the-attributes). + +**Path β€”** `Restaurant.settings.json`. + +```json +{ + "kind": "collectionType", + "connection": "mongo", + "collectionName": "Restaurants_v1", + "globalId": "Restaurants", + "attributes": {} +} +``` + +In this example, the model `Restaurant` will be accessible through the `Restaurants` global variable. The data will be stored in the `Restaurants_v1` collection or table and the model will use the `mongo` connection defined in `./config/database.js` + +::: warning +If not set manually in the JSON file, Strapi will adopt the filename as `globalId`. +The `globalId` serves as a reference to your model within relations and Strapi APIs. If you chose to rename it (either by renaming your file or by changing the value of the `globalId`), you'd have to migrate your tables manually and update the references. +Please note that you should not alter the Strapi's models `globalId` (plugins and core models) since they are used directly within Strapi APIs and other models' relations. +::: + +::: tip +The `connection` value can be changed whenever you want, but you should be aware that there is no automatic data migration process. Also if the new connection doesn't use the same ORM you will have to rewrite your queries. +::: + +## Model information + +The info key on the model-json states information about the model. This information is used in the admin interface, when showing the model. + +- `name`: The name of the model, as shown in admin interface. +- `description`: The description of the model. +- `icon`: The fontawesome V5 name - _only for Components_ + +**Path β€”** `Restaurant.settings.json`. + +```json +{ + "info": { + "name": "restaurant", + "description": "" + } +} +``` + +## Model options + +The options key on the model-json states. + +- `timestamps`: This tells the model which attributes to use for timestamps. Accepts either `boolean` or `Array` of strings where first element is create date and second element is update date. Default value when set to `true` for Bookshelf is `["created_at", "updated_at"]` and for MongoDB is `["createdAt", "updatedAt"]`. + +- `privateAttributes`: This configuration allows to treat a set of attributes as private, even if they're not actually defined as attributes in the model. Accepts an `Array` of strings. It could be used to remove from API responses timestamps or `_v` when using MongoDB. The set of `privateAttributes` defined in the model are merged with the `privateAttributes` defined in the global Strapi configuration. + +- `populateCreatorFields`: Configure whether the API response should include `created_by` and `updated_by` fields or not. Accepts a `boolean`. The default value is `false`. + +- `draftAndPublish`: Enable the draft and publish feature. Accepts a `boolean`. The default value is `false`. + +**Path β€”** `Restaurant.settings.json`. + +```json +{ + "options": { + "timestamps": true, + "privateAttributes": ["id", "created_at"], + "populateCreatorFields": true, + "draftAndPublish": false + } +} +``` + +## Define the attributes + +The following types are currently available: + +- `string` +- `text` +- `richtext` +- `email` +- `password` +- `integer` +- `biginteger` +- `float` +- `decimal` +- `date` +- `time` +- `datetime` +- `boolean` +- `enumeration` +- `json` +- `uid` + +### Validations + +You can apply basic validations to attributes. The following supported validations are _only supported by MongoDB_ database connections. +If you're using SQL databases, you should use the native SQL constraints to apply them. + +- `required` (boolean) β€” If true, adds a required validator for this property. +- `unique` (boolean) β€” Whether to define a unique index on this property. +- `index` (boolean) β€” Adds an index on this property, this will create a [single field index](https://docs.mongodb.com/manual/indexes/#single-field) that will run in the background. _Only supported by MongoDB._ +- `max` (integer) β€” Checks if the value is greater than or equal to the given maximum. +- `min` (integer) β€” Checks if the value is less than or equal to the given minimum. + +**Security validations** + +To improve the Developer Experience when developing or using the administration panel, the framework enhances the attributes with these "security validations": + +- `private` (boolean) β€” If true, the attribute will be removed from the server response. (This is useful to hide sensitive data). +- `configurable` (boolean) - If false, the attribute isn't configurable from the Content Type Builder plugin. +- `autoPopulate` (boolean) - If false, the related data will not populate within REST responses. (This will not stop querying the relational data on GraphQL) + +### Exceptions + +**uid** + +- `targetField`(string) β€” The value is the name of an attribute that has `string` of the `text` type. +- `options` (string) β€” The value is a set of options passed to [the underlying `uid` generator](https://github.com/sindresorhus/slugify). A caveat is that the resulting `uid` must abide to the following RegEx `/^[A-Za-z0-9-_.~]*$`. + +### Example + +**Path β€”** `Restaurant.settings.json`. + +```json +{ + ... + "attributes": { + "title": { + "type": "string", + "min": 3, + "max": 99, + "unique": true + }, + "description": { + "default": "My description", + "type": "text", + "required": true + }, + "slug": { + "type": "uid", + "targetField": "title" + } + ... + } +} +``` + +## Relations + +Relations let you create links (relations) between your Content Types. + +:::: tabs + +::: tab One-Way + +One-way relationships are useful to link one entry to one other entry. However, only one of the models can be queried with its linked item. + +#### Example + +A `pet` can be owned by someone (a `user`). + +**Path β€”** `./api/pet/models/Pet.settings.json`. + +```json +{ + "attributes": { + "owner": { + "model": "user" + } + } +} +``` + +**Example** + +```js +// Create a pet +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/pets', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + owner: '5c151d9d5b1d55194d3209be', // The id of the user you want to link + }) +); +``` + +::: + +::: tab Many-way + +Many-way relationships are useful to link one entry to many other entries. However, only one of the models can be queried with its linked items. + +#### Example + +A `pet` can be owned by many people (multiple `users`). + +**Path β€”** `./api/pet/models/Pet.settings.json`. + +```json +{ + "attributes": { + "owners": { + "collection": "user" + } + } +} +``` + +**Example** + +```js +// Create a pet +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/pets', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + owners: ['5c151d9d5b1d55194d3209be', '5fc666a5bf16f48ed050ef5b'], // The id of the users you want to link + }) +); +``` + +::: + +::: tab One-to-One + +One-to-One relationships are useful when you have one entity that could be linked to only one other entity. _**And vice versa**_. + +#### Example + +A `user` can have one `address`. And this address is only related to this `user`. + +**Path β€”** `./api/user/models/User.settings.json`. + +```json +{ + "attributes": { + "address": { + "model": "address", + "via": "user" + } + } +} +``` + +**Path β€”** `./api/address/models/Address.settings.json`. + +```json +{ + "attributes": { + "user": { + "model": "user" + } + } +} +``` + +**Example** + +```js +// Create an address +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/addresses', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + user: '5c151d9d5b1d55194d3209be', // The id of the user you want to link + }) +); +``` + +::: + +::: tab One-to-Many + +One-to-Many relationships are useful when an entry can be linked to multiple entries of another Content Type. And an entry of the other Content Type can be linked to only one entry. + +#### Example + +A `user` can have many `articles`, and an `article` can be related to only one `user` (author). + +**Path β€”** `./api/user/models/User.settings.json`. + +```json +{ + "attributes": { + "articles": { + "collection": "article", + "via": "author" + } + } +} +``` + +**Path β€”** `./api/article/models/Article.settings.json`. + +```json +{ + "attributes": { + "author": { + "model": "user" + } + } +} +``` + +**Examples** + +```js +// Create an article +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/articles', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + author: '5c151d9d5b1d55194d3209be', // The id of the user you want to link + }) +); + +// Update an article +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/users/5c151d9d5b1d55194d3209be', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + articles: ['5c151d51eb28fd19457189f6', '5c151d51eb28fd19457189f8'], // Set of ALL articles linked to the user (existing articles + new article or - removed article) + }) +); +``` + +::: + +::: tab Many-to-Many + +Many-to-Many relationships are useful when an entry can be linked to multiple entries of another Content Type. And an entry of the other Content Type can be linked to many entries. + +#### Example + +A `product` can be related to many `categories` and a `category` can have many `products`. + +**Path β€”** `./api/product/models/Product.settings.json`. + +```json +{ + "attributes": { + "categories": { + "collection": "category", + "via": "products", + "dominant": true, + "collectionName": "products_categories__categories_products" // optional + } + } +} +``` + +**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`. + +```json +{ + "attributes": { + "products": { + "collection": "product", + "via": "categories" + } + } +} +``` + +**Example** + +```js +// Update a product +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/products/5c151d9d5b1d55194d3209be', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + categories: ['5c151d51eb28fd19457189f6', '5c151d51eb28fd19457189f8'], // Set of ALL categories linked to the product (existing categories + new category or - removed category). + }) +); +``` + +::: + +::: tab Polymorphic + +Polymorphic relationships are the solution when you don't know which kind of model will be associated to your entry, or when you want to connect different types of models to a model. +A common use case is an `Image` model that can be associated to different types of models (Article, Product, User, etc.). + +#### 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`. + +```json +{ + "attributes": { + "related": { + "collection": "*", + "filter": "field" + } + } +} +``` + +#### Filter + +The `filter` attribute is optional (but we highly recommend to use it every time). If it's provided it adds a new match level to retrieve the related data. + +For example, the `Product` model might have two attributes which are associated to the `Image` model. To distinguish which image is attached to the `cover` field and which images are attached to the `pictures` field, we need to save and provide this to the database. + +**Path β€”** `./api/article/models/Product.settings.json`. + +```json +{ + "attributes": { + "cover": { + "model": "image", + "via": "related" + }, + "pictures": { + "collection": "image", + "via": "related" + } + } +} +``` + +The value of the `filter` attribute is the name of the column where the information is stored. + +#### Example + +An `Image` model might belong to many `Article` models or `Product` models. + +**Path β€”** `./api/image/models/Image.settings.json`. + +```json +{ + "attributes": { + "related": { + "collection": "*", + "filter": "field" + } + } +} +``` + +**Path β€”** `./api/article/models/Article.settings.json`. + +```json +{ + "attributes": { + "avatar": { + "model": "image", + "via": "related" + } + } +} +``` + +**Path β€”** `./api/article/models/Product.settings.json`. + +```json +{ + "attributes": { + "pictures": { + "collection": "image", + "via": "related" + } + } +} +``` + +::: + +:::: + +## Components + +Component fields let your create a relation between your Content Type and a Component structure. + +#### Example + +Lets say we created an `openinghours` component in `restaurant` category. + +**Path β€”** `./api/restaurant/models/Restaurant.settings.json`. + +```json +{ + "attributes": { + "openinghours": { + "type": "component", + "repeatable": true, + "component": "restaurant.openinghours" + } + } +} +``` + +- `repeatable` (boolean): Could be `true` or `false` that let you create a list of data. +- `component` (string): It follows this format `.`. + +:::: tabs + +::: tab Create + +Create a restaurant with non-repeatable component + +```js +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/restaurants', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + openinghour: { + opening_at: '10am', + closing_at: '6pm', + day: 'monday', + }, + }) +); +``` + +Create a restaurant with repeatable component + +```js +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/restaurants', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + openinghours: [ + { + opening_at: '10am', + closing_at: '6pm', + day: 'monday', + }, + { + opening_at: '10am', + closing_at: '6pm', + day: 'tuesday', + }, + ], + }) +); +``` + +::: + +::: tab Update + +Update a restaurant with non-repeatable component + +```js +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/restaurants/1', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + openinghour: { + id: 1, // the ID of the entry + opening_at: '11am', + closing_at: '7pm', + day: 'wednesday', + }, + }) +); +``` + +Update a restaurant with repeatable component + +```js +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/restaurants/2', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + openinghours: [ + { + "id": 1 // the ID of the entry you want to update + "opening_at": "10am", + "closing_at": "6pm", + "day": "monday" + }, + { + "id": 2, // you also have to put the ID of entries you don't want to update + "opening_at": "10am", + "closing_at": "6pm", + "day": "tuesday" + } + ] + }) +); +``` + +**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 + +```js +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/restaurants/1', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + openinghour: null, + }) +); +``` + +Delete a restaurant with repeatable component + +```js +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/restaurants/2', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + openinghours: [ + { + "id": 1 // the ID of the entry you want to keep + "opening_at": "10am", + "closing_at": "6pm", + "day": "monday" + } + ] + }) +); +``` + +::: + +:::: + +## Dynamic Zone + +Dynamic Zone fields let you create a flexible space in which to compose content, based on a mixed list of components. + +#### Example + +Lets say we created an `slider` and `content` component in `article` category. + +**Path β€”** `./api/article/models/Article.settings.json`. + +```json +{ + "attributes": { + "body": { + "type": "dynamiczone", + "components": ["article.slider", "article.content"] + } + } +} +``` + +- `components` (array): Array of components that follows this format `.`. + +:::: tabs + +::: tab Create + +```js +const xhr = new XMLHttpRequest(); +xhr.open('POST', '/articles', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + body: [ + { + __component: 'article.content', + content: 'This is a content', + }, + { + __component: 'article.slider', + name: 'Slider name', + }, + ], + }) +); +``` + +::: + +::: tab Update + +```js +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/restaurant/2', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + body: [ + { + "id": 1 // the ID of the entry you want to update + "__component": "article.content", + "content": "This is an updated content", + }, + { + "id": 2, // you also have to put the ID of entries you don't want to update + "__component": "article.slider", + "name": "Slider name", + } + ] + }) +); +``` + +**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 + +```js +const xhr = new XMLHttpRequest(); +xhr.open('PUT', '/restaurant/2', true); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.send( + JSON.stringify({ + body: [ + { + "id": 1 // the ID of the entry you want to keep + "__component": "article.content", + "content": "This is an updated content", + } + ] + }) +); +``` + +::: + +:::: + +## Lifecycle hooks + +The lifecycle hooks are functions that get triggered when the Strapi [`queries`](../concepts/queries.md) are called. They will get triggered automatically when you manage your content in the Admin Panel or when you develop custom code using `queries`Β· + +To configure a `ContentType` lifecycle hook you can set a `lifecycles` key in the `{modelName}.js` file located in the `./api/{apiName}/models` folder. + +### Available Lifecycle hooks + +:::: 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)_ | + +::: + +:::: + +### Example + +**Path β€”** `./api/user/models/User.js`. + +```js +module.exports = { + /** + * Triggered before user creation. + */ + lifecycles: { + async beforeCreate(data) { + const passwordHashed = await strapi.api.user.services.user.hashPassword(data.password); + data.password = passwordHashed; + }, + }, +}; +``` + +::: tip +You can mutate one of the parameters to change its properties. Make sure not to reassign the parameter as it will have no effect: + +**This will Work** + +```js +module.exports = { + lifecycles: { + beforeCreate(data) { + data.name = 'Some fixed name'; + }, + }, +}; +``` + +**This will NOT Work** + +```js +module.exports = { + lifecycles: { + beforeCreate(data) { + data = { + ...data, + name: 'Some fixed name', + }; + }, + }, +}; +``` + +::: + +### Custom use + +When you are building custom ORM specific queries the lifecycles will not be triggered. You can however call a lifecycle function directly if you wish. + +**Bookshelf example** + +**Path -** `./api/{apiName}/services/{serviceName}.js` + +```js +module.exports = { + async createCustomEntry() { + const ORMModel = strapi.query(modelName).model; + + const newCustomEntry = await ORMModel.forge().save(); + + // trigger manually + ORMModel.lifecycles.afterCreate(newCustomEntry.toJSON()); + }, +}; +``` + +::: tip +When calling a lifecycle function directly, you will need to make sure you call it with the expected parameters. +::: diff --git a/developer-docs/latest/concepts/queries.md b/developer-docs/latest/concepts/queries.md new file mode 100644 index 0000000000..d268ecb503 --- /dev/null +++ b/developer-docs/latest/concepts/queries.md @@ -0,0 +1,417 @@ +# Queries + +Strapi provides a utility function `strapi.query` to make database queries. + +You can just call `strapi.query('modelName', 'pluginName')` to access the query API for any model. + +These queries handle for you specific Strapi features like `components`, `dynamic zones`, `filters` and `search`. + +## API Reference + +:::: tabs + +::: tab find + +### `find` + +This method returns a list of entries matching Strapi filters. +You can also pass a populate option to specify which relations you want to be populated. + +#### Examples + +**Find by id**: + +```js +strapi.query('restaurant').find({ id: 1 }); +``` + +**Find by in IN, with a limit**: + +```js +strapi.query('restaurant').find({ _limit: 10, id_in: [1, 2] }); +``` + +**Find by date orderBy name**: + +```js +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** + +```js +strapi.query('restaurant').find({ id_nin: [1], _start: 10 }, ['category', 'category.name']); +``` + +::: + +::: tab findOne + +### `findOne` + +This method returns the first entry matching some basic params. +You can also pass a populate option to specify which relations you want to be populated. + +#### Examples + +**Find one by id**: + +```js +strapi.query('restaurant').findOne({ id: 1 }); +``` + +**Find one by name**: + +```js +strapi.query('restaurant').findOne({ name: 'restaurant name' }); +``` + +**Find one by name and creation_date**: + +```js +strapi.query('restaurant').findOne({ name: 'restaurant name', date: '2019-01-01T00:00:00Z' }); +``` + +**Find one by id and populate a relation** + +```js +strapi.query('restaurant').findOne({ id: 1 }, ['category', 'category.name']); +``` + +::: + +::: tab create + +### `create` + +Creates an entry in the database and returns the entry. + +#### Example + +```js +strapi.query('restaurant').create({ + name: 'restaurant name', + // this is a dynamiczone field. the order is persisted in db. + content: [ + { + __component: 'blog.rich-text', + title: 'Some title', + subTitle: 'Some sub title', + }, + { + __component: 'blog.quote', + quote: 'Some interesting quote', + author: 1, + }, + ], + // this is a component field. the order is persisted in db. + opening_hours: [ + { + day_interval: 'Mon', + opening_hour: '7:00 PM', + closing_hour: '11:00 PM', + }, + { + day_interval: 'Tue', + opening_hour: '7:00 PM', + closing_hour: '11:00 PM', + }, + ], + // pass the id of a media to link it to the entry + cover: 1, + // automatically creates the relations when passing the ids in the field + reviews: [1, 2, 3], +}); +``` + +::: + +::: tab update + +### `update` + +Updates an entry in the database and returns the entry. + +#### Examples + +**Update by id** + +```js +strapi.query('restaurant').update( + { id: 1 }, + { + name: 'restaurant name', + content: [ + { + __component: 'blog.rich-text', + title: 'Some title', + subTitle: 'Some sub title', + }, + { + __component: 'blog.quote', + quote: 'Some interesting quote', + author: 1, + }, + ], + opening_hours: [ + { + day_interval: 'Mon', + opening_hour: '7:00 PM', + closing_hour: '11:00 PM', + }, + { + day_interval: 'Tue', + opening_hour: '7:00 PM', + closing_hour: '11:00 PM', + }, + ], + // pass the id of a media to link it to the entry + cover: 1, + // automatically creates the relations when passing the ids in the field + reviews: [1, 2, 3], + } +); +``` + +When updating an entry with its components or dynamic zones beware that if you send the components without any `id` the previous components will be deleted and replaced. You can update the components by sending their `id` with the rest of the fields: + +**Update by id and update previous components** + +```js +strapi.query('restaurant').update( + { id: 1 }, + { + name: 'Mytitle', + content: [ + { + __component: 'blog.rich-text', + id: 1, + title: 'Some title', + subTitle: 'Some sub title', + }, + { + __component: 'blog.quote', + id: 1, + quote: 'Some interesting quote', + author: 1, + }, + ], + opening_hours: [ + { + id: 2, + day_interval: 'Mon', + opening_hour: '7:00 PM', + closing_hour: '11:00 PM', + }, + { + id: 1, + day_interval: 'Tue', + opening_hour: '7:00 PM', + closing_hour: '11:00 PM', + }, + ], + // pass the id of a media to link it to the entry + cover: 1, + // automatically creates the relations when passing the ids in the field + reviews: [1, 2, 3], + } +); +``` + +**Partial update by name** + +```js +strapi.query('restaurant').update( + { title: 'specific name' }, + { + title: 'restaurant name', + } +); +``` + +::: + +::: tab delete + +### `delete` + +Deletes an entry and returns its value before deletion. +You can delete multiple entries at once with the passed params. + +#### Examples + +**Delete one by id** + +```js +strapi.query('restaurant').delete({ id: 1 }); +``` + +**Delete multiple by field** + +```js +strapi.query('restaurant').delete({ district: '_18th' }); +``` + +::: + +::: tab count + +### `count` + +Returns the count of entries matching Strapi filters. + +#### Examples + +**Count by district** + +```js +strapi.query('restaurant').count({ district: '_1st' }); +``` + +**Count by name contains** + +```js +strapi.query('restaurant').count({ name_contains: 'food' }); +``` + +**Count by date less than** + +```js +strapi.query('restaurant').count({ date_lt: '2019-08-01T00:00:00Z' }); +``` + +::: + +::: tab search + +### `search` + +Returns entries based on a search on all fields allowing it. (this feature will return all entries on sqlite). + +#### Examples + +**Search first ten starting at 20** + +```js +strapi.query('restaurant').search({ _q: 'my search query', _limit: 10, _start: 20 }); +``` + +**Search and sort** + +```js +strapi.query('restaurant').search({ _q: 'my search query', _limit: 100, _sort: 'date:desc' }); +``` + +::: + +::: tab countSearch + +### `countSearch` + +Returns the total count of entries based on a search. (this feature will return all entries on sqlite). + +#### Example + +```js +strapi.query('restaurant').countSearch({ _q: 'my search query' }); +``` + +::: + +:::: + +## Custom Queries + +When you want to customize your services or create new ones you will have to build your queries with the underlying ORM models. + +To access the underlying model: + +```js +strapi.query(modelName, plugin).model; +``` + +Then you can run any queries available on the model. You should refer to the specific ORM documentation for more details: + +:::: tabs + +::: tab SQL + +### Bookshelf + +Documentation: [https://bookshelfjs.org/](https://bookshelfjs.org/) + +**Example** + +```js +const result = await strapi + .query('restaurant') + .model.query(qb => { + qb.where('id', 1); + }) + .fetch(); + +const fields = result.toJSON(); +``` + +### Knex + +Knex.js can be used to build and make custom queries directly to the database. + +Documentation: [http://knexjs.org/#Builder](http://knexjs.org/#Builder) + +You can access the Knex instance with: + +```js +const knex = strapi.connections.default; +``` + +You can then use Knex to build your own custom queries. You will lose all the functionalities of the model, +but this could come handy if you are building a more custom schema. +Please note that if you are using the [draft system](draft-and-publish.md), Strapi nullyfies all the Draft columns util they are published. + +**Example** + +```js +const _ = require('lodash'); + +const knex = strapi.connections.default; +const result = await knex('restaurants') + .where('cities', 'berlin') + .whereNot('cities.published_at', null) + .join('chefs', 'restaurants.id', 'chefs.restaurant_id') + .select('restaurants.name as restaurant') + .select('chef.name as chef') + +// Lodash's groupBy method can be used to +// return a grouped key-value object generated from +// the response + +return (_.groupBy(result, 'chef'); +``` + +**We strongly suggest to sanitize any strings before making queries to the DB** +Never attempt to make a raw query with data coming straight from the front-end; if you +were looking for raw queries, please refer to [this section](http://knexjs.org/#Raw-Bindings) +of the documentation. + +::: + +::: tab MongoDB + +### Mongoose + +Documentation: [https://mongoosejs.com/](https://mongoosejs.com/) + +**Example** + +```js +const result = strapi.query('restaurant').model.find({ + date: { $gte: '2019-01-01T00.00.00Z' }, +}); + +const fields = result.map(entry => entry.toObject()); +``` + +::: + +:::: diff --git a/developer-docs/latest/concepts/services.md b/developer-docs/latest/concepts/services.md new file mode 100644 index 0000000000..6eabbeb7e0 --- /dev/null +++ b/developer-docs/latest/concepts/services.md @@ -0,0 +1,478 @@ +# Services + +## Concept + +Services are a set of reusable functions. They are particularly useful to respect the DRY (don’t repeat yourself) programming concept and to simplify [controllers](./controllers.md) logic. + +## Core services + +When you create a new `Content Type` or a new model, you will see a new empty service has been created. It is because Strapi builds a generic service for your models by default and allows you to override and extend it in the generated files. + +### Extending a Model Service + +Here are the core methods (and their current implementation). +You can simply copy and paste this code to your own service file to customize the methods. + +You can read about `strapi.query` calls [here](./queries.md). + +::: tip +In the following example your controller, service and model are named `restaurant`. +::: + +#### Utils + +If you're extending the `create` or `update` service, first require the following utility function: + +```js +const { isDraft } = require('strapi-utils').contentTypes; +``` + +- `isDraft`: This function checks if the entry is a draft. + + +#### Collection Type + +:::: tabs + +::: tab find + +#### `find` + +```js +module.exports = { + /** + * Promise to fetch all records + * + * @return {Promise} + */ + find(params, populate) { + return strapi.query('restaurant').find(params, populate); + }, +}; +``` + +- `params` (object): this represent filters for your find request.
+ The object follow the URL query format, [refer to this documentation.](../content-api/parameters.md). + +```json +{ + "name": "Tokyo Sushi" +} +// or +{ + "_limit": 20, + "name_contains": "sushi" +} +``` + +- `populate` (array): you have to mention data you want populate `["author", "author.name", "comment", "comment.content"]` + +::: + +::: tab findOne + +#### `findOne` + +```js +module.exports = { + /** + * Promise to fetch record + * + * @return {Promise} + */ + + findOne(params, populate) { + return strapi.query('restaurant').findOne(params, populate); + }, +}; +``` + +- `params` (object): this represent filters for your find request.
+ The object follow the URL query format, [refer to this documentation.](../content-api/parameters.md). + +```json +{ + "name": "Tokyo Sushi" +} +// or +{ + "name_contains": "sushi" +} +``` + +- `populate` (array): you have to mention data you want populate `["author", "author.name", "comment", "comment.content"]` + +::: + +::: tab count + +#### `count` + +```js +module.exports = { + /** + * Promise to count record + * + * @return {Promise} + */ + + count(params) { + return strapi.query('restaurant').count(params); + }, +}; +``` + +- `params` (object): this represent filters for your find request.
+ The object follow the URL query format, [refer to this documentation.](../content-api/parameters.md). + +```json +{ + "name": "Tokyo Sushi" +} +// or +{ + "name_contains": "sushi" +} +``` + +::: + +::: tab create + +#### `create` + +```js +const { isDraft } = require('strapi-utils').contentTypes; + +module.exports = { + /** + * Promise to add record + * + * @return {Promise} + */ + + async create(data, { files } = {}) { + const isDraft = isDraft(data, strapi.models.restaurant); + const validData = await strapi.entityValidator.validateEntityCreation( + strapi.models.restaurant, + data, + { isDraft } + ); + + const entry = await strapi.query('restaurant').create(validData); + + if (files) { + // automatically uploads the files based on the entry and the model + await strapi.entityService.uploadFiles(entry, files, { + model: 'restaurant', + // if you are using a plugin's model you will have to add the `source` key (source: 'users-permissions') + }); + return this.findOne({ id: entry.id }); + } + + return entry; + }, +}; +``` + +::: + +::: tab update + +#### `update` + +```js +const { isDraft } = require('strapi-utils').contentTypes; + +module.exports = { + /** + * Promise to edit record + * + * @return {Promise} + */ + + async update(params, data, { files } = {}) { + const existingEntry = await db.query('restaurant').findOne(params); + + const isDraft = isDraft(existingEntry, strapi.models.restaurant); + const validData = await strapi.entityValidator.validateEntityUpdate( + strapi.models.restaurant, + data, + { isDraft } + ); + + const entry = await strapi.query('restaurant').update(params, validData); + + if (files) { + // automatically uploads the files based on the entry and the model + await strapi.entityService.uploadFiles(entry, files, { + model: 'restaurant', + // if you are using a plugin's model you will have to add the `source` key (source: 'users-permissions') + }); + return this.findOne({ id: entry.id }); + } + + return entry; + }, +}; +``` + +- `params` (object): it should look like this `{id: 1}` + +::: + +::: tab delete + +#### `delete` + +```js +module.exports = { + /** + * Promise to delete a record + * + * @return {Promise} + */ + + delete(params) { + return strapi.query('restaurant').delete(params); + }, +}; +``` + +- `params` (object): it should look like this `{id: 1}` + +::: + +::: tab search + +#### `search` + +```js +module.exports = { + /** + * Promise to search records + * + * @return {Promise} + */ + + search(params) { + return strapi.query('restaurant').search(params); + }, +}; +``` + +- `params` (object): this represent filters for your find request.
+ The object follow the URL query format, [refer to this documentation.](../content-api/parameters.md). + +```json +{ + "name": "Tokyo Sushi" +} +// or +{ + "name_contains": "sushi" +} +``` + +::: + +::: tab countSearch + +#### `countSearch` + +```js +module.exports = { + /** + * Promise to count searched records + * + * @return {Promise} + */ + countSearch(params) { + return strapi.query('restaurant').countSearch(params); + }, +}; +``` + +- `params` (object): this represent filters for your find request.
+ The object follow the URL query format, [refer to this documentation.](../content-api/parameters.md). + +```json +{ + "name": "Tokyo Sushi" +} +// or +{ + "name_contains": "sushi" +} +``` + +::: + +:::: + +#### Single Type + +:::: tabs + +::: tab find + +#### `find` + +```js +const _ = require('lodash'); + +module.exports = { + /** + * Promise to fetch the record + * + * @return {Promise} + */ + async find(populate) { + const results = await strapi.query('restaurant').find({ _limit: 1 }, populate); + return _.first(results) || null; + }, +}; +``` + +- `populate` (array): you have to mention data you want populate `["author", "author.name", "comment", "comment.content"]` + +::: + +::: tab createOrUpdate + +#### `createOrUpdate` + +```js +const _ = require('lodash'); + +module.exports = { + /** + * Promise to add/update the record + * + * @return {Promise} + */ + + async createOrUpdate(data, { files } = {}) { + const results = await strapi.query('restaurant').find({ _limit: 1 }); + const entity = _.first(results) || null; + + let entry; + if (!entity) { + entry = await strapi.query('restaurant').create(data); + } else { + entry = await strapi.query('restaurant').update({ id: entity.id }, data); + } + + if (files) { + // automatically uploads the files based on the entry and the model + await strapi.entityService.uploadFiles(entry, files, { + model: 'restaurant', + // if you are using a plugin's model you will have to add the `plugin` key (plugin: 'users-permissions') + }); + return this.findOne({ id: entry.id }); + } + + return entry; + }, +}; +``` + +::: + +::: tab delete + +#### `delete` + +```js +module.exports = { + /** + * Promise to delete a record + * + * @return {Promise} + */ + + delete() { + const results = await strapi.query('restaurant').find({ _limit: 1 }); + const entity = _.first(results) || null; + + if (!entity) return; + + return strapi.query('restaurant').delete({id: entity.id}); + }, +}; +``` + +::: + +:::: + +## Custom services + +You can also create custom services to build your own business logic. + +### How to create a custom service + +There are two ways to create a service. + +- Using the CLI `strapi generate:service restaurant`.
Read the [CLI documentation](../cli/CLI.md) for more information. +- Manually create a JavaScript file named in `./api/**/services/`. + +#### Example + +The goal of a service is to store reusable functions. An `email` service could be useful to send emails from different functions in our codebase: + +**Path β€”** `./api/email/services/Email.js`. + +```js +const nodemailer = require('nodemailer'); + +// Create reusable transporter object using SMTP transport. +const transporter = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'user@gmail.com', + pass: 'password', + }, +}); + +module.exports = { + send: (from, to, subject, text) => { + // Setup e-mail data. + const options = { + from, + to, + subject, + text, + }; + + // Return a promise of the function that sends the email. + return transporter.sendMail(options); + }, +}; +``` + +::: tip +please make sure you installed `nodemailer` (`npm install nodemailer`) for this example. +::: + +The service is now available through the `strapi.services` global variable. We can use it in another part of our codebase. For example a controller like below: + +**Path β€”** `./api/user/controllers/User.js`. + +```js +module.exports = { + // GET /hello + signup: async ctx => { + // Store the new user in database. + const user = await User.create(ctx.query); + + // Send an email to validate his subscriptions. + strapi.services.email.send('welcome@mysite.com', user.email, 'Welcome', '...'); + + // Send response to the server. + ctx.send({ + ok: true, + }); + }, +}; +``` diff --git a/developer-docs/latest/concepts/sso.md b/developer-docs/latest/concepts/sso.md new file mode 100644 index 0000000000..3b387732f8 --- /dev/null +++ b/developer-docs/latest/concepts/sso.md @@ -0,0 +1,443 @@ +# Single Sign On [](https://strapi.io/pricing) + +Single-Sign-On on Strapi allows you to configure additional sign-in and sign-up methods for your administration panel. + +::: warning CAUTION +It is currently not possible to associate a unique SSO provider to an email address used for a Strapi account, meaning that the access to a Strapi account cannot be restricted to only one SSO provider. For more information and workarounds to solve this issue, [please refer to the dedicated GitHub issue](https://github.com/strapi/strapi/issues/9466#issuecomment-783587648). +::: + +## Prerequisites + +- A Strapi application running on version 3.5.0 or higher is required. +- To configure SSO on your application, you will need an EE license with a Gold plan. +- Make sure Strapi is part of the applications you can access with your provider. For example, with Microsoft (Azure) Active Directory, you must first ask someone with the right permissions to add Strapi to the list of allowed applications. Please refer to your provider(s) documentation to learn more about that. + +## Usage + +SSO configuration lives in the server configuration of your application found within `/config/server.js`. + +### Accessing the configuration + +The providers' configuration should be written within the `admin.auth.providers` path of the server configuration. + +`admin.auth.providers` is an array of [provider configuration](#provider-configuration). + +```javascript +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + providers: [], // The providers' configuration lives there + }, + }, +}); +``` + +### Provider Configuration + +A provider's configuration is a Javascript object built with the following properties: + +| Name | Required | Type | Description | +|----------------- |----------|----------|--------------------------------------------------------------------------------| +| `uid` | true | string | The UID of the strategy. It must match the strategy's name | +| `displayName` | true | string | The name that will be used on the login page to reference the provider | +| `icon` | false | string | An image URL. If specified, it will replace the displayName on the login page | +| `createStrategy` | true | function | A factory that will build and return a new passport strategy for your provider. Takes the strapi instance as parameter | + +::: tip +The `uid` property is the unique identifier of each strategy and is generally found in the strategy's package. If you are not sure of what it refers to, please contact the maintainer of the strategy. +::: + +#### The `createStrategy` Factory + +A passport strategy is usually built by instantiating it using 2 parameters: the configuration object, and the verify function. + +##### Configuration Object + +The configuration object depends on the strategy needs, but often asks for a callback URL to be redirected to once the connection has been made on the provider side. + +You can generate a specific callback URL for your provider using the `getStrategyCallbackURL` method. This URL also needs to be written on the provider side in order to allow redirection from it. + +The format of the callback URL is the following: `/admin/connect/`. + +::: tip +`strapi.admin.services.passport.getStrategyCallbackURL` is a Strapi helper you can use to get a callback URL for a specific provider. It takes a provider name as a parameter and returns a URL. +::: + +If needed, this is also where you will put your client ID and secret key for your OAuth2 application. + +##### Verify Function + +The verify function is used here as a middleware allowing the user to transform and make extra processing on the data returned from the provider API. + +This function always takes a `done` method as last parameter which is used to transfer needed data to the Strapi layer of SSO. + +Its signature is the following: `void done(error: any, data: object);` and it follows the following rules: + +- If `error` is not set to `null`, then the data sent is ignored, and the controller will throw an error. +- If the SSO's auto-registration feature is disabled, then the `data` object only need to be composed of an `email` property. +- If the SSO's auto-registration feature is enabled, then you will need to define (in addition to the `email`) either a `username` property or both `firstname` and `lastname` within the `data` oject. + +### Adding a provider + +Adding a new provider means adding a new way for your administrators to log-in. + +To achieve a great flexibility and a large choice of provider, Strapi uses [Passport.js](http://www.passportjs.org/). Any valid passport strategy that doesn't need additional custom data should therefore work with Strapi. + +::: warning +Strategies such as [ldapauth](https://github.com/vesse/passport-ldapauth) don't work out of the box since they require extra data to be sent from the admin panel. +If you want to add an LDAP provider to your application, you will need to write a [custom strategy](http://www.passportjs.org/packages/passport-custom/). +You can also use services such as Okta and Auth0 as bridge services. +::: + +### Configuring the provider + +To configure a provider, follow the procedure below: + +1. Make sure to import your strategy in your server configuration file, either from an installed package or a local file. +2. You'll need to add a new item to the `admin.auth.providers` array in your server configuration that will match the [format given above](#provider-configuration) +3. Restart your application, the provider should appear on your admin login page. + +## Examples + +:::::: tabs + +::::: tab Google + +Using: [passport-google-oauth2](https://github.com/mstade/passport-google-oauth2) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-google-oauth2 +``` + +::: + +::: tab npm + +```bash +npm install --save passport-google-oauth2 +``` + +:::: + +`/config/server.js` + +```jsx +'use strict'; + +const GoogleStrategy = require('passport-google-oauth2'); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + /// ... + providers: [ + { + uid: 'google', + displayName: 'Google', + icon: 'https://cdn2.iconfinder.com/data/icons/social-icons-33/128/Google-512.png' , + createStrategy: strapi => new GoogleStrategy({ + clientID: env('GOOGLE_CLIENT_ID'), + clientSecret: env('GOOGLE_CLIENT_SECRET'), + scope: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile' + ], + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL('google') + }, (request, accessToken, refreshToken, profile, done) => { + done(null, { + email: profile.email, + firstname: profile.given_name, + lastname: profile.family_name, + }); + } + ), + }, + ], + }, + }, +}); +``` + +::::: + +::::: tab Github + +Using: [passport-github](https://github.com/cfsghost/passport-github) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-github2 +``` + +::: + +::: tab npm + +```bash +npm install --save passport-github2 +``` + +::: + +:::: + +`/config/server.js` + +```jsx +'use strict'; + +const GithubStrategy = require('passport-github2'); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + // ... + providers: [ + { + uid: 'github', + displayName: 'Github', + icon: 'https://cdn1.iconfinder.com/data/icons/logotypes/32/github-512.png', + createStrategy: strapi => new GithubStrategy({ + clientID: env('GITHUB_CLIENT_ID'), + clientSecret: env('GITHUB_CLIENT_SECRET'), + scope: ['user:email'], + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL('github'), + }, (accessToken, refreshToken, profile, done) => { + done(null, { + email: profile.emails[0].value, + username: profile.username, + }); + }), + }, + ], + }, + }, +}); +``` +::::: + +::::: tab Discord + +Using: [passport-discord](https://github.com/nicholastay/passport-discord#readme) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-discord +``` + +::: + +::: tab npm + +```bash +npm install --save passport-discord +``` + +::: + +:::: + +`/config/server.js` + +```jsx +"use strict"; + +const DiscordStrategy = require("passport-discord"); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + // ... + providers: [ + { + uid: 'discord', + displayName: 'Discord', + icon: 'https://cdn0.iconfinder.com/data/icons/free-social-media-set/24/discord-512.png', + createStrategy: strapi => + new DiscordStrategy( + { + clientID: env('DISCORD_CLIENT_ID'), + clientSecret: env('DISCORD_SECRET'), + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL( + 'discord' + ), + scope: ['identify', 'email'], + }, + (accessToken, refreshToken, profile, done) => { + done(null, { + email: profile.email, + username: `${profile.username}#${profile.discriminator}`, + }); + } + ), + }, + ], + }, + }, +}); +``` +::::: +::::: tab Microsoft + +Using: [passport-azure-ad-oauth2](https://github.com/auth0/passport-azure-ad-oauth2#readme) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-azure-ad-oauth2 jsonwebtoken +``` + +::: + +::: tab npm + +```bash +npm install --save passport-azure-ad-oauth2 jsonwebtoken +``` + +::: + +:::: + +`/config/server.js` + +```jsx +"use strict"; + +const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2'); +const jwt = require('jsonwebtoken'); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + // ... + providers: [ + { + uid: 'azure_ad_oauth2', + displayName: 'Microsoft', + icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Microsoft_logo_%282012%29.svg/320px-Microsoft_logo_%282012%29.svg.png', + createStrategy: strapi => new AzureAdOAuth2Strategy({ + clientID: env('MICROSOFT_CLIENT_ID', ''), + clientSecret: env('MICROSOFT_CLIENT_SECRET', ''), + scope: ['user:email'], + tenant: env('MICROSOFT_TENANT_ID', ''), + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL('azure_ad_oauth2'), + }, (accessToken, refreshToken, params, profile, done) => { + var waadProfile = jwt.decode(params.id_token, '', true); + done(null, { + email: waadProfile.upn, + username: waadProfile.upn + }); + }), + }, + ], + }, + }, +}); +``` +::::: +:::::: + +## Advanced Customization + +### Admin Panel URL + +If your administration panel lives on a different host/port than your Strapi server, you will need to modify the admin URL. +To do so, head to your `/config/server.js` configuration file and tweak the `admin.url` field. + +For example, if your admin application has been started on `https://api.example.com`, your configuration will look like the following: + +`/config/server.js` + +```javascript +module.exports = () => ({ + // ... + admin: { + // ... + url: 'https://api.example.com/admin', + }, +}); +``` + +### Custom Logic + +In some scenarios, you will want to write additional logic for your connection workflow such as: +- Restricting connection and registration for a specific domain +- Triggering actions on connection attempt +- Analytics + +The easiest way to do so is to plug into the verify function of your strategy and write some code. + +For example, if you want to allow only people with an official strapi.io email address, you can instantiate your strategy like this: + +```javascript +const strategyInstance = new Strategy( + configuration, + ({ email, username }, done) => { + // If the email ends with @strapi.io + if (email.endsWith('@strapi.io')) { + // Then we continue with the data given by the provider + return done(null, { email, username }); + } + + // Otherwise, we continue by sending an error to the done function + done(new Error('Forbidden email address')); + }, +); +``` + +### Authentication Events + +The SSO feature adds a new [authentication event](configurations.html#available-options): `onSSOAutoRegistration`. + +This event is triggered whenever a user is created using the auto-register feature added by SSO. +It contains the created user (`event.user`), and the provider used to make the registration (`event.provider`). + +Example: + +`/config/server.js` +```javascript +module.exports = () => ({ + // ... + admin: { + // ... + auth: { + // ... + events: { + onConnectionSuccess(e) {}, + onConnectionError(e) {}, + // ... + onSSOAutoRegistration(e) { + const { user, provider } = e; + + console.log( + `A new user (${user.id}) has been automatically registered using ${provider}` + ); + }, + }, + }, + }, +}); +``` diff --git a/developer-docs/latest/migration-guide/README.md b/developer-docs/latest/migration-guide/README.md new file mode 100644 index 0000000000..7d131ebe6b --- /dev/null +++ b/developer-docs/latest/migration-guide/README.md @@ -0,0 +1,87 @@ +# Migrations guides + +Please also refer to the following [update guide](../guides/update-version.md) for a better understanding of how to update your project. + +**Pay special attention to the [note](../guides/update-version.md#extensions) about upgrading if you are using the extensions system.** + +## Instructions + +When upgrading you will need to follow every migration guide between your current version and the one you are upgrading to. + +**Example** + +If you were upgrading from the `3.0.0-beta.19.5` to `3.2.0`, here are the following guides you would have to follow: + +- Migration guide from beta.19.4+ to beta.20. +- Migration guide from beta.20+ to stable. +- Migration guide from 3.0.x to 3.1.x. +- Migration guide from 3.1.x to 3.2.3. + +## V3 guides + +- [Migration guide from 3.4.x to 3.4.4](migration-guide-3.4.x-to-3.4.4.md) +- [Migration guide from 3.3.x to 3.4.0](migration-guide-3.3.x-to-3.4.0.md) +- [Migration guide from 3.2.5 to 3.3.0](migration-guide-3.2.5-to-3.3.0.md) +- [Migration guide from 3.2.3 to 3.2.4](migration-guide-3.2.3-to-3.2.4.md) +- [Migration guide from 3.1.x to 3.2.3](migration-guide-3.1.x-to-3.2.x.md) +- [Migration guide from 3.0.x to 3.1.x](migration-guide-3.0.x-to-3.1.x.md) + + +## Beta guides + +::: warning + +The Strapi Beta version is no longer supported, you should upgrade to the V3 Stable. + +::: + +- [Migration guide from beta.20+ to stable](migration-guide-beta.20-to-3.0.0.md) +- [Migration guide from beta.19.4+ to beta.20](migration-guide-beta.19-to-beta.20.md) +- [Migration guide from beta.19+ to beta.19.4](migration-guide-beta.19-to-beta.19.4.md) +- [Migration guide from beta.18 to beta.19](migration-guide-beta.18-to-beta.19.md) +- [Migration guide from beta.17+ to beta.18](migration-guide-beta.17-to-beta.18.md) +- [Migration guide from beta.16+ to beta.17.4](migration-guide-beta.16-to-beta.17.4.md) +- [Migration guide from beta.15 to beta.16](migration-guide-beta.15-to-beta.16.md) + +## Alpha guides + +::: warning + +The Strapi Alpha version is no longer supported, you should upgrade to the V3 Stable. +If you have issues upgrading, it's our general recommendation to create a new project. + +::: + +- [Migrating from v1 to v3](migration-guide-1-to-3.md) +- [Migration guide from alpha.7.4 to alpha.8](migration-guide-alpha.7.4-to-alpha.8.md) +- [Migration guide from alpha.8 to alpha.9](migration-guide-alpha.8-to-alpha.9.md) +- [Migration guide from alpha.9 to alpha.10](migration-guide-alpha.9-to-alpha.10.md) +- [Migration guide from alpha.10 to alpha.11](migration-guide-alpha.10-to-alpha.11.md) +- [Migration guide from alpha.11 to alpha.12](migration-guide-alpha.11-to-alpha.12.md) +- [Migration guide from alpha.12.1 to alpha.12.2](migration-guide-alpha.12.1-to-alpha.12.2.md) +- [Migration guide from alpha.12.2 to alpha.12.3](migration-guide-alpha.12.2-to-alpha.12.3.md) +- [Migration guide from alpha.12.3 to alpha.12.4](migration-guide-alpha.12.3-to-alpha.12.4.md) +- [Migration guide from alpha.12.4 to alpha.12.5](migration-guide-alpha.12.4-to-alpha.12.5.md) +- [Migration guide from alpha.12.5 to alpha.12.6](migration-guide-alpha.12.5-to-alpha.12.6.md) +- [Migration guide from alpha.12.6 to alpha.12.7](migration-guide-alpha.12.6-to-alpha.12.7.md) +- [Migration guide from alpha.12.7 to alpha.13](migration-guide-alpha.12.7-to-alpha.13.md) +- [Migration guide from alpha.13 to alpha.13.1](migration-guide-alpha.13-to-alpha.13.1.md) +- [Migration guide from alpha.13.1 to alpha.14](migration-guide-alpha.13.1-to-alpha.14.md) +- [Migration guide from alpha.14 to alpha.14.1](migration-guide-alpha.14-to-alpha.14.1.md) +- [Migration guide from alpha.14.1 to alpha.14.2](migration-guide-alpha.14.1-to-alpha.14.2.md) +- [Migration guide from alpha.14.2 to alpha.14.3](migration-guide-alpha.14.2-to-alpha.14.3.md) +- [Migration guide from alpha.14.3 to alpha.14.4](migration-guide-alpha.14.3-to-alpha.14.4.md) +- [Migration guide from alpha.14.4 to alpha.14.5](migration-guide-alpha.14.4-to-alpha.14.5.md) +- [Migration guide from alpha.14.5 to alpha.15](migration-guide-alpha.14.5-to-alpha.15.md) +- [Migration guide from alpha.15 to alpha.16](migration-guide-alpha.15-to-alpha.16.md) +- [Migration guide from alpha.16 to alpha.17](migration-guide-alpha.16-to-alpha.17.md) +- [Migration guide from alpha.17 to alpha.18](migration-guide-alpha.17-to-alpha.18.md) +- [Migration guide from alpha.18 to alpha.19](migration-guide-alpha.18-to-alpha.19.md) +- [Migration guide from alpha.19 to alpha.20](migration-guide-alpha.19-to-alpha.20.md) +- [Migration guide from alpha.20 to alpha.21](migration-guide-alpha.20-to-alpha.21.md) +- [Migration guide from alpha.21 to alpha.22](migration-guide-alpha.21-to-alpha.22.md) +- [Migration guide from alpha.22 to alpha.23](migration-guide-alpha.22-to-alpha.23.md) +- [Migration guide from alpha.23 to alpha.24](migration-guide-alpha.23-to-alpha.24.md) +- [Migration guide from alpha.24 to alpha.25](migration-guide-alpha.24-to-alpha.25.md) +- [Migration guide from alpha.25 to alpha.26](migration-guide-alpha.25-to-alpha.26.md) +- [Migration guide from alpha.26 to beta](migration-guide-alpha.26-to-beta.md) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index d4deb84780..3c53573bcc 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -135,6 +135,11 @@ const sidebar = { title: 'Plugins', children: [['/user-docs/latest/plugins/introduction-to-plugins', 'Introduction to plugins']], }, + { + collapsable: false, + title: 'General settings', + children: [['/latest/settings/managing-global-settings', 'Managing global settings']], + }, ], }; @@ -155,7 +160,7 @@ const checklinksIgnoredFiles = [ './developer-docs/latest/setup-deployment-guides/configurations.md', // line 940 './developer-docs/latest/developer-resources/content-api/content-api.md', // line 810 './developer-docs/latest/update-migration-guides/migration-guides/migration-guide-beta.20-to-3.0.0.md', // line 93 -] +]; module.exports = { title: '', @@ -169,11 +174,11 @@ module.exports = { ga: 'UA-54313258-1', }, 'check-md': { - ignore: checklinksIgnoredFiles + ignore: checklinksIgnoredFiles, }, seo: { siteTitle: (_, $site) => $site.title, - title: ($page) => $page.title, + title: $page => $page.title, }, }, head: [ diff --git a/docs/developer-docs/latest/concepts/sso.md b/docs/developer-docs/latest/concepts/sso.md new file mode 100644 index 0000000000..53274695da --- /dev/null +++ b/docs/developer-docs/latest/concepts/sso.md @@ -0,0 +1,443 @@ +# Single Sign On [](https://strapi.io/pricing) + +Single-Sign-On on Strapi allows you to configure additional sign-in and sign-up methods for your administration panel. + +::: warning CAUTION +It is currently not possible to associate a unique SSO provider to an email address used for a Strapi account, meaning that the access to a Strapi account cannot be restricted to only one SSO provider. For more information and workarounds to solve this issue, [please refer to the dedicated GitHub issue](https://github.com/strapi/strapi/issues/9466#issuecomment-783587648). +::: + +## Prerequisites + +- A Strapi application running on version 3.5.0 or higher is required. +- To configure SSO on your application, you will need an EE license with a Gold plan. +- Make sure Strapi is part of the applications you can access with your provider. For example, with Microsoft (Azure) Active Directory, you must first ask someone with the right permissions to add Strapi to the list of allowed applications. Please refer to your provider(s) documentation to learn more about that. + +## Usage + +SSO configuration lives in the server configuration of your application found within `/config/server.js`. + +### Accessing the configuration + +The providers' configuration should be written within the `admin.auth.providers` path of the server configuration. + +`admin.auth.providers` is an array of [provider configuration](#provider-configuration). + +```javascript +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + providers: [], // The providers' configuration lives there + }, + }, +}); +``` + +### Provider Configuration + +A provider's configuration is a Javascript object built with the following properties: + +| Name | Required | Type | Description | +|----------------- |----------|----------|--------------------------------------------------------------------------------| +| `uid` | true | string | The UID of the strategy. It must match the strategy's name | +| `displayName` | true | string | The name that will be used on the login page to reference the provider | +| `icon` | false | string | An image URL. If specified, it will replace the displayName on the login page | +| `createStrategy` | true | function | A factory that will build and return a new passport strategy for your provider. Takes the strapi instance as parameter | + +::: tip +The `uid` property is the unique identifier of each strategy and is generally found in the strategy's package. If you are not sure of what it refers to, please contact the maintainer of the strategy. +::: + +#### The `createStrategy` Factory + +A passport strategy is usually built by instantiating it using 2 parameters: the configuration object, and the verify function. + +##### Configuration Object + +The configuration object depends on the strategy needs, but often asks for a callback URL to be redirected to once the connection has been made on the provider side. + +You can generate a specific callback URL for your provider using the `getStrategyCallbackURL` method. This URL also needs to be written on the provider side in order to allow redirection from it. + +The format of the callback URL is the following: `/admin/connect/`. + +::: tip +`strapi.admin.services.passport.getStrategyCallbackURL` is a Strapi helper you can use to get a callback URL for a specific provider. It takes a provider name as a parameter and returns a URL. +::: + +If needed, this is also where you will put your client ID and secret key for your OAuth2 application. + +##### Verify Function + +The verify function is used here as a middleware allowing the user to transform and make extra processing on the data returned from the provider API. + +This function always takes a `done` method as last parameter which is used to transfer needed data to the Strapi layer of SSO. + +Its signature is the following: `void done(error: any, data: object);` and it follows the following rules: + +- If `error` is not set to `null`, then the data sent is ignored, and the controller will throw an error. +- If the SSO's auto-registration feature is disabled, then the `data` object only need to be composed of an `email` property. +- If the SSO's auto-registration feature is enabled, then you will need to define (in addition to the `email`) either a `username` property or both `firstname` and `lastname` within the `data` oject. + +### Adding a provider + +Adding a new provider means adding a new way for your administrators to log-in. + +To achieve a great flexibility and a large choice of provider, Strapi uses [Passport.js](http://www.passportjs.org/). Any valid passport strategy that doesn't need additional custom data should therefore work with Strapi. + +::: warning +Strategies such as [ldapauth](https://github.com/vesse/passport-ldapauth) don't work out of the box since they require extra data to be sent from the admin panel. +If you want to add an LDAP provider to your application, you will need to write a [custom strategy](http://www.passportjs.org/packages/passport-custom/). +You can also use services such as Okta and Auth0 as bridge services. +::: + +### Configuring the provider + +To configure a provider, follow the procedure below: + +1. Make sure to import your strategy in your server configuration file, either from an installed package or a local file. +2. You'll need to add a new item to the `admin.auth.providers` array in your server configuration that will match the [format given above](#provider-configuration) +3. Restart your application, the provider should appear on your admin login page. + +## Examples + +:::::: tabs + +::::: tab Google + +Using: [passport-google-oauth2](https://github.com/mstade/passport-google-oauth2) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-google-oauth2 +``` + +::: + +::: tab npm + +```bash +npm install --save passport-google-oauth2 +``` + +:::: + +`/config/server.js` + +```jsx +'use strict'; + +const GoogleStrategy = require('passport-google-oauth2'); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + /// ... + providers: [ + { + uid: 'google', + displayName: 'Google', + icon: 'https://cdn2.iconfinder.com/data/icons/social-icons-33/128/Google-512.png' , + createStrategy: strapi => new GoogleStrategy({ + clientID: env('GOOGLE_CLIENT_ID'), + clientSecret: env('GOOGLE_CLIENT_SECRET'), + scope: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile' + ], + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL('google') + }, (request, accessToken, refreshToken, profile, done) => { + done(null, { + email: profile.email, + firstname: profile.given_name, + lastname: profile.family_name, + }); + } + ), + }, + ], + }, + }, +}); +``` + +::::: + +::::: tab Github + +Using: [passport-github](https://github.com/cfsghost/passport-github) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-github2 +``` + +::: + +::: tab npm + +```bash +npm install --save passport-github2 +``` + +::: + +:::: + +`/config/server.js` + +```jsx +'use strict'; + +const GithubStrategy = require('passport-github2'); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + // ... + providers: [ + { + uid: 'github', + displayName: 'Github', + icon: 'https://cdn1.iconfinder.com/data/icons/logotypes/32/github-512.png', + createStrategy: strapi => new GithubStrategy({ + clientID: env('GITHUB_CLIENT_ID'), + clientSecret: env('GITHUB_CLIENT_SECRET'), + scope: ['user:email'], + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL('github'), + }, (accessToken, refreshToken, profile, done) => { + done(null, { + email: profile.emails[0].value, + username: profile.username, + }); + }), + }, + ], + }, + }, +}); +``` +::::: + +::::: tab Discord + +Using: [passport-discord](https://github.com/nicholastay/passport-discord#readme) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-discord +``` + +::: + +::: tab npm + +```bash +npm install --save passport-discord +``` + +::: + +:::: + +`/config/server.js` + +```jsx +"use strict"; + +const DiscordStrategy = require("passport-discord"); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + // ... + providers: [ + { + uid: 'discord', + displayName: 'Discord', + icon: 'https://cdn0.iconfinder.com/data/icons/free-social-media-set/24/discord-512.png', + createStrategy: strapi => + new DiscordStrategy( + { + clientID: env('DISCORD_CLIENT_ID'), + clientSecret: env('DISCORD_SECRET'), + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL( + 'discord' + ), + scope: ['identify', 'email'], + }, + (accessToken, refreshToken, profile, done) => { + done(null, { + email: profile.email, + username: `${profile.username}#${profile.discriminator}`, + }); + } + ), + }, + ], + }, + }, +}); +``` +::::: +::::: tab Microsoft + +Using: [passport-azure-ad-oauth2](https://github.com/auth0/passport-azure-ad-oauth2#readme) + +:::: tabs + +::: tab yarn + +```bash +yarn add passport-azure-ad-oauth2 jsonwebtoken +``` + +::: + +::: tab npm + +```bash +npm install --save passport-azure-ad-oauth2 jsonwebtoken +``` + +::: + +:::: + +`/config/server.js` + +```jsx +"use strict"; + +const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2'); +const jwt = require('jsonwebtoken'); + +module.exports = ({ env }) => ({ + // ... + admin: { + // ... + auth: { + // ... + providers: [ + { + uid: 'azure_ad_oauth2', + displayName: 'Microsoft', + icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Microsoft_logo_%282012%29.svg/320px-Microsoft_logo_%282012%29.svg.png', + createStrategy: strapi => new AzureAdOAuth2Strategy({ + clientID: env('MICROSOFT_CLIENT_ID', ''), + clientSecret: env('MICROSOFT_CLIENT_SECRET', ''), + scope: ['user:email'], + tenant: env('MICROSOFT_TENANT_ID', ''), + callbackURL: strapi.admin.services.passport.getStrategyCallbackURL('azure_ad_oauth2'), + }, (accessToken, refreshToken, params, profile, done) => { + var waadProfile = jwt.decode(params.id_token, '', true); + done(null, { + email: waadProfile.upn, + username: waadProfile.upn + }); + }), + }, + ], + }, + }, +}); +``` +::::: +:::::: + +## Advanced Customization + +### Admin Panel URL + +If your administration panel lives on a different host/port than your Strapi server, you will need to modify the admin URL. +To do so, head to your `/config/server.js` configuration file and tweak the `admin.url` field. + +For example, if your admin application has been started on `https://api.example.com`, your configuration will look like the following: + +`/config/server.js` + +```javascript +module.exports = () => ({ + // ... + admin: { + // ... + url: 'https://api.example.com/admin', + }, +}); +``` + +### Custom Logic + +In some scenarios, you will want to write additional logic for your connection workflow such as: +- Restricting connection and registration for a specific domain +- Triggering actions on connection attempt +- Analytics + +The easiest way to do so is to plug into the verify function of your strategy and write some code. + +For example, if you want to allow only people with an official strapi.io email address, you can instantiate your strategy like this: + +```javascript +const strategyInstance = new Strategy( + configuration, + ({ email, username }, done) => { + // If the email ends with @strapi.io + if (email.endsWith('@strapi.io')) { + // Then we continue with the data given by the provider + return done(null, { email, username }); + } + + // Otherwise, we continue by sending an error to the done function + done(new Error('Forbidden email address')); + }, +); +``` + +### Authentication Events + +The SSO feature adds a new [authentication event](/developer-docs/latest/setup-deployment-guides/configurations.md#available-options): `onSSOAutoRegistration`. + +This event is triggered whenever a user is created using the auto-register feature added by SSO. +It contains the created user (`event.user`), and the provider used to make the registration (`event.provider`). + +Example: + +`/config/server.js` +```javascript +module.exports = () => ({ + // ... + admin: { + // ... + auth: { + // ... + events: { + onConnectionSuccess(e) {}, + onConnectionError(e) {}, + // ... + onSSOAutoRegistration(e) { + const { user, provider } = e; + + console.log( + `A new user (${user.id}) has been automatically registered using ${provider}` + ); + }, + }, + }, + }, +}); +``` diff --git a/docs/developer-docs/latest/developer-resources/cli/CLI.md b/docs/developer-docs/latest/developer-resources/cli/CLI.md index e0d4b2e820..821bec4aa5 100644 --- a/docs/developer-docs/latest/developer-resources/cli/CLI.md +++ b/docs/developer-docs/latest/developer-resources/cli/CLI.md @@ -104,6 +104,7 @@ strapi configuration:dump Options: -f, --file Output file, default output is stdout + -p, --pretty Format the output JSON with indentation and line breaks (default: false) ``` **Examples** @@ -191,7 +192,7 @@ options: [--plugin ] - **strapi generate:api <name>**
Generates an API called **<name>** in the `./api` folder at the root of your project. -- **strapi generate:api --draft-and-publish=true**
+- **strapi generate:api <name> --draft-and-publish=true**
Generates an API called **<name>** in the `./api` folder at the root of your project and enabled the draft/publish feature. - **strapi generate:api <name> <attribute:type>**
diff --git a/docs/developer-docs/latest/developer-resources/content-api/content-api.md b/docs/developer-docs/latest/developer-resources/content-api/content-api.md index fd0daab732..6b17d869d7 100644 --- a/docs/developer-docs/latest/developer-resources/content-api/content-api.md +++ b/docs/developer-docs/latest/developer-resources/content-api/content-api.md @@ -140,7 +140,7 @@ Here is the list of endpoints generated for each of your **Content Types**. | Method | Path | Description | | :----- | :---------------- | :-------------------------------- | -| GET | `/{content-type}` | Get a the {content-type} content | +| GET | `/{content-type}` | Get the {content-type} content | | PUT | `/{content-type}` | Update the {content-type} content | | DELETE | `/{content-type}` | Delete the {content-type} content | diff --git a/docs/developer-docs/latest/developer-resources/content-api/integrations/vue-js.md b/docs/developer-docs/latest/developer-resources/content-api/integrations/vue-js.md index 286895141f..3b14430ec6 100644 --- a/docs/developer-docs/latest/developer-resources/content-api/integrations/vue-js.md +++ b/docs/developer-docs/latest/developer-resources/content-api/integrations/vue-js.md @@ -207,7 +207,7 @@ export default { headers: this.headers, }).then(this.checkStatus) .then(this.parseJSON); - this.restaurants = response + this.restaurants = response } catch (error) { this.error = error } diff --git a/docs/developer-docs/latest/development/backend-customization.md b/docs/developer-docs/latest/development/backend-customization.md index b790f94d6a..070bfb59a2 100644 --- a/docs/developer-docs/latest/development/backend-customization.md +++ b/docs/developer-docs/latest/development/backend-customization.md @@ -647,7 +647,6 @@ The context object (`ctx`) contains a list of values and functions useful to man For more information, please refer to the [Koa response documentation](http://koajs.com/#response). - ## Services @@ -781,6 +780,8 @@ module.exports = { ##### `create` ```js +const { isDraft } = require('strapi-utils').contentTypes; + module.exports = { /** * Promise to add record @@ -789,7 +790,13 @@ module.exports = { */ async create(data, { files } = {}) { - const validData = await strapi.entityValidator.validateEntity(strapi.models.restaurant, data); + const isDraft = isDraft(data, strapi.models.restaurant); + const validData = await strapi.entityValidator.validateEntityCreation( + strapi.models.restaurant, + data, + { isDraft } + ); + const entry = await strapi.query('restaurant').create(validData); if (files) { @@ -813,6 +820,8 @@ module.exports = { ##### `update` ```js +const { isDraft } = require('strapi-utils').contentTypes; + module.exports = { /** * Promise to edit record @@ -821,10 +830,15 @@ module.exports = { */ async update(params, data, { files } = {}) { + const existingEntry = await db.query('restaurant').findOne(params); + + const isDraft = isDraft(existingEntry, strapi.models.restaurant); const validData = await strapi.entityValidator.validateEntityUpdate( strapi.models.restaurant, - data + data, + { isDraft } ); + const entry = await strapi.query('restaurant').update(params, validData); if (files) { @@ -1468,7 +1482,7 @@ You can access the Knex instance with: const knex = strapi.connections.default; ``` -You can then use Knex to build your own custom queries. You will lose all the functionalities of the model, +You can then use Knex to build your own custom queries. You will lose all the functionalities of the model, but this could come handy if you are building a more custom schema. Please note that if you are using the [draft system](/developer-docs/latest/concepts/draft-and-publish.md), Strapi nullyfies all the Draft columns util they are published. @@ -1485,8 +1499,8 @@ const result = await knex('restaurants') .select('restaurants.name as restaurant') .select('chef.name as chef') -// Loadsh's groupBy method can be used to -// return a grouped key-value object generated from +// Loadsh's groupBy method can be used to +// return a grouped key-value object generated from // the response return (_.groupBy(result, 'chef'); @@ -1691,6 +1705,8 @@ The options key on the model-json states. - `populateCreatorFields`: Configure whether the API response should include `created_by` and `updated_by` fields or not. Accepts a `boolean`. The default value is `false`. +- `draftAndPublish`: Enable the draft and publish feature. Accepts a `boolean`. The default value is `false`. + **Path β€”** `User.settings.json`. ```json @@ -1698,7 +1714,8 @@ The options key on the model-json states. "options": { "timestamps": true, "privateAttributes": ["id", "created_at"], - "populateCreatorFields": true + "populateCreatorFields": true, + "draftAndPublish": false } } ``` @@ -2662,7 +2679,6 @@ module.exports = { When calling a lifecycle function directly, you will need to make sure you call it with the expected parameters. ::: - ## Webhooks @@ -3021,4 +3037,3 @@ This event is triggered only when you delete a media through the media interface } } ``` - diff --git a/docs/developer-docs/latest/development/local-plugins-customization.md b/docs/developer-docs/latest/development/local-plugins-customization.md index 06aa806cff..4b5d16504d 100644 --- a/docs/developer-docs/latest/development/local-plugins-customization.md +++ b/docs/developer-docs/latest/development/local-plugins-customization.md @@ -17,6 +17,9 @@ In a new terminal window: Generate a new plugin: `cd /path/to/myDevelopmentProject && strapi generate:plugin my-plugin` +::: tip NOTE +After you have successfully generated a plugin, you need to run `strapi build` which adds the new plugin to the admin panel. +::: ## Plugin Folders and Files Architecture @@ -66,9 +69,7 @@ Please refer to [router documentation](/developer-docs/latest/development/backen **Route prefix** -Each route of a plugin is prefixed by the name of the plugin (eg: `/my-plugin/my-plugin-route`). - -To disable the prefix, add the `prefix` attribute to each concerned route, like below: +Each route of a plugin is prefixed by the name of the plugin (eg: `/my-plugin/my-plugin-route`). Using the `prefix` key you can change this option to something custom. You can disable the prefix, by setting the `config.prefix` key to an empty string. ```json { @@ -77,7 +78,7 @@ To disable the prefix, add the `prefix` attribute to each concerned route, like "handler": "MyPlugin.action", "config": { "policies": [], - "prefix": false + "prefix": "my-custom-prefix" } } ``` diff --git a/docs/developer-docs/latest/development/plugins/documentation.md b/docs/developer-docs/latest/development/plugins/documentation.md index 226aea6614..181e055a0c 100644 --- a/docs/developer-docs/latest/development/plugins/documentation.md +++ b/docs/developer-docs/latest/development/plugins/documentation.md @@ -42,7 +42,7 @@ When your plugin is installed, you just have to start your application and it wi The administration panel lets you configure the basic settings of this plugin. -![Accessing the documentation](../../assets/plugins/documentation/open-doc.gif 'Accessing the documentation') +![Accessing the documentation](../../assets/plugins/documentation/open-doc.gif) ## Administration panel diff --git a/docs/developer-docs/latest/development/plugins/graphql.md b/docs/developer-docs/latest/development/plugins/graphql.md index f6b3bf5bb0..84ed651460 100644 --- a/docs/developer-docs/latest/development/plugins/graphql.md +++ b/docs/developer-docs/latest/development/plugins/graphql.md @@ -48,7 +48,7 @@ mutation { } ``` -Then on each request, send along an `Authorization` header in the form of `{ Authorization: "Bearer YOUR_JWT_GOES_HERE" }`. This can be set in the HTTP Headers section of your GraphQL Playground. +Then on each request, send along an `Authorization` header in the form of `{ "Authorization": "Bearer YOUR_JWT_GOES_HERE" }`. This can be set in the HTTP Headers section of your GraphQL Playground. ## Configurations diff --git a/docs/developer-docs/latest/development/plugins/upload.md b/docs/developer-docs/latest/development/plugins/upload.md index 424d7ab94c..cbf177b70e 100644 --- a/docs/developer-docs/latest/development/plugins/upload.md +++ b/docs/developer-docs/latest/development/plugins/upload.md @@ -26,6 +26,33 @@ module.exports = { }; ``` +#### Responsive Images + +When the `Enable responsive friendly upload` setting is enabled in the settings panel the plugin will generate the following responsive image sizes: +| Name | Largest Dimension | +| :------ | :--------- | +| large | 1000px | +| medium | 750px | +| small | 500px | + +These sizes can be overridden in `config/plugins.js`: +```javascript +module.exports = { + upload: { + breakpoints: { + xlarge: 1920, + large: 1000, + medium: 750, + small: 500, + xsmall: 64 + } + } +} +``` +::: warning + Breakpoint changes will only apply to new images, existing images will not be resized or have new sizes generated. +::: + ## Endpoints