UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

263 lines (261 loc) 11.1 kB
/** * 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 });