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.

304 lines (303 loc) 11 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; 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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var Processor_exports = {}; __export(Processor_exports, { default: () => Processor }); module.exports = __toCommonJS(Processor_exports); var import_server = require("@tego/server"); var import_constants = require("./constants"); class Processor { constructor(execution, options) { this.execution = execution; this.options = options; this.nodes = []; this.nodesMap = /* @__PURE__ */ new Map(); this.jobsMap = /* @__PURE__ */ new Map(); this.jobsMapByNodeKey = {}; this.lastSavedJob = null; this.logger = options.plugin.getLogger(execution.workflowId); this.transaction = options.transaction; } // make dual linked nodes list then cache makeNodes(nodes = []) { this.nodes = nodes; nodes.forEach((node) => { this.nodesMap.set(node.id, node); }); nodes.forEach((node) => { if (node.upstreamId) { node.upstream = this.nodesMap.get(node.upstreamId); } if (node.downstreamId) { node.downstream = this.nodesMap.get(node.downstreamId); } }); } makeJobs(jobs) { jobs.forEach((job) => { this.jobsMap.set(job.id, job); const node = this.nodesMap.get(job.nodeId); this.jobsMapByNodeKey[node.key] = job.result; }); } async prepare() { const { execution, transaction } = this; if (!execution.workflow) { execution.workflow = await execution.getWorkflow({ transaction }); } const nodes = await execution.workflow.getNodes({ transaction }); this.makeNodes(nodes); const jobs = await execution.getJobs({ order: [["id", "ASC"]], transaction }); this.makeJobs(jobs); } async start() { const { execution } = this; if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) { throw new Error(`execution was ended with status ${execution.status} before, could not be started again`); } await this.prepare(); if (this.nodes.length) { const head = this.nodes.find((item) => !item.upstream); await this.run(head, { result: execution.context }); } else { await this.exit(import_constants.JOB_STATUS.RESOLVED); } } async resume(job) { const { execution } = this; if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) { throw new Error(`execution was ended with status ${execution.status} before, could not be resumed`); } await this.prepare(); const node = this.nodesMap.get(job.nodeId); await this.recall(node, job); } async exec(instruction, node, prevJob) { const start = Date.now(); let job; try { this.logger.info(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id})`); this.logger.debug(`config of node`, { data: node.config }); job = await instruction(node, prevJob, this); if (!job) { return null; } } catch (err) { this.logger.error( `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) failed: `, { error: err } ); job = { result: err instanceof Error ? { message: err.message, stack: process.env.NODE_ENV === "production" ? 'Error stack will not be shown under "production" environment, please check logs.' : err.stack } : err, status: import_constants.JOB_STATUS.ERROR }; if (prevJob && prevJob.nodeId === node.id) { prevJob.set(job); job = prevJob; } } if (!(job instanceof import_server.Model)) { job.upstreamId = prevJob instanceof import_server.Model ? prevJob.get("id") : null; job.nodeId = node.id; job.nodeKey = node.key; } job.cost = Date.now() - ((job == null ? void 0 : job.createdAt) ? new Date(job.createdAt).getTime() : start); const savedJob = await this.saveJob(job); this.logger.info( `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) finished as status: ${savedJob.status}` ); this.logger.debug(`result of node`, { data: savedJob.result }); if (savedJob.status === import_constants.JOB_STATUS.RESOLVED && node.downstream) { this.logger.debug(`run next node (${node.downstreamId})`); return this.run(node.downstream, savedJob); } return this.end(node, savedJob); } async run(node, input) { const { instructions } = this.options.plugin; const instruction = instructions.get(node.type); if (typeof instruction.run !== "function") { return Promise.reject(new Error("`run` should be implemented for customized execution of the node")); } return this.exec(instruction.run.bind(instruction), node, input); } // parent node should take over the control async end(node, job) { this.logger.debug(`branch ended at node (${node.id})`); const parentNode = this.findBranchParentNode(node); if (parentNode) { this.logger.debug(`not on main, recall to parent entry node (${node.id})})`); await this.recall(parentNode, job); return job; } return this.exit(job.status); } async recall(node, job) { const { instructions } = this.options.plugin; const instruction = instructions.get(node.type); if (typeof instruction.resume !== "function") { return Promise.reject( new Error(`"resume" method should be implemented for [${node.type}] instruction of node (#${node.id})`) ); } return this.exec(instruction.resume.bind(instruction), node, job); } async exit(s) { if (typeof s === "number") { const status = this.constructor.StatusMap[s] ?? Math.sign(s); await this.execution.update({ status }, { transaction: this.transaction }); } this.logger.info(`execution (${this.execution.id}) exiting with status ${this.execution.status}`); return null; } // TODO(optimize) async saveJob(payload) { const { database } = this.execution.constructor; const { transaction } = this; const { model } = database.getCollection("jobs"); let job; if (payload instanceof model) { job = await payload.save({ transaction }); } else if (payload.id) { job = await model.findByPk(payload.id, { transaction }); await job.update(payload, { transaction }); } else { job = await model.create( { ...payload, executionId: this.execution.id }, { transaction } ); } this.jobsMap.set(job.id, job); this.lastSavedJob = job; this.jobsMapByNodeKey[job.nodeKey] = job.result; return job; } getBranches(node) { return this.nodes.filter((item) => item.upstream === node && item.branchIndex !== null).sort((a, b) => Number(a.branchIndex) - Number(b.branchIndex)); } // find the first node in current branch findBranchStartNode(node, parent) { for (let n = node; n; n = n.upstream) { if (!parent) { if (n.branchIndex !== null) { return n; } } else { if (n.upstream === parent) { return n; } } } return null; } // find the node start current branch findBranchParentNode(node) { for (let n = node; n; n = n.upstream) { if (n.branchIndex !== null) { return n.upstream; } } return null; } findBranchEndNode(node) { for (let n = node; n; n = n.downstream) { if (!n.downstream) { return n; } } return null; } findBranchParentJob(job, node) { for (let j = job; j; j = this.jobsMap.get(j.upstreamId)) { if (j.nodeId === node.id) { return j; } } return null; } findBranchLastJob(node, job) { const allJobs = Array.from(this.jobsMap.values()); const branchJobs = []; for (let n = this.findBranchEndNode(node); n && n !== node.upstream; n = n.upstream) { branchJobs.push(...allJobs.filter((item) => item.nodeId === n.id)); } branchJobs.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); for (let i = branchJobs.length - 1; i >= 0; i -= 1) { for (let j = branchJobs[i]; j && j.id !== job.id; j = this.jobsMap.get(j.upstreamId)) { if (j.upstreamId === job.id) { return branchJobs[i]; } } } return null; } getScope(sourceNodeId) { const node = this.nodesMap.get(sourceNodeId); const systemFns = {}; const scope = { execution: this.execution, node }; for (const [name, fn] of this.options.plugin.functions.getEntities()) { systemFns[name] = fn.bind(scope); } const $scopes = {}; for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) { const instruction = this.options.plugin.instructions.get(n.type); if (typeof instruction.getScope === "function") { $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobsMapByNodeKey[n.key], this); } } return { $currentForm: node.config, $context: this.execution.context, $jobsMapByNodeKey: this.jobsMapByNodeKey, $system: systemFns, $scopes, $env: this.options.plugin.app.environment.getVariables() }; } getParsedValue(value, sourceNodeId, additionalScope) { const template = (0, import_server.parse)(value); const scope = Object.assign(this.getScope(sourceNodeId), additionalScope); template.parameters.forEach(({ key }) => { (0, import_server.appendArrayColumn)(scope, key); }); return template(scope); } } Processor.StatusMap = { [import_constants.JOB_STATUS.PENDING]: import_constants.EXECUTION_STATUS.STARTED, [import_constants.JOB_STATUS.RESOLVED]: import_constants.EXECUTION_STATUS.RESOLVED, [import_constants.JOB_STATUS.FAILED]: import_constants.EXECUTION_STATUS.FAILED, [import_constants.JOB_STATUS.ERROR]: import_constants.EXECUTION_STATUS.ERROR, [import_constants.JOB_STATUS.ABORTED]: import_constants.EXECUTION_STATUS.ABORTED, [import_constants.JOB_STATUS.CANCELED]: import_constants.EXECUTION_STATUS.CANCELED, [import_constants.JOB_STATUS.REJECTED]: import_constants.EXECUTION_STATUS.REJECTED, [import_constants.JOB_STATUS.RETRY_NEEDED]: import_constants.EXECUTION_STATUS.RETRY_NEEDED };