feat: Addition of the 'soft' setting

pull/78/head
boazpoolman 2022-11-28 17:00:35 +01:00
parent ad7fc93b84
commit eef725684d
12 changed files with 83 additions and 24 deletions

View File

@ -175,6 +175,15 @@ Use this flag to sync a specific set of configs by giving the CLI a comma-separa
```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`
@ -342,6 +351,7 @@ In the example below you can see how, and also what the default settings are.
config: {
syncDir: "config/sync/",
minify: false,
soft: false,
importOnBootstrap: false,
customTypes: [],
excludedTypes: [],
@ -369,6 +379,14 @@ When enabled all the exported JSON files will be minified.
> `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**.

View File

@ -37,7 +37,7 @@ const ActionButtons = () => {
isOpen={modalIsOpen}
onClose={closeModal}
type={actionType}
onSubmit={() => actionType === 'import' ? dispatch(importAllConfig(partialDiff, toggleNotification)) : dispatch(exportAllConfig(partialDiff, toggleNotification))}
onSubmit={(force) => actionType === 'import' ? dispatch(importAllConfig(partialDiff, force, toggleNotification)) : dispatch(exportAllConfig(partialDiff, toggleNotification))}
/>
</ActionButtonsStyling>
);

View File

@ -1,5 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import { useSelector } from 'react-redux';
import {
Dialog,
@ -9,10 +10,15 @@ import {
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;
@ -33,6 +39,21 @@ const ConfirmModal = ({ isOpen, onClose, onSubmit, type }) => {
</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
@ -49,7 +70,7 @@ const ConfirmModal = ({ isOpen, onClose, onSubmit, type }) => {
variant="secondary"
onClick={() => {
onClose();
onSubmit();
onSubmit(force);
}}
>
{formatMessage({ id: `config-sync.popUpWarning.button.${type}` })}

View File

@ -18,7 +18,7 @@ const ConfigPage = () => {
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']));
const appEnv = useSelector((state) => state.getIn(['config', 'appEnv', 'env']));
useEffect(() => {
dispatch(getAllConfigDiff(toggleNotification));

View File

@ -55,13 +55,16 @@ export function exportAllConfig(partialDiff, toggleNotification) {
};
}
export function importAllConfig(partialDiff, toggleNotification) {
export function importAllConfig(partialDiff, force, toggleNotification) {
return async function(dispatch) {
dispatch(setLoadingState(true));
try {
const { message } = await request('/config-sync/import', {
method: 'POST',
body: partialDiff,
body: {
force,
config: partialDiff,
}
});
toggleNotification({ type: 'success', message });
dispatch(getAllConfigDiff(toggleNotification));
@ -84,10 +87,10 @@ export function setLoadingState(value) {
export function getAppEnv(toggleNotification) {
return async function(dispatch) {
try {
const { env } = await request('/config-sync/app-env', {
const envVars = await request('/config-sync/app-env', {
method: 'GET',
});
dispatch(setAppEnvInState(env));
dispatch(setAppEnvInState(envVars));
} catch (err) {
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
}

View File

@ -16,7 +16,7 @@ const initialState = fromJS({
configDiff: Map({}),
partialDiff: List([]),
isLoading: false,
appEnv: 'development',
appEnv: Map({}),
});
export default function configReducer(state = initialState, action) {

View File

@ -6,6 +6,7 @@
"popUpWarning.button.import": "Yes, import",
"popUpWarning.button.export": "Yes, export",
"popUpWarning.button.cancel": "Cancel",
"popUpWarning.force": "Force",
"Header.Title": "Config Sync",
"Header.Description": "Manage your database config across environments.",

View File

@ -95,7 +95,7 @@ const getConfigState = (diff, configName, syncType) => {
}
};
const handleAction = async (syncType, skipConfirm, configType, partials) => {
const handleAction = async (syncType, skipConfirm, configType, partials, force) => {
const app = await getStrapiApp();
const hasSyncDir = fs.existsSync(app.config.get('plugin.config-sync.syncDir'));
@ -173,7 +173,7 @@ const handleAction = async (syncType, skipConfirm, configType, partials) => {
&& warnings.delete[name]
) warning = warnings.delete[name];
await app.plugin('config-sync').service('main').importSingleConfig(name, onSuccess);
await app.plugin('config-sync').service('main').importSingleConfig(name, onSuccess, force);
if (warning) console.log(`${chalk.yellow.bold('[warning]')} ${warning}`);
}));
console.log(`${chalk.green.bold('[success]')} Finished import`);
@ -221,9 +221,10 @@ program
.option('-t, --type <type>', 'The type of config')
.option('-p, --partial <partials>', 'A comma separated string of configs')
.option('-y, --yes', 'Skip the confirm prompt')
.option('-f, --force', 'Ignore the soft setting')
.description('Import the config')
.action(async ({ yes, type, partial }) => {
return handleAction('import', yes, type, partial);
.action(async ({ yes, type, partial, force }) => {
return handleAction('import', yes, type, partial, force);
});
// `$ config-sync export`

View File

@ -4,6 +4,7 @@ module.exports = {
default: {
syncDir: "config/sync/",
minify: false,
soft: false,
importOnBootstrap: false,
customTypes: [],
excludedTypes: [],

View File

@ -32,13 +32,15 @@ const ConfigType = class ConfigType {
*
* @param {string} configName - The name of the config file.
* @param {string} configContent - The JSON content of the config file.
* @param {boolean} force - Ignore the soft setting.
* @returns {void}
*/
importSingle = async (configName, configContent) => {
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)));
if (shouldExclude) return;
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);
@ -48,7 +50,11 @@ const ConfigType = class ConfigType {
populate: this.relations.map(({ relationName }) => relationName),
});
if (existingConfig && configContent === null) { // Config exists in DB but no configfile content --> delete config from DB
// Config exists in DB but no configfile content --> delete config from DB
if (existingConfig && configContent === null) {
// Don't preform action when soft setting is true.
if (softImport && !force) return false;
await Promise.all(this.relations.map(async ({ queryString, parentName }) => {
const relations = await noLimit(strapi.query(queryString), {
where: {
@ -70,7 +76,8 @@ const ConfigType = class ConfigType {
return;
}
if (!existingConfig) { // Config does not exist in DB --> create config in DB
// Config does not exist in DB --> create config in DB
if (!existingConfig) {
// Format JSON fields.
const query = { ...configContent };
this.jsonFields.map((field) => query[field] = JSON.stringify(configContent[field]));
@ -89,6 +96,9 @@ const ConfigType = class ConfigType {
}));
}));
} else { // Config does exist in DB --> update config in DB
// Don't preform action when soft setting is true.
if (softImport && !force) return false;
// Format JSON fields.
configContent = sanitizeConfig(configContent);
const query = { ...configContent };

View File

@ -45,16 +45,16 @@ module.exports = {
return;
}
if (!ctx.request.body) {
if (!ctx.request.body.config) {
ctx.send({
message: 'No config was specified for the export endpoint.',
message: 'No config was specified for the import endpoint.',
});
return;
}
await Promise.all(ctx.request.body.map(async (configName) => {
await strapi.plugin('config-sync').service('main').importSingleConfig(configName);
await Promise.all(ctx.request.body.config.map(async (configName) => {
await strapi.plugin('config-sync').service('main').importSingleConfig(configName, null, ctx.request.body.force);
}));
ctx.send({
@ -89,6 +89,9 @@ module.exports = {
* @returns {string} The current Strapi environment.
*/
getAppEnv: async () => {
return { env: strapi.server.app.env };
return {
env: strapi.server.app.env,
config: strapi.config.get('plugin.config-sync'),
};
},
};

View File

@ -214,9 +214,10 @@ module.exports = () => ({
*
* @param {string} configName - The name of the config file.
* @param {object} onSuccess - Success callback to run on each single successfull import.
* @param {boolean} force - Ignore the soft setting.
* @returns {void}
*/
importSingleConfig: async (configName, onSuccess) => {
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)));
if (shouldExclude) return;
@ -226,8 +227,8 @@ module.exports = () => ({
const fileContents = await strapi.plugin('config-sync').service('main').readConfigFile(type, name);
try {
await strapi.plugin('config-sync').types[type].importSingle(name, fileContents);
if (onSuccess) onSuccess(`${type}.${name}`);
const importState = await strapi.plugin('config-sync').types[type].importSingle(name, fileContents, force);
if (onSuccess && importState !== false) onSuccess(`${type}.${name}`);
} catch (e) {
throw new Error(`Error when trying to import ${type}.${name}. ${e}`);
}