This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hi@strapi.io. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][mozilla coc].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][faq]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[mozilla coc]: https://github.com/mozilla/diversity
|
||||
[faq]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -0,0 +1,244 @@
|
||||
# Contribute to Strapi
|
||||
|
||||
Strapi is an open-source project administered by [the Strapi team](https://strapi.io/about-us). We appreciate your interest and efforts to contribute to Strapi. See the [LICENSE](https://github.com/strapi/strapi/blob/main/LICENSE) licensing information. All work done is available on GitHub.
|
||||
|
||||
We highly appreciate your effort to contribute, but we recommend you talk to a maintainer before spending a lot of time making a pull request that may not align with the project roadmap. Whether it is from Strapi or contributors, every pull request goes through the same process.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
Feature Requests by the community are highly encouraged. Feel free to submit a new one or upvote an existing feature request on [feedback.strapi.io](https://feedback.strapi.io/).
|
||||
|
||||
## Request For Comments (RFC)
|
||||
|
||||
Larger chunks of changes to Strapi that might affect many users require a thorough design phase before starting working on a PR. We will do our best to respond as soon as possible, but since we need to discuss these proposals thoroughly, please do not expect them to be merged and accepted immediately.
|
||||
|
||||
The Request For Comments process will help us create consensus among the core team and include as much feedback as possible from the community for these upcoming changes.
|
||||
|
||||
A Request For Comments has to be created on the [strapi/rfcs](https://github.com/strapi/rfcs) repository.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project, and everyone participating in it, are governed by the [Strapi Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold it. Make sure to read the [full text](CODE_OF_CONDUCT.md) to understand which type of actions may or may not be tolerated.
|
||||
|
||||
## Contributor License Agreement (CLA)
|
||||
|
||||
### Individual contribution
|
||||
|
||||
You need to sign a Contributor License Agreement (CLA) to accept your pull request. You only need to do this once. If you submit a pull request for the first time, you can complete your CLA [here](https://cla.strapi.io/strapi/strapi), or our CLA bot will automatically ask you to sign before merging the pull request.
|
||||
|
||||
### Company contribution
|
||||
|
||||
If you make contributions to our repositories on behalf of your company, we will need a Corporate Contributor License Agreement (CLA) signed. To do that, please get in touch with us at [contributions@strapi.io](mailto:contributions@strapi.io).
|
||||
|
||||
## Documentation
|
||||
|
||||
Pull requests related to fixing documentation for the latest release should be directed towards the [documentation repository](https://github.com/strapi/documentation). Please follow the [documentation contributing guide](https://github.com/strapi/documentation/blob/main/CONTRIBUTING.md) for more information.
|
||||
|
||||
## Bugs
|
||||
|
||||
Strapi is using [GitHub issues](https://github.com/strapi/strapi/issues) to manage bugs. We keep a close eye on them. Before filing a new issue, try to ensure your problem does not already exist.
|
||||
|
||||
---
|
||||
|
||||
## Before Submitting a Pull Request
|
||||
|
||||
The Strapi core team will review your pull request and either merge it, request changes, or close it.
|
||||
|
||||
## Contribution Prerequisites
|
||||
|
||||
- You have [Node.js](https://nodejs.org/en/) at version `>= v18 and <= v22` and [Yarn](https://yarnpkg.com/en/) at v1.2.0+ installed.
|
||||
- You are familiar with [Git](https://git-scm.com).
|
||||
|
||||
**Before submitting your pull request** make sure the following requirements are fulfilled:
|
||||
|
||||
- Fork the repository and create your new branch from `develop`.
|
||||
- Run `yarn install` in the root of the repository.
|
||||
- Run `yarn setup` in the root of the repository.
|
||||
- If you've fixed a bug or added code that should be tested, please make sure to add tests
|
||||
- Ensure the following test suites are passing:
|
||||
- `yarn test:unit`
|
||||
- `yarn test:front`
|
||||
- `yarn test:e2e --setup --concurrency=1`
|
||||
- you **_may_** need to install Playwright browsers first: `yarn playwright install`
|
||||
- Make sure your code lints by running `yarn lint`.
|
||||
- If your contribution fixes an existing issue, please make sure to link it in your pull request.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Fork the [repository](https://github.com/strapi/strapi)
|
||||
|
||||
[Go to the repository](https://github.com/strapi/strapi) and fork it using your own GitHub account.
|
||||
|
||||
### 2. Clone your repository
|
||||
|
||||
```bash
|
||||
git clone git@github.com:YOUR_USERNAME/strapi.git
|
||||
```
|
||||
|
||||
### 3. Install the dependencies
|
||||
|
||||
Go to the root of the repository and run the setup:
|
||||
|
||||
```bash
|
||||
cd strapi
|
||||
yarn install
|
||||
yarn setup
|
||||
|
||||
```
|
||||
|
||||
### 4. Start the example application
|
||||
|
||||
```bash
|
||||
cd ./examples/getstarted
|
||||
yarn develop
|
||||
```
|
||||
|
||||
Make sure to read the [`getstarted` application README](https://github.com/strapi/strapi/blob/main/examples/getstarted/README.md) for more details.
|
||||
|
||||
### 5. Running the administration panel in development mode
|
||||
|
||||
Start the administration panel server for development:
|
||||
|
||||
```bash
|
||||
cd ./packages/core/admin
|
||||
yarn watch
|
||||
```
|
||||
|
||||
Run the example application but watching the admin panel:
|
||||
|
||||
```bash
|
||||
cd ./examples/getstarted
|
||||
yarn develop --watch-admin
|
||||
```
|
||||
|
||||
Both commands must be running at same time; now you will be able to see the admin panel changes on the application example.
|
||||
|
||||
**Awesome! You are now able to contribute to Strapi.**
|
||||
|
||||
### 6. Available commands
|
||||
|
||||
- `yarn watch` starts yarn watch in all packages.
|
||||
- `yarn build` builds the `strapi-helper-plugin` (use this command when you develop in the administration panel).
|
||||
- `yarn commit` runs an interactive commit CLI to help you write a good commit message inline with our git conventions.
|
||||
- `yarn setup` installs dependencies.
|
||||
- `yarn lint` lints the codebase.
|
||||
- `yarn test:clean` removes the coverage reports.
|
||||
- `yarn test:front` runs front-end related tests.
|
||||
- `yarn test:front:watch` runs an interactive test watcher for the front-end.
|
||||
- `yarn test:unit` runs the back-end unit tests.
|
||||
- `yarn test:api` runs the api integration tests.
|
||||
- `yarn test:generate-app` generates a test application.
|
||||
- `yarn test:run-app` runs a test application.
|
||||
- `yarn test:start-app` starts the test application.
|
||||
|
||||
---
|
||||
|
||||
## Running the API Integration tests
|
||||
|
||||
The API integration tests require a Strapi app to be able to run. You can generate a "test app" using `yarn test:generate-app <database>`:
|
||||
|
||||
```bash
|
||||
$ yarn test:generate-app --db=sqlite
|
||||
$ yarn test:generate-app --db=postgres
|
||||
$ yarn test:generate-app --db=mysql
|
||||
```
|
||||
|
||||
A new app is required every time you run the API integration tests, otherwise the test suite will fail. A command is available to make this process easier: `yarn test:api`.
|
||||
|
||||
This command runs tests using jest behind the scenes. Options for jest can be passed to the command. (e.g. to update snapshots `yarn test:api -u`).
|
||||
|
||||
### Changing the database
|
||||
|
||||
By default the script run by `test:api` generates an app that uses `sqlite` as a database. But you can run the test suites using different databases:
|
||||
|
||||
```bash
|
||||
$ yarn test:api --db=sqlite
|
||||
$ yarn test:api --db=postgres
|
||||
$ yarn test:api --db=mysql
|
||||
```
|
||||
|
||||
### Running the tests for the Enterprise Edition (EE)
|
||||
|
||||
The test suites run the tests for the Community Edition (CE) version of Strapi by default.
|
||||
In order to run the Enterprise Edition tests you need a valid license. To specify a license, you can use the environment variable `STRAPI_LICENSE`:
|
||||
|
||||
```bash
|
||||
$ STRAPI_LICENSE=<license> yarn test:api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Conventions
|
||||
|
||||
### Commit messages
|
||||
|
||||
We use the following convention:
|
||||
|
||||
```
|
||||
type: subject
|
||||
|
||||
body
|
||||
```
|
||||
|
||||
The goal of this convention is to help us generate changelogs that can be communicated to our users.
|
||||
|
||||
#### Type
|
||||
|
||||
The types are based on our GitHub label, here are a subset:
|
||||
|
||||
- `fix` – When fixing an issue.
|
||||
- `chore` – When doing some cleanup, working on tooling, some refactoring. (usually reserved for **internal** work)
|
||||
- `doc` – When writing documentation.
|
||||
- `feat` – When working on a feature.
|
||||
|
||||
You can see the complete list [here](https://github.com/strapi/strapi/blob/1cb6f95889ccaad897759cfa14d2804adeaeb7ee/.commitlintrc.ts#L11).
|
||||
|
||||
#### Subject
|
||||
|
||||
The subject of a commit should be a summary of what the commit is about. It should not describe what the code is doing:
|
||||
|
||||
- `feat: what the feature is`
|
||||
- `fix: what the problem is`
|
||||
- `chore: what the PR is about`
|
||||
- `doc: what is documented`
|
||||
|
||||
Examples:
|
||||
|
||||
- `feat: introduce document service`
|
||||
- `fix: unable to publish documents due to missing permissions`
|
||||
- `chore: refactor data-fetching in EditView to use react-query`
|
||||
- `doc: document service API reference`
|
||||
|
||||
> ⚠️ For a `fix` commit the message should explain what the commit is fixing. Not what the solution is.
|
||||
|
||||
---
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Repository Organization
|
||||
|
||||
We chose to use a monorepo design using [Yarn Workspaces](https://yarnpkg.com/en/docs/workspaces) in the way [React](https://github.com/facebook/react/tree/master/packages) or [Babel](https://github.com/babel/babel/tree/master/packages) does. This allows us to maintain the whole ecosystem keep it up-to-date and consistent.
|
||||
|
||||
We do our best to keep the develop branch as clean as possible, with tests passing at all times. However, the develop branch can move faster than the release cycle. Therefore check the [releases on npm](https://www.npmjs.com/package/@strapi/strapi) so that you are always up-to-date with the latest stable version.
|
||||
|
||||
### Reporting an issue
|
||||
|
||||
Before submitting an issue you need to make sure:
|
||||
|
||||
- You are experiencing a technical issue with Strapi.
|
||||
- You have already searched for related [issues](https://github.com/strapi/strapi/issues) and found none open (if you found a related _closed_ issue, please link to it from your post).
|
||||
- You are not asking a question about how to use Strapi or about whether Strapi has a certain feature. For general help using Strapi, you may:
|
||||
- Refer to the [official Strapi documentation](https://docs.strapi.io).
|
||||
- Ask a member of the community in the [Strapi Discord Community](https://discord.strapi.io/).
|
||||
- Ask a question on the [Strapi community forum](https://forum.strapi.io).
|
||||
- Your issue title is concise, on-topic, and polite.
|
||||
- You provide steps to reproduce the issue.
|
||||
- You have tried all the following (if relevant), and your issue remains:
|
||||
- Make sure you have the right application started.
|
||||
- Make sure the [issue template] is respected.
|
||||
- Make sure your issue body is readable and [well formatted](https://guides.github.com/features/mastering-markdown).
|
||||
- Make sure you've stopped the Strapi server with CTRL+C and restarted it.
|
||||
- Make sure your application has a clean `node_modules` directory, meaning:
|
||||
- you didn't link any dependencies (e.g., by running `yarn link`)
|
||||
- you haven't made any inline changes to files in the `node_modules` directory
|
||||
- you don't have any global dependency loops. If you aren't sure, the easiest way to double-check any of the above is to run: `$ rm -rf node_modules && yarn cache clean && yarn install && yarn setup`.
|
||||
@@ -0,0 +1,15 @@
|
||||
Copyright (c) 2015-present Strapi Solutions SAS
|
||||
|
||||
Portions of the Strapi software are licensed as follows:
|
||||
|
||||
* If you are accessing or using any component of the software that resides under an "ee/" directory, then you are deemed to be using our “Enterprise Edition” of the software and you understand and agree that the software is not licensed under the "MIT Expat" license as set forth below but instead, all the software you access is licensed under the license defined in "strapi/packages/core/admin/ee/LICENSE" and located at https://github.com/strapi/strapi/blob/a76b557047e9ef1c168dbf1b6cf879bcc3022de6/packages/core/admin/ee/LICENSE, unless (a) you or the company you represent has signed an alternative agreement referencing this code, then such signed agreement applies or (b) you are using the software in connection with a subscription to our cloud offering, then the terms of the agreement relevant to the cloud offering which you have assented to apply and the software licenses included in that agreement shall apply.
|
||||
|
||||
* If (a) you are not accessing or using the software that resides under an “ee/” directory and therefore you are only accessing or using our “Community Edition” of the Software and (b) you have no registered account on our cloud offering, then we are providing you the software under the "MIT Expat" license as set forth below.
|
||||
|
||||
MIT Expat License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+146
-39
@@ -1,61 +1,168 @@
|
||||
# 🚀 Getting started with Strapi
|
||||
<p align="center">
|
||||
<a href="https://strapi.io/#gh-light-mode-only">
|
||||
<img src="https://strapi.io/assets/strapi-logo-dark.svg" width="318px" alt="Strapi logo" />
|
||||
</a>
|
||||
<a href="https://strapi.io/#gh-dark-mode-only">
|
||||
<img src="https://strapi.io/assets/strapi-logo-light.svg" width="318px" alt="Strapi logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/dev-docs/cli) (CLI) which lets you scaffold and manage your project in seconds.
|
||||
<h3 align="center">Open-source headless CMS, self-hosted or Cloud you’re in control.</h3>
|
||||
<p align="center">The leading open-source headless CMS, 100% JavaScript/TypeScript, flexible and fully customizable.</p>
|
||||
<p align="center"><a href="https://cloud.strapi.io/signups?source=github1">Cloud</a> · <a href="https://strapi.io/demo?utm_campaign=Growth-Experiments&utm_source=strapi%2Fstrapi%20README.md">Try live demo</a></p>
|
||||
<br />
|
||||
|
||||
### `develop`
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.org/package/@strapi/strapi">
|
||||
<img src="https://img.shields.io/npm/v/@strapi/strapi/latest.svg" alt="NPM Version" />
|
||||
</a>
|
||||
<a href="https://github.com/strapi/strapi/actions/workflows/tests.yml">
|
||||
<img src="https://github.com/strapi/strapi/actions/workflows/tests.yml/badge.svg?branch=main" alt="Tests" />
|
||||
</a>
|
||||
<a href="https://discord.strapi.io">
|
||||
<img src="https://img.shields.io/discord/811989166782021633?label=Discord" alt="Strapi on Discord" />
|
||||
</a>
|
||||
<a href="https://github.com/strapi/strapi/actions/workflows/nightly.yml">
|
||||
<img src="https://github.com/strapi/strapi/actions/workflows/nightly.yml/badge.svg" alt="Strapi Nightly Release Build Status" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-develop)
|
||||
<br>
|
||||
|
||||
```
|
||||
npm run develop
|
||||
# or
|
||||
yarn develop
|
||||
<p align="center">
|
||||
<a href="https://strapi.io">
|
||||
<img src="https://raw.githubusercontent.com/strapi/strapi/main/public/assets/admin-demo.gif" alt="Administration panel" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
Strapi Community Edition is a free and open-source headless CMS enabling you to manage any content, anywhere.
|
||||
|
||||
- **Self-hosted or Cloud**: You can host and scale Strapi projects the way you want. You can save time by deploying to [Strapi Cloud](https://cloud.strapi.io/signups?source=github1) or deploy to the hosting platform you want\*\*: AWS, Azure, Google Cloud, DigitalOcean.
|
||||
- **Modern Admin Panel**: Elegant, entirely customizable and a fully extensible admin panel.
|
||||
- **Multi-database support**: You can choose the database you prefer: PostgreSQL, MySQL, MariaDB, and SQLite.
|
||||
- **Customizable**: You can quickly build your logic by fully customizing APIs, routes, or plugins to fit your needs perfectly.
|
||||
- **Blazing Fast and Robust**: Built on top of Node.js and TypeScript, Strapi delivers reliable and solid performance.
|
||||
- **Front-end Agnostic**: Use any front-end framework (React, Next.js, Vue, Angular, etc.), mobile apps or even IoT.
|
||||
- **Secure by default**: Reusable policies, CORS, CSP, P3P, Xframe, XSS, and more.
|
||||
- **Powerful CLI**: Scaffold projects and APIs on the fly.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<a href="https://docs.strapi.io/developer-docs/latest/getting-started/quick-start.html" target="_blank">Read the Getting Started tutorial</a> or follow the steps below:
|
||||
|
||||
### ⏳ Installation
|
||||
|
||||
Install Strapi with this **Quickstart** command to create a Strapi project instantly:
|
||||
|
||||
- (Use **yarn** to install the Strapi project (recommended). [Install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/).)
|
||||
|
||||
```bash
|
||||
yarn create strapi
|
||||
```
|
||||
|
||||
### `start`
|
||||
**or**
|
||||
|
||||
Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-start)
|
||||
- (Using npx to install the Strapi project.)
|
||||
|
||||
```
|
||||
npm run start
|
||||
# or
|
||||
yarn start
|
||||
```bash
|
||||
npx create-strapi@latest
|
||||
```
|
||||
|
||||
### `build`
|
||||
This command generates a brand new project with the default features (authentication, permissions, content management, content type builder & file upload).
|
||||
|
||||
Build your admin panel. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-build)
|
||||
Enjoy 🎉
|
||||
|
||||
```
|
||||
npm run build
|
||||
# or
|
||||
yarn build
|
||||
```
|
||||
### 🖐 Requirements
|
||||
|
||||
## ⚙️ Deployment
|
||||
Complete installation requirements can be found in the documentation under <a href="https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html">Installation Requirements</a>.
|
||||
|
||||
Strapi gives you many possible deployment options for your project including [Strapi Cloud](https://cloud.strapi.io). Browse the [deployment section of the documentation](https://docs.strapi.io/dev-docs/deployment) to find the best solution for your use case.
|
||||
**Supported operating systems**:
|
||||
|
||||
```
|
||||
yarn strapi deploy
|
||||
```
|
||||
| OS | Recommended | Minimum |
|
||||
| --------------- | ----------- | ---------- |
|
||||
| Ubuntu | 24.04 | LTS |
|
||||
| Debian | 11 | LTS |
|
||||
| RHEL | 9 | LTS |
|
||||
| macOS | 14 | 12 |
|
||||
| Windows Desktop | 11 | 10 |
|
||||
| Windows Server | No Support | No Support |
|
||||
| Docker | N/A | N/A |
|
||||
|
||||
## 📚 Learn more
|
||||
(Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.)
|
||||
|
||||
- [Resource center](https://strapi.io/resource-center) - Strapi resource center.
|
||||
- [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation.
|
||||
- [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community.
|
||||
- [Strapi blog](https://strapi.io/blog) - Official Strapi blog containing articles made by the Strapi team and the community.
|
||||
- [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements.
|
||||
**Node:**
|
||||
|
||||
Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
|
||||
Strapi only supports maintenance and LTS versions of Node.js. Please refer to the <a href="https://nodejs.org/en/about/releases/">Node.js release schedule</a> for more information. NPM versions installed by default with Node.js are supported. Generally it's recommended to use yarn over npm where possible.
|
||||
|
||||
## ✨ Community
|
||||
| Strapi Version | Recommended | Minimum |
|
||||
| --------------- | ----------- | ------- |
|
||||
| 5.0.0 and up | 20.x | 18.x |
|
||||
| 4.14.5 and up | 20.x | 18.x |
|
||||
| 4.11.0 and up | 18.x | 16.x |
|
||||
| 4.3.9 to 4.10.x | 18.x | 14.x |
|
||||
| 4.0.x to 4.3.8 | 16.x | 14.x |
|
||||
|
||||
- [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team.
|
||||
- [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members.
|
||||
- [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi.
|
||||
**Database:**
|
||||
|
||||
---
|
||||
| Database | Recommended | Minimum |
|
||||
| ---------- | ----------- | ------- |
|
||||
| MySQL | 8.0 | 8.0 |
|
||||
| MariaDB | 11.2 | 10.3 |
|
||||
| PostgreSQL | 16.0 | 14.0 |
|
||||
| SQLite | 3 | 3 |
|
||||
|
||||
<sub>🤫 Psst! [Strapi is hiring](https://strapi.io/careers).</sub>
|
||||
**We recommend always using the latest version of Strapi stable to start your new projects**.
|
||||
|
||||
## Features
|
||||
|
||||
- **Content Types Builder**: Build the most flexible publishing experience for your content managers, by giving them the freedom to create any page on the go with [fields](https://docs.strapi.io/user-docs/content-manager/writing-content#filling-up-fields), components and [Dynamic Zones](https://docs.strapi.io/user-docs/content-manager/writing-content#dynamic-zones).
|
||||
- **Media Library**: Upload your images, videos, audio or documents to the media library. Easily find the right asset, edit and reuse it.
|
||||
- **Internationalization**: The Internationalization (i18n) plugin allows Strapi users to create, manage and distribute localized content in different languages, called "locales"
|
||||
- **Role Based Access Control**: Create an unlimited number of custom roles and permissions for admin and end users.
|
||||
- **GraphQL or REST**: Consume the API using REST or GraphQL
|
||||
|
||||
You can unlock additional features such as SSO, Audit Logs, Review Workflows in [Strapi Cloud](https://cloud.strapi.io/login?source=github1) or [Strapi Enterprise](https://strapi.io/enterprise?source=github1).
|
||||
|
||||
**[See more on our website](https://strapi.io/overview)**.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read our [Contributing Guide](./CONTRIBUTING.md) before submitting a Pull Request to the project.
|
||||
|
||||
## Community support
|
||||
|
||||
For general help using Strapi, please refer to [the official Strapi documentation](https://docs.strapi.io). For additional help, you can use one of these channels to ask a question:
|
||||
|
||||
- [Discord](https://discord.strapi.io) (For live discussion with the Community and Strapi team)
|
||||
- [GitHub](https://github.com/strapi/strapi) (Bug reports, Contributions)
|
||||
- [Community Forum](https://forum.strapi.io) (Questions and Discussions)
|
||||
- [Feedback section](https://feedback.strapi.io) (Roadmap, Feature requests)
|
||||
- [Twitter](https://twitter.com/strapijs) (Get the news fast)
|
||||
- [Facebook](https://www.facebook.com/Strapi-616063331867161)
|
||||
- [YouTube Channel](https://www.youtube.com/strapi) (Learn from Video Tutorials)
|
||||
|
||||
## Migration
|
||||
|
||||
Follow our [migration guides](https://docs.strapi.io/developer-docs/latest/update-migration-guides/migration-guides.html) on the documentation to keep your projects up-to-date.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Check out our [roadmap](https://feedback.strapi.io) to get informed of the latest features released and the upcoming ones. You may also give us insights and vote for a specific feature.
|
||||
|
||||
## Documentation
|
||||
|
||||
See our dedicated [repository](https://github.com/strapi/documentation) for the Strapi documentation, or view our documentation live:
|
||||
|
||||
- [Developer docs](https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html)
|
||||
- [User guide](https://docs.strapi.io/user-docs/latest/getting-started/introduction.html)
|
||||
- [Cloud guide](https://docs.strapi.io/cloud/intro)
|
||||
|
||||
## Try live demo
|
||||
|
||||
See for yourself what's under the hood by getting access to a [hosted Strapi project](https://strapi.io/demo) with sample data.
|
||||
|
||||
## License
|
||||
|
||||
See the [LICENSE](./LICENSE) file for licensing information.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As of September 2025 (and until this document is updated), only the v4.x.x and v5.x.x _GA_ or _STABLE_ releases of Strapi are supported for updates and bug fixes. Any previous versions are currently not supported and users are advised to use them "at their own risk".
|
||||
|
||||
**Note**: The v4.x.x LTS version will only receive high/critical severity fixes until April 2026. Any Medium/Low severity issues will not be fixed unless specific exceptions are made.
|
||||
|
||||
| Version | Release Tag | Support Starts | Support Ends | Security Updates Until | Notes |
|
||||
| ------- | ----------- | -------------- | -------------- | ---------------------- | ------------------ |
|
||||
| 5.x.x | GA / Stable | September 2024 | Further Notice | Further Notice | LTS |
|
||||
| 5.x.x | RC | N/A | September 2024 | N/A | End Of Life |
|
||||
| 5.x.x | Beta | N/A | N/A | N/A | End Of Life |
|
||||
| 5.x.x | Alpha | N/A | N/A | N/A | End Of Life |
|
||||
| 4.x.x | GA / Stable | November 2021 | October 2025 | April 2026 | Maintanence Period |
|
||||
| 4.x.x | Beta | N/A | N/A | N/A | End Of Life |
|
||||
| 4.x.x | Alpha | N/A | N/A | N/A | End Of Life |
|
||||
| 3.x.x | N/A | N/A | N/A | N/A | End Of Life |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities via GitHub's security advisory reporting system:
|
||||
Submit your vulnerability via [this link](https://github.com/strapi/strapi/security/advisories/new)
|
||||
|
||||
**Strapi does not currently and has no plans to offer any bug bounties, swag, or any other reward for reporting vulnerabilities.**
|
||||
|
||||
During our public disclosure, we can give credit to the reporter and link to any social accounts you wish to have us add, including linking to your own blog post detailing the vulnerability.
|
||||
|
||||
Please note the following requirements (all are required):
|
||||
|
||||
- Summary of the suspected vulnerability
|
||||
- Detailed information as to what the suspected vulnerability does and what it has access to
|
||||
- Proof of Concept (Code samples at minimum, reproduction video optional)
|
||||
- POC must include how the vulnerability can actually access sensitive data, simply triggering an alert popup in a browser is not a security vulnerability
|
||||
- Impact summary (who does this impact and how)
|
||||
|
||||
Optionally you may also add your estimated CVSS 3.1 score, though we may adjust. There is no need to submit a CVE request as we will do that as part of the advisory process.
|
||||
|
||||
You will receive a response from us within 72 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
|
||||
|
||||
Please note that we follow a very strict internal and public disclosure policy, typically a patch will be issued and included in a release. We then will place a warning that a security vulnerability has been patched and delay detailed disclosure from 2 to 8 weeks depending on the severity of the issue. If you have any resources such as blog posts that you intend to publish on and would like us to include these in our disclosure please advise us ASAP.
|
||||
|
||||
Before doing any public disclosure we do ask that you speak to us first to ensure we are not releasing too much information before a patch is available and time has been given to users to upgrade their projects.
|
||||
|
||||
## Specific Exclusions
|
||||
|
||||
Pursuant to the [CNA Operational Rules](https://www.cve.org/resourcessupport/allresources/cnarules) there are certain exclusions that apply that SHOULD NOT be reported to us as vulnerabilities, these include but are not limited to:
|
||||
|
||||
- Vulnerable packages and/or libraries that have vulnerabilities in them (CNA Operational Rule 4.1.12) but are used by Strapi UNLESS there is significant proof that the libraries cause a new vulnerability within Strapi (CNA Operational Rule 4.1.14):
|
||||
> 4.1.12 The act of updating Product dependencies MUST NOT be determined to be a Vulnerability, regardless of whether the dependencies have Vulnerabilities. For example, updating a library to address a Vulnerability in that library MUST NOT be determined to be a new Vulnerability in a Product that uses the library, and a Vulnerability advisory for the Product SHOULD reference the CVE ID for the Vulnerability in the library.
|
||||
- End of Life versions of Strapi (CNA Operational Rule 4.1.13)
|
||||
> 4.1.13 The state of a Product being EOL, by itself, MUST NOT be determined to be a Vulnerability.
|
||||
- If the vulnerability is the result of improper configuration made by an authorized user assuming they are either well-documented or well-understood (CNA Operational Rule 4.1.3)
|
||||
> 4.1.3 Well-documented or commonly understood non-default configuration or runtime changes made by an authorized user SHOULD NOT be determined to be Vulnerabilities.
|
||||
- Conditions or behaviors that do not lead to a security impact (CNA Operational Rule 4.1.2)
|
||||
> 4.1.2 Conditions or behaviors that do not lead to a security impact SHOULD NOT be determined to be Vulnerabilities. Examples of security impacts include an increase in access for an attacker, a decrease in availability of a target, or another violation of security policy.
|
||||
|
||||
In addition to the above stated rules, we will also apply some generic rules as well:
|
||||
|
||||
- Any intentions to make threats against any team member, employee, or representative of Strapi or its partners
|
||||
- Any intentions to extort or otherwise blackmail a team member, employee, or representative of Strapi or its partners
|
||||
- Any vulnerability report made with malicious intent (such as overwhelming security resource personnel)
|
||||
|
||||
If any of these cases apply to a vulnerability report then the report will be immediately rejected and closed. In cases where applicable we will also report these people to any applicable authorities or security program groups.
|
||||
|
||||
## Security Process Summary
|
||||
|
||||
Below is a breakdown of our security process in order to set expectations:
|
||||
|
||||
- Initial submission of vulnerability via GitHub's Advisory system
|
||||
- Begin internal tracking and communication to reporter
|
||||
- Internal validation of vulnerability
|
||||
- Internal notification and scheduling for patch development
|
||||
- Begin development of patch
|
||||
- Validation of patch (Internal and with the reporter)
|
||||
- GitHub advisory cleanup
|
||||
- Requesting of CVE via GitHub
|
||||
- Disclosure and communication draft
|
||||
- Patch released with initial warning via patch notes
|
||||
- Email communication to Strapi Enterprise customers
|
||||
- Mandatory waiting period (between 2 to 8 weeks)
|
||||
- Publishing GitHub Advisory & CVE
|
||||
- Public disclosure (via blog post)
|
||||
- Email communication to Strapi Enterprise customers
|
||||
|
||||
## Other reporting platforms and bounties
|
||||
|
||||
Strapi does not support other reporting platforms, all security vulnerabilities must be made via GitHub Advisory system. If you are unable to report via this method you may open a security ticket with us by emailing security@strapi.io and we will create one on your behalf but if you do not have a GitHub user account we will not be able to share the private fork, pull request, or any other information with you during the process.
|
||||
|
||||
List of some (though not all) platforms **we do not support**:
|
||||
|
||||
- huntr.dev
|
||||
- Direct email or communication to Strapi employees (Discord, Slack, or Email)
|
||||
- Stack Overflow
|
||||
@@ -0,0 +1,10 @@
|
||||
comment:
|
||||
branches:
|
||||
- main
|
||||
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.mdx'
|
||||
@@ -0,0 +1,32 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: strapi
|
||||
POSTGRES_PASSWORD: strapi
|
||||
POSTGRES_DB: strapi
|
||||
ports:
|
||||
- '5432:5432'
|
||||
|
||||
mysql:
|
||||
image: mysql:8
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: strapi
|
||||
MYSQL_USER: strapi
|
||||
MYSQL_PASSWORD: strapi
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_ROOT_PASSWORD: strapi
|
||||
volumes:
|
||||
- mysqldata:/var/lib/mysql
|
||||
ports:
|
||||
- '3306:3306'
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
mysqldata:
|
||||
@@ -0,0 +1,32 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
volumes:
|
||||
- pgdata_test:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: strapi
|
||||
POSTGRES_PASSWORD: strapi
|
||||
POSTGRES_DB: strapi_test
|
||||
ports:
|
||||
- '5432:5432'
|
||||
|
||||
mysql:
|
||||
image: mysql:8
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: strapi_test
|
||||
MYSQL_USER: strapi
|
||||
MYSQL_PASSWORD: strapi
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_ROOT_PASSWORD: strapi
|
||||
volumes:
|
||||
- mysqldata_test:/var/lib/mysql
|
||||
ports:
|
||||
- '3306:3306'
|
||||
|
||||
volumes:
|
||||
pgdata_test:
|
||||
mysqldata_test:
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['react'],
|
||||
rules: {
|
||||
'react/prop-types': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
/docs/exports
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,29 @@
|
||||
# Strapi contributor documentation
|
||||
|
||||
> If you are looking for the official Strapi documentation, it is available [here](https://docs.strapi.io)
|
||||
|
||||
This documentation is a contributor documentation made for anyone that wants to contribute to the project.
|
||||
|
||||
To run the documentation website, follow the instructions below.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -0,0 +1,661 @@
|
||||
---
|
||||
title: Strapi (WIP)
|
||||
slug: /api/Strapi
|
||||
tags:
|
||||
- class
|
||||
- public
|
||||
- global
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Strapi
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The Strapi class is the main object used in Strapi projects.
|
||||
An instance of Strapi class is available as a global in any Strapi project: `global.strapi`.
|
||||
|
||||
## Class: Strapi
|
||||
|
||||
### `new Strapi(opts)`
|
||||
|
||||
- `opts`: <Type>Object</Type> Options that can be used on Strapi startup
|
||||
- `autoReload`: <Type>Boolean</Type> **Default:** true
|
||||
- If false, deactivate auto reload
|
||||
- If you modify any file in your Strapi project, it reloads your nodejs app
|
||||
- If any content-type is changed, it will reload the nodejs app
|
||||
- `serveAdminPanel`: <Type>Boolean</Type> **Default:** true
|
||||
- Should the admin panel be loaded and serve as a web client
|
||||
- The admin panel build will not be delivered if false
|
||||
- `appDir`: <Type>String</Type> **Default:** `process.cwd()`
|
||||
- The directory relative or absolute path where Strapi will write every file (schemas, generated APIs, controllers or services)
|
||||
- `distDir`: <Type>String</Type> **Default:** appDir value
|
||||
- The directory relative or absolute path where Strapi will read configurations, schemas and any compiled code
|
||||
|
||||
Instances of the Strapi class can be created using the new keyword.
|
||||
|
||||
Strapi extends the [Container](./container) class.
|
||||
|
||||
```javascript
|
||||
const strapiInstance = new Strapi();
|
||||
```
|
||||
|
||||
### `strapi.dirs`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
Stored paths of file system.
|
||||
|
||||
- `dirs.dist`: <Type>[StrapiPathObject](#strapipathobject)</Type>
|
||||
- Build folder
|
||||
- `dirs.app`: <Type>[StrapiPathObject](#strapipathobject)</Type>
|
||||
- Sources folder
|
||||
- `dirs.static`: <Type>Object</Type> Define path to directories involving web client display
|
||||
- `public`: <Type>String</Type> Path to the folder to serve publicly (like files, images, etc..)
|
||||
|
||||
#### StrapiPathObject
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
A set of paths to specific Strapi project parts.
|
||||
|
||||
- `root`: <Type>String</Type> Root path
|
||||
- `src`: <Type>String</Type> Sources route path to project files
|
||||
- `api`: <Type>String</Type> Path to the folder containing project developers' API files (content-types, controllers, services, routes, etc..)
|
||||
- `components`: <Type>String</Type> Path to the folder containing project developers' components
|
||||
- `policies`: <Type>String</Type> Path to the folder where the Strapi project developers' policies are stored
|
||||
- A set of functions that check the state of the data and prevent the access to the API accordingly
|
||||
- `middlewares`: <Type>String</Type> Path to the folder where the Strapi project developers' middlewares are stored
|
||||
- A set of function that wrap around routes and requests
|
||||
- `config`: <Type>String</Type> Path to the folder containing project developers' config files
|
||||
|
||||
### `strapi.isLoaded`
|
||||
|
||||
- <Type>Boolean</Type>
|
||||
|
||||
- `true`: Everything (all `register` and `bootstrap` functions available in your strapi project) has been loaded
|
||||
- `false`: There is something loading
|
||||
|
||||
Note: `register` functions are called before the `bootstrap` functions.
|
||||
|
||||
### `strapi.reload()`
|
||||
|
||||
Reload the app.
|
||||
|
||||
This function defines itself at the construction of the Strapi class.
|
||||
|
||||
### `strapi.server`
|
||||
|
||||
- [<Type>StrapiServer</Type>](./strapi-server)
|
||||
|
||||
Strapi server object.
|
||||
|
||||
### `strapi.fs`
|
||||
|
||||
- [<Type>StrapiFS</Type>](StrapiFS)
|
||||
|
||||
Wrapper around [FS NodeJS module](https://nodejs.org/docs/latest-v18.x/api/fs.html).
|
||||
|
||||
### `strapi.eventHub`
|
||||
|
||||
- [<Type>EventHub</Type>](EventHub)
|
||||
|
||||
The `strapi.eventHub` object is used to manipulate events within a Strapi project. It is an instance of the built-in EventEmitter class from Node.js, which provides a simple way to emit and listen for events.
|
||||
|
||||
The `strapi.eventHub` object is created using the `createEventHub()` function in the [EventHub](EventHub) module of the Strapi core. This function returns a new instance of the EventHub class, which extends the EventEmitter class and adds some additional functionality specific to Strapi.
|
||||
|
||||
#### Examples:
|
||||
|
||||
```javascript
|
||||
// Listen for a 'user.updated' event and log the data
|
||||
strapi.eventHub.on('user.updated', (data) => {
|
||||
console.log(`User ${data.id} has been updated`);
|
||||
});
|
||||
|
||||
// Emit a 'user.created' event with some data
|
||||
strapi.eventHub.emit('user.created', { username: 'johndoe', email: 'johndoe@example.com' });
|
||||
```
|
||||
|
||||
In this example, we are emitting a `user.created` event with some data attached to it, and then listening for a user.updated event and logging the data. These events can be used to trigger actions within the Strapi application or to communicate with external systems.
|
||||
|
||||
For more information on how to use the EventEmitter class and its methods, see the [Node.js documentation](ttps://nodejs.org/docs/latest-v18.x/api/events.html#class-eventemitter).
|
||||
|
||||
### `strapi.startupLogger`
|
||||
|
||||
- [<Type>StartupLogger</Type>](StartupLogger)
|
||||
|
||||
Object containing predefined logger functions. Used for Strapi startup. (do not use as a logger elsewhere)
|
||||
|
||||
### `strapi.log`
|
||||
|
||||
- [<Type>Winston</Type>](https://github.com/winstonjs/winston#creating-your-own-logger)
|
||||
|
||||
A logger provided by Strapi that uses the Winston logging library. It is the result of calling the `winston.createLogger()` function with the configuration defined by the user of the Strapi application.
|
||||
|
||||
The logger provides various methods for logging messages at different levels of severity, including error, warn, info, verbose, debug, and silly. The logging level can be set via the configuration to control which messages are logged.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
// Log an error message
|
||||
strapi.log.error('Failed to start server', { error: err });
|
||||
|
||||
// Log a warning message
|
||||
strapi.log.warn('Server is running in development mode');
|
||||
|
||||
// Log an informational message
|
||||
strapi.log.info(`Server started on port ${PORT}`);
|
||||
|
||||
// Log a verbose message
|
||||
strapi.log.verbose('Application state', { user: currentUser });
|
||||
|
||||
// Log a debug message
|
||||
strapi.log.debug('API request received', { method: req.method, path: req.path });
|
||||
|
||||
// Log a silly message
|
||||
strapi.log.silly('Entered loop', { count: i });
|
||||
```
|
||||
|
||||
In these examples, we are logging messages at different levels of severity, including error, warn, info, verbose, debug, and silly. We are also passing in metadata as an object in the second parameter of each logging method.
|
||||
|
||||
The messages logged by strapi.log will be output according to the logging configuration set by the user of the Strapi application. This configuration determines which messages are logged and where they are logged (e.g. console, file, etc.).
|
||||
|
||||
### `strapi.cron`
|
||||
|
||||
- [<Type>CronService</Type>](Cron)
|
||||
|
||||
Module to schedule cron jobs for Strapi project. It is an instance of a custom Cron object.
|
||||
|
||||
### `strapi.telemetry`
|
||||
|
||||
- [<Type>TelemetryService</Type>](Telemetry)
|
||||
|
||||
The `strapi.telemetry` property provides access to the telemetry service instance. This service collects anonymous usage data about your Strapi application to help the Strapi team improve the product.
|
||||
|
||||
By default, the telemetry service is enabled, but you can disable it by setting the telemetryDisabled property to true in your application's package.json file, or by setting the `STRAPI_TELEMETRY_DISABLED` environment variable to true. You can also disable telemetry programmatically by setting the isDisabled property of the `strapi.telemetry` instance to true.
|
||||
|
||||
### `strapi.requestContext`
|
||||
|
||||
- <Type>Object</Type> Context Storage
|
||||
|
||||
- `run(store, cb)`: <Type>Function</Type>
|
||||
- `store`: <Type>Any</Type> Value that should be retrieved
|
||||
- `cb`: <Type>Function</Type> Callback
|
||||
- `get()` <Type>Function</Type>
|
||||
|
||||
The request context stores the ctx object from KoaJS on each request. This allows users to have access to the context from anywhere through the Strapi instance.
|
||||
|
||||
### `strapi.customFields`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
- `register(customField)`: <Type>Function</Type> Register a new custom field
|
||||
|
||||
This property is a shortcut to `strapi.get('custom-fields').add(customField)`.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
strapi.customFields.register({
|
||||
name: 'color',
|
||||
plugin: 'color-picker',
|
||||
type: 'string',
|
||||
});
|
||||
```
|
||||
|
||||
### `strapi.config`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
Shortcut to `strapi.get('config')`.
|
||||
|
||||
See the [config container](#config).
|
||||
|
||||
### `strapi.services`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('services').getAll()`.
|
||||
|
||||
See the [services' container](#services).
|
||||
|
||||
### `strapi.service(uid)`
|
||||
|
||||
- `uid`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('services').get(uid)`.
|
||||
|
||||
See the [services' container](#services).
|
||||
|
||||
### `strapi.controllers`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('controllers').getAll()`.
|
||||
|
||||
See the [controllers' container](#controllers).
|
||||
|
||||
### `strapi.controller(uid)`
|
||||
|
||||
- `uid`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('controllers').get(uid)`.
|
||||
|
||||
See the [controllers' container](#controllers).
|
||||
|
||||
### `strapi.contentTypes`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('content-types').getAll()`.
|
||||
|
||||
See the [content-types' container](#content-types).
|
||||
|
||||
### `strapi.contentType(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('content-types').get(name)`.
|
||||
|
||||
See the [content-types' container](#content-types).
|
||||
|
||||
### `strapi.policies`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('policies').getAll()`.
|
||||
|
||||
See the [policies' container](#policies).
|
||||
|
||||
### `strapi.policy(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('policies').get(name)`.
|
||||
|
||||
See the [policies' container](#policies).
|
||||
|
||||
### `strapi.middlewares`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('middlewares').getAll()`.
|
||||
|
||||
See the [middlewares container](#middlewares).
|
||||
|
||||
### `strapi.middleware(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('middlewares').get(name)`.
|
||||
|
||||
See the [middlewares container](#middlewares).
|
||||
|
||||
### `strapi.plugins`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('plugins').getAll()`.
|
||||
|
||||
See the [plugins' container](#plugins).
|
||||
|
||||
### `strapi.plugin(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('plugins').get(name)`.
|
||||
|
||||
See the [plugins' container](#plugins).
|
||||
|
||||
### `strapi.hooks`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('hooks').getAll()`.
|
||||
|
||||
See the [hooks' container](#hooks).
|
||||
|
||||
### `strapi.hook(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('hooks').get(name)`.
|
||||
|
||||
See the [hooks' container](#hooks).
|
||||
|
||||
### `strapi.apis`
|
||||
|
||||
- <Type>Object[]</Type>
|
||||
|
||||
Shortcut to `strapi.get('apis').getAll()`.
|
||||
|
||||
See the [apis container](#apis).
|
||||
|
||||
### `strapi.api(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
|
||||
Shortcut to `strapi.get('apis').get(name)`.
|
||||
|
||||
See the [apis container](#apis).
|
||||
|
||||
### `strapi.auth`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
Shortcut to `strapi.get('auth')`.
|
||||
|
||||
See the [auth' container](#auth).
|
||||
|
||||
### `strapi.contentAPI`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
Shortcut to `strapi.get('content-api')`.
|
||||
|
||||
See the [content-api container](#content-api).
|
||||
|
||||
### `strapi.sanitizers`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
Shortcut to `strapi.get('sanitizers')`.
|
||||
|
||||
See the [sanitizers' container](#sanitizers).
|
||||
|
||||
### `strapi.validators`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
Shortcut to `strapi.get('validators')`.
|
||||
|
||||
See the [validators' container](#validators).
|
||||
|
||||
### `strapi.start()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.destroy()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.sendStartupTelemetry()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.openAdmin({ isInitialized })`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.postListen()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.listen()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.stopWithError()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.stop(exitCode)`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadAdmin()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadPlugins()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadPolicies()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadAPIs()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadComponents()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadMiddlewares()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadApp()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.loadSanitizers()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.registerInternalHooks()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.register()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.bootstrap()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.load()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.reload()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.runLifecyclesFunctions()`
|
||||
|
||||
- Returns: Promise
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.getModel(uid)`
|
||||
|
||||
- `uid`: <Type>String</Type>
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `strapi.db.query(uid)`
|
||||
|
||||
- `uid`: <Type>String</Type>
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
## Strapi containers
|
||||
|
||||
The strapi containers are accessible via `strapi.get('name-of-the-container')`.
|
||||
|
||||
### `config`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
- `get(path, defaultValue)`: <Type>Function</Type>
|
||||
- `path`: <Type>String</Type>
|
||||
- `defaultValue`: <Type>Any</Type>
|
||||
- Returns: <Type>Any</Type> - The value located at `path` or, if undefined, `defaultValue`.
|
||||
- `set(path, value)`: <Type>Function</Type>
|
||||
- `path`: <Type>String</Type> - Where the value should be stored
|
||||
- `value`: <Type>Any</Type>
|
||||
- `has(path)`: <Type>Function</Type>
|
||||
- `path`: <Type>String</Type>
|
||||
- Returns: <Type>Boolean</Type> - Does the `path` match a value stored in the config container.
|
||||
- `launchedAt`: <Type>Number</Type> **Default:** `Date.now()`
|
||||
Date in milliseconds when the server has started
|
||||
- `serveAdminPanel`: <Type>Boolean</Type> **Default:** `true`
|
||||
See [Strapi constructor](#new-strapiopts) options
|
||||
- `autoReload`: <Type>Boolean</Type> **Default:** `false`
|
||||
See [Strapi constructor](#new-strapiopts) options
|
||||
- `environment`: <Type>String</Type> - process.env.NODE_ENV
|
||||
- `uuid`: <Type>String</Type> - string extracted from `package.json` located in `strapi.uuid`
|
||||
- `packageJsonStrapi`: <Type>Object</Type> - object extracted from `package.json` located in `strapi` (except uuid)
|
||||
- `info`: <Type>Object</Type>
|
||||
- everything stored in the `package.json`
|
||||
- `strapi`: <Type>String</Type> - Current version of Strapi
|
||||
|
||||
Every file stored under the `config` folder will be injected in this config container object.
|
||||
|
||||
### `services`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `controllers`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `content-types`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `policies`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `plugins`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `hooks`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `apis`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `auth`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `content-api`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `sanitizers`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `validators`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: API (WIP)
|
||||
slug: /api/API
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# API
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The Strapi API module permits to generate a Strapi API object that wrap all the functionalities around Strapi endpoints
|
||||
|
||||
## Module: API
|
||||
|
||||
### `createAPI(strapi, opts)`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Type({ children }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: '#017501',
|
||||
}}
|
||||
>
|
||||
<{children}>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Container
|
||||
slug: /api/container
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Container
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The container module permits to generate containers.
|
||||
|
||||
## Module: container
|
||||
|
||||
### class Container
|
||||
|
||||
### `new Container()`
|
||||
|
||||
- Returns: <Type>Container</Type>
|
||||
|
||||
```javascript
|
||||
const container = new Container();
|
||||
|
||||
container.add('config', {
|
||||
get: (configName) => {},
|
||||
set: (configName, value) => {},
|
||||
});
|
||||
|
||||
const dbConfig = container.get('config').get('database');
|
||||
```
|
||||
|
||||
### `container.add(name, resolver)`
|
||||
|
||||
- `name`: <Type>String</Type> UID of the content
|
||||
- `resolver`: <Type>Function</Type> | <Type>Any</Type>
|
||||
- As a function, the function will be executed when the first get method is called on this content. The result of this function will define the content of this UID.
|
||||
- `resolver(context, args)`
|
||||
- `context`: <Type>Container</Type> The container instance
|
||||
- `args`: <Type>Any</Type> Anything to be used by the resolver function
|
||||
- As anything else, this value will be resolved when getting this specified content through its UID.
|
||||
|
||||
Register a new content to be accessed inside the container. If the name is already used, it will throw an error.
|
||||
|
||||
```javascript
|
||||
const container = new Container();
|
||||
|
||||
container.add('config', (container, args) => {});
|
||||
// or
|
||||
container.add('services', {});
|
||||
```
|
||||
|
||||
### `container.get(name, args)`
|
||||
|
||||
- `name`: <Type>String</Type> UID of the content
|
||||
- `args`: <Type>Any</Type> Value that will be passed to the resolver (if function)
|
||||
|
||||
Get the value stored for a specific `name`.
|
||||
|
||||
```javascript
|
||||
const container = new Container();
|
||||
|
||||
container.add('config', { db: 'sqlite' });
|
||||
|
||||
const config = container.get('config');
|
||||
// config.db === 'sqlite'
|
||||
```
|
||||
|
||||
⚠️ If the **resolver**, used in the [register function](#containerregistername-resolver), is a **function**, the value will be the result of this resolver function with `args` as parameter on the first call to `get`.
|
||||
|
||||
Please pay attention that the resolver result value isn't awaited. So if resolver returns a promise, the value stored will be a promise.
|
||||
|
||||
```javascript
|
||||
const container = new Container();
|
||||
|
||||
container.add('boolean', (bool) => bool);
|
||||
|
||||
// First call - The value is resolved through the resolver above "(bool) => bool"
|
||||
container.get('boolean', true);
|
||||
// true
|
||||
|
||||
// Any further call will use the previously set value
|
||||
container.get('boolean');
|
||||
// true
|
||||
|
||||
// Even if we try to push a new value
|
||||
container.get('boolean', false);
|
||||
// true
|
||||
```
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: Cron Service
|
||||
slug: /api/Cron
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Cron
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The Strapi Cron Service provides a way to add, remove, start, and stop cron jobs in a Strapi application.
|
||||
|
||||
## Module: Cron Service
|
||||
|
||||
### createCronService()
|
||||
|
||||
The `createCronService()` function returns an object that provides methods to manage cron jobs.
|
||||
|
||||
## Methods
|
||||
|
||||
### `cron.add(tasks)`
|
||||
|
||||
- `tasks`: <Type>Object</Type>
|
||||
- Returns: `this`
|
||||
|
||||
Adds one or more cron tasks to the service.
|
||||
|
||||
- Each key of the `tasks` object is the name of the task.
|
||||
- Each value of the `tasks` object can be either a function, or an object with two properties: `task` and `options`.
|
||||
- If the value is a function, it is used as the task to be executed when the cron expression is met.
|
||||
- The key will be considered as the cron expression
|
||||
- If the value is an object, its `task` property is used as the task function, and its `options` property is used as the cron expression options.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
const { createCronService } = require('packages/core/strapi/lib/services/cron.js');
|
||||
|
||||
const cron = createCronService();
|
||||
|
||||
const task = () => {
|
||||
console.log('Task executed!');
|
||||
};
|
||||
|
||||
cron.add({
|
||||
myTask: {
|
||||
task,
|
||||
options: '*/5 * * * *', // Executes every 5 minutes.
|
||||
},
|
||||
'*/1 * * * *': () => console.log('A minute has passed.'),
|
||||
});
|
||||
```
|
||||
|
||||
### `cron.remove(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
- Returns: `this`
|
||||
|
||||
Removes a cron task from the service.
|
||||
|
||||
- The `name` parameter is the name of the task to remove.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
const { createCronService } = require('packages/core/strapi/lib/services/cron.js');
|
||||
|
||||
const cron = createCronService();
|
||||
|
||||
const task = () => {
|
||||
console.log('Task executed!');
|
||||
};
|
||||
|
||||
cron.add({
|
||||
myTask: {
|
||||
task,
|
||||
options: '*/5 * * * *', // Executes every 5 minutes.
|
||||
},
|
||||
});
|
||||
|
||||
cron.remove('myTask');
|
||||
```
|
||||
|
||||
### `cron.start()`
|
||||
|
||||
- Returns: `this`
|
||||
|
||||
Starts the cron service.
|
||||
|
||||
- Schedules all the cron jobs.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
const { createCronService } = require('packages/core/strapi/lib/services/cron.js');
|
||||
|
||||
const cron = createCronService();
|
||||
|
||||
const task = () => {
|
||||
console.log('Task executed!');
|
||||
};
|
||||
|
||||
cron.add({
|
||||
myTask: {
|
||||
task,
|
||||
options: '*/5 * * * *', // Executes every 5 minutes.
|
||||
},
|
||||
});
|
||||
|
||||
cron.start();
|
||||
```
|
||||
|
||||
### `cron.stop()`
|
||||
|
||||
- Returns: `this`
|
||||
|
||||
Stops the cron service.
|
||||
|
||||
- Cancels all the scheduled jobs.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
const { createCronService } = require('packages/core/strapi/lib/services/cron.js');
|
||||
|
||||
const cron = createCronService();
|
||||
|
||||
const task = () => {
|
||||
console.log('Task executed!');
|
||||
};
|
||||
|
||||
cron.add({
|
||||
myTask: {
|
||||
task,
|
||||
options: '*/5 * * * *', // Executes every 5 minutes.
|
||||
},
|
||||
});
|
||||
|
||||
// Start the scheduled cron jobs
|
||||
cron.start();
|
||||
// Stops the cron jobs
|
||||
cron.stop();
|
||||
```
|
||||
|
||||
### `cron.destroy()`
|
||||
|
||||
- Returns: `this`
|
||||
|
||||
Destroys the cron service.
|
||||
|
||||
- Calls the `stop()` method.
|
||||
- Clears the list of cron jobs.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
const { createCronService } = require('packages/core/strapi/lib/services/cron.js');
|
||||
|
||||
const cron = createCronService();
|
||||
|
||||
const task = () => {
|
||||
console.log('Task executed!');
|
||||
};
|
||||
|
||||
cron.add({
|
||||
myTask: {
|
||||
task,
|
||||
options: '*/5 * * * *', // Executes every 5 minutes.
|
||||
},
|
||||
});
|
||||
|
||||
// Start the scheduled cron jobs
|
||||
cron.start();
|
||||
// Stops the cron jobs and remove all scheduled tasks
|
||||
cron.destroy();
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
### `cron.jobs`
|
||||
|
||||
- <Type>Array</Type>
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
- `job`: [<Type>Job</Type>](https://github.com/node-schedule/node-schedule) - Job object by node-schedule
|
||||
- `options`: <Type>String</Type> - String representing the recurrence of the job ( like '_/5 _ \* \* \*' )
|
||||
- `name`: <Type>String</Type> - The name of the task associated to the job
|
||||
|
||||
An array of the cron jobs added to the service.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: EventHub (WIP)
|
||||
slug: /api/EventHub
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Strapi Event Hub
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
Strapi Event Hub module - description to be done
|
||||
|
||||
## Module: EventHub
|
||||
|
||||
### `createEventHub()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Startup Logger
|
||||
slug: /api/StartupLogger
|
||||
tags:
|
||||
- module
|
||||
- private
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# API
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
This module is a simple logger for starting up Strapi with some useful information.
|
||||
|
||||
## Module: Startup Logger
|
||||
|
||||
### `logStats()`
|
||||
|
||||
This log will display information about the instance of Strapi. The time launched, how many times it took and important configuration information.
|
||||
|
||||
```
|
||||
Project information
|
||||
|
||||
┌────────────────────┬──────────────────────────────────────────────────┐
|
||||
│ Time │ Wed Jan 01 2000 00:00:01 GMT+0200 (Central Euro… │
|
||||
│ Launched in │ 2000 ms │
|
||||
│ Environment │ development │
|
||||
│ Process PID │ 42 │
|
||||
│ Version │ 4.9.0 (node v18.12.1) │
|
||||
│ Edition │ Enterprise │
|
||||
│ Database │ postgres │
|
||||
└────────────────────┴──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### `logFirstStartupMessage()`
|
||||
|
||||
This log will display the first time Strapi project is launched. It will ask the user to create its first admin user in the admin panel.
|
||||
|
||||
### `logDefaultStartupMessage()`
|
||||
|
||||
Default message to display when the Strapi server is started.
|
||||
|
||||
```
|
||||
Actions available
|
||||
|
||||
Welcome back!
|
||||
To manage your project 🚀, go to the administration panel at:
|
||||
http://localhost:1337/admin
|
||||
|
||||
To access the server ⚡️, go to:
|
||||
http://localhost:1337
|
||||
```
|
||||
|
||||
### `logStartupMessage({ isInitialized })`
|
||||
|
||||
- `isInitialized`: <Type>Boolean</Type> Has the Strapi project already been initialized?
|
||||
|
||||
Will display the correct start-up message according to the specified boolean.
|
||||
|
||||
:::note
|
||||
|
||||
Can be disabled by setting the server config value:
|
||||
|
||||
```javascript
|
||||
// in config/server.js
|
||||
{
|
||||
logger: {
|
||||
startup: {
|
||||
enabled: false;
|
||||
}
|
||||
},
|
||||
// ...the rest of your server config
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: StrapiFS (WIP)
|
||||
slug: /api/StrapiFS
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Strapi File System
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The Strapi FS module is a wrapper around FS NodeJS module to manipulate local files.
|
||||
|
||||
## Module: StrapiFS
|
||||
|
||||
### `createStrapiFs(strapi)`
|
||||
|
||||
### `strapiFs.writeAppFile(optPath, data)`
|
||||
|
||||
:::caution
|
||||
Deprecated
|
||||
:::
|
||||
|
||||
### `strapiFs.writePluginFile(plugin, optPath, data)`
|
||||
|
||||
:::caution
|
||||
Deprecated
|
||||
:::
|
||||
|
||||
### `strapiFs.removeAppFile(optPath)`
|
||||
|
||||
:::caution
|
||||
Deprecated
|
||||
:::
|
||||
|
||||
### `strapiFs.appendFile(optPath, data)`
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Strapi server (WIP)
|
||||
slug: /api/strapi-server
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Strapi Server
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The Strapi server module permits to generate a Strapi http server.
|
||||
|
||||
## Module: Strapi server
|
||||
|
||||
### `createServer(strapi)`
|
||||
|
||||
- `strapi`: [<Type>Strapi</Type>](Strapi)
|
||||
- Returns: <Type>StrapiServer</Type>
|
||||
|
||||
```javascript
|
||||
const server = createServer(strapi);
|
||||
|
||||
server.listRoutes();
|
||||
```
|
||||
|
||||
### `StrapiServer.app`
|
||||
|
||||
- [<Type>KoaJS</Type>](https://devdocs.io/koa/index)
|
||||
|
||||
Strapi projects are using KoaJS to run the NodeJS server.
|
||||
|
||||
### `StrapiServer.router`
|
||||
|
||||
- [<Type>@koa/router</Type>](https://github.com/ZijianHe/koa-router#router-)
|
||||
|
||||
Strapi projects are using a dependency of KoaJS called @koa/router.
|
||||
|
||||
### `StrapiServer.httpServer`
|
||||
|
||||
- [<Type>http.Server</Type>](https://nodejs.org/docs/latest-v18.x/api/http.html)
|
||||
|
||||
The Strapi's HTTP server.
|
||||
|
||||
### `StrapiServer.api(name)`
|
||||
|
||||
- `name`: <Type>String</Type>
|
||||
- Returns: [<Type>StrapiAPIs</Type>](#strapiapis)
|
||||
|
||||
Getter for apis available in Strapi
|
||||
|
||||
### `StrapiServer.use(...args)`
|
||||
|
||||
- [<Type>KoaApp.use</Type>](https://devdocs.io/koa/index#appusefunction)
|
||||
|
||||
Shortcut for Koa `app.use(...args)` method.
|
||||
|
||||
### `StrapiServer.routes(routes)`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiServer.mount()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiServer.initRouting()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiServer.initMiddlewares()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiServer.listRoutes()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiServer.listen(...args)`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiServer.destroy()`
|
||||
|
||||
:::info
|
||||
TODO
|
||||
:::
|
||||
|
||||
### `StrapiAPIs`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
- `content-api`: [<Type>API</Type>](API)
|
||||
- API used by external requesters
|
||||
- `admin`: [<Type>API</Type>](API)
|
||||
- API used by admin panel
|
||||
|
||||
Strapi APIs is a map of all APIs available inside the Strapi project.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: API Reference example
|
||||
tags:
|
||||
- utils
|
||||
- class
|
||||
- public
|
||||
- global
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Name of Module
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable** | **Legacy** | **Deprecated**
|
||||
|
||||
:::
|
||||
|
||||
_**Stable** - can be use as is_
|
||||
|
||||
_**Legacy** - Old code that needs refactoring to match the current architecture of the code_
|
||||
|
||||
_**Deprecated** - Should **NOT** be used, this will be deleted anytime soon_
|
||||
|
||||
_Description with a general example on how to use this Class / Module_
|
||||
|
||||
## Class: Name of the class
|
||||
|
||||
### Public variable (e.g. in an EventEmitter class `Event: 'close'`)
|
||||
|
||||
### Static methods (e.g. `Static method: Class.default()`)
|
||||
|
||||
### `new Class()`
|
||||
|
||||
Instances of the Class class can be created using the new keyword.
|
||||
|
||||
```javascript
|
||||
const myClass = new Class();
|
||||
```
|
||||
|
||||
### `class.method(param1, param2)`
|
||||
|
||||
- `param1`: <Type>String</Type> (can be linked to other API doc page).
|
||||
- `param2`: <Type>Object</Type>
|
||||
- `options1`: <Type>Number</Type>
|
||||
|
||||
The `class.method()` method display the `param1` and then skip `param2` lines.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
const { Class } = require('pathToClassFile');
|
||||
const textLines = ['Welcome', "That's all", 'Thanks'];
|
||||
|
||||
const classInstance = new Class();
|
||||
|
||||
for (const text of textLines) {
|
||||
classInstance.method(text, 1);
|
||||
}
|
||||
// Prints:
|
||||
// Welcome
|
||||
// That's all
|
||||
// Thanks
|
||||
```
|
||||
|
||||
## Function: `name_of_the_function(param1, param2)`
|
||||
|
||||
- `param1`: <Type>String</Type> (can be linked to other API doc page)
|
||||
- `param2`: <Type>Object</Type>
|
||||
- `options1`: <Type>Number</Type>
|
||||
|
||||
The `name_of_the_function()` method display the `param1` and then skip `param2` lines.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
const { name_of_the_function } = require('pathToFunctionFile');
|
||||
const textLines = ['Welcome', "That's all", 'Thanks'];
|
||||
|
||||
for (const text of textLines) {
|
||||
name_of_the_function(text, 1);
|
||||
}
|
||||
// Prints:
|
||||
// Welcome
|
||||
// That's all
|
||||
// Thanks
|
||||
```
|
||||
|
||||
This structure is highly based on NodeJS API reference documentation. https://nodejs.org/api
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Telemetry Service
|
||||
slug: /api/Telemetry
|
||||
tags:
|
||||
- module
|
||||
- public
|
||||
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Type from '@site/docs/api/components/type';
|
||||
|
||||
# Telemetry
|
||||
|
||||
:::info
|
||||
|
||||
Current state: **Stable**
|
||||
|
||||
:::
|
||||
|
||||
The telemetry service is responsible for collecting and sending anonymous usage data to Strapi. This service is disabled by default, but can be enabled or disabled via configuration.
|
||||
|
||||
## Usage Information
|
||||
|
||||
The collected usage data is used to help Strapi improve the product by identifying areas of improvement, tracking feature adoption, and measuring performance. You can learn more about the usage data that is collected by visiting the following link:
|
||||
|
||||
https://docs.strapi.io/developer-docs/latest/getting-started/usage-information.html
|
||||
|
||||
## Module: Telemetry Service
|
||||
|
||||
### createTelemetryInstance()
|
||||
|
||||
- strapi: [<Type>Strapi</Type>](Strapi) - A strapi instance.
|
||||
|
||||
The `createTelemetryInstance()` function returns an instance of the Telemetry service.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
const createTelemetryInstance = require('path/to/telemetry');
|
||||
|
||||
const telemetry = createTelemetryInstance(strapi);
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### `telemetry.register()`
|
||||
|
||||
Registers the telemetry instance.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
telemetry.register();
|
||||
```
|
||||
|
||||
### `telemetry.bootstrap()`
|
||||
|
||||
Bootstraps the telemetry instance.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
telemetry.bootstrap();
|
||||
```
|
||||
|
||||
### `telemetry.destroy()`
|
||||
|
||||
Destroys the telemetry instance.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
telemetry.destroy();
|
||||
```
|
||||
|
||||
### `telemetry.send(event, payload)`
|
||||
|
||||
- `event`: <Type>String</Type> - The event to be sent.
|
||||
- `payload`: [<Type>TelemetryPayload</Type>](#telemetrypayload) - The payload to be sent with the event.
|
||||
- Returns: Promise
|
||||
|
||||
Sends telemetry event with the given payload.
|
||||
|
||||
#### Examples
|
||||
|
||||
```javascript
|
||||
telemetry.send('event_name', { key: 'value' });
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### `TelemetryPayload`
|
||||
|
||||
- <Type>Object</Type>
|
||||
|
||||
- `eventProperties`: <Type>Object</Type> An object that contains additional information about the event.
|
||||
- `userProperties`: <Type>Object</Type> An object that defines the identity of the user who triggered the event.
|
||||
- `groupProperties`: <Type>Object</Type> An object that defines properties of the application or environment in which the event occurred.
|
||||
|
||||
Examples of event properties in Strapi include model, containsRelationalFields, displayedFields, kind. These properties are specific to the event and are used to provide additional context about what happened.
|
||||
|
||||
User properties can include information such as the user's operating system, node version, and hostname. These properties are typically used to group events by user or to filter events based on certain user characteristics.
|
||||
|
||||
Group properties can include information such as the language(s) used in the application, the database being used, and the number of locales. These properties are typically used to group events by application version, environment, or other characteristics.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
Hello & welcome to the contributor documentation of the Strapi Monorepo! Here you'll find both technical and conceptual documentation on the codebase.
|
||||
|
||||
Generally speaking the documentation structure is as follows:
|
||||
|
||||
```shell
|
||||
core/
|
||||
├─ admin/
|
||||
├─ content-manager/
|
||||
│ ├─ documentation-file.mdx
|
||||
├─ content-type-builder/
|
||||
plugins/
|
||||
├─ documentation/
|
||||
├─ i18n/
|
||||
```
|
||||
|
||||
This helps keep the documentation organized according to the file structure of the `packages` folder within the monorepo. From there however, is dependant on the documentation written, it will most likely change over time when the documentation grows and develops.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"position": 1,
|
||||
"label": "Core",
|
||||
"collapsible": true,
|
||||
"collapsed": false
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- admin
|
||||
---
|
||||
|
||||
# Admin
|
||||
|
||||
This section is an overview of all the features related to admin:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- enterprise-edition
|
||||
---
|
||||
|
||||
# Admin Enterprise Edition
|
||||
|
||||
This section is an overview of all the features related to the Enterprise Edition in Admin:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
|
||||
# Promoting EE features in CE projects
|
||||
|
||||
Everytime a new EE feature is added in Strapi, in the settings menu, you should add the following condition to ensure that the feature promotes itself in CE:
|
||||
|
||||
`packages/core/admin/admin/src/hooks/useSettingsMenu/index.js`
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
...(!window.strapi.features.isEnabled(window.strapi.features.NEW_EE_FEATURE) &&
|
||||
window.strapi?.flags?.promoteEE
|
||||
? [
|
||||
{
|
||||
intlLabel: {
|
||||
id: 'Settings.new-ee-feature.page.title',
|
||||
defaultMessage: 'NEW EE FEATURE',
|
||||
},
|
||||
to: '/settings/purchase-new-ee-feature',
|
||||
id: 'new-ee-feature',
|
||||
licenseOnly: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...
|
||||
```
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Review Workflows
|
||||
description: Review workflow technical design
|
||||
tags:
|
||||
- review-workflows
|
||||
- implementation
|
||||
- tech design
|
||||
---
|
||||
|
||||
# Review Workflows
|
||||
|
||||
## Summary
|
||||
|
||||
The review workflow feature is only available in the Enterprise Edition.
|
||||
That is why, in part, it is completely decoupled from the code of the Community Edition.
|
||||
|
||||
The purpose of this feature is to allow users to assign a tag to the various entities of their Strapi project. This tag is called a 'stage' and is available within what we will call a workflow.
|
||||
|
||||
## Detailed backend design
|
||||
|
||||
The Review Workflow feature have been built with one main consideration, to be decoupled from the Community Edition. As so, the implementation can relate a lot to how a plugin would be built.
|
||||
|
||||
All the backend code related to Review Workflow can be found in `packages/core/admin/ee`.
|
||||
This code is separated into several elements:
|
||||
|
||||
- Content-types
|
||||
- _strapi_workflows_: `packages/core/admin/ee/server/content-types/workflow/index.js`
|
||||
- _strapi_workflows_stages_: `packages/core/admin/ee/server/content-types/workflow-stage/index.js`
|
||||
- Controllers
|
||||
- _workflows_: `packages/core/admin/ee/server/controllers/workflows/index.js`
|
||||
- _stages_: `packages/core/admin/ee/server/controllers/workflows/stages/index.js`
|
||||
- _assignees_: `packages/core/admin/ee/server/controllers/workflows/assignees/index.js`
|
||||
- Middlewares
|
||||
- [_DEPRECATED_] _contentTypeMiddleware_: `packages/core/admin/ee/server/middlewares/review-workflows.js`
|
||||
- Routes
|
||||
- `packages/core/admin/ee/server/routes/index.js`
|
||||
- Services
|
||||
- _review-workflows_: `packages/core/admin/ee/server/services/review-workflows/review-workflows.js`
|
||||
- _workflows_: `packages/core/admin/ee/server/services/review-workflows/workflows.js`
|
||||
- _stages_: `packages/core/admin/ee/server/services/review-workflows/stages.js`
|
||||
- _metrics_: `packages/core/admin/ee/server/services/review-workflows/metrics.js`
|
||||
- _weekly-metrics_: `packages/core/admin/ee/server/services/review-workflows/weekly-metrics.js`
|
||||
- _validation_: `packages/core/admin/ee/server/services/review-workflows/validation.js`
|
||||
- _assignees_: `packages/core/admin/ee/server/services/review-workflows/assignees.js`
|
||||
- _stage-permissions_: `packages/core/admin/ee/server/services/review-workflows/stage-permissions.js`
|
||||
- Utils file
|
||||
- _Review workflows utils_: `packages/core/admin/ee/server/utils/review-workflows.js`
|
||||
- A bootstrap and a register part
|
||||
- `packages/core/admin/ee/server/bootstrap.js`
|
||||
- `packages/core/admin/ee/server/register.js`
|
||||
|
||||
### Content types
|
||||
|
||||
#### strapi_workflows
|
||||
|
||||
This content type stores the workflow information and is responsible for holding all the information about stages and their order. In MVP, only one workflow is stored inside the Strapi database.
|
||||
|
||||
#### strapi_workflows_stages
|
||||
|
||||
This content type store the stage information such as its name.
|
||||
|
||||
### Controllers
|
||||
|
||||
#### workflows
|
||||
|
||||
Used to interact with the `strapi_workflows` content-type.
|
||||
|
||||
#### stages
|
||||
|
||||
Used to interact with the `strapi_workflows_stages` content-type.
|
||||
|
||||
#### assignees
|
||||
|
||||
Used to interact with the `admin_users` content-type entities related to review workflow enabled content types.
|
||||
|
||||
### Middlewares
|
||||
|
||||
#### contentTypeMiddleware - _DEPRECATED_
|
||||
|
||||
In order to properly manage the options for content-type in the root level of the object, it is necessary to relocate the `reviewWorkflows` option within the `options` object located inside the content-type data. By doing so, we can ensure that all options are consistently organized and easily accessible within their respective data structures. This will also make it simpler to maintain and update the options as needed, providing a more streamlined and efficient workflow for developers working with the system. Therefore, it is recommended to move the reviewWorkflows option to its appropriate location within the options object inside the content-type data before sending it to the admin API.
|
||||
|
||||
### Routes
|
||||
|
||||
The Admin API of the Enterprise Edition includes several routes related to the Review Workflow feature. Here is a list of those routes:
|
||||
|
||||
#### GET `/review-workflows/workflows`
|
||||
|
||||
This route returns a list of all workflows.
|
||||
|
||||
#### POST `/review-workflows/workflows`
|
||||
|
||||
This route creates a new workflow.
|
||||
|
||||
#### GET `/review-workflows/workflows/:id`
|
||||
|
||||
This route returns the details of a specific workflow identified by the id parameter.
|
||||
|
||||
#### PUT `/review-workflows/workflows/:id`
|
||||
|
||||
This route updates a specific workflow identified by the id parameter.
|
||||
|
||||
#### DELETE `/review-workflows/workflows/:id`
|
||||
|
||||
This route deletes a specific workflow identified by the id parameter.
|
||||
|
||||
#### GET `/review-workflows/workflows/:workflow_id/stages`
|
||||
|
||||
This route returns a list of all stages associated with a specific workflow identified by the workflow_id parameter.
|
||||
|
||||
#### GET `/review-workflows/workflows/:workflow_id/stages/:id`
|
||||
|
||||
This route returns the details of a specific stage identified by the id parameter and associated with the workflow identified by the workflow_id parameter.
|
||||
|
||||
#### PUT `/review-workflows/workflows/:workflow_id/stages`
|
||||
|
||||
This route updates the stages associated with a specific workflow identified by the workflow_id parameter. The updated stages are passed in the request body.
|
||||
|
||||
#### PUT `/content-manager/(collection|single)-types/:model_uid/:id/stage`
|
||||
|
||||
This route updates the stage of a specific entity identified by the id parameter and belonging to a specific collection identified by the model_uid parameter. The new stage value is passed in the request body.
|
||||
|
||||
#### GET `/content-manager/(collection|single)-types/:model_uid/:id/stages`
|
||||
|
||||
Returns a list of stages that a user has permission to transition into (based on the permission settings of a stage).
|
||||
|
||||
#### PUT `/content-manager/(collection|single)-types/:model_uid/:id/assignee`
|
||||
|
||||
This route updates the assignee of the entity identified by the model_uid and id parameters. The updated entity is passed to the request body.
|
||||
|
||||
### Services
|
||||
|
||||
The Review Workflow feature of the Enterprise Edition includes several services to manipulate workflows and stages. Here is a list of those services:
|
||||
|
||||
#### review-workflows
|
||||
|
||||
This service is used during the bootstrap and register phases of Strapi. Its primary responsibility is to migrate data on entities as needed and add the stage field to the entity schemas.
|
||||
|
||||
#### workflows
|
||||
|
||||
This service is used to manipulate the workflows entities. It provides functionalities to create, retrieve, and update workflows.
|
||||
|
||||
#### stages
|
||||
|
||||
This service is used to manipulate the stages entities and to update stages on other entities. It provides functionalities to create, retrieve, update, and delete stages.
|
||||
|
||||
#### metrics
|
||||
|
||||
This is the telemetry service used to gather information on the usage of this feature. It provides information on the number of workflows and stages created, as well as the frequency of stage updates on entities.
|
||||
|
||||
#### weekly-metrics
|
||||
|
||||
Once a week we report on review workflows usage statistic. This service is used to set up the cron job responsible for gathering and sending statistics on: number of active workflows, average number of stages in a workflow, maximum number of stages across all workflows and the content types on which review workflows is activated.
|
||||
|
||||
#### assignees
|
||||
|
||||
This service is used to interact with admin user assignee relations on review workflow enabled content types. It provides the ability to: find the Id of an entity assignee, update and delete (unassign) the assignee on an entity.
|
||||
|
||||
#### stage-permissions
|
||||
|
||||
This service is used to enable RBAC functionality for review workflow stages. Each entry of the `strapi_workflows_stages` has a manyToMany relation with `admin_permissions`. The permissions held in this relation indicate which roles can change the review stage of an entry in this stage. The service provides the ability to: register and unregister new stage permissions based on stage and role Ids and to find out whether a role can transition from a given stage.
|
||||
|
||||
#### validation
|
||||
|
||||
This service is used to ensure the feature is working as expected and validate the data to be valid.
|
||||
|
||||
## Alternatives
|
||||
|
||||
The Review Workflow feature is currently included as a core feature within the Strapi repository. However, there has been discussion about potentially moving it to a plugin in the future. While no decision has been made on this subject yet, it is possible that it may happen at some point in the future.
|
||||
|
||||
## Resources
|
||||
|
||||
- https://docs.strapi.io/user-docs/settings/review-workflows
|
||||
- https://docs.strapi.io/user-docs/content-type-builder/creating-new-content-type#creating-a-new-content-type
|
||||
- https://docs.strapi.io/user-docs/users-roles-permissions/configuring-administrator-roles#plugins-and-settings
|
||||
- [Content Manager Review Workflows](../../content-manager/02-review-workflows.mdx)
|
||||
- [Content Type Builder Review Workflows](../../content-type-builder/01-review-workflows.mdx)
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Audit Logs
|
||||
description: Guide for Audit Logs
|
||||
tags:
|
||||
- settings
|
||||
- audit-logs
|
||||
---
|
||||
|
||||
# Audit Logs
|
||||
|
||||
## Summary
|
||||
|
||||
Audit Logs provide a way to view the history of all user actions at Admin API level. This includes actions related to entries (including publish actions), media (including its folders), users, login & logout of admin users, components, roles, and permissions, you can see the list of all the default events [here](https://github.com/strapi/strapi/blob/main/packages/core/admin/ee/server/services/audit-logs.js#L9).
|
||||
|
||||
## Backend design
|
||||
|
||||
The Audit Logs feature was built to take advantage of the eventHub. You can find the service and its core code at `packages/core/admin/ee/server/services`.
|
||||
|
||||
### Audit Logs Local Provider
|
||||
|
||||
To save audit log data, we utilize an Audit Logs local provider responsible for interacting with the database. This provider should return an object with a `register` function, and this function should return an object with functions to handle `saveEvent`, `findMany`, `findOne`, and `deleteExpiredEvents`.
|
||||
|
||||
### Content types
|
||||
|
||||
#### strapi_audit_logs
|
||||
|
||||
This content type stores all the audit logs. For each allowed event, we save an entry in the audit logs content type.
|
||||
|
||||
### Subscribing to all events
|
||||
|
||||
The Audit Logs feature adds a subscriber to the [EventHub](/docs/core/strapi/event-hub), allowing it to listen to all events in the application. However, we don't save every event in the audit logs; we only save the default ones (see the defaultEvents array in the service file).
|
||||
|
||||
### Retention days
|
||||
|
||||
As the number of events in our Audit Logs can grow significantly, we run a daily job at midnight to delete logs that are older than the retention days defined.
|
||||
|
||||
By default, the retention days are set to 90 days, but this value can be changed. For enterprise self-hosted projects, users can set a configuration variable (admin.auditLogs.retentionDays) and use that one.
|
||||
|
||||
For cloud projects, the retention days are determined by the license. In a cloud project, users can set a custom retention days in the configuration, but this value cannot exceed the retention days defined by the license.
|
||||
|
||||
In both cases, if we want to set a custom retention days we can modify the Admin Panel API config file (`./config/admin.js`). You can find all the possible options as well as other configurations for the Admin Panel on the [documentation page](https://docs.strapi.io/dev-docs/configurations/admin-panel#available-options).
|
||||
|
||||
### Audit Logs format
|
||||
|
||||
Every Audit Log has the following format:
|
||||
|
||||
```typescript
|
||||
type Event {
|
||||
action: string, // Name of the event
|
||||
date: Date, // When the event happens
|
||||
userId: number, // Id of the user that trigger the event
|
||||
payload?: Object, // Extra info of the event
|
||||
};
|
||||
```
|
||||
|
||||
To understand how we obtain this information, we need to know how we emit an event with the Event Hub. To emit an event, we use the following function: (To see more info about the EventHub, click [here](/docs/core/strapi/event-hub)
|
||||
|
||||
```typescript
|
||||
strapi.eventHub.emit(name: Pick<Event, 'name'>, payload: Pick<Event, 'payload'>);
|
||||
```
|
||||
|
||||
First, we check the event is coming from admin requests and it's on our [default events](https://github.com/strapi/strapi/blob/main/packages/core/admin/ee/server/services/audit-logs.js#L9) list, then when creating our Audit Log, we retrieve the action and payload from this emitted event, where the first argument is the action or event name, and the second one is the payload. We obtain the user from the requestContext.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Hooks",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: useLicenseLimits
|
||||
description: API reference for the useLicenseLimits hook
|
||||
tags:
|
||||
- admin
|
||||
- hooks
|
||||
- users
|
||||
- ee
|
||||
---
|
||||
|
||||
An abstraction around `react-query`'s `useQuery` hook to fetch license limits for a project.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const { license, getFeature, isError, isLoading } = useLicenseLimits(options);
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### `enabled`
|
||||
|
||||
Boolean flag which is passed onto react-query indicating whether the license check should be performed. This
|
||||
can be useful if e.g. other permissions need to be checked before.
|
||||
|
||||
## Result
|
||||
|
||||
### `license`
|
||||
|
||||
An object that contains the raw admin API response.
|
||||
|
||||
### `getFeature(name: string)`
|
||||
|
||||
Returns options for a given feature. If the feature was not found, it returns an empty object. This is mostly a
|
||||
convenience method, to avoid having to filter the features array on the license object every time.
|
||||
|
||||
#### Usage
|
||||
|
||||
```
|
||||
const { getFeature } = useLicenseLimits();
|
||||
|
||||
const reviewWorkflowOptions = getFeature('review-workflows');
|
||||
```
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
import { UseQueryResult } from 'react-query';
|
||||
|
||||
// Note: the list of attributes might be incomplete
|
||||
interface License {
|
||||
enforcementUserCount: number;
|
||||
currentActiveUserCount: number;
|
||||
permittedSeats: number;
|
||||
shouldNotify: boolean;
|
||||
shouldStopCreate: boolean;
|
||||
licenseLimitStatus: 'OVER_LIMIT' | 'AT_LIMIT';
|
||||
isHostedOnStrapiCloud: boolean;
|
||||
features: LicenseFeature[];
|
||||
}
|
||||
|
||||
interface LicenseFeature {
|
||||
name: string;
|
||||
options?: object;
|
||||
}
|
||||
|
||||
type Options {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
type UseLicenseLimit = (options: Options) => Pick<UseQueryResult, 'isError' | 'isLoading'> & { license: License, getFeature: (name: string) => object }
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "EE",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Introduction
|
||||
slug: /permissions-rbac
|
||||
tags:
|
||||
- permissions
|
||||
- RBAC
|
||||
---
|
||||
|
||||
# Permissions (RBAC)
|
||||
|
||||
:::caution
|
||||
This is not about the `user-permissions` plugin which is API orientated, this is about roles based access control commonly known as RBAC.
|
||||
:::
|
||||
|
||||
This section is an overview of all the features related to the Permissions core plugin:
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ To document ⚠️
|
||||
|
||||
- RBAC in the settings page of the admin app
|
||||
- How RBAC works serverside
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: How Permissions Work
|
||||
tags:
|
||||
- permissions
|
||||
- RBAC
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The RBAC permission system relies under the hood on the [CASL](https://casl.js.org) library.
|
||||
|
||||
A `permission` is the combination of an `action`, a `subject`, some `properties` and some `conditions`.
|
||||
The logic is that: a user can perform an `action` on a `subject` and some of its `properties` only if it its role has the `permission` and if the `conditions` associated to the permission pass.
|
||||
The creation and edition of permissions is done through the [edit page](http://localhost:1337/admin/settings/roles/2) of a role by checking or unchecking checkboxes.
|
||||
|
||||
Example 1: the action `update` can be applied on the subject `article` and property `title`, this result is the permission to update the title of an article.
|
||||
|
||||
Example 2: the condition `isCreator` added to the previous permission will ensure that only the creator of the article can update the title.
|
||||
|
||||
Example 3: the action `access the Marketplace page` has no subject to apply on. The permission only contains the action `admin::marketplace.read`.
|
||||
|
||||
The permissions are attached to a role. Each role can have different permissions independentely. There is no inheritance system.
|
||||
To check if an action can be performed by a user, frontend and backend simply retrieve the list of the permissions associated with the user's roles and check if it contains the permission associated with the action about to be performed. The backend also runs the associated conditions.
|
||||
|
||||
:::tip
|
||||
If a user has several role, the user will be allowed to perform an action if at least one of its roles is allowed to perform it.
|
||||
:::
|
||||
|
||||
## Actions
|
||||
|
||||
### Action definition
|
||||
|
||||
An action contains the following information:
|
||||
|
||||
| key | description | type | required | Default value | example |
|
||||
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------- | -------------------------- |
|
||||
| **uid** | An id that has to be unique within the plugin. | string | required | - | `'audit-logs.read'` |
|
||||
| **pluginName** | Name of the plugin registrering the action. | string | - | `'api'` | `'admin'` |
|
||||
| **section** | Name of the section among `contentTypes`, `plugins`, `settings` and `internal`. It will define in which permission tab the action will appear. <br/> `internal` is not displayed in any tab but used for internal purposes. | string | required | - | `'settings'` |
|
||||
| **category** | Name of the category. It will define in which category the action will appear. _Only for the plugins and settings section_. | string | - | - | `'audit logs'` |
|
||||
| **subCategory** | Name of the subcategory. It will define in which subcategory or the category the action will appear. _Only for the plugins and settings section_. | string | - | - | `'options'` |
|
||||
| **displayName** | Human name of the action. | string | required | - | `'Read'` |
|
||||
| **subjects** | List of subjects the action can be applied to. _Only for the contentTypes section_. | array | - | - | `['api::article.article']` |
|
||||
| **options** | Option object | object | - | `{}` | `{}` |
|
||||
| **options.applyToProperties** | List of properties the action can be applied to. _Only for the contentTypes section_. | - | - | `[]` | `['fields', 'locale']` |
|
||||
|
||||
import rbacEditPageImage from '@site/static/img/permissions/rbac-edit-page.png';
|
||||
|
||||
<figure>
|
||||
<img
|
||||
src={rbacEditPageImage}
|
||||
alt="Screenshot of the RBAC edit page showing the where are located the section, category, subcategory and display name"
|
||||
/>
|
||||
<figcaption>
|
||||
Screenshot of the RBAC edit page showing the where are located the section, category,
|
||||
subcategory and display name
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
:::note
|
||||
`uid` and `pluginName` are used to create a global unique id.
|
||||
|
||||
Examples: `admin::audit-logs.read`, `plugin::content-manager.explorer.create`
|
||||
:::
|
||||
|
||||
### Register an action
|
||||
|
||||
An action can be registered in the backend during the `bootstrap` phase of the app.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
// bootstrap.ts
|
||||
module.exports = async () => {
|
||||
const actions = [
|
||||
{
|
||||
uid: 'provider-login.read',
|
||||
displayName: 'Read',
|
||||
pluginName: 'admin',
|
||||
section: 'settings',
|
||||
category: 'single sign on',
|
||||
subCategory: 'options',
|
||||
},
|
||||
];
|
||||
await strapi.service('admin::permission').actionProvider.registerMany(actions);
|
||||
};
|
||||
```
|
||||
|
||||
### Parametrize actions
|
||||
|
||||
Actions can be parametrized with the `actionParameters` property. This property is an object that can contain any key/value pair.
|
||||
|
||||
An example of the feature:
|
||||
|
||||
- The action `review-workflows.stage.transition` can be parametrized with the `from` and `to` parameters, which are the ids of the stages a user can transition from and to.
|
||||
|
||||
At the moment only Review Workflows use this feature internally.
|
||||
|
||||
## Conditions
|
||||
|
||||
### Condition definition
|
||||
|
||||
A condition contains the following information:
|
||||
|
||||
| key | description | type | required | default value | example |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------- | -------------- |
|
||||
| **displayName** | The condition name as shown in the admin panel | string | required | - | `'Is creator'` |
|
||||
| **name** | A name that has to be unique within the plugin | string | required | - | `'is-creator'` |
|
||||
| **plugin** | Name of the plugin that register the condition | string | - | `'api'` | `'admin'` |
|
||||
| **category** | Name of the category. Conditions can be grouped into categories available in the admin panel | string | - | `'Default'` | `'admin'` |
|
||||
| **handler** | A function used to verify the condition (see [docs.strapi.io](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/rbac.html#using-the-condition-handler)) | string | required | - | `'admin'` |
|
||||
|
||||
:::note
|
||||
Currently the user can choose to apply a condition on any action. There is no way to define a list of actions on which the condition can be applied to. This is why it is possible to set the condition `isCreator` on a the action `plugin::content-manager.explorer.create` even if it doesn't make sense.
|
||||
:::
|
||||
:::note
|
||||
`name` and `plugin` are used to create a global unique id.
|
||||
|
||||
Examples: `admin::is-creator`
|
||||
:::
|
||||
|
||||
### Registering a condition
|
||||
|
||||
A condition can be registered in the backend during the `bootstrap` phase of the app.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
// bootstrap.ts
|
||||
module.exports = async () => {
|
||||
const conditions = [
|
||||
{
|
||||
displayName: 'Is creator',
|
||||
name: 'is-creator',
|
||||
plugin: 'admin',
|
||||
handler: (user) => ({ 'createdBy.id': user.id }),
|
||||
},
|
||||
];
|
||||
await strapi.service('admin::permission').conditionProvider.registerMany(conditions);
|
||||
};
|
||||
```
|
||||
|
||||
More information on how the handler works [here](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/rbac.html#using-the-condition-handler).
|
||||
|
||||
## Permissions
|
||||
|
||||
### Permission definition
|
||||
|
||||
The permissions are modified in the admin panel (on the [edit page](http://localhost:1337/admin/settings/roles/2) of a role) and stored in the database with the following information:
|
||||
|
||||
| key | description | type | required | example |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------------------------------------------------------- |
|
||||
| **action** | Id of the action that will be permitted. | string | required | `'plugin::content-manager.explorer.update'` |
|
||||
| **actionParameters** | Object to parametrize actions. | string | - | `{}` |
|
||||
| **subject** | Id of the subject on which the action will be permitted. | string | - | `'api::article.article'` |
|
||||
| **properties** | List of the properties of the subject on which the action will be permitted | object | - | `{ fields: ['title', 'description'], locales: ['en', 'fr'] }` |
|
||||
| **conditions** | List of the conditions that will be ran against an entry to determine whether the action on this entry is permitted or not | array | - | `['admin::is-creator']` |
|
||||
|
||||
A permission contains all needed information for the backend and the frontend to prevent users to perform non-permitted action.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Frontend",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Fetching Permissions
|
||||
tags:
|
||||
- permissions
|
||||
- RBAC
|
||||
---
|
||||
|
||||
This document lays out the code-flow of fetching the permissions in the CMS and how the data is then injected in to the app
|
||||
to be used by components.
|
||||
|
||||
## Where do we fetch them?
|
||||
|
||||
At the very root of the entire admin panel we handle 4 routes:
|
||||
|
||||
- Signing into the app
|
||||
- Not Found
|
||||
- Collecting information from a first time user
|
||||
- The authenticated application (where you use the CMS)
|
||||
|
||||
Permissions, along with other vital application information is fetched in in the `AuthenticatedApp` component, located
|
||||
in the `packages/core/admin/admin/src/AuthenticatedApp` folder. The cache key (used with `react-query`'s `useQueries` hook)
|
||||
for the particular call is `admin-users-permission`.
|
||||
|
||||
## What do we fetch?
|
||||
|
||||
Because permissions are based on users in this case the individual logged in, we fetch their permissions from the route:
|
||||
`/admin/users/me/permissions`. An example can be seen below:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 426,
|
||||
"action": "admin::api-tokens.access",
|
||||
"subject": null,
|
||||
"properties": {},
|
||||
"conditions": []
|
||||
},
|
||||
{
|
||||
"id": 427,
|
||||
"action": "admin::api-tokens.regenerate",
|
||||
"subject": null,
|
||||
"properties": {},
|
||||
"conditions": []
|
||||
},
|
||||
{
|
||||
"id": 763,
|
||||
"action": "plugin::content-manager.explorer.create",
|
||||
"subject": "api::homepage.homepage",
|
||||
"properties": {
|
||||
"fields": ["title", "slug", "single", "multiple"],
|
||||
"locales": ["en", "fr"]
|
||||
},
|
||||
"conditions": []
|
||||
},
|
||||
{
|
||||
"id": 765,
|
||||
"action": "plugin::content-manager.explorer.read",
|
||||
"subject": "api::homepage.homepage",
|
||||
"properties": {
|
||||
"fields": ["title", "slug", "single", "multiple"],
|
||||
"locales": ["en", "fr"]
|
||||
},
|
||||
"conditions": []
|
||||
},
|
||||
{
|
||||
"id": 767,
|
||||
"action": "plugin::content-manager.explorer.update",
|
||||
"subject": "api::homepage.homepage",
|
||||
"properties": {
|
||||
"fields": ["title", "slug", "single", "multiple"],
|
||||
"locales": ["en", "fr"]
|
||||
},
|
||||
"conditions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
An explanation on how these work can be found [here](../01-how-they-work.mdx). But as a quick overview we can see that permissions
|
||||
typically associate to actions and in the case of the content manager they have subjects to be more granular and then fields & locales to
|
||||
be even more granular. These actions are defined by domains of the application registered via the application as they also have effect
|
||||
when calling APIs.
|
||||
|
||||
## What do we do with the data?
|
||||
|
||||
The data is then passed to the `RBACProvider` component which passes it to a public facing [React Context](https://beta.reactjs.org/learn/passing-data-deeply-with-context)
|
||||
as well as injecting the information into the global `redux` store – see diagram below:
|
||||
|
||||
<img
|
||||
src="/img/permissions/permissions-into-app.png"
|
||||
alt="diagram of how permissions get from the server to the application"
|
||||
/>
|
||||
|
||||
Inside the `RBACProviderContext` we also pass the ability to refetch the permissions. These two inconjunction with one another are accessed
|
||||
via the `useRBACProvider` hook. You can see the API for this hook [here](../../02-permissions/02-frontend/using-permissions.mdx#typescript)
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
---
|
||||
title: Using Permissions
|
||||
tags:
|
||||
- permissions
|
||||
- RBAC
|
||||
- useRBAC
|
||||
---
|
||||
|
||||
Currently, there are a few different ways to use permissions in the application. This document will explain the different ways, how to use them and suggestions
|
||||
on when is best to use them. It's advised that you've read [how they work](../01-how-they-work.mdx) and [fetching permissions](fetching-permissions.mdx) before reading this document so you have the full context.
|
||||
|
||||
## Components
|
||||
|
||||
There are two different wrapper components available to use. They are very similar but have different use cases. They are both
|
||||
available in the `@strapi/strapi/admin`.
|
||||
|
||||
### CheckPagePermissions
|
||||
|
||||
Used to apply RBAC to a view/page of the application. If the user does not have the permissions to access this page they will
|
||||
be redirect to the homepage.
|
||||
|
||||
#### Usage
|
||||
|
||||
```tsx
|
||||
import { Page } from '@strapi/strapi/admin';
|
||||
|
||||
const permissions = [{ action: 'plugins::my-plugin.access', subject: null }];
|
||||
|
||||
const MyPage = () => {
|
||||
return (
|
||||
<Page.Protect permissions={permissions}>
|
||||
<h1>aha you found me</h1>
|
||||
</Page.Protect>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Typescript
|
||||
|
||||
```ts
|
||||
interface Permission {
|
||||
id: number;
|
||||
action: string;
|
||||
subject: string | null;
|
||||
// This can be custom defined to the needs of the plugin/application
|
||||
properties: Record<string, any>;
|
||||
conditions: Array<string>;
|
||||
}
|
||||
|
||||
type CheckPagePermissionsProps = {
|
||||
permissions: Permission[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type CheckPagePermissions = (props: CheckPagePermissionsProps) => JSX.Element;
|
||||
```
|
||||
|
||||
## useRBAC
|
||||
|
||||
Is a wrapper around the [`hasPermissions`](#haspermissions) function which calls the `/admin/permissions/check` endpoint with a provided
|
||||
list of permissions to assertain if a user can do a specific action. This hook is typically used with plugin permissions alongside the
|
||||
global permissions object generated from the [`useRBACProvider`](#userbacprovider) hook which can either be passed or will be accessed internally.
|
||||
|
||||
Because it's fetching, we also provide a `isLoading` state which can be used to show a loading state while the permissions are being fetched.
|
||||
If the hook is unmouted before the fetch request completes, the request will be cancelled via an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
|
||||
### Usage
|
||||
|
||||
In the below example, we're checking if a user can "create" a post from the content-manager list view. If they can't, the button is `disabled`.
|
||||
|
||||
```tsx
|
||||
import { useRBAC } from '@strapi/strapi/admin';
|
||||
|
||||
const MyComponent = () => {
|
||||
const { isLoading, allowedActions } = useRBAC({
|
||||
create: [
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.create',
|
||||
subject: 'api::post.post',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
return <button disabled={!allowedActions.canCreate}>create entry</button>;
|
||||
};
|
||||
```
|
||||
|
||||
### Typescript
|
||||
|
||||
```ts
|
||||
interface Permission {
|
||||
id: number;
|
||||
action: string;
|
||||
subject: string | null;
|
||||
// This can be custom defined to the needs of the plugin/application
|
||||
properties: Record<string, any>;
|
||||
conditions: Array<string>;
|
||||
}
|
||||
|
||||
interface PluginPermissions {
|
||||
create: Permission[];
|
||||
read: Permission[];
|
||||
update: Permission[];
|
||||
delete: Permission[];
|
||||
publish: Permission[];
|
||||
}
|
||||
|
||||
type UseRBAC = (
|
||||
pluginPermissions: PluginPermissions,
|
||||
permissions?: Permission[]
|
||||
) => {
|
||||
isLoading: boolean;
|
||||
setIsLoading: () => void;
|
||||
allowedActions: {
|
||||
canCreate: boolean;
|
||||
canRead: boolean;
|
||||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
canPublish: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Permissions (RBAC)",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Introduction
|
||||
slug: /settings/intro
|
||||
tags:
|
||||
- settings
|
||||
---
|
||||
|
||||
# Settings
|
||||
|
||||
This section is an overview of all the features related to Settings:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Review Workflows
|
||||
slug: /settings/review-workflows
|
||||
description: Guide for review workflows in settings.
|
||||
tags:
|
||||
- settings
|
||||
- review-workflows
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
There are 3 views available to manage workflows and stages: List, Edit and Create. These pages are only **accessible in enterprise mode** and
|
||||
if the user has the permission `admin::review-workflows.read`.
|
||||
|
||||
Upon mount the settings pages injects themselves into the global redux store under the namespace `settings_review-workflows`. Redux is
|
||||
then used for all state management updates. `Formik` is used to render and validate the edit and create views. It
|
||||
is integrated with redux, so that all input components are controlled components.
|
||||
|
||||
### License limits
|
||||
|
||||
Most licenses have feature-based usage limits configured through Chargebee. These limits are exposed to the frontend through [`useLicenseLimits`](/docs/core/admin/ee/hooks/use-license-limits).
|
||||
Offline licenses do not have these limits and therefore the endpoint does not return any. For offline licenses hard-coded limits apply: max. 200 workflows, max. 200 stages per workflow.
|
||||
|
||||
The frontend aswell as the admin API restrict actions on the settings pages in various ways (more below), if the license limits are about to exceed or already exceeded.
|
||||
|
||||
### List view
|
||||
|
||||
The list-view displays all available workflows.
|
||||
|
||||
#### License limits
|
||||
|
||||
Displays a modal in case the user is at or above the license limit on page load. In case a user attempts to create a new workflow the same modal is shown again, in case that creation
|
||||
would exceed the limit.
|
||||
|
||||
### Edit & Create views
|
||||
|
||||
The edit and create views allow workflows and stages to be edited (existing ones) and new workflows to be created. To enable a review-workflow on a content-type users select
|
||||
from a list of all content-types (collections aswell as single-types) for each workflow. In case a content-type is already assigned to another content-type, the content-type
|
||||
will be re-assigned to the new workflow.
|
||||
|
||||
Stages always have a color assigned to them. Colors are stored as hex-codes in the database, to avoid coupling with the design-system and developers to know about possible color-names. The
|
||||
admin app renders the color badges for a stage based in the matching theme-color and in case it doesn't find any the stored hex-code. It was not possible to normalize the hex-codes
|
||||
to be uppercase everywhere (e.g. if they are created via lifecycle methods), so the frontend does the normalization.
|
||||
|
||||
#### Form submission
|
||||
|
||||
The form wrapping boths views submits all stages at once, because we expect the number of stages per workflow to be
|
||||
rather small. Because of this we can simply re-order stages by sending them in a different order. Every stage that sends a corresponding `id`
|
||||
attribute will be re-ordered and not created.
|
||||
|
||||
Stages without an `id` property will be created in the database on submission. Stages that existed already, but are not submitted again will be deleted on submission.
|
||||
|
||||
When editing a workflow and the `permissions` of a stage have not been modified by a user, they are sent as `undefined`. This special case allows users to edit a workflow without having to have
|
||||
permissions to read `roles`, because the API won't update anything in the database.
|
||||
|
||||
Users without read permissions for roles are able to create workflows, but they will not be able to define which roles can change a stage, so that stages of that workflow can only be changed
|
||||
by the super-admin unless the roles are set.
|
||||
|
||||
### Edit view
|
||||
|
||||
Displays a license-limits modal in case the user is above the license limit on page load and upon form submission.
|
||||
|
||||
#### Deletion of a stage
|
||||
|
||||
If a stage is deleted, all **entities which are connected to that stage are moved to the previous stage**. Because a stage deletion
|
||||
might have big effects on the database, a confirmation is shown before users save changes.
|
||||
|
||||
Changes are only applied once the user clicks "Save". It is not possible to remove all stages from a workflow (neither in the UI nor the API).
|
||||
|
||||
#### Create view
|
||||
|
||||
Displays a license-limit modal in case the user is at or above the license limit on page load and upon form submission.
|
||||
|
||||
### Hooks
|
||||
|
||||
#### `useReviewWorkflows(queryParams)`
|
||||
|
||||
This hook allows to fetch either one (if `params` contains an `id`) or all workflows. By default stages are populated. The
|
||||
hooks returns a partial react-query result.
|
||||
|
||||
```ts
|
||||
useReviewWorkflows(queryParams: object): {
|
||||
meta: { workflowCount: number }
|
||||
workflows: Workflow[],
|
||||
isLoading: boolean,
|
||||
status: string,
|
||||
refetch: () => Promise<void>,
|
||||
}
|
||||
```
|
||||
|
||||
#### `useReviewWorkflowsStages({ id, layout }, reactQueryOptions)`
|
||||
|
||||
This hook allows to fetch stages for a given workflow, that a user has permissions to transition to.
|
||||
|
||||
```ts
|
||||
type ContentTypeLayout {
|
||||
uid: string;
|
||||
kind: 'collectionType' | 'singleType';
|
||||
}
|
||||
|
||||
useReviewWorkflowsStages({ id: number, layout: ContentTypeLayout }, reactQueryOptions): {
|
||||
meta: { workflowCount: number, stagesCount: number }
|
||||
stages: Stage[],
|
||||
isLoading: boolean,
|
||||
refetch: () => Promise<void>,
|
||||
}
|
||||
```
|
||||
|
||||
### Data shapes
|
||||
|
||||
```ts
|
||||
type Stage {
|
||||
id: number;
|
||||
color: string; // hex code
|
||||
name: string; // max-length: 255 characters
|
||||
permissions: Permission[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
type Workflow {
|
||||
id: number;
|
||||
name: string; // max-length: 255 characters, unique
|
||||
contentTypes: number[];
|
||||
stages: Stage[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### Endpoints
|
||||
|
||||
For a list of all available endpoints please refer to the [detailed backend design documentation](/docs/core/admin/ee/review-workflows).
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Settings",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Features",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Hooks",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: useAdminRoles
|
||||
description: API reference for the useAdminRoles hook
|
||||
tags:
|
||||
- admin
|
||||
- hooks
|
||||
- roles
|
||||
---
|
||||
|
||||
An abstraction around `react-query`'s `useQuery` hook. It can be used to fetch one ore more admin role.
|
||||
|
||||
## Usage
|
||||
|
||||
The hooks can receive two optional parameters:
|
||||
|
||||
1. query params: an object containing the query params to be sent to the API. They are going to be
|
||||
stringified by `qs`. All params are equal except `id`, which is used to fetch a single users, if
|
||||
it is passed.
|
||||
2. options: an object containing the options to be passed to `useQuery`.
|
||||
|
||||
It returns an object containing some of the react-query attributes.
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
import { UseQueryOptions } from 'react-query'
|
||||
|
||||
type Role = object;
|
||||
|
||||
useAdminRoles(queryParams: object, reactQueryOptions: UseQueryOptions): {
|
||||
roles: Role[],
|
||||
isLoading: boolean;
|
||||
error: object;
|
||||
isError: boolean;
|
||||
refetch: () => Promise<void>;
|
||||
};
|
||||
```
|
||||
|
||||
### Fetch all roles
|
||||
|
||||
```jsx
|
||||
import { useAdminRoles } from 'path/to/hooks';
|
||||
|
||||
const MyComponent = ({ onMoveItem }) => {
|
||||
const { roles, isLoading, refetch } = useAdminRoles();
|
||||
|
||||
return /* ... */;
|
||||
};
|
||||
```
|
||||
|
||||
### Fetch one role
|
||||
|
||||
```jsx
|
||||
import { Box } from '@strapi/design-system';
|
||||
|
||||
import { useAdminRoles } from 'path/to/hooks';
|
||||
|
||||
const MyComponent = ({ onMoveItem }) => {
|
||||
const {
|
||||
users: [user],
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useAdminRoles({ id: 1 });
|
||||
|
||||
return /* ... */;
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: unstable_useDocument
|
||||
description: API reference for the useDocument hook
|
||||
tags:
|
||||
- admin
|
||||
- hooks
|
||||
- document
|
||||
- validations
|
||||
---
|
||||
|
||||
:::caution
|
||||
This hook is not stable and **likely to change**. Use at your own risk.
|
||||
:::
|
||||
|
||||
A hook that returns utilities to work with documents.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
function Component() {
|
||||
const { validate } = unstable_useDocument();
|
||||
}
|
||||
```
|
||||
|
||||
At the moment, useDocument only returns a validate function.
|
||||
|
||||
### `validate()`
|
||||
|
||||
```ts
|
||||
const validate(
|
||||
entry: Entity,
|
||||
{
|
||||
contentType: Schema.ContentType,
|
||||
components: {
|
||||
[key: Schema.Component['uid']]: Schema.Component;
|
||||
},
|
||||
isCreatingEntry?: boolean;
|
||||
}
|
||||
): {
|
||||
errors: { [key: string]: TranslationMessage }
|
||||
}
|
||||
```
|
||||
|
||||
With this function, you can apply the same validations we use in the admin. Please be aware that for this function to work, you need to ensure the following:
|
||||
|
||||
1. If you are validating an entry with relations/components/dynamic zones then it **MUST** be populated. Otherwise, the validations will pass even if there is an error on one populated field.
|
||||
2. The correct schema for the contentType and the schemas for any components related to that content type are mandatory for this hook to function correctly.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: useEnterprise
|
||||
description: API reference for the useEnterprise hook
|
||||
tags:
|
||||
- admin
|
||||
- hooks
|
||||
- users
|
||||
---
|
||||
|
||||
A hook that returns either community or enterprise-edition data-structures based on the global `window.strapi.isEE` flag.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
import { CE_DATA } from './data';
|
||||
|
||||
function Component() {
|
||||
const data = useEnterprise(CE_DATA, async () => (await import('./ee/data')).default);
|
||||
}
|
||||
```
|
||||
|
||||
It accepts an optional third argument to pass in options customizing the hook behavior:
|
||||
|
||||
### `combine()`
|
||||
|
||||
THe `combine` callback can be used as a custom "merge" function for the ce and ee arguments:
|
||||
|
||||
```
|
||||
const data = useEnterprise({ a: 1 }, () => { b: 1 }, { combine(ce, ee) { return { ...ce, ...ee } } });
|
||||
|
||||
console.log(data); // { a: 1, b: 1 }
|
||||
```
|
||||
|
||||
### `defaultValue`
|
||||
|
||||
By default the hook returns `null` if `window.strapi.isEE` is true and the enterprise data structure is not yet loaded. Customizing
|
||||
this value can help implementing various loading scenarios:
|
||||
|
||||
```
|
||||
// display a loading state while an EE component is loading
|
||||
const Component = useEnterprise(() => <p>CE</p>, () => <p>EE</p>, {
|
||||
defaultValue: () => <div>loading ...</div>
|
||||
})
|
||||
|
||||
// display nothing while an EE component is loading, but don't block the overall rendering
|
||||
const Component = useEnterprise(() => <p>CE</p>, () => <p>EE</p>, {
|
||||
defaultValue: () => null
|
||||
})
|
||||
|
||||
// display nothing while an EE component is loading
|
||||
const Component = useEnterprise(() => <p>CE</p>, () => <p>EE</p>)
|
||||
|
||||
if (!Component) {
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### `enabled`
|
||||
|
||||
Similar to react-query this boolean flag allows disabling the EE import, e.g. when more than one condition needs to be applied. If `enabled`
|
||||
is set to false, the first argument (CE_DATA) will be returned.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: NPS
|
||||
tags:
|
||||
- admin
|
||||
- nps
|
||||
---
|
||||
|
||||
## What does it do
|
||||
|
||||
The NPS survey is shown to users to get their feedback about Strapi. It is based on a rating scale from 0 to 10, and we also invite users to provide additional comments.
|
||||
|
||||
## When do we show the survey?
|
||||
|
||||
The NPS survey is only displayed to admin users who have selected the "Keep me updated" checkbox during registration. The survey is displayed after 5 minutes of activity.
|
||||
|
||||
The survey is shown to eligible users based on the following rules:
|
||||
|
||||
- If a user responds to the survey, the survey will be presented again within 90 days.
|
||||
- If a user does not respond to the survey the first time after their last response, the survey will be presented again after 7 days.
|
||||
- If a user does not respond to the survey for the second or subsequent time after their last response, the survey will be presented again after 90 days.
|
||||
|
||||
## Where data is submitted
|
||||
|
||||
The data is sent to this endpoint: `https://analytics.strapi.io/submit-nps`.
|
||||
|
||||
## Hooks
|
||||
|
||||
### useNpsSurveySettings
|
||||
|
||||
This hook uses the `usePersistentState` hook from the helper-plugin (more information available [here](/docs/core/helper-plugin/hooks/use-persistent-state)). It is exported so that it can be used during the registration process to determine whether users have selected the "Keep me updated" checkbox.
|
||||
@@ -0,0 +1,382 @@
|
||||
# Guided Tour
|
||||
|
||||
The Guided Tour provides an interactive onboarding experience that guides new users through key features of the admin panel. This documentation explains how the system works and how to create new guided tours.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Guided Tour system is built with a modular architecture consisting of:
|
||||
|
||||
- **Context Provider** (`Context.tsx`) - Global state management and persistence
|
||||
- **Tour Factory** (`Tours.tsx`) - Tour factory and GuidedTourTooltip
|
||||
- **Steps** (`Steps`) - Step factory, reusable step components, and tour specific step components
|
||||
- **Overview Component** (`Overview.tsx`) - Homepage tour overview and progress tracking
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Tours and Steps
|
||||
|
||||
Tours are collections of steps that guide users through specific workflows. Each step will display its content in a popover.
|
||||
|
||||
```typescript
|
||||
const tours = {
|
||||
contentTypeBuilder: createTour('contentTypeBuilder', [
|
||||
{
|
||||
name: 'Introduction',
|
||||
content: (Step) => (
|
||||
<Step.Root>
|
||||
<Step.Title id="tour.title" defaultMessage="Welcome!" />
|
||||
<Step.Content id="tour.content" defaultMessage="Let's get started." />
|
||||
<Step.Actions showSkip />
|
||||
</Step.Root>
|
||||
),
|
||||
},
|
||||
// ... more steps
|
||||
]),
|
||||
};
|
||||
```
|
||||
|
||||
### 2. State Management
|
||||
|
||||
The tour state is managed through a React Context with a reducer pattern:
|
||||
|
||||
```typescript
|
||||
type State = {
|
||||
tours: Tour; // Tour progress for each tour
|
||||
enabled: boolean; // Whether tours are globally enabled
|
||||
completedActions: ExtendedCompletedActions; // User-completed actions
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Conditional Steps
|
||||
|
||||
Steps can be conditionally displayed based on user actions:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: 'ConditionalStep',
|
||||
when: (completedActions) => completedActions.includes('didCreateContent'),
|
||||
content: (Step) => (/* step content */)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Excluding Steps from Step Count
|
||||
|
||||
Some steps shouldn't be counted in the step count displayed to the user. Use `excludeFromStepCount` to exclude them:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: 'Welcome',
|
||||
excludeFromStepCount: true,
|
||||
content: (Step) => (
|
||||
<Step.Root>
|
||||
<Step.Title id="tour.welcome" defaultMessage="Welcome!" />
|
||||
<Step.Content id="tour.intro" defaultMessage="Let's get started with this tour." />
|
||||
<Step.Actions showStepCount={false} />
|
||||
</Step.Root>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`createTour()` will add a `_meta` property to the tour object that provides information about the number of steps in the tour (total steps) and then the number of steps that will actually be displayed to the user:
|
||||
|
||||
- `_meta.totalStepCount` - The total number of steps defined for the tour
|
||||
- `_meta.displayedStepCount` - The total number of steps - the number of steps with `excludeFromStepCount: true`
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Creating a New Tour
|
||||
|
||||
1. **Define the tour structure** in `Tours.tsx`:
|
||||
|
||||
```typescript
|
||||
const myNewTour = createTour('myNewTour', [
|
||||
{
|
||||
name: 'Introduction',
|
||||
content: (Step) => (
|
||||
<Step.Root>
|
||||
<Step.Title
|
||||
id="tours.myNewTour.Introduction.title"
|
||||
defaultMessage="My New Feature"
|
||||
/>
|
||||
<Step.Content
|
||||
id="tours.myNewTour.Introduction.content"
|
||||
defaultMessage="This tour will show you how to use this feature."
|
||||
/>
|
||||
<Step.Actions showSkip />
|
||||
</Step.Root>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'MainAction',
|
||||
content: (Step) => (
|
||||
<Step.Root side="right" sideOffset={16}>
|
||||
<Step.Title
|
||||
id="tours.myNewTour.MainAction.title"
|
||||
defaultMessage="Main Action"
|
||||
/>
|
||||
<Step.Content
|
||||
id="tours.myNewTour.MainAction.content"
|
||||
defaultMessage="Click this button to perform the main action."
|
||||
/>
|
||||
<Step.Actions />
|
||||
</Step.Root>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Finish',
|
||||
content: (Step) => (
|
||||
<Step.Root>
|
||||
<Step.Title
|
||||
id="tours.myNewTour.Finish.title"
|
||||
defaultMessage="You're all set!"
|
||||
/>
|
||||
<Step.Content
|
||||
id="tours.myNewTour.Finish.content"
|
||||
defaultMessage="You've successfully completed this tour."
|
||||
/>
|
||||
<Step.Actions showStepCount={false} to="/next-page" />
|
||||
</Step.Root>
|
||||
),
|
||||
when: (completedActions) => completedActions.includes('didCompleteMainAction'),
|
||||
},
|
||||
]);
|
||||
|
||||
// Add to the tours object
|
||||
const tours = {
|
||||
// ... existing tours
|
||||
myNewTour,
|
||||
} as const;
|
||||
```
|
||||
|
||||
2. **Add tour components to Strapi**:
|
||||
|
||||
Wrap the element that should be the anchor for the step popover.
|
||||
|
||||
```tsx
|
||||
import { tours } from '@strapi/admin/strapi-admin';
|
||||
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<tours.myNewTour.Introduction>
|
||||
<h1>My Feature Title</h1>
|
||||
</tours.myNewTour.Introduction>
|
||||
|
||||
<tours.myNewTour.MainAction>
|
||||
<Button onClick={handleMainAction}>Main Action</Button>
|
||||
</tours.myNewTour.MainAction>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
3. **Mark actions complete** to trigger conditional steps:
|
||||
|
||||
```tsx
|
||||
import { useGuidedTour } from './Context';
|
||||
|
||||
const MyComponent = () => {
|
||||
const dispatch = useGuidedTour('MyComponent', (s) => s.dispatch);
|
||||
|
||||
const handleMainAction = () => {
|
||||
// Perform the action
|
||||
performMainAction();
|
||||
|
||||
// Track the completion
|
||||
dispatch({
|
||||
type: 'set_completed_actions',
|
||||
payload: ['didCompleteSomeAction'],
|
||||
});
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Step Components API
|
||||
|
||||
Each step provides three main components:
|
||||
|
||||
#### Step.Root
|
||||
|
||||
The container for the popover with positioning options. Wraps and receives the same props as the [Radix popover](https://www.radix-ui.com/primitives/docs/components/popover).
|
||||
|
||||
```tsx
|
||||
<Step.Root
|
||||
side="top|right|bottom|left" // Popover position
|
||||
align="start|center|end" // Alignment along the side
|
||||
sideOffset={number} // Offset from the anchor
|
||||
withArrow={boolean} // Show/hide arrow (default: true)
|
||||
>
|
||||
```
|
||||
|
||||
#### Step.Title
|
||||
|
||||
The step title with i18n support:
|
||||
|
||||
```tsx
|
||||
<Step.Title
|
||||
id="translation.key"
|
||||
defaultMessage="Default Title"
|
||||
/>
|
||||
|
||||
// Or with custom content:
|
||||
<Step.Title>
|
||||
<CustomTitleComponent />
|
||||
</Step.Title>
|
||||
```
|
||||
|
||||
#### Step.Content
|
||||
|
||||
The step content with i18n support:
|
||||
|
||||
```tsx
|
||||
<Step.Content
|
||||
id="translation.key"
|
||||
defaultMessage="Default content message"
|
||||
/>
|
||||
|
||||
// Or with custom content:
|
||||
<Step.Content>
|
||||
<CustomContentComponent />
|
||||
</Step.Content>
|
||||
```
|
||||
|
||||
#### Step.Actions
|
||||
|
||||
Action buttons with built-in functionality:
|
||||
|
||||
```tsx
|
||||
<Step.Actions
|
||||
showStepCount={boolean} // Show "Step X of Y" (default: true)
|
||||
showSkip={boolean} // Show skip button (default: false)
|
||||
to="/path" // Navigate to path on next (optional)
|
||||
/>
|
||||
|
||||
// Or with custom actions:
|
||||
<Step.Actions>
|
||||
<CustomActionsComponent />
|
||||
</Step.Actions>
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### State
|
||||
|
||||
The guided tour system initializes with a default state structure defined in the Context:
|
||||
|
||||
```typescript
|
||||
type State = {
|
||||
tours: Tour;
|
||||
enabled: boolean;
|
||||
completedActions: ExtendedCompletedActions;
|
||||
};
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
const initialState = {
|
||||
tours: {
|
||||
contentManager: {
|
||||
currentStep: 0,
|
||||
isCompleted: false,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
completedActions: ['didCreateSchema'],
|
||||
};
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
The tour reducer handles these actions:
|
||||
|
||||
- `next_step` - Advance to the next step in a tour
|
||||
- `previous_step` - Go back to the previous step in a tour
|
||||
- `go_to_step` - Go to a specific step
|
||||
- `skip_tour` - Mark a tour as completed (skipped)
|
||||
- `skip_all_tours` - Disable all tours globally
|
||||
- `reset_all_tours` - Reset all tours to initial state
|
||||
- `set_completed_actions` - Update the list of completed user actions
|
||||
|
||||
### Using the Hook
|
||||
|
||||
```tsx
|
||||
import { useGuidedTour } from './Context';
|
||||
|
||||
const MyComponent = () => {
|
||||
const state = useGuidedTour('MyComponent', (s) => s.state);
|
||||
const dispatch = useGuidedTour('MyComponent', (s) => s.dispatch);
|
||||
|
||||
const currentTour = state.tours.myNewTour;
|
||||
const isEnabled = state.enabled;
|
||||
const completedActions = state.completedActions;
|
||||
|
||||
// Dispatch actions
|
||||
const handleNext = () => {
|
||||
dispatch({ type: 'next_step', payload: 'myNewTour' });
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Persistence
|
||||
|
||||
Tour state is automatically persisted to localStorage using the `usePersistentState` hook with the key `STRAPI_GUIDED_TOUR`.
|
||||
|
||||
## Backend Integration
|
||||
|
||||
Tours integrate with the backend through:
|
||||
|
||||
`useGetGuidedTourMetaQuery()`
|
||||
|
||||
- Fetches relative metadata from the server such as `isFirstSuperAdminUser`
|
||||
|
||||
## E2E Testing
|
||||
|
||||
Tours are tested in the e2e test suite `tests/e2e/tests/admin/guided-tour.spec.ts`
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
// Tour configuration
|
||||
type TourStep<P extends string> = {
|
||||
name: P;
|
||||
content: Content;
|
||||
when?: (completedActions: ExtendedCompletedActions) => boolean;
|
||||
excludeFromStepCount?: boolean; // Exclude from "Step X of Y" counting
|
||||
};
|
||||
|
||||
// State management
|
||||
type State = {
|
||||
tours: Tour;
|
||||
enabled: boolean;
|
||||
completedActions: ExtendedCompletedActions;
|
||||
};
|
||||
|
||||
type Action =
|
||||
| { type: 'next_step'; payload: ValidTourName }
|
||||
| { type: 'skip_tour'; payload: ValidTourName }
|
||||
| { type: 'previous_step'; payload: ValidTourName }
|
||||
| { type: 'go_to_step'; payload: { tourName: ValidTourName; step: number } }
|
||||
| { type: 'set_completed_actions'; payload: ExtendedCompletedActions }
|
||||
| { type: 'skip_all_tours' }
|
||||
| { type: 'reset_all_tours' };
|
||||
|
||||
// Tour metadata
|
||||
type TourMeta = {
|
||||
totalStepCount: number;
|
||||
displayedStepCount: number;
|
||||
};
|
||||
|
||||
// Step components
|
||||
type Step = {
|
||||
Root: React.ForwardRefExoticComponent<PopoverContentProps & { withArrow?: boolean }>;
|
||||
Title: (props: StepProps) => React.ReactNode;
|
||||
Content: (props: StepProps) => React.ReactNode;
|
||||
Actions: (props: ActionsProps & { to?: string } & FlexProps) => React.ReactNode;
|
||||
};
|
||||
|
||||
// Tour object includes both steps and metadata
|
||||
type Tour = Components & { _meta: TourMeta };
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Admin",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Sessions and JWT
|
||||
description: How admin and Content API authentication works with SessionManager, tokens, and configuration.
|
||||
---
|
||||
|
||||
This page summarizes how authentication works in this repository for:
|
||||
|
||||
- Admin (back office)
|
||||
- Content API via users-permissions plugin (UP)
|
||||
|
||||
It is intentionally concise for contributors working in the monorepo.
|
||||
|
||||
## SessionManager
|
||||
|
||||
Core provides a `SessionManager` that issues:
|
||||
|
||||
- Short‑lived access tokens (JWT, used in `Authorization: Bearer <token>`)
|
||||
- Refresh/session tokens (JWT, stored/handled differently per origin)
|
||||
|
||||
Per-origin configuration is defined at bootstrap time with:
|
||||
|
||||
- `jwtSecret`
|
||||
- `accessTokenLifespan` (seconds)
|
||||
- `maxRefreshTokenLifespan`, `idleRefreshTokenLifespan` (seconds)
|
||||
- `maxSessionLifespan`, `idleSessionLifespan` (seconds)
|
||||
|
||||
Each origin stores session rows in `admin::session` (hidden content-type) using the table/collection `strapi_sessions`, containing `userId`, `sessionId`, `deviceId`, `origin`, `expiresAt`, `absoluteExpiresAt`, `status`, `type`.
|
||||
|
||||
### Public API (per origin)
|
||||
|
||||
- `generateRefreshToken(userId, deviceId?, { type?: 'refresh' | 'session' })`
|
||||
- `rotateRefreshToken(refreshToken)`
|
||||
- `generateAccessToken(refreshToken)`
|
||||
- `validateAccessToken(token)`
|
||||
- `invalidateRefreshToken(userId, deviceId?)`
|
||||
- `isSessionActive(sessionId)`
|
||||
|
||||
Locations:
|
||||
|
||||
- Core service: `packages/core/core/src/services/session-manager.ts`
|
||||
- Types: `packages/core/types/src/modules/session-manager.ts`
|
||||
|
||||
## Admin authentication
|
||||
|
||||
Admin defines an origin `admin` with config under `admin.auth.sessions.*`.
|
||||
|
||||
- Login/Register/Reset password create a refresh/session token and set an httpOnly cookie `strapi_admin_refresh`.
|
||||
- The response includes the short‑lived access token in `data.token`.
|
||||
- Clients use `Authorization: Bearer <access token>` for subsequent admin requests.
|
||||
|
||||
Endpoints (admin server):
|
||||
|
||||
- `POST /admin/login`
|
||||
- `POST /admin/register` and `POST /admin/register-admin`
|
||||
- `POST /admin/reset-password`
|
||||
- `POST /admin/access-token` — rotates the refresh cookie and returns `{ data: { token } }`
|
||||
- `POST /admin/logout` — clears cookie and revokes refresh tokens; body may include `{ deviceId }` to revoke a single device
|
||||
|
||||
Optional request fields on login/register:
|
||||
|
||||
- `deviceId` (UUID). When provided, enables device-scoped revocation.
|
||||
- `rememberMe` (boolean). When true, uses the long-lived refresh family and sets a persistent cookie; otherwise uses session family (session cookie).
|
||||
|
||||
Configuration:
|
||||
|
||||
- `admin.auth.secret` — required JWT secret used by admin origin
|
||||
- `admin.auth.sessions.accessTokenLifespan` (default 1800)
|
||||
- `admin.auth.sessions.maxRefreshTokenLifespan` (default 30 days)
|
||||
- `admin.auth.sessions.idleRefreshTokenLifespan` (default 7 days)
|
||||
- `admin.auth.sessions.maxSessionLifespan` (default 7 days)
|
||||
- `admin.auth.sessions.idleSessionLifespan` (default 1 hour)
|
||||
- Cookie options (applied to `strapi_admin_refresh`):
|
||||
- `admin.auth.cookie.domain` (or `admin.auth.domain`)
|
||||
- `admin.auth.cookie.path` (default `/admin`)
|
||||
- `admin.auth.cookie.sameSite` (default `lax`)
|
||||
|
||||
Key files:
|
||||
|
||||
- Bootstrap/config: `packages/core/admin/server/src/bootstrap.ts`
|
||||
- Routes: `packages/core/admin/server/src/routes/authentication.ts`
|
||||
- Controller: `packages/core/admin/server/src/controllers/authentication.ts`
|
||||
- Strategy (Bearer access token validation): `packages/core/admin/server/src/strategies/admin.ts`
|
||||
|
||||
## Content API (users-permissions)
|
||||
|
||||
The users-permissions (UP) plugin supports two modes, controlled by `plugin::users-permissions.jwtManagement`:
|
||||
|
||||
- `legacy-support` (default): issues long‑lived `jwt` configured by `plugin::users-permissions.jwt`.
|
||||
- `refresh`: uses `SessionManager` to issue short‑lived access tokens (`jwt`) and separate `refreshToken`.
|
||||
|
||||
When `jwtManagement` is `refresh`:
|
||||
|
||||
- Login/Register/Provider callback responses include `{ jwt, refreshToken }`.
|
||||
- New endpoints:
|
||||
- `POST /api/auth/refresh` — body `{ refreshToken }`, returns `{ jwt }` and rotates the refresh token
|
||||
- `POST /api/auth/logout` — revokes sessions for the authenticated user; optional `{ deviceId }` in body to revoke only one device
|
||||
|
||||
Configuration keys:
|
||||
|
||||
- `plugin::users-permissions.jwtManagement`: `'legacy-support' | 'refresh'`
|
||||
- `plugin::users-permissions.sessions.accessTokenLifespan`
|
||||
- `plugin::users-permissions.sessions.maxRefreshTokenLifespan`
|
||||
- `plugin::users-permissions.sessions.idleRefreshTokenLifespan`
|
||||
- `plugin::users-permissions.sessions.maxSessionLifespan`
|
||||
- `plugin::users-permissions.sessions.idleSessionLifespan`
|
||||
|
||||
Key files:
|
||||
|
||||
- Plugin bootstrap/config: `packages/plugins/users-permissions/server/bootstrap/index.js`, `packages/plugins/users-permissions/server/config.js`
|
||||
- Controller: `packages/plugins/users-permissions/server/controllers/auth.js`
|
||||
- Routes: `packages/plugins/users-permissions/server/routes/content-api/auth.js`
|
||||
- JWT service: `packages/plugins/users-permissions/server/services/jwt.js`
|
||||
|
||||
## Notes
|
||||
|
||||
- Access tokens are always used in the `Authorization` header as `Bearer <token>`.
|
||||
- Admin refresh tokens are stored in an httpOnly cookie and are not exposed to JavaScript.
|
||||
- Device-bound sessions allow targeted logout by `deviceId`.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- configuration
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
This section is an overview of how configuration files are loaded in Strapi and how they should be used.
|
||||
|
||||
## Config loading
|
||||
|
||||
Loading of configuration is the very first thing Strapi does in bootstrap (after setting signal handlers), which is necessary because most of the Strapi bootstrap process depends on values from the configuration.
|
||||
|
||||
This means that configuration loading happens even before Strapi creates a logger. Therefore any errors or warnings from the configuration unfortunately must output to `console`
|
||||
|
||||
NOTE: For now. in theory, we could define some configuration files that do not depend on being passed a `strapi` object and could provide logger settings in there, but at this time we do not have that capability.
|
||||
|
||||
Currently, Strapi loads every .js and .json file within a project's `./config` directory directly into the strapi config object. That is, if there is a file called `./config/myconfig.js` it will be loaded automatically and accessible from `strapi.config.get('myconfig')`
|
||||
|
||||
(NOT YET IMPLEMENTED) Starting in Strapi v5, will load every defined value from the `STRAPI_` environment namespace into the configuration, using underscore (\_) as the object delimiter. For example, if you define `STRAPI_ADMIN_APP_KEYS=abcd,efg` it will be available from `strapi.config.get('admin.app.keys')` as `['abcd', 'efg']`
|
||||
|
||||
Along with that feature, all configuration values will become case-insensitive. That is, accessing `strapi.config.get('admin.App.Keys')` will return the same value as `strapi.config.get('admin.app.keys')`.
|
||||
|
||||
## Base Strapi Configurations
|
||||
|
||||
The following are considered 'base' Strapi configurations and a description of what each file contains is given, so that new settings can be placed in the appropriate place.
|
||||
|
||||
A Strapi configuration file must export either an object, or a function that returns an object.
|
||||
|
||||
WARNING: There are some discrepancies where certain configuration files such as middlewares may return an array and some files may not be able to export a function. Please confirm before use, and update this documentation when exceptions are found.
|
||||
|
||||
(NOT YET IMPLEMENTED) Starting in Strapi v5, the contents of the base configurations will be strictly defined and no values that are not created by Strapi will be allowed in them; that is, an error will be thrown on unrecognized values. Any user additions must go in separate configuration namespaces.
|
||||
|
||||
### Base Config Names
|
||||
|
||||
#### admin
|
||||
|
||||
Defines settings related to the Strapi admin panel, such as the 'autoopen' option for `yarn develop`
|
||||
|
||||
#### server
|
||||
|
||||
Defines settings related to the Strapi backend server, such as 'host' and 'port'
|
||||
|
||||
#### database
|
||||
|
||||
Defines the database configuration options.
|
||||
|
||||
NOTE: Most of this is defined by Knex, be careful when adding options that they are compatible or placed in the appropriate location
|
||||
|
||||
#### api
|
||||
|
||||
Content API configuration, for example the 'rest.maxLimit' option to set the maximum `limit` available in a query.
|
||||
|
||||
#### features
|
||||
|
||||
Feature flags for enabling and configuring future features that would be breaking changes and cannot be enabled by default, or experimental features that are not ready for public stable release.
|
||||
|
||||
#### plugins
|
||||
|
||||
Contains plugin configurations, with each root-level value the id of a plugin, for example, 'users-permissions'
|
||||
|
||||
IMPORTANT: Plugins are not loaded like other Strapi configurations into the `plugins.` namespace, because they are loaded during the module loading.
|
||||
That means that in an example plugins.js file like below, its values are accessible at, for example, `strapi.config.get('plugin::graphql.endpoint')`
|
||||
|
||||
```
|
||||
module.exports = () => ({
|
||||
graphql: {
|
||||
enabled: true,
|
||||
config: {
|
||||
endpoint: '/graphql',
|
||||
|
||||
defaultLimit: 25,
|
||||
maxLimit: 100,
|
||||
|
||||
apolloServer: {
|
||||
tracing: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
NOTE: Because this configuration contains user and plugin keys and Strapi does not yet provide a method to extend the definitions, this configuration is not strictly typed and will not be loaded from the environment variables
|
||||
|
||||
#### middlewares
|
||||
|
||||
The configuration of middlewares.
|
||||
|
||||
NOTE: This is the only configuration that is currently an array rather than an object. It also very loosely typed. Future improvements are planned to strongly type all the built-in Strapi middlewares.
|
||||
|
||||
### Typings
|
||||
|
||||
Most of the base configurations have been typed, although they are not currently publicly available. They can be found in: `packages/core/types/src/types/core/config`
|
||||
|
||||
Any time a new configuration option is added to Strapi, it must also be added to the types for the appropriate config file (and once the env var loading feature is available, a parser for it such as string, integer, stringArray, etc must be defined)
|
||||
|
||||
In Strapi v5, they will be used for:
|
||||
|
||||
- user-facing config factories to assist in typing project configuration
|
||||
- as part of the environment variable configuration loading to validate the structure of the base config file parsers to parse and strongly type values from the environment
|
||||
|
||||
### Configuration filename restrictions
|
||||
|
||||
In Strapi v5, environment variables with the prefix `STRAPI_` will be loaded automatically into Strapi configuration. Because of that, some naming restrictions have been added to prevent conflicts.
|
||||
|
||||
- filenames without extension must be case-insensitive unique (ie, only one file among ADMIN.js and admin.js or admin.js and admin.json)
|
||||
- All base Strapi configurations and a set of restricted names currently found in `packages/core/core/src/configuration/config-loader.ts` may not be used **even as prefixes**
|
||||
|
||||
IMPORTANT: Any new base Strapi configurations should also be limited to a-z0-9, no special characters! This is necessary to allow environment variable loading. The same restriction does not apply to user config files, but if not followed that file will not support autoloading env.
|
||||
|
||||
### Base attribute name restrictions
|
||||
|
||||
Configuration options defined for Strapi configurations must include only a-zA-Z0-9! If a key is used with a special character, underscore, or dash, it will not be configurable from the environment variables, and people will get angry that it was poorly designed. Please use camelCase like all existing Strapi configuration keys. There are not currently any special characters defined in Strapi configurations, with the sole exception of plugins.js 'users-permissions', please keep it that way.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- content-manager
|
||||
---
|
||||
|
||||
# Content Manager
|
||||
|
||||
## What is the Content Manager?
|
||||
|
||||
The content-manager is a plugin that allows users to write / update & delete their content, it's currently held within the `@strapi/admin` package, but from V5 will be removed back to to its own plugin. At its very basic form, the CM is just a table & a few forms. There are a few public APIs to manipulate these forms & tables as well as some universal hooks exported for user's to additionally interact with within their own plugins outside of the CM plugin & within.
|
||||
|
||||
## Sections
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items.filter(item => item.label !== "Introduction")} />
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Documents
|
||||
description: An introduction to documents in the content-manager
|
||||
tags:
|
||||
- content-manager
|
||||
- documents
|
||||
---
|
||||
|
||||
At the very core of the CM is the concept of a document. Although the logic for creating a document lives primarily within the `@strapi/core` package, we still need to understand what they are to interact with them. Introduced for the _new_ draft & publish feature in V5 (Q1 2024), documents are essentially a matrix of entries, this matrix grows in complexity based on the amount of dimensions your doucment has. For example, having Draft & Publish and Internationalisation (with 2 locales) enabled would give you 4 entries within the document – a draft for each locale and a published entry for each locale.
|
||||
|
||||
:::note
|
||||
Draft & Publish is optional on content-types
|
||||
:::
|
||||
|
||||
## Getting Documents
|
||||
|
||||
We get documents via the [`useDocument`](./hooks/use-document.mdx) hook, which requires us to understand which "model" we want – the model is basically which content-type e.g. `api::article.article`, the "kind" of content-type e.g. `single-type` or `collection-type` and the specific `id` of said document. Using these three parameters we can fetch any document, without passing specific parameters e.g. `locale` we get a default version defined by the user's application. However, all dimensions should be queryable to narrow down the point on the document matrix to a specific entry e.g. `published & 'en-GB'`.
|
||||
|
||||
## Interacting with Documents
|
||||
|
||||
There are universal actions that can be performed on documents, these are:
|
||||
|
||||
- `create` – creates a new document
|
||||
- `update` – updates an existing document
|
||||
- `delete` – deletes an existing document
|
||||
|
||||
Documents that are `collection-types` additionally can be cloned. If a document has draft & published enabled then we can additionally publish & unpublish the document. All of this functionality is exposed via the [`useDocumentActions`](./hooks/use-document-actions.mdx) hook. Additional actions can be added by plugins via APIs.
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Layouts
|
||||
description: How layouts work with the List & Edit view
|
||||
tags:
|
||||
- content-manager
|
||||
- layouts
|
||||
- edit-view
|
||||
- list-view
|
||||
---
|
||||
|
||||
Layouts are fundamental to how the CM renders its list & edit views. The edit view's layout can be manipulated via plugins using the `'Admin/CM/pages/EditView/mutate-edit-view-layout'` hook. Meanwhile the list view can currently only have table columns injected.
|
||||
|
||||
## What is a layout?
|
||||
|
||||
A layout is simply a data-structure we iterate over to understand how to render the views. For both List & Edit view their structures have explicitly been designed to be similar:
|
||||
|
||||
```ts
|
||||
interface ListLayout {
|
||||
layout: ListFieldLayout[];
|
||||
components?: never;
|
||||
metadatas: object;
|
||||
settings: object;
|
||||
}
|
||||
|
||||
interface EditLayout {
|
||||
layout: Array<Array<EditFieldLayout[]>>;
|
||||
components: Record<string, Omit<EditLayout, 'metadatas' | 'components'>>;
|
||||
metadatas: object;
|
||||
settings: object;
|
||||
}
|
||||
```
|
||||
|
||||
The above types have been simplified for the purpose of explanation, but you can see from the `components` property of the `EditLayout` that it's essentially the same as its parent, just a nested and organised per component. This consistency allows us to easily iterate over the layouts and render the views using the same components for the general layout & component layouts.
|
||||
|
||||
## How are they made?
|
||||
|
||||
The layout is a combination of a content-types's schema & its configuration file, while their data-structures are similar they differ at the very root interface.
|
||||
|
||||
### EditView
|
||||
|
||||
The very base level interface for the EditView layout is an `EditFieldLayout` which is primarily derived from the `InputProps` of our Form Components:
|
||||
|
||||
```ts
|
||||
interface InputProps {
|
||||
disabled?: boolean;
|
||||
hint?: string;
|
||||
label: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
type: Exclude<
|
||||
Attribute.Kind,
|
||||
'media' | 'blocks' | 'richtext' | 'uid' | 'dynamiczone' | 'component' | 'relation'
|
||||
>;
|
||||
}
|
||||
|
||||
interface EditFieldSharedProps extends Omit<InputProps, 'type'> {
|
||||
mainField?: string;
|
||||
size: number;
|
||||
unique?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over all the types in Attribute Types and use that to create a union of new types where the attribute type
|
||||
* is under the property attribute and the type is under the property type.
|
||||
*/
|
||||
type EditFieldLayout = {
|
||||
[K in Attribute.Kind]: EditFieldSharedProps & {
|
||||
attribute: Extract<Attribute.Any, { type: K }>;
|
||||
type: K;
|
||||
};
|
||||
}[Attribute.Kind];
|
||||
```
|
||||
|
||||
The excluded `type` values are unique to the content-manager and as such aren't expected to be rendered as universal form inputs. This data-structure is passed to the `InputRenderer` component to render any form input (previously known as `GenericInputs` from the helper-plugin). Notice how these inputs don't recieve their `value`, `onChange` or `error` prop. These are extracted from the `Form` using `useField(name)`:
|
||||
|
||||
```ts
|
||||
export const StringInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ disabled, label, hint, name, placeholder, required }, ref) => {
|
||||
const field = useField(name);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
hint={hint}
|
||||
label={label}
|
||||
name={name}
|
||||
defaultValue={field.initialValue}
|
||||
onChange={field.onChange}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
value={field.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
To handle the CM specific inputs, the EditView domain has its own `InputRenderer` component which follows the same principle but this will additionally handle attributes like custom-fields etc. The EditView them "simply" renders over its layout, passing props to the `InputRenderer`, if there's a dynamic-zone or component we recursively render the layout by utilising the `components` property of the `EditLayout` data-structure, as previously said, because this follows the same structure as its parent, we can iterate over it the exact same way rendering _any fields_ required, incl. deeply nested components and dynamic-zones.
|
||||
|
||||
### ListView
|
||||
|
||||
Because the list view is essentailly a giant table, its data-structure is considerably simpler than the edit view:
|
||||
|
||||
```ts
|
||||
interface ListFieldLayout {
|
||||
/**
|
||||
* The attribute data from the content-type's schema for the field
|
||||
*/
|
||||
attribute: Attribute.Any | { type: 'custom' }; // custom attributes are expected to use `cellFormatter`.
|
||||
/**
|
||||
* Typically used by plugins to render a custom cell
|
||||
*/
|
||||
cellFormatter?: (
|
||||
data: { [key: string]: unknown },
|
||||
header: Omit<ListFieldLayout, 'cellFormatter'>
|
||||
) => React.ReactNode;
|
||||
label: string | MessageDescriptor;
|
||||
/**
|
||||
* the name of the attribute we use to display the actual name e.g. relations
|
||||
* are just ids, so we use the mainField to display something meaninginful by
|
||||
* looking at the target's schema
|
||||
*/
|
||||
mainField?: string;
|
||||
name: string;
|
||||
searchable?: boolean;
|
||||
sortable?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
The `cellFormatter` property is paramount to plugin developers being able to inject their own columns, because we, most likely, will not have the components necessary to render the data the plugin provides. This is why they receive the data for the entire row & the header that they injected via the `'Admin/CM/pages/ListView/inject-column-in-table'` hook.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: RBAC
|
||||
description: How RBAC work with documents in the content-manager
|
||||
tags:
|
||||
- content-manager
|
||||
- RBAC
|
||||
- edit-view
|
||||
- documents
|
||||
---
|
||||
|
||||
:::note
|
||||
This is not a detailed breakdown of permissions within strapi, if you're looking for this you should look at [Permissions Intro](../admin/02-permissions/00-intro.mdx).
|
||||
:::
|
||||
|
||||
Each document's permission's object will contain `properties.fields` which is an array of strings, we can use these to understand which properties can be created / updated etc. Via the subject of the permission.
|
||||
|
||||
```json
|
||||
// An example permission object
|
||||
{
|
||||
"id": 666,
|
||||
"action": "plugin::content-manager.explorer.create",
|
||||
"actionParameters": {},
|
||||
"subject": "api::article.article",
|
||||
"properties": {
|
||||
"fields": ["short_text", "blocks", "single_compo.name", "single_compo.test", "dynamiczone"]
|
||||
},
|
||||
"conditions": []
|
||||
}
|
||||
```
|
||||
|
||||
The above permissions relate to what fields the user can create on the article content-type. The list of fields are their names in the schema, not their labels (which can be overridden in the EditViewSettings), components are dot separated paths where the component name will be the first part of said path, repeatable compoenents **will not** have indexes in the path and finally, in dynamic zones all fields are always allowed.
|
||||
|
||||
## DocumentRBAC Component
|
||||
|
||||
The `DocumentRBAC` component wraps the ListView & EditView pages providing
|
||||
|
||||
- if a user can `create/read/update/delete/publish` a document at all
|
||||
- the list of fields for each action
|
||||
- a utility function to check if a user `canAction`
|
||||
|
||||
```ts
|
||||
interface DocumentRBACContextValue {
|
||||
canCreate?: boolean;
|
||||
canCreateFields: string[];
|
||||
canDelete?: boolean;
|
||||
canPublish?: boolean;
|
||||
canRead?: boolean;
|
||||
canReadFields: string[];
|
||||
canUpdate?: boolean;
|
||||
canUpdateFields: string[];
|
||||
canUserAction: (
|
||||
fieldName: string,
|
||||
fieldsUserCanAction: string[],
|
||||
fieldType: Attribute.Kind
|
||||
) => boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
Because the `useRBAC` hook fetches data from the API to check against `conditions` of a permission, we optionally have the `isLoading` returned incase a component needs to await this.
|
||||
:::
|
||||
|
||||
Using all this information, we can disabled & hide fields in the application based on the user's permissions.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Content Releases
|
||||
description: Guide for content releases in the content-manager.
|
||||
tags:
|
||||
- content-manager
|
||||
- content-releases
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Content releases is an enterprise feature so you need a valid license.
|
||||
Additionally, your user role must be granted specific Permissions, such as `plugin::content-releases.read` and `plugin::content-releases.create-action`. You can modify these permissions in the Settings section. The capability to add entries to a release within the content manager is available through the content-manager edit view.
|
||||
|
||||
### Edit view
|
||||
|
||||
If the feature is enabled with the correct license, and the user has the necessary permissions enabled, a 'Releases' section will appear on the right side of the Edit View. The 'Releases' section has an 'Add to release' button. Clicking this button opens a modal allowing you to assign the entry to a specific release and specify the desired action (Publish/Unpublish).
|
||||
|
||||
In the event that no releases are present, users need to create a release first, which can be done on the [Content Releases](../content-releases/00-intro.md) page.
|
||||
|
||||
If the user's permissions also includes the `plugin::content-releases.delete-action`, the user can access additional options by clicking the 'More' button (represented by three dots) in the releases sidebar. This will reveal the 'Remove from release' button, providing the ability to detach the entry from the selected release.
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: Relations
|
||||
description: Conceptual guide to relations in the Content Manager focussing on the technical decisions taken.
|
||||
tags:
|
||||
- content-manager
|
||||
- relations
|
||||
- redux-store
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Relations are a term used to describe how two or more entities are connected. Previously in the sidebar of an entity,
|
||||
in Nov2020 we released a refactor that moved these fields into the main editing flow for a better editor experience
|
||||
and to improve performance of the CMS application when many relations were used.
|
||||
|
||||
<img
|
||||
src="/img/content-manager/relations/component-example.png"
|
||||
alt="An example of the relations input in the CMS edit view"
|
||||
/>
|
||||
|
||||
_above: An example of the relations input in the CMS edit view_
|
||||
|
||||
## Data management in frontend
|
||||
|
||||
<img
|
||||
src="/img/content-manager/relations/relations-statemanagemen-diagram.png"
|
||||
alt="a diagram overview explaining how state management works in relations"
|
||||
/>
|
||||
|
||||
_above: A high-level diagram of how relations state management works_
|
||||
|
||||
### Preparing relation fields in the store
|
||||
|
||||
When you first open an existing entity, we call the admin API and put the data into the store to pre-populate fields
|
||||
with existing values. However, its important to know when you have fields with `type === 'relation'` in your schema
|
||||
that the data you receive will not be an array, but rather an object with the count of how many relations in that
|
||||
field exist. For example, a section of the response may look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_relations": {
|
||||
"count": 6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So without intervention, your inputs would try to append new relations to the `my_relations` object, which would not
|
||||
work. Instead of this, before calling the redux action `INIT_FORM` we recursively find the paths fields based on the
|
||||
following conditions:
|
||||
|
||||
- The field is a relation
|
||||
- The field is a component
|
||||
- The field is a repeatable component
|
||||
- The field is a dynamic zone
|
||||
|
||||
These paths _do not_ take into account index values. So if you have a repetable component field where the schema looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"repeatable_single_component_relation": {
|
||||
"type": "component",
|
||||
"repeatable": true,
|
||||
"component": "basic.relation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and the components looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"basic.relation": {
|
||||
"attributes": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"categories": {
|
||||
"type": "relation",
|
||||
"relation": "oneToMany",
|
||||
"target": "api::category.category",
|
||||
"targetModel": "api::category.category",
|
||||
"relationType": "oneToMany"
|
||||
},
|
||||
"my_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then the path to the relation field would be `repeatable_single_component_relation.categories`. Even though when
|
||||
relations are added the path to the field in the redux store would be `repeatable_single_component_relation.0.categories`.
|
||||
|
||||
Inside the reducer we reduce the array of `relationalFieldPaths` to an object with the `initialValues` clone as
|
||||
as the base. If there is `modifiedData` in the browser i.e. you've made changes to the entity and saved those changes,
|
||||
we just replace the first level of the field with the `modifiedData` so the data structure is preserved and we're not
|
||||
loosing the relations we had already loaded in the component. If the first part of the path is highlighted as the
|
||||
`relationalField` then we simply replace that intial object with an empty array.
|
||||
|
||||
However, if the first part of the path is either a repeatable component, a dynamic zone or a regular component then we
|
||||
recursively find the relation fields and replace the object with an array. This is handled by the `findLeafByPathAndReplace`
|
||||
utility function. This function in short, takes an end path (in this case the relational field) and a primitive to replace
|
||||
when it finds the endpath (an empty array in this case). It then recursively reduces the paths to the relational field mapping
|
||||
through arrays if necessary (in the instance of repetable components for example) replacing the endpath with the primitive.
|
||||
|
||||
When this is done, we have sucessfully prepared our initial data for usage with relations.
|
||||
|
||||
### Handling updates to relation fields
|
||||
|
||||
Because we've prepared the fields prior to the component loading, adding & removing relations, it's relatively easy to do so.
|
||||
When a relation is added, we simply push the new relation to the array of relations. When a relation is removed, we simply
|
||||
filter out the relation from the array of relations. This is handled inside the reducer actions `CONNECT_RELATION` &
|
||||
`DISCONNECT_RELATION` respectively.
|
||||
|
||||
:::note
|
||||
Connecting relations adds the item to the end of the list, whilst loading more relations prepends to
|
||||
the beginning of the list. This is the expected behaviour, to keep the order of the list in the UI in sync with the API response.
|
||||
:::
|
||||
|
||||
The `RelationInput` component takes the field in `modifiedData` as its source of truth. You could therefore consider this to
|
||||
be the `browserState` and `initialData` to be the `serverState`. When relations are loaded they're added to both the `intialData`
|
||||
and `modifiedData` objects, but when you connect/disconnect only the `modifiedData` is updated. This is useful when we're preparing
|
||||
data for the api.
|
||||
|
||||
### Cleaning data to be posted to the API
|
||||
|
||||
The API to update the entity expects relations to be categorised into two groups, a `connect` array and `disconnect` array.
|
||||
You could do this as the user interacts with the input but we found this to be confusing and then involved us managing three
|
||||
different arrays which makes the code more complex. Instead, because the browser doesn't really care about whats new and removed
|
||||
and we have a copy of the slice of data we're mutating from the server we can run a small diff algorithm to determine which
|
||||
relations have been connected and which have been disconnected. Returning an object like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_relations": {
|
||||
"connect": [{ "id": 1 }, { "id": 2 }],
|
||||
"disconnect": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend component architecture
|
||||
|
||||
The input field for relation fields consist of two components:
|
||||
|
||||
### `RelationInputDataManager`
|
||||
|
||||
This container component handles data fetching and data normalization for the `RelationInput` component. This has been extracted from
|
||||
the `RelationInput` so that Strapi is able to move the underlying component into the design-system if the community would need it
|
||||
(most other input components can be consumed from there).
|
||||
|
||||
### `RelationInput`
|
||||
|
||||
This component is the presentational counterpart to the `RelationInputDataManager` component. It renders an input field based on the data passed from the data manager.
|
||||
|
||||
Under the hood it is using `react-window` to render a list of relations in a virtualized view. Some fields need to render thousands of relations, which
|
||||
would otherwise have a negative impact on the overall performance of the content-manager.
|
||||
|
||||
## useRelation() hook
|
||||
|
||||
This hook takes care of data-fetching and normalizes results relations aswell as search-results.
|
||||
|
||||
```ts
|
||||
const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: Array<string | object>, options: Options);
|
||||
```
|
||||
|
||||
### `Options`
|
||||
|
||||
`option`s is a mandatory configuration and should implement the following shape:
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
name: string; // name of the relation field
|
||||
relation: RelationConfiguration;
|
||||
search: SearchConfiguration;
|
||||
}
|
||||
|
||||
type RelationConfiguration = {
|
||||
endpoint: string; // URL from where existing relations should be fetched
|
||||
enabled: boolean; // defines whether relations should be fetched once the hook is called
|
||||
pageParams: object; // additional query params which will be appended to `endpoint`
|
||||
onLoad: (results: RelationResult[]) => void; // callback that will be fired after relations have been fetched (paginated)
|
||||
normalizeArguments = {
|
||||
mainFieldName: string; // name of the target model main field, determining which field to display (fallback: id)
|
||||
shouldAddLink: boolean; // if the user is allowed to read the target model, the returned relations should include a link to the target
|
||||
targetModel: object; // target content-type model
|
||||
};
|
||||
pageGoal: number; // the current page-count of the already loaded relations used to keep the redux store and query cache in sync.
|
||||
}
|
||||
|
||||
type SearchConfiguration = {
|
||||
endpoint: string; // URL from where new relations should be fetched
|
||||
pageParams: object; // additional query params which will be appended to `endpoint`
|
||||
}
|
||||
```
|
||||
|
||||
### Return values
|
||||
|
||||
`relations` and `search` both return a consistent relation format:
|
||||
|
||||
```ts
|
||||
type RelationResults = RelationResult[];
|
||||
|
||||
type RelationResult = {
|
||||
id: number;
|
||||
href?: string; // based on `shouldAddLink` and the `targetModel`
|
||||
publicationState: 'draft' | 'published';
|
||||
mainField: string; // will fallback to "id" if not set
|
||||
};
|
||||
```
|
||||
|
||||
#### `relations`
|
||||
|
||||
`relations` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated relational data
|
||||
aswell as methods to check if there are more pages or fetch more paginated results. Relations for a given field are fetched as soon as the hook is called.
|
||||
|
||||
#### `search`
|
||||
|
||||
`search` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated search results
|
||||
for a relational field. Search results are only fetched after `searchFor()` has been called.
|
||||
|
||||
#### `searchFor(string)`
|
||||
|
||||
`searchFor` is a method which can be used to search for entities which haven't been connected with the source entity yet. The method accepts a search-term: `searchFor("term")`.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Content Manager",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Features",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Review Workflows
|
||||
description: Guide for review workflows in the content-manager.
|
||||
tags:
|
||||
- content-manager
|
||||
- review-workflows
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Review workflows are disabled for all content-types by default and have to be enabled for each of them individually. More about how to
|
||||
[enable review-workflows for a content-type](/docs/settings/review-workflows#edit--create-view). The feature itself is visible in two places
|
||||
of the content-manager: list and edit view.
|
||||
|
||||
### Nullish stages
|
||||
|
||||
Entities which are not created through the admin API, content API or entity service will not have a stage assigned by default (e.g. lifecycle methods). This
|
||||
means at any place in the admin app displaying a stage, it has to be prepared to receive `null` and should not crash.
|
||||
|
||||
### List view
|
||||
|
||||
If the feature is enabled for a content-type a new column will show up, displaying the current stage of an entity. If no stage was assigned to an entity,
|
||||
the column is displayed as empty.
|
||||
|
||||
The information which stage is currently assigned to an entity is send as part of the content-type response payload for each entity in the attribute `strapi_stage`.
|
||||
Please refer to the [type definitions](/settings/review-workflows) for more information.
|
||||
|
||||
```ts
|
||||
{
|
||||
// ... entity attributes
|
||||
strapi_stage?: Stage | null
|
||||
}
|
||||
```
|
||||
|
||||
`http://localhost:1337/content-manager/content-types` returns whether the feature is enabled for the content-type. `options.reviewWorkflows` is either `true`, `false` or `undefined`.
|
||||
|
||||
**Note**: Downgrading from EE to CE won't delete the associated review workflow data and `http://localhost:1337/content-manager/content-types` still returns true. The admin app had to
|
||||
add an additional check if the feature toggle returned in `http://localhost:1337/admin/project-type` is enabled.
|
||||
|
||||
### Edit view
|
||||
|
||||
If the feature is enabled on the current content-type, the selected stage will show up in the information sidebar next to the edit view. Users
|
||||
can select any other stage of the current workflow provided they have the necessary permissions to change the current stage see [EE Review Workflows](../admin/01-ee/01-review-workflows.md).
|
||||
|
||||
Stage assignments are decoupled from entities, meaning updating entity attributes won't set the selected stage at the same time. Instead the stage select
|
||||
component will trigger an atomic update on the admin API to assign/ update a stage to the current entity.
|
||||
|
||||
Because of this decoupling, stages **can not be assigned on entity creation** and only after they have been created.
|
||||
|
||||
Information about which stage is currently assigned to an entity is sent as part of the entity response payload in the attribute `strapi_stage`.
|
||||
Please refer to the [type definitions](/settings/review-workflows) for more information.
|
||||
|
||||
```ts
|
||||
{
|
||||
// ... entity attributes
|
||||
strapi_stage?: Stage | null
|
||||
}
|
||||
```
|
||||
|
||||
#### License limits
|
||||
|
||||
If the user has exceeded the license limit (based on their license) for either the number of workflows or the number of stages of the workflow assigned
|
||||
to the entities content-type, any stage update action will trigger a license-limit overlay and abort the request.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Hooks",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: useContentTypes
|
||||
description: API reference for the useContentTypes hook in Strapi's Content Manager
|
||||
tags:
|
||||
- content-manager
|
||||
- hooks
|
||||
- fetch
|
||||
- content-types
|
||||
- components
|
||||
---
|
||||
|
||||
An abstraction around `react-query` to fetch content-types and components. It returns the raw API response
|
||||
for components. `collectionTypes` and `singleTypes` are filtered by `isDisplayed=true`.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import { useContentTypes } from 'path/to/hooks';
|
||||
|
||||
const MyComponent = () => {
|
||||
const { isLoading, collectionTypes, singleTypes, components } = useContentTypes();
|
||||
|
||||
return (/* ... */);
|
||||
};
|
||||
```
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: useDocumentActions
|
||||
description: API reference for the useDocumentActions hook from Strapi's Content Manager
|
||||
tags:
|
||||
- content-manager
|
||||
- hooks
|
||||
- public API
|
||||
---
|
||||
|
||||
:::warning
|
||||
This hook is considered unstable and may change in a future release.
|
||||
:::
|
||||
|
||||
Contains all the operations that can be performed on a single document. Designed to be able to be
|
||||
used anywhere within a Strapi app. The hooks will handle notifications should the operation fail,
|
||||
however the response is always returned incase the user needs to handle side-effects.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { Form } from '@strapi/admin/admin';
|
||||
|
||||
const { id, model, collectionType } = useParams<{
|
||||
id: string;
|
||||
model: string;
|
||||
collectionType: string;
|
||||
}>();
|
||||
const { update } = useDocumentActions();
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await update({ collectionType, model, documentId: id }, data);
|
||||
};
|
||||
|
||||
return <Form method="PUT" onSubmit={handleSubmit} />;
|
||||
```
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
type OperationResponse<TResponse extends { data: any; meta: any; error?: any }> =
|
||||
| Pick<TResponse, 'data' | 'meta'>
|
||||
| { error: BaseQueryError | SerializedError };
|
||||
|
||||
type UseDocumentActions = () => {
|
||||
clone: (
|
||||
args: {
|
||||
model: string;
|
||||
documentId: string;
|
||||
params: object;
|
||||
},
|
||||
document: Omit<Document, 'documentId'>,
|
||||
trackerProperty?: Extract<
|
||||
TrackingEvent,
|
||||
{ name: 'willCreateEntry' | 'didCreateEntry' | 'didNotCreateEntry' }
|
||||
>['properties']
|
||||
) => Promise<OperationResponse<Contracts.CollectionTypes.Clone.Response>>;
|
||||
create: (
|
||||
args: {
|
||||
model: string;
|
||||
params?: object;
|
||||
},
|
||||
document: Omit<Document, 'documentId'>,
|
||||
trackerProperty?: Extract<
|
||||
TrackingEvent,
|
||||
{ name: 'willCreateEntry' | 'didCreateEntry' | 'didNotCreateEntry' }
|
||||
>['properties']
|
||||
) => Promise<OperationResponse<Contracts.CollectionTypes.Create.Response>>;
|
||||
delete: (
|
||||
args: {
|
||||
collectionType: string;
|
||||
model: string;
|
||||
documentId: string;
|
||||
},
|
||||
trackerProperty?: Extract<
|
||||
TrackingEvent,
|
||||
{ name: 'willDeleteEntry' | 'didDeleteEntry' | 'didNotDeleteEntry' }
|
||||
>['properties']
|
||||
) => Promise<OperationResponse<Contracts.CollectionTypes.Delete.Response>>;
|
||||
getDocument: (args: {
|
||||
collectionType: string;
|
||||
model: string;
|
||||
documentId?: string;
|
||||
params?: object;
|
||||
}) => Promise<Contracts.CollectionTypes.FindOne.Response | undefined>;
|
||||
publish: (args: {
|
||||
collectionType: string;
|
||||
model: string;
|
||||
documentId: string;
|
||||
}) => Promise<OperationResponse<Contracts.CollectionTypes.Publish.Response>>;
|
||||
update: (
|
||||
args: {
|
||||
collectionType: string;
|
||||
model: string;
|
||||
documentId: string;
|
||||
params: object;
|
||||
},
|
||||
document: Document,
|
||||
trackerProperty?: Extract<
|
||||
TrackingEvent,
|
||||
{ name: 'willEditEntry' | 'didEditEntry' | 'didNotEditEntry' }
|
||||
>['properties']
|
||||
) => Promise<OperationResponse<Contracts.CollectionTypes.Update.Response>>;
|
||||
unpublish: (args: {
|
||||
collectionType: string;
|
||||
model: string;
|
||||
documentId: string;
|
||||
}) => Promise<OperationResponse<Contracts.CollectionTypes.Unpublish.Response>>;
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: useDocument
|
||||
description: API reference for the useDocument hook from Strapi's Content Manager
|
||||
tags:
|
||||
- content-manager
|
||||
- hooks
|
||||
- public API
|
||||
---
|
||||
|
||||
:::warning
|
||||
This hook is considered unstable and may change in a future release.
|
||||
:::
|
||||
|
||||
Returns a document based on the model, collection type & id passed as arguments. Also extracts its schema from the redux cache to be used for creating a validation schema.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const { id, model, collectionType } = useParams<{
|
||||
id: string;
|
||||
model: string;
|
||||
collectionType: string;
|
||||
}>();
|
||||
|
||||
if (!model || !collectionType) return null;
|
||||
|
||||
const { document, isLoading, validate } = useDocument({
|
||||
documentId: id,
|
||||
model,
|
||||
collectionType,
|
||||
params: { locale: 'en-GB' },
|
||||
});
|
||||
const { update } = useDocumentActions();
|
||||
|
||||
const onSubmit = async (entity: Entity) => {
|
||||
const errors = validate(entity);
|
||||
|
||||
if (errors) {
|
||||
// handle errors
|
||||
}
|
||||
|
||||
await update({ collectionType, model, documentId: id }, entity);
|
||||
};
|
||||
```
|
||||
|
||||
### Content Manager
|
||||
|
||||
If you're using this in the Strapi Content-Manager package, it is preferable to import `useDoc` instead.
|
||||
This is because whilst it's a light wrapper around `useDocument` it automatically extracts the params from
|
||||
react-router hooks to pre-provide the information for you e.g. `model` and `collectionType`. Saving us from
|
||||
getting the params each time and validating that they are not `undefined`.
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
import type { Attribute } from '@strapi/strapi';
|
||||
|
||||
interface Document {
|
||||
documentId: string;
|
||||
[key: string]: Attribute.GetValue<Attribute.Any>;
|
||||
}
|
||||
|
||||
interface UseDocumentArgs {
|
||||
collectionType: string;
|
||||
model: string;
|
||||
documentId?: string;
|
||||
params?: object;
|
||||
}
|
||||
|
||||
type UseDocument = (args: UseDocumentArgs) => {
|
||||
document?: Document;
|
||||
isLoading: boolean;
|
||||
validate: (entity: Entity) => null | Record<string, TranslationMessage>;
|
||||
};
|
||||
```
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
---
|
||||
title: useDragAndDrop
|
||||
description: API reference for the useDragAndDrop hook in Strapi's Content Manager
|
||||
tags:
|
||||
- content-manager
|
||||
- hooks
|
||||
- drag-and-drop
|
||||
---
|
||||
|
||||
An abstraction around `react-dnd`'s `useDrag` and `useDrop` hooks. It provides a simple API to handle drag and drop
|
||||
events maintaining the same behaviour across the application e.g.
|
||||
|
||||
1. When we consider the item to be above a new drop zone and move it immediately.
|
||||
2. Item is above the drop zone, we move it when user drops it.
|
||||
|
||||
This hook also wraps an internal hook `useKeyboardDragAndDrop` which implements keyboard accessibile drag and drop by
|
||||
returning an onKeyDown handler to be passed to the component's drag icon button.
|
||||
|
||||
## Usage
|
||||
|
||||
:::note
|
||||
The following examples assume that you have already set up the `DndProvider` with `HTML5Backend` in your application and
|
||||
that you are somewhat familiar with `@strapi/design-system` components.
|
||||
:::
|
||||
|
||||
### Basic usage - Move items on hovering over drop zone
|
||||
|
||||
Below is a basic example usage where we move items immediately and we're not interested in rendering custom previews in the DragLayer.
|
||||
However, we do replace the current item with a placeholder.
|
||||
|
||||
```jsx
|
||||
import { Box, Flex, IconButton } from '@strapi/design-system';
|
||||
import { Drag } from '@strapi/icons';
|
||||
|
||||
import { useDragAndDrop } from 'path/to/hooks';
|
||||
import { composeRefs } from 'path/to/utils';
|
||||
|
||||
import { Placeholder } from './Placeholder';
|
||||
|
||||
const MyComponent = ({ onMoveItem }) => {
|
||||
const [{ handlerId, isDragging, handleKeyDown }, myRef, dropRef, dragRef] = useDragAndDrop(true, {
|
||||
type: 'my-type',
|
||||
index,
|
||||
onMoveItem,
|
||||
});
|
||||
|
||||
const composedRefs = composeRefs(myRef, dragRef);
|
||||
|
||||
return (
|
||||
<Box ref={dropRef} cursor={'all-scroll'}>
|
||||
{isDragging ? (
|
||||
<Placeholder />
|
||||
) : (
|
||||
<Flex ref={composedRefs} data-handler-id={handlerId}>
|
||||
<IconButton
|
||||
tag="div"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Drag"
|
||||
borderWidth={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
{'My item'}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Basic usage - Move items on dropping over drop zone
|
||||
|
||||
Below is a basic example usage where we move items immediately without custom previews in the DragLayer. We do not replace
|
||||
the current item until its dropped and show a drop placeholder instead next to drop target using drag direction.
|
||||
|
||||
```jsx
|
||||
import { Box, Flex, IconButton } from '@strapi/design-system';
|
||||
import { Drag } from '@strapi/icons';
|
||||
|
||||
import { useDragAndDrop } from 'path/to/hooks';
|
||||
import { composeRefs } from 'path/to/utils';
|
||||
|
||||
import { Placeholder } from './Placeholder';
|
||||
|
||||
const MyComponent = ({ onDropItem }) => {
|
||||
const [{ handlerId, handleKeyDown, isOverDropTarget, direction }, myRef, dropRef, dragRef] =
|
||||
useDragAndDrop(true, {
|
||||
type: 'my-type',
|
||||
index,
|
||||
onDropItem,
|
||||
});
|
||||
|
||||
const composedRefs = composeRefs(myRef, dragRef);
|
||||
|
||||
return (
|
||||
<Box ref={dropRef} cursor={'all-scroll'}>
|
||||
{isOverDropTarget && <Placeholder direction={direction} />}
|
||||
<Flex ref={composedRefs} data-handler-id={handlerId}>
|
||||
<IconButton
|
||||
tag="div"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Drag"
|
||||
borderWidth={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
{'My item'}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Using custom previews
|
||||
|
||||
The only really difference between the previous example and this one is that
|
||||
we're using the `getEmptyImage` function from `react-dnd-html5-backend` to render custom previews in the DragLayer.
|
||||
|
||||
```jsx
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { Box, Flex, IconButton } from '@strapi/design-system';
|
||||
import { Drag } from '@strapi/icons';
|
||||
|
||||
import { useDragAndDrop } from 'path/to/hooks';
|
||||
import { composeRefs } from 'path/to/utils';
|
||||
|
||||
import { Placeholder } from './Placeholder';
|
||||
|
||||
const MyComponent = ({ onMoveItem }) => {
|
||||
const [{ handlerId, isDragging, handleKeyDown }, myRef, dropRef, dragRef, dragPreviewRef] =
|
||||
useDragAndDrop(true, {
|
||||
type: 'my-type',
|
||||
index,
|
||||
onMoveItem,
|
||||
});
|
||||
|
||||
// highlight-start
|
||||
useEffect(() => {
|
||||
dragPreviewRef(getEmptyImage());
|
||||
}, [dragPreviewRef]);
|
||||
// highlight-end
|
||||
|
||||
const composedRefs = composeRefs(myRef, dragRef);
|
||||
|
||||
return (
|
||||
<Box ref={dropRef} cursor={'all-scroll'}>
|
||||
{isDragging ? (
|
||||
<Placeholder />
|
||||
) : (
|
||||
<Flex ref={composedRefs} data-handler-id={handlerId}>
|
||||
<IconButton
|
||||
tag="div"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Drag"
|
||||
borderWidth={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
{'My item'}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
import { Identifier } from 'dnd-core';
|
||||
import { ConnectDropTarget, ConnectDragSource, ConnectDragPreview } from 'react-dnd';
|
||||
|
||||
interface UseDragAndDropOptions {
|
||||
index: number | Array<number>;
|
||||
onMoveItem?: (newIndex: number | Array<number>, currentIndex: number | Array<number>) => void;
|
||||
/**
|
||||
* @default "regular"
|
||||
* Defines whether the change in index should be immediately over another
|
||||
* dropzone or half way over it (regular).
|
||||
*/
|
||||
dropSensitivity?: 'immediate' | 'regular';
|
||||
item?: object;
|
||||
/**
|
||||
* @default 'STRAPI_DND'
|
||||
*/
|
||||
type?: string;
|
||||
onCancel?: (index: number) => void;
|
||||
onDropItem?: (currentIndex: number | Array<number>, newIndex: number | Array<number>) => void;
|
||||
onEnd?: () => void;
|
||||
onGrabItem?: (index: number) => void;
|
||||
onStart?: () => void;
|
||||
}
|
||||
|
||||
type UseDragAndDropReturn = [
|
||||
props: {
|
||||
handlerId: Identifier;
|
||||
isDragging: boolean;
|
||||
handleKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
|
||||
isOverDropTarget: boolean;
|
||||
direction: string;
|
||||
},
|
||||
objectRef: React.RefObject<HTMLElement>,
|
||||
dropRef: ConnectDropTarget,
|
||||
dragRef: ConnectDragSource,
|
||||
dragPreviewRef: ConnectDragPreview,
|
||||
];
|
||||
|
||||
type UseDragAndDrop = (active: boolean, options: UseDragAndDropOptions) => UseDragAndDropReturn;
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
Its advised to implement a [live text region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) in the
|
||||
parent component holding your individual dnd children. This should be done to inform the user of the current state of the drag and drop.
|
||||
To implement this, you need to pass the `onDropItem`, `onGrabItem` and `onCancel` callbacks to the `useDragAndDrop` hook which are fired
|
||||
only with the purpose of updating the live region, hence why they're optional. You would also update the live region as part of your
|
||||
`onMoveItem` callback. There are generic messages that can be used in the `intl` provider, an example of using this may look like:
|
||||
|
||||
```js
|
||||
setLiveText(
|
||||
formatMessage(
|
||||
{
|
||||
id: getTrad('dnd.drop-item'),
|
||||
defaultMessage: `{item}, dropped. Final position in list: {position}.`,
|
||||
},
|
||||
{
|
||||
item: 'my item',
|
||||
position: 1,
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [react-dnd docs](https://react-dnd.github.io/react-dnd/docs/overview)
|
||||
- [useDrag API](https://react-dnd.github.io/react-dnd/docs/api/use-drag)
|
||||
- [useDrop API](https://react-dnd.github.io/react-dnd/docs/api/use-drop)
|
||||
- [useDragLayer API](https://react-dnd.github.io/react-dnd/docs/api/use-drag-layer)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Firefox quirks
|
||||
|
||||
You might notice in the [basic usage](#basic-usage) section this piece of code:
|
||||
|
||||
```jsx
|
||||
<IconButton
|
||||
tag="div"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Drag"
|
||||
borderWidth={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
In `firefox` the drag handler will not work if you click and drag when the element is a `button`, this is known [bug in the browser](https://bugzilla.mozilla.org/show_bug.cgi?id=568313).
|
||||
Therefore the workaround is to use the `forwardedAs` prop to render a `div` instead of a `button`
|
||||
and add the `role` and `tabIndex` props to make this accessible. The actual `IconButton` component
|
||||
adds an accessible lable from the `aria-label` prop. So we don't have to concern ourselves with that.
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# Permission Checker
|
||||
|
||||
## Overview
|
||||
|
||||
The **Permission Checker** is a service in Strapi that helps enforce access control policies by verifying user permissions for various actions on content entities. It provides methods to check, sanitize, and validate user actions based on their permissions.
|
||||
|
||||
## Features
|
||||
|
||||
- **Permission Checking**: Verify whether a user has the necessary permissions for a specific action.
|
||||
- **Sanitization**: Remove unauthorized fields from input and output data.
|
||||
- **Validation**: Ensure that queries and input data comply with permission rules.
|
||||
- **Query Enforcement**: Modify queries to include permission constraints automatically.
|
||||
|
||||
## Actions Supported
|
||||
|
||||
The service defines the following actions:
|
||||
|
||||
```ts
|
||||
const ACTIONS = {
|
||||
read: 'plugin::content-manager.explorer.read',
|
||||
create: 'plugin::content-manager.explorer.create',
|
||||
update: 'plugin::content-manager.explorer.update',
|
||||
delete: 'plugin::content-manager.explorer.delete',
|
||||
publish: 'plugin::content-manager.explorer.publish',
|
||||
unpublish: 'plugin::content-manager.explorer.publish',
|
||||
discard: 'plugin::content-manager.explorer.update',
|
||||
} as const;
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Instantiating the Permission Checker
|
||||
|
||||
To use the permission checker, retrieve it from the **content-manager** plugin:
|
||||
|
||||
```ts
|
||||
const permissionChecker = strapi
|
||||
.plugin('content-manager')
|
||||
.service('permission-checker')
|
||||
.create({ userAbility, model });
|
||||
```
|
||||
|
||||
- `userAbility`: The user's ability object containing their permissions. Accessed on the request context object: `ctx.state.userAbility`
|
||||
- `model`: The content type model for which permissions are checked.
|
||||
|
||||
### Checking Permissions
|
||||
|
||||
#### `can.<action>()`
|
||||
|
||||
Determines if the user **has** permission to perform a specific action.
|
||||
|
||||
```ts
|
||||
if (permissionChecker.can.create()) {
|
||||
console.log('User can create content');
|
||||
}
|
||||
```
|
||||
|
||||
#### `cannot.<action>()`
|
||||
|
||||
Determines if the user **does not** have permission to perform a specific action.
|
||||
|
||||
```ts
|
||||
if (permissionChecker.cannot.delete()) {
|
||||
throw new errors.ForbiddenError('User is not allowed to delete content');
|
||||
}
|
||||
```
|
||||
|
||||
### Sanitization Methods
|
||||
|
||||
#### `sanitizeOutput(data, { action })`
|
||||
|
||||
Cleans output data based on permissions.
|
||||
|
||||
```ts
|
||||
const sanitizedData = permissionChecker.sanitizeOutput(entity);
|
||||
```
|
||||
|
||||
#### `sanitizeQuery(query, { action })`
|
||||
|
||||
Cleans a query before execution.
|
||||
|
||||
```ts
|
||||
const safeQuery = permissionChecker.sanitizeQuery({ page: '1', pageSize: '10' });
|
||||
```
|
||||
|
||||
#### `sanitizeInput(action, data, entity)`
|
||||
|
||||
Cleans input data before saving.
|
||||
|
||||
```ts
|
||||
const sanitizedInput = permissionChecker.sanitizeInput('create', inputData);
|
||||
```
|
||||
|
||||
#### `sanitizeCreateInput(data)`
|
||||
|
||||
Shortcut for sanitizing create input.
|
||||
|
||||
```ts
|
||||
const sanitizedCreateData = permissionChecker.sanitizeCreateInput(inputData);
|
||||
```
|
||||
|
||||
#### `sanitizeUpdateInput(entity) => (data)`
|
||||
|
||||
Shortcut for sanitizing update input.
|
||||
|
||||
```ts
|
||||
const sanitizedUpdateData = permissionChecker.sanitizeUpdateInput(existingEntity)(inputData);
|
||||
```
|
||||
|
||||
### Validation Methods
|
||||
|
||||
#### `validateQuery(query, { action })`
|
||||
|
||||
Ensures a query is valid based on permissions.
|
||||
|
||||
```ts
|
||||
const validatedQuery = permissionChecker.validateQuery({ page: '1', pageSize: '10' });
|
||||
```
|
||||
|
||||
#### `validateInput(action, data, entity)`
|
||||
|
||||
Ensures input data is valid before saving.
|
||||
|
||||
```ts
|
||||
permissionChecker.validateInput('update', inputData, existingEntity);
|
||||
```
|
||||
|
||||
### Query Enforcement
|
||||
|
||||
#### `sanitizedQuery.<action>(query)`
|
||||
|
||||
Modifies queries to enforce permissions automatically.
|
||||
|
||||
```ts
|
||||
const securedQuery = permissionChecker.sanitizedQuery.read({ sort: 'createdAt:desc' });
|
||||
```
|
||||
|
||||
## Exported Service
|
||||
|
||||
```ts
|
||||
export default ({ strapi }: { strapi: Core.Strapi }) => ({
|
||||
create: createPermissionChecker(strapi),
|
||||
});
|
||||
```
|
||||
|
||||
## Example Usage in Strapi
|
||||
|
||||
```ts
|
||||
const canCreate = strapi.plugin('content-manager').service('permission-checker').can.create();
|
||||
if (!canCreate) {
|
||||
throw new errors.ForbiddenError('User does not have permission to create content');
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Services",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- content-releases
|
||||
---
|
||||
|
||||
# Content Releases
|
||||
|
||||
A release contains various content entries, each capable of being assigned a specific action such as publish or unpublish. Within a release, entries may be in different locales or come from different content types. With a simple click of a button, a release can execute the designated action for each entry. Content Releases is an enterprise edition feature.
|
||||
|
||||
### Architecture
|
||||
|
||||
As opposed to other EE features built in the [EE folder](docs/docs/01-core/admin/01-ee/00-intro.md), Releases is built as a plugin. The plugin can be found in:
|
||||
|
||||
```
|
||||
packages/core/content-releases
|
||||
```
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
|
||||
### Release's status
|
||||
|
||||
Releases are assigned one of five statuses:
|
||||
|
||||
- **Ready**: Indicates that the release is fully prepared for publishing, with no invalid entries present.
|
||||
- **Blocked**: Release has at least one invalid entry preventing publishing.
|
||||
- **Empty**: Release contains no entries and cannot be published.
|
||||
- **Failed**: Indicates that the publishing attempt for the release has encountered an error with no changes since then.
|
||||
- **Done**: Confirms that the release has been successfully published without encountering any errors.
|
||||
|
||||
These statuses are dynamically updated based on actions such as creation, addition/removal of entries, updates, and publishing attempts. They provide a concise overview of release readiness and validity, ensuring smooth operations and data integrity.
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
title: Backend Design
|
||||
description: Content Releases backend
|
||||
tags:
|
||||
- content-releases
|
||||
- tech design
|
||||
---
|
||||
|
||||
All backend code can be found in:
|
||||
|
||||
```
|
||||
packages/core/content-releases/server
|
||||
```
|
||||
|
||||
## Content-types
|
||||
|
||||
The content-releases plugin creates two hidden content-types.
|
||||
|
||||
### Release
|
||||
|
||||
The `Release` content type stores all the information about a release and its associated Release Actions. It is saved in the database as `strapi_releases`. The schema can be found in:
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/content-types/release/schema.ts
|
||||
```
|
||||
|
||||
### Release Action
|
||||
|
||||
Th `Release Action` content type is associated with any entry from any content-type that has draft and publish enabled. It is responsible for storing the action to perform for an associated entry. It is saved in the database as `strapi_release_actions`. In v4, we used built-in polymorphic relations, but for v5, we stored `contentType`, `locale`, and `entryDocumentId` in the Release Action schema to create a "manual" relationship between actions and entries. This approach allows us to link a release action to a document ID instead of a specific entry ID, as the entry ID may change over time and is not reliable.
|
||||
|
||||
The schema can be found in:
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/content-types/release-action/schema.ts
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Release and Release Action routes are only accessible on the Admin API.
|
||||
|
||||
### Release
|
||||
|
||||
Release routes can be found in:
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/routes/release.ts
|
||||
```
|
||||
|
||||
**Get all releases**:
|
||||
|
||||
- method: `GET`
|
||||
- endpoint: `/content-releases/`
|
||||
- params:
|
||||
```ts
|
||||
{
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Get all releases with/without an entry**:
|
||||
|
||||
- method: `GET`
|
||||
- endpoint: `/content-releases/getByDocumentAttached`
|
||||
- params:
|
||||
```ts
|
||||
{
|
||||
contentTypeUid: string;
|
||||
locale?: string;
|
||||
documentId?: string;
|
||||
hasEntryAttached?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Get a single release**
|
||||
|
||||
- method: `GET`
|
||||
- endpoint: `/content-releases/:id`
|
||||
|
||||
**Create a release**:
|
||||
|
||||
- method: `POST`
|
||||
- endpoint: `/content-releases/`
|
||||
- body:
|
||||
```ts
|
||||
{
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Update a release**:
|
||||
|
||||
- method: `PUT`
|
||||
- endpoint: `/content-releases/:id`
|
||||
- body:
|
||||
```ts
|
||||
{
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Delete a release**:
|
||||
|
||||
- method: `DELETE`
|
||||
- endpoint: `/content-releases/:id`
|
||||
|
||||
**Publish a release**:
|
||||
|
||||
- method: `POST`
|
||||
- endpoint: `/content-releases/:id/publish`
|
||||
|
||||
### Release Action
|
||||
|
||||
**Create a release action**
|
||||
|
||||
- method: `POST`
|
||||
- endpoint: `/content-releases/:releaseId/actions`
|
||||
- body:
|
||||
|
||||
```ts
|
||||
{
|
||||
type: 'publish' | 'unpublish',
|
||||
contentType: string;
|
||||
locale?: string;
|
||||
entryDocumentId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Get release actions from a release**
|
||||
|
||||
- method: `GET`
|
||||
- endpoint: `/content-releases/:releaseId/actions`
|
||||
- body:
|
||||
```ts
|
||||
{
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Update a release action**
|
||||
|
||||
- method: `PUT`
|
||||
- endpoint: `/content-releases/:releaseId/actions/:actionId`
|
||||
- body:
|
||||
```ts
|
||||
{
|
||||
type: 'publish' | 'unpublish';
|
||||
}
|
||||
```
|
||||
|
||||
**Delete a release action**
|
||||
|
||||
- method: `DELETE`
|
||||
- endpoint: `/content-releases/:releaseId/actions/:actionId`
|
||||
|
||||
## Controllers
|
||||
|
||||
### Release
|
||||
|
||||
Handles requests to interact with the Release content type
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/controllers/release.ts
|
||||
```
|
||||
|
||||
### Release Action
|
||||
|
||||
Handles requests to interact with the Release Action content type
|
||||
|
||||
## Services
|
||||
|
||||
### Release
|
||||
|
||||
Interacts with the database for Release CRUD operations
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/services/release.ts
|
||||
```
|
||||
|
||||
### Release Actions
|
||||
|
||||
Interacts with the database for Release Actions CRUD operations
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/services/release-action.ts
|
||||
```
|
||||
|
||||
### Release Validation
|
||||
|
||||
Exposes validation functions to run before performing operations on a Release
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/services/validation.ts
|
||||
```
|
||||
|
||||
### Scheduling
|
||||
|
||||
:::caution
|
||||
Scheduling is still under development, but you can try it **at your own risk** with future flags. The future flag to enable scheduling is `contentReleasesScheduling`.
|
||||
:::
|
||||
|
||||
Exposes methods to schedule release date for releases.
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/services/scheduling.ts
|
||||
```
|
||||
|
||||
### Release status update triggers:
|
||||
|
||||
Considering that retrieving the status of all entries in a release is a heavy operation, we don't fetch it every time a user wants to access a release. Instead, we store the status in a field within the Release Content Type, and we only update it when an action that changes the status is triggered. These actions include:
|
||||
|
||||
#### Creating a release:
|
||||
|
||||
When creating a release, its status is automatically set to "Empty" as there are no entries initially.
|
||||
|
||||
#### Adding an entry to a release:
|
||||
|
||||
Upon adding an entry to a release, its status is recalculated to either "Ready" or "Blocked" based on the validity of the added entry.
|
||||
|
||||
#### Removing an entry from a release:
|
||||
|
||||
After removing an entry from a release, the status is recalculated to determine if the release is now "Ready", "Blocked", or "Empty".
|
||||
|
||||
#### Updating a release:
|
||||
|
||||
Whenever a release is updated, its status is recalculated based on the validity of the actions performed during the update.
|
||||
|
||||
#### Publishing a release:
|
||||
|
||||
During the publishing process, if successful, the status changes to "Done"; otherwise, it changes to "Failed".
|
||||
|
||||
#### Listening to events on entries:
|
||||
|
||||
When an entry is updated or deleted, the status of all releases containing that entry is recalculated to reflect any changes in validity.
|
||||
|
||||
## Migrations
|
||||
|
||||
We have two migrations that we run every time we sync the content types.
|
||||
|
||||
### `deleteActionsOnDisableDraftAndPublish`
|
||||
|
||||
When a user disables Draft and Publish in one Content Type we make sure to remove all the release actions related to entries of that content type to avoid errors.
|
||||
|
||||
### `deleteActionsOnDeleteContentType`
|
||||
|
||||
When a Content Type is deleted, delete all actions containing entries from that Content Type.
|
||||
|
||||
## Subscribing to Lifecycles Events
|
||||
|
||||
When an entry is deleted delete all actions containing that entry.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- content-releases
|
||||
- tech design
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
There are two pages, ReleasesPage and ReleaseDetailsPage. To access these pages a user will need a valid Strapi license with the feature enabled and at lease `plugin::content-releases.read` permissions.
|
||||
|
||||
Redux toolkit is used to manage content releases data (data retrieval, release creation and editing, and fetching release actions). `Formik` is used to create/edit a release and all input components are controlled components.
|
||||
|
||||
### License limits
|
||||
|
||||
Most licenses have feature-based usage limits configured through Chargebee. These limits are exposed to the frontend through [`useLicenseLimits`](/docs/core/admin/ee/hooks/use-license-limits).
|
||||
If the license doesn't specify the number of maximum pending releases an hard-coded is used: max. 3 pending releases.
|
||||
|
||||
### Endpoints
|
||||
|
||||
For a list of all available endpoints please refer to the [detailed backend design documentation](/docs/core/content-releases/backend).
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Releases page
|
||||
tags:
|
||||
- content-releases
|
||||
- tech design
|
||||
---
|
||||
|
||||
### Overview
|
||||
|
||||
The releases page provides a comprehensive display of all available content releases. Content is organized into two tabs: 'pending' (releases not yet published) and 'done' (releases already published).
|
||||
|
||||
#### New Release Creation:
|
||||
|
||||
If a user has the `plugin::content-releases.update` permissions, a 'New release' button will be visible in the header. Clicking the button opens a form requiring a name. The name of a pending release must be unique.
|
||||
|
||||
#### License limits
|
||||
|
||||
If the user reaches the limit of maximum pending releases defined on their license then the 'New release' button is disabled and a banner is displayed to inform the user.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Release details page
|
||||
tags:
|
||||
- content-releases
|
||||
- tech design
|
||||
---
|
||||
|
||||
### Overview:
|
||||
|
||||
The release details page allows users with appropriate permissions to do the following:
|
||||
|
||||
#### Group entries:
|
||||
|
||||
Entries in a release can be be grouped by the following options:
|
||||
|
||||
- Content-Types: Group all entries with the same content type together
|
||||
- Locales: Group all entries with the same locale together.
|
||||
- Actions: Group all entries with the same action (publish/unpublish) together.
|
||||
|
||||
#### Edit a release:
|
||||
|
||||
If the user has the `plugin::content-releases.update` permissions, a "three dot" button will appear in the header giving access to a menu with an "Edit" option.
|
||||
The Edit button opens a modal, enabling users to modify the release name. To save the changes, the new name must be unique (only for pending releases), non-empty, and distinctly modified.
|
||||
|
||||
#### Delete a release:
|
||||
|
||||
If the user has the `plugin::content-releases.delete` permissions, a "three dot" button will appear in the header giving access to a menu with a "Delete" option. Selecting this option triggers a confirmation modal.
|
||||
|
||||
#### View Entry Status
|
||||
|
||||
Each entry within a release is assigned a status, indicating its readiness for actions such as Publish/Unpublish or any validation errors present. If the entry has validation errors, a user can use the entry's "three dots" menu to access the "Edit entry" link which navigates directly to the entry in the content manager.
|
||||
|
||||
Within the CM edit view, users can resolve any identified errors. Upon completion, clicking the "Refresh" button on the release details page updates the status of each entry to reflect any changes made.
|
||||
|
||||
#### Publish a release:
|
||||
|
||||
To execute the release of a content release, users can simply click on the "Publish" button (shown if you have the "plugin::content-releases.publish" permission). If any of the entries exhibit validation errors, the "Publish" action triggers a notification to alert users of the errors.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Frontend",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Content Releases Scheduling
|
||||
description: Content Releases Scheduling
|
||||
tags:
|
||||
- content-releases
|
||||
- tech design
|
||||
---
|
||||
|
||||
:::caution
|
||||
Content Releases Scheduling is not yet a stable feature. Therefore, all the elements documented on this page are not currently visible. If you wish to try Releases Scheduling, you can enable it **at your own** risk using the `contentReleasesScheduling` feature flag.
|
||||
:::
|
||||
|
||||
Scheduling provides users with the ability to set a scheduled date for the release, automating its publication or unpublishing. When this happens, a webhook is triggered, providing the result of the attempt to publish the release.
|
||||
|
||||
## How it works
|
||||
|
||||
Everytime you create or update a release and add a scheduled date, the server responsible for handling this request will generate a new cronjob (utilizing node-schedule) for the selected date to publish the release.
|
||||
|
||||
## Timezones
|
||||
|
||||
When selecting a scheduled date, you have the option to choose a specific timezone; by default, your system timezone is selected. This measure is taken to prevent any potential confusion when selecting the publication time for a release. Consequently, if a user sets a schedule for 16:00 using the "Europe/Paris" timezone (UTC+01:00), another user accessing the same release will see the same time (16:00 (UTC+01:00)), regardless of their system's timezone.
|
||||
|
||||
## Scheduling in a architecture with multiple Strapi instances
|
||||
|
||||
It's possible that your Strapi project runs on multiple instances. In such cases, what happens with the cronjobs? Do they all run simultaneously, attempting to publish the release multiple times? To understand how we address this scenario, it's important to differentiate between two cases when scheduling a release:
|
||||
|
||||
### Release scheduled on runtime
|
||||
|
||||
If you have 3 Strapi instances running concurrently, and you distribute traffic among them using any method, there is not a big problem. This is because the server responsible for handling one request to create/update a release and add a schedule will be the only server with the associated cronjob. Then, there's no duplication, and potential race condition problems are avoided.
|
||||
|
||||
### Starting a strapi instance
|
||||
|
||||
The problem is starting a new Strapi instance, because we retrieve all scheduled releases and ensure that cronjobs are created for each one. Consequently, multiple Strapi instances might end up with the same cronjob for a release publish. To address this, we implement the following logic:
|
||||
|
||||
<img
|
||||
src="/img/content-manager/content-releases/scheduling-publish.png"
|
||||
alt="a diagram overview explaining the publish release flow"
|
||||
/>
|
||||
|
||||
We set up a transaction that locks the release being published using SQL forUpdate. This means that any other processes attempting to access the release row will be put on hold until the first one finishes executing.
|
||||
|
||||
If the validation of the release entries is successful, the publish action proceeds smoothly. In this scenario, we update the releasedAt column of the release with the current date and release the row lock. Subsequently, any incoming processes attempting to access the release would simply encounter an error because the release has already been published.
|
||||
|
||||
On the other hand, if the publish process fails, we update the release's status to "failed". When the status is marked as failed, any subsequent attempts to publish will fail silently. The "failed" status only changes when a user makes alterations to the release.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Content Releases",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Introduction
|
||||
tags:
|
||||
- content-type-builder
|
||||
---
|
||||
|
||||
# Content Type Builder
|
||||
|
||||
This section is an overview of all the features related to the Content Type Builder:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Content Type Builder",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Data Transfer
|
||||
|
||||
This section is an overview of all the features related to the data-transfer package:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
|
||||
Note: The data-transfer package is written in Typescript and any additions or changes must include all necessary typings.
|
||||
@@ -0,0 +1,531 @@
|
||||
---
|
||||
title: Transfer Engine
|
||||
description: Conceptual guide to the data transfer engine
|
||||
tags:
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
The transfer engine manages the data transfer process by facilitating communication between a source provider and a destination provider.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Ops["Stream Operations"]
|
||||
direction LR
|
||||
|
||||
subgraph ReadOps["Read Operations"]
|
||||
direction TB
|
||||
SR[Schema Read]
|
||||
SR --> ER[Entity Read]
|
||||
ER --> AR[Asset Read]
|
||||
AR --> LR[Link Read]
|
||||
LR --> CR[Config Read]
|
||||
end
|
||||
|
||||
subgraph Sources["Source Providers"]
|
||||
direction TB
|
||||
RSP[Remote Source Provider]
|
||||
RSP --> LSP[Local Source Provider]
|
||||
LSP --> FSP[File Source Provider]
|
||||
style Sources fill:#f0f0ff,stroke:#d0d0ff
|
||||
end
|
||||
|
||||
subgraph Engine["Transfer Engine"]
|
||||
direction TB
|
||||
TE[Transfer Engine]
|
||||
TE --> TF[Transform Stream]
|
||||
TF --> PT[Progress Tracker]
|
||||
style Engine fill:#f0f0ff,stroke:#d0d0ff
|
||||
end
|
||||
|
||||
subgraph Dests["Destination Providers"]
|
||||
direction TB
|
||||
RDP[Remote Destination Provider]
|
||||
RDP --> LDP[Local Destination Provider]
|
||||
LDP --> FDP[File Destination Provider]
|
||||
style Dests fill:#f0f0ff,stroke:#d0d0ff
|
||||
end
|
||||
|
||||
subgraph WriteOps["Write Operations"]
|
||||
direction TB
|
||||
SW[Schema Write]
|
||||
SW --> EW[Entity Write]
|
||||
EW --> AW[Asset Write]
|
||||
AW --> LW[Link Write]
|
||||
LW --> CW[Config Write]
|
||||
end
|
||||
|
||||
ReadOps --> Sources
|
||||
Sources --> Engine
|
||||
Engine --> Dests
|
||||
Dests --> WriteOps
|
||||
end
|
||||
|
||||
%% Styling
|
||||
classDef provider fill:#fff0f0,stroke:#d0d0ff,color:black
|
||||
classDef engine fill:#fff0f0,stroke:#d0d0ff,color:black
|
||||
classDef stream fill:#c8ffc8,stroke:#a0ffa0,color:black
|
||||
classDef ops fill:#f8f8f8,stroke:#e8e8e8,color:black
|
||||
|
||||
class RSP,LSP,FSP,RDP,LDP,FDP provider
|
||||
class TE,TF,PT engine
|
||||
class SR,ER,AR,LR,CR,SW,EW,AW,LW,CW stream
|
||||
class ReadOps,WriteOps ops
|
||||
style Ops fill:#f8f8f8,stroke:#e8e8e8
|
||||
```
|
||||
|
||||
## Code location
|
||||
|
||||
`packages/core/data-transfer/src/engine/index.ts`
|
||||
|
||||
## The transfer process
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant TE as Transfer Engine
|
||||
participant SP as Source Provider
|
||||
participant DP as Destination Provider
|
||||
participant DH as Diff Handler
|
||||
|
||||
rect rgb(230, 245, 230)
|
||||
Note over Client,DP: 1️⃣ Bootstrap & Initialize Phase
|
||||
Client->>+TE: Start Transfer
|
||||
TE->>SP: Bootstrap
|
||||
Note right of SP: Connect to files,<br/>databases, etc.
|
||||
TE->>DP: Bootstrap
|
||||
Note right of DP: Setup connections,<br/>prepare storage
|
||||
TE->>SP: Get Metadata
|
||||
SP-->>TE: Source Metadata
|
||||
TE->>DP: Get Metadata
|
||||
DP-->>TE: Destination Metadata
|
||||
end
|
||||
|
||||
rect rgb(230, 240, 250)
|
||||
Note over Client,DP: 2️⃣ Integrity Check Phase
|
||||
TE->>TE: Version Strategy Check
|
||||
Note right of TE: Compare Strapi versions
|
||||
alt Version Mismatch
|
||||
TE->>Client: Version Validation Error
|
||||
end
|
||||
TE->>SP: Get Schemas
|
||||
SP-->>TE: Source Schemas
|
||||
TE->>DP: Get Schemas
|
||||
DP-->>TE: Destination Schemas
|
||||
TE->>TE: Schema Strategy Check
|
||||
alt Schema Differences
|
||||
TE->>+DH: onSchemaDiff
|
||||
Note right of DH: Resolve schema<br/>differences
|
||||
alt Resolved
|
||||
DH-->>-TE: Continue
|
||||
else Unresolved
|
||||
DH-->>TE: Schema Validation Error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(245, 235, 250)
|
||||
Note over Client,DP: 3️⃣ Data Transfer Phase
|
||||
|
||||
Note over TE,DP: Stage 1: Schemas
|
||||
TE->>SP: Create Schemas ReadStream
|
||||
TE->>DP: Create Schemas WriteStream
|
||||
SP-->>DP: Stream Schema Data
|
||||
|
||||
Note over TE,DP: Stage 2: Entities
|
||||
TE->>SP: Create Entities ReadStream
|
||||
TE->>DP: Create Entities WriteStream
|
||||
SP-->>DP: Stream Entity Data
|
||||
|
||||
Note over TE,DP: Stage 3: Assets
|
||||
TE->>SP: Create Assets ReadStream
|
||||
TE->>DP: Create Assets WriteStream
|
||||
SP-->>DP: Stream Asset Files
|
||||
|
||||
Note over TE,DP: Stage 4: Links
|
||||
TE->>SP: Create Links ReadStream
|
||||
TE->>DP: Create Links WriteStream
|
||||
SP-->>DP: Stream Relation Data
|
||||
|
||||
Note over TE,DP: Stage 5: Configuration
|
||||
TE->>SP: Create Config ReadStream
|
||||
TE->>DP: Create Config WriteStream
|
||||
SP-->>DP: Stream Config Data
|
||||
end
|
||||
|
||||
rect rgb(255, 240, 230)
|
||||
Note over Client,DP: 4️⃣ Cleanup Phase
|
||||
TE->>SP: Close
|
||||
SP-->>TE: Closed
|
||||
TE->>DP: Close
|
||||
DP-->>TE: Closed
|
||||
TE->>-Client: Transfer Complete
|
||||
end
|
||||
```
|
||||
|
||||
A transfer starts by bootstrapping and initializing itself and the providers. That is the stage where providers attempt to make any necessary connections to files, databases, websockets, etc.
|
||||
|
||||
After that, the integrity check between the source and destination is run, which validates the requirements set by the chosen schemaStrategy and versionStrategy.
|
||||
|
||||
Note: Schema differences during this stage can be resolved programmatically by adding an `onSchemaDiff` handler. However, be aware that this interface is likely to change to a more generic engine handler (such as `engine.on('schemaDiff', handler)`) before this feature is stable.
|
||||
|
||||
Once the integrity check has passed, the transfer begins by opening streams from the source to the destination one stage at a time. The following is a list of the stages in the order they are run:
|
||||
|
||||
1. schemas - content type schemas. Note: with all built-in Strapi destination providers, only the Strapi file provider makes use of this data
|
||||
2. entities - all entities (including components, dynamic zones, and media data but not media files) _without their relations_
|
||||
3. assets - the files from the /uploads folder
|
||||
4. links - the relations between entities
|
||||
5. configuration - the Strapi project configuration data
|
||||
|
||||
Once all stages have been completed, the transfer waits for all providers to close and then emits a finish event and the transfer completes.
|
||||
|
||||
## Setting up the transfer engine
|
||||
|
||||
A transfer engine object is created by using `createTransferEngine`, which accepts a [source provider](./02-providers/01-source-providers.md), a [destination provider](./02-providers/02-destination-providers.md), and an options object.
|
||||
|
||||
Note: By default, a transfer engine will transfer ALL data, including admin data, api tokens, etc. Transform filters must be used if you wish to exclude, as seen in the example below. An array called `DEFAULT_IGNORED_CONTENT_TYPES` is available from @strapi/data-transfer containing the uids that are excluded by default from the import, export, and transfer commands. If you intend to transfer admin data, be aware that this behavior will likely change in the future to automatically exclude the entire `admin::` uid namespace and will instead require them to be explicitly included.
|
||||
|
||||
```typescript
|
||||
const engine = createTransferEngine(source, destination, options);
|
||||
```
|
||||
|
||||
### Engine Options
|
||||
|
||||
An example using every available option:
|
||||
|
||||
```typescript
|
||||
const options = {
|
||||
versionStrategy: 'ignore', // see versionStragy documentation
|
||||
schemaStrategy: 'strict', // see schemaStragey documentation
|
||||
exclude: [], // exclude these classifications of data; see CLI documentation of `--exclude` for list
|
||||
only: [], // transfer only these classifications of data; see CLI documentation of `--only` for list
|
||||
throttle: 0, // add a delay of this many millseconds between each item transferred
|
||||
|
||||
// the keys of `transforms` are the stage names for which they are run
|
||||
transforms: {
|
||||
links: [
|
||||
{
|
||||
// exclude all relations to ignored content types
|
||||
filter(link) {
|
||||
return (
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
// Note: map exists for links but is not recommended
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
// exclude all ignored content types
|
||||
filter(entity) {
|
||||
return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
|
||||
},
|
||||
},
|
||||
{
|
||||
map(entity) {
|
||||
// remove somePrivateField from privateThing entities
|
||||
if (entity.type === 'api::privateThing.privateThing') {
|
||||
entity.somePrivateField = undefined;
|
||||
}
|
||||
|
||||
return entity;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### versionStrategy
|
||||
|
||||
The following `versionStrategy` values may be used:
|
||||
|
||||
`'ignore'` - allow transfer between any versions of Strapi
|
||||
`'exact'` - require an exact version match (including tags such as -alpha and -beta)
|
||||
`'major'` - require only the semver major version to match (but allow minor, patch, and tag to differ)
|
||||
`'minor'` - require only the semver major and minor versions to match (but allow patch to differ)
|
||||
`'patch'` - require only the semver major, minor, and patch version to match (but allow tag differences such as -alpha and -beta)
|
||||
|
||||
The default `versionStrategy` used when one is not provided is `'ignore'`.
|
||||
|
||||
#### schemaStrategy
|
||||
|
||||
The follow `schemaStrategy` values may be used:
|
||||
|
||||
`'ignore'` - bypass schema validation (transfer will attempt to run but throw errors on incompatible data type inserts)
|
||||
`'strict'` - disallow mismatches that are expected to cause errors in the transfer, but allow certain non-data fields in the schema to differ
|
||||
`'exact'` - schema must be identical with no changes
|
||||
|
||||
Note: The "strict" schema strategy is defined as "anything expected to cause errors in the transfer" and is the default method for the import, export, and transfer CLI commands. Therefore, the technical functionality will always be subject to change. If you need to find the definition for the current version of Strapi, see `packages/core/data-transfer/src/engine/validation/schemas/index.ts`
|
||||
|
||||
The default `schemaStrategy` used when one is not provided is `'strict'`.
|
||||
|
||||
##### Handling Schema differences
|
||||
|
||||
When a schema diff is discovered with a given schemaStrategy, an error is throw. However, before throwing the error the engine checks to see if there are any schema diff handlers set via `engine.onSchemaDiff(handler)` which allows errors to be bypassed (for example, by prompting the user if they wish to proceed).
|
||||
|
||||
A diff handler is an optionally asynchronous middleware function that accepts a `context` and a `next` parameter.
|
||||
|
||||
`context` is an object of type `SchemaDiffHandlerContext`
|
||||
|
||||
```typescript
|
||||
// type Diff can be found in /packages/core/data-transfer/src/utils/json.ts
|
||||
type SchemaDiffHandlerContext = {
|
||||
ignoredDiffs: Record<string, Diff[]>;
|
||||
diffs: Record<string, Diff[]>;
|
||||
source: ISourceProvider;
|
||||
destination: IDestinationProvider;
|
||||
};
|
||||
```
|
||||
|
||||
`next` is a function that is called, passing the modified `context` object, to proceed to the next middleware function.
|
||||
|
||||
```typescript
|
||||
const diffHandler = async (context, next) => {
|
||||
const ignoreThese = {};
|
||||
// loop through the diffs
|
||||
Object.entries(context.diffs).forEach(([uid, diffs]) => {
|
||||
for (const [i, diff] of diffs) {
|
||||
// get the path of the diff in the schema
|
||||
const path = [uid].concat(diff.path).join('.');
|
||||
|
||||
// Allow a diff on the country schema displayName
|
||||
if (path === 'api::country.country.info.displayName') {
|
||||
if (!isArray(context.ignoredDiffs[uid])) {
|
||||
context.ignoredDiffs[uid] = [];
|
||||
}
|
||||
context.ignoredDiffs[uid][i] = diff;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return next(context);
|
||||
};
|
||||
|
||||
engine.onSchemaDiff(diffHandler);
|
||||
```
|
||||
|
||||
After all the schemaDiffHandler middlewares have been run, another diff is run between `context.ignoredDiffs` and `context.diffs` and any remaining diffs that have not been ignored are thrown as fatal errors and the engine will abort the transfer.
|
||||
|
||||
### Progress Tracking events
|
||||
|
||||
The transfer engine allows tracking the progress of your transfer either directly with the engine.progress.data object, or with listeners using the engine.progress.stream PassThrough stream. The engine.progress.data object definition is of type TransferProgress.
|
||||
|
||||
Here is an example that logs a message at the beginning and end of each stage, as well as a message after each item has been transferred
|
||||
|
||||
```typescript
|
||||
const progress = engine.progress.stream;
|
||||
|
||||
progress.on(`stage::start`, ({ stage, data }) => {
|
||||
console.log(`${stage} has started at ${data[stage].startTime}`);
|
||||
});
|
||||
|
||||
progress.on('stage::finish', ({ stage, data }) => {
|
||||
console.log(`${stage} has finished at ${data[stage].endTime}`);
|
||||
});
|
||||
|
||||
progress.on('stage::progress', ({ stage, data }) => {
|
||||
console.log('Transferred ${data[stage].bytes} bytes / ${data[stage].count} entities');
|
||||
});
|
||||
```
|
||||
|
||||
Note: There is currently no way for a source provider to give a "total" number of records expected to be transferred, but it is expected in a future update.
|
||||
|
||||
The following events are available:
|
||||
|
||||
`stage::start` - at the start of each stage
|
||||
`stage::finish` - at the end of each stage
|
||||
`stage::progress` - after each entitity in that stage has been transferred
|
||||
`stage::skip` - when an entire stage is skipped (eg, when 'only' or 'exclude' are used)
|
||||
`stage::error` - when there is an error thrown during a stage
|
||||
`transfer::init` - at the very beginning of engine.transfer()
|
||||
`transfer::start` - after bootstrapping and initializing the providers, when the transfer is about to start
|
||||
`transfer::finish` - when the transfer has finished
|
||||
`transfer::error` - when there is an error thrown during the transfer
|
||||
|
||||
### Diagnostics events
|
||||
|
||||
The engine includes a diagnostics reporter which can be used to listen for diagnostics information (debug messages, errors, etc).
|
||||
|
||||
Here is an example for creating a diagnostics listener:
|
||||
|
||||
```typescript
|
||||
// listener function
|
||||
const diagnosticListener: DiagnosticListener = (data: GenericDiagnostic) => {
|
||||
// handle the diagnostics event, for example with custom logging
|
||||
};
|
||||
|
||||
// add a generic listener
|
||||
engine.diagnostics.onDiagnostic(diagnosticsListener);
|
||||
|
||||
// add an error listener
|
||||
engine.diagnostics.on('error', diagnosticListener);
|
||||
|
||||
// add a warning listener
|
||||
engine.diagnostics.on('warning', diagnosticListener);
|
||||
```
|
||||
|
||||
To emit your own diagnostics event:
|
||||
|
||||
```typescript
|
||||
const event: ErrorDiagnostic = {
|
||||
kind: 'error',
|
||||
details: {
|
||||
message: 'Your diagnostics message'
|
||||
createdAt: new Date(),
|
||||
},
|
||||
name: 'yourError',
|
||||
severity: 'fatal',
|
||||
error: new Error('your error message')
|
||||
}
|
||||
|
||||
engine.diagnostics.report(event);
|
||||
```
|
||||
|
||||
Here is an excerpt of the relevant types used in the previous examples:
|
||||
|
||||
```typescript
|
||||
// engine/diagnostic.ts
|
||||
// format of the data sent to the listener
|
||||
export type GenericDiagnostic<K extends DiagnosticKind, T = unknown> = {
|
||||
kind: K;
|
||||
details: {
|
||||
message: string;
|
||||
createdAt: Date;
|
||||
} & T;
|
||||
};
|
||||
|
||||
export type DiagnosticKind = 'error' | 'warning' | 'info';
|
||||
|
||||
export type Diagnostic = ErrorDiagnostic | WarningDiagnostic | InfoDiagnostic;
|
||||
|
||||
export type ErrorDiagnosticSeverity = 'fatal' | 'error' | 'silly';
|
||||
|
||||
export type ErrorDiagnostic = GenericDiagnostic<
|
||||
'error',
|
||||
{
|
||||
name: string;
|
||||
severity: ErrorDiagnosticSeverity;
|
||||
error: Error;
|
||||
}
|
||||
>;
|
||||
|
||||
export type WarningDiagnostic = GenericDiagnostic<
|
||||
'warning',
|
||||
{
|
||||
origin?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export type InfoDiagnostic<T = unknown> = GenericDiagnostic<
|
||||
'info',
|
||||
{
|
||||
params?: T;
|
||||
}
|
||||
>;
|
||||
```
|
||||
|
||||
### Transforms
|
||||
|
||||
Transforms allow you to manipulate the data that is sent from the source before it reaches the destination.
|
||||
|
||||
## Filter (excluding data)
|
||||
|
||||
Filters can be used to exclude data sent from the source before it is streamed to the destination. They are methods that accept an entity, link, schema, etc and return `true` to keep the entity and `false` to remove it.
|
||||
|
||||
Here is an example that filters out all entities with an id higher than 100:
|
||||
|
||||
```typescript
|
||||
const options = {
|
||||
...otherOptions,
|
||||
transforms: {
|
||||
entities: [
|
||||
{
|
||||
// exclude all ignored admin content types
|
||||
filter(entity) {
|
||||
return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
|
||||
},
|
||||
},
|
||||
{
|
||||
// exclude all entities with an id higher than 100
|
||||
filter(entity) {
|
||||
return Number(entity.id) <= 100;
|
||||
},
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
// exclude all relations to ignored content types
|
||||
filter(link) {
|
||||
return (
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
// remember to exclude links as well or else an error will be thrown when attempting to link an entity we filtered
|
||||
filter(entity) {
|
||||
return Number(link.left.id) <= 100 || Number(link.right.id) <= 100)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Map (modifying data)
|
||||
|
||||
Maps can be used to modify data sent from the source before it is streamed to the destination. They are methods that accept an entity, link, schema, etc and return the modified version of the object.
|
||||
|
||||
This can be used, for example, to sanitize data between environments.
|
||||
|
||||
Here is an example that removes a field called `somePrivateField` from a content type `privateThing`.
|
||||
|
||||
```typescript
|
||||
const options = {
|
||||
...otherOptions,
|
||||
transforms: {
|
||||
entities: [
|
||||
{
|
||||
// exclude all ignored content types
|
||||
filter(entity) {
|
||||
return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
|
||||
},
|
||||
},
|
||||
{
|
||||
map(entity) {
|
||||
// remove somePrivateField from privateThing entities
|
||||
if (entity.type === 'api::privateThing.privateThing') {
|
||||
entity.somePrivateField = undefined;
|
||||
}
|
||||
|
||||
return entity;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
By mapping schemas as well as entities, it's even possible (although complex!) to modify data structures between source and destination.
|
||||
|
||||
## Running a transfer
|
||||
|
||||
Running a transfer simply involves calling the asynchrounous engine.transfer() method.
|
||||
|
||||
```typescript
|
||||
const engine = createTransferEngine(source, destination, options);
|
||||
try {
|
||||
await engine.transfer();
|
||||
} catch (e) {
|
||||
console.error('Something went wrong: ', e?.message);
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that engine.transfer() throws on any fatal errors it encounters.
|
||||
|
||||
Note: The transfer engine (and the providers) current only support a single `engine.transfer()` and must be re-instantiated if intended to run multiple times. In the future it is expected to allow them to be used for multiple transfers in a row, but that usage is untested and will result in unpredictable behavior.
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
---
|
||||
title: Stream Lifecycle
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
---
|
||||
|
||||
# Stream Lifecycle in Data Transfer
|
||||
|
||||
## Overview
|
||||
|
||||
The transfer process consists of five sequential stages. Each stage follows the same stream lifecycle pattern but handles different types of data:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Process["Transfer Process"]
|
||||
subgraph S1["1.Schemas Stage"]
|
||||
direction TB
|
||||
DB["Database Structure"]
|
||||
CT["Content Type Configs"]
|
||||
CD["Component Definitions"]
|
||||
end
|
||||
|
||||
subgraph S2["2.Entities Stage"]
|
||||
direction TB
|
||||
AC["Actual Content"]
|
||||
CoD["Component Data"]
|
||||
DZ["Dynamic Zone Data"]
|
||||
MM["Media Metadata"]
|
||||
end
|
||||
|
||||
subgraph S3["3.Assets Stage"]
|
||||
direction TB
|
||||
UF["Upload Files"]
|
||||
FM["File Metadata"]
|
||||
FV["File Variants/Formats"]
|
||||
end
|
||||
|
||||
subgraph S4["4.Links Stage"]
|
||||
direction TB
|
||||
CTR["Content Type Relations"]
|
||||
CR["Component Relations"]
|
||||
MR["Media Relations"]
|
||||
end
|
||||
|
||||
subgraph S5["5.Configuration Stage"]
|
||||
direction TB
|
||||
SC["Strapi Configurations"]
|
||||
PS["Project Settings"]
|
||||
AC2["API Configurations"]
|
||||
end
|
||||
|
||||
S1 ==> S2
|
||||
S2 ==> S3
|
||||
S3 ==> S4
|
||||
S4 ==> S5
|
||||
end
|
||||
|
||||
%% Styling
|
||||
classDef stageClass fill:#f0f0ff,stroke:#d0d0ff,color:black
|
||||
classDef itemClass fill:white,stroke:#d0d0ff,color:black
|
||||
classDef processClass fill:#fafafa,stroke:#e0e0e0
|
||||
|
||||
class S1,S2,S3,S4,S5 stageClass
|
||||
class DB,CT,CD,AC,CoD,DZ,MM,UF,FM,FV,CTR,CR,MR,SC,PS,AC2 itemClass
|
||||
class Process processClass
|
||||
|
||||
%% Link styling
|
||||
linkStyle 0,1,2,3 stroke:#d0d0ff,stroke-width:2px
|
||||
```
|
||||
|
||||
1. **Schemas Stage**: Content type definitions
|
||||
|
||||
- Transfers database structure
|
||||
- Content type configurations
|
||||
- Component definitions
|
||||
|
||||
2. **Entities Stage**: Content without relations
|
||||
|
||||
- Actual content data
|
||||
- Component data
|
||||
- Dynamic zone data
|
||||
- Media metadata (not files)
|
||||
|
||||
3. **Assets Stage**: Media files
|
||||
|
||||
- Files from /uploads folder
|
||||
- File metadata
|
||||
- File variants/formats
|
||||
|
||||
4. **Links Stage**: Relations between entities
|
||||
|
||||
- Content type relations
|
||||
- Component relations
|
||||
- Media relations
|
||||
|
||||
5. **Configuration Stage**: Project settings
|
||||
- Strapi configurations
|
||||
- Project-specific settings
|
||||
- API configurations
|
||||
|
||||
## Stream Lifecycle Phases
|
||||
|
||||
### 1. Stream Creation Phase
|
||||
|
||||
During this phase, the Transfer Engine initializes and connects all necessary streams for the data transfer.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TE as Transfer Engine
|
||||
participant SP as Source Provider
|
||||
participant TR as Transform Stream
|
||||
participant PT as Progress Tracker
|
||||
participant DP as Destination Provider
|
||||
|
||||
rect rgb(200, 255, 200)
|
||||
Note over TE,DP: Stream Creation Phase
|
||||
TE->>SP: createReadStream()
|
||||
activate SP
|
||||
SP-->>TE: Source Stream
|
||||
TE->>TR: createTransformStream()
|
||||
activate TR
|
||||
TE->>PT: createProgressTracker()
|
||||
activate PT
|
||||
TE->>DP: createWriteStream()
|
||||
activate DP
|
||||
end
|
||||
```
|
||||
|
||||
**Process:**
|
||||
|
||||
1. The Transfer Engine requests a read stream from the Source Provider
|
||||
2. A transform stream is created to handle data modifications
|
||||
3. A progress tracker stream is initialized for monitoring
|
||||
4. A write stream is requested from the Destination Provider
|
||||
|
||||
**Example Implementation:**
|
||||
|
||||
```typescript
|
||||
async transferEntities(): Promise {
|
||||
const stage: TransferStage = 'entities';
|
||||
|
||||
// 1. Create read stream from source
|
||||
const source = await this.sourceProvider.createEntitiesReadStream?.();
|
||||
|
||||
// 2. Create transform stream for data modifications
|
||||
const transform = this.#createStageTransformStream(stage);
|
||||
|
||||
// 3. Create progress tracker
|
||||
const tracker = this.#progressTracker(stage);
|
||||
|
||||
// 4. Create write stream from destination
|
||||
const destination = await this.destinationProvider.createEntitiesWriteStream?.();
|
||||
|
||||
// 5. Start the transfer
|
||||
await this.#transferStage({
|
||||
stage,
|
||||
source,
|
||||
destination,
|
||||
transform,
|
||||
tracker
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Stream Pipeline Phase
|
||||
|
||||
This phase represents the active data transfer process where data flows through the connected streams.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SP as Source Provider
|
||||
participant TR as Transform Stream
|
||||
participant PT as Progress Tracker
|
||||
participant DP as Destination Provider
|
||||
|
||||
rect rgb(255, 240, 240)
|
||||
Note over SP,DP: Data Flow Through Pipeline
|
||||
|
||||
SP->>TR: Read Data
|
||||
Note over SP,TR: 1. Read entities from database
|
||||
|
||||
TR->>PT: Transform Data
|
||||
Note over TR,PT: 2. Filter specific content types
|
||||
|
||||
PT->>DP: Track
|
||||
Note over PT,DP: 3. Track progress
|
||||
PT->>DP: Write
|
||||
Note over PT,DP: 4. write to destination
|
||||
end
|
||||
```
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Source Provider reads data (e.g., from database, file system)
|
||||
2. Data passes through transform stream for any modifications
|
||||
3. Progress tracker monitors and reports transfer progress
|
||||
4. Destination Provider writes processed data
|
||||
|
||||
**Key Aspects:**
|
||||
|
||||
- Data flows in chunks through the pipeline
|
||||
- Each stream can process data asynchronously
|
||||
- Progress events are emitted for monitoring
|
||||
- Errors are caught and handled at each step
|
||||
|
||||
**Example Flow:**
|
||||
|
||||
```typescript
|
||||
async #transferStage(options: {
|
||||
stage: TransferStage;
|
||||
source?: Readable;
|
||||
destination?: Writable;
|
||||
transform?: PassThrough;
|
||||
tracker?: PassThrough;
|
||||
}) {
|
||||
const { stage, source, destination, transform, tracker } = options;
|
||||
|
||||
// Create pipeline
|
||||
let stream: Readable = source;
|
||||
|
||||
// Add transform if provided
|
||||
if (transform) {
|
||||
stream = stream.pipe(transform);
|
||||
}
|
||||
|
||||
// Add progress tracker
|
||||
if (tracker) {
|
||||
stream = stream.pipe(tracker);
|
||||
}
|
||||
|
||||
// Connect to destination
|
||||
await new Promise((resolve, reject) => {
|
||||
this.#currentStream = stream
|
||||
.pipe(destination)
|
||||
.on('error', (error) => {
|
||||
this.#emitStageUpdate('error', stage);
|
||||
reject(error);
|
||||
})
|
||||
.on('close', () => {
|
||||
this.#emitStageUpdate('finish', stage);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Stream Closing Phase
|
||||
|
||||
The closing phase handles the cleanup of resources and ensures proper completion of the transfer.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TE as Transfer Engine
|
||||
participant SP as Source Provider
|
||||
participant TR as Transform Stream
|
||||
participant PT as Progress Tracker
|
||||
participant DP as Destination Provider
|
||||
|
||||
rect rgb(240, 240, 255)
|
||||
Note over TE,DP: Cleanup Process
|
||||
SP--xTE: Close Source
|
||||
Note over SP,TE: 1. Close database connections
|
||||
|
||||
TR--xTE: Close Transform
|
||||
Note over TR,TE: 2. Clear transform buffers
|
||||
|
||||
PT--xTE: Close Tracker
|
||||
Note over PT,TE: 3. Final progress update
|
||||
|
||||
DP--xTE: Close Destination
|
||||
Note over DP,TE: 4. Commit transactions
|
||||
end
|
||||
```
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Source stream indicates end of data
|
||||
2. Transform stream processes final data chunks
|
||||
3. Progress tracker records final metrics
|
||||
4. Destination stream completes writing
|
||||
5. Resources are cleaned up
|
||||
|
||||
**Example Implementation:**
|
||||
|
||||
```typescript
|
||||
async close(): Promise {
|
||||
try {
|
||||
// Clean up providers
|
||||
const results = await Promise.allSettled([
|
||||
this.sourceProvider.close?.(),
|
||||
this.destinationProvider.close?.()
|
||||
]);
|
||||
|
||||
// Handle any cleanup errors
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.reportError(result.reason, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Final cleanup
|
||||
this.#currentStream = undefined;
|
||||
this.progress.data = {};
|
||||
} catch (error) {
|
||||
this.panic(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
The transfer engine maintains progress information for each stage:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Start[stage::start] -->
|
||||
Progress[stage::progress] -->
|
||||
Finish[stage::finish]
|
||||
|
||||
Progress -.-> Error[stage::error]
|
||||
Start -.-> Skip[stage::skip]
|
||||
|
||||
style Start fill:#c8ffc8
|
||||
style Progress fill:#f0f0ff
|
||||
style Finish fill:#c8ffc8
|
||||
style Error fill:#ffebee
|
||||
style Skip fill:#e3f2fd
|
||||
```
|
||||
|
||||
Progress events are emitted throughout the transfer:
|
||||
|
||||
- `stage::start`: When a stage begins
|
||||
- `stage::progress`: During data transfer
|
||||
- `stage::finish`: When a stage completes
|
||||
- `stage::error`: If an error occurs
|
||||
- `stage::skip`: If a stage is skipped
|
||||
|
||||
```typescript
|
||||
interface TransferProgress {
|
||||
[stage: TransferStage]: {
|
||||
count: number; // Number of items processed
|
||||
bytes: number; // Amount of data transferred
|
||||
startTime: number; // When stage started
|
||||
endTime?: number; // When stage completed
|
||||
aggregates?: {
|
||||
// Optional grouped metrics
|
||||
[key: string]: {
|
||||
count: number;
|
||||
bytes: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Introduction
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Data Transfer Providers
|
||||
|
||||
Data transfer providers are the interfaces for streaming data during a transfer.
|
||||
|
||||
[Source providers](./01-source-providers.md) provide read streams for each stage in the transfer.
|
||||
|
||||
[Destination providers](./02-destination-providers.md) provide write streams for each stage in the transfer.
|
||||
|
||||
Strapi provides both source and destination providers for the following:
|
||||
|
||||
- [Strapi file](./03-strapi-file/00-overview.md): a standardized file format designed for the transfer process
|
||||
- [Local Strapi](./04-local-strapi/00-overview.md): a connection to a local Strapi project which uses its configured database connection to manage data
|
||||
- [Remote Strapi](./05-remote-strapi/00-overview.md): a wrapper of local Strapi provider that adds a websocket interface to a running remote (network) instance of Strapi
|
||||
|
||||
Each provider must provide the same interface for transferring data, but will usually include its own unique set of options to be passed in when initializing the provider.
|
||||
|
||||
## Creating your own providers
|
||||
|
||||
To create your own providers, you must implement the interface(s) defined in `ISourceProvider` and `IDestinationProvider` found in `packages/core/data-transfer/types/providers.d.ts`.
|
||||
|
||||
It is not necessary to create both a source and destination provider, only the part necessary for your use.
|
||||
|
||||
For examples, see the existing providers such as the local Strapi provider.
|
||||
|
||||
## Asset Transfers
|
||||
|
||||
Currently, all of the data-transfer providers only handle local media assets (the `/upload` folder). Provider media is currently in development. Therefore, everything related to asset transfers -- including Strapi file structure, restore strategy, and rollback for assets -- is currently treated as `unstable` and likely to change in the near future.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Source Providers
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Source Providers
|
||||
|
||||
## Source provider structure
|
||||
|
||||
A source provider must implement the interface ISourceProvider found in `packages/core/data-transfer/types/providers.d.ts`.
|
||||
|
||||
In short, it provides a set of `create{_stage_}ReadStream()` methods for each stage that provide a Readable stream, which will retrieve its data (ideally from its own stream) and then perform a `stream.write(entity)` for each entity, link (relation), asset (file), configuration entity, or content type schema depending on the stage.
|
||||
|
||||
When each stage's stream has finished sending all the data, the stream must be closed before the transfer engine will continue to the next stage.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Destination Providers
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Destination Providers
|
||||
|
||||
## Destination provider structure
|
||||
|
||||
A destination provider must implement the interface IDestinationProvider found in `packages/core/data-transfer/types/providers.d.ts`.
|
||||
|
||||
In short, it provides a set of `create{_stage_}WriteStream()` methods for each stage that provide a Writable stream, which will be passed each entity, link (relation), asset (file), configuration entity, or content type schema (depending on the stage) piped from the Readable source provider stream.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Overview
|
||||
tags:
|
||||
- experimental
|
||||
- providers
|
||||
- import
|
||||
- export
|
||||
- data-transfer
|
||||
---
|
||||
|
||||
# Strapi Data File Providers
|
||||
|
||||
Strapi data file providers transfer data to or from a [Strapi Data File](./01-file-structure.md).
|
||||
|
||||
The files are optionally compressed and/or encrypted using a given key (password).
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Strapi File Structure
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Strapi File Structure
|
||||
|
||||
The Strapi file providers expect a .tar file (optionally compressed with gzip and/or encrypted with 'aes-128-ecb') that internally uses POSIX style file paths with the following structure:
|
||||
|
||||
```
|
||||
./
|
||||
configuration
|
||||
entities
|
||||
links
|
||||
metadata.json
|
||||
schemas
|
||||
|
||||
./configuration:
|
||||
configuration_00001.jsonl
|
||||
|
||||
./entities:
|
||||
entities_00001.jsonl
|
||||
|
||||
./links:
|
||||
links_00001.jsonl
|
||||
|
||||
./schemas:
|
||||
schemas_00001.jsonl
|
||||
```
|
||||
|
||||
## metadata.json
|
||||
|
||||
This file provides metadata about the original source of the data. At minimum, it should include a createdAt timestamp and the version of Strapi that the file was created with (for compatibility checks).
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": "2023-06-26T07:31:20.062Z",
|
||||
"strapi": {
|
||||
"version": "4.11.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## A directory for each stage of data
|
||||
|
||||
There should also be a directory for each stage of data that includes sequentially numbered JSON Lines (.jsonl) files
|
||||
|
||||
The files are named in the format: `{stage}\{stage}_{5-digit sequence number}.jsonl`
|
||||
|
||||
Any number of files may be provided for each stage, as long as the sequence numbers are in order. That is, after first reading 00001, the file source provider will attempt to read file 00002 and if it is not found, it will consider the stage complete.
|
||||
|
||||
### JSONL files
|
||||
|
||||
[JSON Lines](https://jsonlines.org/) files are essentially JSON files, except that newline characters are used to delimit the JSON objects. This allows the provider to read in a single line at a time, rather than loading the entire file into memory, minimizing RAM usage during a transfer and allowing files containing any amount of data.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Source
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Strapi File Source Provider
|
||||
|
||||
This provider will open and read a Strapi Data File as a data source.
|
||||
|
||||
## Provider Options
|
||||
|
||||
The accepted options are defined in `ILocalFileSourceProviderOptions`.
|
||||
|
||||
```typescript
|
||||
file: {
|
||||
path: string; // the file to load
|
||||
};
|
||||
|
||||
encryption: {
|
||||
enabled: boolean; // if the file is encrypted (and should be decrypted)
|
||||
key?: string; // the key to decrypt the file
|
||||
};
|
||||
|
||||
compression: {
|
||||
enabled: boolean; // if the file is compressed (and should be decompressed)
|
||||
};
|
||||
```
|
||||
|
||||
Note: When the Strapi CLI attempts to import a file, the options for compression and encryption are set based on the extension of the file being loaded, eg a file with the .gz extension will have the "compress" option set, and a file that includes the .enc extension will have the "encrypt" option set.
|
||||
|
||||
When using the transfer engine programmatically, you may make the determination whether the file being loaded should be decrypted or compressed by setting
|
||||
those options.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Destination
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Strapi File Destination Provider
|
||||
|
||||
This provider will output a Strapi Data File.
|
||||
|
||||
Note: this destination provider does not provide a schema or metadata, and will therefore never report a schema match error or version validation error
|
||||
|
||||
## Provider Options
|
||||
|
||||
The accepted options are defined in `ILocalFileDestinationProviderOptions`.
|
||||
|
||||
```typescript
|
||||
encryption: {
|
||||
enabled: boolean; // if the file should be encrypted
|
||||
key?: string; // the key to use when encryption.enabled is true
|
||||
};
|
||||
|
||||
compression: {
|
||||
enabled: boolean; // if the file should be compressed with gzip
|
||||
};
|
||||
|
||||
file: {
|
||||
path: string; // the filename to create
|
||||
maxSize?: number; // the max size of a single backup file
|
||||
maxSizeJsonl?: number; // the max lines of each jsonl file before creating the next file
|
||||
};
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "File Providers",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Overview
|
||||
tags:
|
||||
- experimental
|
||||
- providers
|
||||
- import
|
||||
- export
|
||||
- data-transfer
|
||||
---
|
||||
|
||||
# Local Strapi Providers
|
||||
|
||||
The local Strapi provider allows using the local Strapi instance (the same project that the data transfer engine is being run from) as a data source.
|
||||
|
||||
Creating a local Strapi data provider requires passing in an initialized `strapi` server object to interact with that server's Entity Service and Query Engine to manage the data. Therefore if the local Strapi project cannot be started (due to errors), the providers cannot be used.
|
||||
|
||||
**Important**: When a transfer completes, the `strapi` object passed in is shut down automatically based on the `autoDestroy` option. If you are running a transfer via an external script, it is recommended to use `autoDestroy: true` to ensure it is shut down properly, but if you are running a transfer within a currently running Strapi instance you should set `autoDestroy: false` or your Strapi instance will be shut down at the end of the transfer.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Source
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Local Strapi Source Provider
|
||||
|
||||
This provider will retrieve data from an initialized `strapi` instance using its Entity Service and Query Engine.
|
||||
|
||||
## Provider Options
|
||||
|
||||
The accepted options are defined in `ILocalFileSourceProviderOptions`.
|
||||
|
||||
```typescript
|
||||
getStrapi(): Strapi.Strapi | Promise<Strapi.Strapi>; // return an initialized instance of Strapi
|
||||
|
||||
autoDestroy?: boolean; // shut down the instance returned by getStrapi() at the end of the transfer
|
||||
```
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Destination
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Local Strapi Destination Provider
|
||||
|
||||
This provider will insert data into an initialized `strapi` instance using its Entity Service and Query Engine.
|
||||
|
||||
## Provider Options
|
||||
|
||||
The accepted options are defined in `ILocalFileSourceProviderOptions`.
|
||||
|
||||
```typescript
|
||||
getStrapi(): Strapi.Strapi | Promise<Strapi.Strapi>; // return an initialized instance of Strapi
|
||||
autoDestroy?: boolean; // shut down the instance returned by getStrapi() at the end of the transfer
|
||||
restore?: restore.IRestoreOptions; // the options to use when strategy is 'restore'
|
||||
strategy: 'restore'; // conflict management strategy; only the restore strategy is available at this time
|
||||
```
|
||||
|
||||
`strategy` defines the conflict management strategy used. Currently, only `"restore"` is available as an option.
|
||||
|
||||
### Restore
|
||||
|
||||
A conflict management strategy of "restore" deletes all existing Strapi data before a transfer to avoid any conflicts.
|
||||
|
||||
The following restore options are available:
|
||||
|
||||
```typecript
|
||||
export interface IRestoreOptions {
|
||||
assets?: boolean; // delete media library files before transfer
|
||||
configuration?: {
|
||||
webhook?: boolean; // delete webhooks before transfer
|
||||
coreStore?: boolean; // delete core store before transfer
|
||||
};
|
||||
entities?: {
|
||||
include?: string[]; // only delete these stage entities before transfer
|
||||
exclude?: string[]; // exclude these stage entities from deletion
|
||||
filters?: ((contentType: ContentTypeSchema) => boolean)[]; // custom filters to exclude a content type from deletion
|
||||
params?: { [uid: string]: unknown }; // params object passed to deleteMany before transfer for custom deletions
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Rollbacks
|
||||
|
||||
This local Strapi destination provider automatically provides a rollback mechanism on error.
|
||||
|
||||
For Strapi data, that is done with a database transaction wrapped around the restore and the insertion of data and committing on succes and rolling back on failure.
|
||||
|
||||
For Strapi assets (ie, the media library files) this is done by attempting to temporarily move the existing assets to a backup directory to `uploads_backup_{timestamp}`, and then deleting it on success, or deleting the failed import files and putting the backup back into place on failure. In some cases of failure, it may be impossible to move the backup files back into place, so you will need to manually restore the backup assets files.
|
||||
|
||||
Note: Because of the need for write access, environments without filesystem permissions to move the assets folder (common for virtual environments where /uploads is mounted as a read-only drive) will be unable to include assets in a transfer and the asset stage must be excluded in order to run the transfer.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Local Strapi Providers",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Overview
|
||||
tags:
|
||||
- experimental
|
||||
- providers
|
||||
- import
|
||||
- export
|
||||
- data-transfer
|
||||
---
|
||||
|
||||
# Remote Strapi Providers
|
||||
|
||||
Remote Strapi providers connect to an instance of Strapi over a network using a websocket.
|
||||
|
||||
Internally, the remote Strapi providers map websocket requests to a local Strapi provider of the instance it is running in.
|
||||
|
||||
In order to use remote transfer providers, the remote Strapi server must have a value for `transfer.token.salt` configured in `config/admin.js` and the remote transfer feature must not be disabled.
|
||||
|
||||
## Disabling Remote Transfers
|
||||
|
||||
The remote transfer feature of a server can be completely disabled by setting the server config value:
|
||||
|
||||
```javascript
|
||||
// in config/server.js
|
||||
{
|
||||
transfer: {
|
||||
remote: {
|
||||
enabled: false;
|
||||
}
|
||||
}
|
||||
// ...the rest of your server config
|
||||
}
|
||||
```
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
---
|
||||
title: Websocket
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# WebSocket
|
||||
|
||||
## Websocket Messages / Dispatcher
|
||||
|
||||
The remote websocket server only accepts specific websocket messages which we call transfer commands. These commands must also be sent in a specific order, and an error messages will be returned if an unexpected message is received by the server.
|
||||
|
||||
A message dispatcher object should be created to send messages to the server. See `packages/core/data-transfer/src/strapi/providers/utils.ts` for more inofrmation on the dispatcher.
|
||||
|
||||
The dispatcher includes
|
||||
|
||||
### dispatchCommand
|
||||
|
||||
Accepts "commands" used for opening and closing a transfer.
|
||||
|
||||
Allows the following `command` values:
|
||||
|
||||
- `init`: for initializing a connection. Returns a transferID that must be sent with all future messages in this transfer
|
||||
- `end`: for ending a connection
|
||||
|
||||
### dispatchTransferStep
|
||||
|
||||
Used for switching between stages of a transfer and streaming the actual data of a transfer.
|
||||
|
||||
Accepts the following `action` values:
|
||||
|
||||
- `start`: sent with a `step` value for the name of the step/stage
|
||||
- any number of `stream`: sent with a `step` value and the `data` being sent (ie, an array of entities)
|
||||
- `end`: sent with a `step` value for the step being ended
|
||||
|
||||
### dispatchTransferAction
|
||||
|
||||
Used for triggering 'actions' on the server equivalent to the local providers.
|
||||
|
||||
- `bootstrap`
|
||||
- `getMetadata`
|
||||
- `beforeTransfer`
|
||||
- `getSchemas`
|
||||
- `rollback` (destination only)
|
||||
- `close`: for completing a transfer (but doesn't close the connection)
|
||||
|
||||
See `packages/core/data-transfer/dist/strapi/remote/handlers/pull.d.ts` and `packages/core/data-transfer/dist/strapi/remote/handlers/push.d.ts` for complete and precise definitions of the messages that must be sent.
|
||||
|
||||
## Transfer Lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Dispatcher
|
||||
participant WebSocket
|
||||
participant Server
|
||||
|
||||
Note over Client,Server: Connection Phase
|
||||
Client->>WebSocket: Create WebSocket Connection
|
||||
Note right of WebSocket: /admin/transfer/runner/pull or push
|
||||
Note right of WebSocket: Authorization: Bearer <transfer_token>
|
||||
|
||||
Client->>Dispatcher: Create Message Dispatcher
|
||||
|
||||
Note over Client,Server: Initialization Phase
|
||||
Dispatcher->>Server: dispatchCommand("init")
|
||||
Server->>Dispatcher: Returns transferID
|
||||
Note right of Dispatcher: All future messages include transferID
|
||||
|
||||
Note over Client,Server: Transfer Actions Phase
|
||||
Dispatcher->>Server: dispatchTransferAction("bootstrap")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
Dispatcher->>Server: dispatchTransferAction("getMetadata")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
Dispatcher->>Server: dispatchTransferAction("beforeTransfer")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
Dispatcher->>Server: dispatchTransferAction("getSchemas")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
Note over Client,Server: Transfer Steps Phase
|
||||
loop Each Step (schemas, entities, assets, links, config)
|
||||
Dispatcher->>Server: dispatchTransferStep(step, "start")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
loop Data Streaming
|
||||
Dispatcher->>Server: dispatchTransferStep(step, "stream", data)
|
||||
alt Success
|
||||
Server->>Dispatcher: Response
|
||||
else Timeout
|
||||
Note over Dispatcher: Wait retryMessageTimeout
|
||||
Dispatcher->>Server: Retry Message
|
||||
end
|
||||
end
|
||||
|
||||
Dispatcher->>Server: dispatchTransferStep(step, "end")
|
||||
Server->>Dispatcher: Response
|
||||
end
|
||||
|
||||
Note over Client,Server: Closing Phase
|
||||
Dispatcher->>Server: dispatchTransferAction("close")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
Dispatcher->>Server: dispatchCommand("end")
|
||||
Server->>Dispatcher: Response
|
||||
|
||||
Client->>WebSocket: Close Connection
|
||||
```
|
||||
|
||||
### 1. Connection Phase
|
||||
|
||||
When the data transfer feature is enabled for a Strapi server (an `admin.transfer.token.salt` config value has been set and `server.transfer.remote.enabled` is not set to false), Strapi will create websocket servers available on the routes `/admin/transfer/runner/pull` and `/admin/transfer/runner/push`.
|
||||
|
||||
**Create WebSocket Connection**
|
||||
|
||||
- Opening a websocket connection on those routes requires a valid transfer token as a bearer token in the Authorization header.:
|
||||
```
|
||||
Authorization: Bearer <transfer_token>
|
||||
```
|
||||
- Server validates the token and establishes the connection
|
||||
|
||||
Please see the `bootstrap()` method of the remote providers for an example of how to make the initial connection to the Strapi websocket.
|
||||
|
||||
**Event Listener Attachment**
|
||||
Immediately after WebSocket creation, the following listeners are attached:
|
||||
|
||||
- `'open'`: Handles successful connection establishment
|
||||
- `'close'`: Manages connection termination
|
||||
- `'error'`: Handles connection and transmission errors
|
||||
- `'message'`: Processes incoming messages from server
|
||||
|
||||
### 2. Initialization Phase
|
||||
|
||||
Client sends initial command to establish transfer, server responds with unique transferID.
|
||||
|
||||
```javascript
|
||||
const transferID = await dispatcher.dispatchCommand('init');
|
||||
// All subsequent messages must include this transferID
|
||||
```
|
||||
|
||||
All subsequent messages must include this transferID
|
||||
|
||||
### 3. Transfer Actions Phase
|
||||
|
||||
Sequential actions executed through `dispatchTransferAction`:
|
||||
|
||||
1. `bootstrap`: Initializes transfer environment
|
||||
2. `getMetadata`: Retrieves transfer metadata
|
||||
3. `beforeTransfer`: Performs pre-transfer preparations
|
||||
4. `getSchemas`: Retrieves content type schemas, used for validation between source and destination
|
||||
|
||||
### 4. Transfer Steps Phase
|
||||
|
||||
The main phase where actual data transfer occurs, processing different types of data (schemas, entities, assets, links, configuration) in sequence:
|
||||
|
||||
**Stage Start**
|
||||
|
||||
```javascript
|
||||
dispatchTransferStep(action: "start", step)
|
||||
```
|
||||
|
||||
**Data Streaming**
|
||||
|
||||
```javascript
|
||||
dispatchTransferStep(action:"stream", step, data)
|
||||
```
|
||||
|
||||
**Stage Completion**
|
||||
|
||||
```javascript
|
||||
dispatchTransferStep(action:"end", step)
|
||||
```
|
||||
|
||||
**Retry Mechanism**
|
||||
During data transfer:
|
||||
|
||||
- If server response not received within `retryMessageTimeout`
|
||||
- System attempts retry up to `retryMessageMaxRetries` times
|
||||
- Automatic retry on timeout
|
||||
- Transfer aborts if max retries exceeded
|
||||
|
||||
### 5.Closing Phase
|
||||
|
||||
**Cleanup Actions**
|
||||
|
||||
1. Send close action:
|
||||
```javascript
|
||||
dispatchTransferAction('close');
|
||||
```
|
||||
2. Send end command:
|
||||
```javascript
|
||||
dispatchCommand({ command: 'end', params: { transferID } });
|
||||
```
|
||||
|
||||
**Connection Termination**
|
||||
|
||||
1. Remove event listeners in reverse order:
|
||||
- Remove `message` listener
|
||||
- Remove `error` listener
|
||||
- Remove `open` listener
|
||||
- Remove `close` listener
|
||||
2. Close WebSocket connection
|
||||
|
||||
## Message Timeouts and Retries
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Init
|
||||
Init --> Ready: Connection Established
|
||||
Ready --> Dispatching: Send Message
|
||||
Dispatching --> WaitingResponse: Message Sent
|
||||
WaitingResponse --> Ready: Response Received
|
||||
WaitingResponse --> Retrying: Timeout
|
||||
Retrying --> WaitingResponse: Retry Attempt
|
||||
Retrying --> Error: Max Retries Exceeded
|
||||
Error --> [*]
|
||||
|
||||
note right of WaitingResponse
|
||||
Configurable timeout
|
||||
via retryMessageTimeout
|
||||
end note
|
||||
|
||||
note right of Retrying
|
||||
Limited by
|
||||
retryMessageMaxRetries
|
||||
end note
|
||||
```
|
||||
|
||||
Because the transfer relies on a message->response protocol, if the websocket server is unable to send a reply, for example due to network instability, the connection would halt. For this reason, each provider's options includes `retryMessageOptions` which attempt to resend a message after a given timeout is reached and a max retry option to abort the transfer after a given number of failed retry attempts.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Source
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Strapi Remote Source Provider
|
||||
|
||||
The Strapi remote source provider connects to a remote Strapi websocket server and sends messages to move between stages and pull data.
|
||||
|
||||
## Provider Options
|
||||
|
||||
The remote source provider accepts `url`, `auth`, and `retryMessageOptions` described below.
|
||||
|
||||
```typescript
|
||||
interface ITransferTokenAuth {
|
||||
type: 'token';
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IRemoteStrapiDestinationProviderOptions
|
||||
extends Pick<ILocalStrapiDestinationProviderOptions, 'restore' | 'strategy'> {
|
||||
url: URL;
|
||||
auth?: ITransferTokenAuth;
|
||||
retryMessageOptions?: {
|
||||
retryMessageTimeout: number; // milliseconds to wait for a response from a message
|
||||
retryMessageMaxRetries: number; // max number of retries for a message before aborting transfer
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Note: `url` must include the protocol `https` or `http` which will then be converted to `wss` or `ws` to make the connection. A secure connection is strongly recommended, especially given the high access level that the transfer token provides.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Destination
|
||||
tags:
|
||||
- providers
|
||||
- data-transfer
|
||||
- experimental
|
||||
---
|
||||
|
||||
# Strapi Remote Destination Provider
|
||||
|
||||
The Strapi remote destination provider connects to a remote Strapi websocket server and sends messages to move between stages and push data.
|
||||
|
||||
## Provider Options
|
||||
|
||||
The remote destination provider accepts the same `restore` and `strategy` options from local Strapi destination provider, plus `url`, `auth`, and `retryMessageOptions` described below.
|
||||
|
||||
```typescript
|
||||
interface ITransferTokenAuth {
|
||||
type: 'token'; // the name of the auth strategy
|
||||
token: string; // the transfer token
|
||||
}
|
||||
|
||||
export interface IRemoteStrapiDestinationProviderOptions
|
||||
extends Pick<ILocalStrapiDestinationProviderOptions, 'restore' | 'strategy'> {
|
||||
url: URL; // the url of the remote Strapi admin
|
||||
auth?: ITransferTokenAuth;
|
||||
retryMessageOptions?: {
|
||||
retryMessageTimeout: number; // milliseconds to wait for a response from a message
|
||||
retryMessageMaxRetries: number; // max number of retries for a message before aborting transfer
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Note: `url` must include the protocol `https` or `http` which will then be converted to `wss` or `ws` to make the connection. A secure connection is strongly recommended, especially given the high access level that the transfer token provides.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Remote Strapi Providers",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Providers",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"label": "Data Transfer",
|
||||
"collapsible": true,
|
||||
"collapsed": true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user