@kernhq/module-inventory 0.1.1 → 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.
- package/README.md +55 -9
- package/dist/contract/capabilities.d.ts +49 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +52 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +33 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +22 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +15 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +15 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +146 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +107 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/permissions.d.ts +22 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +26 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/{contract.d.ts → contract/router.d.ts} +24 -85
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +42 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/contract/settings.d.ts +18 -0
- package/dist/contract/settings.d.ts.map +1 -0
- package/dist/contract/settings.js +29 -0
- package/dist/contract/settings.js.map +1 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/{_impl.d.ts → router.d.ts} +33 -8
- package/dist/server/{_impl.d.ts.map → router.d.ts.map} +1 -1
- package/dist/server/router.js +83 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +25 -11
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +28 -10
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +73 -0
- package/dist/server/services/assets.d.ts.map +1 -0
- package/dist/server/services/assets.js +261 -0
- package/dist/server/services/assets.js.map +1 -0
- package/dist/server/services/index.d.ts +10 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +15 -0
- package/dist/server/services/index.js.map +1 -0
- package/dist/server/services/notify.d.ts +63 -0
- package/dist/server/services/notify.d.ts.map +1 -0
- package/dist/server/services/notify.js +105 -0
- package/dist/server/services/notify.js.map +1 -0
- package/migrations/0000_init.sql +12 -3
- package/migrations/0001_rls.sql +24 -0
- package/migrations/meta/0000_snapshot.json +40 -13
- package/migrations/meta/_journal.json +2 -2
- package/package.json +12 -9
- package/src/client/api.ts +1 -1
- package/src/client/components/AssetFormDialog.svelte +130 -47
- package/src/client/i18n.ts +11 -166
- package/src/client/index.ts +8 -1
- package/src/client/messages.test.ts +169 -0
- package/src/client/messages.ts +399 -0
- package/src/client/mock.test.ts +161 -0
- package/src/client/mock.ts +267 -45
- package/src/client/module.ts +22 -2
- package/src/client/pages/AssetsPage.svelte +358 -137
- package/src/client/permissions.ts +1 -1
- package/src/client/price.test.ts +106 -0
- package/src/client/price.ts +135 -0
- package/src/client/query.test.ts +58 -0
- package/src/client/query.ts +15 -2
- package/src/client/settings/GeneralSettings.svelte +0 -0
- package/src/client/settings/core-api.ts +32 -0
- package/src/client/widgets/OverviewWidget.svelte +16 -3
- package/src/contract/capabilities.ts +55 -0
- package/src/contract/events.ts +34 -0
- package/src/contract/index.ts +15 -0
- package/src/contract/models.ts +123 -0
- package/src/contract/permissions.ts +26 -0
- package/src/contract/router.ts +46 -0
- package/src/contract/settings.ts +30 -0
- package/src/module.test.ts +140 -7
- package/src/server/index.ts +16 -13
- package/src/server/inventory.int.test.ts +819 -0
- package/src/server/migrations.test.ts +138 -0
- package/src/server/router.ts +118 -0
- package/src/server/schema.ts +27 -10
- package/src/server/services/assets.ts +368 -0
- package/src/server/services/index.ts +23 -0
- package/src/server/services/notify.ts +151 -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.map +0 -1
- package/dist/contract.js +0 -119
- package/dist/contract.js.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,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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"}
|
package/dist/server/schema.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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: "
|
|
295
|
-
data:
|
|
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: [
|
|
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":"AAgBA;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM,gDAA4B,CAAA;AAS/C
|
|
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"}
|
package/dist/server/schema.js
CHANGED
|
@@ -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,
|
|
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()
|
|
30
|
-
key: text('key').
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EACL,OAAO,EACP,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,OAAO,EACP,KAAK,EACL,
|
|
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
|