UNPKG

@nocobase/flow-engine

Version:

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

176 lines (157 loc) 6.68 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. */ import type { FlowContext, PropertyMeta, PropertyMetaFactory } from '../flowContext'; import type { JSONValue } from './params-resolvers'; import type { Collection } from '../data-source'; import { createCollectionContextMeta } from './createCollectionContextMeta'; import { buildServerContextParams, type RecordRef, type ServerContextParams } from '../utils/serverContextParams'; import { extractUsedVariableNames as _extractUsedVariableNames, extractUsedVariablePaths as _extractUsedVariablePaths, } from '@nocobase/utils/client'; // Narrowest resource shape we rely on for inference type ResourceLike = { getResourceName?: () => string; getDataSourceKey?: () => string | undefined; getFilterByTk?: () => unknown; getSourceId?: () => unknown; }; function isObject(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null; } function getResource(ctx: FlowContext): ResourceLike | undefined { const r = (ctx as FlowContext)['resource']; return isObject(r) ? (r as ResourceLike) : undefined; } type CollectionShape = { name?: string; dataSourceKey?: string }; function getCollection(ctx: FlowContext): CollectionShape | undefined { const c = (ctx as FlowContext)['collection'] as unknown; if (isObject(c)) { const mc = c as Partial<CollectionShape>; return { name: mc.name, dataSourceKey: mc.dataSourceKey }; } return undefined; } export function inferRecordRef(ctx: FlowContext): RecordRef | undefined { const resource = getResource(ctx); const collectionObj = getCollection(ctx); const resourceName = collectionObj?.name || resource?.getResourceName?.(); const dataSourceKey = collectionObj?.dataSourceKey || resource?.getDataSourceKey?.(); const filterByTk = resource?.getFilterByTk?.(); if (!resourceName || typeof filterByTk === 'undefined' || filterByTk === null) return undefined; const parts = String(resourceName).split('.'); const collection = parts[parts.length - 1]; return { collection, dataSourceKey, filterByTk }; } export function inferParentRecordRef(ctx: FlowContext): RecordRef | undefined { const resource = getResource(ctx); const dataSourceKey = getCollection(ctx)?.dataSourceKey || resource?.getDataSourceKey?.(); const rn = resource?.getResourceName?.(); const sourceId = resource?.getSourceId?.(); if (!rn || typeof sourceId === 'undefined' || sourceId === null) return undefined; const parts = String(rn).split('.'); if (parts.length < 2) return undefined; return { collection: parts[0], dataSourceKey, filterByTk: sourceId }; } export type RecordParamsBuilder = (ctx: FlowContext) => RecordRef | Promise<RecordRef> | undefined; /** * Build a PropertyMeta for a record-like property with variablesParams included. */ export async function buildRecordMeta( collectionAccessor: () => Collection | null, title?: string, paramsBuilder?: RecordParamsBuilder, ): Promise<PropertyMeta | null> { const base = await createCollectionContextMeta(collectionAccessor, title)(); if (!base) return null; return { ...base, buildVariablesParams: (ctx: FlowContext) => (paramsBuilder ? paramsBuilder(ctx) : inferRecordRef(ctx)), } as PropertyMeta; } /** * Convenience helper to create a PropertyMetaFactory for a record-like variable. * It sets the factory.title so UI can display an immediate label before lazy resolution. */ export function createRecordMetaFactory( collectionAccessor: () => Collection | null, title: string, paramsBuilder?: RecordParamsBuilder, ): PropertyMetaFactory { const factory: PropertyMetaFactory = async () => buildRecordMeta(collectionAccessor, title, paramsBuilder); factory.title = title; return factory; } /** * Sugar for the most common case: a current record meta bound to FlowContext. * - Title: t('Current record') * - Params: inferRecordRef(ctx) */ export function createCurrentRecordMetaFactory( ctx: FlowContext, collectionAccessor: () => Collection | null, ): PropertyMetaFactory { const title = (ctx as any)?.t?.('Current record') || 'Current record'; return createRecordMetaFactory(collectionAccessor, title, (c) => inferRecordRef(c)); } /** * Extract top-level ctx variable names used inside a JSON template. * Supports dot and bracket notations, e.g. {{ ctx.record.id }}, {{ ctx['parentRecord'].name }}. */ export const extractUsedVariableNames = _extractUsedVariableNames; /** * Extract used top-level ctx variables with their subpaths. * Returns a map: varName -> string[] subPaths * Examples: * - {{ ctx.user.id }} => { user: ['id'] } * - {{ ctx['user'].roles[0].name }} => { user: ['roles[0].name'] } * - {{ ctx.view.record.id }} => { view: ['record.id'] } * - {{ ctx.twice(21) }} => { twice: [''] } // method call -> empty subPath */ export const extractUsedVariablePaths = _extractUsedVariablePaths; /** * 根据模板中用到的 ctx 变量,收集并构建服务端解析所需的 contextParams。 * - 通过 FlowContext 的 PropertyMeta.buildVariablesParams 收集 RecordRef * - 仅包含模板中实际使用到的顶层变量键,避免无谓膨胀 */ export async function collectContextParamsForTemplate( ctx: FlowContext, template: JSONValue, ): Promise<ServerContextParams | undefined> { try { const usage = _extractUsedVariablePaths(template) || {}; const varNames = Object.keys(usage); if (!varNames.length) return undefined; const input: Record<string, any> = {}; for (const key of varNames) { const opt = (ctx as any).getPropertyOptions?.(key); const meta: PropertyMeta | (() => Promise<PropertyMeta | null> | PropertyMeta | null) | undefined = opt?.meta; if (!meta) continue; let built: any; try { const resolvedMeta = typeof meta === 'function' ? await (meta as any)() : (meta as any); const builder = resolvedMeta?.buildVariablesParams; if (typeof builder === 'function') { built = await builder(ctx); } } catch (err) { // 忽略构建变量参数时的异常,继续处理其他变量 // 使用 void 引用以避免 no-unused-vars,同时避免空代码块触发 no-empty void err; continue; } if (built) { input[key] = built; } } return buildServerContextParams(ctx, input); } catch (_) { return undefined; } }