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