Merge branch 'master' of github.com:boazpoolman/strapi-plugin-config-sync

pull/94/head
Alexander Engel 2024-10-14 12:07:03 -07:00
commit d735151d9b
67 changed files with 11957 additions and 2616 deletions

View File

@ -26,6 +26,14 @@
"strapi": true
},
"rules": {
"import/no-unresolved": [2, {
"ignore": [
"@strapi/strapi/admin",
"@strapi/icons/symbols",
"@strapi/admin/strapi-admin"
]
}],
"template-curly-spacing" : "off",
"indent" : "off",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -12,7 +12,7 @@ jobs:
uses: actions/setup-node@v3
with:
always-auth: true
node-version: 16
node-version: 18
cache: 'yarn'
registry-url: 'https://registry.npmjs.org/'
- name: Install dependencies
@ -31,3 +31,4 @@ jobs:
with:
commit_message: 'chore: Bump version to ${{ steps.get_version.outputs.VERSION }}'
file_pattern: 'package.json'
branch: master

View File

@ -8,6 +8,7 @@ on:
branches:
- master
- develop
- beta
jobs:
lint:
@ -15,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14, 16, 18]
node: [18, 20]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
@ -32,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14, 16, 18]
node: [18, 20]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
@ -40,9 +41,13 @@ jobs:
node-version: ${{ matrix.node }}
cache: 'yarn'
- name: Install dependencies plugin
run: yarn --frozen-lockfile --unsafe-perm --production
run: yarn --no-lockfile --unsafe-perm
- name: Push the package to yalc
run: yarn build
- name: Add yalc package to the playground
run: yarn playground:yalc-add
- name: Install dependencies playground
run: yarn playground:install --frozen-lockfile --unsafe-perm
run: cd playground && yarn install --unsafe-perm
- name: Build playground
run: yarn playground:build
- name: Run test
@ -60,7 +65,7 @@ jobs:
# runs-on: ubuntu-latest
# strategy:
# matrix:
# node: [14, 16, 18]
# node: [16, 18, 20]
# steps:
# - uses: actions/checkout@v2
# - uses: actions/setup-node@v2

View File

@ -4,60 +4,55 @@ We want this community to be friendly and respectful to each other. Please follo
## Development Workflow
To get started with the project, make sure you have a local instance of Strapi running.
See the [Strapi docs](https://github.com/strapi/strapi#getting-started) on how to setup a Strapi project.
This plugin provides a local development instance of Strapi to develop it's features. We call this instance `playground` and it can be found in the playground folder in the root of the project. For that reason it is not needed to have your own Strapi instance running to work on this plugin. Just clone the repo and you're ready to go!
#### 1. Fork the [repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
#### 1. Fork the [repository](https://github.com/pluginpal/strapi-plugin-config-sync)
[Go to the repository](https://github.com/boazpoolman/strapi-plugin-config-sync) and fork it to your own GitHub account.
[Go to the repository](https://github.com/pluginpal/strapi-plugin-config-sync) and fork it to your own GitHub account.
#### 2. Clone from your repository into the plugins folder
#### 2. Clone the forked repository
```bash
cd YOUR_STRAPI_PROJECT/src/plugins
git clone git@github.com:YOUR_USERNAME/strapi-plugin-config-sync.git config-sync
git clone git@github.com:YOUR_USERNAME/strapi-plugin-config-sync.git
```
#### 3. Install the dependencies
Go to the plugin and install it's dependencies.
Go to the folder and install the dependencies
```bash
cd YOUR_STRAPI_PROJECT/src/plugins/config-sync/ && yarn plugin:install
cd strapi-plugin-config-sync && yarn install
```
#### 4. Enable the plugin
#### 4. Install the playground dependencies
Add the following lines to the `config/plugins.js` file in your Strapi project.
```
const path = require('path');
// ...
{
'config-sync': {
enabled: true,
resolve: path.resolve(__dirname, '../src/plugins/config-sync'),
},
}
```
#### 5. Rebuild your Strapi project
Rebuild your strapi project to build the admin part of the plugin.
Run this in the root of the repository
```bash
cd YOUR_STRAPI_PROJECT && yarn build
yarn playground:install
```
#### 6. Running the administration panel in development mode
#### 5. Run the compiler of the plugin
**Start the administration panel server for development**
We use `yalc` to publish the package to a local registry. Run the following command o watch for changes and push to `yalc` every time a change is made:
```bash
cd YOUR_STRAPI_PROJECT && yarn develop --watch-admin
yarn develop
```
The administration panel will be available at http://localhost:8080/admin
#### 6. Start the playground instance
Leave the watcher running, open up a new terminal window and browse back to the root of the plugin repo. Run the following command:
```bash
yarn playground:develop
```
This will start the playground instance that will have the plugin installed from the `yalc` registry. Browse to http://localhost:1337 and create a test admin user to log in to the playground.
#### 7. Start your contribution!
You can now start working on your contribution. If you had trouble setting up this testing environment please feel free to report an issue on Github.
### Commit message convention
@ -82,12 +77,10 @@ The `package.json` file contains various scripts for common tasks:
- `yarn eslint`: lint files with ESLint.
- `yarn eslint:fix`: auto-fix ESLint issues.
- `yarn test:unit`: run unit tests with Jest.
- `yarn test:integration`: run integration tests with Jest.
### Sending a pull request
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
When you're sending a pull request:
- Prefer small pull requests focused on one change.

371
README.md
View File

@ -1,7 +1,9 @@
<div align="center">
<h1>Strapi config-sync plugin</h1>
<p style="margin-top: 0;">This plugin is a multi-purpose tool to manage your Strapi database records through JSON files. Mostly used to version control <a href="#-config-types">config data</a> for automated deployment, automated tests and data sharing for collaboration purposes.</p>
<p style="margin-top: 0;">This plugin is a multi-purpose tool to manage your Strapi database records through JSON files. Mostly used to version controlconfig data for automated deployment, automated tests and data sharing for collaboration purposes.</p>
<a href="https://docs.pluginpal.io/config-sync">Read the documentation</a>
<p>
<a href="https://www.npmjs.org/package/strapi-plugin-config-sync">
@ -19,19 +21,6 @@
</p>
</div>
## Table of Contents
- [Features](#-features)
- [Installation](#-installation)
- [Requirements](#-requirements)
- [Motivation](#-motivation)
- [CLI](#-command-line-interface-cli)
- [Admin panel](#%EF%B8%8F-admin-panel-gui)
- [Usage / Workflow](#%EF%B8%8F-usage--workflow)
- [Config types](#-config-types)
- [Naming convention](#-naming-convention)
- [Settings](#-settings)
## ✨ Features
- **CLI** - `config-sync` CLI for syncing the config from the command line
@ -42,9 +31,9 @@
- **Exclusion** - Exclude single config entries or all entries of a given type
- **Diff viewer** - A git-style diff viewer to inspect the config changes
## ⏳ Installation
## ⏳ Getting started
Install the plugin in your Strapi project.
[Read the Getting Started tutorial](https://docs.pluginpal.io/config-sync) or follow the steps below:
```bash
# using yarn
@ -81,353 +70,15 @@ npm run develop
The **Config Sync** plugin should now appear in the **Settings** section of your Strapi app.
To start tracking your config changes you have to make the first export. This will dump all your configuration data to the `/config/sync` directory. You can export either through [the CLI](#-command-line-interface-cli) or [Strapi admin panel](#%EF%B8%8F-admin-panel-gui)
To start tracking your config changes you have to make the first export. This will dump all your configuration data to the `/config/sync` directory. You can export either through [the CLI](https://docs.pluginpal.io/config-sync/cli) or [Strapi admin panel](https://docs.pluginpal.io/config-sync/admin-gui)
Enjoy 🎉
## 🖐 Requirements
## 📓 Documentation
Complete installation requirements are the exact same as for Strapi itself and can be found in the [Strapi documentation](https://strapi.io/documentation).
**Supported Strapi versions**:
- Strapi 4.3.2 (recently tested)
- Strapi ^4.x (use `strapi-plugin-config-sync@^1.0.0`)
- Strapi ^3.4.x (use `strapi-plugin-config-sync@0.1.6`)
## 💡 Motivation
In Strapi we come across what I would call config types. These are models of which the records are stored in our database, just like content types. Though the big difference here is that your code ofter relies on the database records of these types.
Having said that, it makes sense that these records can be exported, added to git, and be migrated across environments. This way we can make sure we have all the data our code relies on, on each environment.
Examples of these types are:
- Admin roles _(admin::role)_
- User roles _(plugin::users-permissions.role)_
- Admin settings _(strapi::core-store)_
- I18n locale _(plugin::i18n.locale)_
This plugin gives you the tools to sync this data. You can export the data as JSON files on one env, and import them on every other env. By writing this data as JSON files you can easily track them in your version control system (git).
_With great power comes great responsibility - Spider-Man_
## 🔌 Command line interface (CLI)
Add the `config-sync` command as a script to the `package.json` of your Strapi project:
```
"scripts": {
// ...
"cs": "config-sync"
},
```
You can now run all the `config-sync` commands like this:
```bash
# using yarn
yarn cs --help
# using npm
npm run cs --help
```
### ⬆️ Import ⬇️ Export
> _Command:_ `import` _Alias:_ `i`
>
> _Command:_ `export` _Alias:_ `e`
These commands are used to sync the config in your Strapi project.
_Example:_
```bash
# using yarn
yarn cs import
yarn cs export
# using npm
npm run cs import
npm run cs export
```
##### Flag: `-y`, `--yes`
Use this flag to skip the confirm prompt and go straight to syncing the config.
```bash
[command] --yes
```
##### Flag: `-t`, `--type`
Use this flag to specify the type of config you want to sync.
```bash
[command] --type user-role
```
##### Flag: `-p`, `--partial`
Use this flag to sync a specific set of configs by giving the CLI a comma-separated string of config names.
```bash
[command] --partial user-role.public,i18n-locale.en
```
##### Flag: `-f`, `--force`
If you're using the soft setting to gracefully import config, you can use this flag to ignore the setting for the current command and forcefully import all changes anyway.
```bash
[command] --force
```
### ↔️ Diff
> _Command:_ `diff` | _Alias:_ `d`
This command is used to see the difference between the config as found in the sync directory, and the config as found in the database.
_Example:_
```bash
# using yarn
yarn cs diff
# using npm
npm run cs diff
```
##### Argument: `<single>`
Add a single config name as the argument of the `diff` command to see the difference of that single file in a git-style diff viewer.
_Example:_
```bash
# using yarn
yarn cs diff user-role.public
# using npm
npm run cs diff user-role.public
```
## 🖥️ Admin panel (GUI)
This plugin ships with a React app which can be accessed from the settings page in Strapi admin panel. On this page you can pretty much do the same as you can from the CLI. You can import, export and see the difference between the config as found in the sync directory, and the config as found in the database.
**Pro tip:**
By clicking on one of the items in the diff table you can see the exact difference between sync dir and database in a git-style diff viewer.
<img src="https://raw.githubusercontent.com/boazpoolman/strapi-plugin-config-sync/master/.github/config-diff.png" alt="Config diff in admin" />
## ⌨️ Usage / Workflow
This plugin works best when you use `git` for the version control of your Strapi project.
_The following workflows are assuming you're using `git`._
### Intro
All database records tracked with this plugin will be exported to JSON files. Once exported each change to the file or the record will be tracked. Meaning you can now do one of two things:
- Change the file(s), and run an import. You have now imported from filesystem -> database.
- Change the record(s), and run an export. You have now exported from database -> filesystem.
### Local development
When building a new feature locally for your Strapi project you'd use the following workflow:
- Build the feature.
- Export the config.
- Commit and push the files to git.
### Deployment
When deploying the newly created feature - to either a server, or a co-worker's machine - you'd use the following workflow:
- Pull the latest file changes to the environment.
- (Re)start your Strapi instance.
- Import the config.
### Production deployment
The production deployment will be the same as a regular deployment. You just have to be careful before running the import. Ideally making sure the are no open changes before you pull the new code to the environment.
## 🚀 Config types
By default the plugin will track 4 (official) types.
To track your own custom types you can register them by setting some plugin config.
### Default types
These 4 types are by default registered in the sync process.
#### Admin role
> Config name: `admin-role` | UID: `code` | Query string: `admin::role`
#### User role
> Config name: `user-role` | UID: `type` | Query string: `plugin::users-permissions.role`
#### Core store
> Config name: `core-store` | UID: `key` | Query string: `strapi::core-store`
#### I18n locale
> Config name: `i18n-locale` | UID: `code` | Query string: `plugin::i18n.locale`
### Custom types
Your custom types can be registered through the `customTypes` plugin config. This is a setting that can be set in the `config/plugins.js` file in your project.
_Read more about the `config/plugins.js` file [here](#-settings)._
You can register a type by giving the `customTypes` array an object which contains at least the following 3 properties:
```
customTypes: [{
configName: 'webhook',
queryString: 'webhook',
uid: 'name',
}],
```
_The example above will register the Strapi webhook type._
#### Config name
The name of the config type. This value will be used as the first part of the filename for all config of this type. It should be unique from the other types and is preferably written in kebab-case.
###### Key: `configName`
> `required:` YES | `type:` string
#### Query string
This is the query string of the type. Each type in Strapi has its own query string you can use to programatically preform CRUD actions on the entries of the type. Often for custom types in Strapi the format is something like `api::custom-api.custom-type`.
###### Key: `queryString`
> `required:` YES | `type:` string
#### UID
The UID represents a field on the registered type. The value of this field will act as a unique identifier to identify the entries across environments. Therefore it should be unique and preferably un-editable after initial creation.
Mind that you can not use an auto-incremental value like the `id` as auto-increment does not play nice when you try to match entries across different databases.
If you do not have a single unique value, you can also pass in a array of keys for a combined uid key. This is for example the case for all content types which use i18n features (An example config would be `uid: ['productId', 'locale']`).
###### Key: `uid`
> `required:` YES | `type:` string | string[]
#### JSON fields
This property can accept an array of field names from the type. It is meant to specify the JSON fields on the type so the plugin can better format the field values when calculating the config difference.
###### Key: `jsonFields`
> `required:` NO | `type:` array
## 🔍 Naming convention
All the config files written in the sync directory have the same naming convention. It goes as follows:
[config-type].[identifier].json
- `config-type` - Corresponds to the `configName` of the config type.
- `identifier` - Corresponds to the value of the `uid` field of the config type.
## 🔧 Settings
The settings of the plugin can be overridden in the `config/plugins.js` file.
In the example below you can see how, and also what the default settings are.
##### `config/plugins.js`:
module.exports = ({ env }) => ({
// ...
'config-sync': {
enabled: true,
config: {
syncDir: "config/sync/",
minify: false,
soft: false,
importOnBootstrap: false,
customTypes: [],
excludedTypes: [],
excludedConfig: [
"core-store.plugin_users-permissions_grant",
"core-store.plugin_upload_metrics",
"core-store.strapi_content_types_schema",
"core-store.ee_information",
],
},
},
});
### Sync dir
The path for reading and writing the sync files.
###### Key: `syncDir`
> `required:` YES | `type:` string | `default:` `config/sync/`
### Minify
When enabled all the exported JSON files will be minified.
###### Key: `minify`
> `required:` NO | `type:` bool | `default:` `false`
### Soft
When enabled the import action will be limited to only create new entries. Entries to be deleted, or updated will be skipped from the import process and will remain in it's original state.
###### Key: `soft`
> `required:` NO | `type:` bool | `default:` `false`
### Import on bootstrap
Allows you to let the config be imported automaticly when strapi is bootstrapping (on `strapi start`). This setting can't be used locally and should be handled very carefully as it can unintendedly overwrite the changes in your database. **PLEASE USE WITH CARE**.
###### Key: `importOnBootstrap`
> `required:` NO | `type:` bool | `default:` `false`
### Custom types
With this setting you can register your own custom config types. This is an array which expects objects with at least the `configName`, `queryString` and `uid` properties. Read more about registering custom types in the [Custom config types](#custom-types) documentation.
###### Key: `customTypes`
> `required:` NO | `type:` array | `default:` `[]`
### Excluded types
This setting will exclude all the config from a given type from the syncing process. The config types are specified by the `configName` of the type.
For example:
```
excludedTypes: ['admin-role']
```
###### Key: `excludedTypes`
> `required:` NO | `type:` array | `default:` `[]`
### Excluded config
Specify the names of configs you want to exclude from the syncing process. By default the API tokens for users-permissions, which are stored in core_store, are excluded. This setting expects the config names to comply with the naming convention.
###### Key: `excludedConfig`
> `required:` NO | `type:` array | `default:` `['core-store.plugin_users-permissions_grant', 'core-store.plugin_upload_metrics', 'core-store.strapi_content_types_schema', 'core-store.ee_information',]`
See our dedicated [repository](https://github.com/pluginpal/docs) for all of PluginPal's documentation, or view the Config Sync documentation live:
- [Config Sync documentation](https://docs.pluginpal.io/config-sync)
## 🤝 Contributing
@ -439,8 +90,10 @@ Give a star if this project helped you.
## 🔗 Links
- [PluginPal marketplace](https://www.pluginpal.io/plugin/config-sync)
- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
- [Strapi marketplace](https://market.strapi.io/plugins/strapi-plugin-config-sync)
## 🌎 Community support
@ -449,4 +102,4 @@ Give a star if this project helped you.
## 📝 Resources
- [MIT License](LICENSE.md)
- [MIT License](https://github.com/pluginpal/strapi-plugin-config-sync/blob/master/LICENSE.md)

View File

@ -1,57 +0,0 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import { Button } from '@strapi/design-system';
import { Map } from 'immutable';
import { useNotification } from '@strapi/helper-plugin';
import ConfirmModal from '../ConfirmModal';
import { downloadZip, exportAllConfig, importAllConfig } from '../../state/actions/Config';
const ActionButtons = () => {
const dispatch = useDispatch();
const toggleNotification = useNotification();
const [modalIsOpen, setModalIsOpen] = useState(false);
const [actionType, setActionType] = useState('');
const partialDiff = useSelector((state) => state.getIn(['config', 'partialDiff'], Map({}))).toJS();
const closeModal = () => {
setActionType('');
setModalIsOpen(false);
};
const openModal = (type) => {
setActionType(type);
setModalIsOpen(true);
};
return (
<ActionButtonsStyling>
<Button disabled={isEmpty(partialDiff)} onClick={() => openModal('import')}>Import</Button>
<Button disabled={isEmpty(partialDiff)} onClick={() => openModal('export')}>Export</Button>
<Button onClick={() => dispatch(downloadZip(toggleNotification))}>Download Config</Button>
{!isEmpty(partialDiff) && (
<h4 style={{ display: 'inline' }}>{Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"}</h4>
)}
<ConfirmModal
isOpen={modalIsOpen}
onClose={closeModal}
type={actionType}
onSubmit={(force) => actionType === 'import' ? dispatch(importAllConfig(partialDiff, force, toggleNotification)) : dispatch(exportAllConfig(partialDiff, toggleNotification))}
/>
</ActionButtonsStyling>
);
};
const ActionButtonsStyling = styled.div`
padding: 10px 0 20px 0;
display: flex;
align-items: center;
> button {
margin-right: 10px;
}
`;
export default ActionButtons;

View File

@ -0,0 +1,57 @@
import React from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import { Button, Typography } from '@strapi/design-system';
import { Map } from 'immutable';
import { getFetchClient, useNotification } from '@strapi/strapi/admin';
import { useIntl } from 'react-intl';
import ConfirmModal from '../ConfirmModal';
import { exportAllConfig, importAllConfig } from '../../state/actions/Config';
const ActionButtons = () => {
const { post, get } = getFetchClient();
const dispatch = useDispatch();
const { toggleNotification } = useNotification();
const partialDiff = useSelector((state) => state.getIn(['config', 'partialDiff'], Map({}))).toJS();
const { formatMessage } = useIntl();
return (
<ActionButtonsStyling>
<ConfirmModal
type="import"
trigger={(
<Button disabled={isEmpty(partialDiff)}>
{formatMessage({ id: 'config-sync.Buttons.Import' })}
</Button>
)}
onSubmit={(force) => dispatch(importAllConfig(partialDiff, force, toggleNotification, formatMessage, post, get))}
/>
<ConfirmModal
type="export"
trigger={(
<Button disabled={isEmpty(partialDiff)}>
{formatMessage({ id: 'config-sync.Buttons.Export' })}
</Button>
)}
onSubmit={(force) => dispatch(exportAllConfig(partialDiff, toggleNotification, formatMessage, post, get))}
/>
{!isEmpty(partialDiff) && (
<Typography variant="epsilon">{Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"}</Typography>
)}
</ActionButtonsStyling>
);
};
const ActionButtonsStyling = styled.div`
padding: 10px 0 20px 0;
display: flex;
align-items: center;
> button {
margin-right: 10px;
}
`;
export default ActionButtons;

View File

@ -1,48 +0,0 @@
import React from 'react';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
import {
ModalLayout,
ModalBody,
ModalHeader,
Grid,
GridItem,
Typography,
} from '@strapi/design-system';
const ConfigDiff = ({ isOpen, onClose, oldValue, newValue, configName }) => {
if (!isOpen) {
return null;
}
return (
<ModalLayout
onClose={onClose}
labelledBy="title"
>
<ModalHeader>
<Typography variant="omega" fontWeight="bold" textColor="neutral800">
Config changes for {configName}
</Typography>
</ModalHeader>
<ModalBody>
<Grid paddingBottom={4} style={{ textAlign: 'center' }}>
<GridItem col={6}>
<Typography variant="delta">Sync directory</Typography>
</GridItem>
<GridItem col={6}>
<Typography variant="delta">Database</Typography>
</GridItem>
</Grid>
<ReactDiffViewer
oldValue={JSON.stringify(oldValue, null, 2)}
newValue={JSON.stringify(newValue, null, 2)}
splitView
compareMethod={DiffMethod.WORDS}
/>
</ModalBody>
</ModalLayout>
);
};
export default ConfigDiff;

View File

@ -0,0 +1,48 @@
import React from 'react';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
import { useIntl } from 'react-intl';
import {
Modal,
Grid,
Typography,
} from '@strapi/design-system';
const ConfigDiff = ({ oldValue, newValue, configName, trigger }) => {
const { formatMessage } = useIntl();
return (
<Modal.Root>
<Modal.Trigger>
{trigger}
</Modal.Trigger>
<Modal.Content>
<Modal.Header>
<Typography variant="omega" fontWeight="bold" textColor="neutral800">
{formatMessage({ id: 'config-sync.ConfigDiff.Title' })} {configName}
</Typography>
</Modal.Header>
<Modal.Body>
<Grid.Root paddingBottom={4} style={{ textAlign: 'center' }}>
<Grid.Item col={6}>
<Typography variant="delta" style={{ width: '100%' }}>{formatMessage({ id: 'config-sync.ConfigDiff.SyncDirectory' })}</Typography>
</Grid.Item>
<Grid.Item col={6}>
<Typography variant="delta" style={{ width: '100%' }}>{formatMessage({ id: 'config-sync.ConfigDiff.Database' })}</Typography>
</Grid.Item>
</Grid.Root>
<Typography variant="pi">
<ReactDiffViewer
oldValue={JSON.stringify(oldValue, null, 2)}
newValue={JSON.stringify(newValue, null, 2)}
splitView
compareMethod={DiffMethod.WORDS}
/>
</Typography>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
};
export default ConfigDiff;

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Tr, Td, BaseCheckbox } from '@strapi/design-system';
import { Tr, Td, Checkbox, Typography } from '@strapi/design-system';
const CustomRow = ({ row, checked, updateValue }) => {
const CustomRow = ({ row, checked, updateValue, ...props }) => {
const { configName, configType, state, onClick } = row;
const stateStyle = (stateStr) => {
@ -34,6 +34,7 @@ const CustomRow = ({ row, checked, updateValue }) => {
return (
<Tr
{...props}
onClick={(e) => {
if (e.target.type !== 'checkbox') {
onClick(configType, configName);
@ -42,20 +43,20 @@ const CustomRow = ({ row, checked, updateValue }) => {
style={{ cursor: 'pointer' }}
>
<Td>
<BaseCheckbox
<Checkbox
aria-label={`Select ${configName}`}
value={checked}
onValueChange={updateValue}
checked={checked}
onCheckedChange={updateValue}
/>
</Td>
<Td>
<p>{configName}</p>
<Td onClick={(e) => props.onClick(e)}>
<Typography variant="omega">{configName}</Typography>
</Td>
<Td>
<p>{configType}</p>
<Td onClick={(e) => props.onClick(e)}>
<Typography variant="omega">{configType}</Typography>
</Td>
<Td>
<p style={stateStyle(state)}>{state}</p>
<Td onClick={(e) => props.onClick(e)}>
<Typography variant="omega" style={stateStyle(state)}>{state}</Typography>
</Td>
</Tr>
);

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux';
@ -9,7 +10,7 @@ import {
Tr,
Th,
Typography,
BaseCheckbox,
Checkbox,
Loader,
} from '@strapi/design-system';
@ -19,31 +20,32 @@ import NoChanges from '../NoChanges';
import ConfigListRow from './ConfigListRow';
import { setConfigPartialDiffInState } from '../../state/actions/Config';
const ConfigList = ({ diff, isLoading }) => {
const [openModal, setOpenModal] = useState(false);
const [originalConfig, setOriginalConfig] = useState({});
const [newConfig, setNewConfig] = useState({});
const [cName, setCname] = useState('');
const [rows, setRows] = useState([]);
const [checkedItems, setCheckedItems] = useState([]);
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const getConfigState = (configName) => {
if (
diff.fileConfig[configName]
&& diff.databaseConfig[configName]
) {
return 'Different';
return formatMessage({ id: 'config-sync.ConfigList.Different' });
} else if (
diff.fileConfig[configName]
&& !diff.databaseConfig[configName]
) {
return 'Only in sync dir';
return formatMessage({ id: 'config-sync.ConfigList.OnlyDir' });
} else if (
!diff.fileConfig[configName]
&& diff.databaseConfig[configName]
) {
return 'Only in DB';
return formatMessage({ id: 'config-sync.ConfigList.OnlyDB' });
}
};
@ -69,7 +71,6 @@ const ConfigList = ({ diff, isLoading }) => {
setOriginalConfig(diff.fileConfig[`${configType}.${configName}`]);
setNewConfig(diff.databaseConfig[`${configType}.${configName}`]);
setCname(`${configType}.${configName}`);
setOpenModal(true);
},
});
});
@ -86,17 +87,10 @@ const ConfigList = ({ diff, isLoading }) => {
dispatch(setConfigPartialDiffInState(newPartialDiff));
}, [checkedItems]);
const closeModal = () => {
setOriginalConfig({});
setNewConfig({});
setCname('');
setOpenModal(false);
};
if (isLoading) {
return (
<div style={{ textAlign: 'center', marginTop: 40 }}>
<Loader>Loading content...</Loader>
<Loader>{formatMessage({ id: 'config-sync.ConfigList.Loading' })}</Loader>
</div>
);
}
@ -114,39 +108,36 @@ const ConfigList = ({ diff, isLoading }) => {
return (
<div>
<ConfigDiff
isOpen={openModal}
oldValue={originalConfig}
newValue={newConfig}
onClose={closeModal}
configName={cName}
/>
<Table colCount={4} rowCount={rows.length + 1}>
<Thead>
<Tr>
<Th>
<BaseCheckbox
aria-label="Select all entries"
indeterminate={isIndeterminate}
onValueChange={(value) => setCheckedItems(checkedItems.map(() => value))}
value={allChecked}
<Checkbox
aria-label={formatMessage({ id: 'config-sync.ConfigList.SelectAll' })}
checked={isIndeterminate ? "indeterminate" : allChecked}
onCheckedChange={(value) => setCheckedItems(checkedItems.map(() => value))}
/>
</Th>
<Th>
<Typography variant="sigma">Config name</Typography>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigName' })}</Typography>
</Th>
<Th>
<Typography variant="sigma">Config type</Typography>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigType' })}</Typography>
</Th>
<Th>
<Typography variant="sigma">State</Typography>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.State' })}</Typography>
</Th>
</Tr>
</Thead>
<Tbody>
{rows.map((row, index) => (
<ConfigListRow
<ConfigDiff
key={row.configName}
oldValue={originalConfig}
newValue={newConfig}
configName={cName}
trigger={(
<ConfigListRow
row={row}
checked={checkedItems[index]}
updateValue={() => {
@ -154,6 +145,8 @@ const ConfigList = ({ diff, isLoading }) => {
setCheckedItems([...checkedItems]);
}}
/>
)}
/>
))}
</Tbody>
</Table>

View File

@ -1,83 +0,0 @@
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import { useSelector } from 'react-redux';
import {
Dialog,
DialogBody,
DialogFooter,
Flex,
Typography,
Stack,
Button,
Checkbox,
Divider,
Box,
} from '@strapi/design-system';
import { ExclamationMarkCircle } from '@strapi/icons';
const ConfirmModal = ({ isOpen, onClose, onSubmit, type }) => {
const soft = useSelector((state) => state.getIn(['config', 'appEnv', 'config', 'soft'], false));
const [force, setForce] = useState(false);
const { formatMessage } = useIntl();
if (!isOpen) return null;
return (
<Dialog
onClose={onClose}
title="Confirmation"
isOpen={isOpen}
>
<DialogBody icon={<ExclamationMarkCircle />}>
<Stack size={2}>
<Flex justifyContent="center">
<Typography variant="omega" id="confirm-description" style={{ textAlign: 'center' }}>
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_1` })}<br />
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_2` })}
</Typography>
</Flex>
</Stack>
</DialogBody>
{(soft && type === 'import') && (
<React.Fragment>
<Divider />
<Box padding={4}>
<Checkbox
onValueChange={(value) => setForce(value)}
value={force}
name="force"
hint="Check this to ignore the soft setting."
>
{formatMessage({ id: 'config-sync.popUpWarning.force' })}
</Checkbox>
</Box>
</React.Fragment>
)}
<DialogFooter
startAction={(
<Button
onClick={() => {
onClose();
}}
variant="tertiary"
>
{formatMessage({ id: 'config-sync.popUpWarning.button.cancel' })}
</Button>
)}
endAction={(
<Button
variant="secondary"
onClick={() => {
onClose();
onSubmit(force);
}}
>
{formatMessage({ id: `config-sync.popUpWarning.button.${type}` })}
</Button>
)} />
</Dialog>
);
};
export default ConfirmModal;

View File

@ -0,0 +1,80 @@
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import { useSelector } from 'react-redux';
import {
Dialog,
Flex,
Typography,
Button,
Checkbox,
Divider,
Box,
Field,
} from '@strapi/design-system';
import { WarningCircle } from '@strapi/icons';
const ConfirmModal = ({ onClose, onSubmit, type, trigger }) => {
const soft = useSelector((state) => state.getIn(['config', 'appEnv', 'config', 'soft'], false));
const [force, setForce] = useState(false);
const { formatMessage } = useIntl();
return (
<Dialog.Root>
<Dialog.Trigger>
{trigger}
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>{formatMessage({ id: "config-sync.popUpWarning.Confirmation" })}</Dialog.Header>
<Dialog.Body>
<WarningCircle fill="danger600" width="32px" height="32px" />
<Flex size={2}>
<Flex justifyContent="center">
<Typography variant="omega" id="confirm-description" style={{ textAlign: 'center' }}>
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_1` })}<br />
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_2` })}
</Typography>
</Flex>
</Flex>
{(soft && type === 'import') && (
<Box width="100%">
<Divider marginTop={4} />
<Box paddingTop={6}>
<Field.Root hint="Check this to ignore the soft setting.">
<Checkbox
onValueChange={(value) => setForce(value)}
value={force}
name="force"
>
{formatMessage({ id: 'config-sync.popUpWarning.force' })}
</Checkbox>
<Field.Hint />
</Field.Root>
</Box>
</Box>
)}
</Dialog.Body>
<Dialog.Footer>
<Dialog.Cancel>
<Button fullWidth variant="tertiary">
{formatMessage({ id: 'config-sync.popUpWarning.button.cancel' })}
</Button>
</Dialog.Cancel>
<Dialog.Action>
<Button
fullWidth
variant="secondary"
onClick={() => {
onSubmit(force);
}}
>
{formatMessage({ id: `config-sync.popUpWarning.button.${type}` })}
</Button>
</Dialog.Action>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
);
};
export default ConfirmModal;

View File

@ -1,34 +0,0 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { NoContent, useNotification } from '@strapi/helper-plugin';
import { Button } from '@strapi/design-system';
import { exportAllConfig } from '../../state/actions/Config';
import ConfirmModal from '../ConfirmModal';
const FirstExport = () => {
const toggleNotification = useNotification();
const dispatch = useDispatch();
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
<div>
<ConfirmModal
isOpen={modalIsOpen}
onClose={() => setModalIsOpen(false)}
type="export"
onSubmit={() => dispatch(exportAllConfig([], toggleNotification))}
/>
<NoContent
content={{
id: 'emptyState',
defaultMessage:
'Looks like this is your first time using config-sync for this project.',
}}
action={<Button onClick={() => setModalIsOpen(true)}>Make the initial export</Button>}
/>
</div>
);
};
export default FirstExport;

View File

@ -0,0 +1,37 @@
import React from 'react';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { getFetchClient, useNotification } from '@strapi/strapi/admin';
import { Button, EmptyStateLayout } from '@strapi/design-system';
import { EmptyDocuments } from '@strapi/icons/symbols';
import { exportAllConfig } from '../../state/actions/Config';
import ConfirmModal from '../ConfirmModal';
const FirstExport = () => {
const { post, get } = getFetchClient();
const { toggleNotification } = useNotification();
const dispatch = useDispatch();
const { formatMessage } = useIntl();
return (
<div>
<EmptyStateLayout
content={formatMessage({ id: 'config-sync.FirstExport.Message' })}
action={(
<ConfirmModal
type="export"
onSubmit={() => dispatch(exportAllConfig([], toggleNotification, formatMessage, post, get))}
trigger={(
<Button>{formatMessage({ id: 'config-sync.FirstExport.Button' })}</Button>
)}
/>
)}
icon={<EmptyDocuments width={160} />}
/>
</div>
);
};
export default FirstExport;

View File

@ -7,14 +7,15 @@
import React, { memo } from 'react';
import { useIntl } from 'react-intl';
import { HeaderLayout, Box } from '@strapi/design-system';
import { Layouts } from '@strapi/admin/strapi-admin';
import { Box } from '@strapi/design-system';
const HeaderComponent = () => {
const { formatMessage } = useIntl();
return (
<Box background="neutral100">
<HeaderLayout
<Layouts.Header
title={formatMessage({ id: 'config-sync.Header.Title' })}
subtitle={formatMessage({ id: 'config-sync.Header.Description' })}
as="h2"

View File

@ -1,14 +0,0 @@
import React from 'react';
import { NoContent } from '@strapi/helper-plugin';
const NoChanges = () => (
<NoContent
content={{
id: 'emptyState',
defaultMessage:
'No differences between DB and sync directory. You are up-to-date!',
}}
/>
);
export default NoChanges;

View File

@ -0,0 +1,16 @@
import React from 'react';
import { EmptyStateLayout } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { EmptyDocuments } from '@strapi/icons/symbols';
const NoChanges = () => {
const { formatMessage } = useIntl();
return (
<EmptyStateLayout
content={formatMessage({ id: 'config-sync.NoChanges.Message', defaultMessage: 'No differences between DB and sync directory. You are up-to-date!' })}
icon={<EmptyDocuments width={160} />}
/>
);
};
export default NoChanges;

View File

@ -7,7 +7,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { CheckPagePermissions } from '@strapi/helper-plugin';
import { Page } from '@strapi/strapi/admin';
import pluginPermissions from '../../permissions';
import Header from '../../components/Header';
@ -16,12 +16,12 @@ import ConfigPage from '../ConfigPage';
const App = () => {
return (
<CheckPagePermissions permissions={pluginPermissions.settings}>
<Page.Protect permissions={pluginPermissions.settings}>
<Provider store={store}>
<Header />
<ConfigPage />
</Provider>
</CheckPagePermissions>
</Page.Protect>
);
};

View File

@ -3,30 +3,34 @@ import { useDispatch, useSelector } from 'react-redux';
import { Map } from 'immutable';
import {
Box,
ContentLayout,
Alert,
Typography,
} from '@strapi/design-system';
import { useNotification } from '@strapi/helper-plugin';
import { useNotification } from '@strapi/strapi/admin';
import { getFetchClient, Layouts } from '@strapi/admin/strapi-admin';
import { useIntl } from 'react-intl';
import { getAllConfigDiff, getAppEnv } from '../../state/actions/Config';
import ConfigList from '../../components/ConfigList';
import ActionButtons from '../../components/ActionButtons';
const ConfigPage = () => {
const toggleNotification = useNotification();
const { toggleNotification } = useNotification();
const { get } = getFetchClient();
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading'], Map({})));
const configDiff = useSelector((state) => state.getIn(['config', 'configDiff'], Map({})));
const appEnv = useSelector((state) => state.getIn(['config', 'appEnv', 'env']));
useEffect(() => {
dispatch(getAllConfigDiff(toggleNotification));
dispatch(getAppEnv(toggleNotification));
dispatch(getAllConfigDiff(toggleNotification, formatMessage, get));
dispatch(getAppEnv(toggleNotification, formatMessage, get));
}, []);
return (
<ContentLayout paddingBottom={8}>
<Layouts.Content paddingBottom={8}>
{appEnv === 'production' && (
<Box paddingBottom={4}>
<Alert variant="danger">
@ -38,7 +42,7 @@ const ConfigPage = () => {
)}
<ActionButtons />
<ConfigList isLoading={isLoading} diff={configDiff.toJS()} />
</ContentLayout>
</Layouts.Content>
);
};

View File

@ -1,8 +1,8 @@
const pluginPkg = require('../../../package.json');
import pluginPkg from '../../../package.json';
const pluginId = pluginPkg.name.replace(
/^strapi-plugin-/i,
'',
);
module.exports = pluginId;
export default pluginId;

View File

@ -0,0 +1,11 @@
const prefixPluginTranslations = (trad, pluginId) => {
if (!pluginId) {
throw new TypeError("pluginId can't be empty");
}
return Object.keys(trad).reduce((acc, current) => {
acc[`${pluginId}.${current}`] = trad[current];
return acc;
}, {});
};
export { prefixPluginTranslations };

View File

@ -1,6 +1,6 @@
import { prefixPluginTranslations } from '@strapi/helper-plugin';
import pluginPkg from '../../package.json';
import pluginId from './helpers/pluginId';
import { prefixPluginTranslations } from './helpers/prefixPluginTranslations';
import pluginPermissions from './permissions';
// import pluginIcon from './components/PluginIcon';
// import getTrad from './helpers/getTrad';

View File

@ -3,20 +3,18 @@
* Main actions
*
*/
import { request } from '@strapi/helper-plugin';
import { saveAs } from 'file-saver';
export function getAllConfigDiff(toggleNotification) {
return async function (dispatch) {
export function getAllConfigDiff(toggleNotification, formatMessage, get) {
return async function(dispatch) {
dispatch(setLoadingState(true));
try {
const configDiff = await request('/config-sync/diff', { method: 'GET' });
const configDiff = await get('/config-sync/diff');
dispatch(setConfigPartialDiffInState([]));
dispatch(setConfigDiffInState(configDiff));
dispatch(setConfigDiffInState(configDiff.data));
dispatch(setLoadingState(false));
} catch (err) {
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
dispatch(setLoadingState(false));
}
};
@ -38,31 +36,26 @@ export function setConfigPartialDiffInState(config) {
};
}
export function exportAllConfig(partialDiff, toggleNotification) {
return async function (dispatch) {
export function exportAllConfig(partialDiff, toggleNotification, formatMessage, post, get) {
return async function(dispatch) {
dispatch(setLoadingState(true));
try {
const { message } = await request('/config-sync/export', {
method: 'POST',
body: partialDiff,
});
toggleNotification({ type: 'success', message });
dispatch(getAllConfigDiff(toggleNotification));
const response = await post('/config-sync/export', partialDiff);
toggleNotification({ type: 'success', message: response.data.message });
dispatch(getAllConfigDiff(toggleNotification, formatMessage, get));
dispatch(setLoadingState(false));
} catch (err) {
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
dispatch(setLoadingState(false));
}
};
}
export function downloadZip(toggleNotification) {
export function downloadZip(toggleNotification, formatMessage, post, get) {
return async function (dispatch) {
dispatch(setLoadingState(true));
try {
const { message, base64Data, name } = await request('/config-sync/zip', {
method: 'GET'
});
const { message, base64Data, name } = await get('/config-sync/zip');
toggleNotification({ type: 'success', message });
if (base64Data) {
function b64toBlob(dataURI) {
@ -78,28 +71,25 @@ export function downloadZip(toggleNotification) {
}
dispatch(setLoadingState(false));
} catch (err) {
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
dispatch(setLoadingState(false));
}
};
}
export function importAllConfig(partialDiff, force, toggleNotification) {
return async function (dispatch) {
export function importAllConfig(partialDiff, force, toggleNotification, formatMessage, post, get) {
return async function(dispatch) {
dispatch(setLoadingState(true));
try {
const { message } = await request('/config-sync/import', {
method: 'POST',
body: {
const response = await post('/config-sync/import', {
force,
config: partialDiff,
},
});
toggleNotification({ type: 'success', message });
dispatch(getAllConfigDiff(toggleNotification));
toggleNotification({ type: 'success', message: response.data.message });
dispatch(getAllConfigDiff(toggleNotification, formatMessage, get));
dispatch(setLoadingState(false));
} catch (err) {
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
dispatch(setLoadingState(false));
}
};
@ -113,15 +103,13 @@ export function setLoadingState(value) {
};
}
export function getAppEnv(toggleNotification) {
return async function (dispatch) {
export function getAppEnv(toggleNotification, formatMessage, get) {
return async function(dispatch) {
try {
const envVars = await request('/config-sync/app-env', {
method: 'GET',
});
dispatch(setAppEnvInState(envVars));
const envVars = await get('/config-sync/app-env');
dispatch(setAppEnvInState(envVars.data));
} catch (err) {
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
}
};
}

View File

@ -7,10 +7,32 @@
"popUpWarning.button.export": "Yes, export",
"popUpWarning.button.cancel": "Cancel",
"popUpWarning.force": "Force",
"popUpWarning.Confirmation": "Confirmation",
"Header.Title": "Config Sync",
"Header.Description": "Manage your database config across environments.",
"ConfigList.Loading": "Loading content...",
"ConfigList.SelectAll": "Select all entries",
"ConfigList.ConfigName": "Config name",
"ConfigList.ConfigType": "Config type",
"ConfigList.State": "State",
"ConfigList.Different": "Different",
"ConfigList.OnlyDir": "Only in sync dir",
"ConfigList.OnlyDB": "Only in DB",
"NoChanges.Message": "No differences between DB and sync directory. You are up-to-date!",
"ConfigDiff.Title": "Config changes for",
"ConfigDiff.SyncDirectory": "Sync directory",
"ConfigDiff.Database": "Database",
"Buttons.Export": "Export",
"Buttons.Import": "Import",
"FirstExport.Message": "Looks like this is your first time using config-sync for this project.",
"FirstExport.Button": "Make the initial export",
"Settings.Tool.Title": "Interface",
"plugin.name": "Config Sync"

View File

@ -1 +1,39 @@
{}
{
"popUpWarning.warning.import_1": "Si continuas todos tus ficheros de configuración locales",
"popUpWarning.warning.import_2": "se importarán a la base de datos.",
"popUpWarning.warning.export_1": "Si continuas las configuraciones de tu base de datos",
"popUpWarning.warning.export_2": "se escribirán en ficheros de configuración locales.",
"popUpWarning.button.import": "Sí, importar",
"popUpWarning.button.export": "Sí, exportar",
"popUpWarning.button.cancel": "Cancelar",
"popUpWarning.force": "Forzar",
"popUpWarning.Confirmation": "Confirmación",
"Header.Title": "Config Sync",
"Header.Description": "Gestiona las configuraciones de tu base de datos entre diferentes entornos o instancias.",
"ConfigList.Loading": "Cargando contenido...",
"ConfigList.SelectAll": "Seleccionar todas las entradas",
"ConfigList.ConfigName": "Nombre",
"ConfigList.ConfigType": "Tipo",
"ConfigList.State": "Estado",
"ConfigList.Different": "Diferentes",
"ConfigList.OnlyDir": "Sólo en directorio de sincronización",
"ConfigList.OnlyDB": "Sólo en la base de datos",
"NoChanges.Message": "No hay diferencia entre la base de datos y el directorio de sincronización. ¡Estás actualizado!",
"ConfigDiff.Title": "Cambios en la configuración para",
"ConfigDiff.SyncDirectory": "Directorio de sincronización",
"ConfigDiff.Database": "Base de datos",
"Buttons.Import": "Importar",
"Buttons.Export": "Exportar",
"FirstExport.Message": "Parece ser la primera vez que se usa config-sync en este proyecto.",
"FirstExport.Button": "Hacer la exportación inicial",
"Settings.Tool.Title": "Interfaz",
"plugin.name": "Config Sync"
}

View File

@ -1,6 +1,6 @@
{
"name": "strapi-plugin-config-sync",
"version": "1.1.2",
"version": "2.0.0-beta.4",
"description": "Migrate your config data across environments using the CLI or Strapi admin panel.",
"strapi": {
"displayName": "Config Sync",
@ -14,12 +14,15 @@
"config-sync": "./bin/config-sync"
},
"scripts": {
"develop": "nodemon -e js,jsx --ignore playground --exec \"yalc publish && yalc push\"",
"build": "yalc push --publish",
"eslint": "eslint --max-warnings=0 './**/*.{js,jsx}'",
"eslint:fix": "eslint --fix './**/*.{js,jsx}'",
"test:unit": "jest --verbose",
"test:integration": "cd playground && node_modules/.bin/jest --verbose",
"plugin:install": "yarn install && rm -rf node_modules/@strapi/helper-plugin",
"playground:install": "cd playground && yarn install",
"test:integration": "cd playground && node_modules/.bin/jest --verbose --forceExit --detectOpenHandles",
"playground:install": "yarn playground:yalc-add-link && cd playground && yarn install",
"playground:yalc-add": "cd playground && yalc add strapi-plugin-config-sync",
"playground:yalc-add-link": "cd playground && yalc add --link strapi-plugin-config-sync",
"playground:build": "cd playground && yarn build",
"playground:develop": "cd playground && yarn develop"
},
@ -31,19 +34,20 @@
"git-diff": "^2.0.6",
"immutable": "^3.8.2",
"inquirer": "^8.2.0",
"react-diff-viewer-continued": "^3.2.3",
"react-diff-viewer-continued": "3.2.6",
"react-intl": "6.6.2",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.3.0"
},
"author": {
"name": "Boaz Poolman",
"email": "info@boazpoolman.nl",
"email": "boaz@pluginpal.io",
"url": "https://github.com/boazpoolman"
},
"maintainers": [
{
"name": "Boaz Poolman",
"email": "info@boazpoolman.nl",
"email": "boaz@pluginpal.io",
"url": "https://github.com/boazpoolman"
}
],
@ -55,13 +59,13 @@
"strapi-server.js"
],
"peerDependencies": {
"@strapi/strapi": "^4.0.0"
"@strapi/strapi": "^5.0.0"
},
"devDependencies": {
"@strapi/design-system": "^1.3.1",
"@strapi/helper-plugin": "^4.5.5",
"@strapi/icons": "^1.3.1",
"@strapi/utils": "^4.5.2",
"@strapi/design-system": "2.0.0-rc.11",
"@strapi/icons": "2.0.0-rc.11",
"@strapi/strapi": "5.0.4",
"@strapi/utils": "5.0.4",
"babel-eslint": "9.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
@ -74,22 +78,23 @@
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^2.3.0",
"jest": "^29.3.1",
"jest": "^29.7.0",
"jest-cli": "^29.3.1",
"jest-styled-components": "^7.0.2",
"lodash": "^4.17.11",
"nodemon": "^3.1.7",
"react": "^17.0.2",
"react-intl": "^5.20.12",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"styled-components": "^5.2.3"
"styled-components": "^5.2.3",
"yalc": "^1.0.0-pre.53"
},
"bugs": {
"url": "https://github.com/boazpoolman/strapi-plugin-config-sync/issues"
"url": "https://github.com/pluginpal/strapi-plugin-config-sync/issues"
},
"homepage": "https://github.com/boazpoolman/strapi-plugin-config-sync#readme",
"homepage": "https://www.pluginpal.io/plugin/config-sync",
"engines": {
"node": ">=10.0.0",
"node": ">=18.0.0",
"npm": ">=6.0.0"
},
"license": "MIT",

View File

@ -1,6 +1,10 @@
HOST=0.0.0.0
PORT=1337
APP_KEYS=SIwLyqu+IpSHIuUBDQfPZg==,Nzqbq2C3ATsR19u5XEAJQA==,/Agk5Sn8M4EzfoSiIHcDlQ==,gSxT2T0k2zbQatKXUV0zCA==
API_TOKEN_SALT=reQcUBbGXD2KWG2QpRn7DA==
ADMIN_JWT_SECRET= 69mzgwRGfEBUhPEaas8EBA==
JWT_SECRET=E0TTVdsr+M/FXAjfrNIgXA==
APP_KEYS=ujfpKPEst1tv0WDxJEhjJw==,MOnFjWYKbWYmtrBZ3cQTFQ==,zQpX70tJw/Mw+Y656kXfVA==,xJT1vbsiz3cgabfgpLu72w==
API_TOKEN_SALT=5FoJkYoZV8IA6+NnZJDzng==
ADMIN_JWT_SECRET=tkeg3+HqE+QmTd2ITEivtA==
TRANSFER_TOKEN_SALT=UUMCRQ2cx9qvKw/RkB815Q==
# Database
DATABASE_CLIENT=sqlite
DATABASE_FILENAME=.tmp/data.db
JWT_SECRET=Dn/nUGQsREUw4/lfQYOScw==

View File

@ -1,2 +1,7 @@
HOST=0.0.0.0
PORT=1337
APP_KEYS="toBeModified1,toBeModified2"
API_TOKEN_SALT=tobemodified
ADMIN_JWT_SECRET=tobemodified
TRANSFER_TOKEN_SALT=tobemodified
JWT_SECRET=tobemodified

View File

@ -114,3 +114,7 @@ exports
*.cache
build
.strapi-updater.json
# yalc
.yalc
yalc.lock

View File

@ -0,0 +1,16 @@
/**
* This file was automatically generated by Strapi.
* Any modifications made will be discarded.
*/
import strapiCloud from "@strapi/plugin-cloud/strapi-admin";
import usersPermissions from "@strapi/plugin-users-permissions/strapi-admin";
import configSync from "strapi-plugin-config-sync/strapi-admin";
import { renderAdmin } from "@strapi/strapi/admin";
renderAdmin(document.getElementById("strapi"), {
plugins: {
"strapi-cloud": strapiCloud,
"users-permissions": usersPermissions,
"config-sync": configSync,
},
});

View File

@ -0,0 +1,63 @@
<!doctype html>
<html lang="en">
<!--
This file was automatically generated by Strapi.
Any modifications made will be discarded.
-->
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<meta name="robots" content="noindex" />
<meta name="referrer" content="same-origin" />
<title>Strapi Admin</title>
<style>
html,
body,
#strapi {
height: 100%;
}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
}
</style>
</head>
<body>
<div id="strapi"></div>
<noscript
><div class="strapi--root">
<div class="strapi--no-js">
<style type="text/css">
.strapi--root {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: #fff;
}
.strapi--no-js {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-family: helvetica, arial, sans-serif;
}
</style>
<h1>JavaScript disabled</h1>
<p>
Please
<a href="https://www.enable-javascript.com/">enable JavaScript</a>
in your browser and reload the page to proceed.
</p>
</div>
</div></noscript
>
<script type="module" src="/.strapi/client/app.js"></script>
</body>
</html>

View File

@ -1,3 +1,57 @@
# Strapi application
# 🚀 Getting started with Strapi
A quick description of your strapi application
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.
### `develop`
Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-develop)
```
npm run develop
# or
yarn develop
```
### `start`
Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-start)
```
npm run start
# or
yarn start
```
### `build`
Build your admin panel. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-build)
```
npm run build
# or
yarn build
```
## ⚙️ Deployment
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.
## 📚 Learn more
- [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.
Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
## ✨ Community
- [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.
---
<sub>🤫 Psst! [Strapi is hiring](https://strapi.io/careers).</sub>

View File

@ -6,7 +6,6 @@ const exec = util.promisify(require('child_process').exec);
jest.setTimeout(20000);
describe('Test the config-sync CLI', () => {
afterAll(async () => {
// Remove the generated files and the DB.
await exec('rm -rf config/sync');
@ -14,26 +13,47 @@ describe('Test the config-sync CLI', () => {
});
test('Export', async () => {
const { stdout } = await exec('yarn cs export -y');
expect(stdout).toContain('Finished export');
const { stdout: exportOutput } = await exec('yarn cs export -y');
expect(exportOutput).toContain('Finished export');
const { stdout: diffOutput } = await exec('yarn cs diff');
expect(diffOutput).toContain('No differences between DB and sync directory');
});
test('Import', async () => {
await exec('rm -rf config/sync/admin-role.strapi-editor.json');
const { stdout } = await exec('yarn cs import -y');
expect(stdout).toContain('Finished import');
test('Import (delete)', async () => {
// Remove a file to trigger a delete.
await exec('mv config/sync/admin-role.strapi-editor.json .tmp');
const { stdout: importOutput } = await exec('yarn cs import -y');
expect(importOutput).toContain('Finished import');
const { stdout: diffOutput } = await exec('yarn cs diff');
expect(diffOutput).toContain('No differences between DB and sync directory');
});
test('Diff', async () => {
const { stdout } = await exec('yarn cs diff');
expect(stdout).toContain('No differences between DB and sync directory');
test('Import (update)', async () => {
// Update a core-store file.
await exec('sed -i \'s/"description":"",/"description":"test",/g\' config/sync/core-store.plugin_content_manager_configuration_content_types##plugin##users-permissions.user.json');
// Update a file that has relations.
await exec('sed -i \'s/{"action":"plugin::users-permissions.auth.register"},//g\' config/sync/user-role.public.json');
const { stdout: importOutput } = await exec('yarn cs import -y');
expect(importOutput).toContain('Finished import');
const { stdout: diffOutput } = await exec('yarn cs diff');
expect(diffOutput).toContain('No differences between DB and sync directory');
});
test('Import (create)', async () => {
// Add a file to trigger a creation.
await exec('mv .tmp/admin-role.strapi-editor.json config/sync/');
const { stdout: importOutput } = await exec('yarn cs import -y');
expect(importOutput).toContain('Finished import');
const { stdout: diffOutput } = await exec('yarn cs diff');
expect(diffOutput).toContain('No differences between DB and sync directory');
});
test('Non-empty diff returns 1', async () => {
await exec('rm -rf config/sync/admin-role.strapi-author.json');
await exec('rm -rf config/sync/admin-role.strapi-editor.json');
// Work around Jest not supporting custom error matching.
// https://github.com/facebook/jest/issues/8140
let error;
try {
await exec('yarn cs diff');
} catch(e) {
} catch (e) {
error = e;
}
expect(error).toHaveProperty('code', 1);

View File

@ -0,0 +1,35 @@
const fs = require('fs');
const { createStrapi, compileStrapi } = require('@strapi/strapi');
let instance;
async function setupStrapi() {
if (!instance) {
const appContext = await compileStrapi();
await createStrapi(appContext).load();
instance = strapi;
await instance.server.mount();
}
return instance;
}
async function cleanupStrapi() {
const dbSettings = strapi.config.get('database.connection');
// close server to release the db-file.
await strapi.server.httpServer.close();
// close the connection to the database before deletion.
await strapi.db.connection.destroy();
// delete test database after all tests have completed.
if (dbSettings && dbSettings.connection && dbSettings.connection.filename) {
const tmpDbFile = dbSettings.connection.filename;
if (fs.existsSync(tmpDbFile)) {
fs.unlinkSync(tmpDbFile);
}
}
}
module.exports = { setupStrapi, cleanupStrapi };

View File

@ -0,0 +1,48 @@
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { setupStrapi, cleanupStrapi } = require('./helpers');
jest.setTimeout(20000);
afterEach(async () => {
// Disable importOnBootstrap
await exec('sed -i "s/importOnBootstrap: true/importOnBootstrap: false/g" config/plugins.js');
await cleanupStrapi();
await exec('rm -rf config/sync');
});
describe('Test the importOnBootstrap feature', () => {
test('Without a database', async () => {
// Do the initial export and remove the database.
await exec('yarn cs export -y');
await exec('rm -rf .tmp');
// Manually change the plugins.js to enable importOnBoostrap.
await exec('sed -i "s/importOnBootstrap: false/importOnBootstrap: true/g" config/plugins.js');
// Start up Strapi to initiate the importOnBootstrap function.
await setupStrapi();
expect(strapi).toBeDefined();
});
test('With a database', async () => {
// Delete any existing database and do an export.
await exec('rm -rf .tmp');
await exec('yarn cs export -y');
// Manually change the plugins.js to enable importOnBoostrap.
await exec('sed -i "s/importOnBootstrap: false/importOnBootstrap: true/g" config/plugins.js');
// Remove a config file to make sure the importOnBoostrap
// function actually attempts to import.
await exec('rm -rf config/sync/admin-role.strapi-editor.json');
// Start up Strapi to initiate the importOnBootstrap function.
await setupStrapi();
expect(strapi).toBeDefined();
});
});

View File

@ -1,8 +1,20 @@
module.exports = ({ env }) => ({
auth: {
secret: env('ADMIN_JWT_SECRET', 'c27c3833823a12b0761e32b22dc0113a'),
secret: env('ADMIN_JWT_SECRET'),
},
apiToken: {
salt: env('API_TOKEN_SALT'),
},
transfer: {
token: {
salt: env('TRANSFER_TOKEN_SALT'),
},
},
flags: {
nps: env.bool('FLAG_NPS', true),
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
},
watchIgnoreFiles: [
'**/config/sync/**',
],
'!**/.yalc/**/server/**',
]
});

View File

@ -1,11 +1,12 @@
module.exports = [
'strapi::logger',
'strapi::errors',
'strapi::security',
'strapi::cors',
'strapi::poweredBy',
'strapi::logger',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];

View File

@ -0,0 +1,9 @@
module.exports = {
'config-sync': {
enabled: true,
config: {
importOnBootstrap: false,
minify: true,
},
},
};

View File

@ -1,4 +1,10 @@
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
app: {
keys: env.array('APP_KEYS'),
},
webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
},
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

BIN
playground/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

8
playground/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"moduleResolution": "nodenext",
"target": "ES2021",
"checkJs": true,
"allowJs": true
}
}

View File

@ -1,5 +1,5 @@
{
"name": "playground",
"name": "strapi-5-beta",
"private": true,
"version": "0.1.0",
"description": "A Strapi application",
@ -11,24 +11,30 @@
"cs": "config-sync"
},
"devDependencies": {
"jest": "^26.0.1",
"jest-cli": "^26.0.1"
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
"supertest": "^6.3.3",
"yalc": "^1.0.0-pre.53"
},
"dependencies": {
"@strapi/plugin-i18n": "^4.0.0",
"@strapi/plugin-users-permissions": "^4.0.0",
"@strapi/strapi": "^4.0.0",
"better-sqlite3": "7.4.6",
"strapi-plugin-config-sync": "./.."
"@strapi/plugin-cloud": "5.0.4",
"@strapi/plugin-users-permissions": "5.0.4",
"@strapi/strapi": "5.0.4",
"better-sqlite3": "9.4.3",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.0.0",
"strapi-plugin-config-sync": "link:.yalc/strapi-plugin-config-sync",
"styled-components": "^6.0.0"
},
"author": {
"name": "A Strapi developer"
},
"strapi": {
"uuid": "2e84e366-1e09-43c2-a99f-a0d0acbc2ca5"
"uuid": "edadddbd-0f25-4da7-833b-d4cd7dcae2fc"
},
"engines": {
"node": ">=14.x.x <=18.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"license": "MIT"

View File

@ -0,0 +1,23 @@
{
"kind": "singleType",
"collectionName": "homes",
"info": {
"singularName": "home",
"pluralName": "homes",
"displayName": "Home",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string"
},
"slug": {
"type": "uid",
"targetField": "title"
}
}
}

View File

@ -0,0 +1,9 @@
'use strict';
/**
* home controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::home.home');

View File

@ -0,0 +1,9 @@
'use strict';
/**
* home router
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::home.home');

View File

@ -0,0 +1,9 @@
'use strict';
/**
* home service
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::home.home');

View File

@ -0,0 +1,19 @@
{
"kind": "collectionType",
"collectionName": "pages",
"info": {
"singularName": "page",
"pluralName": "pages",
"displayName": "Page"
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string",
"required": true
}
}
}

View File

@ -0,0 +1,9 @@
'use strict';
/**
* page controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::page.page');

View File

@ -0,0 +1,9 @@
'use strict';
/**
* page router
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::page.page');

View File

@ -0,0 +1,9 @@
'use strict';
/**
* page service
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::page.page');

View File

@ -0,0 +1,3 @@
/*
* The app doesn't have any components yet.
*/

View File

@ -0,0 +1,924 @@
import type { Struct, Schema } from '@strapi/strapi';
export interface ApiHomeHome extends Struct.SingleTypeSchema {
collectionName: 'homes';
info: {
singularName: 'home';
pluralName: 'homes';
displayName: 'Home';
description: '';
};
options: {
draftAndPublish: false;
};
attributes: {
title: Schema.Attribute.String;
slug: Schema.Attribute.UID<'title'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::home.home'>;
};
}
export interface ApiPagePage extends Struct.CollectionTypeSchema {
collectionName: 'pages';
info: {
singularName: 'page';
pluralName: 'pages';
displayName: 'Page';
};
options: {
draftAndPublish: false;
};
attributes: {
title: Schema.Attribute.String & Schema.Attribute.Required;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::page.page'>;
};
}
export interface PluginUploadFile extends Struct.CollectionTypeSchema {
collectionName: 'files';
info: {
singularName: 'file';
pluralName: 'files';
displayName: 'File';
description: '';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String & Schema.Attribute.Required;
alternativeText: Schema.Attribute.String;
caption: Schema.Attribute.String;
width: Schema.Attribute.Integer;
height: Schema.Attribute.Integer;
formats: Schema.Attribute.JSON;
hash: Schema.Attribute.String & Schema.Attribute.Required;
ext: Schema.Attribute.String;
mime: Schema.Attribute.String & Schema.Attribute.Required;
size: Schema.Attribute.Decimal & Schema.Attribute.Required;
url: Schema.Attribute.String & Schema.Attribute.Required;
previewUrl: Schema.Attribute.String;
provider: Schema.Attribute.String & Schema.Attribute.Required;
provider_metadata: Schema.Attribute.JSON;
related: Schema.Attribute.Relation<'morphToMany'>;
folder: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'> &
Schema.Attribute.Private;
folderPath: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Private &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::upload.file'
>;
};
}
export interface PluginUploadFolder extends Struct.CollectionTypeSchema {
collectionName: 'upload_folders';
info: {
singularName: 'folder';
pluralName: 'folders';
displayName: 'Folder';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
pathId: Schema.Attribute.Integer &
Schema.Attribute.Required &
Schema.Attribute.Unique;
parent: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'>;
children: Schema.Attribute.Relation<'oneToMany', 'plugin::upload.folder'>;
files: Schema.Attribute.Relation<'oneToMany', 'plugin::upload.file'>;
path: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::upload.folder'
>;
};
}
export interface PluginI18NLocale extends Struct.CollectionTypeSchema {
collectionName: 'i18n_locale';
info: {
singularName: 'locale';
pluralName: 'locales';
collectionName: 'locales';
displayName: 'Locale';
description: '';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.SetMinMax<
{
min: 1;
max: 50;
},
number
>;
code: Schema.Attribute.String & Schema.Attribute.Unique;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::i18n.locale'
>;
};
}
export interface PluginContentReleasesRelease
extends Struct.CollectionTypeSchema {
collectionName: 'strapi_releases';
info: {
singularName: 'release';
pluralName: 'releases';
displayName: 'Release';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String & Schema.Attribute.Required;
releasedAt: Schema.Attribute.DateTime;
scheduledAt: Schema.Attribute.DateTime;
timezone: Schema.Attribute.String;
status: Schema.Attribute.Enumeration<
['ready', 'blocked', 'failed', 'done', 'empty']
> &
Schema.Attribute.Required;
actions: Schema.Attribute.Relation<
'oneToMany',
'plugin::content-releases.release-action'
>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::content-releases.release'
>;
};
}
export interface PluginContentReleasesReleaseAction
extends Struct.CollectionTypeSchema {
collectionName: 'strapi_release_actions';
info: {
singularName: 'release-action';
pluralName: 'release-actions';
displayName: 'Release Action';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
type: Schema.Attribute.Enumeration<['publish', 'unpublish']> &
Schema.Attribute.Required;
contentType: Schema.Attribute.String & Schema.Attribute.Required;
entryDocumentId: Schema.Attribute.String;
locale: Schema.Attribute.String;
release: Schema.Attribute.Relation<
'manyToOne',
'plugin::content-releases.release'
>;
isEntryValid: Schema.Attribute.Boolean;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::content-releases.release-action'
>;
};
}
export interface PluginReviewWorkflowsWorkflow
extends Struct.CollectionTypeSchema {
collectionName: 'strapi_workflows';
info: {
name: 'Workflow';
description: '';
singularName: 'workflow';
pluralName: 'workflows';
displayName: 'Workflow';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique;
stages: Schema.Attribute.Relation<
'oneToMany',
'plugin::review-workflows.workflow-stage'
>;
contentTypes: Schema.Attribute.JSON &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<'[]'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::review-workflows.workflow'
>;
};
}
export interface PluginReviewWorkflowsWorkflowStage
extends Struct.CollectionTypeSchema {
collectionName: 'strapi_workflows_stages';
info: {
name: 'Workflow Stage';
description: '';
singularName: 'workflow-stage';
pluralName: 'workflow-stages';
displayName: 'Stages';
};
options: {
version: '1.1.0';
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String;
color: Schema.Attribute.String & Schema.Attribute.DefaultTo<'#4945FF'>;
workflow: Schema.Attribute.Relation<
'manyToOne',
'plugin::review-workflows.workflow'
>;
permissions: Schema.Attribute.Relation<'manyToMany', 'admin::permission'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::review-workflows.workflow-stage'
>;
};
}
export interface PluginUsersPermissionsPermission
extends Struct.CollectionTypeSchema {
collectionName: 'up_permissions';
info: {
name: 'permission';
description: '';
singularName: 'permission';
pluralName: 'permissions';
displayName: 'Permission';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
action: Schema.Attribute.String & Schema.Attribute.Required;
role: Schema.Attribute.Relation<
'manyToOne',
'plugin::users-permissions.role'
>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.permission'
>;
};
}
export interface PluginUsersPermissionsRole
extends Struct.CollectionTypeSchema {
collectionName: 'up_roles';
info: {
name: 'role';
description: '';
singularName: 'role';
pluralName: 'roles';
displayName: 'Role';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 3;
}>;
description: Schema.Attribute.String;
type: Schema.Attribute.String & Schema.Attribute.Unique;
permissions: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.permission'
>;
users: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.user'
>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.role'
>;
};
}
export interface PluginUsersPermissionsUser
extends Struct.CollectionTypeSchema {
collectionName: 'up_users';
info: {
name: 'user';
description: '';
singularName: 'user';
pluralName: 'users';
displayName: 'User';
};
options: {
timestamps: true;
draftAndPublish: false;
};
attributes: {
username: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
minLength: 3;
}>;
email: Schema.Attribute.Email &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 6;
}>;
provider: Schema.Attribute.String;
password: Schema.Attribute.Password &
Schema.Attribute.Private &
Schema.Attribute.SetMinMaxLength<{
minLength: 6;
}>;
resetPasswordToken: Schema.Attribute.String & Schema.Attribute.Private;
confirmationToken: Schema.Attribute.String & Schema.Attribute.Private;
confirmed: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
blocked: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
role: Schema.Attribute.Relation<
'manyToOne',
'plugin::users-permissions.role'
>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.user'
>;
};
}
export interface AdminPermission extends Struct.CollectionTypeSchema {
collectionName: 'admin_permissions';
info: {
name: 'Permission';
description: '';
singularName: 'permission';
pluralName: 'permissions';
displayName: 'Permission';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
action: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
actionParameters: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<{}>;
subject: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
properties: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<{}>;
conditions: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<[]>;
role: Schema.Attribute.Relation<'manyToOne', 'admin::role'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::permission'>;
};
}
export interface AdminUser extends Struct.CollectionTypeSchema {
collectionName: 'admin_users';
info: {
name: 'User';
description: '';
singularName: 'user';
pluralName: 'users';
displayName: 'User';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
firstname: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
lastname: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
username: Schema.Attribute.String;
email: Schema.Attribute.Email &
Schema.Attribute.Required &
Schema.Attribute.Private &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
minLength: 6;
}>;
password: Schema.Attribute.Password &
Schema.Attribute.Private &
Schema.Attribute.SetMinMaxLength<{
minLength: 6;
}>;
resetPasswordToken: Schema.Attribute.String & Schema.Attribute.Private;
registrationToken: Schema.Attribute.String & Schema.Attribute.Private;
isActive: Schema.Attribute.Boolean &
Schema.Attribute.Private &
Schema.Attribute.DefaultTo<false>;
roles: Schema.Attribute.Relation<'manyToMany', 'admin::role'> &
Schema.Attribute.Private;
blocked: Schema.Attribute.Boolean &
Schema.Attribute.Private &
Schema.Attribute.DefaultTo<false>;
preferedLanguage: Schema.Attribute.String;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::user'>;
};
}
export interface AdminRole extends Struct.CollectionTypeSchema {
collectionName: 'admin_roles';
info: {
name: 'Role';
description: '';
singularName: 'role';
pluralName: 'roles';
displayName: 'Role';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
code: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
description: Schema.Attribute.String;
users: Schema.Attribute.Relation<'manyToMany', 'admin::user'>;
permissions: Schema.Attribute.Relation<'oneToMany', 'admin::permission'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::role'>;
};
}
export interface AdminApiToken extends Struct.CollectionTypeSchema {
collectionName: 'strapi_api_tokens';
info: {
name: 'Api Token';
singularName: 'api-token';
pluralName: 'api-tokens';
displayName: 'Api Token';
description: '';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
description: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}> &
Schema.Attribute.DefaultTo<''>;
type: Schema.Attribute.Enumeration<['read-only', 'full-access', 'custom']> &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<'read-only'>;
accessKey: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
lastUsedAt: Schema.Attribute.DateTime;
permissions: Schema.Attribute.Relation<
'oneToMany',
'admin::api-token-permission'
>;
expiresAt: Schema.Attribute.DateTime;
lifespan: Schema.Attribute.BigInteger;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::api-token'>;
};
}
export interface AdminApiTokenPermission extends Struct.CollectionTypeSchema {
collectionName: 'strapi_api_token_permissions';
info: {
name: 'API Token Permission';
description: '';
singularName: 'api-token-permission';
pluralName: 'api-token-permissions';
displayName: 'API Token Permission';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
action: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
token: Schema.Attribute.Relation<'manyToOne', 'admin::api-token'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'admin::api-token-permission'
>;
};
}
export interface AdminTransferToken extends Struct.CollectionTypeSchema {
collectionName: 'strapi_transfer_tokens';
info: {
name: 'Transfer Token';
singularName: 'transfer-token';
pluralName: 'transfer-tokens';
displayName: 'Transfer Token';
description: '';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
description: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}> &
Schema.Attribute.DefaultTo<''>;
accessKey: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
lastUsedAt: Schema.Attribute.DateTime;
permissions: Schema.Attribute.Relation<
'oneToMany',
'admin::transfer-token-permission'
>;
expiresAt: Schema.Attribute.DateTime;
lifespan: Schema.Attribute.BigInteger;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'admin::transfer-token'
>;
};
}
export interface AdminTransferTokenPermission
extends Struct.CollectionTypeSchema {
collectionName: 'strapi_transfer_token_permissions';
info: {
name: 'Transfer Token Permission';
description: '';
singularName: 'transfer-token-permission';
pluralName: 'transfer-token-permissions';
displayName: 'Transfer Token Permission';
};
options: {
draftAndPublish: false;
};
pluginOptions: {
'content-manager': {
visible: false;
};
'content-type-builder': {
visible: false;
};
};
attributes: {
action: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 1;
}>;
token: Schema.Attribute.Relation<'manyToOne', 'admin::transfer-token'>;
createdAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
publishedAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String;
localizations: Schema.Attribute.Relation<
'oneToMany',
'admin::transfer-token-permission'
>;
};
}
declare module '@strapi/strapi' {
export module Public {
export interface ContentTypeSchemas {
'api::home.home': ApiHomeHome;
'api::page.page': ApiPagePage;
'plugin::upload.file': PluginUploadFile;
'plugin::upload.folder': PluginUploadFolder;
'plugin::i18n.locale': PluginI18NLocale;
'plugin::content-releases.release': PluginContentReleasesRelease;
'plugin::content-releases.release-action': PluginContentReleasesReleaseAction;
'plugin::review-workflows.workflow': PluginReviewWorkflowsWorkflow;
'plugin::review-workflows.workflow-stage': PluginReviewWorkflowsWorkflowStage;
'plugin::users-permissions.permission': PluginUsersPermissionsPermission;
'plugin::users-permissions.role': PluginUsersPermissionsRole;
'plugin::users-permissions.user': PluginUsersPermissionsUser;
'admin::permission': AdminPermission;
'admin::user': AdminUser;
'admin::role': AdminRole;
'admin::api-token': AdminApiToken;
'admin::api-token-permission': AdminApiTokenPermission;
'admin::transfer-token': AdminTransferToken;
'admin::transfer-token-permission': AdminTransferTokenPermission;
}
}
}

View File

@ -23,21 +23,21 @@ module.exports = async () => {
// The default types provided by the plugin.
defaultTypes(strapi).map((type) => {
if (!strapi.config.get('plugin.config-sync.excludedTypes').includes(type.configName)) {
if (!strapi.config.get('plugin::config-sync.excludedTypes').includes(type.configName)) {
types[type.configName] = new ConfigType(type);
}
});
// The types provided by other plugins.
strapi.plugin('config-sync').pluginTypes.map((type) => {
if (!strapi.config.get('plugin.config-sync.excludedTypes').includes(type.configName)) {
if (!strapi.config.get('plugin::config-sync.excludedTypes').includes(type.configName)) {
types[type.configName] = new ConfigType(type);
}
});
// The custom types provided by the user.
strapi.config.get('plugin.config-sync.customTypes').map((type) => {
if (!strapi.config.get('plugin.config-sync.excludedTypes').includes(type.configName)) {
strapi.config.get('plugin::config-sync.customTypes').map((type) => {
if (!strapi.config.get('plugin::config-sync.excludedTypes').includes(type.configName)) {
types[type.configName] = new ConfigType(type);
}
});
@ -47,10 +47,12 @@ module.exports = async () => {
strapi.plugin('config-sync').types = registerTypes();
// Import on bootstrap.
if (strapi.config.get('plugin.config-sync.importOnBootstrap')) {
if (strapi.config.get('plugin::config-sync.importOnBootstrap')) {
if (strapi.server.app.env === 'development') {
strapi.log.warn(logMessage(`You can't use the 'importOnBootstrap' setting in the development env.`));
} else if (fs.existsSync(strapi.config.get('plugin.config-sync.syncDir'))) {
} else if (process.env.CONFIG_SYNC_CLI === 'true') {
strapi.log.warn(logMessage(`The 'importOnBootstrap' setting was ignored because Strapi was started from the config-sync CLI itself.`));
} else if (fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
await strapi.plugin('config-sync').service('main').importAllConfig();
}
}

View File

@ -6,7 +6,7 @@ const Table = require('cli-table');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { isEmpty } = require('lodash');
const strapi = require('@strapi/strapi'); // eslint-disable-line
const { createStrapi, compileStrapi } = require('@strapi/strapi');
const gitDiff = require('git-diff');
const warnings = require('./warnings');
@ -15,31 +15,11 @@ const packageJSON = require('../package.json');
const program = new Command();
const getStrapiApp = async () => {
try {
const tsUtils = require('@strapi/typescript-utils'); // eslint-disable-line
const appDir = process.cwd();
const isTSProject = await tsUtils.isUsingTypeScript(appDir);
const outDir = await tsUtils.resolveOutDir(appDir);
const alreadyCompiled = await fs.existsSync(outDir);
if (isTSProject && !alreadyCompiled) {
await tsUtils.compile(appDir, {
watch: false,
configOptions: { options: { incremental: true } },
});
}
const distDir = isTSProject ? outDir : appDir;
const app = await strapi({ appDir, distDir }).load();
process.env.CONFIG_SYNC_CLI = 'true';
const appContext = await compileStrapi();
const app = await createStrapi(appContext).load();
return app;
} catch (e) {
// Fallback for pre Strapi 4.2.
const app = await strapi().load();
return app;
}
};
const initTable = (head) => {
@ -98,7 +78,7 @@ const getConfigState = (diff, configName, syncType) => {
const handleAction = async (syncType, skipConfirm, configType, partials, force) => {
const app = await getStrapiApp();
const hasSyncDir = fs.existsSync(app.config.get('plugin.config-sync.syncDir'));
const hasSyncDir = fs.existsSync(app.config.get('plugin::config-sync.syncDir'));
// No import with empty sync dir.
if (!hasSyncDir && syncType === 'import') {

View File

@ -11,6 +11,7 @@ module.exports = {
excludedConfig: [
"core-store.plugin_users-permissions_grant",
"core-store.plugin_upload_metrics",
"core-store.plugin_upload_api-folder",
"core-store.strapi_content_types_schema",
"core-store.ee_information",
],

View File

@ -1,9 +1,10 @@
const { isEmpty } = require('lodash');
const { logMessage, sanitizeConfig, dynamicSort, noLimit, getCombinedUid, getCombinedUidWhereFilter, getUidParamsFromName } = require('../utils');
const { difference, same } = require('../utils/getArrayDiff');
const queryFallBack = require('../utils/queryFallBack');
const ConfigType = class ConfigType {
constructor({ queryString, configName, uid, jsonFields, relations }) {
constructor({ queryString, configName, uid, jsonFields, relations, components }) {
if (!configName) {
strapi.log.error(logMessage('A config type was registered without a config name.'));
process.exit(0);
@ -25,6 +26,7 @@ const ConfigType = class ConfigType {
this.configPrefix = configName;
this.jsonFields = jsonFields || [];
this.relations = relations || [];
this.components = components || null;
}
/**
@ -37,10 +39,10 @@ const ConfigType = class ConfigType {
*/
importSingle = async (configName, configContent, force) => {
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => `${this.configPrefix}.${configName}`.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => `${this.configPrefix}.${configName}`.startsWith(option)));
if (shouldExclude) return;
const softImport = strapi.config.get('plugin.config-sync.soft');
const softImport = strapi.config.get('plugin::config-sync.soft');
const queryAPI = strapi.query(this.queryString);
const uidParams = getUidParamsFromName(this.uidKeys, configName);
const combinedUidWhereFilter = getCombinedUidWhereFilter(this.uidKeys, uidParams);
@ -68,15 +70,15 @@ const ConfigType = class ConfigType {
});
await Promise.all(relations.map(async (relation) => {
await strapi.query(queryString).delete({
where: { id: relation.id },
});
await queryFallBack.delete(queryString, { where: {
id: relation.id,
}});
}));
}));
await queryAPI.delete({
where: { id: existingConfig.id },
});
await queryFallBack.delete(this.queryString, { where: {
id: existingConfig.id,
}});
return;
}
@ -89,15 +91,17 @@ const ConfigType = class ConfigType {
// Create entity.
this.relations.map(({ relationName }) => delete query[relationName]);
const newEntity = await queryAPI.create({ data: query });
const newEntity = await queryFallBack.create(this.queryString, {
data: query,
});
// Create relation entities.
await Promise.all(this.relations.map(async ({ queryString, relationName, parentName }) => {
const relationQueryApi = strapi.query(queryString);
await Promise.all(configContent[relationName].map(async (relationEntity) => {
const relationQuery = { ...relationEntity, [parentName]: newEntity };
await relationQueryApi.create({ data: relationQuery });
await queryFallBack.create(queryString, {
data: relationQuery,
});
}));
}));
} else { // Config does exist in DB --> update config in DB
@ -105,19 +109,22 @@ const ConfigType = class ConfigType {
if (softImport && !force) return false;
// Format JSON fields.
configContent = sanitizeConfig(configContent);
configContent = sanitizeConfig({
config: configContent,
configName,
});
const query = { ...configContent };
this.jsonFields.map((field) => query[field] = JSON.stringify(configContent[field]));
// Update entity.
this.relations.map(({ relationName }) => delete query[relationName]);
const entity = await queryAPI.update({ where: combinedUidWhereFilter, data: query });
const entity = await queryFallBack.update(this.queryString, { where: combinedUidWhereFilter, data: query });
// Delete/create relations.
await Promise.all(this.relations.map(async ({ queryString, relationName, parentName, relationSortFields }) => {
const relationQueryApi = strapi.query(queryString);
existingConfig = sanitizeConfig(existingConfig, relationName, relationSortFields);
configContent = sanitizeConfig(configContent, relationName, relationSortFields);
existingConfig = sanitizeConfig({ config: existingConfig, configName, relation: relationName, relationSortFields });
configContent = sanitizeConfig({ config: configContent, configName, relation: relationName, relationSortFields });
const configToAdd = difference(configContent[relationName], existingConfig[relationName], relationSortFields);
const configToDelete = difference(existingConfig[relationName], configContent[relationName], relationSortFields);
@ -137,7 +144,7 @@ const ConfigType = class ConfigType {
}));
await Promise.all(configToAdd.map(async (config) => {
await relationQueryApi.create({
await queryFallBack.create(queryString, {
data: { ...config, [parentName]: entity.id },
});
}));
@ -170,7 +177,7 @@ const ConfigType = class ConfigType {
const formattedDiff = await strapi.plugin('config-sync').service('main').getFormattedDiff(this.configPrefix);
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
if (shouldExclude) return;
const currentConfig = formattedDiff.databaseConfig[configName];
@ -203,10 +210,12 @@ const ConfigType = class ConfigType {
* @returns {object} Object with key value pairs of configs.
*/
getAllFromDatabase = async () => {
const AllConfig = await noLimit(strapi.query(this.queryString), {});
const AllConfig = await noLimit(strapi.query(this.queryString), {
populate: this.components,
});
const configs = {};
await Promise.all(Object.values(AllConfig).map(async (config) => {
await Promise.all(Object.entries(AllConfig).map(async ([configName, config]) => {
const combinedUid = getCombinedUid(this.uidKeys, config);
const combinedUidWhereFilter = getCombinedUidWhereFilter(this.uidKeys, config);
@ -216,16 +225,16 @@ const ConfigType = class ConfigType {
}
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => `${this.configPrefix}.${combinedUid}`.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => `${this.configPrefix}.${combinedUid}`.startsWith(option)));
if (shouldExclude) return;
const formattedConfig = { ...sanitizeConfig(config) };
const formattedConfig = { ...sanitizeConfig({ config, configName }) };
await Promise.all(this.relations.map(async ({ queryString, relationName, relationSortFields, parentName }) => {
const relations = await noLimit(strapi.query(queryString), {
where: { [parentName]: combinedUidWhereFilter },
});
relations.map((relation) => sanitizeConfig(relation));
relations.map((relation) => sanitizeConfig({ config: relation, configName: relationName }));
relationSortFields.map((sortField) => {
relations.sort(dynamicSort(sortField));
});

View File

@ -25,7 +25,7 @@ module.exports = {
ctx.send({
message: `Config was successfully exported to ${strapi.config.get('plugin.config-sync.syncDir')}.`,
message: `Config was successfully exported to ${strapi.config.get('plugin::config-sync.syncDir')}.`,
});
},
@ -37,7 +37,7 @@ module.exports = {
*/
importAll: async (ctx) => {
// Check for existance of the config file sync dir.
if (!fs.existsSync(strapi.config.get('plugin.config-sync.syncDir'))) {
if (!fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
ctx.send({
message: 'No config files were found.',
});
@ -73,7 +73,7 @@ module.exports = {
*/
getDiff: async (ctx) => {
// Check for existance of the config file sync dir.
if (!fs.existsSync(strapi.config.get('plugin.config-sync.syncDir'))) {
if (!fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
ctx.send({
message: 'No config files were found.',
});
@ -104,7 +104,7 @@ module.exports = {
getAppEnv: async () => {
return {
env: strapi.server.app.env,
config: strapi.config.get('plugin.config-sync'),
config: strapi.config.get('plugin::config-sync'),
};
},
};

View File

@ -22,23 +22,23 @@ module.exports = () => ({
*/
writeConfigFile: async (configType, configName, fileContents) => {
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => `${configType}.${configName}`.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => `${configType}.${configName}`.startsWith(option)));
if (shouldExclude) return;
// Replace reserved characters in filenames.
configName = configName.replace(/:/g, "#").replace(/\//g, "$");
// Check if the JSON content should be minified.
const json = !strapi.config.get('plugin.config-sync').minify
const json = !strapi.config.get('plugin::config-sync').minify
? JSON.stringify(fileContents, null, 2)
: JSON.stringify(fileContents);
if (!fs.existsSync(strapi.config.get('plugin.config-sync.syncDir'))) {
fs.mkdirSync(strapi.config.get('plugin.config-sync.syncDir'), { recursive: true });
if (!fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
fs.mkdirSync(strapi.config.get('plugin::config-sync.syncDir'), { recursive: true });
}
const writeFile = util.promisify(fs.writeFile);
await writeFile(`${strapi.config.get('plugin.config-sync.syncDir')}${configType}.${configName}.json`, json)
await writeFile(`${strapi.config.get('plugin::config-sync.syncDir')}${configType}.${configName}.json`, json)
.then(() => {
// @TODO:
// Add logging for successfull config export.
@ -57,13 +57,13 @@ module.exports = () => ({
*/
deleteConfigFile: async (configName) => {
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
if (shouldExclude) return;
// Replace reserved characters in filenames.
configName = configName.replace(/:/g, "#").replace(/\//g, "$");
fs.unlinkSync(`${strapi.config.get('plugin.config-sync.syncDir')}${configName}.json`);
fs.unlinkSync(`${strapi.config.get('plugin::config-sync.syncDir')}${configName}.json`);
},
/**
@ -95,7 +95,7 @@ module.exports = () => ({
configName = configName.replace(/:/g, "#").replace(/\//g, "$");
const readFile = util.promisify(fs.readFile);
return readFile(`${strapi.config.get('plugin.config-sync.syncDir')}${configType}.${configName}.json`)
return readFile(`${strapi.config.get('plugin::config-sync.syncDir')}${configType}.${configName}.json`)
.then((data) => {
return JSON.parse(data);
})
@ -112,11 +112,11 @@ module.exports = () => ({
* @returns {object} Object with key value pairs of configs.
*/
getAllConfigFromFiles: async (configType = null) => {
if (!fs.existsSync(strapi.config.get('plugin.config-sync.syncDir'))) {
if (!fs.existsSync(strapi.config.get('plugin::config-sync.syncDir'))) {
return {};
}
const configFiles = fs.readdirSync(strapi.config.get('plugin.config-sync.syncDir'));
const configFiles = fs.readdirSync(strapi.config.get('plugin::config-sync.syncDir'));
const getConfigs = async () => {
const fileConfigs = {};
@ -131,7 +131,7 @@ module.exports = () => ({
if (
configType && configType !== type
|| !strapi.plugin('config-sync').types[type]
|| !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => `${type}.${name}`.startsWith(option)))
|| !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => `${type}.${name}`.startsWith(option)))
) {
return;
}
@ -237,7 +237,7 @@ module.exports = () => ({
*/
importSingleConfig: async (configName, onSuccess, force) => {
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
if (shouldExclude) return;
const type = configName.split('.')[0]; // Grab the first part of the filename.
@ -262,7 +262,7 @@ module.exports = () => ({
*/
exportSingleConfig: async (configName, onSuccess) => {
// Check if the config should be excluded.
const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option)));
if (shouldExclude) return;
const type = configName.split('.')[0]; // Grab the first part of the filename.

View File

@ -46,11 +46,17 @@ const dynamicSort = (property) => {
};
};
const sanitizeConfig = (config, relation, relationSortFields) => {
const sanitizeConfig = ({
config,
relation,
relationSortFields,
configName,
}) => {
delete config._id;
delete config.id;
delete config.updatedAt;
delete config.createdAt;
delete config.publishedAt;
if (relation) {
const formattedRelations = [];
@ -59,6 +65,7 @@ const sanitizeConfig = (config, relation, relationSortFields) => {
delete relationEntity._id;
delete relationEntity.id;
delete relationEntity.updatedAt;
delete config.publishedAt;
delete relationEntity.createdAt;
relationEntity = sortByKeys(relationEntity);
@ -74,6 +81,26 @@ const sanitizeConfig = (config, relation, relationSortFields) => {
config[relation] = formattedRelations;
}
// We recursively sanitize the config to remove environment specific data.
// Except for the plugin_content_manager_configuration.
// This is because that stores the "edit the view" data which includes only configuration, not content.
if (configName && !configName.startsWith('plugin_content_manager_configuration_')) {
const recursiveSanitizeConfig = (recursivedSanitizedConfig) => {
delete recursivedSanitizedConfig._id;
delete recursivedSanitizedConfig.id;
delete recursivedSanitizedConfig.updatedAt;
delete recursivedSanitizedConfig.createdAt;
Object.keys(recursivedSanitizedConfig).map((key, index) => {
if (recursivedSanitizedConfig[key] && typeof recursivedSanitizedConfig[key] === "object") {
recursiveSanitizeConfig(recursivedSanitizedConfig[key]);
}
});
};
recursiveSanitizeConfig(config);
}
return config;
};

View File

@ -0,0 +1,36 @@
const queryFallBack = {
create: async (queryString, options) => {
try {
const newEntity = await strapi.documents(queryString).create(options);
return newEntity;
} catch (e) {
return strapi.query(queryString).create(options);
}
},
update: async (queryString, options) => {
try {
const entity = await strapi.query(queryString).findOne(options);
const updatedEntity = await strapi.documents(queryString).update({
documentId: entity.documentId,
...options,
});
return updatedEntity;
} catch (e) {
return strapi.query(queryString).update(options);
}
},
delete: async (queryString, options) => {
try {
const entity = await strapi.query(queryString).findOne(options);
await strapi.documents(queryString).delete({
documentId: entity.documentId,
});
} catch (e) {
await strapi.query(queryString).delete(options);
}
},
};
module.exports = queryFallBack;

View File

@ -1,3 +1,3 @@
'use strict';
import admin from './admin/src';
module.exports = require('./admin/src').default;
export default admin;

11745
yarn.lock

File diff suppressed because it is too large Load Diff