nocobase/nocobase · error · Error

flowModels:move source and target must be sibling nodes unde

Error message

flowModels:move source and target must be sibling nodes under the same parent/subKey

What it means

Thrown by FlowModelRepository.move in the same-parent fast path: both source and target share a parentUid and type (subKey), but the sibling sort rows do not actually contain both nodes — sourceRow is missing or targetIndex is -1. The in-memory sort index and the tree table are out of sync.

Source

Thrown at packages/plugins/@nocobase/plugin-flow-engine/src/server/repository.ts:2058

    const sourceInfo = await this.findSiblingInfo(sourceUid, transaction);
    const targetInfo = await this.findSiblingInfo(targetUid, transaction);
    if (!targetInfo) {
      throw new Error('flowModels:move target is not attached to a parent');
    }
    const targetInstance = await this.model.findByPk(targetUid, { transaction });
    const targetOptions = FlowModelRepository.optionsToJson(targetInstance?.get('options') || {});
    const targetSubType = targetOptions.subType === 'object' ? 'object' : 'array';
    if (targetInfo.parentUid === sourceUid || (await this.isAncestorOf(sourceUid, targetInfo.parentUid, transaction))) {
      throw new Error('flowModels:move cycle detected');
    }

    if (sourceInfo?.parentUid === targetInfo.parentUid && sourceInfo.type === targetInfo.type) {
      const siblingRows = await this.findSiblingSortRows(sourceInfo.parentUid, sourceInfo.type, transaction);
      const sourceRow = siblingRows.find((row) => row.uid === sourceUid);
      const targetIndex = siblingRows.findIndex((row) => row.uid === targetUid);
      if (!sourceRow || targetIndex === -1) {
        throw new Error('flowModels:move source and target must be sibling nodes under the same parent/subKey');
      }

      const rowsWithoutSource = siblingRows.filter((row) => row.uid !== sourceUid);
      const insertIndex = rowsWithoutSource.findIndex((row) => row.uid === targetUid);
      rowsWithoutSource.splice(position === 'after' ? insertIndex + 1 : insertIndex, 0, sourceRow);
      await this.writeSiblingSorts(
        sourceInfo.parentUid,
        rowsWithoutSource.map((row) => row.uid),
        transaction,
      );
      return await this.findModelById(sourceUid, { transaction });
    }

    await this.normalizeSiblingSorts(sourceInfo, transaction);
    await this.normalizeSiblingSorts(targetInfo, transaction);
    await this.updateModelParentOptions(sourceUid, targetInfo.parentUid, targetInfo.type, targetSubType, transaction);

    await this.insertSingleNode(

View on GitHub (pinned to fa42722fef)

Solutions

  1. Normalize sibling sorts first (e.g. call move once with sourceId === targetId, which triggers normalizeSiblingSorts and returns null), then retry the real move.
  2. Re-attach the affected children under the parent to rebuild both tree and sort rows.
  3. Add a repair routine that diffs findSiblingSortRows against findSiblingInfo and writes missing rows via writeSiblingSorts.

Example fix

// before
await repo.move({ sourceId, targetId, position: 'after' }); // throws when sort rows are stale
// after
try {
  await repo.move({ sourceId, targetId, position: 'after' });
} catch (e) {
  if (String(e.message).includes('must be sibling nodes')) {
    await repo.move({ sourceId, targetId: sourceId, position: 'after' }); // normalize sorts
    await repo.move({ sourceId, targetId, position: 'after' });
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const rows = await repo.findSiblingSortRows(parentUid, subKey);
if (!rows.some((r) => r.uid === sourceId) || !rows.some((r) => r.uid === targetId)) {
  await repo.move({ sourceId, targetId: sourceId, position: 'after' }); // triggers normalizeSiblingSorts
}
await repo.move({ sourceId, targetId, position });

Try / catch

try {
  await repo.move(opts);
} catch (e) {
  if (String(e.message).includes('must be sibling nodes under the same parent/subKey')) {
    await repo.move({ sourceId: opts.sourceId, targetId: opts.sourceId, position: 'after' });
    await repo.move(opts);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling move between two nodes whose parentUid/type match per findSiblingInfo, but whose sort rows were never written (corrupt sort table), were written with different uids (case/whitespace mismatch), or were deleted concurrently.

Common situations: Manual DB edits or imports that populate the tree table but skip the sibling sort rows; a crash between writing tree paths and sort rows outside one transaction; duplicate uid entries with differing trailing whitespace.

Related errors


AI-assisted analysis of nocobase/nocobase@fa42722fef (2026-09-01). Data as JSON: /api/errors/3a1fd4a630552923. Report an issue: GitHub.