@kernhq/module-inventory 0.2.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 (198) hide show
  1. package/README.md +106 -25
  2. package/dist/contract/capabilities.d.ts +4 -4
  3. package/dist/contract/capabilities.d.ts.map +1 -1
  4. package/dist/contract/capabilities.js +51 -9
  5. package/dist/contract/capabilities.js.map +1 -1
  6. package/dist/contract/events.d.ts +43 -0
  7. package/dist/contract/events.d.ts.map +1 -1
  8. package/dist/contract/events.js +40 -0
  9. package/dist/contract/events.js.map +1 -1
  10. package/dist/contract/index.d.ts +1 -0
  11. package/dist/contract/index.d.ts.map +1 -1
  12. package/dist/contract/index.js +1 -0
  13. package/dist/contract/index.js.map +1 -1
  14. package/dist/contract/models.d.ts +345 -0
  15. package/dist/contract/models.d.ts.map +1 -1
  16. package/dist/contract/models.js +245 -0
  17. package/dist/contract/models.js.map +1 -1
  18. package/dist/contract/notifications.d.ts +24 -0
  19. package/dist/contract/notifications.d.ts.map +1 -0
  20. package/dist/contract/notifications.js +72 -0
  21. package/dist/contract/notifications.js.map +1 -0
  22. package/dist/contract/permissions.d.ts +52 -2
  23. package/dist/contract/permissions.d.ts.map +1 -1
  24. package/dist/contract/permissions.js +55 -2
  25. package/dist/contract/permissions.js.map +1 -1
  26. package/dist/contract/router.d.ts +1108 -0
  27. package/dist/contract/router.d.ts.map +1 -1
  28. package/dist/contract/router.js +253 -1
  29. package/dist/contract/router.js.map +1 -1
  30. package/dist/contract/settings.d.ts +2 -0
  31. package/dist/contract/settings.d.ts.map +1 -1
  32. package/dist/contract/settings.js +24 -4
  33. package/dist/contract/settings.js.map +1 -1
  34. package/dist/server/index.d.ts +8 -4
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +289 -5
  37. package/dist/server/index.js.map +1 -1
  38. package/dist/server/jobs.d.ts +48 -0
  39. package/dist/server/jobs.d.ts.map +1 -0
  40. package/dist/server/jobs.js +358 -0
  41. package/dist/server/jobs.js.map +1 -0
  42. package/dist/server/router.d.ts +1392 -0
  43. package/dist/server/router.d.ts.map +1 -1
  44. package/dist/server/router.js +385 -7
  45. package/dist/server/router.js.map +1 -1
  46. package/dist/server/schema.d.ts +134 -1
  47. package/dist/server/schema.d.ts.map +1 -1
  48. package/dist/server/schema.js +169 -4
  49. package/dist/server/schema.js.map +1 -1
  50. package/dist/server/services/assets.d.ts +69 -3
  51. package/dist/server/services/assets.d.ts.map +1 -1
  52. package/dist/server/services/assets.js +199 -31
  53. package/dist/server/services/assets.js.map +1 -1
  54. package/dist/server/services/attachments.d.ts +80 -0
  55. package/dist/server/services/attachments.d.ts.map +1 -0
  56. package/dist/server/services/attachments.js +182 -0
  57. package/dist/server/services/attachments.js.map +1 -0
  58. package/dist/server/services/audience.d.ts +15 -0
  59. package/dist/server/services/audience.d.ts.map +1 -0
  60. package/dist/server/services/audience.js +64 -0
  61. package/dist/server/services/audience.js.map +1 -0
  62. package/dist/server/services/categories.d.ts +157 -0
  63. package/dist/server/services/categories.d.ts.map +1 -0
  64. package/dist/server/services/categories.js +309 -0
  65. package/dist/server/services/categories.js.map +1 -0
  66. package/dist/server/services/cursor.d.ts +68 -0
  67. package/dist/server/services/cursor.d.ts.map +1 -0
  68. package/dist/server/services/cursor.js +39 -0
  69. package/dist/server/services/cursor.js.map +1 -0
  70. package/dist/server/services/custody.d.ts +175 -0
  71. package/dist/server/services/custody.d.ts.map +1 -0
  72. package/dist/server/services/custody.js +367 -0
  73. package/dist/server/services/custody.js.map +1 -0
  74. package/dist/server/services/db-errors.d.ts +7 -0
  75. package/dist/server/services/db-errors.d.ts.map +1 -0
  76. package/dist/server/services/db-errors.js +32 -0
  77. package/dist/server/services/db-errors.js.map +1 -0
  78. package/dist/server/services/index.d.ts +16 -0
  79. package/dist/server/services/index.d.ts.map +1 -1
  80. package/dist/server/services/index.js +25 -1
  81. package/dist/server/services/index.js.map +1 -1
  82. package/dist/server/services/members.d.ts +27 -0
  83. package/dist/server/services/members.d.ts.map +1 -0
  84. package/dist/server/services/members.js +39 -0
  85. package/dist/server/services/members.js.map +1 -0
  86. package/dist/server/services/notify.d.ts +43 -1
  87. package/dist/server/services/notify.d.ts.map +1 -1
  88. package/dist/server/services/notify.js +47 -5
  89. package/dist/server/services/notify.js.map +1 -1
  90. package/dist/server/services/offboarding.d.ts +70 -0
  91. package/dist/server/services/offboarding.d.ts.map +1 -0
  92. package/dist/server/services/offboarding.js +116 -0
  93. package/dist/server/services/offboarding.js.map +1 -0
  94. package/dist/server/services/repairs.d.ts +204 -0
  95. package/dist/server/services/repairs.d.ts.map +1 -0
  96. package/dist/server/services/repairs.js +476 -0
  97. package/dist/server/services/repairs.js.map +1 -0
  98. package/dist/server/services/search.d.ts +85 -0
  99. package/dist/server/services/search.d.ts.map +1 -0
  100. package/dist/server/services/search.js +142 -0
  101. package/dist/server/services/search.js.map +1 -0
  102. package/dist/server/services/stats.d.ts +42 -0
  103. package/dist/server/services/stats.d.ts.map +1 -0
  104. package/dist/server/services/stats.js +80 -0
  105. package/dist/server/services/stats.js.map +1 -0
  106. package/dist/server/services/status.d.ts +102 -0
  107. package/dist/server/services/status.d.ts.map +1 -0
  108. package/dist/server/services/status.js +71 -0
  109. package/dist/server/services/status.js.map +1 -0
  110. package/migrations/0002_custody_and_categories.sql +23 -0
  111. package/migrations/0003_repairs.sql +23 -0
  112. package/migrations/0004_platform_surfaces.sql +51 -0
  113. package/migrations/0005_repair_dates.sql +35 -0
  114. package/migrations/0006_workspace_registry_read.sql +50 -0
  115. package/migrations/0007_history_sequence.sql +83 -0
  116. package/migrations/0008_category_order_unique.sql +71 -0
  117. package/migrations/meta/0002_snapshot.json +1054 -0
  118. package/migrations/meta/0003_snapshot.json +1070 -0
  119. package/migrations/meta/0004_snapshot.json +1130 -0
  120. package/migrations/meta/0005_snapshot.json +1135 -0
  121. package/migrations/meta/_journal.json +49 -0
  122. package/package.json +2 -1
  123. package/src/client/api-instance.ts +27 -2
  124. package/src/client/bidi.test.ts +148 -0
  125. package/src/client/bidi.ts +85 -0
  126. package/src/client/components/AssetDetailPanel.svelte +614 -0
  127. package/src/client/components/AssetFormDialog.svelte +71 -22
  128. package/src/client/components/AssetPhoto.svelte +178 -0
  129. package/src/client/components/AttachmentsSection.svelte +327 -0
  130. package/src/client/components/CustodyDialog.svelte +201 -0
  131. package/src/client/components/RepairDialog.svelte +271 -0
  132. package/src/client/components/RepairsSection.svelte +318 -0
  133. package/src/client/components/Timeline.svelte +347 -0
  134. package/src/client/components/TimelineText.svelte +124 -0
  135. package/src/client/core-api.ts +71 -0
  136. package/src/client/custody.test.ts +31 -0
  137. package/src/client/custody.ts +34 -0
  138. package/src/client/errors.test.ts +395 -0
  139. package/src/client/errors.ts +229 -0
  140. package/src/client/links.test.ts +74 -0
  141. package/src/client/links.ts +44 -0
  142. package/src/client/members.test.ts +132 -0
  143. package/src/client/members.ts +116 -0
  144. package/src/client/messages.test.ts +127 -0
  145. package/src/client/messages.ts +1106 -18
  146. package/src/client/mock.test.ts +465 -1
  147. package/src/client/mock.ts +1105 -79
  148. package/src/client/module.ts +73 -1
  149. package/src/client/pages/AssetsPage.svelte +317 -45
  150. package/src/client/permissions.ts +7 -0
  151. package/src/client/price.test.ts +151 -3
  152. package/src/client/price.ts +155 -11
  153. package/src/client/query.ts +36 -0
  154. package/src/client/reorder.test.ts +100 -0
  155. package/src/client/reorder.ts +79 -0
  156. package/src/client/repairs.test.ts +38 -0
  157. package/src/client/repairs.ts +38 -0
  158. package/src/client/sequence.test.ts +248 -0
  159. package/src/client/sequence.ts +185 -0
  160. package/src/client/settings/CategoriesSettings.svelte +746 -0
  161. package/src/client/settings/GeneralSettings.svelte +0 -0
  162. package/src/client/status.ts +29 -0
  163. package/src/client/timeline.test.ts +175 -0
  164. package/src/client/timeline.ts +206 -0
  165. package/src/client/widgets/OverviewWidget.svelte +127 -26
  166. package/src/client/widgets/RepairsWidget.svelte +124 -0
  167. package/src/contract/capabilities.ts +53 -9
  168. package/src/contract/events.ts +49 -0
  169. package/src/contract/index.ts +1 -0
  170. package/src/contract/models.ts +270 -0
  171. package/src/contract/notifications.ts +73 -0
  172. package/src/contract/permissions.ts +55 -2
  173. package/src/contract/router.ts +284 -1
  174. package/src/contract/settings.ts +24 -4
  175. package/src/module.test.ts +217 -4
  176. package/src/server/index.ts +311 -4
  177. package/src/server/inventory.int.test.ts +4109 -19
  178. package/src/server/jobs.ts +444 -0
  179. package/src/server/migrations.test.ts +253 -2
  180. package/src/server/router.ts +486 -6
  181. package/src/server/schema.ts +171 -3
  182. package/src/server/services/assets.ts +214 -54
  183. package/src/server/services/attachments.ts +215 -0
  184. package/src/server/services/audience.ts +77 -0
  185. package/src/server/services/categories.ts +337 -0
  186. package/src/server/services/cursor.ts +104 -0
  187. package/src/server/services/custody.ts +471 -0
  188. package/src/server/services/db-errors.ts +42 -0
  189. package/src/server/services/index.ts +34 -1
  190. package/src/server/services/members.ts +54 -0
  191. package/src/server/services/notify.ts +52 -7
  192. package/src/server/services/offboarding.ts +150 -0
  193. package/src/server/services/repairs.ts +567 -0
  194. package/src/server/services/search.ts +166 -0
  195. package/src/server/services/stats.ts +88 -0
  196. package/src/server/services/status.test.ts +34 -0
  197. package/src/server/services/status.ts +143 -0
  198. package/src/client/settings/core-api.ts +0 -32
@@ -0,0 +1,50 @@
1
+ -- The one read in this module that has no workspace to be bound to.
2
+ --
3
+ -- Hand-written, because drizzle-kit does not generate policies — the same reason `0001_rls.sql` is
4
+ -- hand-written, and it carries the same guard: `create policy` has no `if not exists`, so it is
5
+ -- preceded by an explicit drop. `migrations.test.ts` applies this folder twice against a database
6
+ -- created from nothing and is what proves the guard is real rather than intended.
7
+ --
8
+ -- **Append-only.** 0.2.0 is published and `core` depends on it. This adds a policy and changes no
9
+ -- table, column or index, so the 0.2.0 image reads the schema afterwards exactly as it read it
10
+ -- before and rolling back needs no dump.
11
+ --
12
+ -- ## What was wrong
13
+ --
14
+ -- `mod_inventory.workspaces` is the registry the two nightly sweeps enumerate: a cron handler is
15
+ -- woken by a clock, so it has no workspace and `app.workspace_id` is unset for it by definition.
16
+ -- The table is a tenant table like every other — it carries `workspace_id` — so `0004` gave it
17
+ -- `enable`, `force` and the standard per-workspace policy, and the enumeration was left resting on
18
+ -- "the application connects as the schema's owner, and an owner bypasses RLS".
19
+ --
20
+ -- That is false, and `force row level security` is exactly the clause that makes it false: forcing
21
+ -- subjects the **table owner** to the policies as well. Only a superuser (or a role with BYPASSRLS)
22
+ -- is exempt. So on every deployment whose application role is an ordinary login role — which is what
23
+ -- the project's own RLS note asks for — `select workspace_id from mod_inventory.workspaces` with no
24
+ -- workspace bound returned **zero rows**. Not an error, not a warning: both sweeps found nothing to
25
+ -- do, every night, for ever. The development database runs as a superuser, which is precisely why
26
+ -- nothing noticed.
27
+ --
28
+ -- ## Why this shape of fix
29
+ --
30
+ -- Permissive policies are OR-ed, so this adds a second `for select` policy rather than touching the
31
+ -- first. It admits a session with **no workspace bound** — which is the scheduler, by construction,
32
+ -- since every request-bound read in this module goes through `kernel.database.withWorkspace` and
33
+ -- sets the GUC. A request-bound session therefore still matches only the original policy and still
34
+ -- sees exactly its own row: this widens nothing for anybody holding a workspace.
35
+ --
36
+ -- It is keyed on the *absence of a binding* rather than on a role name because a role name is a
37
+ -- deployment's choice — `kern`, `kern_app`, whatever an operator called it — and a migration that
38
+ -- named one would be right on one instance and wrong on the next. `set_config(..., true)` is
39
+ -- transaction-local, so an unbound read on a pooled connection sees the setting unset (NULL) or
40
+ -- empty; both are covered.
41
+ --
42
+ -- The table holds nothing but workspace ids and a timestamp — it is this module's own bookkeeping,
43
+ -- not tenant data anybody reads through the API. `assets`, `custody_periods`, `repairs` and the rest
44
+ -- are untouched and keep answering nothing at all to a session with no workspace, which is what
45
+ -- `inventory.int.test.ts` proves against a plain login role.
46
+
47
+ drop policy if exists "workspaces_unbound_read" on "mod_inventory"."workspaces";--> statement-breakpoint
48
+ create policy "workspaces_unbound_read" on "mod_inventory"."workspaces"
49
+ for select
50
+ using (coalesce(current_setting('app.workspace_id', true), '') = '');
@@ -0,0 +1,83 @@
1
+ -- The timeline's own ordering key, because a uuidv7 is only ordered to the millisecond.
2
+ --
3
+ -- Hand-written, because drizzle-kit does not generate a sequence, a backfill or a `setval` — the
4
+ -- same reason `0001_rls.sql` and `0006_workspace_registry_read.sql` are hand-written. 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
+ -- `asset_history` is read newest-first ordered by its primary key, on the grounds that a uuidv7
11
+ -- already carries the clock and is unique where `occurred_at` is not. Half of that is true. The
12
+ -- kernel's `uuidv7()` puts the millisecond in bytes 0-5 and fills bytes 6-15 from `randomUUID()`
13
+ -- with **no intra-millisecond counter**, so two entries written in the same millisecond sort in an
14
+ -- order decided by ten random bytes. It is stable for a given set of rows, so keyset paging stayed
15
+ -- correct — no gaps, no repeats — and the defect never showed up as a broken list. What it showed
16
+ -- up as is a timeline that renders "Bruno removed the file" above "Bruno added the file", which is
17
+ -- the one record a company later argues from.
18
+ --
19
+ -- `occurred_at` is no rescue: it defaults to `now()`, which is the *transaction* timestamp, so two
20
+ -- entries written in one transaction share it exactly.
21
+ --
22
+ -- ## Why a sequence
23
+ --
24
+ -- A sequence is exact rather than probabilistic: `nextval` hands out strictly increasing values
25
+ -- whatever the clock does, at whatever rate, and two entries written a microsecond apart are two
26
+ -- values. It is also cheap — one `nextval` per history row, no extra round trip — and it is the one
27
+ -- ordering key a module can own outright, where the clock belongs to the machine.
28
+ --
29
+ -- The unique index is on `(workspace_id, asset_id, seq)` because that is what the timeline reads:
30
+ -- one asset's entries, newest first, paged on `seq`. `inventory_asset_history_ws_asset_row_idx` is
31
+ -- left where it is — dropping an index is a schema change and this file only adds — and goes one
32
+ -- release later.
33
+ --
34
+ -- ## Append-only, and readable by the image before this one
35
+ --
36
+ -- 0.2.0 is published and `core` depends on it, so this file only adds: a sequence, a column, an
37
+ -- index. The column is `not null` **with a default**, which is what keeps the previous image
38
+ -- writing: it never names `seq`, so the default fills it, and it never selects `seq`, so it reads
39
+ -- the table exactly as it read it before. Rolling that image back needs no dump.
40
+ --
41
+ -- The backfill orders existing rows by `(occurred_at, id)` rather than leaving them to the physical
42
+ -- order a rewrite would produce. Within one millisecond that is still the arbitrary uuid order this
43
+ -- migration exists to replace — nothing can recover an order that was never recorded — but across
44
+ -- milliseconds it is the order the timeline already showed, so an existing instance's history does
45
+ -- not visibly reshuffle on upgrade.
46
+
47
+ CREATE SEQUENCE IF NOT EXISTS "mod_inventory"."asset_history_seq" AS bigint;--> statement-breakpoint
48
+
49
+ ALTER TABLE "mod_inventory"."asset_history" ADD COLUMN IF NOT EXISTS "seq" bigint;--> statement-breakpoint
50
+
51
+ -- Only the rows that have none, so a replay matches nothing and changes nothing.
52
+ --
53
+ -- `created_at` rather than `occurred_at`: the drizzle field is `occurredAt` and the *column* it maps
54
+ -- to is `created_at`, because `asset_history` reuses the module's `created()` factory. A migration
55
+ -- talks to the column, and this file's first draft talked to the field — which fails at
56
+ -- `column "occurred_at" does not exist`, during the module's own migration, which is a host service
57
+ -- that never binds its port rather than a broken timeline.
58
+ UPDATE "mod_inventory"."asset_history" AS h
59
+ SET "seq" = ordered.rn
60
+ FROM (SELECT "id", row_number() OVER (ORDER BY "created_at", "id") AS rn
61
+ FROM "mod_inventory"."asset_history"
62
+ WHERE "seq" IS NULL) AS ordered
63
+ WHERE h."id" = ordered."id";--> statement-breakpoint
64
+
65
+ -- `GREATEST` so this only ever moves the sequence forward. A replay against a live database must
66
+ -- not hand back a value some open transaction has already taken.
67
+ SELECT setval('mod_inventory.asset_history_seq',
68
+ GREATEST((SELECT coalesce(max("seq"), 0) FROM "mod_inventory"."asset_history"),
69
+ (SELECT last_value FROM "mod_inventory"."asset_history_seq")),
70
+ true);--> statement-breakpoint
71
+
72
+ ALTER TABLE "mod_inventory"."asset_history"
73
+ ALTER COLUMN "seq" SET DEFAULT nextval('mod_inventory.asset_history_seq');--> statement-breakpoint
74
+
75
+ ALTER TABLE "mod_inventory"."asset_history" ALTER COLUMN "seq" SET NOT NULL;--> statement-breakpoint
76
+
77
+ -- So `drop schema mod_inventory cascade` takes the sequence with it, and so does dropping the
78
+ -- column one release from now.
79
+ ALTER SEQUENCE "mod_inventory"."asset_history_seq"
80
+ OWNED BY "mod_inventory"."asset_history"."seq";--> statement-breakpoint
81
+
82
+ CREATE UNIQUE INDEX IF NOT EXISTS "inventory_asset_history_ws_asset_seq_uq"
83
+ ON "mod_inventory"."asset_history" USING btree ("workspace_id","asset_id","seq");
@@ -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;