@kernhq/module-inventory 0.1.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 (43) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +32 -0
  3. package/dist/contract.d.ts +387 -0
  4. package/dist/contract.d.ts.map +1 -0
  5. package/dist/contract.js +119 -0
  6. package/dist/contract.js.map +1 -0
  7. package/dist/server/_impl.d.ts +427 -0
  8. package/dist/server/_impl.d.ts.map +1 -0
  9. package/dist/server/_impl.js +204 -0
  10. package/dist/server/_impl.js.map +1 -0
  11. package/dist/server/index.d.ts +3 -0
  12. package/dist/server/index.d.ts.map +1 -0
  13. package/dist/server/index.js +32 -0
  14. package/dist/server/index.js.map +1 -0
  15. package/dist/server/schema.d.ts +1552 -0
  16. package/dist/server/schema.d.ts.map +1 -0
  17. package/dist/server/schema.js +175 -0
  18. package/dist/server/schema.js.map +1 -0
  19. package/drizzle.config.ts +8 -0
  20. package/migrations/0000_init.sql +129 -0
  21. package/migrations/0001_rls.sql +62 -0
  22. package/migrations/meta/0000_snapshot.json +994 -0
  23. package/migrations/meta/_journal.json +20 -0
  24. package/package.json +106 -0
  25. package/src/client/api-instance.ts +28 -0
  26. package/src/client/api.ts +10 -0
  27. package/src/client/components/AssetFormDialog.svelte +185 -0
  28. package/src/client/i18n.ts +169 -0
  29. package/src/client/index.ts +26 -0
  30. package/src/client/mock.ts +95 -0
  31. package/src/client/module.ts +96 -0
  32. package/src/client/pages/AssetsPage.svelte +266 -0
  33. package/src/client/permissions.ts +30 -0
  34. package/src/client/query.ts +12 -0
  35. package/src/client/widgets/OverviewWidget.svelte +92 -0
  36. package/src/contract.ts +143 -0
  37. package/src/module.test.ts +79 -0
  38. package/src/server/_impl.ts +275 -0
  39. package/src/server/index.ts +33 -0
  40. package/src/server/schema.ts +228 -0
  41. package/tsconfig.client.json +10 -0
  42. package/tsconfig.json +10 -0
  43. package/vitest.config.ts +5 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,175 @@
1
+ import { moduleSchema } from '@kernhq/kernel';
2
+ import { sql } from 'drizzle-orm';
3
+ import { boolean, char, date, index, integer, jsonb, pgEnum, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
4
+ /**
5
+ * This module's tables, in its own Postgres schema.
6
+ *
7
+ * Two rules, neither optional:
8
+ *
9
+ * - every tenant table carries `workspace_id` and an index that starts with it;
10
+ * - every tenant table gets a row-level security policy, hand-written in the migration, because
11
+ * drizzle-kit does not generate one. RLS is the last line — the API check is the first, and
12
+ * somebody will eventually write a query that skips it.
13
+ */
14
+ export const schema = moduleSchema('inventory');
15
+ /** Local column factories, so the conventions stay in one place. */
16
+ const id = () => uuid('id').primaryKey().default(sql `uuidv7()`);
17
+ const ws = () => uuid('workspace_id').notNull();
18
+ const ts = (name) => timestamp(name, { withTimezone: true, mode: 'date' });
19
+ const created = () => ts('created_at').notNull().defaultNow();
20
+ const updated = () => ts('updated_at').notNull().defaultNow();
21
+ /**
22
+ * Asset lifecycle. Stored rather than derived because every list filter asks for it; every
23
+ * transition is written inside the same transaction as the row it derives from.
24
+ */
25
+ export const assetStatus = pgEnum('asset_status', ['in_stock', 'assigned', 'under_repair', 'retired']);
26
+ export const counters = schema.table('counters',
27
+ /** Per-workspace sequence sources (`asset_code`). Narrow on purpose: one row per key. */
28
+ {
29
+ workspaceId: ws().primaryKey(),
30
+ key: text('key').primaryKey(),
31
+ value: integer('value').notNull(),
32
+ });
33
+ export const categories = schema.table('categories', {
34
+ id: id(),
35
+ workspaceId: ws(),
36
+ name: text('name').notNull(),
37
+ order: integer('order').notNull().default(0),
38
+ createdAt: created(),
39
+ updatedAt: updated(),
40
+ }, (t) => [
41
+ uniqueIndex('inventory_categories_ws_name_uq').on(t.workspaceId, t.name),
42
+ index('inventory_categories_ws_idx').on(t.workspaceId, t.order),
43
+ ]);
44
+ export const assets = schema.table('assets', {
45
+ id: id(),
46
+ workspaceId: ws(),
47
+ code: text('code').notNull(),
48
+ name: text('name').notNull(),
49
+ description: text('description').notNull().default(''),
50
+ categoryId: uuid('category_id'),
51
+ status: assetStatus('status').notNull().default('in_stock'),
52
+ /** Denormalized from `custody_periods`, which stays authoritative for history. */
53
+ custodianUserId: uuid('custodian_user_id'),
54
+ custodySince: ts('custody_since'),
55
+ serialNumber: text('serial_number'),
56
+ location: text('location'),
57
+ purchasedOn: date('purchased_on'),
58
+ purchasedFrom: text('purchased_from'),
59
+ priceMinor: integer('price_minor'),
60
+ currency: char('currency', { length: 3 }),
61
+ warrantyUntil: date('warranty_until'),
62
+ photoFileId: uuid('photo_file_id'),
63
+ createdAt: created(),
64
+ updatedAt: updated(),
65
+ archivedAt: ts('archived_at'),
66
+ }, (t) => [
67
+ uniqueIndex('inventory_assets_ws_code_uq').on(t.workspaceId, t.code),
68
+ index('inventory_assets_ws_created_idx').on(t.workspaceId, t.createdAt),
69
+ index('inventory_assets_ws_status_idx').on(t.workspaceId, t.status),
70
+ index('inventory_assets_ws_category_idx').on(t.workspaceId, t.categoryId),
71
+ // The "what leaves warranty this month" scan, before a job makes it a widget's cheap query.
72
+ index('inventory_assets_ws_warranty_idx')
73
+ .on(t.workspaceId, t.warrantyUntil)
74
+ .where(sql `warranty_until is not null`),
75
+ ]);
76
+ /**
77
+ * Workspace-defined extra fields, copied from tracker's `field_defs` (which cannot be shared:
78
+ * cross-schema joins are what the module boundary exists to prevent). The `key` **is** the key a
79
+ * value lives under in `assets.custom`, hence unique per workspace whatever the category scope.
80
+ */
81
+ export const fieldDefs = schema.table('field_defs', {
82
+ id: id(),
83
+ workspaceId: ws(),
84
+ categoryId: uuid('category_id'),
85
+ key: text('key').notNull(),
86
+ name: text('name').notNull(),
87
+ description: text('description'),
88
+ type: text('type').notNull(), // text | number | date | select | multiselect | checkbox | url
89
+ options: jsonb('options').$type(),
90
+ defaultValue: jsonb('default_value'),
91
+ required: boolean('required').notNull().default(false),
92
+ searchable: boolean('searchable').notNull().default(false),
93
+ showInList: boolean('show_in_list').notNull().default(false),
94
+ order: integer('order').notNull().default(0),
95
+ archivedAt: ts('archived_at'),
96
+ createdAt: created(),
97
+ updatedAt: updated(),
98
+ }, (t) => [
99
+ uniqueIndex('inventory_field_defs_ws_key_uq').on(t.workspaceId, t.key),
100
+ index('inventory_field_defs_ws_idx').on(t.workspaceId, t.order),
101
+ ]);
102
+ /**
103
+ * Custody over time. Never updated in place — a change closes the open row and inserts a new one,
104
+ * the way HR keeps employments. The exclusion constraint (hand-written migration) makes two open
105
+ * periods for one asset impossible at the database level, not merely unlikely in the service.
106
+ */
107
+ export const custodyPeriods = schema.table('custody_periods', {
108
+ id: id(),
109
+ workspaceId: ws(),
110
+ assetId: uuid('asset_id').notNull(),
111
+ userId: uuid('user_id').notNull(),
112
+ note: text('note'),
113
+ effectiveFrom: ts('effective_from').notNull().defaultNow(),
114
+ effectiveTo: ts('effective_to'),
115
+ createdBy: uuid('created_by'),
116
+ createdAt: created(),
117
+ }, (t) => [index('inventory_custody_ws_asset_idx').on(t.workspaceId, t.assetId, t.effectiveFrom)]);
118
+ /**
119
+ * Append-only. What changed, when, by whom — the timeline an asset page renders and the answer to
120
+ * "who had this laptop before me". Written inside the caller's transaction; nothing edits or
121
+ * deletes rows here, ever.
122
+ */
123
+ export const assetHistory = schema.table('asset_history', {
124
+ id: id(),
125
+ workspaceId: ws(),
126
+ assetId: uuid('asset_id').notNull(),
127
+ actorId: uuid('actor_id'),
128
+ action: text('action').notNull(), // created | updated | transferred | returned | repair_logged | repair_completed | attachment_added | retired | restored
129
+ changes: jsonb('changes').$type(),
130
+ data: jsonb('data'),
131
+ occurredAt: created(),
132
+ }, (t) => [index('inventory_asset_history_asset_idx').on(t.workspaceId, t.assetId, t.occurredAt)]);
133
+ export const repairs = schema.table('repairs', {
134
+ id: id(),
135
+ workspaceId: ws(),
136
+ assetId: uuid('asset_id').notNull(),
137
+ summary: text('summary').notNull(),
138
+ detail: text('detail'),
139
+ vendor: text('vendor'),
140
+ costMinor: integer('cost_minor'),
141
+ currency: char('currency', { length: 3 }),
142
+ sentOn: date('sent_on').notNull(),
143
+ /** Null while the item is still away — also how `under_repair` is derived. */
144
+ returnedOn: date('returned_on'),
145
+ createdBy: uuid('created_by'),
146
+ createdAt: created(),
147
+ updatedAt: updated(),
148
+ }, (t) => [index('inventory_repairs_ws_asset_idx').on(t.workspaceId, t.assetId, t.sentOn)]);
149
+ /** Bytes live in core object storage via `uploadFile`; this only records that an asset has one. */
150
+ export const attachments = schema.table('attachments', {
151
+ id: id(),
152
+ workspaceId: ws(),
153
+ assetId: uuid('asset_id').notNull(),
154
+ repairId: uuid('repair_id'),
155
+ fileId: uuid('file_id').notNull(),
156
+ name: text('name').notNull(),
157
+ mimeType: text('mime_type'),
158
+ size: integer('size'),
159
+ uploadedBy: uuid('uploaded_by'),
160
+ createdAt: created(),
161
+ }, (t) => [
162
+ uniqueIndex('inventory_attachments_asset_file_uq').on(t.assetId, t.fileId),
163
+ index('inventory_attachments_ws_asset_idx').on(t.workspaceId, t.assetId, t.createdAt),
164
+ ]);
165
+ /** Every tenant table, so the RLS migration can be checked against one list rather than memory. */
166
+ export const TENANT_TABLES = [
167
+ 'categories',
168
+ 'assets',
169
+ 'field_defs',
170
+ 'custody_periods',
171
+ 'asset_history',
172
+ 'repairs',
173
+ 'attachments',
174
+ ];
175
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+ // Generates SQL migrations for this module's own schema. Run: pnpm db:generate
3
+ export default defineConfig({
4
+ dialect: 'postgresql',
5
+ schema: './src/server/schema.ts',
6
+ out: './migrations',
7
+ schemaFilter: ['mod_inventory'],
8
+ })
@@ -0,0 +1,129 @@
1
+ -- `create schema if not exists` matters: the kernel creates `mod_inventory` before running these, so
2
+ -- the bare form fails on boot.
3
+ CREATE SCHEMA IF NOT EXISTS "mod_inventory";
4
+ --> statement-breakpoint
5
+ CREATE TYPE "public"."asset_status" AS ENUM('in_stock', 'assigned', 'under_repair', 'retired');--> statement-breakpoint
6
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."asset_history" (
7
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
8
+ "workspace_id" uuid NOT NULL,
9
+ "asset_id" uuid NOT NULL,
10
+ "actor_id" uuid,
11
+ "action" text NOT NULL,
12
+ "changes" jsonb,
13
+ "data" jsonb,
14
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."assets" (
18
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
19
+ "workspace_id" uuid NOT NULL,
20
+ "code" text NOT NULL,
21
+ "name" text NOT NULL,
22
+ "description" text DEFAULT '' NOT NULL,
23
+ "category_id" uuid,
24
+ "status" "asset_status" DEFAULT 'in_stock' NOT NULL,
25
+ "custodian_user_id" uuid,
26
+ "custody_since" timestamp with time zone,
27
+ "serial_number" text,
28
+ "location" text,
29
+ "purchased_on" date,
30
+ "purchased_from" text,
31
+ "price_minor" integer,
32
+ "currency" char(3),
33
+ "warranty_until" date,
34
+ "photo_file_id" uuid,
35
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
36
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
37
+ "archived_at" timestamp with time zone
38
+ );
39
+ --> statement-breakpoint
40
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."attachments" (
41
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
42
+ "workspace_id" uuid NOT NULL,
43
+ "asset_id" uuid NOT NULL,
44
+ "repair_id" uuid,
45
+ "file_id" uuid NOT NULL,
46
+ "name" text NOT NULL,
47
+ "mime_type" text,
48
+ "size" integer,
49
+ "uploaded_by" uuid,
50
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
51
+ );
52
+ --> statement-breakpoint
53
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."categories" (
54
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
55
+ "workspace_id" uuid NOT NULL,
56
+ "name" text NOT NULL,
57
+ "order" integer DEFAULT 0 NOT NULL,
58
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
59
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
60
+ );
61
+ --> statement-breakpoint
62
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."counters" (
63
+ "workspace_id" uuid NOT NULL,
64
+ "key" text NOT NULL,
65
+ "value" integer NOT NULL,
66
+ CONSTRAINT "counters_workspace_id_key_pk" PRIMARY KEY("workspace_id","key")
67
+ );
68
+ --> statement-breakpoint
69
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."custody_periods" (
70
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
71
+ "workspace_id" uuid NOT NULL,
72
+ "asset_id" uuid NOT NULL,
73
+ "user_id" uuid NOT NULL,
74
+ "note" text,
75
+ "effective_from" timestamp with time zone DEFAULT now() NOT NULL,
76
+ "effective_to" timestamp with time zone,
77
+ "created_by" uuid,
78
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
79
+ );
80
+ --> statement-breakpoint
81
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."field_defs" (
82
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
83
+ "workspace_id" uuid NOT NULL,
84
+ "category_id" uuid,
85
+ "key" text NOT NULL,
86
+ "name" text NOT NULL,
87
+ "description" text,
88
+ "type" text NOT NULL,
89
+ "options" jsonb,
90
+ "default_value" jsonb,
91
+ "required" boolean DEFAULT false NOT NULL,
92
+ "searchable" boolean DEFAULT false NOT NULL,
93
+ "show_in_list" boolean DEFAULT false NOT NULL,
94
+ "order" integer DEFAULT 0 NOT NULL,
95
+ "archived_at" timestamp with time zone,
96
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
97
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
98
+ );
99
+ --> statement-breakpoint
100
+ CREATE TABLE IF NOT EXISTS "mod_inventory"."repairs" (
101
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
102
+ "workspace_id" uuid NOT NULL,
103
+ "asset_id" uuid NOT NULL,
104
+ "summary" text NOT NULL,
105
+ "detail" text,
106
+ "vendor" text,
107
+ "cost_minor" integer,
108
+ "currency" char(3),
109
+ "sent_on" date NOT NULL,
110
+ "returned_on" date,
111
+ "created_by" uuid,
112
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
113
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
114
+ );
115
+ --> statement-breakpoint
116
+ CREATE INDEX IF NOT EXISTS "inventory_asset_history_asset_idx" ON "mod_inventory"."asset_history" USING btree ("workspace_id","asset_id","created_at");--> statement-breakpoint
117
+ CREATE UNIQUE INDEX IF NOT EXISTS "inventory_assets_ws_code_uq" ON "mod_inventory"."assets" USING btree ("workspace_id","code");--> statement-breakpoint
118
+ CREATE INDEX IF NOT EXISTS "inventory_assets_ws_created_idx" ON "mod_inventory"."assets" USING btree ("workspace_id","created_at");--> statement-breakpoint
119
+ CREATE INDEX IF NOT EXISTS "inventory_assets_ws_status_idx" ON "mod_inventory"."assets" USING btree ("workspace_id","status");--> statement-breakpoint
120
+ CREATE INDEX IF NOT EXISTS "inventory_assets_ws_category_idx" ON "mod_inventory"."assets" USING btree ("workspace_id","category_id");--> statement-breakpoint
121
+ CREATE INDEX IF NOT EXISTS "inventory_assets_ws_warranty_idx" ON "mod_inventory"."assets" USING btree ("workspace_id","warranty_until") WHERE warranty_until is not null;--> statement-breakpoint
122
+ CREATE UNIQUE INDEX IF NOT EXISTS "inventory_attachments_asset_file_uq" ON "mod_inventory"."attachments" USING btree ("asset_id","file_id");--> statement-breakpoint
123
+ CREATE INDEX IF NOT EXISTS "inventory_attachments_ws_asset_idx" ON "mod_inventory"."attachments" USING btree ("workspace_id","asset_id","created_at");--> statement-breakpoint
124
+ CREATE UNIQUE INDEX IF NOT EXISTS "inventory_categories_ws_name_uq" ON "mod_inventory"."categories" USING btree ("workspace_id","name");--> statement-breakpoint
125
+ CREATE INDEX IF NOT EXISTS "inventory_categories_ws_idx" ON "mod_inventory"."categories" USING btree ("workspace_id","order");--> statement-breakpoint
126
+ CREATE INDEX IF NOT EXISTS "inventory_custody_ws_asset_idx" ON "mod_inventory"."custody_periods" USING btree ("workspace_id","asset_id","effective_from");--> statement-breakpoint
127
+ CREATE UNIQUE INDEX IF NOT EXISTS "inventory_field_defs_ws_key_uq" ON "mod_inventory"."field_defs" USING btree ("workspace_id","key");--> statement-breakpoint
128
+ CREATE INDEX IF NOT EXISTS "inventory_field_defs_ws_idx" ON "mod_inventory"."field_defs" USING btree ("workspace_id","order");--> statement-breakpoint
129
+ CREATE INDEX IF NOT EXISTS "inventory_repairs_ws_asset_idx" ON "mod_inventory"."repairs" USING btree ("workspace_id","asset_id","sent_on");
@@ -0,0 +1,62 @@
1
+ -- Row-level security. Hand-written, because drizzle-kit does not generate it.
2
+ --
3
+ -- This is the last line, not the first: the API already checks membership and permission. It exists
4
+ -- for the query that skips them — a job, a report, a mistake — and for anyone who reaches the
5
+ -- database another way. A tenant table without a policy is simply readable.
6
+ --
7
+ -- `@kernhq/kernel` exports `rlsPolicySql('mod_inventory', '<table>')`, which emits exactly this
8
+ -- block per table. Superusers bypass RLS, so a database owned by one will pass a test that proves
9
+ -- nothing: run the application as a plain role.
10
+
11
+ alter table "mod_inventory"."categories" enable row level security;--> statement-breakpoint
12
+ alter table "mod_inventory"."categories" force row level security;--> statement-breakpoint
13
+ create policy "categories_ws_isolation" on "mod_inventory"."categories"
14
+ using (workspace_id::text = current_setting('app.workspace_id', true))
15
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
16
+
17
+ alter table "mod_inventory"."assets" enable row level security;--> statement-breakpoint
18
+ alter table "mod_inventory"."assets" force row level security;--> statement-breakpoint
19
+ create policy "assets_ws_isolation" on "mod_inventory"."assets"
20
+ using (workspace_id::text = current_setting('app.workspace_id', true))
21
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
22
+
23
+ alter table "mod_inventory"."field_defs" enable row level security;--> statement-breakpoint
24
+ alter table "mod_inventory"."field_defs" force row level security;--> statement-breakpoint
25
+ create policy "field_defs_ws_isolation" on "mod_inventory"."field_defs"
26
+ using (workspace_id::text = current_setting('app.workspace_id', true))
27
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
28
+
29
+ alter table "mod_inventory"."custody_periods" enable row level security;--> statement-breakpoint
30
+ alter table "mod_inventory"."custody_periods" force row level security;--> statement-breakpoint
31
+ create policy "custody_periods_ws_isolation" on "mod_inventory"."custody_periods"
32
+ using (workspace_id::text = current_setting('app.workspace_id', true))
33
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
34
+
35
+ alter table "mod_inventory"."asset_history" enable row level security;--> statement-breakpoint
36
+ alter table "mod_inventory"."asset_history" force row level security;--> statement-breakpoint
37
+ create policy "asset_history_ws_isolation" on "mod_inventory"."asset_history"
38
+ using (workspace_id::text = current_setting('app.workspace_id', true))
39
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
40
+
41
+ alter table "mod_inventory"."repairs" enable row level security;--> statement-breakpoint
42
+ alter table "mod_inventory"."repairs" force row level security;--> statement-breakpoint
43
+ create policy "repairs_ws_isolation" on "mod_inventory"."repairs"
44
+ using (workspace_id::text = current_setting('app.workspace_id', true))
45
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
46
+
47
+ alter table "mod_inventory"."attachments" enable row level security;--> statement-breakpoint
48
+ alter table "mod_inventory"."attachments" force row level security;--> statement-breakpoint
49
+ create policy "attachments_ws_isolation" on "mod_inventory"."attachments"
50
+ using (workspace_id::text = current_setting('app.workspace_id', true))
51
+ with check (workspace_id::text = current_setting('app.workspace_id', true));
52
+
53
+ -- Custody is effective-dated: a change closes the open row and inserts a new one. This constraint
54
+ -- is what makes two overlapping custody periods for one asset impossible at the database level —
55
+ -- two concurrent transfers cannot both win. `effective_to` null means "still open", which tstzrange
56
+ -- treats as unbounded, so an open period conflicts with everything that would follow it.
57
+ alter table "mod_inventory"."custody_periods"
58
+ add constraint "inventory_custody_no_overlap"
59
+ exclude using gist (
60
+ asset_id with =,
61
+ tstzrange(effective_from, effective_to, '[)') with &&
62
+ );