UNPKG

@grouparoo/core

Version:
409 lines (408 loc) 16 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 App_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.App = void 0; const sequelize_typescript_1 = require("sequelize-typescript"); const actionhero_1 = require("actionhero"); const Source_1 = require("./Source"); const Option_1 = require("./Option"); const optionHelper_1 = require("./../modules/optionHelper"); const stateMachine_1 = require("./../modules/stateMachine"); const Destination_1 = require("./Destination"); const app_1 = require("../modules/ops/app"); const lockableHelper_1 = require("../modules/lockableHelper"); const configWriter_1 = require("../modules/configWriter"); const apiData_1 = require("../modules/apiData"); const AppRefreshQuery_1 = require("./AppRefreshQuery"); const commonModel_1 = require("../classes/commonModel"); const appsCache_1 = require("../modules/caches/appsCache"); const cls_1 = require("../modules/cls"); 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 App = App_1 = class App extends commonModel_1.CommonModel { idPrefix() { return "app"; } async appOptions() { const { pluginApp } = await this.getPlugin(); const staticAppOptions = pluginApp.options; const appOptions = typeof pluginApp.methods.appOptions === "function" ? await pluginApp.methods.appOptions() : {}; for (const staticOption of staticAppOptions) { if (staticOption.type && !appOptions[staticOption.key]) { appOptions[staticOption.key] = { type: staticOption.type }; } } return appOptions; } async getOptions(sourceFromEnvironment = true, obfuscatePasswords = false) { return optionHelper_1.OptionHelper.getOptions(this, sourceFromEnvironment, obfuscatePasswords); } async setOptions(options, externallyValidate = true) { return optionHelper_1.OptionHelper.setOptions(this, options, externallyValidate); } async afterSetOptions(hasChanges) { if (hasChanges) await App_1.invalidateCache(); if (hasChanges && this.state !== "draft" && !this.isNewRecord) { await actionhero_1.redis.doCluster("api.rpc.app.disconnect", [this.id], undefined, true); } } async validateOptions(options, externallyValidate = true) { if (!options) options = await this.getOptions(true); const { pluginApp } = await this.getPlugin(); if (!pluginApp) throw new Error(`cannot find a pluginApp for type ${this.type}`); const appOptions = externallyValidate ? await this.appOptions() : {}; const optionsSpec = pluginApp.options.map((opt) => { var _a, _b; return ({ ...opt, options: (_b = (_a = appOptions[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 setConnection(connection) { actionhero_1.api.plugins.persistentConnections[this.id] = connection; } async getConnection() { const connection = actionhero_1.api.plugins.persistentConnections[this.id]; if (!connection) return this.connect(null); return connection; } async connect(options) { return app_1.AppOps.connect(this, options, null); } async disconnect() { return app_1.AppOps.disconnect(this, undefined); } async test(options) { return app_1.AppOps.test(this, options); } async getParallelism() { const { pluginApp } = await this.getPlugin(); const method = pluginApp.methods.parallelism; if (!method) return Infinity; const appOptions = await this.getOptions(); return method({ app: this, appOptions }); } async checkAndUpdateParallelism(direction) { const key = this.parallelismKey(); const redis = actionhero_1.api.redis.clients.client; const limit = await this.getParallelism(); const count = await redis[direction](key); if (count < 0) { // invalid. how did this happen? reset it await redis.set(key, 0); } if (count <= limit || direction === "decr") { return true; } else { // move it back down because incremented await redis.decr(key); return false; } } parallelismKey() { return `app:${this.id}:ratelimit:parallel`; } async apiData() { const options = await this.getOptions(false, true); const icon = await this._getIcon(); const provides = this.provides(); const { plugin, pluginApp } = await this.getPlugin(); const refreshQueryAvailable = await this.refreshQueryAvailable(); const appRefreshQuery = await this.$get("appRefreshQuery", { scope: null }); return { id: this.id, name: this.name, icon, type: this.type, state: this.state, locked: this.locked, options, provides, pluginName: plugin.name, pluginApp, refreshQueryAvailable, createdAt: apiData_1.APIData.formatDate(this.createdAt), updatedAt: apiData_1.APIData.formatDate(this.updatedAt), appRefreshQuery: appRefreshQuery ? await appRefreshQuery.apiData() : null, }; } async refreshQueryAvailable() { var _a; const { pluginApp } = await this.getPlugin(); if (typeof ((_a = pluginApp === null || pluginApp === void 0 ? void 0 : pluginApp.methods) === null || _a === void 0 ? void 0 : _a.appQuery) !== "function") { return false; } return true; } /** * Determine if this App can provide Source or Destination Connections */ provides() { const source = actionhero_1.api.plugins.plugins.find((p) => { var _a; return (_a = p === null || p === void 0 ? void 0 : p.connections) === null || _a === void 0 ? void 0 : _a.find((c) => c.apps.includes(this.type) && c.direction === "import"); }) ? true : false; const destination = actionhero_1.api.plugins.plugins.find((p) => { var _a; return (_a = p === null || p === void 0 ? void 0 : p.connections) === null || _a === void 0 ? void 0 : _a.find((c) => c.apps.includes(this.type) && c.direction === "export"); }) ? true : false; return { source, destination }; } getConfigId() { return this.idIsDefault() ? configWriter_1.ConfigWriter.generateId(this.name) : this.id; } async getConfigObject() { const { type, name } = this; const options = await this.getOptions(false); const appRefreshQuery = await this.$get("appRefreshQuery"); if (!name) return; return { class: "App", id: this.getConfigId(), name, type, options, refresh: appRefreshQuery ? { query: appRefreshQuery.refreshQuery, recurringFrequency: appRefreshQuery.recurringFrequency, } : undefined, }; } async _getIcon() { const { plugin } = await this.getPlugin(); return plugin === null || plugin === void 0 ? void 0 : plugin.icon; } // --- Class Methods --- // // Disconnect all Apps from their persistent connections static async disconnect(id) { const apps = id ? await App_1.scope(null).findAll({ where: { id } }) : await App_1.scope(null).findAll(); for (const app of apps) await app.disconnect(); } static async checkMaxInstances(instance) { const count = await App_1.scope(null).count({ where: { type: instance.type }, }); const { pluginApp } = await instance.getPlugin(); if (pluginApp && pluginApp.maxInstances && pluginApp.maxInstances < count + 1) { throw new Error(`cannot create a new ${instance.type} app, only ${pluginApp.maxInstances} allowed`); } } static async validateType(instance) { await instance.getPlugin(); // will throw if not found } 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 ensureNotInUse(instance) { const sources = await Source_1.Source.scope(null).findAll({ where: { appId: instance.id }, }); if (sources.length > 0) { throw new Error(`cannot delete this app, source ${sources[0].id} relies on it`); } const destinations = await Destination_1.Destination.scope(null).findAll({ where: { appId: instance.id }, }); if (destinations.length > 0) { throw new Error(`cannot delete this app, destination ${destinations[0].id} relies on it`); } } static async checkMinInstances(instance) { const count = await App_1.scope(null).count({ where: { type: instance.type }, }); const { pluginApp } = await instance.getPlugin(); if (pluginApp && pluginApp.minInstances && pluginApp.minInstances > count - 1) { throw new Error(`cannot delete this ${instance.type} app, at least ${pluginApp.minInstances} required`); } } static async destroyOptions(instance) { return Option_1.Option.destroy({ where: { ownerId: instance.id, ownerType: "app", }, }); } static async destroyAppRefreshQueries(instance) { return AppRefreshQuery_1.AppRefreshQuery.destroy({ where: { appId: instance.id, }, }); } static async removeParallelismKey(instance) { const key = instance.parallelismKey(); const redis = actionhero_1.api.redis.clients.client; return redis.del(key); } static async invalidateCache() { appsCache_1.AppsCache.invalidate(); await cls_1.CLS.afterCommit(async () => await actionhero_1.redis.doCluster("api.rpc.app.invalidateCache")); } }; __decorate([ (0, sequelize_typescript_1.Length)({ min: 0, max: 191 }), (0, sequelize_typescript_1.Default)(""), sequelize_typescript_1.Column, __metadata("design:type", String) ], App.prototype, "name", void 0); __decorate([ (0, sequelize_typescript_1.AllowNull)(false), sequelize_typescript_1.Column, __metadata("design:type", String) ], App.prototype, "type", void 0); __decorate([ sequelize_typescript_1.Column, __metadata("design:type", String) ], App.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) ], App.prototype, "state", void 0); __decorate([ (0, sequelize_typescript_1.HasOne)(() => AppRefreshQuery_1.AppRefreshQuery), __metadata("design:type", AppRefreshQuery_1.AppRefreshQuery) ], App.prototype, "appRefreshQuery", void 0); __decorate([ (0, sequelize_typescript_1.HasMany)(() => Option_1.Option, { foreignKey: "ownerId", scope: { ownerType: "app" }, }), __metadata("design:type", Array) ], App.prototype, "__options", void 0); __decorate([ (0, sequelize_typescript_1.HasMany)(() => Source_1.Source), __metadata("design:type", Array) ], App.prototype, "sources", void 0); __decorate([ sequelize_typescript_1.BeforeCreate, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "checkMaxInstances", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "validateType", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "updateState", null); __decorate([ sequelize_typescript_1.BeforeSave, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "noUpdateIfLocked", null); __decorate([ sequelize_typescript_1.BeforeDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "noDestroyIfLocked", null); __decorate([ sequelize_typescript_1.BeforeDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "ensureNotInUse", null); __decorate([ sequelize_typescript_1.BeforeDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "checkMinInstances", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "destroyOptions", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "destroyAppRefreshQueries", null); __decorate([ sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", [App]), __metadata("design:returntype", Promise) ], App, "removeParallelismKey", null); __decorate([ sequelize_typescript_1.AfterSave, sequelize_typescript_1.AfterDestroy, __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], App, "invalidateCache", null); App = App_1 = __decorate([ (0, sequelize_typescript_1.DefaultScope)(() => ({ where: { state: "ready" }, })), (0, sequelize_typescript_1.Table)({ tableName: "apps", paranoid: false }) ], App); exports.App = App;