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