@kernhq/module-inventory 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -25
- package/dist/contract/capabilities.d.ts +4 -4
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +51 -9
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +43 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +40 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/models.d.ts +322 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +213 -0
- package/dist/contract/models.js.map +1 -1
- package/dist/contract/notifications.d.ts +24 -0
- package/dist/contract/notifications.d.ts.map +1 -0
- package/dist/contract/notifications.js +72 -0
- package/dist/contract/notifications.js.map +1 -0
- package/dist/contract/permissions.d.ts +52 -2
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +55 -2
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +1052 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +225 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/contract/settings.d.ts +2 -0
- package/dist/contract/settings.d.ts.map +1 -1
- package/dist/contract/settings.js +24 -4
- package/dist/contract/settings.js.map +1 -1
- package/dist/server/index.d.ts +8 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +289 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +48 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +358 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +1337 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +363 -7
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +134 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +158 -4
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +69 -3
- package/dist/server/services/assets.d.ts.map +1 -1
- package/dist/server/services/assets.js +199 -31
- package/dist/server/services/assets.js.map +1 -1
- package/dist/server/services/attachments.d.ts +80 -0
- package/dist/server/services/attachments.d.ts.map +1 -0
- package/dist/server/services/attachments.js +182 -0
- package/dist/server/services/attachments.js.map +1 -0
- package/dist/server/services/audience.d.ts +15 -0
- package/dist/server/services/audience.d.ts.map +1 -0
- package/dist/server/services/audience.js +64 -0
- package/dist/server/services/audience.js.map +1 -0
- package/dist/server/services/categories.d.ts +57 -0
- package/dist/server/services/categories.d.ts.map +1 -0
- package/dist/server/services/categories.js +124 -0
- package/dist/server/services/categories.js.map +1 -0
- package/dist/server/services/cursor.d.ts +68 -0
- package/dist/server/services/cursor.d.ts.map +1 -0
- package/dist/server/services/cursor.js +39 -0
- package/dist/server/services/cursor.js.map +1 -0
- package/dist/server/services/custody.d.ts +175 -0
- package/dist/server/services/custody.d.ts.map +1 -0
- package/dist/server/services/custody.js +367 -0
- package/dist/server/services/custody.js.map +1 -0
- package/dist/server/services/db-errors.d.ts +7 -0
- package/dist/server/services/db-errors.d.ts.map +1 -0
- package/dist/server/services/db-errors.js +32 -0
- package/dist/server/services/db-errors.js.map +1 -0
- package/dist/server/services/index.d.ts +16 -0
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/index.js +25 -1
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/members.d.ts +27 -0
- package/dist/server/services/members.d.ts.map +1 -0
- package/dist/server/services/members.js +39 -0
- package/dist/server/services/members.js.map +1 -0
- package/dist/server/services/notify.d.ts +43 -1
- package/dist/server/services/notify.d.ts.map +1 -1
- package/dist/server/services/notify.js +47 -5
- package/dist/server/services/notify.js.map +1 -1
- package/dist/server/services/offboarding.d.ts +70 -0
- package/dist/server/services/offboarding.d.ts.map +1 -0
- package/dist/server/services/offboarding.js +116 -0
- package/dist/server/services/offboarding.js.map +1 -0
- package/dist/server/services/repairs.d.ts +204 -0
- package/dist/server/services/repairs.d.ts.map +1 -0
- package/dist/server/services/repairs.js +476 -0
- package/dist/server/services/repairs.js.map +1 -0
- package/dist/server/services/search.d.ts +85 -0
- package/dist/server/services/search.d.ts.map +1 -0
- package/dist/server/services/search.js +142 -0
- package/dist/server/services/search.js.map +1 -0
- package/dist/server/services/stats.d.ts +42 -0
- package/dist/server/services/stats.d.ts.map +1 -0
- package/dist/server/services/stats.js +80 -0
- package/dist/server/services/stats.js.map +1 -0
- package/dist/server/services/status.d.ts +102 -0
- package/dist/server/services/status.d.ts.map +1 -0
- package/dist/server/services/status.js +71 -0
- package/dist/server/services/status.js.map +1 -0
- package/migrations/0002_custody_and_categories.sql +23 -0
- package/migrations/0003_repairs.sql +23 -0
- package/migrations/0004_platform_surfaces.sql +51 -0
- package/migrations/0005_repair_dates.sql +35 -0
- package/migrations/0006_workspace_registry_read.sql +50 -0
- package/migrations/0007_history_sequence.sql +83 -0
- package/migrations/meta/0002_snapshot.json +1054 -0
- package/migrations/meta/0003_snapshot.json +1070 -0
- package/migrations/meta/0004_snapshot.json +1130 -0
- package/migrations/meta/0005_snapshot.json +1135 -0
- package/migrations/meta/_journal.json +42 -0
- package/package.json +1 -1
- package/src/client/api-instance.ts +27 -2
- package/src/client/bidi.test.ts +148 -0
- package/src/client/bidi.ts +85 -0
- package/src/client/components/AssetDetailPanel.svelte +614 -0
- package/src/client/components/AssetFormDialog.svelte +71 -22
- package/src/client/components/AssetPhoto.svelte +178 -0
- package/src/client/components/AttachmentsSection.svelte +327 -0
- package/src/client/components/CustodyDialog.svelte +201 -0
- package/src/client/components/RepairDialog.svelte +271 -0
- package/src/client/components/RepairsSection.svelte +318 -0
- package/src/client/components/Timeline.svelte +347 -0
- package/src/client/components/TimelineText.svelte +124 -0
- package/src/client/core-api.ts +71 -0
- package/src/client/custody.test.ts +31 -0
- package/src/client/custody.ts +34 -0
- package/src/client/errors.test.ts +365 -0
- package/src/client/errors.ts +201 -0
- package/src/client/links.test.ts +74 -0
- package/src/client/links.ts +44 -0
- package/src/client/members.test.ts +132 -0
- package/src/client/members.ts +116 -0
- package/src/client/messages.test.ts +127 -0
- package/src/client/messages.ts +1043 -18
- package/src/client/mock.test.ts +395 -1
- package/src/client/mock.ts +1066 -79
- package/src/client/module.ts +54 -0
- package/src/client/pages/AssetsPage.svelte +317 -45
- package/src/client/permissions.ts +7 -0
- package/src/client/price.test.ts +151 -3
- package/src/client/price.ts +155 -11
- package/src/client/query.ts +36 -0
- package/src/client/repairs.test.ts +38 -0
- package/src/client/repairs.ts +38 -0
- package/src/client/settings/CategoriesSettings.svelte +421 -0
- package/src/client/settings/GeneralSettings.svelte +0 -0
- package/src/client/status.ts +29 -0
- package/src/client/timeline.test.ts +175 -0
- package/src/client/timeline.ts +206 -0
- package/src/client/widgets/OverviewWidget.svelte +127 -26
- package/src/client/widgets/RepairsWidget.svelte +124 -0
- package/src/contract/capabilities.ts +53 -9
- package/src/contract/events.ts +49 -0
- package/src/contract/index.ts +1 -0
- package/src/contract/models.ts +237 -0
- package/src/contract/notifications.ts +73 -0
- package/src/contract/permissions.ts +55 -2
- package/src/contract/router.ts +255 -1
- package/src/contract/settings.ts +24 -4
- package/src/module.test.ts +194 -4
- package/src/server/index.ts +311 -4
- package/src/server/inventory.int.test.ts +3573 -18
- package/src/server/jobs.ts +444 -0
- package/src/server/migrations.test.ts +113 -0
- package/src/server/router.ts +462 -6
- package/src/server/schema.ts +160 -3
- package/src/server/services/assets.ts +214 -54
- package/src/server/services/attachments.ts +215 -0
- package/src/server/services/audience.ts +77 -0
- package/src/server/services/categories.ts +136 -0
- package/src/server/services/cursor.ts +104 -0
- package/src/server/services/custody.ts +471 -0
- package/src/server/services/db-errors.ts +42 -0
- package/src/server/services/index.ts +34 -1
- package/src/server/services/members.ts +54 -0
- package/src/server/services/notify.ts +52 -7
- package/src/server/services/offboarding.ts +150 -0
- package/src/server/services/repairs.ts +567 -0
- package/src/server/services/search.ts +166 -0
- package/src/server/services/stats.ts +88 -0
- package/src/server/services/status.test.ts +34 -0
- package/src/server/services/status.ts +143 -0
- package/src/client/settings/core-api.ts +0 -32
|
@@ -38,6 +38,15 @@ export declare class NotifyService {
|
|
|
38
38
|
private readonly kernel;
|
|
39
39
|
constructor(kernel: Kernel);
|
|
40
40
|
private best;
|
|
41
|
+
/**
|
|
42
|
+
* `best`, asked the other question: **did the call actually happen**, rather than what it returned.
|
|
43
|
+
*
|
|
44
|
+
* `best` cannot answer that — a procedure that legitimately answers `null` or `undefined` is
|
|
45
|
+
* indistinguishable from one that threw and was swallowed, and a caller writing "I have told
|
|
46
|
+
* them" into a database on the strength of that is writing a lie. So the value is thrown away
|
|
47
|
+
* here and the boolean is the whole answer.
|
|
48
|
+
*/
|
|
49
|
+
private attempted;
|
|
41
50
|
/**
|
|
42
51
|
* Append to the asset's own history, inside the caller's transaction.
|
|
43
52
|
*
|
|
@@ -51,12 +60,45 @@ export declare class NotifyService {
|
|
|
51
60
|
history(tx: Tx, input: HistoryInput): Promise<void>;
|
|
52
61
|
/** The workspace-wide activity feed's copy. Best-effort, and only ever after the commit. */
|
|
53
62
|
activity(input: HistoryInput): Promise<void>;
|
|
54
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Tell these people, and answer **who it was meant for and how many of them were actually
|
|
65
|
+
* written** — both numbers, because one of them cannot be checked without the other.
|
|
66
|
+
*
|
|
67
|
+
* Everything this class does is best-effort, so a failed `core.notifications.create` is swallowed
|
|
68
|
+
* and logged; a caller that then records "this one has been notified" has recorded something that
|
|
69
|
+
* never happened. The two nightly sweeps write exactly such a marker, once per row for ever, so a
|
|
70
|
+
* swallowed failure there is not a delayed notice but a notice nobody will ever receive.
|
|
71
|
+
*
|
|
72
|
+
* This used to answer a single count, and the sweeps marked the row whenever it was above zero —
|
|
73
|
+
* so a notice that reached one of three recipients was stamped *told*, and the two who missed it
|
|
74
|
+
* never heard. **A partial delivery is a failure for the people it did not reach**, and the row
|
|
75
|
+
* cannot record two answers, so the honest one is the pessimistic one: the sweeps mark nothing
|
|
76
|
+
* unless `delivered === targeted`, and say it again in the morning. The cost is that whoever did
|
|
77
|
+
* hear it hears it twice; `groupKey` collapses that in the notification centre, and it is a
|
|
78
|
+
* strictly smaller price than one person never being told at all.
|
|
79
|
+
*
|
|
80
|
+
* `targeted` is what is left after the de-duplication and the exclusions, which is why the caller
|
|
81
|
+
* cannot work it out from the list it passed in. `targeted: 0` means there was nobody to tell,
|
|
82
|
+
* which is also not "told".
|
|
83
|
+
*/
|
|
84
|
+
notify(input: NotifyInput): Promise<{
|
|
85
|
+
targeted: number;
|
|
86
|
+
delivered: number;
|
|
87
|
+
}>;
|
|
55
88
|
/** Realtime cache invalidation for connected clients. */
|
|
56
89
|
change(workspaceId: string, entity: string, id: string, op: EntityChange['op'], opts?: {
|
|
57
90
|
patch?: Record<string, unknown>;
|
|
58
91
|
scope?: Record<string, string>;
|
|
59
92
|
}): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* The workspace-wide search index, written to by `SearchService` and by nothing else.
|
|
95
|
+
*
|
|
96
|
+
* These two spent a release with no caller: `objectTypes` had been taken off the module
|
|
97
|
+
* definition, because a declared type with no indexer and no resolver renders a link to nothing.
|
|
98
|
+
* Both halves exist now — `src/server/services/search.ts` builds the documents, `src/server/
|
|
99
|
+
* index.ts` declares the indexer and the resolver — and every asset mutation reindexes after it
|
|
100
|
+
* has committed.
|
|
101
|
+
*/
|
|
60
102
|
index(documents: core.SearchDocument[]): Promise<void>;
|
|
61
103
|
unindex(workspaceId: string, type: string, ids: string[]): Promise<void>;
|
|
62
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAI7D,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oDAAoD;IACpD,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CAC9C;AAED;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;YAE7B,IAAI;IAYlB;;;;;;;;;OASG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYzD,4FAA4F;IACtF,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAI7D,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oDAAoD;IACpD,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CAC9C;AAED;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;YAE7B,IAAI;IAYlB;;;;;;;OAOG;YACW,SAAS;IASvB;;;;;;;;;OASG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYzD,4FAA4F;IACtF,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BlF,yDAAyD;IACnD,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAO,GAC7E,OAAO,CAAC,IAAI,CAAC;IAahB;;;;;;;;OAQG;IACG,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtD,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ/E"}
|
|
@@ -23,6 +23,20 @@ export class NotifyService {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* `best`, asked the other question: **did the call actually happen**, rather than what it returned.
|
|
28
|
+
*
|
|
29
|
+
* `best` cannot answer that — a procedure that legitimately answers `null` or `undefined` is
|
|
30
|
+
* indistinguishable from one that threw and was swallowed, and a caller writing "I have told
|
|
31
|
+
* them" into a database on the strength of that is writing a lie. So the value is thrown away
|
|
32
|
+
* here and the boolean is the whole answer.
|
|
33
|
+
*/
|
|
34
|
+
async attempted(what, fn) {
|
|
35
|
+
return ((await this.best(what, async () => {
|
|
36
|
+
await fn();
|
|
37
|
+
return true;
|
|
38
|
+
})) === true);
|
|
39
|
+
}
|
|
26
40
|
/**
|
|
27
41
|
* Append to the asset's own history, inside the caller's transaction.
|
|
28
42
|
*
|
|
@@ -56,12 +70,33 @@ export class NotifyService {
|
|
|
56
70
|
data: input.data ?? {},
|
|
57
71
|
}));
|
|
58
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Tell these people, and answer **who it was meant for and how many of them were actually
|
|
75
|
+
* written** — both numbers, because one of them cannot be checked without the other.
|
|
76
|
+
*
|
|
77
|
+
* Everything this class does is best-effort, so a failed `core.notifications.create` is swallowed
|
|
78
|
+
* and logged; a caller that then records "this one has been notified" has recorded something that
|
|
79
|
+
* never happened. The two nightly sweeps write exactly such a marker, once per row for ever, so a
|
|
80
|
+
* swallowed failure there is not a delayed notice but a notice nobody will ever receive.
|
|
81
|
+
*
|
|
82
|
+
* This used to answer a single count, and the sweeps marked the row whenever it was above zero —
|
|
83
|
+
* so a notice that reached one of three recipients was stamped *told*, and the two who missed it
|
|
84
|
+
* never heard. **A partial delivery is a failure for the people it did not reach**, and the row
|
|
85
|
+
* cannot record two answers, so the honest one is the pessimistic one: the sweeps mark nothing
|
|
86
|
+
* unless `delivered === targeted`, and say it again in the morning. The cost is that whoever did
|
|
87
|
+
* hear it hears it twice; `groupKey` collapses that in the notification centre, and it is a
|
|
88
|
+
* strictly smaller price than one person never being told at all.
|
|
89
|
+
*
|
|
90
|
+
* `targeted` is what is left after the de-duplication and the exclusions, which is why the caller
|
|
91
|
+
* cannot work it out from the list it passed in. `targeted: 0` means there was nobody to tell,
|
|
92
|
+
* which is also not "told".
|
|
93
|
+
*/
|
|
59
94
|
async notify(input) {
|
|
60
95
|
const excluded = new Set([...(input.exclude ?? [])].filter(Boolean));
|
|
61
96
|
const targets = [...new Set(input.userIds)].filter((id) => id && !excluded.has(id));
|
|
62
97
|
if (!targets.length)
|
|
63
|
-
return;
|
|
64
|
-
await Promise.all(targets.map((userId) => this.
|
|
98
|
+
return { targeted: 0, delivered: 0 };
|
|
99
|
+
const written = await Promise.all(targets.map((userId) => this.attempted('notifications.create', () => this.kernel.call('core.notifications.create', {
|
|
65
100
|
userId,
|
|
66
101
|
workspaceId: input.workspaceId,
|
|
67
102
|
module: MODULE_ID,
|
|
@@ -74,6 +109,7 @@ export class NotifyService {
|
|
|
74
109
|
groupKey: input.groupKey ?? null,
|
|
75
110
|
actorId: input.actorId ?? null,
|
|
76
111
|
}))));
|
|
112
|
+
return { targeted: targets.length, delivered: written.filter(Boolean).length };
|
|
77
113
|
}
|
|
78
114
|
/** Realtime cache invalidation for connected clients. */
|
|
79
115
|
async change(workspaceId, entity, id, op, opts = {}) {
|
|
@@ -86,9 +122,15 @@ export class NotifyService {
|
|
|
86
122
|
...(opts.scope ? { scope: opts.scope } : {}),
|
|
87
123
|
}));
|
|
88
124
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The workspace-wide search index, written to by `SearchService` and by nothing else.
|
|
127
|
+
*
|
|
128
|
+
* These two spent a release with no caller: `objectTypes` had been taken off the module
|
|
129
|
+
* definition, because a declared type with no indexer and no resolver renders a link to nothing.
|
|
130
|
+
* Both halves exist now — `src/server/services/search.ts` builds the documents, `src/server/
|
|
131
|
+
* index.ts` declares the indexer and the resolver — and every asset mutation reindexes after it
|
|
132
|
+
* has committed.
|
|
133
|
+
*/
|
|
92
134
|
async index(documents) {
|
|
93
135
|
if (!documents.length)
|
|
94
136
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AA0B3C;;;;;;;GAOG;AACH,MAAM,OAAO,aAAa;IACK;IAA7B,YAA6B,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAEvC,KAAK,CAAC,IAAI,CAAI,IAAY,EAAE,EAAoB;QACtD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAA;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAClB,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EACvD,8BAA8B,CAC/B,CAAA;YACD,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,OAAO,CAAC,EAAM,EAAE,KAAmB;QACvC,MAAM,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC;YACnC,EAAE,EAAE,MAAM,EAAE;YACZ,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE;YAC5B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;SACvB,CAAC,CAAA;IACJ,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,QAAQ,CAAC,KAAmB;QAChC,MAAM,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,EAAE,CACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE;YACvC,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE;YAC/D,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE;YAC5B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;SACvB,CAAC,CACH,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAAkB;QAC7B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAa,CAAC,CAAA;QAChF,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QACnF,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Kernel } from '@kernhq/kernel';
|
|
2
|
+
import type { NotifyService } from './notify.js';
|
|
3
|
+
/** Why the checklist is being raised, which is the only difference between the two callers. */
|
|
4
|
+
export type Departure =
|
|
5
|
+
/** `core.member.removed` — the account is out of the workspace already. */
|
|
6
|
+
'removed'
|
|
7
|
+
/** `hr.person.status_changed` to a status that ends employment — they may still be at their desk. */
|
|
8
|
+
| 'leaving';
|
|
9
|
+
export interface ReturnList {
|
|
10
|
+
items: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
code: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Who was told. Empty when there was nothing to return, or nobody who could act on it. */
|
|
16
|
+
recipients: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Somebody is leaving, and the register still says things are theirs.
|
|
20
|
+
*
|
|
21
|
+
* **This raises a checklist and notifies. It never moves anything.** Custody changes because a
|
|
22
|
+
* person did something — handed an item over, handed it on, took it back — and every one of those
|
|
23
|
+
* writes a period row and a line of history saying who did it. A hook that quietly returned an
|
|
24
|
+
* item on somebody's last day would write a handover nobody performed, into the one record the
|
|
25
|
+
* company later argues from; and the laptop would still be in their bag. So the answer is a message
|
|
26
|
+
* to the people who can take it back, and the register stays exactly as true as it was.
|
|
27
|
+
*
|
|
28
|
+
* **It is inert without HR.** Nothing here imports HR, depends on it, or assumes it is installed:
|
|
29
|
+
* `hr.person.status_changed` simply never arrives in a workspace that has no HR, and the second
|
|
30
|
+
* caller — `core.member.removed` — is core's own event and needs nothing at all. Both paths check
|
|
31
|
+
* the *inventory* module is switched on for the workspace before doing anything, because an event
|
|
32
|
+
* bus is instance-wide and a workspace that has never enabled this module must not be sent its
|
|
33
|
+
* notifications.
|
|
34
|
+
*/
|
|
35
|
+
export declare class OffboardingService {
|
|
36
|
+
private readonly kernel;
|
|
37
|
+
private readonly notify;
|
|
38
|
+
constructor(kernel: Kernel, notify: NotifyService);
|
|
39
|
+
/**
|
|
40
|
+
* What one person is still recorded as holding.
|
|
41
|
+
*
|
|
42
|
+
* Read from `assets.custodian_user_id` — denormalised inside the transaction that writes the
|
|
43
|
+
* custody period, and indexed — rather than from an open-period join, which is the same choice
|
|
44
|
+
* `custody.byUser` makes and for the same reason. Archived rows cannot appear: an item somebody
|
|
45
|
+
* holds cannot be archived at all, because `assets.archive` refuses it.
|
|
46
|
+
*/
|
|
47
|
+
held(workspaceId: string, userId: string): Promise<ReturnList['items']>;
|
|
48
|
+
/**
|
|
49
|
+
* Raise the return list, if there is one.
|
|
50
|
+
*
|
|
51
|
+
* Answers with what it found and who it told, so a test can assert both and a caller can log the
|
|
52
|
+
* count. Doing nothing is the ordinary case — most people leave holding nothing — and it is not
|
|
53
|
+
* an error.
|
|
54
|
+
*
|
|
55
|
+
* Redelivery sends the same message again: an event handler is retried, and `core.notifications
|
|
56
|
+
* .create` has no idempotency key. The two events behind this are rare enough that the cost is a
|
|
57
|
+
* duplicate row rather than a stream, and `groupKey` collapses them where a client groups. That
|
|
58
|
+
* is the same trade `module-tracker`'s `due-soon` makes, written down rather than assumed.
|
|
59
|
+
*/
|
|
60
|
+
raise(workspaceId: string, userId: string, departure: Departure): Promise<ReturnList>;
|
|
61
|
+
/**
|
|
62
|
+
* What to call the person in the sentence.
|
|
63
|
+
*
|
|
64
|
+
* Core is asked, and a failure falls back to "a former member" rather than to the uuid: a
|
|
65
|
+
* notification with a uuid in the middle of it is the product admitting it does not know who it
|
|
66
|
+
* is talking about, and this module already refuses to print one on a screen for the same reason.
|
|
67
|
+
*/
|
|
68
|
+
private nameOf;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=offboarding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offboarding.d.ts","sourceRoot":"","sources":["../../../src/server/services/offboarding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAK5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAYhD,+FAA+F;AAC/F,MAAM,MAAM,SAAS;AACnB,2EAA2E;AACzE,SAAS;AACX,qGAAqG;GACnG,SAAS,CAAA;AAEb,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,2FAA2F;IAC3F,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAkB;IAE3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa;IAGxC;;;;;;;OAOG;IACG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAiB7E;;;;;;;;;;;OAWG;IACG,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA8C3F;;;;;;OAMG;YACW,MAAM;CAMrB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { and, asc, eq, isNull } from 'drizzle-orm';
|
|
2
|
+
import { MODULE_ID } from '../../contract/models.js';
|
|
3
|
+
import { assets } from '../schema.js';
|
|
4
|
+
import { membersWithPermission } from './audience.js';
|
|
5
|
+
import { assetUrl } from './search.js';
|
|
6
|
+
/** How many tags fit in one sentence before it stops being readable. */
|
|
7
|
+
const TAGS_IN_BODY = 5;
|
|
8
|
+
/**
|
|
9
|
+
* More than anybody is plausibly holding, and a bound all the same: this reads every live asset of
|
|
10
|
+
* one person, and an unbounded query in an event handler is an unbounded query.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_HELD = 200;
|
|
13
|
+
/**
|
|
14
|
+
* Somebody is leaving, and the register still says things are theirs.
|
|
15
|
+
*
|
|
16
|
+
* **This raises a checklist and notifies. It never moves anything.** Custody changes because a
|
|
17
|
+
* person did something — handed an item over, handed it on, took it back — and every one of those
|
|
18
|
+
* writes a period row and a line of history saying who did it. A hook that quietly returned an
|
|
19
|
+
* item on somebody's last day would write a handover nobody performed, into the one record the
|
|
20
|
+
* company later argues from; and the laptop would still be in their bag. So the answer is a message
|
|
21
|
+
* to the people who can take it back, and the register stays exactly as true as it was.
|
|
22
|
+
*
|
|
23
|
+
* **It is inert without HR.** Nothing here imports HR, depends on it, or assumes it is installed:
|
|
24
|
+
* `hr.person.status_changed` simply never arrives in a workspace that has no HR, and the second
|
|
25
|
+
* caller — `core.member.removed` — is core's own event and needs nothing at all. Both paths check
|
|
26
|
+
* the *inventory* module is switched on for the workspace before doing anything, because an event
|
|
27
|
+
* bus is instance-wide and a workspace that has never enabled this module must not be sent its
|
|
28
|
+
* notifications.
|
|
29
|
+
*/
|
|
30
|
+
export class OffboardingService {
|
|
31
|
+
kernel;
|
|
32
|
+
notify;
|
|
33
|
+
constructor(kernel, notify) {
|
|
34
|
+
this.kernel = kernel;
|
|
35
|
+
this.notify = notify;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What one person is still recorded as holding.
|
|
39
|
+
*
|
|
40
|
+
* Read from `assets.custodian_user_id` — denormalised inside the transaction that writes the
|
|
41
|
+
* custody period, and indexed — rather than from an open-period join, which is the same choice
|
|
42
|
+
* `custody.byUser` makes and for the same reason. Archived rows cannot appear: an item somebody
|
|
43
|
+
* holds cannot be archived at all, because `assets.archive` refuses it.
|
|
44
|
+
*/
|
|
45
|
+
async held(workspaceId, userId) {
|
|
46
|
+
return this.kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
47
|
+
.select({ id: assets.id, code: assets.code, name: assets.name })
|
|
48
|
+
.from(assets)
|
|
49
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.custodianUserId, userId), isNull(assets.archivedAt)))
|
|
50
|
+
.orderBy(asc(assets.code))
|
|
51
|
+
.limit(MAX_HELD));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Raise the return list, if there is one.
|
|
55
|
+
*
|
|
56
|
+
* Answers with what it found and who it told, so a test can assert both and a caller can log the
|
|
57
|
+
* count. Doing nothing is the ordinary case — most people leave holding nothing — and it is not
|
|
58
|
+
* an error.
|
|
59
|
+
*
|
|
60
|
+
* Redelivery sends the same message again: an event handler is retried, and `core.notifications
|
|
61
|
+
* .create` has no idempotency key. The two events behind this are rare enough that the cost is a
|
|
62
|
+
* duplicate row rather than a stream, and `groupKey` collapses them where a client groups. That
|
|
63
|
+
* is the same trade `module-tracker`'s `due-soon` makes, written down rather than assumed.
|
|
64
|
+
*/
|
|
65
|
+
async raise(workspaceId, userId, departure) {
|
|
66
|
+
if (!(await this.kernel.isModuleEnabled(workspaceId, MODULE_ID).catch(() => false)))
|
|
67
|
+
return { items: [], recipients: [] };
|
|
68
|
+
const items = await this.held(workspaceId, userId);
|
|
69
|
+
if (!items.length)
|
|
70
|
+
return { items: [], recipients: [] };
|
|
71
|
+
// Whoever may take an item back, which is the thing this message is asking for. Not "the
|
|
72
|
+
// admins": a workspace that gave its office manager a custom role did that on purpose.
|
|
73
|
+
const recipients = await membersWithPermission(this.kernel, workspaceId, 'inventory.custody.manage');
|
|
74
|
+
if (!recipients.length) {
|
|
75
|
+
this.kernel.log.warn({ module: MODULE_ID, workspaceId, held: items.length }, 'inventory: somebody left holding items and nobody in the workspace may take them back');
|
|
76
|
+
return { items, recipients: [] };
|
|
77
|
+
}
|
|
78
|
+
const who = await this.nameOf(userId);
|
|
79
|
+
const tags = items.slice(0, TAGS_IN_BODY).map((item) => `${item.code} ${item.name}`);
|
|
80
|
+
const rest = items.length - tags.length;
|
|
81
|
+
await this.notify.notify({
|
|
82
|
+
workspaceId,
|
|
83
|
+
userIds: recipients,
|
|
84
|
+
type: 'inventory.custody.return_due',
|
|
85
|
+
title: departure === 'removed'
|
|
86
|
+
? `${who} has left the workspace still holding ${items.length === 1 ? 'an item' : `${items.length} items`}`
|
|
87
|
+
: `${who} is leaving and still holds ${items.length === 1 ? 'an item' : `${items.length} items`}`,
|
|
88
|
+
body: rest > 0 ? `${tags.join(', ')} and ${rest} more` : tags.join(', '),
|
|
89
|
+
// One item has an object worth pointing at; several do not, and naming the first would send
|
|
90
|
+
// everybody to one laptop out of five. The URL below is the list either way.
|
|
91
|
+
object: items.length === 1 ? { module: MODULE_ID, type: 'asset', id: items[0].id } : null,
|
|
92
|
+
url: items.length === 1 ? assetUrl(items[0].id) : `/inventory?custodian=${userId}`,
|
|
93
|
+
data: { userId, departure, assetIds: items.map((item) => item.id) },
|
|
94
|
+
// One group per person, so the two events cannot stack two separate piles about one leaver.
|
|
95
|
+
groupKey: `inventory.return_due.${userId}`,
|
|
96
|
+
// Never to the person leaving: they are being *chased*, and on the `removed` path they are no
|
|
97
|
+
// longer a member of this workspace at all.
|
|
98
|
+
exclude: [userId],
|
|
99
|
+
});
|
|
100
|
+
return { items, recipients };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What to call the person in the sentence.
|
|
104
|
+
*
|
|
105
|
+
* Core is asked, and a failure falls back to "a former member" rather than to the uuid: a
|
|
106
|
+
* notification with a uuid in the middle of it is the product admitting it does not know who it
|
|
107
|
+
* is talking about, and this module already refuses to print one on a screen for the same reason.
|
|
108
|
+
*/
|
|
109
|
+
async nameOf(userId) {
|
|
110
|
+
const user = await this.kernel
|
|
111
|
+
.call('core.users.get', { id: userId })
|
|
112
|
+
.catch(() => null);
|
|
113
|
+
return user?.name?.trim() || user?.email?.trim() || 'A former member';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=offboarding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offboarding.js","sourceRoot":"","sources":["../../../src/server/services/offboarding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { type Tx } from '@kernhq/kernel';
|
|
2
|
+
import type { RepairInput, RepairListItem, Repair as RepairModel, RepairPatchInput } from '../../contract/models.js';
|
|
3
|
+
import { assets, repairs } from '../schema.js';
|
|
4
|
+
import type { HistoryInput, NotifyService } from './notify.js';
|
|
5
|
+
type Row = typeof repairs.$inferSelect;
|
|
6
|
+
type AssetRow = typeof assets.$inferSelect;
|
|
7
|
+
/** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
|
|
8
|
+
export declare function toRepair(row: Row): RepairModel;
|
|
9
|
+
/**
|
|
10
|
+
* What one repair mutation wrote, and what may only leave the module once it has committed.
|
|
11
|
+
*
|
|
12
|
+
* The asset comes back beside the repair because sending an item away moves `assets.status`, and a
|
|
13
|
+
* caller that fetched the asset again afterwards would be reading a row somebody else may have
|
|
14
|
+
* changed in between — the panel would then show a repair that has already been completed.
|
|
15
|
+
*/
|
|
16
|
+
export interface RepairWritten {
|
|
17
|
+
repair: Row;
|
|
18
|
+
asset: AssetRow;
|
|
19
|
+
/** `null` when nothing happened worth recording, which is what an edit that changed nothing is. */
|
|
20
|
+
activity: HistoryInput | null;
|
|
21
|
+
}
|
|
22
|
+
export interface RepairListInput {
|
|
23
|
+
limit: number;
|
|
24
|
+
cursor?: string;
|
|
25
|
+
assetId?: string;
|
|
26
|
+
/** `true` for still away, `false` for finished, absent for both. */
|
|
27
|
+
open?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What went away to be fixed, and what came back.
|
|
31
|
+
*
|
|
32
|
+
* Three things happen in one transaction on every change here, and the point of the class is that
|
|
33
|
+
* they cannot come apart: the repair row moves, `assets.status` is brought into step with it through
|
|
34
|
+
* `deriveStatus`, and an `asset_history` entry records it.
|
|
35
|
+
*
|
|
36
|
+
* **The database is the arbiter of "already away", not this file.** There is deliberately no
|
|
37
|
+
* `select … for update` on the asset *before* the repair row is written:
|
|
38
|
+
* `inventory_repairs_one_open_uq` — a unique index on `(asset_id) where returned_on is null` — is
|
|
39
|
+
* what makes two open repairs impossible, so two people pressing *Send for repair* on the same
|
|
40
|
+
* laptop in the same instant both read "it is here", both insert, and Postgres refuses exactly one
|
|
41
|
+
* of them. Checking first and inserting after is the race, not the fix. What this file owes the
|
|
42
|
+
* loser is a sentence they can act on rather than drizzle's "Failed query: insert into
|
|
43
|
+
* mod_inventory.repairs …".
|
|
44
|
+
*
|
|
45
|
+
* **`restamp` does lock it, afterwards, and that is a different job.** `assets.status` is derived
|
|
46
|
+
* from this module's repair rows *and* from custody, which another service writes, so the winner
|
|
47
|
+
* still has to compute an answer nobody can overwrite from a stale snapshot. See `lockAsset` in
|
|
48
|
+
* `status.ts`: the index decides the contest, the lock orders the bookkeeping that follows it.
|
|
49
|
+
*
|
|
50
|
+
* **Repairs do not touch custody.** An item at the repairer is still somebody's responsibility; see
|
|
51
|
+
* `status.ts`, where that rule is argued in full.
|
|
52
|
+
*/
|
|
53
|
+
export declare class RepairService {
|
|
54
|
+
private readonly notify;
|
|
55
|
+
constructor(notify: NotifyService);
|
|
56
|
+
/**
|
|
57
|
+
* Today, as this module means it: the UTC date.
|
|
58
|
+
*
|
|
59
|
+
* Not the browser's date, and not a workspace time zone — a repair is dated to the day, and the
|
|
60
|
+
* two hours a workspace in Istanbul is ahead of UTC would put an evening repair on tomorrow for
|
|
61
|
+
* one reader and today for another. One clock, the server's, the same reasoning that keeps asset
|
|
62
|
+
* tags server-side. A workspace that needs the exact day sends `sentOn` itself.
|
|
63
|
+
*/
|
|
64
|
+
private static today;
|
|
65
|
+
/** The one error a lost race produces, in every place a race can be lost. */
|
|
66
|
+
private static alreadyAway;
|
|
67
|
+
/**
|
|
68
|
+
* A repair cannot come back before it was sent — checked on every path that writes either date.
|
|
69
|
+
*
|
|
70
|
+
* `complete` has always refused a return date before the send date, and `update` did not: it took
|
|
71
|
+
* `sentOn` from the patch and wrote it whatever the row already said, so correcting the send date
|
|
72
|
+
* of a *finished* repair could move it past the day the item came back. What that stores is a
|
|
73
|
+
* repair that ended before it started — every "how long was it away" answer negative, the overdue
|
|
74
|
+
* sweep measuring from a date in the future, and nothing anywhere to say which of the two dates is
|
|
75
|
+
* the wrong one.
|
|
76
|
+
*
|
|
77
|
+
* Both are `date` columns, so they read back as `YYYY-MM-DD` and compare correctly as text.
|
|
78
|
+
*
|
|
79
|
+
* The database holds the same rule (`inventory_repairs_returned_after_sent`), because two
|
|
80
|
+
* transactions can each pass this check and still write a pair that fails it: one moving `sent_on`
|
|
81
|
+
* while the other logs the item back. This runs first so the ordinary case gets a sentence rather
|
|
82
|
+
* than a constraint violation; `outOfOrder` is what the loser of that race gets.
|
|
83
|
+
*/
|
|
84
|
+
private static requireInOrder;
|
|
85
|
+
private static outOfOrder;
|
|
86
|
+
/**
|
|
87
|
+
* One day past today, which is the whole tolerance a date typed by a person needs.
|
|
88
|
+
*
|
|
89
|
+
* `today()` is UTC — one clock, the server's, for the reason it documents — and a workspace in
|
|
90
|
+
* Auckland is up to fourteen hours ahead of it, so *their* today is UTC's tomorrow for a large
|
|
91
|
+
* part of their working day. Refusing at exactly UTC-today would refuse the ordinary case in half
|
|
92
|
+
* the world's offices. A day is enough for every real time zone and is nowhere near enough to be
|
|
93
|
+
* the defect below.
|
|
94
|
+
*/
|
|
95
|
+
private static readonly FUTURE_GRACE_DAYS;
|
|
96
|
+
/**
|
|
97
|
+
* A repair cannot be sent from the future — checked on every path that writes `sent_on`.
|
|
98
|
+
*
|
|
99
|
+
* `sentOn` is a date a person types, and nothing bounded it. A repair dated 2030 is one the
|
|
100
|
+
* overdue sweep can never find: it looks for `sent_on <= today - repairOverdueDays`, so a send
|
|
101
|
+
* date years ahead is permanently outside the window and the chase never fires. Not for a while —
|
|
102
|
+
* **ever**, for the life of that row, and silently, because a sweep that finds nothing looks
|
|
103
|
+
* exactly like a sweep with nothing to do. It also makes every "how long has it been away" answer
|
|
104
|
+
* negative and puts the item at the top of a list ordered by when it left.
|
|
105
|
+
*
|
|
106
|
+
* A typo is the likely cause and a deliberate one is the dangerous case: this is the one field
|
|
107
|
+
* that decides whether anybody is ever reminded that a vendor still has the company's laptop.
|
|
108
|
+
*
|
|
109
|
+
* Bounded on the *server*, not in the contract, and both halves of that are deliberate. The
|
|
110
|
+
* contract is shared with the browser, so a `refine` there would compare against the reader's own
|
|
111
|
+
* clock — and a client whose date is a day ahead would refuse a date the server accepts, or the
|
|
112
|
+
* other way round. And it is a sentence rather than a bare `invalid_string`, because "a repair
|
|
113
|
+
* cannot be sent in the future" is something a person can act on.
|
|
114
|
+
*
|
|
115
|
+
* **A `BAD_REQUEST` with no `reason`, like `checkPhoto`'s refusal and the category check in
|
|
116
|
+
* `AssetService`.** The date is malformed rather than contested — nothing raced, no state
|
|
117
|
+
* changed underneath anybody, the value simply cannot be true — and `CONFLICT` is this module's
|
|
118
|
+
* word for losing a race. The cost is that the sentence reaches a Persian or Turkish reader in
|
|
119
|
+
* English: a translated refusal needs a stable `reason` **and** its five bundles in
|
|
120
|
+
* `src/client/errors.ts`, and the two have to arrive together or `errors.test.ts` fails from
|
|
121
|
+
* whichever side is ahead.
|
|
122
|
+
*/
|
|
123
|
+
private static requireNotFuture;
|
|
124
|
+
private asset;
|
|
125
|
+
get(tx: Tx, workspaceId: string, repairId: string): Promise<Row>;
|
|
126
|
+
/**
|
|
127
|
+
* One asset's repairs, or the whole workspace's — one query with one filter, because they are one
|
|
128
|
+
* question asked at two scopes and a second query answering it would be a second one to keep in
|
|
129
|
+
* step.
|
|
130
|
+
*
|
|
131
|
+
* **Paged by id, newest logged first.** An id is uuidv7, so it already carries the clock, and it
|
|
132
|
+
* is unique where `sent_on` is a date two repairs logged on the same day share — a page boundary
|
|
133
|
+
* between two rows that share a sort key repeats one and drops the other. The same reasoning
|
|
134
|
+
* `assets.list` gives for `sort: 'recent'` and `assets.history` gives for ordering on the row id.
|
|
135
|
+
*/
|
|
136
|
+
list(tx: Tx, workspaceId: string, input: RepairListInput): Promise<{
|
|
137
|
+
items: RepairListItem[];
|
|
138
|
+
nextCursor: string | null;
|
|
139
|
+
}>;
|
|
140
|
+
/**
|
|
141
|
+
* The currency to store, given what the caller said and what is already there.
|
|
142
|
+
*
|
|
143
|
+
* A cost with no unit is not a cost, so an amount recorded with no currency inherits the asset's.
|
|
144
|
+
* **Inheritance fires in exactly one case** — an amount is being recorded and the repair has no
|
|
145
|
+
* currency at all — and getting that wrong is silent in both directions:
|
|
146
|
+
*
|
|
147
|
+
* - inheriting whenever a cost *exists* rather than whenever one *arrives* means an unrelated
|
|
148
|
+
* edit (correcting a vendor) silently gives the asset's currency back to a repair whose
|
|
149
|
+
* currency somebody deliberately cleared;
|
|
150
|
+
* - inheriting over a currency the repair already has means recording an amount in dollars on an
|
|
151
|
+
* asset priced in euros quietly relabels it as euros.
|
|
152
|
+
*
|
|
153
|
+
* `undefined` means "not mentioned" and an explicit `null` means "no currency", which a workspace
|
|
154
|
+
* that records amounts and not currencies genuinely means. Collapsing the two is the mistake
|
|
155
|
+
* `assets.update` documents one file over.
|
|
156
|
+
*/
|
|
157
|
+
private static currencyFor;
|
|
158
|
+
/** Send it away. Refuses when it is already at a repairer — that is what `complete` is for. */
|
|
159
|
+
create(tx: Tx, workspaceId: string, actorId: string | null, assetId: string, input: RepairInput): Promise<RepairWritten>;
|
|
160
|
+
/**
|
|
161
|
+
* Correct what was recorded — a vendor, or a cost that arrived with the invoice a week later.
|
|
162
|
+
*
|
|
163
|
+
* **`returnedOn` is deliberately not patchable.** That one column decides whether the asset reads
|
|
164
|
+
* as `under_repair`, so exactly one procedure moves it and the derived status has one door rather
|
|
165
|
+
* than two. Editing a finished repair is allowed all the same: the invoice usually arrives after
|
|
166
|
+
* the item does.
|
|
167
|
+
*
|
|
168
|
+
* **No `actorId`, because nothing here records one.** This writes no timeline entry — see the
|
|
169
|
+
* bottom of the method — and a parameter kept for symmetry with `create` and `complete` would be
|
|
170
|
+
* a parameter every caller has to supply and nothing reads.
|
|
171
|
+
*/
|
|
172
|
+
update(tx: Tx, workspaceId: string, repairId: string, patch: RepairPatchInput): Promise<RepairWritten>;
|
|
173
|
+
/**
|
|
174
|
+
* It came back.
|
|
175
|
+
*
|
|
176
|
+
* `and returned_on is null` in the predicate is the optimistic guard, exactly as
|
|
177
|
+
* `CustodyService.close` uses: under READ COMMITTED a concurrent complete blocks this statement,
|
|
178
|
+
* and when it resumes the row no longer matches, so zero rows come back rather than a second
|
|
179
|
+
* completion silently overwriting the first one's date.
|
|
180
|
+
*/
|
|
181
|
+
complete(tx: Tx, workspaceId: string, actorId: string | null, repairId: string, input: {
|
|
182
|
+
returnedOn?: string;
|
|
183
|
+
costMinor?: number | null;
|
|
184
|
+
currency?: string | null;
|
|
185
|
+
}): Promise<RepairWritten>;
|
|
186
|
+
/**
|
|
187
|
+
* Bring `assets.status` back into step with the facts, inside the same transaction.
|
|
188
|
+
*
|
|
189
|
+
* Both facts are **read** rather than assumed — the open repair as it now stands, and the
|
|
190
|
+
* custodian as the asset row carries it — and `deriveStatus` decides. Assuming `under_repair`
|
|
191
|
+
* after an insert and `in_stock` after a completion is the version of this that silently released
|
|
192
|
+
* whoever was still holding the item.
|
|
193
|
+
*
|
|
194
|
+
* **The asset is locked and re-read here, rather than passed in.** It used to take the row the
|
|
195
|
+
* caller had already fetched, which is a snapshot from before the repair was written and before
|
|
196
|
+
* anything else that touched the asset in between: a handover committing in that window was
|
|
197
|
+
* invisible, so this derived a status from a custodian that had stopped being current and wrote it
|
|
198
|
+
* over the handover's answer. Taking the row under the lock is what makes "read both facts, then
|
|
199
|
+
* derive" true rather than merely intended. `lockAsset` argues it in full.
|
|
200
|
+
*/
|
|
201
|
+
private restamp;
|
|
202
|
+
}
|
|
203
|
+
export {};
|
|
204
|
+
//# sourceMappingURL=repairs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repairs.d.ts","sourceRoot":"","sources":["../../../src/server/services/repairs.ts"],"names":[],"mappings":"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"}
|