@kernhq/module-inventory 0.1.2 → 0.3.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.
- package/README.md +134 -9
- package/dist/contract/capabilities.d.ts +49 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +94 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +76 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +62 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +16 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +468 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +320 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/notifications.d.ts +24 -0
- package/dist/contract/notifications.d.ts.map +1 -0
- package/dist/contract/notifications.js +72 -0
- package/dist/contract/notifications.js.map +1 -0
- package/dist/contract/permissions.d.ts +72 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +79 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/contract/router.d.ts +1378 -0
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +266 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/contract/settings.d.ts +20 -0
- package/dist/contract/settings.d.ts.map +1 -0
- package/dist/contract/settings.js +49 -0
- package/dist/contract/settings.js.map +1 -0
- package/dist/server/index.d.ts +8 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +288 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +48 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +358 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +1789 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +439 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +156 -9
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +183 -11
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +139 -0
- package/dist/server/services/assets.d.ts.map +1 -0
- package/dist/server/services/assets.js +429 -0
- package/dist/server/services/assets.js.map +1 -0
- package/dist/server/services/attachments.d.ts +80 -0
- package/dist/server/services/attachments.d.ts.map +1 -0
- package/dist/server/services/attachments.js +182 -0
- package/dist/server/services/attachments.js.map +1 -0
- package/dist/server/services/audience.d.ts +15 -0
- package/dist/server/services/audience.d.ts.map +1 -0
- package/dist/server/services/audience.js +64 -0
- package/dist/server/services/audience.js.map +1 -0
- package/dist/server/services/categories.d.ts +57 -0
- package/dist/server/services/categories.d.ts.map +1 -0
- package/dist/server/services/categories.js +124 -0
- package/dist/server/services/categories.js.map +1 -0
- package/dist/server/services/cursor.d.ts +68 -0
- package/dist/server/services/cursor.d.ts.map +1 -0
- package/dist/server/services/cursor.js +39 -0
- package/dist/server/services/cursor.js.map +1 -0
- package/dist/server/services/custody.d.ts +175 -0
- package/dist/server/services/custody.d.ts.map +1 -0
- package/dist/server/services/custody.js +367 -0
- package/dist/server/services/custody.js.map +1 -0
- package/dist/server/services/db-errors.d.ts +7 -0
- package/dist/server/services/db-errors.d.ts.map +1 -0
- package/dist/server/services/db-errors.js +32 -0
- package/dist/server/services/db-errors.js.map +1 -0
- package/dist/server/services/index.d.ts +26 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +39 -0
- package/dist/server/services/index.js.map +1 -0
- package/dist/server/services/members.d.ts +27 -0
- package/dist/server/services/members.d.ts.map +1 -0
- package/dist/server/services/members.js +39 -0
- package/dist/server/services/members.js.map +1 -0
- package/dist/server/services/notify.d.ts +105 -0
- package/dist/server/services/notify.d.ts.map +1 -0
- package/dist/server/services/notify.js +147 -0
- package/dist/server/services/notify.js.map +1 -0
- package/dist/server/services/offboarding.d.ts +70 -0
- package/dist/server/services/offboarding.d.ts.map +1 -0
- package/dist/server/services/offboarding.js +116 -0
- package/dist/server/services/offboarding.js.map +1 -0
- package/dist/server/services/repairs.d.ts +204 -0
- package/dist/server/services/repairs.d.ts.map +1 -0
- package/dist/server/services/repairs.js +476 -0
- package/dist/server/services/repairs.js.map +1 -0
- package/dist/server/services/search.d.ts +85 -0
- package/dist/server/services/search.d.ts.map +1 -0
- package/dist/server/services/search.js +142 -0
- package/dist/server/services/search.js.map +1 -0
- package/dist/server/services/stats.d.ts +42 -0
- package/dist/server/services/stats.d.ts.map +1 -0
- package/dist/server/services/stats.js +80 -0
- package/dist/server/services/stats.js.map +1 -0
- package/dist/server/services/status.d.ts +102 -0
- package/dist/server/services/status.d.ts.map +1 -0
- package/dist/server/services/status.js +71 -0
- package/dist/server/services/status.js.map +1 -0
- package/migrations/0000_init.sql +12 -3
- package/migrations/0001_rls.sql +24 -0
- package/migrations/0002_custody_and_categories.sql +23 -0
- package/migrations/0003_repairs.sql +23 -0
- package/migrations/0004_platform_surfaces.sql +51 -0
- package/migrations/0005_repair_dates.sql +35 -0
- package/migrations/0006_workspace_registry_read.sql +50 -0
- package/migrations/0007_history_sequence.sql +83 -0
- package/migrations/meta/0000_snapshot.json +40 -13
- package/migrations/meta/0002_snapshot.json +1054 -0
- package/migrations/meta/0003_snapshot.json +1070 -0
- package/migrations/meta/0004_snapshot.json +1130 -0
- package/migrations/meta/0005_snapshot.json +1135 -0
- package/migrations/meta/_journal.json +44 -2
- package/package.json +5 -4
- package/src/client/api-instance.ts +27 -2
- package/src/client/api.ts +1 -1
- package/src/client/bidi.test.ts +148 -0
- package/src/client/bidi.ts +85 -0
- package/src/client/components/AssetDetailPanel.svelte +614 -0
- package/src/client/components/AssetFormDialog.svelte +191 -59
- package/src/client/components/AssetPhoto.svelte +178 -0
- package/src/client/components/AttachmentsSection.svelte +327 -0
- package/src/client/components/CustodyDialog.svelte +201 -0
- package/src/client/components/RepairDialog.svelte +271 -0
- package/src/client/components/RepairsSection.svelte +318 -0
- package/src/client/components/Timeline.svelte +347 -0
- package/src/client/components/TimelineText.svelte +124 -0
- package/src/client/core-api.ts +71 -0
- package/src/client/custody.test.ts +31 -0
- package/src/client/custody.ts +34 -0
- package/src/client/errors.test.ts +365 -0
- package/src/client/errors.ts +201 -0
- package/src/client/i18n.ts +11 -166
- package/src/client/index.ts +8 -1
- package/src/client/links.test.ts +74 -0
- package/src/client/links.ts +44 -0
- package/src/client/members.test.ts +132 -0
- package/src/client/members.ts +116 -0
- package/src/client/messages.test.ts +296 -0
- package/src/client/messages.ts +1424 -0
- package/src/client/mock.test.ts +555 -0
- package/src/client/mock.ts +1261 -52
- package/src/client/module.ts +76 -2
- package/src/client/pages/AssetsPage.svelte +638 -145
- package/src/client/permissions.ts +8 -1
- package/src/client/price.test.ts +254 -0
- package/src/client/price.ts +279 -0
- package/src/client/query.test.ts +58 -0
- package/src/client/query.ts +51 -2
- package/src/client/repairs.test.ts +38 -0
- package/src/client/repairs.ts +38 -0
- package/src/client/settings/CategoriesSettings.svelte +421 -0
- package/src/client/settings/GeneralSettings.svelte +403 -0
- package/src/client/status.ts +29 -0
- package/src/client/timeline.test.ts +175 -0
- package/src/client/timeline.ts +206 -0
- package/src/client/widgets/OverviewWidget.svelte +140 -26
- package/src/client/widgets/RepairsWidget.svelte +124 -0
- package/src/contract/capabilities.ts +99 -0
- package/src/contract/events.ts +83 -0
- package/src/contract/index.ts +16 -0
- package/src/contract/models.ts +360 -0
- package/src/contract/notifications.ts +73 -0
- package/src/contract/permissions.ts +79 -0
- package/src/contract/router.ts +300 -0
- package/src/contract/settings.ts +50 -0
- package/src/module.test.ts +330 -7
- package/src/server/index.ts +318 -8
- package/src/server/inventory.int.test.ts +4374 -0
- package/src/server/jobs.ts +444 -0
- package/src/server/migrations.test.ts +251 -0
- package/src/server/router.ts +574 -0
- package/src/server/schema.ts +184 -10
- package/src/server/services/assets.ts +528 -0
- package/src/server/services/attachments.ts +215 -0
- package/src/server/services/audience.ts +77 -0
- package/src/server/services/categories.ts +136 -0
- package/src/server/services/cursor.ts +104 -0
- package/src/server/services/custody.ts +471 -0
- package/src/server/services/db-errors.ts +42 -0
- package/src/server/services/index.ts +56 -0
- package/src/server/services/members.ts +54 -0
- package/src/server/services/notify.ts +196 -0
- package/src/server/services/offboarding.ts +150 -0
- package/src/server/services/repairs.ts +567 -0
- package/src/server/services/search.ts +166 -0
- package/src/server/services/stats.ts +88 -0
- package/src/server/services/status.test.ts +34 -0
- package/src/server/services/status.ts +143 -0
- package/tsconfig.base.json +22 -0
- package/tsconfig.client.json +1 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +18 -3
- package/dist/contract.d.ts +0 -387
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js +0 -119
- package/dist/contract.js.map +0 -1
- package/dist/server/_impl.d.ts +0 -427
- package/dist/server/_impl.d.ts.map +0 -1
- package/dist/server/_impl.js +0 -204
- package/dist/server/_impl.js.map +0 -1
- package/src/contract.ts +0 -143
- package/src/server/_impl.ts +0 -275
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- One open repair per asset.
|
|
2
|
+
--
|
|
3
|
+
-- Generated from `src/server/schema.ts` with `pnpm db:generate` and then guarded, in that order —
|
|
4
|
+
-- generating first is what keeps the file the schema's own output rather than a hand-written guess
|
|
5
|
+
-- at it.
|
|
6
|
+
--
|
|
7
|
+
-- **Append-only.** 0.2.0 is published and `core` depends on it, so this file adds an index and
|
|
8
|
+
-- nothing else: no table is created, dropped, renamed or narrowed, and the 0.2.0 image reads the
|
|
9
|
+
-- schema afterwards exactly as it read it before. Rolling that image back needs no dump.
|
|
10
|
+
--
|
|
11
|
+
-- **The guard is not decoration.** A module's migrations are the first thing the kernel runs, so one
|
|
12
|
+
-- that throws does not break its own feature — it takes down the whole host service, and `core`
|
|
13
|
+
-- hosts five modules. drizzle emits a bare `CREATE UNIQUE INDEX` here; `if not exists` is added by
|
|
14
|
+
-- hand, and `migrations.test.ts` applies this folder twice against a database created from nothing
|
|
15
|
+
-- to prove the guard is real rather than intended.
|
|
16
|
+
--
|
|
17
|
+
-- Why an index and not a service check: two people pressing *Send for repair* on the same laptop in
|
|
18
|
+
-- the same instant both read "it is here" and both insert. Checking first and inserting after is the
|
|
19
|
+
-- race, not the fix. Custody settles the same problem with a GiST exclusion constraint; a repair has
|
|
20
|
+
-- no range to overlap, only a flag, so a partial unique index says exactly the same thing more
|
|
21
|
+
-- cheaply. `RepairService` translates the resulting 23505 into a sentence.
|
|
22
|
+
|
|
23
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "inventory_repairs_one_open_uq" ON "mod_inventory"."repairs" USING btree ("asset_id") WHERE returned_on is null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- The platform surfaces: a workspace registry the scheduler can read, and the two "already told
|
|
2
|
+
-- somebody" markers the two sweeps need.
|
|
3
|
+
--
|
|
4
|
+
-- Generated from `src/server/schema.ts` with `pnpm db:generate` and then guarded, in that order.
|
|
5
|
+
-- Hand-writing the SQL is what puts an unguarded `ALTER TABLE … ADD PRIMARY KEY` in a file instead
|
|
6
|
+
-- of an inline key inside a `CREATE TABLE IF NOT EXISTS`; generating first keeps the file the
|
|
7
|
+
-- schema's own output. The `if not exists` on the table, the two columns and the index, and the
|
|
8
|
+
-- `drop policy if exists` under them, are what was added afterwards.
|
|
9
|
+
--
|
|
10
|
+
-- **Append-only.** 0.2.0 is published and `core` depends on it, so this file only adds: a new table,
|
|
11
|
+
-- two nullable columns and an index. Nothing is dropped, renamed or narrowed, so the 0.2.0 image
|
|
12
|
+
-- reads the schema afterwards exactly as it read it before and rolling back needs no dump.
|
|
13
|
+
--
|
|
14
|
+
-- **Every statement is idempotent.** A module's migrations are the first thing the kernel runs, so
|
|
15
|
+
-- one that throws does not break its own feature — it takes down the whole host service, and `core`
|
|
16
|
+
-- hosts five modules. `migrations.test.ts` applies this folder twice against a database created from
|
|
17
|
+
-- nothing and is what proves the guards are real rather than intended.
|
|
18
|
+
--
|
|
19
|
+
-- Why `workspaces` exists at all: a cron handler is woken by a clock, so it starts with no workspace
|
|
20
|
+
-- and nothing to derive one from. Core would answer, and asking it every tick makes an overnight
|
|
21
|
+
-- sweep fail whenever core is briefly away. `module-tracker` keeps the same table for the same
|
|
22
|
+
-- reason. It gets a policy like every other table carrying `workspace_id`; the enumeration itself
|
|
23
|
+
-- cannot honour that policy — `app.workspace_id` is unset there by definition — and reads as the
|
|
24
|
+
-- owner, which is what every module's scheduler here already does.
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS "mod_inventory"."workspaces" (
|
|
27
|
+
"workspace_id" uuid PRIMARY KEY NOT NULL,
|
|
28
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
29
|
+
);
|
|
30
|
+
--> statement-breakpoint
|
|
31
|
+
|
|
32
|
+
alter table "mod_inventory"."workspaces" enable row level security;--> statement-breakpoint
|
|
33
|
+
alter table "mod_inventory"."workspaces" force row level security;--> statement-breakpoint
|
|
34
|
+
drop policy if exists "workspaces_ws_isolation" on "mod_inventory"."workspaces";--> statement-breakpoint
|
|
35
|
+
create policy "workspaces_ws_isolation" on "mod_inventory"."workspaces"
|
|
36
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
37
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
38
|
+
|
|
39
|
+
-- A warranty falls inside the notice window on every one of the days before it expires, so a sweep
|
|
40
|
+
-- with no marker sends the same notice every morning for a month. Cleared when `warranty_until`
|
|
41
|
+
-- itself moves.
|
|
42
|
+
ALTER TABLE "mod_inventory"."assets" ADD COLUMN IF NOT EXISTS "warranty_notified_at" timestamp with time zone;--> statement-breakpoint
|
|
43
|
+
|
|
44
|
+
-- The same, for a repair that has been away too long: it is still too long tomorrow.
|
|
45
|
+
ALTER TABLE "mod_inventory"."repairs" ADD COLUMN IF NOT EXISTS "overdue_notified_at" timestamp with time zone;--> statement-breakpoint
|
|
46
|
+
|
|
47
|
+
-- What the overdue sweep reads: one workspace's still-open repairs, oldest first.
|
|
48
|
+
-- `inventory_repairs_ws_asset_idx` starts (workspace_id, asset_id, sent_on) and cannot answer this
|
|
49
|
+
-- without visiting every asset, and `inventory_repairs_one_open_uq` is partial on the right
|
|
50
|
+
-- predicate but keyed by `asset_id` alone, so it cannot be scoped to a workspace.
|
|
51
|
+
CREATE INDEX IF NOT EXISTS "inventory_repairs_ws_open_idx" ON "mod_inventory"."repairs" USING btree ("workspace_id","sent_on") WHERE returned_on is null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- A repair cannot come back before it was sent.
|
|
2
|
+
--
|
|
3
|
+
-- Generated from `src/server/schema.ts` with `pnpm db:generate` and then guarded, in that order —
|
|
4
|
+
-- generating first is what keeps the file the schema's own output rather than a hand-written guess
|
|
5
|
+
-- at it. The `drop constraint if exists` and the `not valid` are what was added afterwards.
|
|
6
|
+
--
|
|
7
|
+
-- **Append-only.** 0.2.0 is published and `core` depends on it, so this file adds a constraint and
|
|
8
|
+
-- nothing else: no table is created, dropped, renamed or narrowed, and the 0.2.0 image reads the
|
|
9
|
+
-- schema afterwards exactly as it read it before. Rolling that image back needs no dump.
|
|
10
|
+
--
|
|
11
|
+
-- **Idempotent.** `add constraint` has no `if not exists`, so it is preceded by an explicit drop —
|
|
12
|
+
-- the same shape `0001_rls.sql` uses for the custody exclusion constraint, and for the same reason:
|
|
13
|
+
-- a module's migrations are the first thing the kernel runs, so one that throws does not degrade its
|
|
14
|
+
-- own feature, it stops the host service booting, and `core` hosts five modules. A regenerated
|
|
15
|
+
-- `migrations/meta/_journal.json` is enough to cause a replay. `migrations.test.ts` applies this
|
|
16
|
+
-- folder twice against a database created from nothing to prove the guard is real.
|
|
17
|
+
--
|
|
18
|
+
-- **`not valid`, and that is the whole difference between a safe upgrade and an outage.** The
|
|
19
|
+
-- constraint is enforced on every insert and update from the moment it exists; what `not valid`
|
|
20
|
+
-- skips is the scan of rows that are already there. Those rows are exactly the ones this defect may
|
|
21
|
+
-- have written — `repairs.update` would move `sent_on` past a `returned_on` — and a validating
|
|
22
|
+
-- constraint that met one of them would throw *during migration*, on somebody's instance, during an
|
|
23
|
+
-- upgrade, taking core down with it. An instance that wants the scan can run
|
|
24
|
+
-- `alter table … validate constraint …` by hand once it has corrected its data; nothing needs it to.
|
|
25
|
+
--
|
|
26
|
+
-- Why the database as well as the service: `repairs.update` and `repairs.complete` each check the
|
|
27
|
+
-- pair before writing it, and two transactions can each pass that check and still commit a pair that
|
|
28
|
+
-- fails it — one moving `sent_on` while the other logs the item back. The service check is what
|
|
29
|
+
-- gives an ordinary mistake a sentence; this is what makes the rule true of the table.
|
|
30
|
+
|
|
31
|
+
ALTER TABLE "mod_inventory"."repairs"
|
|
32
|
+
DROP CONSTRAINT IF EXISTS "inventory_repairs_returned_after_sent";--> statement-breakpoint
|
|
33
|
+
ALTER TABLE "mod_inventory"."repairs"
|
|
34
|
+
ADD CONSTRAINT "inventory_repairs_returned_after_sent"
|
|
35
|
+
CHECK (returned_on is null or returned_on >= sent_on) NOT VALID;
|
|
@@ -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");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "33173d81-879e-409b-a7c7-d8f9977b9e6c",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -139,8 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"status": {
|
|
141
141
|
"name": "status",
|
|
142
|
-
"type": "
|
|
143
|
-
"typeSchema": "public",
|
|
142
|
+
"type": "text",
|
|
144
143
|
"primaryKey": false,
|
|
145
144
|
"notNull": true,
|
|
146
145
|
"default": "'in_stock'"
|
|
@@ -205,6 +204,13 @@
|
|
|
205
204
|
"primaryKey": false,
|
|
206
205
|
"notNull": false
|
|
207
206
|
},
|
|
207
|
+
"custom": {
|
|
208
|
+
"name": "custom",
|
|
209
|
+
"type": "jsonb",
|
|
210
|
+
"primaryKey": false,
|
|
211
|
+
"notNull": true,
|
|
212
|
+
"default": "'{}'::jsonb"
|
|
213
|
+
},
|
|
208
214
|
"created_at": {
|
|
209
215
|
"name": "created_at",
|
|
210
216
|
"type": "timestamp with time zone",
|
|
@@ -311,6 +317,28 @@
|
|
|
311
317
|
"method": "btree",
|
|
312
318
|
"with": {}
|
|
313
319
|
},
|
|
320
|
+
"inventory_assets_ws_custodian_idx": {
|
|
321
|
+
"name": "inventory_assets_ws_custodian_idx",
|
|
322
|
+
"columns": [
|
|
323
|
+
{
|
|
324
|
+
"expression": "workspace_id",
|
|
325
|
+
"isExpression": false,
|
|
326
|
+
"asc": true,
|
|
327
|
+
"nulls": "last"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"expression": "custodian_user_id",
|
|
331
|
+
"isExpression": false,
|
|
332
|
+
"asc": true,
|
|
333
|
+
"nulls": "last"
|
|
334
|
+
}
|
|
335
|
+
],
|
|
336
|
+
"isUnique": false,
|
|
337
|
+
"where": "custodian_user_id is not null",
|
|
338
|
+
"concurrently": false,
|
|
339
|
+
"method": "btree",
|
|
340
|
+
"with": {}
|
|
341
|
+
},
|
|
314
342
|
"inventory_assets_ws_warranty_idx": {
|
|
315
343
|
"name": "inventory_assets_ws_warranty_idx",
|
|
316
344
|
"columns": [
|
|
@@ -568,13 +596,13 @@
|
|
|
568
596
|
"workspace_id": {
|
|
569
597
|
"name": "workspace_id",
|
|
570
598
|
"type": "uuid",
|
|
571
|
-
"primaryKey":
|
|
599
|
+
"primaryKey": false,
|
|
572
600
|
"notNull": true
|
|
573
601
|
},
|
|
574
602
|
"key": {
|
|
575
603
|
"name": "key",
|
|
576
604
|
"type": "text",
|
|
577
|
-
"primaryKey":
|
|
605
|
+
"primaryKey": false,
|
|
578
606
|
"notNull": true
|
|
579
607
|
},
|
|
580
608
|
"value": {
|
|
@@ -586,7 +614,12 @@
|
|
|
586
614
|
},
|
|
587
615
|
"indexes": {},
|
|
588
616
|
"foreignKeys": {},
|
|
589
|
-
"compositePrimaryKeys": {
|
|
617
|
+
"compositePrimaryKeys": {
|
|
618
|
+
"counters_workspace_id_key_pk": {
|
|
619
|
+
"name": "counters_workspace_id_key_pk",
|
|
620
|
+
"columns": ["workspace_id", "key"]
|
|
621
|
+
}
|
|
622
|
+
},
|
|
590
623
|
"uniqueConstraints": {},
|
|
591
624
|
"policies": {},
|
|
592
625
|
"checkConstraints": {},
|
|
@@ -972,13 +1005,7 @@
|
|
|
972
1005
|
"isRLSEnabled": false
|
|
973
1006
|
}
|
|
974
1007
|
},
|
|
975
|
-
"enums": {
|
|
976
|
-
"public.asset_status": {
|
|
977
|
-
"name": "asset_status",
|
|
978
|
-
"schema": "public",
|
|
979
|
-
"values": ["in_stock", "assigned", "under_repair", "retired"]
|
|
980
|
-
}
|
|
981
|
-
},
|
|
1008
|
+
"enums": {},
|
|
982
1009
|
"schemas": {
|
|
983
1010
|
"mod_inventory": "mod_inventory"
|
|
984
1011
|
},
|