fix: incorrect resolving of the parent popup’s record variable (#7637)

This commit is contained in:
gchust
2025-10-22 22:13:45 +08:00
committed by GitHub
parent ed6bbe657d
commit cee0e7a1bb
2 changed files with 172 additions and 57 deletions
@@ -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<RecordRef | undefined> => {
const getParentRecordRef = async (level: number, flowCtx?: FlowContext): Promise<RecordRef | undefined> => {
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<string, any> = { 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<string, any> = 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<string, any> = {};
@@ -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<PopupNode> {
export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
const nav = view?.navigation;
const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
const buildNode = async (idx: number): Promise<PopupNode | undefined> => {
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;
}
},
});
}
@@ -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<string, string[]>();
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) 一层 recordvarName.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) 深层 recordvarName.<a>.<b>[.<c>...] 是记录(如 popup.parent.record / popup.parent.parent.record
type RecordParams = { collection: string; filterByTk: unknown; dataSourceKey?: string };
const deepRecordMap = new Map<string, RecordParams>(); // relativePath -> recordParams
const cp = contextParams;
if (cp && typeof cp === 'object') {
const cpRec = cp as Record<string, unknown>;
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<string>();
// 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<ServerBaseContext, Map<string, ServerBaseContext>>();
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,
});