Skip to content

Commit b9e6a37

Browse files
authored
Merge pull request #5 from boazpoolman/develop
Develop
2 parents 148a92e + 86439e3 commit b9e6a37

File tree

13 files changed

+324
-83
lines changed

13 files changed

+324
-83
lines changed

README.md

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A lot of configuration of your Strapi project is stored in the database. Like core_store, user permissions, user roles & webhooks. Things you might want to have the same on all environments. But when you update them locally, you will have to manually update them on all other environments too.
44

5-
That's where this plugin comes in to play. It allows you to export these configs as individual JSON files for each config, and write them somewhere in your project. With the configs written in your filesystem your can keep track of them through version control (git), and easily pull and import them across environments.
5+
That's where this plugin comes in to play. It allows you to export these configs as individual JSON files for each config, and write them somewhere in your project. With the configs written in your filesystem you can keep track of them through version control (git), and easily pull and import them across environments.
66

77
Importing, exporting and keeping track of config changes is done in the admin page of the plugin.
88

@@ -27,44 +27,50 @@ This way your app won't reload when you export the config in development.
2727

2828
admin: {
2929
auth: {
30-
...
30+
// ...
3131
},
3232
watchIgnoreFiles: [
3333
'**/config-sync/files/**',
34-
]
34+
],
3535
},
3636

3737

3838
## Settings
39-
Some settings for the plugin are able to be modified by creating a file `extensions/config-sync/config/config.json` and overwriting the default settings.
40-
41-
#### Default settings:
42-
{
43-
"destination": "extensions/config-sync/files/",
44-
"minify": false,
45-
"importOnBootstrap": false,
46-
"include": [
47-
"core-store",
48-
"role-permissions",
49-
"webhooks"
50-
],
51-
"exclude": []
52-
}
39+
The settings of the plugin can be overridden in the `config/plugins.js` file.
40+
In the example below you can see how, and also what the default settings are.
41+
42+
##### `config/plugins.js`:
43+
module.exports = ({ env }) => ({
44+
// ...
45+
'config-sync': {
46+
destination: "extensions/config-sync/files/",
47+
minify: false,
48+
importOnBootstrap: false,
49+
include: [
50+
"core-store",
51+
"role-permissions"
52+
],
53+
exclude: [
54+
"core-store.plugin_users-permissions_grant"
55+
]
56+
},
57+
// ...
58+
});
5359

54-
| Property | Default | Description |
55-
| -------- | ------- | ----------- |
56-
| destination | extensions/config-sync/files/ | The path for reading and writing the config JSON files. |
57-
| minify | false | Setting to minify the JSON that's being exported. It defaults to false for better readability in git commits. |
58-
| importOnBootstrap | false | Allows you to let the config be imported automaticly when strapi is bootstrapping (on `yarn start`). This setting should only be used in production, and should be handled very carefully as it can unintendedly overwrite the changes in your database. PLEASE USE WITH CARE. |
59-
| include | ["core-store", "role-permissions", "webhooks"] | Configs you want to include. Allowed values: `core-store`, `role-permissions`, `webhooks`. |
60-
| exclude | [] | You might not want all your database config exported and managed in git. This settings allows you to add an array of config names which should not be tracked by the config-sync plugin. *Currently not working* |
60+
| Property | Type | Description |
61+
| -------- | ---- | ----------- |
62+
| destination | string | The path for reading and writing the sync files. |
63+
| minify | bool | When enabled all the exported JSON files will be minified. |
64+
| importOnBootstrap | bool | Allows you to let the config be imported automaticly when strapi is bootstrapping (on `strapi start`). This setting should only be used in production, and should be handled very carefully as it can unintendedly overwrite the changes in your database. PLEASE USE WITH CARE. |
65+
| include | array | Configs types you want to include in the syncing process. Allowed values: `core-store`, `role-permissions`, `webhooks`. |
66+
| exclude | array | 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. |
6167

6268
## Naming convention
6369
All the config files written in the file destination have the same naming convention. It goes as follows:
6470

6571
[config-type].[config-name].json
6672

67-
- `config-type` - Corresponds to the value in from the config.include setting.
73+
- `config-type` - Corresponds to the value in from the include setting.
6874
- `config-name` - The unique identifier of the config.
6975
- For `core-store` config this is the `key` value.
7076
- For `role-permissions` config this is the `type` value.
@@ -78,7 +84,14 @@ All the config files written in the file destination have the same naming conven
7884
- Exporting of EE roles & permissions
7985
- Add partial import/export functionality
8086
- Add CLI commands for importing/exporting
81-
- Track config deletions
87+
- ~~Track config deletions~~
88+
89+
## ⭐️ Show your support
90+
91+
Give a star if this project helped you.
92+
93+
## Credits
94+
Shout out to [@ScottAgirs](https://github.com/ScottAgirs) for making [strapi-plugin-migrate](https://github.com/ijsto/strapi-plugin-migrate) as it was a big help while making the config-sync plugin.
8295

8396
## Resources
8497

@@ -88,7 +101,3 @@ All the config files written in the file destination have the same naming conven
88101

89102
- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
90103
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
91-
92-
## ⭐️ Show your support
93-
94-
Give a star if this project helped you.

admin/src/components/ActionButtons/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ const ActionButtons = ({ diff }) => {
2323

2424
return (
2525
<ActionButtonsStyling>
26-
<Button disabled={isEmpty(diff.fileConfig)} color="primary" label="Import" onClick={() => openModal('import')} />
27-
<Button disabled={isEmpty(diff.fileConfig)} color="primary" label="Export" onClick={() => openModal('export')} />
26+
<Button disabled={isEmpty(diff.diff)} color="primary" label="Import" onClick={() => openModal('import')} />
27+
<Button disabled={isEmpty(diff.diff)} color="primary" label="Export" onClick={() => openModal('export')} />
28+
{!isEmpty(diff.diff) && (
29+
<h4 style={{ display: 'inline' }}>{Object.keys(diff.diff).length} {Object.keys(diff.diff).length === 1 ? "config change" : "config changes"}</h4>
30+
)}
2831
<ConfirmModal
2932
isOpen={modalIsOpen}
3033
onClose={closeModal}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
4+
const CustomRow = ({ row }) => {
5+
const { config_name, config_type, state, onClick } = row;
6+
7+
8+
return (
9+
<tr onClick={() => onClick(config_type, config_name)}>
10+
<td>
11+
<p>{config_name}</p>
12+
</td>
13+
<td>
14+
<p>{config_type}</p>
15+
</td>
16+
<td>
17+
<p style={stateStyle(state)}>{state}</p>
18+
</td>
19+
</tr>
20+
);
21+
};
22+
23+
const stateStyle = (state) => {
24+
let style = {
25+
display: 'inline-flex',
26+
padding: '0 10px',
27+
borderRadius: '12px',
28+
height: '24px',
29+
alignItems: 'center',
30+
fontWeight: '500',
31+
};
32+
33+
if (state === 'Only in DB') {
34+
style.backgroundColor = '#cbf2d7';
35+
style.color = '#1b522b';
36+
}
37+
38+
if (state === 'Only in sync dir') {
39+
style.backgroundColor = '#f0cac7';
40+
style.color = '#3d302f';
41+
}
42+
43+
if (state === 'Different') {
44+
style.backgroundColor = '#e8e6b7';
45+
style.color = '#4a4934';
46+
}
47+
48+
return style;
49+
};
50+
51+
52+
export default CustomRow

admin/src/components/ConfigList/index.js

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Table } from '@buffetjs/core';
33
import { isEmpty } from 'lodash';
44
import ConfigDiff from '../ConfigDiff';
55
import FirstExport from '../FirstExport';
6+
import ConfigListRow from './ConfigListRow';
67

78
const headers = [
89
{
@@ -14,8 +15,8 @@ const headers = [
1415
value: 'config_type',
1516
},
1617
{
17-
name: 'Change',
18-
value: 'change_type',
18+
name: 'State',
19+
value: 'state',
1920
},
2021
];
2122

@@ -26,21 +27,46 @@ const ConfigList = ({ diff, isLoading }) => {
2627
const [configName, setConfigName] = useState('');
2728
const [rows, setRows] = useState([]);
2829

30+
const getConfigState = (configName) => {
31+
if (
32+
diff.fileConfig[configName] &&
33+
diff.databaseConfig[configName]
34+
) {
35+
return 'Different'
36+
} else if (
37+
diff.fileConfig[configName] &&
38+
!diff.databaseConfig[configName]
39+
) {
40+
return 'Only in sync dir'
41+
} else if (
42+
!diff.fileConfig[configName] &&
43+
diff.databaseConfig[configName]
44+
) {
45+
return 'Only in DB'
46+
}
47+
};
48+
2949
useEffect(() => {
3050
if (isEmpty(diff.diff)) {
3151
setRows([]);
3252
return;
3353
}
3454

3555
let formattedRows = [];
36-
Object.keys(diff.fileConfig).map((configName) => {
56+
Object.keys(diff.diff).map((configName) => {
3757
const type = configName.split('.')[0]; // Grab the first part of the filename.
3858
const name = configName.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
3959

4060
formattedRows.push({
4161
config_name: name,
4262
config_type: type,
43-
change_type: ''
63+
state: getConfigState(configName),
64+
onClick: (config_type, config_name) => {
65+
setOriginalConfig(diff.fileConfig[`${config_type}.${config_name}`]);
66+
setNewConfig(diff.databaseConfig[`${config_type}.${config_name}`]);
67+
setConfigName(`${config_type}.${config_name}`);
68+
setOpenModal(true);
69+
}
4470
});
4571
});
4672

@@ -70,12 +96,7 @@ const ConfigList = ({ diff, isLoading }) => {
7096
/>
7197
<Table
7298
headers={headers}
73-
onClickRow={(e, { config_type, config_name }) => {
74-
setOriginalConfig(diff.fileConfig[`${config_type}.${config_name}`]);
75-
setNewConfig(diff.databaseConfig[`${config_type}.${config_name}`]);
76-
setConfigName(`${config_type}.${config_name}`);
77-
setOpenModal(true);
78-
}}
99+
customRow={ConfigListRow}
79100
rows={!isLoading ? rows : []}
80101
isLoading={isLoading}
81102
tableEmptyText="No config changes. You are up to date!"

admin/src/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export default strapi => {
2323
blockerComponent: null,
2424
blockerComponentProps: {},
2525
description: pluginDescription,
26-
icon: pluginPkg.strapi.icon,
2726
id: pluginId,
2827
initializer: Initializer,
2928
injectedComponents: [],

config/config.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"importOnBootstrap": false,
55
"include": [
66
"core-store",
7-
"role-permissions",
8-
"webhooks"
7+
"role-permissions"
98
],
10-
"exclude": []
9+
"exclude": [
10+
"core-store.plugin_users-permissions_grant"
11+
]
1112
}

controllers/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ module.exports = {
7070
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles();
7171
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase();
7272

73-
const diff = difference(fileConfig, databaseConfig);
73+
const diff = difference(databaseConfig, fileConfig);
74+
7475
formattedDiff.diff = diff;
7576

7677
Object.keys(diff).map((changedConfigName) => {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "strapi-plugin-config-sync",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. ",
55
"strapi": {
66
"name": "config-sync",
7-
"icon": "plug",
7+
"icon": "sync",
88
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. "
99
},
1010
"dependencies": {

services/core-store.js

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const coreStoreQueryString = 'core_store';
44
const configPrefix = 'core-store'; // Should be the same as the filename.
5+
const difference = require('../utils/getObjectDiff');
56

67
/**
78
* Import/Export for core-store configs.
@@ -14,11 +15,38 @@ module.exports = {
1415
* @returns {void}
1516
*/
1617
exportAll: async () => {
17-
const coreStore = await strapi.query(coreStoreQueryString).find({ _limit: -1 });
18+
const formattedDiff = {
19+
fileConfig: {},
20+
databaseConfig: {},
21+
diff: {}
22+
};
23+
24+
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles(configPrefix);
25+
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase(configPrefix);
26+
const diff = difference(databaseConfig, fileConfig);
27+
28+
formattedDiff.diff = diff;
29+
30+
Object.keys(diff).map((changedConfigName) => {
31+
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
32+
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
33+
})
34+
35+
await Promise.all(Object.entries(diff).map(async ([configName, config]) => {
36+
// Check if the config should be excluded.
37+
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configName}`);
38+
if (shouldExclude) return;
1839

19-
await Promise.all(Object.values(coreStore).map(async ({ id, ...config }) => {
20-
config.value = JSON.parse(config.value);
21-
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, config.key, config);
40+
const currentConfig = formattedDiff.databaseConfig[configName];
41+
42+
if (
43+
!currentConfig &&
44+
formattedDiff.fileConfig[configName]
45+
) {
46+
await strapi.plugins['config-sync'].services.main.deleteConfigFile(configName);
47+
} else {
48+
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, currentConfig.key, currentConfig);
49+
}
2250
}));
2351
},
2452

@@ -30,11 +58,22 @@ module.exports = {
3058
* @returns {void}
3159
*/
3260
importSingle: async (configName, configContent) => {
33-
const { value, ...fileContent } = configContent;
61+
// Check if the config should be excluded.
62+
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${configName}`);
63+
if (shouldExclude) return;
64+
3465
const coreStoreAPI = strapi.query(coreStoreQueryString);
3566

3667
const configExists = await coreStoreAPI
37-
.findOne({ key: configName, environment: fileContent.environment });
68+
.findOne({ key: configName });
69+
70+
if (configExists && configContent === null) {
71+
await coreStoreAPI.delete({ key: configName });
72+
73+
return;
74+
}
75+
76+
const { value, ...fileContent } = configContent;
3877

3978
if (!configExists) {
4079
await coreStoreAPI.create({ value: JSON.stringify(value), ...fileContent });
@@ -53,6 +92,10 @@ module.exports = {
5392
let configs = {};
5493

5594
Object.values(coreStore).map( ({ id, value, key, ...config }) => {
95+
// Check if the config should be excluded.
96+
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${key}`);
97+
if (shouldExclude) return;
98+
5699
configs[`${configPrefix}.${key}`] = { key, value: JSON.parse(value), ...config };
57100
});
58101

0 commit comments

Comments
 (0)