strapi new version
Build and Push Docker Images / build-and-push (push) Failing after 1m54s

This commit is contained in:
2025-09-29 16:13:41 +02:00
parent a4f1ff23a8
commit af60fe7ff9
5275 changed files with 568757 additions and 18089 deletions
+83
View File
@@ -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
+244
View File
@@ -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`.
+15
View File
@@ -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
View File
@@ -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 youre 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.
+94
View File
@@ -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
+10
View File
@@ -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,
},
};
+21
View File
@@ -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*
+29
View File
@@ -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',
}}
>
&lt;{children}&gt;
</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
}
@@ -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
@@ -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.
@@ -0,0 +1,5 @@
{
"label": "Frontend",
"collapsible": true,
"collapsed": true
}
@@ -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)
@@ -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} />
```
@@ -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
}
@@ -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.
@@ -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:
- Shortlived 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 shortlived 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 longlived `jwt` configured by `plugin::users-permissions.jwt`.
- `refresh`: uses `SessionManager` to issue shortlived 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
}
@@ -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
}
@@ -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 (/* ... */);
};
```
@@ -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>;
};
```
@@ -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.
@@ -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).
@@ -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.
@@ -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.
@@ -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.
@@ -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;
};
};
};
}
```
@@ -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.
@@ -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.
@@ -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.
@@ -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).
@@ -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.
@@ -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.
@@ -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
};
```
@@ -0,0 +1,5 @@
{
"label": "File Providers",
"collapsible": true,
"collapsed": true
}
@@ -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.
@@ -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
```
@@ -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.
@@ -0,0 +1,5 @@
{
"label": "Local Strapi Providers",
"collapsible": true,
"collapsed": true
}
@@ -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
}
```
@@ -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.
@@ -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.
@@ -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.
@@ -0,0 +1,5 @@
{
"label": "Remote Strapi Providers",
"collapsible": true,
"collapsed": true
}
@@ -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