@nocobase/flow-engine
Version:
A standalone flow engine for NocoBase, managing workflows, models, and actions.
310 lines (308 loc) • 11.6 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 params_resolvers_exports = {};
__export(params_resolvers_exports, {
enqueueVariablesResolve: () => enqueueVariablesResolve,
preprocessExpression: () => preprocessExpression,
resolveCreateModelOptions: () => resolveCreateModelOptions,
resolveDefaultParams: () => resolveDefaultParams,
resolveExpressions: () => resolveExpressions
});
module.exports = __toCommonJS(params_resolvers_exports);
var import_lodash = __toESM(require("lodash"));
var import_client = require("@nocobase/utils/client");
async function resolveDefaultParams(defaultParams, ctx) {
if (!defaultParams) {
return {};
}
if (typeof defaultParams === "function") {
try {
const result = await defaultParams(ctx);
return result || {};
} catch (error) {
console.error("Error resolving defaultParams function:", error);
return {};
}
}
return defaultParams;
}
__name(resolveDefaultParams, "resolveDefaultParams");
async function resolveCreateModelOptions(createModelOptions, ctx, extra) {
if (!createModelOptions) {
return {};
}
if (typeof createModelOptions === "function") {
try {
const result = await createModelOptions(ctx, extra);
return result || {};
} catch (error) {
console.error("Error resolving createModelOptions function:", error);
return {};
}
}
return createModelOptions;
}
__name(resolveCreateModelOptions, "resolveCreateModelOptions");
const VAR_AGGREGATOR = { queue: [], timer: null, inflightByRun: /* @__PURE__ */ new Map() };
const BATCH_FLUSH_DELAY_MS = 10;
function sortKeysDeep(input) {
if (Array.isArray(input)) return input.map(sortKeysDeep);
if (import_lodash.default.isPlainObject(input)) {
const entries = Object.entries(input).map(([k, v]) => [k, sortKeysDeep(v)]);
const sortedEntries = import_lodash.default.sortBy(entries, ([k]) => k);
return Object.fromEntries(sortedEntries);
}
return input;
}
__name(sortKeysDeep, "sortKeysDeep");
function stableStringifyOrdered(obj) {
try {
return JSON.stringify(sortKeysDeep(obj));
} catch {
try {
return JSON.stringify(obj);
} catch {
return String(obj);
}
}
}
__name(stableStringifyOrdered, "stableStringifyOrdered");
function enqueueVariablesResolve(ctx, payload) {
const agg = VAR_AGGREGATOR;
const runId = ctx.runId || "GLOBAL";
const dedupKey = stableStringifyOrdered(payload);
let runMap = agg.inflightByRun.get(runId);
if (!runMap) {
runMap = /* @__PURE__ */ new Map();
agg.inflightByRun.set(runId, runMap);
}
const existing = runMap.get(dedupKey);
if (existing) return existing;
let resolveFn;
let rejectFn;
const p = new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
});
runMap.set(dedupKey, p);
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const item = { id, ctx, payload, resolve: resolveFn, reject: rejectFn, runId, dedupKey };
agg.queue.push(item);
const flush = /* @__PURE__ */ __name(async () => {
var _a, _b, _c, _d, _e, _f;
const items = agg.queue.splice(0, agg.queue.length);
agg.timer = null;
if (!items.length) return;
const api = (_a = items[0].ctx) == null ? void 0 : _a.api;
if (!api) {
for (const it of items) {
try {
it.resolve(it.payload.template);
} catch (e) {
it.reject(e);
} finally {
(_b = agg.inflightByRun.get(it.runId)) == null ? void 0 : _b.delete(it.dedupKey);
}
}
return;
}
try {
const batch = items.map((it) => ({
id: it.id,
template: it.payload.template,
contextParams: it.payload.contextParams || {}
}));
const res = await api.request({ method: "POST", url: "variables:resolve", data: { values: { batch } } });
const top = res.data ?? res;
const root = top.data ?? top;
const arr1 = root == null ? void 0 : root.results;
const resultsArr = Array.isArray(arr1) ? arr1 : Array.isArray(root) ? root : [];
const map = /* @__PURE__ */ new Map();
for (const r of resultsArr) {
const k = (r == null ? void 0 : r.id) != null ? String(r.id) : "";
if (k) map.set(k, r == null ? void 0 : r.data);
}
for (const it of items) {
try {
const k = String(it.id);
const resolved = map.has(k) ? map.get(k) : it.payload.template;
it.resolve(resolved);
} catch (e) {
it.reject(e);
} finally {
(_c = agg.inflightByRun.get(it.runId)) == null ? void 0 : _c.delete(it.dedupKey);
}
}
} catch (e) {
for (const it of items) {
try {
(_e = (_d = ctx == null ? void 0 : ctx.logger) == null ? void 0 : _d.warn) == null ? void 0 : _e.call(_d, { err: e }, "variables:resolve(batch) failed, fallback");
it.resolve(it.payload.template);
} catch (err) {
it.reject(err);
} finally {
(_f = agg.inflightByRun.get(it.runId)) == null ? void 0 : _f.delete(it.dedupKey);
}
}
}
}, "flush");
if (!agg.timer) {
agg.timer = setTimeout(flush, BATCH_FLUSH_DELAY_MS);
}
return p;
}
__name(enqueueVariablesResolve, "enqueueVariablesResolve");
async function resolveExpressions(params, ctx) {
const compile = /* @__PURE__ */ __name(async (source) => {
if (typeof source === "string" && /\{\{.*?\}\}/.test(source)) {
return await compileExpression(source, ctx);
}
if (Array.isArray(source)) {
return Promise.all(source.map(compile));
}
if (source && typeof source === "object") {
const result = {};
for (const [key, value] of Object.entries(source)) {
result[key] = await compile(value);
}
return result;
}
return source;
}, "compile");
return compile(params);
}
__name(resolveExpressions, "resolveExpressions");
async function processExpression(expression, ctx) {
const processedExpr = await preprocessExpression(expression.trim(), ctx);
const result = await ctx.runjs(`return ${processedExpr}`, { t: ctx.t });
return (result == null ? void 0 : result.success) ? result.value : void 0;
}
__name(processExpression, "processExpression");
async function preprocessExpression(expression, ctx) {
const pathRegex = /ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*(?:(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\[[^\]]+\]))*)(?!\s*\()/g;
const pathMatches = [];
const firstLevelKeys = /* @__PURE__ */ new Set();
let match;
while ((match = pathRegex.exec(expression)) !== null) {
const fullPath = match[0];
const pathAfterCtx = match[1];
const firstKeyMatch = pathAfterCtx.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (firstKeyMatch) {
const firstKey = firstKeyMatch[1];
const restPath = pathAfterCtx.substring(firstKey.length);
pathMatches.push({
fullPath,
firstKey,
restPath
// 单层路径时为空字符串,多层路径可能包含数组索引
});
firstLevelKeys.add(firstKey);
}
}
if (pathMatches.length === 0) {
return expression;
}
const recordProxyMap = /* @__PURE__ */ new Map();
for (const firstKey of firstLevelKeys) {
try {
const rawValue = await ctx[firstKey];
const isRecordProxy = rawValue && rawValue.__isRecordProxy__ === true;
recordProxyMap.set(firstKey, isRecordProxy);
} catch (error) {
recordProxyMap.set(firstKey, false);
}
}
pathMatches.sort((a, b) => b.fullPath.length - a.fullPath.length);
let processedExpr = expression;
for (const { fullPath, firstKey, restPath } of pathMatches) {
const isRecordProxy = recordProxyMap.get(firstKey) || false;
let processedPath;
if (restPath === "") {
processedPath = `await ctx.${firstKey}`;
} else {
processedPath = isRecordProxy ? `await (await ctx.${firstKey})${restPath}` : `(await ctx.${firstKey})${restPath}`;
}
const escapedFullPath = fullPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`\\b${escapedFullPath}\\b`, "g");
processedExpr = processedExpr.replace(regex, processedPath);
}
return processedExpr;
}
__name(preprocessExpression, "preprocessExpression");
async function compileExpression(expression, ctx) {
const matchDotOnly = /* @__PURE__ */ __name((expr) => {
const m = expr.trim().match(/^ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)$/);
return m ? m[1] : null;
}, "matchDotOnly");
const resolveDotOnlyPath = /* @__PURE__ */ __name(async (dotPath) => {
const segs = dotPath.split(".");
const first = segs.shift();
if (!first) return void 0;
const base = await ctx[first];
if (segs.length === 0) return base;
return (0, import_client.getValuesByPath)(base, segs.join("."));
}, "resolveDotOnlyPath");
const singleMatch = expression.match(/^\{\{\s*(.+)\s*\}\}$/);
if (singleMatch) {
const inner = singleMatch[1];
const dotPath = matchDotOnly(inner);
if (dotPath) {
return await resolveDotOnlyPath(dotPath);
}
return await processExpression(inner, ctx);
}
const matches = [...expression.matchAll(/\{\{\s*(.+?)\s*\}\}/g)];
let result = expression;
for (const [fullMatch, innerExpr] of matches) {
const dotPath = matchDotOnly(innerExpr);
const value = dotPath ? await resolveDotOnlyPath(dotPath) : await processExpression(innerExpr, ctx);
if (value !== void 0) {
const replacement = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
result = result.replace(fullMatch, replacement);
}
}
return result;
}
__name(compileExpression, "compileExpression");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
enqueueVariablesResolve,
preprocessExpression,
resolveCreateModelOptions,
resolveDefaultParams,
resolveExpressions
});