{"record":{"id":"eb3e16438819668c","repo":"nocobase/nocobase","slug":"flowmodels-attach-position-target-is-not-a-sibling","errorCode":null,"errorMessage":"flowModels:attach position target is not a sibling under the same parent/subKey","messagePattern":"flowModels:attach position target is not a sibling under the same parent/subKey","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugins/@nocobase/plugin-flow-engine/src/server/repository.ts","lineNumber":1980,"sourceCode":"                            ON NodeInfo.descendant = TreeTable.descendant\n                                AND NodeInfo.depth = 0\n         WHERE TreeTable.depth = 1\n           AND TreeTable.ancestor = :ancestor\n           AND TreeTable.descendant = :descendant\n           AND NodeInfo.type = :type\n         LIMIT 1`,\n        {\n          type: 'SELECT',\n          replacements: {\n            ancestor: parentId,\n            descendant: target,\n            type: subKey,\n          },\n          transaction,\n        },\n      );\n      if (!ok?.length) {\n        throw new Error('flowModels:attach position target is not a sibling under the same parent/subKey');\n      }\n    }\n\n    // 清理旧路径缓存（旧祖先）\n    await this.clearXUidPathCache(modelUid, transaction);\n\n    // 更新 root options：确保 nodesToModel 能按 subKey/subType 正确挂载\n    await modelInstance.update(\n      {\n        options: {\n          ...lodash.omit(modelInstance.get('options') as any, ['uid']),\n          parentId,\n          parent: parentId,\n          subKey,\n          subType,\n        },\n      },\n      { transaction, hooks: false },","sourceCodeStart":1962,"sourceCodeEnd":1998,"githubUrl":"https://github.com/nocobase/nocobase/blob/fa42722fefe44265490dff2c27d79e2882bce4fa/packages/plugins/@nocobase/plugin-flow-engine/src/server/repository.ts#L1962-L1998","documentation":"Thrown by FlowModelRepository.attach when the before/after anchor target does not exist as a depth-1 child of the given parentId with the same subKey (type). The anchor must be an existing sibling in the exact same parent/subKey bucket; the SQL existence check returned no rows.","triggerScenarios":"Calling flowModels.attach with position { type:'before'|'after', target } where the target uid is: not yet attached, attached under a different parent, attached under the same parent but a different subKey, or simply misspelled/deleted.","commonSituations":"Anchoring against a node that was just deleted in another tab; anchoring against a cousin node (same grandparent, different parent); race between fetching sibling list and attaching; uid typo after copy-paste.","solutions":["Re-fetch the current children of parentId for the given subKey and use one of their uids as target.","Verify the target is attached under the same parentId AND same subKey before anchoring.","Fall back to position 'last' when no valid sibling anchor is available."],"exampleFix":"// before\nawait repo.attach({ uid, parentId, subKey, subType: 'array', position: { type: 'after', target: staleUid } });\n// after\nconst siblings = await repo.findChildren(parentId, subKey);\nconst position = siblings.some((s) => s.uid === targetUid)\n  ? { type: 'after', target: targetUid }\n  : 'last';\nawait repo.attach({ uid, parentId, subKey, subType: 'array', position });","handlingStrategy":"validation","validationCode":"const siblings = await repo.findChildren(parentId, subKey);\nconst anchorOk = typeof position === 'object' && siblings.some((s) => s.uid === position.target);\nawait repo.attach({ uid, parentId, subKey, subType, position: anchorOk ? position : 'last' });","typeGuard":"const isExistingSibling = (siblings: { uid: string }[], target: string) =>\n  siblings.some((s) => s.uid === String(target).trim());","tryCatchPattern":"try {\n  await repo.attach(opts);\n} catch (e) {\n  if (String(e.message).includes('not a sibling under the same parent/subKey')) {\n    await refreshSiblings();\n    await repo.attach({ ...opts, position: 'last' });\n  } else throw e;\n}","preventionTips":["Re-fetch the sibling list right before attaching; never cache anchors across requests","Verify the anchor shares both parentId and subKey, not just ancestry","Handle concurrent deletions by falling back to 'last'"],"tags":["flow-engine","tree","stale-reference","validation"],"backgroundTag":"anchor-node-not-found","analyzedSha":"fa42722fefe44265490dff2c27d79e2882bce4fa","analyzedAt":"2026-09-01T00:54:31.202Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}