@kernhq/module-inventory 0.1.2 → 0.2.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 (103) hide show
  1. package/README.md +55 -9
  2. package/dist/contract/capabilities.d.ts +49 -0
  3. package/dist/contract/capabilities.d.ts.map +1 -0
  4. package/dist/contract/capabilities.js +52 -0
  5. package/dist/contract/capabilities.js.map +1 -0
  6. package/dist/contract/events.d.ts +33 -0
  7. package/dist/contract/events.d.ts.map +1 -0
  8. package/dist/contract/events.js +22 -0
  9. package/dist/contract/events.js.map +1 -0
  10. package/dist/contract/index.d.ts +15 -0
  11. package/dist/contract/index.d.ts.map +1 -0
  12. package/dist/contract/index.js +15 -0
  13. package/dist/contract/index.js.map +1 -0
  14. package/dist/contract/models.d.ts +146 -0
  15. package/dist/contract/models.d.ts.map +1 -0
  16. package/dist/contract/models.js +107 -0
  17. package/dist/contract/models.js.map +1 -0
  18. package/dist/contract/permissions.d.ts +22 -0
  19. package/dist/contract/permissions.d.ts.map +1 -0
  20. package/dist/contract/permissions.js +26 -0
  21. package/dist/contract/permissions.js.map +1 -0
  22. package/dist/{contract.d.ts → contract/router.d.ts} +24 -85
  23. package/dist/contract/router.d.ts.map +1 -0
  24. package/dist/contract/router.js +42 -0
  25. package/dist/contract/router.js.map +1 -0
  26. package/dist/contract/settings.d.ts +18 -0
  27. package/dist/contract/settings.d.ts.map +1 -0
  28. package/dist/contract/settings.js +29 -0
  29. package/dist/contract/settings.js.map +1 -0
  30. package/dist/server/index.d.ts +4 -1
  31. package/dist/server/index.d.ts.map +1 -1
  32. package/dist/server/index.js +9 -13
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/server/{_impl.d.ts → router.d.ts} +33 -8
  35. package/dist/server/{_impl.d.ts.map → router.d.ts.map} +1 -1
  36. package/dist/server/router.js +83 -0
  37. package/dist/server/router.js.map +1 -0
  38. package/dist/server/schema.d.ts +25 -11
  39. package/dist/server/schema.d.ts.map +1 -1
  40. package/dist/server/schema.js +28 -10
  41. package/dist/server/schema.js.map +1 -1
  42. package/dist/server/services/assets.d.ts +73 -0
  43. package/dist/server/services/assets.d.ts.map +1 -0
  44. package/dist/server/services/assets.js +261 -0
  45. package/dist/server/services/assets.js.map +1 -0
  46. package/dist/server/services/index.d.ts +10 -0
  47. package/dist/server/services/index.d.ts.map +1 -0
  48. package/dist/server/services/index.js +15 -0
  49. package/dist/server/services/index.js.map +1 -0
  50. package/dist/server/services/notify.d.ts +63 -0
  51. package/dist/server/services/notify.d.ts.map +1 -0
  52. package/dist/server/services/notify.js +105 -0
  53. package/dist/server/services/notify.js.map +1 -0
  54. package/migrations/0000_init.sql +12 -3
  55. package/migrations/0001_rls.sql +24 -0
  56. package/migrations/meta/0000_snapshot.json +40 -13
  57. package/migrations/meta/_journal.json +2 -2
  58. package/package.json +5 -4
  59. package/src/client/api.ts +1 -1
  60. package/src/client/components/AssetFormDialog.svelte +130 -47
  61. package/src/client/i18n.ts +11 -166
  62. package/src/client/index.ts +8 -1
  63. package/src/client/messages.test.ts +169 -0
  64. package/src/client/messages.ts +399 -0
  65. package/src/client/mock.test.ts +161 -0
  66. package/src/client/mock.ts +267 -45
  67. package/src/client/module.ts +22 -2
  68. package/src/client/pages/AssetsPage.svelte +358 -137
  69. package/src/client/permissions.ts +1 -1
  70. package/src/client/price.test.ts +106 -0
  71. package/src/client/price.ts +135 -0
  72. package/src/client/query.test.ts +58 -0
  73. package/src/client/query.ts +15 -2
  74. package/src/client/settings/GeneralSettings.svelte +0 -0
  75. package/src/client/settings/core-api.ts +32 -0
  76. package/src/client/widgets/OverviewWidget.svelte +16 -3
  77. package/src/contract/capabilities.ts +55 -0
  78. package/src/contract/events.ts +34 -0
  79. package/src/contract/index.ts +15 -0
  80. package/src/contract/models.ts +123 -0
  81. package/src/contract/permissions.ts +26 -0
  82. package/src/contract/router.ts +46 -0
  83. package/src/contract/settings.ts +30 -0
  84. package/src/module.test.ts +140 -7
  85. package/src/server/index.ts +16 -13
  86. package/src/server/inventory.int.test.ts +819 -0
  87. package/src/server/migrations.test.ts +138 -0
  88. package/src/server/router.ts +118 -0
  89. package/src/server/schema.ts +27 -10
  90. package/src/server/services/assets.ts +368 -0
  91. package/src/server/services/index.ts +23 -0
  92. package/src/server/services/notify.ts +151 -0
  93. package/tsconfig.base.json +22 -0
  94. package/tsconfig.client.json +1 -1
  95. package/tsconfig.json +1 -1
  96. package/vitest.config.ts +18 -3
  97. package/dist/contract.d.ts.map +0 -1
  98. package/dist/contract.js +0 -119
  99. package/dist/contract.js.map +0 -1
  100. package/dist/server/_impl.js +0 -204
  101. package/dist/server/_impl.js.map +0 -1
  102. package/src/contract.ts +0 -143
  103. package/src/server/_impl.ts +0 -275
@@ -0,0 +1,83 @@
1
+ import { defineModule, defineServerModule, packageVersion, requires, workspaceScoped, } from '@kernhq/kernel';
2
+ import { implement } from '@orpc/server';
3
+ import { inventoryContract, inventoryEvents, MODULE_ID } from '../contract/index.js';
4
+ import { toAsset } from './services/assets.js';
5
+ import { inventoryServices } from './services/index.js';
6
+ /**
7
+ * oRPC router for `/api/inventory`.
8
+ *
9
+ * Deliberately thin: it opens the workspace-bound transaction and hands straight over to a service,
10
+ * which is where the logic lives. Kept apart from `index.ts` so `module.test.ts` can walk it as data
11
+ * without booting a kernel.
12
+ *
13
+ * Two middlewares on every procedure, and the test fails if either is missing: `workspaceScoped`
14
+ * (a real membership, and the module switched on for that workspace) and `requires` (the permission
15
+ * this particular call needs). A capability adds a third, between them.
16
+ */
17
+ export { defineModule, defineServerModule, packageVersion };
18
+ const os = implement(inventoryContract).$context();
19
+ export function inventoryRouter(kernel) {
20
+ const svc = inventoryServices(kernel);
21
+ const scoped = os.use(workspaceScoped(MODULE_ID));
22
+ /**
23
+ * Everything a write announces, once the transaction that wrote it has actually committed.
24
+ *
25
+ * Three things, and all three are after the commit on purpose: the core activity feed's copy of
26
+ * the history row, the event for whatever reacts later, and the realtime change that redraws a
27
+ * screen somebody is looking at now. A mutation that skips them leaves the product believing the
28
+ * old answer; a mutation that fires them *inside* the transaction tells the rest of the instance
29
+ * about a row a rollback then took away, which is worse — it cannot be retracted.
30
+ */
31
+ const announce = async (workspaceId, written, event, op, actorId) => {
32
+ const assetId = written.row.id;
33
+ if (written.activity)
34
+ await svc.notify.activity(written.activity);
35
+ await kernel.emit(event, { assetId, workspaceId }, { workspaceId, actorId: actorId ?? undefined });
36
+ await svc.notify.change(workspaceId, 'asset', assetId, op);
37
+ };
38
+ /** Every handler is this: a workspace-bound transaction, tagged with who is asking, and a service. */
39
+ const run = (context, workspaceId, fn) => kernel.database.withWorkspace(workspaceId, fn, { userId: context.principal.userId });
40
+ return os.router({
41
+ assets: {
42
+ list: scoped.assets.list
43
+ .use(requires('inventory.asset.view'))
44
+ .handler(({ input, context }) => run(context, input.workspaceId, (tx) => svc.assets.list(tx, input))),
45
+ get: scoped.assets.get
46
+ .use(requires('inventory.asset.view'))
47
+ .handler(({ input, context }) => run(context, input.workspaceId, async (tx) => toAsset(await svc.assets.get(tx, input.workspaceId, input.assetId)))),
48
+ create: scoped.assets.create
49
+ .use(requires('inventory.asset.manage'))
50
+ .handler(async ({ input, context }) => {
51
+ const actorId = context.principal.userId;
52
+ // Module settings are read *before* the transaction opens. They come from core over the
53
+ // broker, and awaiting that inside the transaction holds a pooled connection and the
54
+ // counter row lock across a network call to another service.
55
+ const format = await svc.assets.codeFormat(input.workspaceId);
56
+ const written = await run(context, input.workspaceId, (tx) => svc.assets.create(tx, input.workspaceId, actorId, input, format));
57
+ await announce(input.workspaceId, written, inventoryEvents.assetCreated, 'created', actorId);
58
+ return toAsset(written.row);
59
+ }),
60
+ update: scoped.assets.update
61
+ .use(requires('inventory.asset.manage'))
62
+ .handler(async ({ input, context }) => {
63
+ const actorId = context.principal.userId;
64
+ const written = await run(context, input.workspaceId, (tx) => svc.assets.update(tx, input.workspaceId, actorId, input.assetId, input));
65
+ await announce(input.workspaceId, written, inventoryEvents.assetUpdated, 'updated', actorId);
66
+ return toAsset(written.row);
67
+ }),
68
+ archive: scoped.assets.archive
69
+ .use(requires('inventory.asset.manage'))
70
+ .handler(async ({ input, context }) => {
71
+ const actorId = context.principal.userId;
72
+ const written = await run(context, input.workspaceId, (tx) => svc.assets.archive(tx, input.workspaceId, actorId, input.assetId, input.archived));
73
+ // This procedure is the restore path too, and it used to announce `archived` either way —
74
+ // telling every subscriber that a restored item had just been retired, while the module's
75
+ // own history row beside it said `restored`.
76
+ const event = input.archived ? inventoryEvents.assetArchived : inventoryEvents.assetRestored;
77
+ await announce(input.workspaceId, written, event, 'updated', actorId);
78
+ return toAsset(written.row);
79
+ }),
80
+ },
81
+ });
82
+ }
83
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"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"}
@@ -9,11 +9,6 @@
9
9
  * somebody will eventually write a query that skips it.
10
10
  */
11
11
  export declare const schema: import("drizzle-orm/pg-core").PgSchema<string>;
12
- /**
13
- * Asset lifecycle. Stored rather than derived because every list filter asks for it; every
14
- * transition is written inside the same transaction as the row it derives from.
15
- */
16
- export declare const assetStatus: import("drizzle-orm/pg-core").PgEnum<["in_stock", "assigned", "under_repair", "retired"]>;
17
12
  export declare const counters: import("drizzle-orm/pg-core").PgTableWithColumns<{
18
13
  name: "counters";
19
14
  schema: string;
@@ -27,7 +22,7 @@ export declare const counters: import("drizzle-orm/pg-core").PgTableWithColumns<
27
22
  driverParam: string;
28
23
  notNull: true;
29
24
  hasDefault: false;
30
- isPrimaryKey: true;
25
+ isPrimaryKey: false;
31
26
  isAutoincrement: false;
32
27
  hasRuntimeDefault: false;
33
28
  enumValues: undefined;
@@ -44,7 +39,7 @@ export declare const counters: import("drizzle-orm/pg-core").PgTableWithColumns<
44
39
  driverParam: string;
45
40
  notNull: true;
46
41
  hasDefault: false;
47
- isPrimaryKey: true;
42
+ isPrimaryKey: false;
48
43
  isAutoincrement: false;
49
44
  hasRuntimeDefault: false;
50
45
  enumValues: [string, ...string[]];
@@ -291,15 +286,15 @@ export declare const assets: import("drizzle-orm/pg-core").PgTableWithColumns<{
291
286
  name: "status";
292
287
  tableName: "assets";
293
288
  dataType: "string";
294
- columnType: "PgEnumColumn";
295
- data: "in_stock" | "assigned" | "under_repair" | "retired";
289
+ columnType: "PgText";
290
+ data: string;
296
291
  driverParam: string;
297
292
  notNull: true;
298
293
  hasDefault: true;
299
294
  isPrimaryKey: false;
300
295
  isAutoincrement: false;
301
296
  hasRuntimeDefault: false;
302
- enumValues: ["in_stock", "assigned", "under_repair", "retired"];
297
+ enumValues: [string, ...string[]];
303
298
  baseColumn: never;
304
299
  identity: undefined;
305
300
  generated: undefined;
@@ -476,6 +471,25 @@ export declare const assets: import("drizzle-orm/pg-core").PgTableWithColumns<{
476
471
  identity: undefined;
477
472
  generated: undefined;
478
473
  }, {}, {}>;
474
+ custom: import("drizzle-orm/pg-core").PgColumn<{
475
+ name: "custom";
476
+ tableName: "assets";
477
+ dataType: "json";
478
+ columnType: "PgJsonb";
479
+ data: Record<string, unknown>;
480
+ driverParam: unknown;
481
+ notNull: true;
482
+ hasDefault: true;
483
+ isPrimaryKey: false;
484
+ isAutoincrement: false;
485
+ hasRuntimeDefault: false;
486
+ enumValues: undefined;
487
+ baseColumn: never;
488
+ identity: undefined;
489
+ generated: undefined;
490
+ }, {}, {
491
+ $type: Record<string, unknown>;
492
+ }>;
479
493
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
480
494
  name: string;
481
495
  tableName: "assets";
@@ -1548,5 +1562,5 @@ export declare const attachments: import("drizzle-orm/pg-core").PgTableWithColum
1548
1562
  dialect: "pg";
1549
1563
  }>;
1550
1564
  /** Every tenant table, so the RLS migration can be checked against one list rather than memory. */
1551
- export declare const TENANT_TABLES: readonly ["categories", "assets", "field_defs", "custody_periods", "asset_history", "repairs", "attachments"];
1565
+ export declare const TENANT_TABLES: readonly ["counters", "categories", "assets", "field_defs", "custody_periods", "asset_history", "repairs", "attachments"];
1552
1566
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM,gDAA4B,CAAA;AAS/C,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiBpB,CAAA;AAED,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EActB,CAAA;AAED,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4ClB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBrB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAc1B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAQoB,MAAM;sBAAQ,OAAO;oBAAM,OAAO;;;;;;;;;;;;;;uBAAlC,MAAM;sBAAQ,OAAO;oBAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK9E,CAAA;AAED,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBnB,CAAA;AAED,mGAAmG;AACnG,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBvB,CAAA;AAED,mGAAmG;AACnG,eAAO,MAAM,aAAa,2HAahB,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { moduleSchema } from '@kernhq/kernel';
2
2
  import { sql } from 'drizzle-orm';
3
- import { boolean, char, date, index, integer, jsonb, pgEnum, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
3
+ import { boolean, char, date, index, integer, jsonb, primaryKey, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
4
4
  /**
5
5
  * This module's tables, in its own Postgres schema.
6
6
  *
@@ -18,18 +18,22 @@ const ws = () => uuid('workspace_id').notNull();
18
18
  const ts = (name) => timestamp(name, { withTimezone: true, mode: 'date' });
19
19
  const created = () => ts('created_at').notNull().defaultNow();
20
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
21
  export const counters = schema.table('counters',
27
22
  /** Per-workspace sequence sources (`asset_code`). Narrow on purpose: one row per key. */
28
23
  {
29
- workspaceId: ws().primaryKey(),
30
- key: text('key').primaryKey(),
24
+ workspaceId: ws(),
25
+ key: text('key').notNull(),
31
26
  value: integer('value').notNull(),
32
- });
27
+ },
28
+ /**
29
+ * Composite, declared here rather than as two `.primaryKey()` columns.
30
+ *
31
+ * Column-level `.primaryKey()` twice reads like a compound key and is not one: drizzle emits
32
+ * `PRIMARY KEY` on both columns and Postgres refuses the table outright — "multiple primary keys
33
+ * for table are not allowed", SQLSTATE 42P16. The module's migration is the first thing the
34
+ * kernel runs, so the failure is not a broken table but a service that will not boot.
35
+ */
36
+ (t) => [primaryKey({ columns: [t.workspaceId, t.key] })]);
33
37
  export const categories = schema.table('categories', {
34
38
  id: id(),
35
39
  workspaceId: ws(),
@@ -48,7 +52,10 @@ export const assets = schema.table('assets', {
48
52
  name: text('name').notNull(),
49
53
  description: text('description').notNull().default(''),
50
54
  categoryId: uuid('category_id'),
51
- status: assetStatus('status').notNull().default('in_stock'),
55
+ /** `AssetStatus` in the contract. Text, not a pg enum: every other Kern module stores a
56
+ * status this way, and an enum named `asset_status` in `public` is a type this module
57
+ * leaves behind when it is removed. */
58
+ status: text('status').notNull().default('in_stock'),
52
59
  /** Denormalized from `custody_periods`, which stays authoritative for history. */
53
60
  custodianUserId: uuid('custodian_user_id'),
54
61
  custodySince: ts('custody_since'),
@@ -60,6 +67,8 @@ export const assets = schema.table('assets', {
60
67
  currency: char('currency', { length: 3 }),
61
68
  warrantyUntil: date('warranty_until'),
62
69
  photoFileId: uuid('photo_file_id'),
70
+ /** Values for this workspace's own `field_defs`, keyed by their `key`. */
71
+ custom: jsonb('custom').$type().notNull().default({}),
63
72
  createdAt: created(),
64
73
  updatedAt: updated(),
65
74
  archivedAt: ts('archived_at'),
@@ -68,6 +77,10 @@ export const assets = schema.table('assets', {
68
77
  index('inventory_assets_ws_created_idx').on(t.workspaceId, t.createdAt),
69
78
  index('inventory_assets_ws_status_idx').on(t.workspaceId, t.status),
70
79
  index('inventory_assets_ws_category_idx').on(t.workspaceId, t.categoryId),
80
+ // "What is Ada holding?" — asked by the person, by the offboarding hook, and by a widget.
81
+ index('inventory_assets_ws_custodian_idx')
82
+ .on(t.workspaceId, t.custodianUserId)
83
+ .where(sql `custodian_user_id is not null`),
71
84
  // The "what leaves warranty this month" scan, before a job makes it a widget's cheap query.
72
85
  index('inventory_assets_ws_warranty_idx')
73
86
  .on(t.workspaceId, t.warrantyUntil)
@@ -164,6 +177,11 @@ export const attachments = schema.table('attachments', {
164
177
  ]);
165
178
  /** Every tenant table, so the RLS migration can be checked against one list rather than memory. */
166
179
  export const TENANT_TABLES = [
180
+ // `counters` is a tenant table like any other: it carries `workspace_id`, so one workspace's
181
+ // asset-code sequence is readable to another without a policy. It was left out of this list —
182
+ // and therefore out of `0001_rls.sql` — because it holds no asset data, which is not the rule
183
+ // the file states at the top. Tracker's structurally identical `issue_counters` is covered.
184
+ 'counters',
167
185
  'categories',
168
186
  'assets',
169
187
  'field_defs',
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { type Kernel, type Tx } from '@kernhq/kernel';
2
+ import type { AssetCreateInput, Asset as AssetModel, AssetPatchInput, AssetSort } from '../../contract/models.js';
3
+ import { assets } from '../schema.js';
4
+ import type { HistoryInput, NotifyService } from './notify.js';
5
+ type Row = typeof assets.$inferSelect;
6
+ /**
7
+ * What a mutation wrote, and what may only leave the module once it has committed.
8
+ *
9
+ * `asset_history` is written inside the caller's transaction and is authoritative; the mirror of it
10
+ * in core's activity feed is not, and used to be fired off with `void` while the transaction was
11
+ * still open. A rollback then left the workspace's feed showing an event for an id that does not
12
+ * exist. Everything else this module announces — the event, the realtime change — already waits for
13
+ * the commit, so the activity record is handed back here and flushed beside them.
14
+ */
15
+ export interface Written {
16
+ row: Row;
17
+ /** `null` when nothing happened worth recording, which is what an update that changed nothing is. */
18
+ activity: HistoryInput | null;
19
+ }
20
+ /** How this workspace spells an asset tag. Read before the transaction opens — see `codeFormat`. */
21
+ export interface CodeFormat {
22
+ prefix: string;
23
+ pad: number;
24
+ }
25
+ /** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
26
+ export declare function toAsset(row: Row): AssetModel;
27
+ export interface ListInput {
28
+ workspaceId: string;
29
+ limit: number;
30
+ cursor?: string;
31
+ q?: string;
32
+ categoryId?: string;
33
+ status?: string;
34
+ custodianUserId?: string;
35
+ archived: boolean;
36
+ sort: AssetSort;
37
+ }
38
+ export declare class AssetService {
39
+ private readonly kernel;
40
+ private readonly notify;
41
+ constructor(kernel: Kernel, notify: NotifyService);
42
+ /**
43
+ * How this workspace spells an asset tag.
44
+ *
45
+ * **Called before the transaction opens, never inside one.** `kernel.settings.module` is a
46
+ * `core.settings.getModule` call over the broker, and awaiting a remote service while holding a
47
+ * pooled connection *and* the counter row lock is two failures waiting: the pool starves under
48
+ * concurrent creates, and a create fails outright whenever core is briefly away — which is the
49
+ * one thing `NotifyService`'s own docblock says a mutation here must never do.
50
+ */
51
+ codeFormat(workspaceId: string): Promise<CodeFormat>;
52
+ /**
53
+ * The next asset tag for a workspace.
54
+ *
55
+ * One narrow row per workspace and key, incremented under the insert's conflict lock — two
56
+ * concurrent creates each read the value their own statement returned, so codes stay unique
57
+ * without a retry loop. `INV-0042`, because people say asset tags out loud. Nothing in here
58
+ * leaves the database, which is what keeps the lock short.
59
+ */
60
+ nextCode(tx: Tx, workspaceId: string, format: CodeFormat): Promise<string>;
61
+ list(tx: Tx, input: ListInput): Promise<{
62
+ items: AssetModel[];
63
+ nextCursor: string | null;
64
+ }>;
65
+ get(tx: Tx, workspaceId: string, assetId: string): Promise<Row>;
66
+ create(tx: Tx, workspaceId: string, actorId: string | null, input: AssetCreateInput, format: CodeFormat): Promise<Written>;
67
+ /** Fields a person edits directly, and therefore the fields the timeline reports a diff for. */
68
+ private static readonly EDITABLE;
69
+ update(tx: Tx, workspaceId: string, actorId: string | null, assetId: string, input: AssetPatchInput): Promise<Written>;
70
+ archive(tx: Tx, workspaceId: string, actorId: string | null, assetId: string, archived: boolean): Promise<Written>;
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../../src/server/services/assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAGxE,OAAO,KAAK,EACV,gBAAgB,EAChB,KAAK,IAAI,UAAU,EACnB,eAAe,EACf,SAAS,EACV,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAAE,MAAM,EAAY,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE9D,KAAK,GAAG,GAAG,OAAO,MAAM,CAAC,YAAY,CAAA;AAErC;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,GAAG,CAAA;IACR,qGAAqG;IACrG,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;CAC9B;AAED,oGAAoG;AACpG,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,oGAAoG;AACpG,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,UAAU,CAwB5C;AAgED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,IAAI,EAAE,SAAS,CAAA;CAChB;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa;IAGxC;;;;;;;;OAQG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAK1D;;;;;;;OAOG;IACG,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1E,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAoE3F,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAS/D,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,OAAO,CAAC;IAyBnB,gGAAgG;IAChG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAYtB;IAEJ,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,OAAO,CAAC;IAuCb,OAAO,CACX,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,OAAO,CAAC;CAgBpB"}
@@ -0,0 +1,261 @@
1
+ import { KernError, uuidv7 } from '@kernhq/kernel';
2
+ import { and, asc, desc, eq, ilike, isNull, or, sql } from 'drizzle-orm';
3
+ import { alias } from 'drizzle-orm/pg-core';
4
+ import { InventorySettings } from '../../contract/settings.js';
5
+ import { assets, counters } from '../schema.js';
6
+ /** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
7
+ export function toAsset(row) {
8
+ return {
9
+ id: row.id,
10
+ workspaceId: row.workspaceId,
11
+ code: row.code,
12
+ name: row.name,
13
+ description: row.description,
14
+ categoryId: row.categoryId,
15
+ status: row.status,
16
+ custodianUserId: row.custodianUserId,
17
+ custodySince: row.custodySince?.toISOString() ?? null,
18
+ serialNumber: row.serialNumber,
19
+ location: row.location,
20
+ purchasedOn: row.purchasedOn ?? null,
21
+ purchasedFrom: row.purchasedFrom,
22
+ priceMinor: row.priceMinor,
23
+ currency: row.currency,
24
+ warrantyUntil: row.warrantyUntil ?? null,
25
+ photoFileId: row.photoFileId,
26
+ custom: row.custom ?? {},
27
+ createdAt: row.createdAt.toISOString(),
28
+ updatedAt: row.updatedAt.toISOString(),
29
+ archivedAt: row.archivedAt?.toISOString() ?? null,
30
+ };
31
+ }
32
+ const encode = (b) => Buffer.from(JSON.stringify(b), 'utf8').toString('base64url');
33
+ /** Cheap and total, and it runs before the value can reach a `::uuid` cast. */
34
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
35
+ function decode(cursor, sort) {
36
+ const refuse = () => KernError.badRequest('That page marker is not one this list issued');
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
40
+ }
41
+ catch {
42
+ throw refuse();
43
+ }
44
+ if (typeof parsed?.i !== 'string' || !UUID.test(parsed.i))
45
+ throw refuse();
46
+ // One comparison covers both a sort this list never issues and a sort it issued under a
47
+ // *different* request. Either way the bookmark cannot be read against the ordering asked for,
48
+ // and pretending otherwise is what made "Load more" loop for ever.
49
+ if (parsed.s !== sort)
50
+ throw refuse();
51
+ return { i: parsed.i, s: sort };
52
+ }
53
+ /**
54
+ * `%`, `_` and `\` are pattern syntax to `ilike`, not text.
55
+ *
56
+ * Unescaped, a search for `50%` matched every row in the workspace and a search for `_hair` matched
57
+ * "Chair" — both of which read as a broken search rather than as a clever one. Escaped rather than
58
+ * stripped: somebody typing a per-cent sign means the character.
59
+ */
60
+ const contains = (q) => `%${q.replace(/[\\%_]/g, '\\$&')}%`;
61
+ /** Which column an ordering sorts on, for the list itself and for the bookmarked row alike. */
62
+ const sortKey = (t, sort) => sort === 'name' ? t.name : sort === 'code' ? t.code : t.id;
63
+ export class AssetService {
64
+ kernel;
65
+ notify;
66
+ constructor(kernel, notify) {
67
+ this.kernel = kernel;
68
+ this.notify = notify;
69
+ }
70
+ /**
71
+ * How this workspace spells an asset tag.
72
+ *
73
+ * **Called before the transaction opens, never inside one.** `kernel.settings.module` is a
74
+ * `core.settings.getModule` call over the broker, and awaiting a remote service while holding a
75
+ * pooled connection *and* the counter row lock is two failures waiting: the pool starves under
76
+ * concurrent creates, and a create fails outright whenever core is briefly away — which is the
77
+ * one thing `NotifyService`'s own docblock says a mutation here must never do.
78
+ */
79
+ async codeFormat(workspaceId) {
80
+ const settings = await this.kernel.settings.module(workspaceId, 'inventory', InventorySettings);
81
+ return { prefix: settings.assetCodePrefix, pad: settings.assetCodePad };
82
+ }
83
+ /**
84
+ * The next asset tag for a workspace.
85
+ *
86
+ * One narrow row per workspace and key, incremented under the insert's conflict lock — two
87
+ * concurrent creates each read the value their own statement returned, so codes stay unique
88
+ * without a retry loop. `INV-0042`, because people say asset tags out loud. Nothing in here
89
+ * leaves the database, which is what keeps the lock short.
90
+ */
91
+ async nextCode(tx, workspaceId, format) {
92
+ const [row] = await tx
93
+ .insert(counters)
94
+ .values({ workspaceId, key: 'asset_code', value: 1 })
95
+ .onConflictDoUpdate({
96
+ target: [counters.workspaceId, counters.key],
97
+ set: { value: sql `${counters.value} + 1` },
98
+ })
99
+ .returning();
100
+ return `${format.prefix}${String(row.value).padStart(format.pad, '0')}`;
101
+ }
102
+ async list(tx, input) {
103
+ const filters = [eq(assets.workspaceId, input.workspaceId)];
104
+ if (input.q) {
105
+ // A code is something somebody reads off a sticker; matching name, code and serial loosely
106
+ // matters more than word-splitting here. Full-text arrives with the core indexer.
107
+ const pattern = contains(input.q);
108
+ filters.push(or(ilike(assets.name, pattern), ilike(assets.code, pattern), ilike(sql `coalesce(${assets.serialNumber}, '')`, pattern)));
109
+ }
110
+ if (input.categoryId)
111
+ filters.push(eq(assets.categoryId, input.categoryId));
112
+ if (input.status)
113
+ filters.push(eq(assets.status, input.status));
114
+ if (input.custodianUserId)
115
+ filters.push(eq(assets.custodianUserId, input.custodianUserId));
116
+ if (!input.archived)
117
+ filters.push(isNull(assets.archivedAt));
118
+ // `recent` sorts by id rather than by `created_at`: ids are uuidv7, so they carry the clock,
119
+ // and one indexed unique column is a cheaper and unambiguous page boundary than a timestamp
120
+ // two rows can share. Ordered to the millisecond and no finer — the kernel's `uuidv7()` fills
121
+ // its last ten bytes from `randomUUID()` with no counter — so two assets created inside one
122
+ // millisecond come back in a stable but arbitrary order relative to each other. That is fine
123
+ // for a list and fatal for a test that expects creation order; see `inventory.int.test.ts`.
124
+ const column = sortKey(assets, input.sort);
125
+ const descending = input.sort === 'recent';
126
+ if (input.cursor) {
127
+ const mark = decode(input.cursor, input.sort);
128
+ /**
129
+ * The sort key comes back out of the bookmarked row rather than out of the cursor, which is
130
+ * what keeps the cursor small and stops it disagreeing with the row it names. Scoped by
131
+ * `workspace_id` as well as by id, so a cursor cannot be used to probe another workspace's
132
+ * ordering — this module reaches the database as a superuser in `core`, with RLS bypassed.
133
+ *
134
+ * A row deleted between two pages leaves the subquery empty, the row comparison NULL and the
135
+ * page empty: the list simply ends. That is deliberate, and preferred to a 400 — somebody
136
+ * else archiving a row while you read is an ordinary race, not a malformed request, and an
137
+ * error toast on "Load more" would be a worse answer than a list that has run out.
138
+ */
139
+ const marker = alias(assets, 'page_marker');
140
+ const bookmarked = tx
141
+ .select({ key: sortKey(marker, input.sort), id: marker.id })
142
+ .from(marker)
143
+ .where(and(eq(marker.id, mark.i), eq(marker.workspaceId, input.workspaceId)));
144
+ filters.push(descending
145
+ ? sql `(${column}, ${assets.id}) < (${bookmarked})`
146
+ : sql `(${column}, ${assets.id}) > (${bookmarked})`);
147
+ }
148
+ const order = descending ? [desc(column), desc(assets.id)] : [asc(column), asc(assets.id)];
149
+ const rows = await tx
150
+ .select()
151
+ .from(assets)
152
+ .where(and(...filters))
153
+ .orderBy(...order)
154
+ .limit(input.limit + 1);
155
+ const items = rows.slice(0, input.limit);
156
+ const last = items.at(-1);
157
+ const nextCursor = rows.length > input.limit && last ? encode({ i: last.id, s: input.sort }) : null;
158
+ return { items: items.map(toAsset), nextCursor };
159
+ }
160
+ async get(tx, workspaceId, assetId) {
161
+ const [row] = await tx
162
+ .select()
163
+ .from(assets)
164
+ .where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)));
165
+ if (!row)
166
+ throw KernError.notFound('Asset');
167
+ return row;
168
+ }
169
+ async create(tx, workspaceId, actorId, input, format) {
170
+ const [row] = await tx
171
+ .insert(assets)
172
+ .values({
173
+ id: uuidv7(),
174
+ workspaceId,
175
+ code: await this.nextCode(tx, workspaceId, format),
176
+ name: input.name,
177
+ description: input.description,
178
+ categoryId: input.categoryId ?? null,
179
+ serialNumber: input.serialNumber ?? null,
180
+ location: input.location ?? null,
181
+ purchasedFrom: input.purchasedFrom ?? null,
182
+ purchasedOn: input.purchasedOn ?? null,
183
+ warrantyUntil: input.warrantyUntil ?? null,
184
+ priceMinor: input.priceMinor ?? null,
185
+ currency: input.currency ?? null,
186
+ photoFileId: input.photoFileId ?? null,
187
+ })
188
+ .returning();
189
+ const activity = { workspaceId, assetId: row.id, actorId, action: 'created' };
190
+ await this.notify.history(tx, activity);
191
+ return { row: row, activity };
192
+ }
193
+ /** Fields a person edits directly, and therefore the fields the timeline reports a diff for. */
194
+ static EDITABLE = [
195
+ 'name',
196
+ 'description',
197
+ 'categoryId',
198
+ 'serialNumber',
199
+ 'location',
200
+ 'purchasedFrom',
201
+ 'purchasedOn',
202
+ 'warrantyUntil',
203
+ 'priceMinor',
204
+ 'currency',
205
+ 'photoFileId',
206
+ ];
207
+ async update(tx, workspaceId, actorId, assetId, input) {
208
+ const [prev] = await tx
209
+ .select()
210
+ .from(assets)
211
+ .where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
212
+ .for('update');
213
+ if (!prev)
214
+ throw KernError.notFound('Asset');
215
+ // `undefined` means "not mentioned"; `null` means "clear it". Collapsing the two is how an
216
+ // edit of one field quietly wipes the others.
217
+ const patch = { updatedAt: new Date() };
218
+ for (const field of AssetService.EDITABLE) {
219
+ const value = input[field];
220
+ patch[field] = value !== undefined ? (value ?? null) : prev[field];
221
+ }
222
+ // Filtered by workspace as well as by id. The `select … for update` three statements up already
223
+ // carries the predicate, so this is not reachable today — but `core` connects as a superuser
224
+ // with RLS bypassed, which makes the predicate in the statement the only barrier there is. A
225
+ // barrier that holds only because of what another statement happens to do is not one.
226
+ const [row] = await tx
227
+ .update(assets)
228
+ .set(patch)
229
+ .where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
230
+ .returning();
231
+ const iso = (v) => (v instanceof Date ? v.toISOString() : v) ?? null;
232
+ const changes = AssetService.EDITABLE.filter((f) => iso(patch[f]) !== iso(prev[f])).map((f) => ({
233
+ field: f,
234
+ from: iso(prev[f]),
235
+ to: iso(patch[f]),
236
+ }));
237
+ if (changes.length === 0)
238
+ return { row: row, activity: null };
239
+ const activity = { workspaceId, assetId, actorId, action: 'updated', changes };
240
+ await this.notify.history(tx, activity);
241
+ return { row: row, activity };
242
+ }
243
+ async archive(tx, workspaceId, actorId, assetId, archived) {
244
+ const [row] = await tx
245
+ .update(assets)
246
+ .set({ archivedAt: archived ? new Date() : null, updatedAt: new Date() })
247
+ .where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
248
+ .returning();
249
+ if (!row)
250
+ throw KernError.notFound('Asset');
251
+ const activity = {
252
+ workspaceId,
253
+ assetId,
254
+ actorId,
255
+ action: archived ? 'retired' : 'restored',
256
+ };
257
+ await this.notify.history(tx, activity);
258
+ return { row, activity };
259
+ }
260
+ }
261
+ //# sourceMappingURL=assets.js.map