From cee0e7a1bbccd9be74db531a7d3cdf06f564964f Mon Sep 17 00:00:00 2001 From: gchust Date: Wed, 22 Oct 2025 22:13:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20incorrect=20resolving=20of=20the=20paren?= =?UTF-8?q?t=20popup=E2=80=99s=20record=20variable=20(#7637)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flow-engine/src/views/createViewMeta.ts | 94 ++++++++---- .../src/server/variables/registry.ts | 135 ++++++++++++++---- 2 files changed, 172 insertions(+), 57 deletions(-) diff --git a/packages/core/flow-engine/src/views/createViewMeta.ts b/packages/core/flow-engine/src/views/createViewMeta.ts index 396e31f6cef..558a2fc96cf 100644 --- a/packages/core/flow-engine/src/views/createViewMeta.ts +++ b/packages/core/flow-engine/src/views/createViewMeta.ts @@ -13,6 +13,8 @@ import type { RecordRef } from '../utils/serverContextParams'; import type { Collection } from '../data-source'; import type { FlowView } from './FlowView'; +type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined; + // 判断是否为普通对象(Plain Object),避免对类实例/代理等进行深度遍历 function isPlainObject(val: any) { if (val === null || typeof val !== 'object') return false; @@ -140,7 +142,8 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { const getCurrentCollection = (): Collection | null => { try { const ref = inferViewRecordRef(ctx); - if (!ref?.filterByTk) return null; + // 避免 0 等 falsy 值被误判为无效主键 + if (typeof ref?.filterByTk === 'undefined' || ref.filterByTk === null) return null; const ds = ctx.dataSourceManager?.getDataSource?.(ref.dataSourceKey || 'main'); return ds?.collectionManager?.getCollection?.(ref.collection) || null; } catch (_) { @@ -149,9 +152,10 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { }; // 从视图堆栈推断 level 级父弹窗(level=1 上一层) - const getParentRecordRef = async (level: number): Promise => { + const getParentRecordRef = async (level: number, flowCtx?: FlowContext): Promise => { try { - const nav = ctx.view?.navigation; + const useCtx = flowCtx || ctx; + const nav = useCtx.view?.navigation; const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : []; if (stack.length < 2 || level < 1) return undefined; const idx = stack.length - 1 - level; @@ -159,12 +163,12 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { const parent = stack[idx]; if (!parent?.viewUid) return undefined; - let model: any = ctx.engine?.getModel?.(parent.viewUid); - if (!model && typeof ctx.engine?.loadModel === 'function') { + let model = useCtx.engine?.getModel?.(parent.viewUid) as PopupModelLike; + if (!model) { try { - model = await ctx.engine.loadModel({ uid: parent.viewUid }); + model = (await useCtx.engine.loadModel({ uid: parent.viewUid })) as PopupModelLike; } catch (e) { - console.warn('[FlowEngine] popup.getParentRecordRef loadModel failed:', e); + (useCtx.logger || ctx.logger)?.warn?.({ err: e }, '[FlowEngine] popup.getParentRecordRef loadModel failed'); } } const params = model?.getStepParams?.('popupSettings', 'openView') || {}; @@ -174,7 +178,7 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { if (!collection || typeof filterByTk === 'undefined' || filterByTk === null) return undefined; return { collection, dataSourceKey, filterByTk }; } catch (e) { - console.warn('[FlowEngine] popup.getParentRecordRef failed:', e); + (flowCtx?.logger || ctx.logger)?.warn?.({ err: e }, '[FlowEngine] popup.getParentRecordRef failed'); return undefined; } }; @@ -251,10 +255,37 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { const meta: PropertyMeta = { type: 'object', title: t('Current popup'), - buildVariablesParams: (c) => { + buildVariablesParams: async (c) => { const ref = inferViewRecordRef(c); - const inputArgs = (c?.view as any)?.inputArgs || {}; - const out: Record = { record: ref }; + const inputArgs = c.view?.inputArgs; + type PopupVariableParams = { + record?: RecordRef; + sourceRecord?: RecordRef; + parent?: PopupVariableParams; + }; + const params: PopupVariableParams = {}; + if (ref) params.record = ref; + + // 构建 parent 链(用于服务端解析 ctx.popup.parent[.parent...].record.*) + try { + const nav = c.view?.navigation; + const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : []; + if (stack.length >= 2) { + let cur: Record = params; + let level = 1; + let parentRef = await getParentRecordRef(level, c); + while (parentRef) { + if (!cur.parent) cur.parent = {}; + cur.parent.record = parentRef; + cur = cur.parent; + level += 1; + parentRef = await getParentRecordRef(level, c); + } + } + } catch (err) { + c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: build parent-chain failed'); + } + try { const srcId = inputArgs?.sourceId; const assoc: string | undefined = inputArgs?.associationName; @@ -263,17 +294,17 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { // associationName 形如 `posts.comments`,父级集合为 `posts` const parentCollectionName = String(assoc).split('.')[0]; if (parentCollectionName) { - out.sourceRecord = { + params.sourceRecord = { collection: parentCollectionName, dataSourceKey: dsKey, filterByTk: srcId, }; } } - } catch (_) { - // 忽略异常,保持 record 正常返回 + } catch (err) { + c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: infer sourceRecord failed'); } - return out; + return params; }, properties: async () => { const props: Record = {}; @@ -285,20 +316,20 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { if (base) props.record = base; // 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断) try { - const inputArgs = (ctx.view as any)?.inputArgs || {}; + const inputArgs = ctx.view?.inputArgs; const srcId = inputArgs?.sourceId; let assoc: string | undefined = inputArgs?.associationName; let dsKey: string = inputArgs?.dataSourceKey || 'main'; // 兜底:若 associationName 缺失或不含“.”,尝试从当前视图模型的 openView 参数推断 if (!assoc || typeof assoc !== 'string' || !assoc.includes('.')) { - const nav = (ctx.view as any)?.navigation; + const nav = ctx.view?.navigation; const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : []; const last = stack?.[stack.length - 1]; if (last?.viewUid) { - let model: any = ctx?.engine?.getModel?.(last.viewUid); + let model = ctx?.engine?.getModel?.(last.viewUid) as PopupModelLike; if (!model) { - model = await ctx.engine.loadModel({ uid: last.viewUid }); + model = (await ctx.engine.loadModel({ uid: last.viewUid })) as PopupModelLike; } const p = model?.getStepParams?.('popupSettings', 'openView') || {}; assoc = p?.associationName || assoc; @@ -327,8 +358,8 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory { } } } - } catch (_) { - // ignore + } catch (err) { + ctx.logger?.debug?.({ err }, '[FlowEngine] popup.properties: build sourceRecord failed'); } const resourceMeta: PropertyMeta = { type: 'object', @@ -373,15 +404,15 @@ interface PopupNode { parent?: PopupNode; } -export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise { +export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise { const nav = view?.navigation; const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : []; const buildNode = async (idx: number): Promise => { if (idx < 0 || !stack[idx]?.viewUid) return undefined; const viewUid = stack[idx].viewUid; - let model: any = ctx.engine?.getModel?.(viewUid); - if (!model && typeof ctx.engine?.loadModel === 'function') { - model = await ctx.engine?.loadModel({ uid: viewUid }); + let model = ctx.engine?.getModel?.(viewUid) as PopupModelLike; + if (!model) { + model = (await ctx.engine?.loadModel({ uid: viewUid })) as PopupModelLike; } const p = model?.getStepParams?.('popupSettings', 'openView') || {}; const collectionName = p?.collectionName; @@ -407,13 +438,22 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi * 在视图上下文中注册 popup 变量(统一消除重复) */ export function registerPopupVariable(ctx: FlowContext, view: FlowView) { + // - 顶层 record / sourceRecord 及其子字段 + // - 任意层级 parent.parent... 下的 record / sourceRecord 及其子字段 + const POPUP_SERVER_PATH_RE = + /^(?:record|sourceRecord)(?:\.|$)|^parent(?:\.parent)*(?:\.(?:record|sourceRecord))(?:\.|$)/; // 始终注册 popup 变量: // - 若当前视图无可推断记录,仅在元信息中不呈现 record 字段; // - 但仍可依据 navigation 推断并展示上级弹窗信息。 ctx.defineProperty('popup', { get: async () => buildPopupRuntime(ctx, view), meta: createPopupMeta(ctx), - resolveOnServer: (p: string) => - p === 'record' || p?.startsWith('record.') || p === 'sourceRecord' || p?.startsWith('sourceRecord.'), + resolveOnServer: (p: string) => { + try { + return !!p && POPUP_SERVER_PATH_RE.test(p); + } catch (_) { + return false; + } + }, }); } diff --git a/packages/plugins/@nocobase/plugin-flow-engine/src/server/variables/registry.ts b/packages/plugins/@nocobase/plugin-flow-engine/src/server/variables/registry.ts index e9881338e69..d7c3d01930f 100644 --- a/packages/plugins/@nocobase/plugin-flow-engine/src/server/variables/registry.ts +++ b/packages/plugins/@nocobase/plugin-flow-engine/src/server/variables/registry.ts @@ -259,19 +259,20 @@ async function fetchRecordWithRequestCache( const json = rec ? rec.toJSON() : undefined; if (cache) cache.set(key, json); return json; - } catch (e: any) { + } catch (e: unknown) { const log = koaCtx.app?.logger?.child({ module: 'plugin-flow-engine', submodule: 'variables.resolve', method: 'fetchRecordWithRequestCache', }); - log?.debug('[variables.resolve] fetchRecordWithRequestCache error', { + const errMsg = e instanceof Error ? e.message : String(e); + log?.warn('[variables.resolve] fetchRecordWithRequestCache error', { ds: dataSourceKey, collection, tk: filterByTk, fields, appends, - error: e?.message || String(e), + error: errMsg, }); return undefined; } @@ -321,7 +322,6 @@ function attachGenericRecordVariables( continue; // If top-level is record, nested processing under same varName is unnecessary } - // Nested record-like under varName // Group paths by first segment(支持首段后直接跟数字索引,如 record[0].name) const segmentMap = new Map(); const splitHead = (path: string): { seg: string; remainder: string } => { @@ -337,7 +337,6 @@ function attachGenericRecordVariables( const seg = m[1]; const idxPart = m[2] || ''; const tail = m[3] || ''; - // 若紧跟 [n],则把 [n] 保留到 remainder 中,seg 仅为标识符本体 const remainder = (idxPart ? `${idxPart}${tail ? `.${tail}` : ''}` : tail) || ''; return { seg, remainder }; } @@ -354,9 +353,9 @@ function attachGenericRecordVariables( segmentMap.set(seg, arr); } - // Build a container sub-context lazily only if any child is record-like + // 1) 一层 record:varName.seg 是记录 const segEntries = Array.from(segmentMap.entries()); - const recordChildren = segEntries.filter(([seg]) => { + const oneLevelRecordChildren = segEntries.filter(([seg]) => { const idx = parseIndexSegment(seg); const nestedObj = _.get(contextParams, [varName, seg]) ?? (idx ? _.get(contextParams, [varName, idx]) : undefined); @@ -364,31 +363,40 @@ function attachGenericRecordVariables( (contextParams || {})[`${varName}.${seg}`] ?? (idx ? (contextParams || {})[`${varName}.${idx}`] : undefined); return isRecordParams(nestedObj) || isRecordParams(dotted); }); - if (!recordChildren.length) continue; + + // 2) 深层 record:varName..[....] 是记录(如 popup.parent.record / popup.parent.parent.record) + type RecordParams = { collection: string; filterByTk: unknown; dataSourceKey?: string }; + const deepRecordMap = new Map(); // relativePath -> recordParams + const cp = contextParams; + if (cp && typeof cp === 'object') { + const cpRec = cp as Record; + for (const key of Object.keys(cpRec)) { + if (!key || (key !== varName && !key.startsWith(`${varName}.`))) continue; + if (key === varName) continue; + const val = cpRec[key]; + if (!isRecordParams(val)) continue; + const relative = key.slice(varName.length + 1); // e.g. 'parent.record' + if (!relative) continue; + deepRecordMap.set(relative, val); + } + } + + if (!oneLevelRecordChildren.length && deepRecordMap.size === 0) continue; flowCtx.defineProperty(varName, { get: () => { - const subContext = new ServerBaseContext(); - for (const [seg, remainders] of recordChildren) { - const idx = parseIndexSegment(seg); - const recordParams = - _.get(contextParams, [varName, seg]) ?? - (idx ? _.get(contextParams, [varName, idx]) : undefined) ?? - (contextParams || {})[`${varName}.${seg}`] ?? - (idx ? (contextParams || {})[`${varName}.${idx}`] : undefined); - // 优先使用本段的子路径;若解析不到任何子路径,则尝试从原始 usedPaths 回退计算一次(去掉首段 `${seg}.`) - let effRemainders = remainders.filter((r) => !!r); - if (!effRemainders.length) { - const all = usedPaths - .map((p) => - p.startsWith(`${seg}.`) ? p.slice(seg.length + 1) : p.startsWith(`${seg}[`) ? p.slice(seg.length) : '', - ) - .filter((x) => !!x); - if (all.length) effRemainders = all; - } - const { generatedAppends, generatedFields } = inferSelectsFromUsage(effRemainders, recordParams); - const definitionKey = idx ?? seg; // define numeric index for [0] so lodash.get '[0]' resolves to '0' - subContext.defineProperty(definitionKey, { + const root = new ServerBaseContext(); + const definedFirstLevel = new Set(); + + // Helper: define a record getter at container with given key + const defineRecordGetter = ( + container: ServerBaseContext, + key: string, + recordParams: { collection: string; filterByTk: unknown; dataSourceKey?: string }, + subPaths: string[] = [], + ) => { + const { generatedAppends, generatedFields } = inferSelectsFromUsage(subPaths, recordParams); + container.defineProperty(key, { get: async () => { const dataSourceKey = recordParams?.dataSourceKey || 'main'; return await fetchRecordWithRequestCache( @@ -402,8 +410,75 @@ function attachGenericRecordVariables( }, cache: true, }); + }; + + // Helper: get or create sub container under ctx with given key + const subContainers = new Map>(); + const ensureSubContainer = (parent: ServerBaseContext, key: string): ServerBaseContext => { + let map = subContainers.get(parent); + if (!map) { + map = new Map(); + subContainers.set(parent, map); + } + let child = map.get(key); + if (!child) { + const inst = new ServerBaseContext(); + parent.defineProperty(key, { get: () => inst.createProxy(), cache: true }); + map.set(key, inst); + child = inst; + } + return child; + }; + + // First: handle one-level record children (varName.seg) + for (const [seg, remainders] of oneLevelRecordChildren) { + const idx = parseIndexSegment(seg); + const recordParams = + _.get(contextParams, [varName, seg]) ?? + (idx ? _.get(contextParams, [varName, idx]) : undefined) ?? + (contextParams || {})[`${varName}.${seg}`] ?? + (idx ? (contextParams || {})[`${varName}.${idx}`] : undefined); + + let effRemainders = (remainders || []).filter((r) => !!r); + if (!effRemainders.length) { + const all = usedPaths + .map((p) => + p.startsWith(`${seg}.`) ? p.slice(seg.length + 1) : p.startsWith(`${seg}[`) ? p.slice(seg.length) : '', + ) + .filter((x) => !!x); + if (all.length) effRemainders = all; + } + + defineRecordGetter(root, idx ?? seg, recordParams, effRemainders); + definedFirstLevel.add(idx ?? seg); } - return subContext.createProxy(); + + // Then: handle deep record children (varName.a.b[.c...]) + for (const [relative, recordParams] of deepRecordMap.entries()) { + const segs = String(relative).split('.').filter(Boolean); + if (segs.length === 0) continue; + const first = segs[0]; + // Ensure first-level container exists, but avoid overriding previously defined first-level record getters + let container: ServerBaseContext; + if (definedFirstLevel.has(first)) { + // 已定义为 record getter 的一层 key,无法作为容器复用;跳过(由上层 one-level 逻辑覆盖)。 + continue; + } else { + container = root; + for (let i = 0; i < segs.length - 1; i++) { + container = ensureSubContainer(container, segs[i]); + } + } + + const leaf = segs[segs.length - 1]; + // 计算该记录下的使用子路径(相对 relative) + const subPaths = (usedPaths || []) + .map((p) => (p === relative ? '' : p.startsWith(relative + '.') ? p.slice(relative.length + 1) : '')) + .filter((x) => x !== ''); + defineRecordGetter(container, leaf, recordParams, subPaths); + } + + return root.createProxy(); }, cache: true, });