feat: Addition of the 'soft' setting
parent
ad7fc93b84
commit
eef725684d
18
README.md
18
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**.
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}` })}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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' } });
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ const initialState = fromJS({
|
|||
configDiff: Map({}),
|
||||
partialDiff: List([]),
|
||||
isLoading: false,
|
||||
appEnv: 'development',
|
||||
appEnv: Map({}),
|
||||
});
|
||||
|
||||
export default function configReducer(state = initialState, action) {
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
default: {
|
||||
syncDir: "config/sync/",
|
||||
minify: false,
|
||||
soft: false,
|
||||
importOnBootstrap: false,
|
||||
customTypes: [],
|
||||
excludedTypes: [],
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue