From 4f0cce1b54e4978b5a63f560ca70472c4f6090b9 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Wed, 24 Mar 2021 16:22:03 +0100 Subject: [PATCH] Add import/export services for roles, permissions & webhooks. --- config/config.json | 5 ++ config/functions/bootstrap.js | 8 +-- config/routes.json | 4 +- controllers/config.js | 19 ++--- services/config.js | 78 --------------------- services/core-store.js | 65 ++++++++++++++++++ services/main.js | 126 ++++++++++++++++++++++++++++++++++ services/role-permissions.js | 85 +++++++++++++++++++++++ services/webhooks.js | 62 +++++++++++++++++ 9 files changed, 352 insertions(+), 100 deletions(-) delete mode 100644 services/config.js create mode 100644 services/core-store.js create mode 100644 services/main.js create mode 100644 services/role-permissions.js create mode 100644 services/webhooks.js diff --git a/config/config.json b/config/config.json index f0a37b5..e42c20e 100644 --- a/config/config.json +++ b/config/config.json @@ -2,5 +2,10 @@ "destination": "extensions/config-sync/files/", "minify": false, "importOnBootstrap": false, + "include": [ + "core-store", + "role-permissions", + "webhooks" + ], "exclude": [] } diff --git a/config/functions/bootstrap.js b/config/functions/bootstrap.js index 9324aff..d8b9dee 100644 --- a/config/functions/bootstrap.js +++ b/config/functions/bootstrap.js @@ -12,14 +12,10 @@ const fs = require('fs'); * 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 (fs.existsSync(strapi.plugins['config-sync'].config.destination)) { - const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination); - - configFiles.map((file) => { - strapi.plugins['config-sync'].services.config.importFromFile(file.slice(0, -5)); - }); + await strapi.plugins['config-sync'].services.main.importAllConfig(); } } }; diff --git a/config/routes.json b/config/routes.json index 7379637..fc7c0ea 100644 --- a/config/routes.json +++ b/config/routes.json @@ -3,7 +3,7 @@ { "method": "GET", "path": "/export", - "handler": "config.export", + "handler": "config.exportAll", "config": { "policies": [] } @@ -11,7 +11,7 @@ { "method": "GET", "path": "/import", - "handler": "config.import", + "handler": "config.importAll", "config": { "policies": [] } diff --git a/controllers/config.js b/controllers/config.js index 745315c..4636f53 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -13,13 +13,8 @@ module.exports = { * @param {object} ctx - Request context object. * @returns {void} */ - export: async (ctx) => { - const coreStoreAPI = strapi.query('core_store'); - const coreStore = await coreStoreAPI.find({ _limit: -1 }); - - Object.values(coreStore).map(async ({ key, value }) => { - await strapi.plugins['config-sync'].services.config.writeConfigFile(key, value); - }); + exportAll: async (ctx) => { + await strapi.plugins['config-sync'].services.main.exportAllConfig(); ctx.send({ message: `Config was successfully exported to ${strapi.plugins['config-sync'].config.destination}.` @@ -32,7 +27,7 @@ module.exports = { * @param {object} ctx - Request context object. * @returns {void} */ - import: async (ctx) => { + importAll: async (ctx) => { // Check for existance of the config file destination dir. if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) { ctx.send({ @@ -42,11 +37,7 @@ module.exports = { return; } - const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination); - - configFiles.map((file) => { - strapi.plugins['config-sync'].services.config.importFromFile(file.slice(0, -5)); - }); + await strapi.plugins['config-sync'].services.main.importAllConfig(); ctx.send({ message: 'Config was successfully imported.' @@ -75,7 +66,7 @@ module.exports = { const getConfigs = async () => { 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); + const fileContents = await strapi.plugins['config-sync'].services.main.readConfigFile(formattedConfigName); formattedConfigs[formattedConfigName] = fileContents; })); }; diff --git a/services/config.js b/services/config.js deleted file mode 100644 index ab1cc1c..0000000 --- a/services/config.js +++ /dev/null @@ -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 }); - } - } -}; diff --git a/services/core-store.js b/services/core-store.js new file mode 100644 index 0000000..da97c3a --- /dev/null +++ b/services/core-store.js @@ -0,0 +1,65 @@ +'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 }); + } + }, + + /** + * 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. + }, +}; diff --git a/services/main.js b/services/main.js new file mode 100644 index 0000000..5954662 --- /dev/null +++ b/services/main.js @@ -0,0 +1,126 @@ +'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; + }); + }, + + /** + * 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) => { + + }, +}; diff --git a/services/role-permissions.js b/services/role-permissions.js new file mode 100644 index 0000000..598e488 --- /dev/null +++ b/services/role-permissions.js @@ -0,0 +1,85 @@ +'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); + } + }, + + /** + * 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. + }, +}; diff --git a/services/webhooks.js b/services/webhooks.js new file mode 100644 index 0000000..082e1f1 --- /dev/null +++ b/services/webhooks.js @@ -0,0 +1,62 @@ +'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); + } + }, + + /** + * 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. + }, +};