tachybase_todo/packages/plugins/workflow/src/server/instructions/condition.ts

177 lines
4.1 KiB
TypeScript

import { evaluators } from '@nocobase/evaluators';
import { Registry } from '@nocobase/utils';
import { Instruction } from '.';
import { Processor } from '..';
import { JOB_STATUS } from '../constants';
import type { FlowNodeModel, JobModel } from '../types';
type Comparer = (a: any, b: any) => boolean;
export const calculators = new Registry<Comparer>();
// built-in functions
function equal(a, b) {
return a == b;
}
function notEqual(a, b) {
return a != b;
}
function gt(a, b) {
return a > b;
}
function gte(a, b) {
return a >= b;
}
function lt(a, b) {
return a < b;
}
function lte(a, b) {
return a <= b;
}
calculators.register('equal', equal);
calculators.register('notEqual', notEqual);
calculators.register('gt', gt);
calculators.register('gte', gte);
calculators.register('lt', lt);
calculators.register('lte', lte);
calculators.register('==', equal);
calculators.register('!=', notEqual);
calculators.register('>', gt);
calculators.register('>=', gte);
calculators.register('<', lt);
calculators.register('<=', lte);
function includes(a, b) {
return a.includes(b);
}
function notIncludes(a, b) {
return !a.includes(b);
}
function startsWith(a: string, b: string) {
return a.startsWith(b);
}
function notStartsWith(a: string, b: string) {
return !a.startsWith(b);
}
function endsWith(a: string, b: string) {
return a.endsWith(b);
}
function notEndsWith(a: string, b: string) {
return !a.endsWith(b);
}
calculators.register('includes', includes);
calculators.register('notIncludes', notIncludes);
calculators.register('startsWith', startsWith);
calculators.register('notStartsWith', notStartsWith);
calculators.register('endsWith', endsWith);
calculators.register('notEndsWith', notEndsWith);
type CalculationItem = {
calculator?: string;
operands?: [any?, any?];
};
type CalculationGroup = {
group: {
type: 'and' | 'or';
calculations?: Calculation[];
};
};
type Calculation = CalculationItem | CalculationGroup;
function calculate(calculation: CalculationItem = {}) {
type NewType = Comparer;
let fn: NewType;
if (!(calculation.calculator && (fn = calculators.get(calculation.calculator)))) {
throw new Error(`no calculator function registered for "${calculation.calculator}"`);
}
return Boolean(fn(...(calculation.operands ?? [])));
}
function logicCalculate(calculation?: Calculation) {
if (!calculation) {
return true;
}
if (typeof calculation['group'] === 'object') {
const method = calculation['group'].type === 'and' ? 'every' : 'some';
return (calculation['group'].calculations ?? [])[method]((item: Calculation) => logicCalculate(item));
}
return calculate(calculation as CalculationItem);
}
export default {
async run(node: FlowNodeModel, prevJob, processor: Processor) {
const { engine, calculation, expression, rejectOnFalse } = node.config || {};
const evaluator = evaluators.get(engine);
let result = true;
try {
result = evaluator
? evaluator(expression, processor.getScope(node.id))
: logicCalculate(processor.getParsedValue(calculation, node.id));
} catch (e) {
return {
result: e.toString(),
status: JOB_STATUS.ERROR,
};
}
if (!result && rejectOnFalse) {
return {
status: JOB_STATUS.FAILED,
result,
};
}
const job = {
status: JOB_STATUS.RESOLVED,
result,
// TODO(optimize): try unify the building of job
nodeId: node.id,
upstreamId: (prevJob && prevJob.id) || null,
};
const branchNode = processor.nodes.find((item) => item.upstream === node && Boolean(item.branchIndex) === result);
if (!branchNode) {
return job;
}
const savedJob = await processor.saveJob(job);
await processor.run(branchNode, savedJob);
return null;
},
async resume(node: FlowNodeModel, branchJob: JobModel, processor: Processor) {
const job = processor.findBranchParentJob(branchJob, node) as JobModel;
if (branchJob.status === JOB_STATUS.RESOLVED) {
// return to continue node.downstream
return job;
}
// pass control to upper scope by ending current scope
return processor.exit(branchJob.status);
},
} as Instruction;