UNPKG

@grouparoo/core

Version:
289 lines (288 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.cleanClass = exports.sortConfigObjectsWithIds = exports.getParentIds = exports.getDirectParentId = exports.getConfigObjectsWithIds = exports.validateConfigObjects = exports.sortConfigurationObjects = exports.getAutoBootstrappedProperty = exports.extractNonNullParts = exports.logModel = exports.validateConfigObjectKeys = exports.getParentByName = exports.getCodeConfigLockKey = void 0; const actionhero_1 = require("actionhero"); const mustacheUtils_1 = require("../modules/mustacheUtils"); const topLevelGroupRules_1 = require("../modules/topLevelGroupRules"); const topologicalSort_1 = require("../modules/topologicalSort"); const arrayUtils_1 = require("../modules/arrayUtils"); // Utils function getCodeConfigLockKey() { return "config:code"; } exports.getCodeConfigLockKey = getCodeConfigLockKey; async function getParentByName(model, parentId) { if (!parentId) throw new Error(`missing parent id to find a ${model.name}`); const instance = await model.scope(null).findOne({ where: { id: parentId } }); if (!instance) { throw new Error(`cannot find ${model.name} with id "${parentId}"`); } return instance; } exports.getParentByName = getParentByName; function validateConfigObjectKeys(model, configObject, additionalAllowedKeys = []) { const errors = []; const modelKeys = Object.keys(model.rawAttributes).concat(additionalAllowedKeys); let idFound = false; const configKeys = Object.keys(configObject) .filter((k) => k !== "class") .map((k) => { if (k === "id") { idFound = true; return "id"; } else { return k; } }); if (!idFound) errors.push(`id is required for a ${model.name}`); configKeys.forEach((k) => { if (!modelKeys.includes(k)) { errors.push(`${k} is not a valid property of a ${model.name}`); } }); if (errors.length > 0) throw new Error(errors.join(", ")); } exports.validateConfigObjectKeys = validateConfigObjectKeys; /** * Log a Sequelize Model */ function logModel(instance, mode, name) { let logLevel = "info"; if (mode === "created") logLevel = "notice"; if (mode === "deleted") logLevel = "warning"; (0, actionhero_1.log)(`[ config ] ${mode} ${instance.constructor.name} \`${name || instance.key || instance.email || instance.name}\` (${instance.id})`, logLevel); } exports.logModel = logModel; function extractNonNullParts(configObject, key) { const cleanedOptions = {}; if (configObject[key]) { for (const i in configObject[key]) { const value = configObject[key][i]; if (value !== null && value !== undefined) cleanedOptions[i] = value; } } return cleanedOptions; } exports.extractNonNullParts = extractNonNullParts; function getAutoBootstrappedProperty(sourceConfigObject, otherConfigObjects) { if (cleanClass(sourceConfigObject) !== "source") return null; if (!sourceConfigObject["mapping"]) return null; const mappingValues = Object.values(sourceConfigObject["mapping"]); for (const value of mappingValues) { // If this source id == its mapped property's sourceId, we should bootstrap the property const autoBootstrappedProperty = otherConfigObjects .filter((o) => typeof o === "object") // TS Trick to mock a type guard .find((o) => cleanClass(o) === "property" && o.id === value && o["sourceId"] === sourceConfigObject.id); if (autoBootstrappedProperty) { if (!autoBootstrappedProperty["unique"]) { throw new Error(`The property "${autoBootstrappedProperty.id}" needs to be set as "unique: true" to be used as the mapping for the source "${sourceConfigObject.id}"`); } if (autoBootstrappedProperty["isArray"]) { throw new Error(`The property "${autoBootstrappedProperty.id}" cannot be an array to be used as the mapping for the source "${sourceConfigObject.id}"`); } return autoBootstrappedProperty; } } return null; } exports.getAutoBootstrappedProperty = getAutoBootstrappedProperty; async function sortConfigurationObjects(configObjects) { const configObjectsWithIds = await getConfigObjectsWithIds(configObjects); const sortedConfigObjectsWithIds = sortConfigObjectsWithIds(configObjectsWithIds); return sortedConfigObjectsWithIds.map((o) => o.configObject); } exports.sortConfigurationObjects = sortConfigurationObjects; /** * Check a set of config objects for duplicate IDs within the same type. * * @param configObjects ConfigurationObject[] */ function validateConfigObjects(configObjects) { let errors = []; const idTypes = {}; for (const configObject of configObjects) { if (!configObject.id) { errors.push( //@ts-ignore (configObject["name"] ? //@ts-ignore `"${configObject["name"]}"` : //@ts-ignore configObject["key"] ? //@ts-ignore `"${configObject["key"]}"` : "A config object") + " is missing an ID"); } const _class = cleanClass(configObject); if (!idTypes[_class]) { idTypes[_class] = []; } if (idTypes[_class].includes(configObject.id)) { errors.push(`Duplicate ID values found for ${configObject.id} of class ${_class}`); } else { idTypes[_class].push(configObject.id); } } return { configObjects, errors }; } exports.validateConfigObjects = validateConfigObjects; async function getConfigObjectsWithIds(configObjects) { const configObjectsWithIds = []; for (const configObject of configObjects) { const { providedIds, prerequisiteIds } = await getParentIds(configObject, configObjects); configObjectsWithIds.push({ configObject, providedIds, prerequisiteIds, }); } return configObjectsWithIds; } exports.getConfigObjectsWithIds = getConfigObjectsWithIds; async function getDirectParentId(configObject) { const parentKeys = { destination: "appId", source: "appId", property: "sourceId", teammember: "teamId", }; const parentKey = parentKeys[cleanClass(configObject)]; if (!parentKey) return null; // @ts-ignore const parentId = configObject[parentKey]; if (!parentId) return null; return parentId; } exports.getDirectParentId = getDirectParentId; async function getParentIds(configObject, otherConfigObjects = []) { const keys = Object.keys(configObject); // IDs here are prepended with the class of the object to allow for ID duplication between classes, but not of the same type const prerequisiteIds = []; const providedIds = []; providedIds.push(`${cleanClass(configObject)}:${configObject.id}`); // special cases // - property with mustache dependency // @ts-ignore if (configObject["options"]) { // @ts-ignore for (const [k, v] of Object.entries(configObject["options"])) { if (cleanClass(configObject) === "property" && typeof v === "string" && v.includes("{{") && v.length > 4) { const mustachePrerequisiteIds = await mustacheUtils_1.MustacheUtils.getMustacheVariablesAsPropertyIds(v, otherConfigObjects); prerequisiteIds.push(...mustachePrerequisiteIds.map((p) => `property:${p}`)); } } } // prerequisites for (const i in keys) { if (keys[i].match(/.+Id$/)) { const _class = keys[i].replace(/Id$/, ""); // @ts-ignore const value = configObject[keys[i]]; prerequisiteIds.push(`${_class}:${value}`); } } const objectContainers = ["options", "source", "destination"]; const validContainerKeys = ["propertyId"]; objectContainers.map((_container) => { // @ts-ignore if (configObject[_container]) { // @ts-ignore const containerKeys = Object.keys(configObject[_container]); for (const i in containerKeys) { if (validContainerKeys.includes(containerKeys[i])) { prerequisiteIds.push( // @ts-ignore `property:${configObject[_container][containerKeys[i]]}`); } } } }); const arrayContainers = ["rules"]; arrayContainers.map((_container) => { // @ts-ignore for (const i in configObject[_container]) { // @ts-ignore const record = configObject[_container][i]; const recordKeys = Object.keys(record); for (const j in recordKeys) { if (recordKeys[j].match(/.+Id$/)) { const value = record[recordKeys[j]]; // special case: topLevel properties if (topLevelGroupRules_1.TopLevelGroupRules.map((r) => r.key).includes(value)) { continue; } prerequisiteIds.push(`property:${value}`); } } } }); // @ts-ignore if (configObject["mapping"]) { const autoBootstrappedProperty = getAutoBootstrappedProperty(configObject, otherConfigObjects); // @ts-ignore const mappingValues = Object.values(configObject["mapping"]); for (const value of mappingValues) { if (!autoBootstrappedProperty || value !== autoBootstrappedProperty.id) prerequisiteIds.push(`property:${value}`); } } // @ts-ignore if (configObject["destinationGroupMemberships"]) { const groupIds = Object.values( // @ts-ignore configObject["destinationGroupMemberships"]); groupIds.forEach((v) => prerequisiteIds.push(`group:${v}`)); } // @ts-ignore if (configObject["properties"]) { // @ts-ignore const propertyIds = Object.keys(configObject["properties"]); propertyIds.forEach((v) => prerequisiteIds.push(`property:${v}`)); } return { prerequisiteIds: prerequisiteIds.filter(arrayUtils_1.uniqueArrayValues), providedIds: providedIds.filter(arrayUtils_1.uniqueArrayValues), }; } exports.getParentIds = getParentIds; function sortConfigObjectsWithIds(configObjectsWithIds) { const sortedConfigObjectsWithIds = []; const dependencyGraph = {}; configObjectsWithIds.forEach((o) => { o.providedIds.forEach((providedId) => { dependencyGraph[providedId] = o.prerequisiteIds.filter((preReq) => preReq !== providedId); }); }); const sortedKeys = (0, topologicalSort_1.topologicalSort)(dependencyGraph); sortedKeys.forEach((typeAndId) => { const [_class, id] = typeAndId.split(":"); const parent = configObjectsWithIds.find((o) => cleanClass(o.configObject) === _class && o.configObject.id === id); if (parent) { sortedConfigObjectsWithIds.push(parent); } }); return sortedConfigObjectsWithIds.filter(arrayUtils_1.uniqueArrayValues); } exports.sortConfigObjectsWithIds = sortConfigObjectsWithIds; function cleanClass(configObject) { var _a, _b; return (_b = (_a = configObject.class) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase()) !== null && _b !== void 0 ? _b : undefined; } exports.cleanClass = cleanClass;