@grouparoo/core
Version:
The Grouparoo Core
289 lines (288 loc) • 11.8 kB
JavaScript
;
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;