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;