commit
cc7da36de0
32
README.md
32
README.md
|
@ -6,9 +6,7 @@ That's where this plugin comes in to play. It allows you to export these configs
|
||||||
|
|
||||||
Importing, exporting and keeping track of config changes is done in the admin page of the plugin.
|
Importing, exporting and keeping track of config changes is done in the admin page of the plugin.
|
||||||
|
|
||||||
*Currently only the core_store changes are being tracked.*
|
**THIS PLUGIN IS NOT STABLE**
|
||||||
|
|
||||||
**THIS PLUGIN IS STILL IN DEVELOPMENT**
|
|
||||||
|
|
||||||
**PLEASE USE WITH CARE**
|
**PLEASE USE WITH CARE**
|
||||||
|
|
||||||
|
@ -45,20 +43,40 @@ PLEASE USE WITH CARE.
|
||||||
|
|
||||||
> `required:` NO | `type:` bool | `default:` false
|
> `required:` NO | `type:` bool | `default:` false
|
||||||
|
|
||||||
|
#### `include`
|
||||||
|
|
||||||
|
Configs you want to include. Allowed values: `core-store`, `role-permissions`, `webhooks`. By default these are all enabled.
|
||||||
|
|
||||||
|
> `required:` NO | `type:` array | `default:` ["core-store", "role-permissions", "webhooks"]
|
||||||
|
|
||||||
#### `exclude`
|
#### `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.
|
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.
|
||||||
|
|
||||||
For now only the `core_store` table is being tracked. Add the `key` value of a `core_store` item to the array to exclude it from being tracked.
|
*Currently not working*
|
||||||
|
|
||||||
> `required:` NO | `type:` array | `default:` []
|
> `required:` NO | `type:` array | `default:` []
|
||||||
|
|
||||||
|
## 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-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.
|
||||||
|
- For `webhooks` config this is the `id` value
|
||||||
|
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
- Exporting of user roles & permissions
|
- ~~Exporting of user roles & permissions~~
|
||||||
- Exporting of webhooks
|
- ~~Exporting of webhooks~~
|
||||||
- Specify which tables you want to track in the plugin configurations
|
- ~~Specify which tables you want to track in the plugin configurations~~
|
||||||
|
- Exporting of EE roles & permissions
|
||||||
- Add partial import/export functionality
|
- Add partial import/export functionality
|
||||||
- Add CLI commands for importing/exporting
|
- Add CLI commands for importing/exporting
|
||||||
|
- Track config deletions
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ const ActionButtons = ({ diff }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionButtonsStyling>
|
<ActionButtonsStyling>
|
||||||
<Button disabled={isEmpty(diff)} color="primary" label="Import" onClick={() => openModal('import')} />
|
<Button disabled={isEmpty(diff.diff)} color="primary" label="Import" onClick={() => openModal('import')} />
|
||||||
<Button disabled={isEmpty(diff)} color="primary" label="Export" onClick={() => openModal('export')} />
|
<Button disabled={isEmpty(diff.diff)} color="primary" label="Export" onClick={() => openModal('export')} />
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={modalIsOpen}
|
isOpen={modalIsOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Table } from '@buffetjs/core';
|
import { Table } from '@buffetjs/core';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import ConfigDiff from '../ConfigDiff';
|
import ConfigDiff from '../ConfigDiff';
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
|
@ -8,8 +9,8 @@ const headers = [
|
||||||
value: 'config_name',
|
value: 'config_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Database table',
|
name: 'Config type',
|
||||||
value: 'table_name',
|
value: 'config_type',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Change',
|
name: 'Change',
|
||||||
|
@ -17,7 +18,7 @@ const headers = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ConfigList = ({ fileConfig, databaseConfig, diff, isLoading }) => {
|
const ConfigList = ({ diff, isLoading }) => {
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [originalConfig, setOriginalConfig] = useState({});
|
const [originalConfig, setOriginalConfig] = useState({});
|
||||||
const [newConfig, setNewConfig] = useState({});
|
const [newConfig, setNewConfig] = useState({});
|
||||||
|
@ -25,15 +26,23 @@ const ConfigList = ({ fileConfig, databaseConfig, diff, isLoading }) => {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isEmpty(diff)) {
|
||||||
|
setRows([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let formattedRows = [];
|
let formattedRows = [];
|
||||||
Object.keys(diff).map((config) => {
|
Object.keys(diff.fileConfig).map((configName) => {
|
||||||
// @TODO implement different config types, roles/permissions e.g.
|
const type = configName.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const name = configName.split(/\.(.+)/)[1].split('.')[0] // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
formattedRows.push({
|
formattedRows.push({
|
||||||
config_name: config,
|
config_name: name,
|
||||||
table_name: 'core_store',
|
config_type: type,
|
||||||
change_type: ''
|
change_type: ''
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setRows(formattedRows);
|
setRows(formattedRows);
|
||||||
}, [diff]);
|
}, [diff]);
|
||||||
|
|
||||||
|
@ -56,10 +65,10 @@ const ConfigList = ({ fileConfig, databaseConfig, diff, isLoading }) => {
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
headers={headers}
|
headers={headers}
|
||||||
onClickRow={(e, data) => {
|
onClickRow={(e, { config_type, config_name }) => {
|
||||||
setOriginalConfig(fileConfig.get(data.config_name));
|
setOriginalConfig(diff.fileConfig[`${config_type}.${config_name}`]);
|
||||||
setNewConfig(databaseConfig.get(data.config_name));
|
setNewConfig(diff.databaseConfig[`${config_type}.${config_name}`]);
|
||||||
setConfigName(data.config_name);
|
setConfigName(`${config_type}.${config_name}`);
|
||||||
setOpenModal(true);
|
setOpenModal(true);
|
||||||
}}
|
}}
|
||||||
rows={!isLoading ? rows : []}
|
rows={!isLoading ? rows : []}
|
||||||
|
|
|
@ -2,27 +2,23 @@ import React, { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
import { getAllConfig } from '../../state/actions/Config';
|
import { getAllConfigDiff } from '../../state/actions/Config';
|
||||||
import ConfigList from '../../components/ConfigList';
|
import ConfigList from '../../components/ConfigList';
|
||||||
import ActionButtons from '../../components/ActionButtons';
|
import ActionButtons from '../../components/ActionButtons';
|
||||||
import difference from '../../helpers/getObjectDiff';
|
|
||||||
|
|
||||||
const ConfigPage = () => {
|
const ConfigPage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading']), Map());
|
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading']), Map());
|
||||||
const fileConfig = useSelector((state) => state.getIn(['config', 'fileConfig']), Map());
|
const configDiff = useSelector((state) => state.getIn(['config', 'configDiff']), Map());
|
||||||
const databaseConfig = useSelector((state) => state.getIn(['config', 'databaseConfig']), Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllConfig());
|
dispatch(getAllConfigDiff());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const diff = difference(fileConfig.toJS(), databaseConfig.toJS());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ActionButtons diff={diff} />
|
<ActionButtons diff={configDiff.toJS()} />
|
||||||
<ConfigList fileConfig={fileConfig} databaseConfig={databaseConfig} isLoading={isLoading} diff={diff} />
|
<ConfigList isLoading={isLoading} diff={configDiff.toJS()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,12 @@
|
||||||
import { request } from 'strapi-helper-plugin';
|
import { request } from 'strapi-helper-plugin';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
export function getAllConfig() {
|
export function getAllConfigDiff() {
|
||||||
return async function(dispatch) {
|
return async function(dispatch) {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const databaseConfig = await request('/config-sync/all/from-database', { method: 'GET' });
|
const configDiff = await request('/config-sync/diff', { method: 'GET' });
|
||||||
const fileConfig = await request('/config-sync/all/from-files', { method: 'GET' });
|
dispatch(setConfigDiffInState(configDiff));
|
||||||
dispatch(setFileConfigInState(fileConfig));
|
|
||||||
dispatch(setDatabaseConfigInState(databaseConfig));
|
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
strapi.notification.error('notification.error');
|
strapi.notification.error('notification.error');
|
||||||
|
@ -23,18 +21,10 @@ export function getAllConfig() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_DATABASE_CONFIG_IN_STATE = 'SET_DATABASE_CONFIG_IN_STATE';
|
export const SET_CONFIG_DIFF_IN_STATE = 'SET_CONFIG_DIFF_IN_STATE';
|
||||||
export function setDatabaseConfigInState(config) {
|
export function setConfigDiffInState(config) {
|
||||||
return {
|
return {
|
||||||
type: SET_DATABASE_CONFIG_IN_STATE,
|
type: SET_CONFIG_DIFF_IN_STATE,
|
||||||
config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SET_FILE_CONFIG_IN_STATE = 'SET_FILE_CONFIG_IN_STATE';
|
|
||||||
export function setFileConfigInState(config) {
|
|
||||||
return {
|
|
||||||
type: SET_FILE_CONFIG_IN_STATE,
|
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -44,8 +34,7 @@ export function exportAllConfig() {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const { message } = await request('/config-sync/export', { method: 'GET' });
|
const { message } = await request('/config-sync/export', { method: 'GET' });
|
||||||
dispatch(setFileConfigInState(Map({})));
|
dispatch(setConfigDiffInState(Map({})));
|
||||||
dispatch(setDatabaseConfigInState(Map({})));
|
|
||||||
|
|
||||||
strapi.notification.success(message);
|
strapi.notification.success(message);
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
|
@ -61,8 +50,7 @@ export function importAllConfig() {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const { message } = await request('/config-sync/import', { method: 'GET' });
|
const { message } = await request('/config-sync/import', { method: 'GET' });
|
||||||
dispatch(setFileConfigInState(Map({})));
|
dispatch(setConfigDiffInState(Map({})));
|
||||||
dispatch(setDatabaseConfigInState(Map({})));
|
|
||||||
|
|
||||||
strapi.notification.success(message);
|
strapi.notification.success(message);
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
|
|
|
@ -5,22 +5,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fromJS, Map } from 'immutable';
|
import { fromJS, Map } from 'immutable';
|
||||||
import { SET_DATABASE_CONFIG_IN_STATE, SET_FILE_CONFIG_IN_STATE, SET_LOADING_STATE } from '../../actions/Config';
|
import { SET_CONFIG_DIFF_IN_STATE, SET_LOADING_STATE } from '../../actions/Config';
|
||||||
|
|
||||||
const initialState = fromJS({
|
const initialState = fromJS({
|
||||||
databaseConfig: Map({}),
|
configDiff: Map({}),
|
||||||
fileConfig: Map({}),
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function configReducer(state = initialState, action) {
|
export default function configReducer(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_DATABASE_CONFIG_IN_STATE:
|
case SET_CONFIG_DIFF_IN_STATE:
|
||||||
return state
|
return state
|
||||||
.update('databaseConfig', () => fromJS(action.config))
|
.update('configDiff', () => fromJS(action.config))
|
||||||
case SET_FILE_CONFIG_IN_STATE:
|
|
||||||
return state
|
|
||||||
.update('fileConfig', () => fromJS(action.config))
|
|
||||||
case SET_LOADING_STATE:
|
case SET_LOADING_STATE:
|
||||||
return state
|
return state
|
||||||
.update('isLoading', () => fromJS(action.value))
|
.update('isLoading', () => fromJS(action.value))
|
||||||
|
|
|
@ -2,5 +2,10 @@
|
||||||
"destination": "extensions/config-sync/files/",
|
"destination": "extensions/config-sync/files/",
|
||||||
"minify": false,
|
"minify": false,
|
||||||
"importOnBootstrap": false,
|
"importOnBootstrap": false,
|
||||||
|
"include": [
|
||||||
|
"core-store",
|
||||||
|
"role-permissions",
|
||||||
|
"webhooks"
|
||||||
|
],
|
||||||
"exclude": []
|
"exclude": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,10 @@ const fs = require('fs');
|
||||||
* See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#bootstrap
|
* See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#bootstrap
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = async () => {
|
||||||
if (strapi.plugins['config-sync'].config.importOnBootstrap) {
|
if (strapi.plugins['config-sync'].config.importOnBootstrap) {
|
||||||
if (fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
if (fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
await strapi.plugins['config-sync'].services.main.importAllConfig();
|
||||||
|
|
||||||
configFiles.map((file) => {
|
|
||||||
strapi.plugins['config-sync'].services.config.importFromFile(file.slice(0, -5));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/export",
|
"path": "/export",
|
||||||
"handler": "config.export",
|
"handler": "config.exportAll",
|
||||||
"config": {
|
"config": {
|
||||||
"policies": []
|
"policies": []
|
||||||
}
|
}
|
||||||
|
@ -11,23 +11,15 @@
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/import",
|
"path": "/import",
|
||||||
"handler": "config.import",
|
"handler": "config.importAll",
|
||||||
"config": {
|
"config": {
|
||||||
"policies": []
|
"policies": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/all/from-files",
|
"path": "/diff",
|
||||||
"handler": "config.getConfigsFromFiles",
|
"handler": "config.getDiff",
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/all/from-database",
|
|
||||||
"handler": "config.getConfigsFromDatabase",
|
|
||||||
"config": {
|
"config": {
|
||||||
"policies": []
|
"policies": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const difference = require('../utils/getObjectDiff');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main controllers for config import/export.
|
* Main controllers for config import/export.
|
||||||
|
@ -13,13 +14,8 @@ module.exports = {
|
||||||
* @param {object} ctx - Request context object.
|
* @param {object} ctx - Request context object.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export: async (ctx) => {
|
exportAll: async (ctx) => {
|
||||||
const coreStoreAPI = strapi.query('core_store');
|
await strapi.plugins['config-sync'].services.main.exportAllConfig();
|
||||||
const coreStore = await coreStoreAPI.find({ _limit: -1 });
|
|
||||||
|
|
||||||
Object.values(coreStore).map(async ({ key, value }) => {
|
|
||||||
await strapi.plugins['config-sync'].services.config.writeConfigFile(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.send({
|
ctx.send({
|
||||||
message: `Config was successfully exported to ${strapi.plugins['config-sync'].config.destination}.`
|
message: `Config was successfully exported to ${strapi.plugins['config-sync'].config.destination}.`
|
||||||
|
@ -32,7 +28,7 @@ module.exports = {
|
||||||
* @param {object} ctx - Request context object.
|
* @param {object} ctx - Request context object.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
import: async (ctx) => {
|
importAll: async (ctx) => {
|
||||||
// Check for existance of the config file destination dir.
|
// Check for existance of the config file destination dir.
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
||||||
ctx.send({
|
ctx.send({
|
||||||
|
@ -42,11 +38,7 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
await strapi.plugins['config-sync'].services.main.importAllConfig();
|
||||||
|
|
||||||
configFiles.map((file) => {
|
|
||||||
strapi.plugins['config-sync'].services.config.importFromFile(file.slice(0, -5));
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.send({
|
ctx.send({
|
||||||
message: 'Config was successfully imported.'
|
message: 'Config was successfully imported.'
|
||||||
|
@ -54,12 +46,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all configs as defined in your filesystem.
|
* Get config diff between filesystem & db.
|
||||||
*
|
*
|
||||||
* @param {object} ctx - Request context object.
|
* @param {object} ctx - Request context object.
|
||||||
* @returns {object} Object with key value pairs of configs.
|
* @returns Object with key value pairs of config.
|
||||||
*/
|
*/
|
||||||
getConfigsFromFiles: async (ctx) => {
|
getDiff: async (ctx) => {
|
||||||
// Check for existance of the config file destination dir.
|
// Check for existance of the config file destination dir.
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
||||||
ctx.send({
|
ctx.send({
|
||||||
|
@ -69,37 +61,23 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
const formattedDiff = {
|
||||||
let formattedConfigs = {};
|
fileConfig: {},
|
||||||
|
databaseConfig: {},
|
||||||
const getConfigs = async () => {
|
diff: {}
|
||||||
return Promise.all(configFiles.map(async (file) => {
|
|
||||||
const formattedConfigName = file.slice(0, -5); // remove the .json extension.
|
|
||||||
const fileContents = await strapi.plugins['config-sync'].services.config.readConfigFile(formattedConfigName);
|
|
||||||
formattedConfigs[formattedConfigName] = fileContents;
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await getConfigs();
|
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles();
|
||||||
|
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase();
|
||||||
|
|
||||||
ctx.send(formattedConfigs);
|
const diff = difference(fileConfig, databaseConfig);
|
||||||
|
formattedDiff.diff = diff;
|
||||||
|
|
||||||
|
Object.keys(diff).map((changedConfigName) => {
|
||||||
|
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
|
||||||
|
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
|
||||||
|
})
|
||||||
|
|
||||||
|
return formattedDiff;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all configs as defined in your database.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getConfigsFromDatabase: async (ctx) => {
|
|
||||||
const coreStoreAPI = strapi.query('core_store');
|
|
||||||
const coreStore = await coreStoreAPI.find({ _limit: -1 });
|
|
||||||
|
|
||||||
let formattedConfigs = {};
|
|
||||||
Object.values(coreStore).map(async ({ key, value }) => {
|
|
||||||
formattedConfigs[key] = JSON.parse(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.send(formattedConfigs);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "strapi-plugin-config-sync",
|
"name": "strapi-plugin-config-sync",
|
||||||
"version": "0.0.3",
|
"version": "0.1.0",
|
||||||
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. ",
|
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. ",
|
||||||
"strapi": {
|
"strapi": {
|
||||||
"name": "config-sync",
|
"name": "config-sync",
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const util = require('util');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main services for config import/export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Write a single config file.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @param {string} fileContents - The JSON content of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
writeConfigFile: async (configName, fileContents) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(configName);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
// Check if the JSON content should be minified.
|
|
||||||
const json =
|
|
||||||
!strapi.plugins['config-sync'].config.minify ?
|
|
||||||
JSON.stringify(JSON.parse(fileContents), null, 2)
|
|
||||||
: fileContents;
|
|
||||||
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
fs.mkdirSync(strapi.plugins['config-sync'].config.destination, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
await writeFile(`${strapi.plugins['config-sync'].config.destination}${configName}.json`, json);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read from a config file.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {object} The JSON content of the config file.
|
|
||||||
*/
|
|
||||||
readConfigFile: async (configName) => {
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
|
||||||
return await readFile(`${strapi.plugins['config-sync'].config.destination}${configName}.json`)
|
|
||||||
.then((data) => {
|
|
||||||
return JSON.parse(data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a config file into the db.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importFromFile: async (configName) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(configName);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const coreStoreAPI = strapi.query('core_store');
|
|
||||||
const fileContents = await strapi.plugins['config-sync'].services.config.readConfigFile(configName);
|
|
||||||
|
|
||||||
const configExists = await strapi
|
|
||||||
.query('core_store')
|
|
||||||
.findOne({ key: configName });
|
|
||||||
|
|
||||||
if (!configExists) {
|
|
||||||
await coreStoreAPI.create({ key: configName, value: fileContents });
|
|
||||||
} else {
|
|
||||||
await coreStoreAPI.update({ key: configName }, { value: fileContents });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const coreStoreQueryString = 'core_store';
|
||||||
|
const configPrefix = 'core-store'; // Should be the same as the filename.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import/Export for core-store configs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Export all core-store config to files.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAll: async () => {
|
||||||
|
const coreStore = await strapi.query(coreStoreQueryString).find({ _limit: -1 });
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single core-store config file into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {string} configContent - The JSON content of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importSingle: async (configName, configContent) => {
|
||||||
|
const { value, ...fileContent } = configContent;
|
||||||
|
const coreStoreAPI = strapi.query(coreStoreQueryString);
|
||||||
|
|
||||||
|
const configExists = await coreStoreAPI
|
||||||
|
.findOne({ key: configName, environment: fileContent.environment });
|
||||||
|
|
||||||
|
if (!configExists) {
|
||||||
|
await coreStoreAPI.create({ value: JSON.stringify(value), ...fileContent });
|
||||||
|
} else {
|
||||||
|
await coreStoreAPI.update({ key: configName }, { value: JSON.stringify(value), ...fileContent });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all core-store config from the db.
|
||||||
|
*
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllFromDatabase: async () => {
|
||||||
|
const coreStore = await strapi.query(coreStoreQueryString).find({ _limit: -1 });
|
||||||
|
let configs = {};
|
||||||
|
|
||||||
|
Object.values(coreStore).map( ({ id, value, ...config }) => {
|
||||||
|
configs[`${configPrefix}.${config.key}`] = { value: JSON.parse(value), ...config };
|
||||||
|
});
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all core-store config files into the db.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAll: async () => {
|
||||||
|
// The main.importAllConfig service will loop the core-store.importSingle service.
|
||||||
|
await strapi.plugins['config-sync'].services.main.importAllConfig(configPrefix);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single core-store config to a file.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportSingle: async (configName) => {
|
||||||
|
// @TODO: write export for a single core-store config.
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,175 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main services for config import/export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Write a single config file.
|
||||||
|
*
|
||||||
|
* @param {string} configType - The type of the config.
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {string} fileContents - The JSON content of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
writeConfigFile: async (configType, configName, fileContents) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configType}.${configName}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
// Check if the JSON content should be minified.
|
||||||
|
const json =
|
||||||
|
!strapi.plugins['config-sync'].config.minify ?
|
||||||
|
JSON.stringify(fileContents, null, 2)
|
||||||
|
: JSON.stringify(fileContents);
|
||||||
|
|
||||||
|
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
||||||
|
fs.mkdirSync(strapi.plugins['config-sync'].config.destination, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeFile = util.promisify(fs.writeFile);
|
||||||
|
await writeFile(`${strapi.plugins['config-sync'].config.destination}${configType}.${configName}.json`, json)
|
||||||
|
.then(() => {
|
||||||
|
// @TODO:
|
||||||
|
// Add logging for successfull config export.
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// @TODO:
|
||||||
|
// Add logging for failed config export.
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from a config file.
|
||||||
|
*
|
||||||
|
* @param {string} configType - The type of config.
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {object} The JSON content of the config file.
|
||||||
|
*/
|
||||||
|
readConfigFile: async (configType, configName) => {
|
||||||
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
return await readFile(`${strapi.plugins['config-sync'].config.destination}${configType}.${configName}.json`)
|
||||||
|
.then((data) => {
|
||||||
|
return JSON.parse(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the config JSON from the filesystem.
|
||||||
|
*
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllConfigFromFiles: async () => {
|
||||||
|
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
||||||
|
|
||||||
|
const getConfigs = async () => {
|
||||||
|
let fileConfigs = {};
|
||||||
|
|
||||||
|
await Promise.all(configFiles.map(async (file) => {
|
||||||
|
const type = file.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const name = file.split(/\.(.+)/)[1].split('.').slice(0, -1).join('.'); // Grab the rest of the filename minus the file extension.
|
||||||
|
const fileContents = await strapi.plugins['config-sync'].services.main.readConfigFile(type, name);
|
||||||
|
fileConfigs[`${type}.${name}`] = fileContents;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return fileConfigs;
|
||||||
|
};
|
||||||
|
|
||||||
|
return await getConfigs();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the config JSON from the database.
|
||||||
|
*
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllConfigFromDatabase: async (configType = null) => {
|
||||||
|
const getConfigs = async () => {
|
||||||
|
let databaseConfigs = {};
|
||||||
|
|
||||||
|
await Promise.all(strapi.plugins['config-sync'].config.include.map(async (type) => {
|
||||||
|
if (configType && configType !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await strapi.plugins['config-sync'].services[type].getAllFromDatabase();
|
||||||
|
databaseConfigs = Object.assign(config, databaseConfigs);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return databaseConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getConfigs();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all config files into the db.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAllConfig: async (configType = null) => {
|
||||||
|
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
||||||
|
|
||||||
|
configFiles.map((file) => {
|
||||||
|
const type = file.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const name = file.split(/\.(.+)/)[1].split('.').slice(0, -1).join('.'); // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
|
if (configType && configType !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strapi.plugins['config-sync'].services.main.importSingleConfig(type, name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all config files.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAllConfig: async (configType = null) => {
|
||||||
|
await Promise.all(strapi.plugins['config-sync'].config.include.map(async (type) => {
|
||||||
|
if (configType && configType !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await strapi.plugins['config-sync'].services[type].exportAll();
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single config file into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configType - The type of config.
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importSingleConfig: async (configType, configName) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configType}.${configName}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
const fileContents = await strapi.plugins['config-sync'].services.main.readConfigFile(configType, configName);
|
||||||
|
|
||||||
|
await strapi.plugins['config-sync'].services[configType].importSingle(configName, fileContents);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single config file.
|
||||||
|
*
|
||||||
|
* @param {string} configType - The type of config.
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportSingleConfig: async (configType, configName) => {
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,118 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { sanitizeEntity } = require('strapi-utils');
|
||||||
|
|
||||||
|
const configPrefix = 'role-permissions'; // Should be the same as the filename.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import/Export for role-permissions configs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Export all role-permissions config to files.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAll: async () => {
|
||||||
|
const service =
|
||||||
|
strapi.plugins['users-permissions'].services.userspermissions;
|
||||||
|
|
||||||
|
const [roles, plugins] = await Promise.all([
|
||||||
|
service.getRoles(),
|
||||||
|
service.getPlugins(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rolesWithPermissions = await Promise.all(
|
||||||
|
roles.map(async role => service.getRole(role.id, plugins))
|
||||||
|
);
|
||||||
|
|
||||||
|
const sanitizedRolesArray = rolesWithPermissions.map(role =>
|
||||||
|
sanitizeEntity(role, {
|
||||||
|
model: strapi.plugins['users-permissions'].models.role,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(sanitizedRolesArray.map(async (config) => {
|
||||||
|
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, config.type, config);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single role-permissions config file into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {string} configContent - The JSON content of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importSingle: async (configName, configContent) => {
|
||||||
|
const service =
|
||||||
|
strapi.plugins['users-permissions'].services.userspermissions;
|
||||||
|
|
||||||
|
const role = await strapi
|
||||||
|
.query('role', 'users-permissions')
|
||||||
|
.findOne({ type: configName });
|
||||||
|
|
||||||
|
const users = role ? role.users : [];
|
||||||
|
configContent.users = users;
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
await service.createRole(configContent);
|
||||||
|
} else {
|
||||||
|
await service.updateRole(role.id, configContent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all role-permissions config from the db.
|
||||||
|
*
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllFromDatabase: async () => {
|
||||||
|
const service =
|
||||||
|
strapi.plugins['users-permissions'].services.userspermissions;
|
||||||
|
|
||||||
|
const [roles, plugins] = await Promise.all([
|
||||||
|
service.getRoles(),
|
||||||
|
service.getPlugins(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rolesWithPermissions = await Promise.all(
|
||||||
|
roles.map(async role => service.getRole(role.id, plugins))
|
||||||
|
);
|
||||||
|
|
||||||
|
const sanitizedRolesArray = rolesWithPermissions.map(role =>
|
||||||
|
sanitizeEntity(role, {
|
||||||
|
model: strapi.plugins['users-permissions'].models.role,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let configs = {};
|
||||||
|
|
||||||
|
Object.values(sanitizedRolesArray).map((config) => {
|
||||||
|
configs[`${configPrefix}.${config.type}`] = config;
|
||||||
|
});
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all role-permissions config files into the db.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAll: async () => {
|
||||||
|
// The main.importAllConfig service will loop the role-permissions.importSingle service.
|
||||||
|
await strapi.plugins['config-sync'].services.main.importAllConfig(configPrefix);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single role-permissions config to a file.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportSingle: async (configName) => {
|
||||||
|
// @TODO: write export for a single role-permissions config.
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import/Export for webhook configs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const webhookQueryString = 'strapi_webhooks';
|
||||||
|
const configPrefix = 'webhooks'; // Should be the same as the filename.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Export all webhooks to config files.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAll: async () => {
|
||||||
|
const webhooks = await strapi.query(webhookQueryString).find({ _limit: -1 });
|
||||||
|
|
||||||
|
await Promise.all(Object.values(webhooks).map(async (config) => {
|
||||||
|
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, config.id, config);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single webhook config file into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {string} configContent - The JSON content of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importSingle: async (configName, configContent) => {
|
||||||
|
const webhookAPI = strapi.query(webhookQueryString);
|
||||||
|
|
||||||
|
const configExists = await webhookAPI.findOne({ id: configName });
|
||||||
|
|
||||||
|
if (!configExists) {
|
||||||
|
await webhookAPI.create(configContent);
|
||||||
|
} else {
|
||||||
|
await webhookAPI.update({ id: configName }, configContent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all webhook config from the db.
|
||||||
|
*
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllFromDatabase: async () => {
|
||||||
|
const webhooks = await strapi.query(webhookQueryString).find({ _limit: -1 });
|
||||||
|
let configs = {};
|
||||||
|
|
||||||
|
Object.values(webhooks).map( (config) => {
|
||||||
|
configs[`${configPrefix}.${config.id}`] = config;
|
||||||
|
});
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all webhook config files into the db.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAll: async () => {
|
||||||
|
// The main.importAllConfig service will loop the webhooks.importSingle service.
|
||||||
|
await strapi.plugins['config-sync'].services.main.importAllConfig(configPrefix);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single webhook into a config file.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportSingle: async (configName) => {
|
||||||
|
// @TODO: write export for a single webhook config.
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import { transform, isEqual, isArray, isObject } from 'lodash';
|
'use strict';
|
||||||
|
const { transform, isEqual, isArray, isObject } = require('lodash');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find difference between two objects
|
* Find difference between two objects
|
||||||
|
@ -19,4 +20,4 @@ const difference = (origObj, newObj) => {
|
||||||
return changes(newObj, origObj)
|
return changes(newObj, origObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default difference;
|
module.exports = difference;
|
Loading…
Reference in New Issue