@grouparoo/core
Version:
The Grouparoo Core
285 lines (284 loc) • 12.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.deleteLockedObjects = exports.processConfigObjects = exports.shouldExternallyValidate = exports.loadConfigObjects = exports.loadConfigDirectory = exports.getSeenIds = void 0;
const actionhero_1 = require("actionhero");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const glob_1 = __importDefault(require("glob"));
const json5_1 = __importDefault(require("json5"));
const codeConfig_1 = require("../../classes/codeConfig");
const errors_1 = require("../../config/errors");
const model_1 = require("./model");
const app_1 = require("./app");
const source_1 = require("./source");
const property_1 = require("./property");
const apiKey_1 = require("./apiKey");
const team_1 = require("./team");
const teamMember_1 = require("./teamMember");
const group_1 = require("./group");
const schedule_1 = require("./schedule");
const setting_1 = require("./setting");
const destination_1 = require("./destination");
const configWriter_1 = require("../configWriter");
const record_1 = require("./record");
const sequelize_1 = __importDefault(require("sequelize"));
const deprecation_1 = require("../deprecation");
const pluralize_1 = __importDefault(require("pluralize"));
const freshIdsByClass = () => ({
model: [],
app: [],
source: [],
property: [],
group: [],
schedule: [],
destination: [],
apikey: [],
team: [],
teammember: [],
record: [],
});
function getSeenIds(configObjects) {
return configObjects.reduce((agg, co) => {
var _a;
const klass = (0, codeConfig_1.cleanClass)(co);
if (co.id && klass) {
//@ts-ignore
(_a = agg[klass]) === null || _a === void 0 ? void 0 : _a.push(co.id);
}
return agg;
}, freshIdsByClass());
}
exports.getSeenIds = getSeenIds;
async function loadConfigDirectory(configDir, externallyValidate = true) {
let seenIds = {};
let deletedIds = {};
let errors = [];
if (configDir) {
const { configObjects, errors: loadErrors } = await loadConfigObjects(configDir);
if (loadErrors.length > 0) {
return { errors: loadErrors, seenIds, deletedIds };
}
({ errors, seenIds, deletedIds } = await processConfigObjects(configObjects, externallyValidate));
}
return { seenIds, errors, deletedIds };
}
exports.loadConfigDirectory = loadConfigDirectory;
async function loadConfigObjects(configDir) {
if (!configDir)
return { configObjects: [], errors: [] };
const globSearch = path_1.default.join(configDir, "**", "+(*.json|*.js)");
const configFiles = glob_1.default.sync(globSearch);
let configObjects = [];
let errors = [];
for (const file of configFiles) {
const objects = await loadConfigFile(file);
const nullishObjects = objects.filter((o) => o === null || o === undefined);
if (nullishObjects.length > 0) {
errors.push(`The file \`${file}\` has ${nullishObjects.length} null or undefined top-level config ${(0, pluralize_1.default)("object")}`);
}
configObjects = configObjects
.concat(objects)
.filter((o) => o && Object.keys(o).length > 0); // skip empty objects
}
return { configObjects, errors };
}
exports.loadConfigObjects = loadConfigObjects;
async function loadConfigFile(file) {
let payload = {};
if (file.match(/\.json$/)) {
const contents = fs_1.default.readFileSync(file).toString();
if (contents.trim().includes("{"))
payload = json5_1.default.parse(contents);
}
else {
payload = require(file);
}
const payloadKeys = Object.keys(payload);
if (payloadKeys.length === 1 && payloadKeys[0] === "default") {
payload = payload.default;
}
if (typeof payload === "function") {
payload = await payload(actionhero_1.config);
}
const objects = Array.isArray(payload) ? payload : [payload];
configWriter_1.ConfigWriter.cacheConfigFile({ absFilePath: file, objects });
return objects;
}
async function shouldExternallyValidate(canExternallyValidate, configObject, locallyValidateIds) {
if (!canExternallyValidate)
return false;
if (!locallyValidateIds)
return true;
const objectId = configObject.id;
const parentId = await (0, codeConfig_1.getDirectParentId)(configObject);
if (parentId && locallyValidateIds.has(parentId)) {
locallyValidateIds.add(objectId);
}
if (locallyValidateIds.has(objectId)) {
return false;
}
return true;
}
exports.shouldExternallyValidate = shouldExternallyValidate;
async function processConfigObjects(configObjects, canExternallyValidate, locallyValidateIds, validate = false) {
var _a;
const seenIds = freshIdsByClass();
const errors = [];
const { errors: validationErrors } = (0, codeConfig_1.validateConfigObjects)(configObjects);
validationErrors.map((err) => (0, actionhero_1.log)(`[ config ] ${err}`, actionhero_1.env === "test" ? "info" : "error"));
errors.push(...validationErrors);
if (errors.length > 0) {
return { seenIds, errors, deletedIds: {} };
}
try {
configObjects = await (0, codeConfig_1.sortConfigurationObjects)(configObjects);
}
catch (error) {
// If something we wrong while sorting, log the messages and return. We
// aren't going to process the config objects if we can't be confident we're
// doing it in the right order.
error.message.split("\n").map((msg) => {
if (msg.startsWith("unknownNodeId")) {
msg = `Could not find object with ID: ${msg.slice(14).split(":")[1]}`;
}
const err = new Error(msg);
errors.push(err.message);
(0, actionhero_1.log)(msg, "error");
});
return { seenIds: {}, errors, deletedIds: {} };
}
if (locallyValidateIds) {
const configObjectIds = configObjects.map((o) => o.id);
locallyValidateIds.forEach((id) => !configObjectIds.includes(id) &&
(0, actionhero_1.log)(`[ config ] tried to locally validate \`${id}\`, but an object with that ID does not exist`, "warning"));
}
// Delete unseen config objects ahead of time
const deletedIds = await deleteLockedObjects(getSeenIds(configObjects));
for (const configObject of configObjects) {
if (Object.keys(configObject).length === 0)
continue;
let klass = (_a = configObject === null || configObject === void 0 ? void 0 : configObject.class) === null || _a === void 0 ? void 0 : _a.toLowerCase();
let ids;
const externallyValidate = await shouldExternallyValidate(canExternallyValidate, configObject, locallyValidateIds);
if (!externallyValidate) {
(0, actionhero_1.log)(`[ config ] skipping external validation for ${configObject.class} \`${configObject.id}\``, "notice");
}
try {
switch (klass) {
case "setting":
ids = await (0, setting_1.loadSetting)(configObject, externallyValidate, validate);
break;
case "model":
ids = await (0, model_1.loadModel)(configObject, externallyValidate, validate);
break;
case "app":
ids = await (0, app_1.loadApp)(configObject, externallyValidate, validate);
break;
case "source":
ids = await (0, source_1.loadSource)(configObject, configObjects, externallyValidate, validate);
break;
case "property":
ids = await (0, property_1.loadProperty)(configObject, externallyValidate, validate);
break;
case "group":
ids = await (0, group_1.loadGroup)(configObject, externallyValidate, validate);
break;
case "schedule":
ids = await (0, schedule_1.loadSchedule)(configObject, externallyValidate, validate);
break;
case "destination":
ids = await (0, destination_1.loadDestination)(configObject, externallyValidate, validate);
break;
case "apikey":
ids = await (0, apiKey_1.loadApiKey)(configObject, externallyValidate, validate);
break;
case "team":
ids = await (0, team_1.loadTeam)(configObject, externallyValidate, validate);
break;
case "teammember":
ids = await (0, teamMember_1.loadTeamMember)(configObject, externallyValidate, validate);
break;
case "record":
ids = await (0, record_1.loadRecord)(configObject, externallyValidate, validate);
break;
case "profile":
deprecation_1.Deprecation.warnChanged("config", "Profile", "Record");
ids = await (0, record_1.loadRecord)(configObject, externallyValidate, validate);
break;
default:
throw new Error(`unknown config object class: ${configObject.class}`);
}
}
catch (error) {
// Normally, we can can keep going after an error and keep checking the other config objects
// but, there's some types of errors (like unique key duplicates) which pollute or abort the transaction and we need to stop
if (error instanceof sequelize_1.default.DatabaseError) {
(0, actionhero_1.log)(`TRANSACTION ABORTED at SequelizeDatabaseError at query: ${error.sql}`, "debug");
throw new Error(`Sequelize Database Error with Config object for ${configObject === null || configObject === void 0 ? void 0 : configObject.class
//@ts-ignore
} \`${configObject["key"] || configObject["name"]}\`(${configObject.id}). Cannot validate additional objects.`);
}
const { message, fields } = (0, errors_1.GrouparooErrorSerializer)(error);
const errorMessage = `[ config ] error with ${configObject === null || configObject === void 0 ? void 0 : configObject.class} \`${
//@ts-ignore
configObject["key"] || configObject["name"]}\` (${configObject.id}): ${message}`;
errors.push(errorMessage);
(0, actionhero_1.log)((error === null || error === void 0 ? void 0 : error.stack) || error, "debug");
if (fields.length === 0) {
(0, actionhero_1.log)(errorMessage, "warning");
continue;
}
else {
(0, actionhero_1.log)(errorMessage, "error");
throw new Error(`Cannot validate additional objects.`);
}
}
// should set ids in all cases
for (const className in ids) {
//@ts-ignore
const newIds = ids[className];
//@ts-ignore
seenIds[className].push(...newIds);
}
}
return { seenIds, errors, deletedIds };
}
exports.processConfigObjects = processConfigObjects;
async function deleteLockedObjects(seenIds) {
const deletedIds = freshIdsByClass();
if (seenIds.teammember) {
deletedIds["teammember"] = await (0, teamMember_1.deleteTeamMembers)(seenIds.teammember);
}
if (seenIds.team) {
deletedIds["team"] = await (0, team_1.deleteTeams)(seenIds.team);
}
if (seenIds.apikey) {
deletedIds["apikey"] = await (0, apiKey_1.deleteApiKeys)(seenIds.apikey);
}
if (seenIds.destination) {
deletedIds["destination"] = await (0, destination_1.deleteDestinations)(seenIds.destination);
}
if (seenIds.schedule) {
deletedIds["schedule"] = await (0, schedule_1.deleteSchedules)(seenIds.schedule);
}
if (seenIds.group) {
deletedIds["group"] = await (0, group_1.deleteGroups)(seenIds.group);
}
if (seenIds.property) {
deletedIds["property"] = await (0, property_1.deleteProperties)(seenIds.property);
}
if (seenIds.source) {
deletedIds["source"] = await (0, source_1.deleteSources)(seenIds.source);
}
if (seenIds.app) {
deletedIds["app"] = await (0, app_1.deleteApps)(seenIds.app);
}
if (seenIds.model) {
deletedIds["model"] = await (0, model_1.deleteModels)(seenIds.model);
}
return deletedIds;
}
exports.deleteLockedObjects = deleteLockedObjects;