Skip to content

Commit

Permalink
Merge pull request #5 from boazpoolman/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
boazpoolman authored Mar 27, 2021
2 parents 148a92e + 86439e3 commit b9e6a37
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 83 deletions.
69 changes: 39 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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.

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.
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.

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

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

admin: {
auth: {
...
// ...
},
watchIgnoreFiles: [
'**/config-sync/files/**',
]
],
},


## Settings
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.

#### Default settings:
{
"destination": "extensions/config-sync/files/",
"minify": false,
"importOnBootstrap": false,
"include": [
"core-store",
"role-permissions",
"webhooks"
],
"exclude": []
}
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': {
destination: "extensions/config-sync/files/",
minify: false,
importOnBootstrap: false,
include: [
"core-store",
"role-permissions"
],
exclude: [
"core-store.plugin_users-permissions_grant"
]
},
// ...
});

| Property | Default | Description |
| -------- | ------- | ----------- |
| destination | extensions/config-sync/files/ | The path for reading and writing the config JSON files. |
| minify | false | Setting to minify the JSON that's being exported. It defaults to false for better readability in git commits. |
| 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. |
| include | ["core-store", "role-permissions", "webhooks"] | Configs you want to include. Allowed values: `core-store`, `role-permissions`, `webhooks`. |
| 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* |
| Property | Type | Description |
| -------- | ---- | ----------- |
| destination | string | The path for reading and writing the sync files. |
| minify | bool | When enabled all the exported JSON files will be minified. |
| 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. |
| include | array | Configs types you want to include in the syncing process. Allowed values: `core-store`, `role-permissions`, `webhooks`. |
| 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. |

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

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

- `config-type` - Corresponds to the value in from the config.include setting.
- `config-type` - Corresponds to the value in from the include setting.
- `config-name` - The unique identifier of the config.
- For `core-store` config this is the `key` value.
- For `role-permissions` config this is the `type` value.
Expand All @@ -78,7 +84,14 @@ All the config files written in the file destination have the same naming conven
- Exporting of EE roles & permissions
- Add partial import/export functionality
- Add CLI commands for importing/exporting
- Track config deletions
- ~~Track config deletions~~

## ⭐️ Show your support

Give a star if this project helped you.

## Credits
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.

## Resources

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

- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)

## ⭐️ Show your support

Give a star if this project helped you.
7 changes: 5 additions & 2 deletions admin/src/components/ActionButtons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ const ActionButtons = ({ diff }) => {

return (
<ActionButtonsStyling>
<Button disabled={isEmpty(diff.fileConfig)} color="primary" label="Import" onClick={() => openModal('import')} />
<Button disabled={isEmpty(diff.fileConfig)} color="primary" label="Export" onClick={() => openModal('export')} />
<Button disabled={isEmpty(diff.diff)} color="primary" label="Import" onClick={() => openModal('import')} />
<Button disabled={isEmpty(diff.diff)} color="primary" label="Export" onClick={() => openModal('export')} />
{!isEmpty(diff.diff) && (
<h4 style={{ display: 'inline' }}>{Object.keys(diff.diff).length} {Object.keys(diff.diff).length === 1 ? "config change" : "config changes"}</h4>
)}
<ConfirmModal
isOpen={modalIsOpen}
onClose={closeModal}
Expand Down
52 changes: 52 additions & 0 deletions admin/src/components/ConfigList/ConfigListRow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import styled from 'styled-components';

const CustomRow = ({ row }) => {
const { config_name, config_type, state, onClick } = row;


return (
<tr onClick={() => onClick(config_type, config_name)}>
<td>
<p>{config_name}</p>
</td>
<td>
<p>{config_type}</p>
</td>
<td>
<p style={stateStyle(state)}>{state}</p>
</td>
</tr>
);
};

const stateStyle = (state) => {
let style = {
display: 'inline-flex',
padding: '0 10px',
borderRadius: '12px',
height: '24px',
alignItems: 'center',
fontWeight: '500',
};

if (state === 'Only in DB') {
style.backgroundColor = '#cbf2d7';
style.color = '#1b522b';
}

if (state === 'Only in sync dir') {
style.backgroundColor = '#f0cac7';
style.color = '#3d302f';
}

if (state === 'Different') {
style.backgroundColor = '#e8e6b7';
style.color = '#4a4934';
}

return style;
};


export default CustomRow
41 changes: 31 additions & 10 deletions admin/src/components/ConfigList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Table } from '@buffetjs/core';
import { isEmpty } from 'lodash';
import ConfigDiff from '../ConfigDiff';
import FirstExport from '../FirstExport';
import ConfigListRow from './ConfigListRow';

const headers = [
{
Expand All @@ -14,8 +15,8 @@ const headers = [
value: 'config_type',
},
{
name: 'Change',
value: 'change_type',
name: 'State',
value: 'state',
},
];

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

const getConfigState = (configName) => {
if (
diff.fileConfig[configName] &&
diff.databaseConfig[configName]
) {
return 'Different'
} else if (
diff.fileConfig[configName] &&
!diff.databaseConfig[configName]
) {
return 'Only in sync dir'
} else if (
!diff.fileConfig[configName] &&
diff.databaseConfig[configName]
) {
return 'Only in DB'
}
};

useEffect(() => {
if (isEmpty(diff.diff)) {
setRows([]);
return;
}

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

formattedRows.push({
config_name: name,
config_type: type,
change_type: ''
state: getConfigState(configName),
onClick: (config_type, config_name) => {
setOriginalConfig(diff.fileConfig[`${config_type}.${config_name}`]);
setNewConfig(diff.databaseConfig[`${config_type}.${config_name}`]);
setConfigName(`${config_type}.${config_name}`);
setOpenModal(true);
}
});
});

Expand Down Expand Up @@ -70,12 +96,7 @@ const ConfigList = ({ diff, isLoading }) => {
/>
<Table
headers={headers}
onClickRow={(e, { config_type, config_name }) => {
setOriginalConfig(diff.fileConfig[`${config_type}.${config_name}`]);
setNewConfig(diff.databaseConfig[`${config_type}.${config_name}`]);
setConfigName(`${config_type}.${config_name}`);
setOpenModal(true);
}}
customRow={ConfigListRow}
rows={!isLoading ? rows : []}
isLoading={isLoading}
tableEmptyText="No config changes. You are up to date!"
Expand Down
1 change: 0 additions & 1 deletion admin/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export default strapi => {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
initializer: Initializer,
injectedComponents: [],
Expand Down
7 changes: 4 additions & 3 deletions config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"importOnBootstrap": false,
"include": [
"core-store",
"role-permissions",
"webhooks"
"role-permissions"
],
"exclude": []
"exclude": [
"core-store.plugin_users-permissions_grant"
]
}
3 changes: 2 additions & 1 deletion controllers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ module.exports = {
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles();
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase();

const diff = difference(fileConfig, databaseConfig);
const diff = difference(databaseConfig, fileConfig);

formattedDiff.diff = diff;

Object.keys(diff).map((changedConfigName) => {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "strapi-plugin-config-sync",
"version": "0.1.3",
"version": "0.1.4",
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. ",
"strapi": {
"name": "config-sync",
"icon": "plug",
"icon": "sync",
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. "
},
"dependencies": {
Expand Down
55 changes: 49 additions & 6 deletions services/core-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

/**
* Import/Export for core-store configs.
Expand All @@ -14,11 +15,38 @@ module.exports = {
* @returns {void}
*/
exportAll: async () => {
const coreStore = await strapi.query(coreStoreQueryString).find({ _limit: -1 });
const formattedDiff = {
fileConfig: {},
databaseConfig: {},
diff: {}
};

const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles(configPrefix);
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase(configPrefix);
const diff = difference(databaseConfig, fileConfig);

formattedDiff.diff = diff;

Object.keys(diff).map((changedConfigName) => {
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
})

await Promise.all(Object.entries(diff).map(async ([configName, config]) => {
// Check if the config should be excluded.
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configName}`);
if (shouldExclude) return;

await Promise.all(Object.values(coreStore).map(async ({ id, ...config }) => {
config.value = JSON.parse(config.value);
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, config.key, config);
const currentConfig = formattedDiff.databaseConfig[configName];

if (
!currentConfig &&
formattedDiff.fileConfig[configName]
) {
await strapi.plugins['config-sync'].services.main.deleteConfigFile(configName);
} else {
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, currentConfig.key, currentConfig);
}
}));
},

Expand All @@ -30,11 +58,22 @@ module.exports = {
* @returns {void}
*/
importSingle: async (configName, configContent) => {
const { value, ...fileContent } = configContent;
// Check if the config should be excluded.
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${configName}`);
if (shouldExclude) return;

const coreStoreAPI = strapi.query(coreStoreQueryString);

const configExists = await coreStoreAPI
.findOne({ key: configName, environment: fileContent.environment });
.findOne({ key: configName });

if (configExists && configContent === null) {
await coreStoreAPI.delete({ key: configName });

return;
}

const { value, ...fileContent } = configContent;

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

Object.values(coreStore).map( ({ id, value, key, ...config }) => {
// Check if the config should be excluded.
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${key}`);
if (shouldExclude) return;

configs[`${configPrefix}.${key}`] = { key, value: JSON.parse(value), ...config };
});

Expand Down
Loading

0 comments on commit b9e6a37

Please sign in to comment.