{"record":{"id":"05b6f6e2c28481cc","repo":"nocobase/nocobase","slug":"invalid-flowmodel-ancestor-chain","errorCode":null,"errorMessage":"Invalid FlowModel ancestor chain","messagePattern":"Invalid FlowModel ancestor chain","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugins/@nocobase/plugin-flow-engine/src/server/variables/allow-list.ts","lineNumber":278,"sourceCode":"    new Map<string, FlowModelChildCacheValue>();\n  state[cacheKey] = cache;\n  const childCacheKey = JSON.stringify([parentUid, subKey]);\n  if (cache.has(childCacheKey)) return (await cache.get(childCacheKey)) || null;\n\n  const load = getFlowModelRepository(ctx).findModelNodeSnapshotByParentId(parentUid, { subKey });\n  cache.set(childCacheKey, load);\n  const child = await load;\n  cache.set(childCacheKey, child);\n  return child;\n}\n\nasync function loadFlowModelAncestors(ctx: ResourcerContext, currentNode: FlowModelNodeSnapshot) {\n  const ancestors: FlowModelNodeSnapshot[] = [];\n  const seen = new Set([currentNode.uid]);\n  let parentId = currentNode.parentId;\n  while (parentId) {\n    if (seen.has(parentId) || ancestors.length >= MAX_FLOW_MODEL_ANCESTORS) {\n      throw new Error('Invalid FlowModel ancestor chain');\n    }\n    seen.add(parentId);\n    const parent = await getFlowModelNode(ctx, parentId);\n    if (!parent) throw new Error('Missing FlowModel ancestor');\n    ancestors.push(parent);\n    parentId = parent.parentId;\n  }\n  return Object.freeze(ancestors);\n}\n\nasync function loadFlowModelAncestorUids(ctx: ResourcerContext, currentNode: FlowModelNodeSnapshot) {\n  try {\n    return new Set((await loadFlowModelAncestors(ctx, currentNode)).map((ancestor) => ancestor.uid));\n  } catch {\n    return null;\n  }\n}\n","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/nocobase/nocobase/blob/fa42722fefe44265490dff2c27d79e2882bce4fa/packages/plugins/@nocobase/plugin-flow-engine/src/server/variables/allow-list.ts#L260-L296","documentation":"loadFlowModelAncestors walks up the FlowModel node tree from a starting node, collecting ancestors by repeatedly fetching parentId. It throws 'Invalid FlowModel ancestor chain' when the chain is cyclic (a parentId already seen) or when the chain exceeds MAX_FLOW_MODEL_ANCESTORS. This guards the server against infinite loops / runaway recursion caused by corrupted node graphs.","triggerScenarios":"A FlowModel node's parentId points back to an ancestor already visited (cycle, e.g. node A.parent=B, B.parent=A), or the ancestor chain is longer than MAX_FLOW_MODEL_ANCESTORS. Called via loadFlowModelAncestorUids or createRecordSlotCompilerOptions when building variable allow-lists for a record slot.","commonSituations":"Corrupted flow_models rows after a partial import/export or manual DB edit; data migration bugs that re-parent nodes without breaking old links; duplicate node UIDs restored from a backup; a bug in flow design client writing a self-referencing parentId.","solutions":["Inspect the flow model rows for the offending node: SELECT uid, parent_id FROM flow_models and trace parent links to find the cycle or self-reference.","Break the cycle by updating the offending row's parentId to null (root) or the correct parent.","Restore affected flow model records from a clean backup if the graph is extensively corrupted.","If chains are legitimately deep, check/raise MAX_FLOW_MODEL_ANCESTORS in allow-list.ts, but only after confirming no cycle exists.","Fix the client/import path that produced the bad re-parenting to prevent recurrence."],"exampleFix":"// before (bad data)\n// node-1.parentId = 'node-2'; node-2.parentId = 'node-1'\n// after\n// UPDATE flow_models SET \"parentId\" = NULL WHERE uid = 'node-2';","handlingStrategy":"validation","validationCode":"// before building the allow-list, detect cycles client-side\nfunction hasCycle(node, getNodeId, getParentId, max = 32) {\n  const seen = new Set();\n  let p = getParentId(node);\n  while (p) {\n    if (seen.has(p) || seen.size >= max) return true;\n    seen.add(p);\n    p = getParentId({ parentId: p });\n  }\n  return false;\n}","typeGuard":"const isFiniteChain = (chain: {uid: string; parentId: string | null}[]): boolean => {\n  const seen = new Set<string>();\n  for (const n of chain) {\n    if (seen.has(n.uid)) return false;\n    seen.add(n.uid);\n  }\n  return chain.length <= 32;\n};","tryCatchPattern":"try {\n  const ancestors = await loadFlowModelAncestors(ctx, node);\n} catch (e) {\n  if (e.message === 'Invalid FlowModel ancestor chain') {\n    // flag the flow model as corrupted and surface an admin-facing message\n  }\n  throw e;\n}","preventionTips":["Never re-parent flow model nodes with raw SQL without checking for cycles.","Add a unique constraint/cleanup so deleting a node updates children's parentId.","Keep ancestor chains shallow; restructure deeply nested flows.","Validate parent links after importing/exporting flow definitions."],"tags":["flow-engine","circular-reference","data-integrity"],"backgroundTag":"circular-reference-detected","analyzedSha":"fa42722fefe44265490dff2c27d79e2882bce4fa","analyzedAt":"2026-09-01T00:54:31.202Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}