UNPKG

@tachybase/module-workflow

Version:

A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.

577 lines (576 loc) 24.7 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); var __typeError = (msg) => { throw TypeError(msg); }; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __decoratorStart = (base) => [, , , __create((base == null ? void 0 : base[__knownSymbol("metadata")]) ?? null)]; var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"]; var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn; var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) }); var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]); var __runInitializers = (array, flags, self, value) => { for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value); return value; }; var __decorateElement = (array, flags, name, decorators, target, extra) => { var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16); var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5]; var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []); var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() { return __privateGet(this, extra); }, set [name](x) { return __privateSet(this, extra, x); } }, name)); k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name); for (var i = decorators.length - 1; i >= 0; i--) { ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers); if (k) { ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x }; if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name]; if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y; } it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1; if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it); else if (typeof it !== "object" || it === null) __typeError("Object expected"); else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn); } return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target; }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var Plugin_exports = {}; __export(Plugin_exports, { default: () => PluginWorkflowServer }); module.exports = __toCommonJS(Plugin_exports); var import_node_path = __toESM(require("node:path")); var import_server = require("@tego/server"); var import_lru_cache = require("lru-cache"); var import_actions = __toESM(require("./actions")); var import_constants = require("./constants"); var import_plugin = __toESM(require("./features/_deprecated-js-parse/plugin")); var import_plugin2 = __toESM(require("./features/_deprecated-json-parse/plugin")); var import_Plugin = require("./features/aggregate/Plugin"); var import_Plugin2 = require("./features/delay/Plugin"); var import_Plugin3 = require("./features/dynamic-calculation/Plugin"); var import_exit_branch = require("./features/exit-branch"); var import_interception = require("./features/interception"); var import_Plugin4 = require("./features/loop/Plugin"); var import_Plugin5 = require("./features/manual/Plugin"); var import_plugin3 = __toESM(require("./features/notice/plugin")); var import_omni_trigger = require("./features/omni-trigger"); var import_Plugin6 = require("./features/parallel/Plugin"); var import_Plugin7 = require("./features/request/Plugin"); var import_response = require("./features/response"); var import_plugin4 = __toESM(require("./features/script/plugin")); var import_Plugin8 = require("./features/sql/Plugin"); var import_plugin5 = require("./features/trigger-instruction/plugin"); var import_variables = require("./features/variables"); var import_functions = __toESM(require("./functions")); var import_CalculationInstruction = __toESM(require("./instructions/CalculationInstruction")); var import_ConditionInstruction = __toESM(require("./instructions/ConditionInstruction")); var import_CreateInstruction = __toESM(require("./instructions/CreateInstruction")); var import_DestroyInstruction = __toESM(require("./instructions/DestroyInstruction")); var import_EndInstruction = __toESM(require("./instructions/EndInstruction")); var import_QueryInstruction = __toESM(require("./instructions/QueryInstruction")); var import_SelectInstruction = __toESM(require("./instructions/SelectInstruction")); var import_UpdateInstruction = __toESM(require("./instructions/UpdateInstruction")); var import_UpdateOrCreateInstruction = __toESM(require("./instructions/UpdateOrCreateInstruction")); var import_Processor = __toESM(require("./Processor")); var import_remote_code_fetcher = require("./services/remote-code-fetcher"); var import_CollectionTrigger = __toESM(require("./triggers/CollectionTrigger")); var import_ScheduleTrigger = __toESM(require("./triggers/ScheduleTrigger")); var _PluginWorkflowServer_decorators, _init, _a; _PluginWorkflowServer_decorators = [(0, import_server.InjectedPlugin)({ Services: [import_remote_code_fetcher.WorkflowRemoteCodeFetcher] })]; class PluginWorkflowServer extends (_a = import_server.Plugin) { constructor(app, options) { super(app, options); this.instructions = new import_server.Registry(); this.triggers = new import_server.Registry(); this.functions = new import_server.Registry(); this.enabledCache = /* @__PURE__ */ new Map(); this.ready = false; this.executing = null; this.pending = []; this.events = []; this.eventsCount = 0; this.loggerCache = void 0; this.meter = null; this.onBeforeSave = async (instance, options) => { const Model = instance.constructor; if (instance.enabled) { instance.set("current", true); } else if (!instance.current) { const count = await Model.count({ where: { key: instance.key }, transaction: options.transaction }); if (!count) { instance.set("current", true); } } if (!instance.changed("enabled") || !instance.enabled) { return; } const previous = await Model.findOne({ where: { key: instance.key, current: true, id: { [import_server.Op.ne]: instance.id } }, transaction: options.transaction }); if (previous) { await previous.update( { enabled: false, current: null }, { transaction: options.transaction, hooks: false } ); this.toggle(previous, false); } }; this.prepare = async () => { if (this.executing && this.db.options.dialect === "sqlite") { await this.executing; } const event = this.events.shift(); this.eventsCount = this.events.length; if (!event) { this.getLogger("dispatcher").warn(`events queue is empty, no need to prepare`); return; } const logger = this.getLogger(event[0].id); logger.info(`preparing execution for event`); try { const execution = await this.createExecution(...event); if (execution && !this.executing && !this.pending.length) { this.pending.push([execution]); } } catch (err) { logger.error(`failed to create execution: ${err.message}`, err); } if (this.events.length) { await this.prepare(); } else { this.dispatch(); } }; this.addFeature(import_Plugin8.PluginSql); this.addFeature(import_Plugin7.PluginRequest); this.addFeature(import_Plugin6.PluginParallel); this.addFeature(import_Plugin5.PluginManual); this.addFeature(import_Plugin4.PluginLoop); this.addFeature(import_Plugin3.PluginDynamicCalculation); this.addFeature(import_Plugin2.PluginDelay); this.addFeature(import_Plugin.PluginAggregate); this.addFeature(import_plugin2.default); this.addFeature(import_plugin.default); this.addFeature(import_plugin4.default); this.addFeature(import_interception.PluginInterception); this.addFeature(import_variables.PluginVariables); this.addFeature(import_response.PluginResponse); this.addFeature(import_omni_trigger.PluginOmniTrigger); this.addFeature(import_plugin5.PluginTriggerInstruction); this.addFeature(import_plugin3.default); this.addFeature(import_exit_branch.PluginExitBranch); } getLogger(workflowId) { const now = /* @__PURE__ */ new Date(); const date = `${now.getFullYear()}-${`0${now.getMonth() + 1}`.slice(-2)}-${`0${now.getDate()}`.slice(-2)}`; const key = `${date}-${workflowId}}`; if (this.loggerCache.has(key)) { return this.loggerCache.get(key); } const logger = this.createLogger({ dirname: import_node_path.default.join("workflows", date), filename: `${workflowId}.log`, transports: process.env.APP_ENV !== "production" ? ["console"] : ["file"] }); this.loggerCache.set(key, logger); return logger; } isWorkflowSync(workflow) { const trigger = this.triggers.get(workflow.type); if (!trigger) { throw new Error(`invalid trigger type ${workflow.type} of workflow ${workflow.id}`); } return trigger.sync ?? workflow.sync; } registerTrigger(type, trigger) { if (typeof trigger === "function") { this.triggers.register(type, new trigger(this)); } else if (trigger) { this.triggers.register(type, trigger); } else { throw new Error("invalid trigger type to register"); } } registerInstruction(type, instruction) { if (typeof instruction === "function") { this.instructions.register(type, new instruction(this)); } else if (instruction) { this.instructions.register(type, instruction); } else { throw new Error("invalid instruction type to register"); } } initTriggers(more = {}) { this.registerTrigger("collection", import_CollectionTrigger.default); this.registerTrigger("schedule", import_ScheduleTrigger.default); for (const [name, trigger] of Object.entries(more)) { this.registerTrigger(name, trigger); } } initInstructions(more = {}) { this.registerInstruction("calculation", import_CalculationInstruction.default); this.registerInstruction("condition", import_ConditionInstruction.default); this.registerInstruction("end", import_EndInstruction.default); this.registerInstruction("create", import_CreateInstruction.default); this.registerInstruction("updateorcreate", import_UpdateOrCreateInstruction.default); this.registerInstruction("destroy", import_DestroyInstruction.default); this.registerInstruction("query", import_QueryInstruction.default); this.registerInstruction("select", import_SelectInstruction.default); this.registerInstruction("update", import_UpdateInstruction.default); for (const [name, instruction] of Object.entries({ ...more })) { this.registerInstruction(name, instruction); } } async load() { const { db, options } = this; (0, import_actions.default)(this); this.initTriggers(options.triggers); this.initInstructions(options.instructions); (0, import_functions.default)(this, options.functions); this.loggerCache = new import_lru_cache.LRUCache({ max: 20, updateAgeOnGet: true, dispose(logger) { logger.end(); } }); this.app.acl.registerSnippet({ name: `pm.${this.name}.workflows`, actions: [ "workflows:*", "workflows.nodes:*", "executions:list", "executions:get", "executions:cancel", "executions:retry", "flow_nodes:update", "flow_nodes:destroy", "flow_nodes:moveUp", "flow_nodes:moveDown", "flow_nodes:syncRemoteCode", "workflowCategories:*" ] }); this.app.acl.registerSnippet({ name: "ui.*", actions: ["workflows:list"] }); this.app.acl.allow("workflows", ["trigger", "list"], "loggedIn"); db.on("workflows.beforeSave", this.onBeforeSave); db.on("workflows.afterSave", (model) => this.toggle(model)); db.on("workflows.afterDestroy", (model) => this.toggle(model, false)); this.app.on("beforeStart", async () => { const collection = db.getCollection("workflows"); const workflows = await collection.repository.find({ filter: { enabled: true } }); workflows.forEach((workflow) => { this.toggle(workflow); }); }); this.app.on("afterStart", () => { this.app.setMaintainingMessage("check for not started executions"); this.ready = true; this.dispatch(); }); this.app.on("beforeStop", async () => { const repository = db.getRepository("workflows"); const workflows = await repository.find({ filter: { enabled: true } }); workflows.forEach((workflow) => { this.toggle(workflow, false); }); this.ready = false; if (this.events.length) { await this.prepare(); } if (this.executing) { await this.executing; } }); this.db.on("webhooks.afterCreate", this.removeWebhooksCache.bind(this)); this.db.on("webhooks.afterUpdate", this.removeWebhooksCache.bind(this)); this.db.on("webhooks.afterDestroy", this.removeWebhooksCache.bind(this)); } async removeWebhooksCache() { this.app.cache.del("webhooks"); } toggle(workflow, enable) { const type = workflow.get("type"); const trigger = this.triggers.get(type); if (!trigger) { this.getLogger(workflow.id).error(`trigger type ${workflow.type} of workflow ${workflow.id} is not implemented`); return; } if (enable ?? workflow.get("enabled")) { const prev = workflow.previous(); if (prev.config) { trigger.off({ ...workflow.get(), ...prev }); } trigger.on(workflow); this.enabledCache.set(workflow.id, workflow); } else { trigger.off(workflow); this.enabledCache.delete(workflow.id); } } trigger(workflow, context, options = {}) { const logger = this.getLogger(workflow.id); if (!this.ready) { logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`); logger.debug(`ignored event data:`, context); return; } if (context == null) { logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`); return; } if (this.isWorkflowSync(workflow)) { return this.triggerSync(workflow, context, options); } const { transaction, ...rest } = options; this.events.push([workflow, context, rest]); this.eventsCount = this.events.length; logger.info(`new event triggered, now events: ${this.events.length}`); logger.debug(`event data:`, { context }); if (this.events.length > 1) { return; } setTimeout(this.prepare); } async triggerSync(workflow, context, options = {}) { let execution; try { execution = await this.createExecution(workflow, context, options); } catch (err) { this.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err); return null; } try { return this.process(execution, null, options); } catch (err) { this.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err); } return null; } async resume(job) { if (!job.execution) { job.execution = await job.getExecution(); } this.getLogger(job.execution.workflowId).info( `execution (${job.execution.id}) resuming from job (${job.id}) added to pending list` ); this.pending.push([job.execution, job]); this.dispatch(); } createProcessor(execution, options = {}) { return new import_Processor.default(execution, { ...options, plugin: this }); } async createExecution(workflow, context, options) { const { transaction = await this.db.sequelize.transaction() } = options; const sameTransaction = options.transaction === transaction; const trigger = this.triggers.get(workflow.type); const valid = await trigger.validateEvent(workflow, context, { ...options, transaction }); if (!valid) { if (!sameTransaction) { await transaction.commit(); } return null; } let execution; try { execution = await workflow.createExecution( { context, key: workflow.key, status: import_constants.EXECUTION_STATUS.QUEUEING, parentNode: options.parentNode || null, parentId: options.parent ? options.parent.id : null }, { transaction } ); } catch (err) { if (!sameTransaction) { await transaction.rollback(); } throw err; } this.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`); await workflow.increment(["executed", "allExecuted"], { transaction }); if (this.db.options.dialect !== "postgres") { await workflow.reload({ transaction }); } await workflow.constructor.update( { allExecuted: workflow.allExecuted }, { where: { key: workflow.key }, transaction } ); if (!sameTransaction) { await transaction.commit(); } execution.workflow = workflow; return execution; } dispatch() { if (!this.ready) { this.getLogger("dispatcher").warn(`app is not ready, new dispatching will be ignored`); return; } if (this.executing) { this.getLogger("dispatcher").warn(`workflow executing is not finished, new dispatching will be ignored`); return; } if (this.events.length) { return this.prepare(); } this.executing = (async () => { let next = null; if (this.pending.length) { next = this.pending.shift(); this.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`); } else { const execution = await this.db.getRepository("executions").findOne({ filter: { status: import_constants.EXECUTION_STATUS.QUEUEING, "workflow.enabled": true, "workflow.id": { [import_server.Op.not]: null } }, appends: ["workflow"], sort: "createdAt" }); if (execution) { this.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`); next = [execution]; } } if (next) { await this.process(...next); } this.executing = null; if (next || this.events.length || this.pending.length) { this.dispatch(); } })(); } async process(execution, job, options = {}) { var _a2, _b; if (execution.status === import_constants.EXECUTION_STATUS.QUEUEING) { await execution.update({ status: import_constants.EXECUTION_STATUS.STARTED }, { transaction: options.transaction }); } const logger = this.getLogger(execution.workflowId); const processor = this.createProcessor(execution, options); logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`); try { await (job ? processor.resume(job) : processor.start()); logger.info(`execution (${execution.id}) finished with status: ${execution.status}`, { execution }); let shouldDeleteExecution = false; if (execution.status !== 0) { const executionDuration = execution.updatedAt.getTime() - execution.createdAt.getTime(); await execution.update({ executionCost: executionDuration }, { transaction: options.transaction }); const deleteOnStatus = (_b = (_a2 = execution.workflow) == null ? void 0 : _a2.options) == null ? void 0 : _b.deleteExecutionOnStatus; shouldDeleteExecution = Array.isArray(deleteOnStatus) && deleteOnStatus.includes(execution.status); } if (execution.status && execution.parentNode) { const { database } = execution.constructor; const { model } = database.getCollection("executions"); const parent = await model.findByPk(execution.parentId, options); const jobs = await parent.getJobs(); const job2 = jobs.find((v) => v.status === import_constants.JOB_STATUS.PENDING && v.nodeId === execution.parentNode); if (job2) { const lastSavedJob = processor.lastSavedJob; job2.set({ status: execution.status, result: (lastSavedJob == null ? void 0 : lastSavedJob.result) ?? execution.context }); await this.resume(job2); } else { logger.error(`execution (${execution.id}) error: parent job not found`); } } if (shouldDeleteExecution) { await execution.destroy({ transaction: options.transaction }); return processor; } } catch (err) { logger.error(`execution (${execution.id}) error: ${err.message}`, err); } return processor; } useDataSourceTransaction(dataSourceName = "main", transaction, create = false) { const { db } = this.app.dataSourceManager.dataSources.get(dataSourceName).collectionManager; if (!db) { return; } if (db.sequelize === (transaction == null ? void 0 : transaction.sequelize)) { return transaction; } if (create) { return db.sequelize.transaction(); } } } _init = __decoratorStart(_a); PluginWorkflowServer = __decorateElement(_init, 0, "PluginWorkflowServer", _PluginWorkflowServer_decorators, PluginWorkflowServer); __runInitializers(_init, 1, PluginWorkflowServer);