@nocobase/flow-engine
Version:
A standalone flow engine for NocoBase, managing workflows, models, and actions.
263 lines (261 loc) • 11.1 kB
JavaScript
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
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 __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 FlowExecutor_exports = {};
__export(FlowExecutor_exports, {
FlowExecutor: () => FlowExecutor
});
module.exports = __toCommonJS(FlowExecutor_exports);
var import_lodash = __toESM(require("lodash"));
var import_flowContext = require("../flowContext");
var import_flowEngine = require("../flowEngine");
var import_utils = require("../utils");
var import_exceptions = require("../utils/exceptions");
var import_setupRuntimeContextSteps = require("../utils/setupRuntimeContextSteps");
const _FlowExecutor = class _FlowExecutor {
constructor(engine) {
this.engine = engine;
}
/**
* Execute a single flow on model.
*/
async runFlow(model, flowKey, inputArgs, runId) {
var _a;
const flow = model.getFlow(flowKey);
if (!flow) {
model.context.logger.error(`BaseModel.applyFlow: Flow with key '${flowKey}' not found.`);
return Promise.reject(new Error(`Flow '${flowKey}' not found.`));
}
const flowContext = new import_flowContext.FlowRuntimeContext(model, flowKey);
flowContext.defineProperty("reactView", {
value: model.reactView
});
flowContext.defineProperty("inputArgs", {
value: {
...inputArgs
}
});
flowContext.defineProperty("runId", {
value: runId || `run-${Date.now()}`
});
let lastResult;
const stepResults = flowContext.stepResults;
let eventStep = model.getEvent(typeof flow.on === "string" ? flow.on : (_a = flow.on) == null ? void 0 : _a.eventName);
if (eventStep) {
eventStep = { ...eventStep };
eventStep.defaultParams = { ...import_lodash.default.get(flow, "on.defaultParams", {}), ...eventStep.defaultParams };
}
const stepDefs = eventStep ? { eventStep, ...flow.steps } : flow.steps;
(0, import_setupRuntimeContextSteps.setupRuntimeContextSteps)(flowContext, stepDefs, model, flowKey);
const stepsRuntime = flowContext.steps;
for (const stepKey in stepDefs) {
if (!Object.prototype.hasOwnProperty.call(stepDefs, stepKey)) continue;
const step = stepDefs[stepKey];
let handler;
let combinedParams = {};
let actionDefinition;
let useRawParams = step.useRawParams;
if (step.use) {
actionDefinition = model.getAction(step.use);
if (!actionDefinition) {
flowContext.logger.error(
`BaseModel.applyFlow: Action '${step.use}' not found for step '${stepKey}' in flow '${flowKey}'. Skipping.`
);
continue;
}
handler = step.handler || actionDefinition.handler;
useRawParams = useRawParams ?? actionDefinition.useRawParams;
const actionDefaultParams = await (0, import_utils.resolveDefaultParams)(actionDefinition.defaultParams, flowContext);
const stepDefaultParams = await (0, import_utils.resolveDefaultParams)(step.defaultParams, flowContext);
combinedParams = { ...actionDefaultParams, ...stepDefaultParams };
} else if (step.handler) {
handler = step.handler;
const stepDefaultParams = await (0, import_utils.resolveDefaultParams)(step.defaultParams, flowContext);
combinedParams = { ...stepDefaultParams };
} else {
flowContext.logger.error(
`BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`
);
continue;
}
const modelStepParams = model.getStepParams(flowKey, stepKey);
if (modelStepParams !== void 0) {
combinedParams = { ...combinedParams, ...modelStepParams };
}
if (typeof useRawParams === "function") {
useRawParams = await useRawParams(flowContext);
}
if (!useRawParams) {
combinedParams = await flowContext.resolveJsonTemplate(combinedParams);
}
try {
if (!handler) {
flowContext.logger.error(
`BaseModel.applyFlow: No handler available for step '${stepKey}' in flow '${flowKey}'. Skipping.`
);
continue;
}
const currentStepResult = handler(flowContext, combinedParams);
const isAwait = step.isAwait !== false;
lastResult = isAwait ? await currentStepResult : currentStepResult;
stepResults[stepKey] = lastResult;
stepsRuntime[stepKey].result = stepResults[stepKey];
} catch (error) {
if (error instanceof import_utils.FlowExitException) {
flowContext.logger.info(`[FlowEngine] ${error.message}`);
return Promise.resolve(stepResults);
}
if (error instanceof import_exceptions.FlowExitAllException) {
flowContext.logger.info(`[FlowEngine] ${error.message}`);
return Promise.resolve(error);
}
flowContext.logger.error(
{ err: error },
`BaseModel.applyFlow: Error executing step '${stepKey}' in flow '${flowKey}':`
);
return Promise.reject(error);
}
}
return Promise.resolve(stepResults);
}
/**
* Execute all auto-apply flows for model.
*/
async runAutoFlows(model, inputArgs, useCache = true) {
const autoApplyFlows = model.getAutoFlows();
if (autoApplyFlows.length === 0) {
model.context.logger.warn(`FlowModel: No auto-apply flows found for model '${model.uid}'`);
return [];
}
const cacheKey = useCache ? import_flowEngine.FlowEngine.generateApplyFlowCacheKey(model.getAutoFlowCacheScope(), "all", model.uid) : null;
if (cacheKey && this.engine) {
const cachedEntry = this.engine.applyFlowCache.get(cacheKey);
if (cachedEntry) {
if (cachedEntry.status === "resolved") {
model.context.logger.debug(`[FlowEngine.applyAutoFlows] Using cached result for model: ${model.uid}`);
return cachedEntry.data;
}
if (cachedEntry.status === "rejected") throw cachedEntry.error;
if (cachedEntry.status === "pending") return await cachedEntry.promise;
}
}
const executeAutoFlows = /* @__PURE__ */ __name(async () => {
const results = [];
const runId = `${model.uid}-autoFlow-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
const logger = model.context.logger.child({ module: "flow-engine", component: "FlowExecutor", runId });
logger.debug(
`[FlowExecutor] runAutoFlows: uid=${model.uid}, isFork=${(model == null ? void 0 : model.isFork) === true}, useCache=${useCache}, runId=${runId}, flows=${autoApplyFlows.map((f) => f.key).join(",")}`
);
try {
if (autoApplyFlows.length === 0) {
logger.warn(`FlowModel: No auto-apply flows found for model '${model.uid}'`);
} else {
for (const flow of autoApplyFlows) {
try {
logger.debug(`[FlowExecutor] runFlow: uid=${model.uid}, flowKey=${flow.key}`);
const result = await this.runFlow(model, flow.key, inputArgs, runId);
if (result instanceof import_exceptions.FlowExitAllException) {
logger.debug(`[FlowEngine.applyAutoFlows] ${result.message}`);
break;
}
results.push(result);
} catch (error) {
logger.error({ err: error }, `FlowModel.applyAutoFlows: Error executing auto-apply flow '${flow.key}':`);
throw error;
}
}
}
return results;
} catch (error) {
if (error instanceof import_utils.FlowExitException) {
logger.debug(`[FlowEngine.applyAutoFlows] ${error.message}`);
return results;
}
throw error;
}
}, "executeAutoFlows");
if (!cacheKey || !this.engine) {
return await executeAutoFlows();
}
const promise = executeAutoFlows().then((result) => {
this.engine.applyFlowCache.set(cacheKey, {
status: "resolved",
data: result,
promise: Promise.resolve(result)
});
return result;
}).catch((err) => {
this.engine.applyFlowCache.set(cacheKey, {
status: "rejected",
error: err,
promise: Promise.reject(err)
});
throw err;
});
this.engine.applyFlowCache.set(cacheKey, { status: "pending", promise });
return await promise;
}
/**
* Dispatch an event to flows bound via flow.on and execute them.
*/
async dispatchEvent(model, eventName, inputArgs) {
const flows = Array.from(model.getFlows().values()).filter((flow) => {
const on = flow.on;
if (!on) return false;
if (typeof on === "string") return on === eventName;
if (typeof on === "object") return on.eventName === eventName;
return false;
});
const runId = `${model.uid}-${eventName}-${Date.now()}`;
const logger = model.context.logger;
const promises = flows.map((flow) => {
logger.debug(`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`);
return this.runFlow(model, flow.key, inputArgs, runId).catch((error) => {
logger.error(
{ err: error },
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`
);
});
});
await Promise.all(promises);
}
};
__name(_FlowExecutor, "FlowExecutor");
let FlowExecutor = _FlowExecutor;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FlowExecutor
});