UNPKG

@grouparoo/core

Version:
674 lines (673 loc) 29.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var Destination_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.Destination = exports.DestinationSyncModeData = void 0; const actionhero_1 = require("actionhero"); const sequelize_1 = require("sequelize"); const sequelize_typescript_1 = require("sequelize-typescript"); const apiData_1 = require("../modules/apiData"); const destinationTypeConversions_1 = require("../modules/destinationTypeConversions"); const lockableHelper_1 = require("../modules/lockableHelper"); const export_1 = require("../modules/ops/export"); const plugin_1 = require("../modules/plugin"); const configWriter_1 = require("./../modules/configWriter"); const mappingHelper_1 = require("./../modules/mappingHelper"); const destination_1 = require("./../modules/ops/destination"); const optionHelper_1 = require("./../modules/optionHelper"); const stateMachine_1 = require("./../modules/stateMachine"); const App_1 = require("./App"); const DestinationGroupMembership_1 = require("./DestinationGroupMembership"); const Export_1 = require("./Export"); const Group_1 = require("./Group"); const GroupRule_1 = require("./GroupRule"); const Mapping_1 = require("./Mapping"); const Option_1 = require("./Option"); const GrouparooModel_1 = require("./GrouparooModel"); const modelGuard_1 = require("../modules/modelGuard"); const commonModel_1 = require("../classes/commonModel"); const propertiesCache_1 = require("../modules/caches/propertiesCache"); const destinationsCache_1 = require("../modules/caches/destinationsCache"); const cls_1 = require("../modules/cls"); const SYNC_MODES = ["sync", "additive", "enrich"]; const DESTINATION_COLLECTIONS = ["none", "group", "model"]; exports.DestinationSyncModeData = { sync: { key: "sync", displayName: "Sync", description: "Sync all records (create, update, delete)", operations: { create: true, update: true, delete: true, }, }, additive: { key: "additive", displayName: "Additive", description: "Sync all records, but do not delete (create, update)", operations: { create: true, update: true, delete: false, }, }, enrich: { key: "enrich", displayName: "Enrich", description: "Only update existing records (update)", operations: { create: false, update: true, delete: false, }, }, }; const STATES = ["draft", "ready", "deleted"]; const STATE_TRANSITIONS = [ { from: "draft", to: "ready", checks: [(instance) => instance.validateOptions()], }, { from: "draft", to: "deleted", checks: [] }, { from: "ready", to: "deleted", checks: [] }, { from: "deleted", to: "ready", checks: [(instance) => instance.validateOptions()], }, ]; let Destination = Destination_1 = class Destination extends commonModel_1.CommonModel { idPrefix() { return "dst"; } async apiData(includeApp = true, includeGroup = true) { let app; let group; if (includeApp) app = await this.$get("app", { scope: null, include: [Option_1.Option] }); if (includeGroup) { group = await this.$get("group", { scope: null, include: [GroupRule_1.GroupRule] }); } const model = await this.$get("model"); const mapping = await this.getMapping(); const options = await this.getOptions(null); const destinationGroupMemberships = await this.getDestinationGroupMemberships(); const { pluginConnection } = await this.getPlugin(); const exportTotals = await this.getExportTotals(); const syncMode = await this.getSyncMode(); const { supportedModes } = await this.getSupportedSyncModes(); const syncModeData = supportedModes.map((mode) => exports.DestinationSyncModeData[mode]); return { id: this.id, name: this.name, type: this.type, state: this.state, locked: this.locked, syncMode, syncModes: syncModeData, collection: this.collection, app: app ? await app.apiData() : null, modelId: this.modelId, modelName: model.name, mapping, options, connection: pluginConnection, group: group ? await group.apiData() : null, destinationGroupMemberships, createdAt: apiData_1.APIData.formatDate(this.createdAt), updatedAt: apiData_1.APIData.formatDate(this.updatedAt), exportTotals, }; } async getExportTotals() { return export_1.ExportOps.totals({ destinationId: this.id }); } async getMapping() { return mappingHelper_1.MappingHelper.getMapping(this); } async setMapping(mappings, externallyValidate = true, saveCache = true) { if (externallyValidate) await this.validateMappings(mappings, saveCache); await mappingHelper_1.MappingHelper.setMapping(this, mappings, externallyValidate); await Destination_1.invalidateCache(); } async getOptions(sourceFromEnvironment = true) { return optionHelper_1.OptionHelper.getOptions(this, sourceFromEnvironment); } async setOptions(options, externallyValidate = true) { await this.validateUniqueAppAndOptionsForGroup(options); return optionHelper_1.OptionHelper.setOptions(this, options, externallyValidate); } async afterSetOptions(hasChanges) { if (hasChanges) { await Destination_1.invalidateCache(); return this.exportMembers(); } } async getExportArrayProperties() { return destination_1.DestinationOps.getExportArrayProperties(this); } async getDestinationGroupMemberships() { const destinationGroupMemberships = await DestinationGroupMembership_1.DestinationGroupMembership.findAll({ where: { destinationId: this.id }, include: [Group_1.Group], }); return destinationGroupMemberships.map((dgm) => { return { remoteKey: dgm.remoteKey, groupId: dgm.group.id, groupName: dgm.group.name, }; }); } async setDestinationGroupMemberships(newDestinationGroupMemberships) { for (const groupId in newDestinationGroupMemberships) { const group = await Group_1.Group.findById(groupId); if (group.state === "draft" || group.state === "deleted") { throw new Error(`group ${group.name} is not ready`); } } await DestinationGroupMembership_1.DestinationGroupMembership.destroy({ where: { destinationId: this.id, }, }); for (const groupId in newDestinationGroupMemberships) { await DestinationGroupMembership_1.DestinationGroupMembership.create({ destinationId: this.id, groupId, remoteKey: newDestinationGroupMemberships[groupId], }); } return this.getDestinationGroupMemberships(); } async validateOptions(options, externallyValidate = true) { if (!options) options = await this.getOptions(true); const { pluginConnection } = await this.getPlugin(); if (!pluginConnection) { throw new Error(`cannot find a pluginConnection for type ${this.type}`); } const connectionOptions = externallyValidate ? await this.destinationConnectionOptions(options) : {}; const optionsSpec = pluginConnection.options.map((opt) => { var _a, _b; return ({ ...opt, options: (_b = (_a = connectionOptions[opt.key]) === null || _a === void 0 ? void 0 : _a.options) !== null && _b !== void 0 ? _b : [], }); }); return optionHelper_1.OptionHelper.validateOptions(this, options, optionsSpec); } async getPlugin() { return optionHelper_1.OptionHelper.getPlugin(this); } async exportMembers() { return destination_1.DestinationOps.exportMembers(this); } async updateTracking(collection, collectionId) { return destination_1.DestinationOps.updateTracking(this, collection, collectionId); } async getSupportedSyncModes() { return destination_1.DestinationOps.getSupportedSyncModes(this); } async getSyncMode() { if (this.syncMode) return this.syncMode; const { supportedModes, defaultMode } = await this.getSupportedSyncModes(); if (supportedModes.length > 1 && defaultMode) { // If destination has defined a default sync mode, use it but warn about its usage (0, actionhero_1.log)(`Using default sync mode "${defaultMode}" for destination "${this.name}". You should explicitly set a sync mode for the destination to prevent unintended behavior.`, "warning"); return defaultMode; } else if (supportedModes.length === 1) { // If destination only supports one sync mode, use it return supportedModes[0]; } return null; } async validateSyncMode() { const { supportedModes, defaultMode } = await this.getSupportedSyncModes(); // Sync mode is not required for destinations that: // 1. Have not implemented sync modes // 2. Only support one sync mode (it'll always be used) // 3. Have defined a default sync mode const isRequired = supportedModes.length > 1 && !defaultMode; if (isRequired && !this.syncMode) { throw new Error(`Sync mode is required for destination ${this.name}`); } if (this.syncMode && !supportedModes.includes(this.syncMode)) { throw new Error(`${this.name} does not support sync mode "${this.syncMode}"`); } } async validateMappings(mappings, saveCache = true) { if (Object.keys(mappings).length === 0) return; const destinationMappingOptions = await this.destinationMappingOptions(false, saveCache); const properties = await propertiesCache_1.PropertiesCache.findAllWithCache(this.modelId, "ready"); const exportArrayProperties = await this.getExportArrayProperties(); // check for array properties Object.values(mappings).forEach((k) => { const property = properties.find((r) => r.key === k); if (property && property.isArray && !exportArrayProperties.includes(k) && !exportArrayProperties.includes("*")) { throw new Error(`${k} is an array record property that ${this.name} cannot support`); } }); // required for (const i in destinationMappingOptions.properties.required) { const opt = destinationMappingOptions.properties.required[i]; if (!mappings[opt.key]) { throw new Error(`${opt.key} is a required destination mapping option`); } const property = properties.find((r) => r.key === mappings[opt.key]); const validDestinationTypes = (property === null || property === void 0 ? void 0 : property.type) ? Object.keys(destinationTypeConversions_1.destinationTypeConversions[property.type]) : []; if (property && !(validDestinationTypes === null || validDestinationTypes === void 0 ? void 0 : validDestinationTypes.includes(opt.type))) { throw new Error(`${opt.key} requires a property of type ${opt.type}, but a ${property.type} (${property.key}) was mapped`); } } // known for (const i in destinationMappingOptions.properties.known) { const opt = destinationMappingOptions.properties.known[i]; const property = properties.find((r) => r.key === mappings[opt.key]); const validDestinationTypes = (property === null || property === void 0 ? void 0 : property.type) ? Object.keys(destinationTypeConversions_1.destinationTypeConversions[property.type]) : []; if (property && !(validDestinationTypes === null || validDestinationTypes === void 0 ? void 0 : validDestinationTypes.includes(opt.type))) { throw new Error(`${opt.key} requires a property of type ${opt.type}, but a ${property.type} (${property.key}) was mapped`); } } // optional rules can't be validated... } async parameterizedOptions() { const parameterizedOptions = {}; const options = await this.getOptions(); const keys = Object.keys(options); for (const i in keys) { const k = keys[i]; parameterizedOptions[k] = typeof options[k] === "string" ? await plugin_1.plugin.replaceTemplateRunVariables(options[k].toString()) : options[k]; } return parameterizedOptions; } async destinationConnectionOptions(destinationOptions = {}) { return destination_1.DestinationOps.destinationConnectionOptions(this, destinationOptions); } async destinationMappingOptions(cached, saveCache = true) { return destination_1.DestinationOps.destinationMappingOptions(this, cached, saveCache); } async recordPreview(record, mapping, destinationGroupMemberships) { return destination_1.DestinationOps.recordPreview(this, record, mapping, destinationGroupMemberships); } async checkRecordWillBeExported(record) { const recordGroupIds = (await record.$get("groups", { attributes: ["id"] })).map((group) => group.id); if (!recordGroupIds.includes(this.groupId)) { throw new Error(`record ${record.id} will not be exported by this destination`); } return true; } async exportRecord(record, sync = false, force = false, saveExports = true, toDelete) { const exports = await destination_1.DestinationOps.exportRecords(this, [record], sync, force, saveExports, toDelete); return exports[0]; } async sendExport(_export, sync = false) { return destination_1.DestinationOps.sendExport(this, _export, sync); } async sendExports(_exports, sync = false) { return destination_1.DestinationOps.sendExports(this, _exports, sync); } async runExportProcessor(exportProcessor) { return destination_1.DestinationOps.runExportProcessor(this, exportProcessor); } async validateUniqueAppAndOptionsForGroup(options) { if (!options) options = await this.getOptions(true); const otherDestinations = await Destination_1.scope(null).findAll({ where: { appId: this.appId, type: this.type, id: { [sequelize_1.Op.not]: this.id }, }, }); for (const otherDestination of otherDestinations) { const otherOptions = await otherDestination.getOptions(true); const isSameGroup = this.collection === "group" && otherDestination.collection === "group" ? this.groupId === otherDestination.groupId : false; const isSameModel = this.collection === "model" && otherDestination.collection === "model" ? this.modelId === otherDestination.modelId : false; const isSameOptions = JSON.stringify(Object.entries(otherOptions)) === JSON.stringify(Object.entries(options)); if (isSameOptions && (isSameGroup || isSameModel)) { throw new Error(`destination "${otherDestination.name}" (${otherDestination.id}) is already using this app with the same options and group`); } } } getConfigId() { return this.idIsDefault() ? configWriter_1.ConfigWriter.generateId(this.name) : this.id; } async getConfigObject() { var _a, _b, _c; const { name, type, syncMode } = this; this.app = await this.$get("app"); this.model = await this.$get("model"); const modelId = (_a = this.model) === null || _a === void 0 ? void 0 : _a.getConfigId(); const appId = (_b = this.app) === null || _b === void 0 ? void 0 : _b.getConfigId(); this.group = await this.$get("group"); const groupId = (_c = this.group) === null || _c === void 0 ? void 0 : _c.getConfigId(); const dgms = await DestinationGroupMembership_1.DestinationGroupMembership.findAll({ where: { destinationId: this.id }, include: [Group_1.Group], }); const destinationGroupMemberships = Object.fromEntries(dgms.map((dgm) => [dgm.remoteKey, dgm.group.getConfigId()])); const options = await this.getOptions(false); const mapping = await mappingHelper_1.MappingHelper.getConfigMapping(this); if (!name || !appId || !modelId) { return; } return { class: "Destination", id: this.getConfigId(), modelId, name, type, appId, collection: this.collection, groupId, syncMode, options, mapping, destinationGroupMemberships, }; } // --- Class Methods --- // static async ensureModel(instance) { return modelGuard_1.ModelGuard.check(instance); } static async ensureAppReady(instance) { const app = await App_1.App.findById(instance.appId); if (app.state !== "ready") throw new Error(`app ${app.id} is not ready`); } static async ensureSupportedAppType(instance) { const app = await App_1.App.findById(instance.appId); const { pluginConnection } = await instance.getPlugin(); if (!pluginConnection.apps.includes(app.type)) throw new Error(`Destination of type "${instance.type}" does not support the App \`${app.name}\` (${app.id}) of type "${app.type}". Supported App types: ${pluginConnection.apps.join(", ")}.`); } static async ensureExportRecordsMethod(instance) { const { pluginConnection } = await instance.getPlugin(); if (!pluginConnection) { throw new Error(`a destination of type ${instance.type} cannot be found`); } if (!(pluginConnection === null || pluginConnection === void 0 ? void 0 : pluginConnection.methods.exportRecord) && !(pluginConnection === null || pluginConnection === void 0 ? void 0 : pluginConnection.methods.exportRecords)) { throw new Error(`a destination of type ${instance.type} cannot be created as there are no record export methods`); } } static async validateSyncMode(instance) { if (instance.state !== "ready") return; await instance.validateSyncMode(); } static async validateRecordCollectionMode(instance) { if (instance.collection && !DESTINATION_COLLECTIONS.includes(instance.collection)) { throw new Error(`${instance.collection} is not a valid destination collection`); } if (instance.collection !== "group" && instance.groupId) { instance.groupId = null; } } static async ensureOnlyOneDestinationPerAppWithSameSettingsAndGroup(instance) { await instance.validateUniqueAppAndOptionsForGroup(null); } static async updateState(instance) { await stateMachine_1.StateMachine.transition(instance, STATE_TRANSITIONS); } static async noUpdateIfLocked(instance) { await lockableHelper_1.LockableHelper.beforeSave(instance, ["state"]); } static async noDestroyIfLocked(instance) { await lockableHelper_1.LockableHelper.beforeDestroy(instance); } static async waitForPendingExports(instance) { const pendingExportCount = await instance.$count("exports", { where: { state: ["pending", "processing"] }, }); if (pendingExportCount > 0) { throw new Error(`cannot delete destination until all pending exports have been processed (${pendingExportCount} pending)`); } } static async destroyDestinationMappings(instance) { return Mapping_1.Mapping.destroy({ where: { ownerId: instance.id }, }); } static async destroyDestinationOptions(instance) { return Option_1.Option.destroy({ where: { ownerId: instance.id, ownerType: "destination" }, }); } static async destroyDestinationGroupMemberships(instance) { return DestinationGroupMembership_1.DestinationGroupMembership.destroy({ where: { destinationId: instance.id }, }); } static async unassociateRelatedExports(instance) { return Export_1.Export.update({ destinationId: null }, { where: { destinationId: instance.id } }); } static async invalidateCache() { destinationsCache_1.DestinationsCache.invalidate(); await cls_1.CLS.afterCommit(async () => await actionhero_1.redis.doCluster("api.rpc.destination.invalidateCache")); } }; __decorate([ (0, sequelize_typescript_1.AllowNull)(false), sequelize_typescript_1.Column, (0, sequelize_typescript_1.ForeignKey)(() => App_1.App), __metadata("design:type", String) ], Destination.prototype, "appId", void 0); __decorate([ (0, sequelize_typescript_1.Length)({ min: 0, max: 191 }), (0, sequelize_typescript_1.Default)(""), sequelize_typescript_1.Column, __metadata("design:type", String) ], Destination.prototype, "name", void 0); __decorate([ (0, sequelize_typescript_1.AllowNull)(false), sequelize_typescript_1.Column, __metadata("design:type", String) ], Destination.prototype, "type", void 0); __decorate([ sequelize_typescript_1.Column, __metadata("design:type", String) ], Destination.prototype, "locked", void 0); __decorate([ (0, sequelize_typescript_1.AllowNull)(false), (0, sequelize_typescript_1.Default)("draft"), (0, sequelize_typescript_1.Column)(sequelize_typescript_1.DataType.ENUM(...STATES)), __metadata("design:type", Object) ], Destination.prototype, "state", void 0); __decorate([ sequelize_typescript_1.Column, (0, sequelize_typescript_1.ForeignKey)(() => Group_1.Group), __metadata("design:type", String) ], Destination.prototype, "groupId", void 0); __decorate([ (0, sequelize_typescript_1.Column)(sequelize_typescript_1.DataType.ENUM(...SYNC_MODES)), __metadata("design:type", String) ], Destination.prototype, "syncMode", void 0); __decorate([ (0, sequelize_typescript_1.AllowNull)(false), (0, sequelize_typescript_1.Default)("none"), (0, sequelize_typescript_1.Column)(sequelize_typescript_1.DataType.ENUM(...DESTINATION_COLLECTIONS)), __metadata("design:type", String) ], Destination.prototype, "collection", void 0); __decorate([ (0, sequelize_typescript_1.AllowNull)(false), (0, sequelize_typescript_1.ForeignKey)(() => GrouparooModel_1.GrouparooModel), sequelize_typescript_1.Column, __metadata("design:type", String) ], Destination.prototype, "modelId", void 0); __decorate([ (0, sequelize_typescript_1.BelongsTo)(() => App_1.App), __metadata("design:type", App_1.App) ], Destination.prototype, "app", void 0); __decorate([ (0, sequelize_typescript_1.HasMany)(() => DestinationGroupMembership_1.DestinationGroupMembership), __metadata("design:type", Array) ], Destination.prototype, "destinationGroupMemberships", void 0); __decorate([ (0, sequelize_typescript_1.BelongsTo)(() => Group_1.Group), __metadata("design:type", Group_1.Group) ], Destination.prototype, "group", void 0); __decorate([ (0, sequelize_typescript_1.HasMany)(() => Mapping_1.Mapping), __metadata("design:type", Array) ], Destination.prototype, "mappings", void 0); __decorate([ (0, sequelize_typescript_1.HasMany)(() => Option_1.Option, { foreignKey: "ownerId", scope: { ownerType: "destination" }, }), __metadata("design:type", Array) ], Destination.prototype, "__options", void 0); __decorate([ (0, sequelize_typescript_1.HasMany)(() => Export_1.Export), __metadata("design:type", Array) ], Destination.prototype, "exports", void 0); __decorate([ (0, sequelize_typescript_1.BelongsTo)(() => GrouparooModel_1.GrouparooModel), __metadata("design:type", GrouparooModel_1.GrouparooModel) ], Destination.prototype, "model", void 0); __decorate([ sequelize_typescript_1.BeforeCreate, sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "ensureModel", null); __decorate([ sequelize_typescript_1.BeforeCreate, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "ensureAppReady", null); __decorate([ sequelize_typescript_1.BeforeCreate, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "ensureSupportedAppType", null); __decorate([ sequelize_typescript_1.BeforeCreate, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "ensureExportRecordsMethod", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "validateSyncMode", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "validateRecordCollectionMode", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "ensureOnlyOneDestinationPerAppWithSameSettingsAndGroup", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "updateState", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "noUpdateIfLocked", null); __decorate([ sequelize_typescript_1.BeforeDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "noDestroyIfLocked", null); __decorate([ sequelize_typescript_1.BeforeDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "waitForPendingExports", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "destroyDestinationMappings", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "destroyDestinationOptions", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "destroyDestinationGroupMemberships", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [Destination]), __metadata("design:returntype", Promise) ], Destination, "unassociateRelatedExports", null); __decorate([ sequelize_typescript_1.AfterSave, sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Destination, "invalidateCache", null); Destination = Destination_1 = __decorate([ (0, sequelize_typescript_1.DefaultScope)(() => ({ where: { state: "ready" }, })), (0, sequelize_typescript_1.Scopes)(() => ({ notDraft: { where: { state: { [sequelize_1.Op.notIn]: ["draft"] }, }, }, })), (0, sequelize_typescript_1.Table)({ tableName: "destinations", paranoid: false }) ], Destination); exports.Destination = Destination;