From 3aa1497786cc3d38d1abdaa9e229259f1c448de4 Mon Sep 17 00:00:00 2001 From: tilman Date: Sat, 15 Jan 2022 19:17:19 +0100 Subject: [PATCH] feat: add combined uid feature and update docs --- README.md | 6 ++++-- package.json | 2 +- server/config/type.js | 50 +++++++++++++++++++++++++++---------------- server/utils/index.js | 11 ++++++++++ 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9b5ee22..c96c1ff 100644 --- a/README.md +++ b/README.md @@ -278,13 +278,15 @@ This is the query string of the type. Each type in Strapi has its own query stri #### UID -The UID represents a field on the registered type. The value of this field will act as a unique identifier to identify the entries across environments. Therefor it should be unique and preferably un-editable after initial creation. +The UID represents a field on the registered type. The value of this field will act as a unique identifier to identify the entries across environments. Therefore it should be unique and preferably un-editable after initial creation. Mind that you can not use an auto-incremental value like the `id` as auto-increment does not play nice when you try to match entries across different databases. +If you do not have a single unique value, you can also pass in a array of keys for a combined uid key. This is for example the case for all content types which use i18n features (An example config would be `uid: ['productId', 'locale']`). + ###### Key: `uid` -> `required:` YES | `type:` string +> `required:` YES | `type:` string | string[] #### JSON fields diff --git a/package.json b/package.json index 7379586..374149b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "strapi-plugin-config-sync", - "version": "1.0.0-beta.4", + "version": "1.0.0-beta.5", "description": "CLI & GUI for syncing config data across environments.", "strapi": { "displayName": "Config Sync", diff --git a/server/config/type.js b/server/config/type.js index 55cb4d1..143ddc5 100644 --- a/server/config/type.js +++ b/server/config/type.js @@ -1,5 +1,5 @@ const { isEmpty } = require('lodash'); -const { logMessage, sanitizeConfig, dynamicSort, noLimit } = require('../utils'); +const { logMessage, sanitizeConfig, dynamicSort, noLimit, getCombinedUid, getCombinedUidWhereFilter, getUidParamsFromName } = require('../utils'); const difference = require('../utils/getArrayDiff'); const ConfigType = class ConfigType { @@ -12,13 +12,17 @@ const ConfigType = class ConfigType { strapi.log.error(logMessage(`No query string found for the '${configName}' config type.`)); process.exit(0); } - if (!uid) { - strapi.log.error(logMessage(`No uid found for the '${configName}' config type.`)); + // uid could be a single key or an array for a combined uid. So the type of uid is either string or string[] + if(typeof uid === "string"){ + this.uidKeys = [uid]; + } else if(Array.isArray(uid)){ + this.uidKeys = uid.sort(); + } else { + strapi.log.error(logMessage(`Wrong uid config for the '${configName}' config type.`)); process.exit(0); } this.queryString = queryString; this.configPrefix = configName; - this.uid = uid; this.jsonFields = jsonFields || []; this.relations = relations || []; } @@ -30,24 +34,29 @@ const ConfigType = class ConfigType { * @param {string} configContent - The JSON content of the config file. * @returns {void} */ - importSingle = async (configName, configContent) => { + importSingle = async (configName, configContent) => { // 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 queryAPI = strapi.query(this.queryString); - + const uidParams = getUidParamsFromName(this.uidKeys, configName); + const combinedUidWhereFilter = getCombinedUidWhereFilter(this.uidKeys, uidParams); let existingConfig = await queryAPI .findOne({ - where: { [this.uid]: configName }, + where: combinedUidWhereFilter, populate: this.relations.map(({ relationName }) => relationName), }); - if (existingConfig && configContent === null) { - const entity = await queryAPI.findOne({ - where: { [this.uid]: configName }, - populate: this.relations.map(({ relationName }) => relationName), - }); + console.log("importSingle", configName, uidParams, existingConfig, configContent, combinedUidWhereFilter) + + if (existingConfig && configContent === null) { // Config exists in DB but no configfile content --> delete config from DB + // was there a reason to fetch it again? Isn't this the same as existingConfig? + // const entity = await queryAPI.findOne({ + // where: combinedUidWhereFilter, + // populate: this.relations.map(({ relationName }) => relationName), + // }); + const entity = existingConfig; await Promise.all(this.relations.map(async ({ queryString, parentName }) => { const relations = await noLimit(strapi.query(queryString), { @@ -70,7 +79,7 @@ const ConfigType = class ConfigType { return; } - if (!existingConfig) { + if (!existingConfig) { // Config does not exist in DB --> create config in DB // Format JSON fields. const query = { ...configContent }; this.jsonFields.map((field) => query[field] = JSON.stringify(configContent[field])); @@ -88,7 +97,7 @@ const ConfigType = class ConfigType { await relationQueryApi.create({ data: relationQuery }); })); })); - } else { + } else { // Config does exist in DB --> update config in DB // Format JSON fields. configContent = sanitizeConfig(configContent); const query = { ...configContent }; @@ -96,7 +105,7 @@ const ConfigType = class ConfigType { // Update entity. this.relations.map(({ relationName }) => delete query[relationName]); - const entity = await queryAPI.update({ where: { [this.uid]: configName }, data: query }); + const entity = await queryAPI.update({ where: combinedUidWhereFilter, data: query }); // Delete/create relations. await Promise.all(this.relations.map(async ({ queryString, relationName, parentName, relationSortField }) => { @@ -139,6 +148,7 @@ const ConfigType = class ConfigType { if (shouldExclude) return; const currentConfig = formattedDiff.databaseConfig[configName]; + const combinedUid = getCombinedUid(this.uidKeys, currentConfig); if ( !currentConfig @@ -146,7 +156,7 @@ const ConfigType = class ConfigType { ) { await strapi.plugin('config-sync').service('main').deleteConfigFile(configName); } else { - await strapi.plugin('config-sync').service('main').writeConfigFile(this.configPrefix, currentConfig[this.uid], currentConfig); + await strapi.plugin('config-sync').service('main').writeConfigFile(this.configPrefix, combinedUid, currentConfig); } } @@ -160,14 +170,16 @@ const ConfigType = class ConfigType { const configs = {}; await Promise.all(Object.values(AllConfig).map(async (config) => { + const combinedUid = getCombinedUid(this.uidKeys, config); + const combinedUidWhereFilter = getCombinedUidWhereFilter(this.uidKeys, config); // Check if the config should be excluded. - const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => `${this.configPrefix}.${config[this.uid]}`.startsWith(option))); + const shouldExclude = !isEmpty(strapi.config.get('plugin.config-sync.excludedConfig').filter((option) => `${this.configPrefix}.${combinedUid}`.startsWith(option))); if (shouldExclude) return; const formattedConfig = { ...sanitizeConfig(config) }; await Promise.all(this.relations.map(async ({ queryString, relationName, relationSortField, parentName }) => { const relations = await noLimit(strapi.query(queryString), { - where: { [parentName]: { [this.uid]: config[this.uid] } }, + where: { [parentName]: combinedUidWhereFilter }, }); relations.map((relation) => sanitizeConfig(relation)); @@ -176,7 +188,7 @@ const ConfigType = class ConfigType { })); this.jsonFields.map((field) => formattedConfig[field] = JSON.parse(config[field])); - configs[`${this.configPrefix}.${config[this.uid]}`] = formattedConfig; + configs[`${this.configPrefix}.${combinedUid}`] = formattedConfig; })); diff --git a/server/utils/index.js b/server/utils/index.js index 619c6db..ad31560 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -1,5 +1,12 @@ 'use strict'; +const COMBINED_UID_JOINSTR = ', '; + +const escapeUid = (uid) => uid.replace(/\,/g,'').replace(COMBINED_UID_JOINSTR, ''); +const getCombinedUid = (uidKeys, params) => uidKeys.map((uidKey) => params[uidKey]).join(COMBINED_UID_JOINSTR); +const getCombinedUidWhereFilter = (uidKeys, params) => uidKeys.reduce(((akku, uidKey) => ({ ...akku, [uidKey]: params[uidKey] })),{}); +const getUidParamsFromName = (uidKeys, configName) => configName.split(COMBINED_UID_JOINSTR).reduce((akku, param, i) => ({...akku, [uidKeys[i]]: param}), {}); + const getCoreStore = () => { return strapi.store({ type: 'plugin', name: 'config-sync' }); }; @@ -83,6 +90,10 @@ const noLimit = async (query, parameters, limit = 100) => { }; module.exports = { + escapeUid, + getCombinedUid, + getCombinedUidWhereFilter, + getUidParamsFromName, getService, getCoreStore, logMessage,