From eef725684d64c4250a44a580c5ded9874a6c26ec Mon Sep 17 00:00:00 2001 From: boazpoolman Date: Mon, 28 Nov 2022 17:00:35 +0100 Subject: [PATCH 1/2] feat: Addition of the 'soft' setting --- README.md | 18 +++++++++++++++ admin/src/components/ActionButtons/index.js | 2 +- admin/src/components/ConfirmModal/index.js | 25 +++++++++++++++++++-- admin/src/containers/ConfigPage/index.js | 2 +- admin/src/state/actions/Config.js | 11 +++++---- admin/src/state/reducers/Config/index.js | 2 +- admin/src/translations/en.json | 1 + server/cli.js | 9 ++++---- server/config.js | 1 + server/config/type.js | 16 ++++++++++--- server/controllers/config.js | 13 ++++++----- server/services/main.js | 7 +++--- 12 files changed, 83 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 938de07..18d5820 100644 --- a/README.md +++ b/README.md @@ -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**. diff --git a/admin/src/components/ActionButtons/index.js b/admin/src/components/ActionButtons/index.js index 07f052a..4fa188b 100644 --- a/admin/src/components/ActionButtons/index.js +++ b/admin/src/components/ActionButtons/index.js @@ -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))} /> ); diff --git a/admin/src/components/ConfirmModal/index.js b/admin/src/components/ConfirmModal/index.js index 9ad4e23..9061071 100644 --- a/admin/src/components/ConfirmModal/index.js +++ b/admin/src/components/ConfirmModal/index.js @@ -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 }) => { + {(soft && type === 'import') && ( + + + + setForce(value)} + value={force} + name="force" + hint="Check this to ignore the soft setting." + > + {formatMessage({ id: 'config-sync.popUpWarning.force' })} + + + + )} { variant="secondary" onClick={() => { onClose(); - onSubmit(); + onSubmit(force); }} > {formatMessage({ id: `config-sync.popUpWarning.button.${type}` })} diff --git a/admin/src/containers/ConfigPage/index.js b/admin/src/containers/ConfigPage/index.js index a2e9083..5dee8db 100644 --- a/admin/src/containers/ConfigPage/index.js +++ b/admin/src/containers/ConfigPage/index.js @@ -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)); diff --git a/admin/src/state/actions/Config.js b/admin/src/state/actions/Config.js index 4babcc8..8e149b9 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -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' } }); } diff --git a/admin/src/state/reducers/Config/index.js b/admin/src/state/reducers/Config/index.js index 4af6f32..71a06e1 100644 --- a/admin/src/state/reducers/Config/index.js +++ b/admin/src/state/reducers/Config/index.js @@ -16,7 +16,7 @@ const initialState = fromJS({ configDiff: Map({}), partialDiff: List([]), isLoading: false, - appEnv: 'development', + appEnv: Map({}), }); export default function configReducer(state = initialState, action) { diff --git a/admin/src/translations/en.json b/admin/src/translations/en.json index 1a2eb3e..4a075af 100644 --- a/admin/src/translations/en.json +++ b/admin/src/translations/en.json @@ -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.", diff --git a/server/cli.js b/server/cli.js index c82b068..f494051 100644 --- a/server/cli.js +++ b/server/cli.js @@ -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 ', 'The type of config') .option('-p, --partial ', '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` diff --git a/server/config.js b/server/config.js index 6ad4ca1..860c13f 100644 --- a/server/config.js +++ b/server/config.js @@ -4,6 +4,7 @@ module.exports = { default: { syncDir: "config/sync/", minify: false, + soft: false, importOnBootstrap: false, customTypes: [], excludedTypes: [], diff --git a/server/config/type.js b/server/config/type.js index 4578e77..b397a3e 100644 --- a/server/config/type.js +++ b/server/config/type.js @@ -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 }; diff --git a/server/controllers/config.js b/server/controllers/config.js index 70da936..4de75ee 100644 --- a/server/controllers/config.js +++ b/server/controllers/config.js @@ -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'), + }; }, }; diff --git a/server/services/main.js b/server/services/main.js index 0dba538..c603f62 100644 --- a/server/services/main.js +++ b/server/services/main.js @@ -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}`); } From f300183630fa9cee5eb5b50240c105652b1ff5e8 Mon Sep 17 00:00:00 2001 From: boazpoolman Date: Mon, 28 Nov 2022 18:40:44 +0100 Subject: [PATCH 2/2] style: Fix eslint issues --- .eslintrc | 2 ++ admin/src/state/actions/Config.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 1f2472f..6120fb2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,6 +30,8 @@ "indent" : "off", + "react/jsx-fragments": "off", + "react/jsx-props-no-spreading": "off", "react-hooks/rules-of-hooks": "error", diff --git a/admin/src/state/actions/Config.js b/admin/src/state/actions/Config.js index 8e149b9..5922bfe 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -64,7 +64,7 @@ export function importAllConfig(partialDiff, force, toggleNotification) { body: { force, config: partialDiff, - } + }, }); toggleNotification({ type: 'success', message }); dispatch(getAllConfigDiff(toggleNotification));