import { redis } from "actionhero"; import { Table, Column, CreatedAt, UpdatedAt, AllowNull, BeforeCreate, DataType, BeforeSave, BeforeDestroy, DefaultScope, Default, AfterSave, AfterDestroy, } from "sequelize-typescript"; import { Source } from "./Source"; import { ModelConfigurationObject } from "../classes/codeConfig"; import { APIData } from "../modules/apiData"; import { ConfigWriter } from "../modules/configWriter"; import { LockableHelper } from "../modules/lockableHelper"; import { Destination } from "./Destination"; import { Group } from "./Group"; import { GrouparooRecord } from "./GrouparooRecord"; import { RunOps } from "../modules/ops/runs"; import { StateMachine } from "../modules/stateMachine"; import { CommonModel } from "../classes/commonModel"; import { CLS } from "../modules/cls"; import { ModelsCache } from "../modules/caches/modelsCache"; export const ModelTypes = ["profile", "account", "event", "custom"] as const; export type ModelType = typeof ModelTypes[number]; const STATES = ["ready", "deleted"] as const; const STATE_TRANSITIONS: StateMachine.StateTransition[] = [ { from: "draft", to: "ready", checks: [] }, { from: "ready", to: "deleted", checks: [] }, { from: "deleted", to: "ready", checks: [], }, ]; @DefaultScope(() => ({ where: { state: "ready" }, })) @Table({ tableName: "models", paranoid: false }) export class GrouparooModel extends CommonModel { idPrefix() { return "mod"; } @Column({ primaryKey: true }) id: string; @CreatedAt createdAt: Date; @UpdatedAt updatedAt: Date; @AllowNull(false) @Column name: string; @AllowNull(false) @Column(DataType.ENUM(...ModelTypes)) type: ModelType; @Column locked: string; @AllowNull(false) @Default("ready") @Column(DataType.ENUM(...STATES)) state: typeof STATES[number]; getIcon() { switch (this.type) { case "profile": return "user"; case "account": return "building"; case "event": return "location-arrow"; case "custom": return "database"; default: throw new Error(`no icon for ${this.type} model`); } } async run(destinationId?: string) { return RunOps.run(this, destinationId); } async stopPreviousRuns() { return RunOps.stopPreviousRuns(this); } async apiData() { return { id: this.id, name: this.name, type: this.type, state: this.state, locked: this.locked, icon: this.getIcon(), createdAt: APIData.formatDate(this.createdAt), updatedAt: APIData.formatDate(this.updatedAt), }; } getConfigId() { return this.idIsDefault() ? ConfigWriter.generateId(this.name) : this.id; } async getConfigObject(): Promise { if (!this.name) return; return { class: "Model", id: this.getConfigId(), name: this.name, type: this.type, }; } // --- Class Methods --- // @BeforeCreate @BeforeSave static async ensureValidType(instance: GrouparooModel) { if (!ModelTypes.includes(instance.type)) { throw new Error( `${instance.type} is not a valid model type (${ModelTypes.join(", ")})` ); } } @BeforeSave static async noUpdateIfLocked(instance: GrouparooModel) { await LockableHelper.beforeSave(instance); } @BeforeSave static async updateState(instance: GrouparooModel) { await StateMachine.transition(instance, STATE_TRANSITIONS); } @BeforeDestroy static async noDestroyIfLocked(instance: GrouparooModel) { await LockableHelper.beforeDestroy(instance); } @BeforeDestroy static async ensureNotInUse(instance: GrouparooModel) { const sources = await Source.scope(null).findAll({ where: { modelId: instance.id }, }); if (sources.length > 0) { throw new Error( `cannot delete this model, source ${sources[0].id} relies on it` ); } const destinations = await Destination.scope(null).findAll({ where: { modelId: instance.id }, }); if (destinations.length > 0) { throw new Error( `cannot delete this model, destination ${destinations[0].id} relies on it` ); } const groups = await Group.scope(null).findAll({ where: { modelId: instance.id }, }); if (groups.length > 0) { throw new Error( `cannot delete this model, group ${groups[0].id} relies on it` ); } const records = await GrouparooRecord.unscoped().count({ where: { modelId: instance.id }, }); if (records > 0) { throw new Error( `cannot delete this model, ${records} records rely on it` ); } } @AfterSave @AfterDestroy static async invalidateCache() { ModelsCache.invalidate(); await CLS.afterCommit( async () => await redis.doCluster("api.rpc.model.invalidateCache") ); } }