@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
JavaScript
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);