Follow-ups deferred from #410 (granular category PATCH endpoints). None block #410; grouped here because they share a root cause — category ordering/mutation is modeled as per-row absolute-integer writes across multiple auto-commit transactions, with no concurrency guard on the category node.
1. Optimistic-lock / conflict detection on CatalogueCategory
The category node has no lastUpdateTime, so granular PATCH/CRUD has no TOCTOU guard (unlike item PATCH from #406). Two concurrent edits can silently clobber.
- Add
lastUpdateTime to CatalogueCategory (+ backfill).
- Add the Cypher TOCTOU guard mirroring item PATCH, surfacing a 409 on stale writes.
2. Seed + patch atomicity (review finding #1)
In PatchCatalogueCategoryGroup / ...Property / ...PhysicalProperty, the lazy-order seed and the subsequent patch run as two separate auto-commit transactions (see the NOTE: comment in catalogue-category-patch-service.go). A failure between them leaves siblings renumbered but the requested value unapplied.
- Low risk today: seed fires only once per legacy (all-NULL) group, is idempotent, and the next reorder converges.
- Fix: wrap seed + patch in a single
WriteTransaction closure.
3. Legacy read queries sort by id(n), not order
The existing GET /category/:uid read keeps id(n) ordering. Once the FE actively relies on reorder, update the legacy read to sort by order for consistency.
Cross-repo note (FE)
The FE reorder scheme (computeReorderValue, sends target.order ± 1) can converge sibling order values after repeated moves, eventually producing duplicates that the backend can only tiebreak by id(n). If duplicate-order drift becomes visible, address gap management on the FE (feat/catalogue-redesign) — not by adding server-side sibling-shifting, which would conflict with the single-PATCH client model.
Refs: #410
Follow-ups deferred from #410 (granular category PATCH endpoints). None block #410; grouped here because they share a root cause — category ordering/mutation is modeled as per-row absolute-integer writes across multiple auto-commit transactions, with no concurrency guard on the category node.
1. Optimistic-lock / conflict detection on
CatalogueCategoryThe category node has no
lastUpdateTime, so granular PATCH/CRUD has no TOCTOU guard (unlike item PATCH from #406). Two concurrent edits can silently clobber.lastUpdateTimetoCatalogueCategory(+ backfill).2. Seed + patch atomicity (review finding #1)
In
PatchCatalogueCategoryGroup/...Property/...PhysicalProperty, the lazy-order seed and the subsequent patch run as two separate auto-commit transactions (see theNOTE:comment incatalogue-category-patch-service.go). A failure between them leaves siblings renumbered but the requested value unapplied.WriteTransactionclosure.3. Legacy read queries sort by
id(n), notorderThe existing
GET /category/:uidread keepsid(n)ordering. Once the FE actively relies on reorder, update the legacy read to sort byorderfor consistency.Cross-repo note (FE)
The FE reorder scheme (
computeReorderValue, sendstarget.order ± 1) can converge siblingordervalues after repeated moves, eventually producing duplicates that the backend can only tiebreak byid(n). If duplicate-order drift becomes visible, address gap management on the FE (feat/catalogue-redesign) — not by adding server-side sibling-shifting, which would conflict with the single-PATCH client model.Refs: #410