fix(catalog): never create a page that reuses a sibling's order number
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 41s
CI / tests-unit (push) Successful in 1m53s
CI / tests-integration (push) Successful in 2m19s
CI / tests-ui (push) Successful in 2m46s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m9s

The emulator complained "Sibling order 2 is used more than once" 74 times
in production, covering 19 pages across 12 parents. The cause was that
nearly every page-creation path passed orderNum 0, so each new page
collided with whatever sibling already sat at 0 or 1, and the Park
placeholder pages all shared the sentinel values 99 and 999.

The production rows themselves are repaired out of band (renumbered 1..N
per affected parent, ordered by order_num then id so the existing visual
order is preserved, plus one dangling catalog_items row whose
items_base no longer existed removed). This commit stops it recurring.

- hierarchy.ts: add nextFreeSiblingOrder(), which ignores -1 and 0 as
  the same root set, treats a missing orderNum as 0, and returns an order
  strictly above the highest sibling in use. An explicit order is still
  honoured whenever it is free, so callers that genuinely want a position
  keep it.
- page-commands.ts: createPageCommand resolves the real order through
  nextFreeSiblingOrder instead of writing the requested 0 straight through.

Note that furni-import.ts and upload-import.ts still take their order
from the furnidata catInfo.order, so two categories carrying the same
furnidata order can still collide. That is caught by the emulator audit
and repaired by fixEmulatorIssues(), but it is not prevented here.
This commit is contained in:
openhands committed 2026-09-29 15:34:44 +02:00
1 parent 9cc57cddfc
commit 4be7eaed59
3 files changed
+72 -6

No files matched your search

+6 -1
View File
@@ -9,6 +9,7 @@ import {
CatalogNotFound,
collectSubtree,
type HierarchyPage,
nextFreeSiblingOrder,
positiveId,
validateSiblingOrder,
} from "../domain/hierarchy";
@@ -102,9 +103,13 @@ export async function createPageCommand(
if (!data.caption) throw new CatalogInputError("Category name is required");
return db.transaction(async (tx) => {
const rows = await lockedPages(tx, kind);
assertParent(rows, 0, data.parentId ?? -1);
const parentId = data.parentId ?? -1;
assertParent(rows, 0, parentId);
const assignments = fieldsSql(kind, {
...data,
// Never trust a caller-supplied order number: the emulator rejects a
// catalog where two siblings share one.
orderNum: nextFreeSiblingOrder(rows, parentId, data.orderNum),
captionSave: data.captionSave ?? data.caption?.slice(0, 25),
});
const [result] = await tx.execute(