@kernhq/module-inventory 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +7 -5
  2. package/dist/contract/models.d.ts +24 -1
  3. package/dist/contract/models.d.ts.map +1 -1
  4. package/dist/contract/models.js +35 -3
  5. package/dist/contract/models.js.map +1 -1
  6. package/dist/contract/router.d.ts +58 -2
  7. package/dist/contract/router.d.ts.map +1 -1
  8. package/dist/contract/router.js +29 -1
  9. package/dist/contract/router.js.map +1 -1
  10. package/dist/server/router.d.ts +57 -2
  11. package/dist/server/router.d.ts.map +1 -1
  12. package/dist/server/router.js +23 -1
  13. package/dist/server/router.js.map +1 -1
  14. package/dist/server/schema.d.ts.map +1 -1
  15. package/dist/server/schema.js +11 -0
  16. package/dist/server/schema.js.map +1 -1
  17. package/dist/server/services/categories.d.ts +110 -10
  18. package/dist/server/services/categories.d.ts.map +1 -1
  19. package/dist/server/services/categories.js +198 -13
  20. package/dist/server/services/categories.js.map +1 -1
  21. package/migrations/0008_category_order_unique.sql +71 -0
  22. package/migrations/meta/_journal.json +7 -0
  23. package/package.json +2 -1
  24. package/src/client/errors.test.ts +30 -0
  25. package/src/client/errors.ts +31 -3
  26. package/src/client/messages.ts +82 -19
  27. package/src/client/mock.test.ts +71 -1
  28. package/src/client/mock.ts +51 -12
  29. package/src/client/module.ts +19 -1
  30. package/src/client/reorder.test.ts +100 -0
  31. package/src/client/reorder.ts +79 -0
  32. package/src/client/sequence.test.ts +248 -0
  33. package/src/client/sequence.ts +185 -0
  34. package/src/client/settings/CategoriesSettings.svelte +430 -105
  35. package/src/contract/models.ts +36 -3
  36. package/src/contract/router.ts +29 -0
  37. package/src/module.test.ts +23 -0
  38. package/src/server/inventory.int.test.ts +545 -10
  39. package/src/server/migrations.test.ts +140 -2
  40. package/src/server/router.ts +25 -1
  41. package/src/server/schema.ts +11 -0
  42. package/src/server/services/categories.ts +221 -20
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EACL,MAAM,EACN,OAAO,EACP,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,KAAK,EACL,OAAO,EACP,KAAK,EACL,UAAU,EACV,IAAI,EACJ,SAAS,EACT,WAAW,EACX,IAAI,GACL,MAAM,qBAAqB,CAAA;AAE5B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,CAAA;AAE/C,oEAAoE;AACpE,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,UAAU,CAAC,CAAA;AAC/D,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE,CAAA;AAC/C,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;AAClF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,CAAA;AAC7D,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,CAAA;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE;IACnD,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,UAAU,EAAE;IAC9C,SAAS,EAAE,OAAO,EAAE;CACrB,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAClC,UAAU;AACV,yFAAyF;AACzF;IACE,WAAW,EAAE,EAAE,EAAE;IACjB,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;IAC1B,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE;CAClC;AACD;;;;;;;GAOG;AACH,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CACzD,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CACpC,YAAY,EACZ;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C;;;;;;;;;;;OAWG;IACH,UAAU,EAAE,EAAE,CAAC,aAAa,CAAC;IAC7B,SAAS,EAAE,OAAO,EAAE;IACpB,SAAS,EAAE,OAAO,EAAE;CACrB,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,6FAA6F;IAC7F,+EAA+E;IAC/E,WAAW,CAAC,iCAAiC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC;IACxE,KAAK,CAAC,6BAA6B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC;CAChE,CACF,CAAA;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAChC,QAAQ,EACR;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACtD,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B;;2CAEuC;IACvC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC;IACpD,kFAAkF;IAClF,eAAe,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAC1C,YAAY,EAAE,EAAE,CAAC,eAAe,CAAC;IACjC,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC;IACnC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC;IAC1B,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC;IACjC,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC;IACrC,UAAU,EAAE,OAAO,CAAC,aAAa,CAAC;IAClC,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACzC,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC;IACrC;;;;;;;;;;OAUG;IACH,kBAAkB,EAAE,EAAE,CAAC,sBAAsB,CAAC;IAC9C,WAAW,EAAE,IAAI,CAAC,eAAe,CAAC;IAClC,0EAA0E;IAC1E,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,KAAK,EAA2B,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9E,SAAS,EAAE,OAAO,EAAE;IACpB,SAAS,EAAE,OAAO,EAAE;IACpB,UAAU,EAAE,EAAE,CAAC,aAAa,CAAC;CAC9B,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,WAAW,CAAC,6BAA6B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC;IACpE,KAAK,CAAC,iCAAiC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC;IACvE,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;IACnE,KAAK,CAAC,kCAAkC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC;IACzE,0FAA0F;IAC1F,KAAK,CAAC,mCAAmC,CAAC;SACvC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,eAAe,CAAC;SACpC,KAAK,CAAC,GAAG,CAAA,+BAA+B,CAAC;IAC5C,4FAA4F;IAC5F,KAAK,CAAC,kCAAkC,CAAC;SACtC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,aAAa,CAAC;SAClC,KAAK,CAAC,GAAG,CAAA,4BAA4B,CAAC;CAC1C,CACF,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CACnC,YAAY,EACZ;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;IAC1B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;IAChC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,+DAA+D;IAC7F,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,EAAY;IAC3C,YAAY,EAAE,KAAK,CAAC,eAAe,CAAC;IACpC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACtD,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC1D,UAAU,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC5D,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,UAAU,EAAE,EAAE,CAAC,aAAa,CAAC;IAC7B,SAAS,EAAE,OAAO,EAAE;IACpB,SAAS,EAAE,OAAO,EAAE;CACrB,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,WAAW,CAAC,gCAAgC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC;IACtE,KAAK,CAAC,6BAA6B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC;CAChE,CACF,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CACxC,iBAAiB,EACjB;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACnC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACjC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;IAClB,aAAa,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;IAC1D,WAAW,EAAE,EAAE,CAAC,cAAc,CAAC;IAC/B,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC;IAC7B,SAAS,EAAE,OAAO,EAAE;CACrB,EACD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAC/F,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CACtC,eAAe,EACf;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACnC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC;IACzB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,mIAAmI;IACrK,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,EAAmD;IAClF,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;IACnB,UAAU,EAAE,OAAO,EAAE;IACrB;;;;;;;;;;;;;;;;;;OAkBG;IACH,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,4CAA4C,CAAC;CAC1G,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,KAAK,CAAC,mCAAmC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,UAAU,CAAC;IACrF;;;;;;;;;OASG;IACH,KAAK,CAAC,0CAA0C,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC;IACpF;;;;;OAKG;IACH,WAAW,CAAC,yCAAyC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC;CAC3F,CACF,CAAA;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CACjC,SAAS,EACT;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACnC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IAClC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IACtB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC;IAChC,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACzC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACjC,8EAA8E;IAC9E,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B;;;;;;;;;OASG;IACH,iBAAiB,EAAE,EAAE,CAAC,qBAAqB,CAAC;IAC5C,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC;IAC7B,SAAS,EAAE,OAAO,EAAE;IACpB,SAAS,EAAE,OAAO,EAAE;CACrB,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;IAC9E;;;;;;;OAOG;IACH,KAAK,CAAC,+BAA+B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAA,qBAAqB,CAAC;IAClG;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,+BAA+B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAA,qBAAqB,CAAC;IAC1F;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,uCAAuC,EAAE,GAAG,CAAA,+CAA+C,CAAC;CACnG,CACF,CAAA;AAED,mGAAmG;AACnG,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CACrC,aAAa,EACb;IACE,EAAE,EAAE,EAAE,EAAE;IACR,WAAW,EAAE,EAAE,EAAE;IACjB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACnC,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACjC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC;IACrB,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,SAAS,EAAE,OAAO,EAAE;CACrB,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,WAAW,CAAC,qCAAqC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;IAC1E,KAAK,CAAC,oCAAoC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC;CACtF,CACF,CAAA;AAED,mGAAmG;AACnG,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,6FAA6F;IAC7F,8FAA8F;IAC9F,8FAA8F;IAC9F,4FAA4F;IAC5F,UAAU;IACV,kGAAkG;IAClG,gGAAgG;IAChG,+EAA+E;IAC/E,YAAY;IACZ,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,iBAAiB;IACjB,eAAe;IACf,SAAS;IACT,aAAa;CACL,CAAA"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,13 @@
1
1
  import { type Tx } from '@kernhq/kernel';
2
- import type { Category as CategoryModel } from '../../contract/models.js';
2
+ import { type Category as CategoryModel } from '../../contract/models.js';
3
3
  import { categories } from '../schema.js';
4
4
  type Row = typeof categories.$inferSelect;
5
+ /** What `reorder` returns: the live sequence as it now stands, and which rows actually moved. */
6
+ export interface Reordered {
7
+ rows: Row[];
8
+ /** Only the ids whose `order` changed — a change event for a row that did not move is a lie. */
9
+ moved: string[];
10
+ }
5
11
  /** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
6
12
  export declare function toCategory(row: Row): CategoryModel;
7
13
  /**
@@ -13,24 +19,44 @@ export declare function toCategory(row: Row): CategoryModel;
13
19
  */
14
20
  export declare class CategoryService {
15
21
  /**
16
- * Ordered by `order` and then by name, which is what makes a workspace that never touches the
17
- * order field still get an alphabetical picker rather than an arbitrary one — every row has
18
- * `order` 0, so the tiebreak is doing all the work and has to be a name rather than an id.
22
+ * Ordered by `order` and then by name.
23
+ *
24
+ * `order` is the sequence somebody dragged their categories into, and no two **live** categories
25
+ * share a number: `inventory_categories_ws_order_live_uq` is what makes that true rather than
26
+ * intended. So for the live set the tiebreak never fires.
27
+ *
28
+ * It stays because `list` also reads the archived rows, and those are outside the index: an
29
+ * archived category keeps the number it had when it left, and the very next reorder renumbers a
30
+ * live row onto it. A duplicate must sort the same way twice, and a name is the only column a
31
+ * person could predict.
19
32
  */
20
33
  list(tx: Tx, workspaceId: string, includeArchived: boolean): Promise<CategoryModel[]>;
21
34
  get(tx: Tx, workspaceId: string, categoryId: string): Promise<Row>;
22
35
  /**
23
- * A duplicate name is a `CONFLICT` with the name in it, never a 500.
36
+ * A new category joins the **end** of the sequence, and a duplicate name is a `CONFLICT` with the
37
+ * name in it, never a 500.
24
38
  *
25
- * The unique index is what actually decides — checking first and inserting after is a race that
26
- * two people adding "Laptops" at once will find — so the check is the insert, and the driver's
27
- * 23505 is translated into a sentence rather than shown as "Failed query: insert into
39
+ * The unique index is what actually decides the name — checking first and inserting after is a
40
+ * race that two people adding "Laptops" at once will find — so the check is the insert, and the
41
+ * driver's 23505 is translated into a sentence rather than shown as "Failed query: insert into
28
42
  * mod_inventory.categories …".
43
+ *
44
+ * The position is decided the same way, in the statement rather than around it. It used to be an
45
+ * optional number the caller passed and defaulted to 0, so every category anybody added landed at
46
+ * the *front*, tied with whatever was already there, and the list resolved the tie by name — a
47
+ * new category appearing in the middle of a sequence somebody had arranged by hand.
48
+ *
49
+ * **The subquery is not what makes the number unique — the lock above it is.** This used to say
50
+ * that a subquery inside the insert stopped two creates in flight from taking the same maximum,
51
+ * and that is false: under READ COMMITTED each statement takes its own snapshot, so both read a
52
+ * list without the other's row in it and both appended to the same place. `lockAppends` is what
53
+ * serialises them, and `inventory_categories_ws_order_live_uq` is what refuses the pair if
54
+ * anything ever reaches the table around it.
29
55
  */
30
- create(tx: Tx, workspaceId: string, name: string, order: number): Promise<Row>;
56
+ create(tx: Tx, workspaceId: string, name: string): Promise<Row>;
57
+ /** A rename, and nothing else — the sequence is `reorder`'s to write. */
31
58
  update(tx: Tx, workspaceId: string, categoryId: string, patch: {
32
59
  name?: string;
33
- order?: number;
34
60
  }): Promise<Row>;
35
61
  /**
36
62
  * Archive and restore, which are one procedure because they are one column.
@@ -42,8 +68,82 @@ export declare class CategoryService {
42
68
  * entry saying "category changed to <nothing>". None of it recoverable, all of it caused by a
43
69
  * settings screen. An archived category disappears from every picker and every filter and leaves
44
70
  * each asset able to say what it is.
71
+ *
72
+ * **A restore appends**, for the reason `create` appends. The row kept the position it had when
73
+ * it left, and every live category has been renumbered since — so putting it back where its old
74
+ * number points lands it in the middle of somebody's arrangement, tied with whatever is there
75
+ * now. The end of the list is the one place a person can find it again. Archiving leaves the
76
+ * number alone: it is out of every list that reads it, and it is about to be overwritten anyway.
77
+ *
78
+ * A restore appends, so it races exactly as `create` does and is serialised the same way — and it
79
+ * is the one place other than `create` where the live set grows, so it is the other place the
80
+ * limit is enforced. Archiving needs neither: it takes a row out of the live set, and out of the
81
+ * partial index with it.
45
82
  */
46
83
  archive(tx: Tx, workspaceId: string, categoryId: string, archived: boolean): Promise<Row>;
84
+ /**
85
+ * The sequence, rewritten from the ids somebody put it in — the only thing that writes `order`.
86
+ *
87
+ * Three refusals before a single row is touched, and all three are the same idea: this call
88
+ * describes the whole live list, so a list that does not match the workspace is not a partial
89
+ * instruction to be completed, it is an ordering of something else.
90
+ *
91
+ * - **an id twice** — arithmetic that cannot be carried out, `BAD_REQUEST`;
92
+ * - **an id that is not this workspace's** — `NOT_FOUND`, the answer `update` and `archive`
93
+ * already give for one, and the answer that does not confirm the row exists elsewhere;
94
+ * - **a live category the list does not name, or an archived one it does** — somebody added,
95
+ * archived or restored a category while this page was open. Renumbering what was named would
96
+ * put the missing one wherever its stale number happened to land, silently. `CONFLICT` with
97
+ * `inventory.category.order_stale`, which the client turns into "reload and try again".
98
+ *
99
+ * All of it inside one transaction, opened by the router, so a refusal writes nothing and a
100
+ * renumbering is never half-applied. The `for update` is what makes two reorders arriving at once
101
+ * queue rather than interleave — without it both read the same list, both pass the check, and the
102
+ * writes of one land between the writes of the other, which is how a sequence ends up being
103
+ * neither of the two orders anybody asked for. Locking in id order is what stops two of them
104
+ * taking the same rows in opposite orders and deadlocking.
105
+ */
106
+ reorder(tx: Tx, workspaceId: string, categoryIds: string[]): Promise<Reordered>;
107
+ /**
108
+ * One past the highest position this workspace has used, archived rows counted.
109
+ *
110
+ * Archived rows count because one of them can be restored, and a restored category landing on a
111
+ * live one's number is the tie this whole change exists to remove.
112
+ *
113
+ * **Only correct under `lockAppends`.** A subquery inside the write saves a round trip and settles
114
+ * nothing about concurrency: under READ COMMITTED it is evaluated against the snapshot its own
115
+ * statement started with, so two transactions appending at the same instant read the same maximum
116
+ * and take the same number. That is the defect `0008` exists for.
117
+ */
118
+ private static appended;
119
+ /**
120
+ * Hold the right to append to this workspace's list until the transaction ends.
121
+ *
122
+ * An advisory lock rather than `select … for update`, because the thing being protected is the
123
+ * *next* number rather than any row that exists: a workspace with no categories at all has no row
124
+ * to lock, and two creates against it would still collide. Taken per workspace, so two workspaces
125
+ * adding a category at the same moment never wait for each other.
126
+ *
127
+ * The first key is a constant for this list, so another module taking an advisory lock on the same
128
+ * workspace does not queue behind this one by accident.
129
+ *
130
+ * It cannot deadlock against `reorder`, which takes row locks and never asks for this one — so
131
+ * there is no pair of waits pointing at each other.
132
+ */
133
+ private static lockAppends;
134
+ /**
135
+ * Refuse the one that would take a workspace past `MAX_LIVE_CATEGORIES`, and say so.
136
+ *
137
+ * The number exists because `categories.reorder` is handed every live category at once and that
138
+ * array needs a bound. Leaving the bound only on the array is a silent ceiling: a workspace could
139
+ * pass it one category at a time and then discover that the only procedure that can order them is
140
+ * the one it can no longer call. Enforced here, the array can always name every live category a
141
+ * workspace is allowed to have.
142
+ *
143
+ * **Live rows, not every row ever made**, so that archiving one frees a place — which is what the
144
+ * refusal tells the reader to do, and the advice has to be true.
145
+ */
146
+ private static roomForOneMore;
47
147
  /**
48
148
  * The unique index refused it, or something else did and must not be disguised.
49
149
  *
@@ -1 +1 @@
1
- {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../../src/server/services/categories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAGzC,KAAK,GAAG,GAAG,OAAO,UAAU,CAAC,YAAY,CAAA;AAKzC,oGAAoG;AACpG,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,aAAa,CAUlD;AAED;;;;;;GAMG;AACH,qBAAa,eAAe;IAC1B;;;;OAIG;IACG,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAWrF,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IASxE;;;;;;;OAOG;IACG,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAS9E,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACvC,OAAO,CAAC,GAAG,CAAC;IAqBf;;;;;;;;;;OAUG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;IAU/F;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CAOzB"}
1
+ {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../../src/server/services/categories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAE3D,OAAO,EAAE,KAAK,QAAQ,IAAI,aAAa,EAAuB,MAAM,0BAA0B,CAAA;AAC9F,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAGzC,KAAK,GAAG,GAAG,OAAO,UAAU,CAAC,YAAY,CAAA;AAKzC,iGAAiG;AACjG,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,GAAG,EAAE,CAAA;IACX,gGAAgG;IAChG,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,oGAAoG;AACpG,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,aAAa,CAUlD;AAED;;;;;;GAMG;AACH,qBAAa,eAAe;IAC1B;;;;;;;;;;;OAWG;IACG,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAWrF,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IASxE;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAcrE,yEAAyE;IACnE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC;IAoBrG;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;IAkB/F;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IA0ErF;;;;;;;;;;OAUG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;IAIvB;;;;;;;;;;;;;OAaG;mBACkB,WAAW;IAMhC;;;;;;;;;;;OAWG;mBACkB,cAAc;IAYnC;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CAOzB"}
@@ -1,5 +1,6 @@
1
1
  import { KernError, uuidv7 } from '@kernhq/kernel';
2
- import { and, asc, eq, isNull } from 'drizzle-orm';
2
+ import { and, asc, count, eq, isNull, sql } from 'drizzle-orm';
3
+ import { MAX_LIVE_CATEGORIES } from '../../contract/models.js';
3
4
  import { categories } from '../schema.js';
4
5
  import { violated } from './db-errors.js';
5
6
  /** The unique index `0000_init.sql` put on (workspace_id, name). */
@@ -25,9 +26,16 @@ export function toCategory(row) {
25
26
  */
26
27
  export class CategoryService {
27
28
  /**
28
- * Ordered by `order` and then by name, which is what makes a workspace that never touches the
29
- * order field still get an alphabetical picker rather than an arbitrary one — every row has
30
- * `order` 0, so the tiebreak is doing all the work and has to be a name rather than an id.
29
+ * Ordered by `order` and then by name.
30
+ *
31
+ * `order` is the sequence somebody dragged their categories into, and no two **live** categories
32
+ * share a number: `inventory_categories_ws_order_live_uq` is what makes that true rather than
33
+ * intended. So for the live set the tiebreak never fires.
34
+ *
35
+ * It stays because `list` also reads the archived rows, and those are outside the index: an
36
+ * archived category keeps the number it had when it left, and the very next reorder renumbers a
37
+ * live row onto it. A duplicate must sort the same way twice, and a name is the only column a
38
+ * person could predict.
31
39
  */
32
40
  async list(tx, workspaceId, includeArchived) {
33
41
  const filters = [eq(categories.workspaceId, workspaceId)];
@@ -50,29 +58,47 @@ export class CategoryService {
50
58
  return row;
51
59
  }
52
60
  /**
53
- * A duplicate name is a `CONFLICT` with the name in it, never a 500.
61
+ * A new category joins the **end** of the sequence, and a duplicate name is a `CONFLICT` with the
62
+ * name in it, never a 500.
54
63
  *
55
- * The unique index is what actually decides — checking first and inserting after is a race that
56
- * two people adding "Laptops" at once will find — so the check is the insert, and the driver's
57
- * 23505 is translated into a sentence rather than shown as "Failed query: insert into
64
+ * The unique index is what actually decides the name — checking first and inserting after is a
65
+ * race that two people adding "Laptops" at once will find — so the check is the insert, and the
66
+ * driver's 23505 is translated into a sentence rather than shown as "Failed query: insert into
58
67
  * mod_inventory.categories …".
68
+ *
69
+ * The position is decided the same way, in the statement rather than around it. It used to be an
70
+ * optional number the caller passed and defaulted to 0, so every category anybody added landed at
71
+ * the *front*, tied with whatever was already there, and the list resolved the tie by name — a
72
+ * new category appearing in the middle of a sequence somebody had arranged by hand.
73
+ *
74
+ * **The subquery is not what makes the number unique — the lock above it is.** This used to say
75
+ * that a subquery inside the insert stopped two creates in flight from taking the same maximum,
76
+ * and that is false: under READ COMMITTED each statement takes its own snapshot, so both read a
77
+ * list without the other's row in it and both appended to the same place. `lockAppends` is what
78
+ * serialises them, and `inventory_categories_ws_order_live_uq` is what refuses the pair if
79
+ * anything ever reaches the table around it.
59
80
  */
60
- async create(tx, workspaceId, name, order) {
81
+ async create(tx, workspaceId, name) {
82
+ await CategoryService.lockAppends(tx, workspaceId);
83
+ await CategoryService.roomForOneMore(tx, workspaceId);
61
84
  try {
62
- const [row] = await tx.insert(categories).values({ id: uuidv7(), workspaceId, name, order }).returning();
85
+ const [row] = await tx
86
+ .insert(categories)
87
+ .values({ id: uuidv7(), workspaceId, name, order: CategoryService.appended(workspaceId) })
88
+ .returning();
63
89
  return row;
64
90
  }
65
91
  catch (err) {
66
92
  throw CategoryService.nameTaken(err, name);
67
93
  }
68
94
  }
95
+ /** A rename, and nothing else — the sequence is `reorder`'s to write. */
69
96
  async update(tx, workspaceId, categoryId, patch) {
70
97
  const previous = await this.get(tx, workspaceId, categoryId);
71
- // `undefined` means "not mentioned". Neither field is nullable, so there is no "clear it" here
98
+ // `undefined` means "not mentioned". The column is not nullable, so there is no "clear it" here
72
99
  // and no reason for the `null`-versus-`undefined` care `assets.update` needs.
73
100
  const values = {
74
101
  name: patch.name ?? previous.name,
75
- order: patch.order ?? previous.order,
76
102
  updatedAt: new Date(),
77
103
  };
78
104
  try {
@@ -97,17 +123,176 @@ export class CategoryService {
97
123
  * entry saying "category changed to <nothing>". None of it recoverable, all of it caused by a
98
124
  * settings screen. An archived category disappears from every picker and every filter and leaves
99
125
  * each asset able to say what it is.
126
+ *
127
+ * **A restore appends**, for the reason `create` appends. The row kept the position it had when
128
+ * it left, and every live category has been renumbered since — so putting it back where its old
129
+ * number points lands it in the middle of somebody's arrangement, tied with whatever is there
130
+ * now. The end of the list is the one place a person can find it again. Archiving leaves the
131
+ * number alone: it is out of every list that reads it, and it is about to be overwritten anyway.
132
+ *
133
+ * A restore appends, so it races exactly as `create` does and is serialised the same way — and it
134
+ * is the one place other than `create` where the live set grows, so it is the other place the
135
+ * limit is enforced. Archiving needs neither: it takes a row out of the live set, and out of the
136
+ * partial index with it.
100
137
  */
101
138
  async archive(tx, workspaceId, categoryId, archived) {
139
+ if (!archived) {
140
+ await CategoryService.lockAppends(tx, workspaceId);
141
+ await CategoryService.roomForOneMore(tx, workspaceId);
142
+ }
102
143
  const [row] = await tx
103
144
  .update(categories)
104
- .set({ archivedAt: archived ? new Date() : null, updatedAt: new Date() })
145
+ .set({
146
+ archivedAt: archived ? new Date() : null,
147
+ ...(archived ? {} : { order: CategoryService.appended(workspaceId) }),
148
+ updatedAt: new Date(),
149
+ })
105
150
  .where(and(eq(categories.workspaceId, workspaceId), eq(categories.id, categoryId)))
106
151
  .returning();
107
152
  if (!row)
108
153
  throw KernError.notFound('Category');
109
154
  return row;
110
155
  }
156
+ /**
157
+ * The sequence, rewritten from the ids somebody put it in — the only thing that writes `order`.
158
+ *
159
+ * Three refusals before a single row is touched, and all three are the same idea: this call
160
+ * describes the whole live list, so a list that does not match the workspace is not a partial
161
+ * instruction to be completed, it is an ordering of something else.
162
+ *
163
+ * - **an id twice** — arithmetic that cannot be carried out, `BAD_REQUEST`;
164
+ * - **an id that is not this workspace's** — `NOT_FOUND`, the answer `update` and `archive`
165
+ * already give for one, and the answer that does not confirm the row exists elsewhere;
166
+ * - **a live category the list does not name, or an archived one it does** — somebody added,
167
+ * archived or restored a category while this page was open. Renumbering what was named would
168
+ * put the missing one wherever its stale number happened to land, silently. `CONFLICT` with
169
+ * `inventory.category.order_stale`, which the client turns into "reload and try again".
170
+ *
171
+ * All of it inside one transaction, opened by the router, so a refusal writes nothing and a
172
+ * renumbering is never half-applied. The `for update` is what makes two reorders arriving at once
173
+ * queue rather than interleave — without it both read the same list, both pass the check, and the
174
+ * writes of one land between the writes of the other, which is how a sequence ends up being
175
+ * neither of the two orders anybody asked for. Locking in id order is what stops two of them
176
+ * taking the same rows in opposite orders and deadlocking.
177
+ */
178
+ async reorder(tx, workspaceId, categoryIds) {
179
+ const named = new Set(categoryIds);
180
+ if (named.size !== categoryIds.length)
181
+ throw KernError.badRequest('That list of categories names the same one more than once.');
182
+ const current = await tx
183
+ .select()
184
+ .from(categories)
185
+ .where(eq(categories.workspaceId, workspaceId))
186
+ .orderBy(asc(categories.id))
187
+ .for('update');
188
+ const known = new Map(current.map((row) => [row.id, row]));
189
+ if (categoryIds.some((id) => !known.has(id)))
190
+ throw KernError.notFound('Category');
191
+ const live = current.filter((row) => !row.archivedAt);
192
+ if (live.some((row) => !named.has(row.id)) || categoryIds.some((id) => known.get(id)?.archivedAt))
193
+ throw KernError.conflict('The categories changed while this list was open, so this order was not saved. Reload the list and arrange it again.', 'inventory.category.order_stale');
194
+ // Only the rows that actually move are touched — a workspace has tens of categories, they are
195
+ // already locked, and a change event for a row whose position did not change would tell every
196
+ // screen in the workspace about a write that did not happen. `moved` is settled here, before a
197
+ // single write, so it stays the honest list whatever the two passes below do.
198
+ const now = new Date();
199
+ const going = categoryIds
200
+ .map((id, index) => ({ id, index }))
201
+ .filter(({ id, index }) => known.get(id)?.order !== index);
202
+ const moved = going.map(({ id }) => id);
203
+ /**
204
+ * Parked out of the way first, and only then put down where they belong.
205
+ *
206
+ * `inventory_categories_ws_order_live_uq` is a plain unique index, and Postgres checks one of
207
+ * those row by row rather than at the end of the statement. There is no deferrable form to reach
208
+ * for either: a unique *constraint* can be deferred and cannot be partial, and this one has to be
209
+ * partial. So the single-pass loop writes a collision the moment two rows swap — putting the
210
+ * first on 1 while the second still holds 1 — and a swap is the commonest reorder there is.
211
+ *
212
+ * `park` sits above both the highest number any row in this workspace holds **and** the last
213
+ * place in the new sequence. That is what makes the two passes safe: the parked values are
214
+ * distinct from one another and from every row staying put, and the `0…n-1` the second pass
215
+ * writes into is empty, because every live row that could have been sitting there is either
216
+ * parked or already on the number it is being given.
217
+ *
218
+ * Only the rows that actually move are written, so a reorder that shifts one row does not stamp
219
+ * `updated_at` across the whole list — and `updated_at` is left off the parking pass, which is
220
+ * bookkeeping rather than a change anybody made.
221
+ */
222
+ if (going.length > 0) {
223
+ const park = Math.max(...current.map((row) => row.order), categoryIds.length - 1) + 1;
224
+ for (const [offset, { id }] of going.entries()) {
225
+ await tx
226
+ .update(categories)
227
+ .set({ order: park + offset })
228
+ .where(and(eq(categories.workspaceId, workspaceId), eq(categories.id, id)));
229
+ }
230
+ for (const { id, index } of going) {
231
+ await tx
232
+ .update(categories)
233
+ .set({ order: index, updatedAt: now })
234
+ .where(and(eq(categories.workspaceId, workspaceId), eq(categories.id, id)));
235
+ }
236
+ }
237
+ const rows = await tx
238
+ .select()
239
+ .from(categories)
240
+ .where(and(eq(categories.workspaceId, workspaceId), isNull(categories.archivedAt)))
241
+ .orderBy(asc(categories.order), asc(categories.name));
242
+ return { rows, moved };
243
+ }
244
+ /**
245
+ * One past the highest position this workspace has used, archived rows counted.
246
+ *
247
+ * Archived rows count because one of them can be restored, and a restored category landing on a
248
+ * live one's number is the tie this whole change exists to remove.
249
+ *
250
+ * **Only correct under `lockAppends`.** A subquery inside the write saves a round trip and settles
251
+ * nothing about concurrency: under READ COMMITTED it is evaluated against the snapshot its own
252
+ * statement started with, so two transactions appending at the same instant read the same maximum
253
+ * and take the same number. That is the defect `0008` exists for.
254
+ */
255
+ static appended(workspaceId) {
256
+ return sql `(select coalesce(max(${categories.order}), -1) + 1 from ${categories} where ${categories.workspaceId} = ${workspaceId})`;
257
+ }
258
+ /**
259
+ * Hold the right to append to this workspace's list until the transaction ends.
260
+ *
261
+ * An advisory lock rather than `select … for update`, because the thing being protected is the
262
+ * *next* number rather than any row that exists: a workspace with no categories at all has no row
263
+ * to lock, and two creates against it would still collide. Taken per workspace, so two workspaces
264
+ * adding a category at the same moment never wait for each other.
265
+ *
266
+ * The first key is a constant for this list, so another module taking an advisory lock on the same
267
+ * workspace does not queue behind this one by accident.
268
+ *
269
+ * It cannot deadlock against `reorder`, which takes row locks and never asks for this one — so
270
+ * there is no pair of waits pointing at each other.
271
+ */
272
+ static async lockAppends(tx, workspaceId) {
273
+ await tx.execute(sql `select pg_advisory_xact_lock(hashtext('mod_inventory.categories.order'), hashtext(${workspaceId}))`);
274
+ }
275
+ /**
276
+ * Refuse the one that would take a workspace past `MAX_LIVE_CATEGORIES`, and say so.
277
+ *
278
+ * The number exists because `categories.reorder` is handed every live category at once and that
279
+ * array needs a bound. Leaving the bound only on the array is a silent ceiling: a workspace could
280
+ * pass it one category at a time and then discover that the only procedure that can order them is
281
+ * the one it can no longer call. Enforced here, the array can always name every live category a
282
+ * workspace is allowed to have.
283
+ *
284
+ * **Live rows, not every row ever made**, so that archiving one frees a place — which is what the
285
+ * refusal tells the reader to do, and the advice has to be true.
286
+ */
287
+ static async roomForOneMore(tx, workspaceId) {
288
+ const [row] = await tx
289
+ .select({ n: count() })
290
+ .from(categories)
291
+ .where(and(eq(categories.workspaceId, workspaceId), isNull(categories.archivedAt)));
292
+ if ((row?.n ?? 0) < MAX_LIVE_CATEGORIES)
293
+ return;
294
+ throw KernError.conflict(`This workspace already has ${MAX_LIVE_CATEGORIES} categories, which is as many as Inventory keeps in one order. Archive one it no longer uses to make room.`, 'inventory.category.limit_reached');
295
+ }
111
296
  /**
112
297
  * The unique index refused it, or something else did and must not be disguised.
113
298
  *
@@ -1 +1 @@
1
- {"version":3,"file":"categories.js","sourceRoot":"","sources":["../../../src/server/services/categories.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"categories.js","sourceRoot":"","sources":["../../../src/server/services/categories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAW,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAkC,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9F,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAIzC,oEAAoE;AACpE,MAAM,UAAU,GAAG,iCAAiC,CAAA;AASpD,oGAAoG;AACpG,MAAM,UAAU,UAAU,CAAC,GAAQ;IACjC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,WAAW,EAAE,GAAG,CAAC,WAA2C;QAC5D,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;QACtC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;QACtC,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,IAAI;KAClD,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,eAAe;IAC1B;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,CAAC,EAAM,EAAE,WAAmB,EAAE,eAAwB;QAC9D,MAAM,OAAO,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAA;QACzD,IAAI,CAAC,eAAe;YAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAA;QACjE,MAAM,IAAI,GAAG,MAAM,EAAE;aAClB,MAAM,EAAE;aACR,IAAI,CAAC,UAAU,CAAC;aAChB,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;aACtB,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;QACvD,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAC7B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAM,EAAE,WAAmB,EAAE,UAAkB;QACvD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;aACnB,MAAM,EAAE;aACR,IAAI,CAAC,UAAU,CAAC;aAChB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;QACrF,IAAI,CAAC,GAAG;YAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;QAC9C,OAAO,GAAG,CAAA;IACZ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,MAAM,CAAC,EAAM,EAAE,WAAmB,EAAE,IAAY;QACpD,MAAM,eAAe,CAAC,WAAW,CAAC,EAAE,EAAE,WAAW,CAAC,CAAA;QAClD,MAAM,eAAe,CAAC,cAAc,CAAC,EAAE,EAAE,WAAW,CAAC,CAAA;QACrD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;iBACnB,MAAM,CAAC,UAAU,CAAC;iBAClB,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;iBACzF,SAAS,EAAE,CAAA;YACd,OAAO,GAAI,CAAA;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,eAAe,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,MAAM,CAAC,EAAM,EAAE,WAAmB,EAAE,UAAkB,EAAE,KAAwB;QACpF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,UAAU,CAAC,CAAA;QAC5D,gGAAgG;QAChG,8EAA8E;QAC9E,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI;YACjC,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB,CAAA;QACD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;iBACnB,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC,MAAM,CAAC;iBACX,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;iBAClF,SAAS,EAAE,CAAA;YACd,OAAO,GAAI,CAAA;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,eAAe,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;QACnD,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,OAAO,CAAC,EAAM,EAAE,WAAmB,EAAE,UAAkB,EAAE,QAAiB;QAC9E,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,eAAe,CAAC,WAAW,CAAC,EAAE,EAAE,WAAW,CAAC,CAAA;YAClD,MAAM,eAAe,CAAC,cAAc,CAAC,EAAE,EAAE,WAAW,CAAC,CAAA;QACvD,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;aACnB,MAAM,CAAC,UAAU,CAAC;aAClB,GAAG,CAAC;YACH,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;YACxC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YACrE,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB,CAAC;aACD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;aAClF,SAAS,EAAE,CAAA;QACd,IAAI,CAAC,GAAG;YAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;QAC9C,OAAO,GAAG,CAAA;IACZ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,OAAO,CAAC,EAAM,EAAE,WAAmB,EAAE,WAAqB;QAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QAClC,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,MAAM;YACnC,MAAM,SAAS,CAAC,UAAU,CAAC,4DAA4D,CAAC,CAAA;QAE1F,MAAM,OAAO,GAAG,MAAM,EAAE;aACrB,MAAM,EAAE;aACR,IAAI,CAAC,UAAU,CAAC;aAChB,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;aAC9C,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;aAC3B,GAAG,CAAC,QAAQ,CAAC,CAAA;QAChB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;QAE1D,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;QAClF,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;QACrD,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC;YAC/F,MAAM,SAAS,CAAC,QAAQ,CACtB,qHAAqH,EACrH,gCAAgC,CACjC,CAAA;QAEH,8FAA8F;QAC9F,8FAA8F;QAC9F,+FAA+F;QAC/F,8EAA8E;QAC9E,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;QACtB,MAAM,KAAK,GAAG,WAAW;aACtB,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;aACnC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,KAAK,KAAK,KAAK,CAAC,CAAA;QAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAA;QAEvC;;;;;;;;;;;;;;;;;;WAkBG;QACH,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;YACrF,KAAK,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC/C,MAAM,EAAE;qBACL,MAAM,CAAC,UAAU,CAAC;qBAClB,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,CAAC;qBAC7B,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;YAC/E,CAAC;YACD,KAAK,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,KAAK,EAAE,CAAC;gBAClC,MAAM,EAAE;qBACL,MAAM,CAAC,UAAU,CAAC;qBAClB,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;qBACrC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;YAC/E,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,EAAE;aAClB,MAAM,EAAE;aACR,IAAI,CAAC,UAAU,CAAC;aAChB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;aAClF,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;QACvD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACK,MAAM,CAAC,QAAQ,CAAC,WAAmB;QACzC,OAAO,GAAG,CAAQ,wBAAwB,UAAU,CAAC,KAAK,mBAAmB,UAAU,UAAU,UAAU,CAAC,WAAW,MAAM,WAAW,GAAG,CAAA;IAC7I,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAM,EAAE,WAAmB;QAC1D,MAAM,EAAE,CAAC,OAAO,CACd,GAAG,CAAA,qFAAqF,WAAW,IAAI,CACxG,CAAA;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACK,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,EAAM,EAAE,WAAmB;QAC7D,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;aACnB,MAAM,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;aACtB,IAAI,CAAC,UAAU,CAAC;aAChB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QACrF,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,mBAAmB;YAAE,OAAM;QAC/C,MAAM,SAAS,CAAC,QAAQ,CACtB,8BAA8B,mBAAmB,4GAA4G,EAC7J,kCAAkC,CACnC,CAAA;IACH,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,SAAS,CAAC,GAAY,EAAE,IAAY;QACjD,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;YAAE,OAAO,GAAG,CAAA;QAC1C,OAAO,SAAS,CAAC,QAAQ,CACvB,iDAAiD,IAAI,IAAI,EACzD,+BAA+B,CAChC,CAAA;IACH,CAAC;CACF"}
@@ -0,0 +1,71 @@
1
+ -- No two live categories on the same place, enforced rather than intended.
2
+ --
3
+ -- Hand-written, because the repair below is not something drizzle-kit generates — the same reason
4
+ -- `0001_rls.sql`, `0006_workspace_registry_read.sql` and `0007_history_sequence.sql` are. Every
5
+ -- statement carries its own guard, and `migrations.test.ts` applies this folder twice against a
6
+ -- database created from nothing to prove the guards are real rather than intended.
7
+ --
8
+ -- ## What was wrong
9
+ --
10
+ -- The contract, the changeset and `CategoryService.list`'s own comment all said that `reorder` keeps
11
+ -- the live categories on distinct numbers, so the `order by "order", "name"` tiebreak never fires for
12
+ -- them. All three were describing an intention.
13
+ --
14
+ -- A category joins the end of the list by taking `(select coalesce(max("order"), -1) + 1)`, and a
15
+ -- restore appends the same way. Putting that subquery *inside* the insert removes a round trip; it
16
+ -- does not remove the race. Under READ COMMITTED every statement takes its own snapshot, so two
17
+ -- transactions appending at the same instant each read a list without the other's row in it and both
18
+ -- take the same number. `reorder`'s `select … for update` does not serialise them either: a row lock
19
+ -- cannot cover a row that does not exist yet, and a restore's subquery is evaluated against the
20
+ -- snapshot its statement started with rather than the one it would get after waiting.
21
+ --
22
+ -- Two live categories on one number is not a crash. It is a list whose order changes under somebody
23
+ -- when a name changes, and a *move up* that appears to do nothing because the row it swapped with
24
+ -- sorts back ahead of it by name.
25
+ --
26
+ -- ## Why an index
27
+ --
28
+ -- Because checking first and writing after is the race, not the fix — the same reason
29
+ -- `inventory_repairs_one_open_uq` is an index in `0003`. `CategoryService` takes a per-workspace
30
+ -- advisory lock so the ordinary append never meets this index at all; the index is what makes the
31
+ -- claim true whatever else reaches the table.
32
+ --
33
+ -- **Partial, over the live rows only.** An archived category keeps the number it had when it left,
34
+ -- and the next reorder renumbers a live row straight onto it. That collision is invisible — an
35
+ -- archived category is in no picker, no filter and no sequence — so a total unique index would refuse
36
+ -- an archive-and-reorder that is entirely correct.
37
+ --
38
+ -- ## The repair comes first, or the upgrade takes the host service down
39
+ --
40
+ -- A module's migrations are the first thing the kernel runs, so a `CREATE UNIQUE INDEX` that meets
41
+ -- rows already sharing a number does not break categories: it throws during boot, and `core` hosts
42
+ -- five modules and never binds its port. Every instance that has run 0.2.0 may hold such a pair, so
43
+ -- the duplicates are renumbered before the index is built.
44
+ --
45
+ -- The renumbering is confined to the workspaces that actually have a duplicate, and it walks them in
46
+ -- `("order", "name", "id")` — which is the order `list` already returns and therefore the order the
47
+ -- screen already showed. Nobody's arrangement visibly changes. `"order" <> ordered.rn - 1` is what
48
+ -- makes a replay match nothing rather than rewrite rows a second time.
49
+ --
50
+ -- ## Append-only, and readable by the image before this one
51
+ --
52
+ -- 0.2.0 is published and `core` depends on it. This changes no table and no column: it adds an index
53
+ -- and moves some integers within the range they already occupied, so the previous image reads the
54
+ -- table exactly as it read it before and rolling back needs no dump.
55
+
56
+ UPDATE "mod_inventory"."categories" AS c
57
+ SET "order" = ordered.rn - 1
58
+ FROM (SELECT "id",
59
+ row_number() OVER (PARTITION BY "workspace_id" ORDER BY "order", "name", "id") AS rn
60
+ FROM "mod_inventory"."categories"
61
+ WHERE "archived_at" IS NULL
62
+ AND "workspace_id" IN (SELECT "workspace_id"
63
+ FROM "mod_inventory"."categories"
64
+ WHERE "archived_at" IS NULL
65
+ GROUP BY "workspace_id", "order"
66
+ HAVING count(*) > 1)) AS ordered
67
+ WHERE c."id" = ordered."id"
68
+ AND c."order" <> ordered.rn - 1;--> statement-breakpoint
69
+
70
+ CREATE UNIQUE INDEX IF NOT EXISTS "inventory_categories_ws_order_live_uq"
71
+ ON "mod_inventory"."categories" USING btree ("workspace_id","order") WHERE "archived_at" IS NULL;
@@ -57,6 +57,13 @@
57
57
  "when": 1787788455674,
58
58
  "tag": "0007_history_sequence",
59
59
  "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "7",
64
+ "when": 1787788455675,
65
+ "tag": "0008_category_order_unique",
66
+ "breakpoints": true
60
67
  }
61
68
  ]
62
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-inventory",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Kern inventory module: the company's asset register — what it owns, item by item, with asset tags, serial numbers, purchase and warranty details",
5
5
  "homepage": "https://github.com/KernAIO/module-inventory#readme",
6
6
  "license": "AGPL-3.0-only",
@@ -56,6 +56,7 @@
56
56
  "@orpc/contract": "^1.15.0",
57
57
  "@orpc/server": "^1.15.0",
58
58
  "drizzle-orm": "^0.45.0",
59
+ "svelte-dnd-action": "^0.9.64",
59
60
  "zod": "^4.1.0"
60
61
  },
61
62
  "devDependencies": {
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { describe, expect, it } from 'vitest'
5
+ import { MAX_LIVE_CATEGORIES } from '../contract/models.js'
5
6
  import {
6
7
  codeOf,
7
8
  errorLine,
@@ -203,6 +204,35 @@ describe('which sentence a refusal earns', () => {
203
204
  '<error_custody_not_held>',
204
205
  )
205
206
  })
207
+
208
+ /**
209
+ * The one refusal whose sentence has a number in it.
210
+ *
211
+ * A limit that will not say what it is cannot be planned around, and `KernError.conflict` carries a
212
+ * reason and no data — so the figure comes from the contract, which is the same constant the server
213
+ * enforces. Passed through `t`, which puts it through `Intl.NumberFormat`, so the reader sees it in
214
+ * their own digits rather than in ASCII.
215
+ */
216
+ it('hands the category limit its number, from the constant the server enforces', () => {
217
+ const refusal = conflict('inventory.category.limit_reached')
218
+ expect(errorLine(refusal)).toEqual({
219
+ key: 'error_category_limit_reached',
220
+ detail: null,
221
+ values: { max: MAX_LIVE_CATEGORIES },
222
+ })
223
+ expect(
224
+ errorMessage(refusal, (key, values) => `${key}:${JSON.stringify(values ?? null)}`),
225
+ 'and the screen is handed the values along with the key',
226
+ ).toBe(`error_category_limit_reached:{"max":${MAX_LIVE_CATEGORIES}}`)
227
+ expect(en['inventory.error_category_limit_reached'], 'which the sentence really asks for').toContain(
228
+ '{max}',
229
+ )
230
+ })
231
+
232
+ it('leaves values off every key that has no placeholder', () => {
233
+ expect(errorLine(conflict('inventory.category.order_stale')).values).toBeUndefined()
234
+ expect(errorLine({ code: 'NOT_FOUND' }).values).toBeUndefined()
235
+ })
206
236
  })
207
237
 
208
238
  // ------------------------------------------------------- the two lists that have to stay in step