@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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { KernError, uuidv7 } from '@kernhq/kernel';
|
|
2
|
+
import { and, desc, eq, isNull } from 'drizzle-orm';
|
|
3
|
+
import { assets, custodyPeriods } from '../schema.js';
|
|
4
|
+
import { violated } from './db-errors.js';
|
|
5
|
+
import { awayForRepair, deriveStatus, lockAsset } from './status.js';
|
|
6
|
+
/** The GiST exclusion constraint `0001_rls.sql` added. Two open periods for one asset are it. */
|
|
7
|
+
const NO_OVERLAP = 'inventory_custody_no_overlap';
|
|
8
|
+
/**
|
|
9
|
+
* One millisecond: the finest a JS `Date` resolves, and therefore the shortest custody period this
|
|
10
|
+
* module is able to write. See `instant` — it is what keeps two changes in the same millisecond
|
|
11
|
+
* from recording a period nobody held the item for.
|
|
12
|
+
*/
|
|
13
|
+
const TICK_MS = 1;
|
|
14
|
+
/** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
|
|
15
|
+
export function toCustodyPeriod(row) {
|
|
16
|
+
return {
|
|
17
|
+
id: row.id,
|
|
18
|
+
workspaceId: row.workspaceId,
|
|
19
|
+
assetId: row.assetId,
|
|
20
|
+
userId: row.userId,
|
|
21
|
+
note: row.note,
|
|
22
|
+
effectiveFrom: row.effectiveFrom.toISOString(),
|
|
23
|
+
effectiveTo: row.effectiveTo?.toISOString() ?? null,
|
|
24
|
+
createdBy: row.createdBy,
|
|
25
|
+
createdAt: row.createdAt.toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Who is holding what, over time.
|
|
30
|
+
*
|
|
31
|
+
* Effective-dated the way HR keeps employments: **nothing is ever updated in place**. A change
|
|
32
|
+
* closes the open row and inserts a new one, so the answer to "who had this laptop in March" is a
|
|
33
|
+
* row that still exists rather than a value that was overwritten.
|
|
34
|
+
*
|
|
35
|
+
* Three things happen in one transaction on every change, and the whole point of the class is that
|
|
36
|
+
* they cannot come apart: the period rows move, `assets.custodian_user_id`/`custody_since`/`status`
|
|
37
|
+
* are brought into step with them, and an `asset_history` entry records it. The denormalised
|
|
38
|
+
* columns on `assets` are what the list filters and the widget read; they are correct because they
|
|
39
|
+
* are written here, in the same transaction, and never by a job afterwards.
|
|
40
|
+
*
|
|
41
|
+
* **A repair never refuses a handover.** An item at the repairer is still somebody's
|
|
42
|
+
* responsibility, so none of the three verbs looks at repair state to decide whether it may run —
|
|
43
|
+
* refusing `assign` would mean refusing `return` as well, and somebody leaving the company while
|
|
44
|
+
* their laptop is in the workshop has to be able to hand it back. What repair state *does* affect
|
|
45
|
+
* is the status these three write, which is `deriveStatus`'s job and is argued in `status.ts`.
|
|
46
|
+
*
|
|
47
|
+
* **The database is the arbiter of who won, not this file.** There is deliberately no `select … for
|
|
48
|
+
* update` before the period is inserted. `inventory_custody_no_overlap` — a GiST exclusion
|
|
49
|
+
* constraint on `(asset_id =, tstzrange(effective_from, effective_to, '[)') &&)` — is what makes two
|
|
50
|
+
* open periods impossible, so two people pressing *Hand over* on the same laptop in the same instant
|
|
51
|
+
* both read "nobody has it", both insert, and Postgres refuses exactly one of them. Locking the
|
|
52
|
+
* asset first would serialise them into two successful handovers, which is a worse answer wearing
|
|
53
|
+
* the clothes of a safer one. What this file owes the loser is a sentence they can act on rather
|
|
54
|
+
* than drizzle's "Failed query: insert into mod_inventory.custody_periods …", and that is `refuse()`
|
|
55
|
+
* below.
|
|
56
|
+
*
|
|
57
|
+
* **The asset row *is* locked afterwards, and that is a different job.** `stamp` takes it before it
|
|
58
|
+
* reads the repair state, because `status` is derived from two facts two services write and the
|
|
59
|
+
* winner of the race still has to write an answer nobody can overwrite from a stale snapshot. The
|
|
60
|
+
* distinction is argued at `lockAsset` in `status.ts`: the constraint decides the contest, the lock
|
|
61
|
+
* orders the bookkeeping that follows it.
|
|
62
|
+
*/
|
|
63
|
+
export class CustodyService {
|
|
64
|
+
notify;
|
|
65
|
+
constructor(notify) {
|
|
66
|
+
this.notify = notify;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The one error a lost race produces, in every place a race can be lost.
|
|
70
|
+
*
|
|
71
|
+
* Actionable rather than apologetic: the reader's screen is now out of date, and the only thing
|
|
72
|
+
* they can do about it is look again. `reason` is stable so a client could eventually say it in
|
|
73
|
+
* the reader's own language; the message is the honest fallback until one does.
|
|
74
|
+
*/
|
|
75
|
+
static refuse() {
|
|
76
|
+
return KernError.conflict('Somebody changed who is holding this a moment before you did. Reload to see where it is now.', 'inventory.custody.conflict');
|
|
77
|
+
}
|
|
78
|
+
async asset(tx, workspaceId, assetId) {
|
|
79
|
+
const [row] = await tx
|
|
80
|
+
.select()
|
|
81
|
+
.from(assets)
|
|
82
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)));
|
|
83
|
+
if (!row)
|
|
84
|
+
throw KernError.notFound('Asset');
|
|
85
|
+
// An archived asset is one the workspace has said it no longer tracks. Handing it to somebody
|
|
86
|
+
// would make them answerable for something that is not in the register, and the timeline would
|
|
87
|
+
// carry a handover after a retirement.
|
|
88
|
+
if (row.archivedAt)
|
|
89
|
+
throw KernError.conflict('This item is archived. Restore it before handing it over.', 'inventory.custody.archived');
|
|
90
|
+
return row;
|
|
91
|
+
}
|
|
92
|
+
/** The period that has not been closed, if there is one. At most one exists, by construction. */
|
|
93
|
+
async open(tx, workspaceId, assetId) {
|
|
94
|
+
const [row] = await tx
|
|
95
|
+
.select()
|
|
96
|
+
.from(custodyPeriods)
|
|
97
|
+
.where(and(eq(custodyPeriods.workspaceId, workspaceId), eq(custodyPeriods.assetId, assetId), isNull(custodyPeriods.effectiveTo)))
|
|
98
|
+
.limit(1);
|
|
99
|
+
return row;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Every period for one asset, newest first.
|
|
103
|
+
*
|
|
104
|
+
* Capped rather than paged: the rows are bounded by how many times one item changed hands, which
|
|
105
|
+
* is tens over its life. Ordered by `effective_from` and then by id, because a hand-on closes one
|
|
106
|
+
* row and opens another at the same instant and the two would otherwise have no order between
|
|
107
|
+
* them — the panel would show the handover before the return it replaced, at random.
|
|
108
|
+
*/
|
|
109
|
+
async history(tx, workspaceId, assetId, limit) {
|
|
110
|
+
return tx
|
|
111
|
+
.select()
|
|
112
|
+
.from(custodyPeriods)
|
|
113
|
+
.where(and(eq(custodyPeriods.workspaceId, workspaceId), eq(custodyPeriods.assetId, assetId)))
|
|
114
|
+
.orderBy(desc(custodyPeriods.effectiveFrom), desc(custodyPeriods.id))
|
|
115
|
+
.limit(limit);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Close the open period at `at`, or refuse.
|
|
119
|
+
*
|
|
120
|
+
* `and effective_to is null` in the predicate is the optimistic guard: under READ COMMITTED a
|
|
121
|
+
* concurrent close blocks this statement, and when it resumes the row no longer matches, so zero
|
|
122
|
+
* rows come back rather than a second close silently overwriting the first one's timestamp.
|
|
123
|
+
*/
|
|
124
|
+
async close(tx, workspaceId, periodId, at) {
|
|
125
|
+
const [row] = await tx
|
|
126
|
+
.update(custodyPeriods)
|
|
127
|
+
.set({ effectiveTo: at })
|
|
128
|
+
.where(and(eq(custodyPeriods.workspaceId, workspaceId), eq(custodyPeriods.id, periodId), isNull(custodyPeriods.effectiveTo)))
|
|
129
|
+
.returning();
|
|
130
|
+
if (!row)
|
|
131
|
+
throw CustodyService.refuse();
|
|
132
|
+
return row;
|
|
133
|
+
}
|
|
134
|
+
async openPeriod(tx, workspaceId, assetId, userId, note, actorId, at) {
|
|
135
|
+
try {
|
|
136
|
+
const [row] = await tx
|
|
137
|
+
.insert(custodyPeriods)
|
|
138
|
+
.values({
|
|
139
|
+
id: uuidv7(),
|
|
140
|
+
workspaceId,
|
|
141
|
+
assetId,
|
|
142
|
+
userId,
|
|
143
|
+
note,
|
|
144
|
+
effectiveFrom: at,
|
|
145
|
+
createdBy: actorId,
|
|
146
|
+
})
|
|
147
|
+
.returning();
|
|
148
|
+
return row;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
// The constraint bit: another transaction opened a period for this asset between our read and
|
|
152
|
+
// our insert. Anything else is a real fault and must not be disguised as a lost race.
|
|
153
|
+
if (violated(err, NO_OVERLAP))
|
|
154
|
+
throw CustodyService.refuse();
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The three denormalised columns, brought into step inside the same transaction.
|
|
160
|
+
*
|
|
161
|
+
* **`status` is not `userId ? 'assigned' : 'in_stock'`, and that line is what this comment is
|
|
162
|
+
* about.** An item can be at a repairer *and* assigned to somebody — the repair does not release
|
|
163
|
+
* whoever is answerable for it — so a handover that wrote `assigned` unconditionally would
|
|
164
|
+
* announce a laptop as back in the office while it was still in the workshop. Both facts are read
|
|
165
|
+
* and `deriveStatus` decides; the rule is argued in full in `status.ts`.
|
|
166
|
+
*
|
|
167
|
+
* **The asset row is locked before the repair state is read, and the order is the point.** Without
|
|
168
|
+
* it a repair completing in another transaction is invisible here and this handover's status is
|
|
169
|
+
* derived from a snapshot that has already stopped being true — the two writes then interleave
|
|
170
|
+
* into a status matching neither. See `lockAsset`.
|
|
171
|
+
*
|
|
172
|
+
* The lock is also what makes `assets.archive`'s refusal real rather than advisory. Archiving
|
|
173
|
+
* reads "nobody is holding it" under the same lock, so a handover racing an archive is ordered
|
|
174
|
+
* against it: whichever gets the lock second sees what the first committed, and the re-check below
|
|
175
|
+
* is what turns that into a refusal instead of an archived asset with an open custody period.
|
|
176
|
+
*
|
|
177
|
+
* @param repairsOn whether the workspace records repairs, read before the transaction opened.
|
|
178
|
+
* A workspace that has switched the capability off has no `under_repair`, because the procedure
|
|
179
|
+
* that would end one answers 404 — `deriveStatus` argues it in full, and this is one of the two
|
|
180
|
+
* write paths that lets an asset out of a status nothing else could move it out of.
|
|
181
|
+
*/
|
|
182
|
+
async stamp(tx, workspaceId, assetId, userId, at, repairsOn) {
|
|
183
|
+
const locked = await lockAsset(tx, workspaceId, assetId);
|
|
184
|
+
/**
|
|
185
|
+
* Re-read under the lock, because the check in `asset()` was made against a snapshot an archive
|
|
186
|
+
* committed a moment later could already have replaced.
|
|
187
|
+
*
|
|
188
|
+
* **Only when somebody is being *given* the item, and that is a narrowing rather than an escape
|
|
189
|
+
* hatch.** A return never reaches this branch with an archived row at all: `return` calls
|
|
190
|
+
* `asset()` first, and `asset()` refuses an archived asset outright, whichever verb asked. The
|
|
191
|
+
* condition is here because a *return* has nothing to race — it can only ever leave the asset
|
|
192
|
+
* unheld — so re-checking it would be a refusal with no state behind it. This comment used to
|
|
193
|
+
* claim the opposite, that a return on an archived asset is deliberately allowed so an item that
|
|
194
|
+
* had reached the impossible state could be undone; it is not allowed, it never was, and the
|
|
195
|
+
* lock in `assets.archive` is what makes that state unreachable rather than merely rare.
|
|
196
|
+
*/
|
|
197
|
+
if (userId !== null && locked.archivedAt)
|
|
198
|
+
throw KernError.conflict('This item is archived. Restore it before handing it over.', 'inventory.custody.archived');
|
|
199
|
+
const status = deriveStatus({
|
|
200
|
+
custodianUserId: userId,
|
|
201
|
+
awayForRepair: await awayForRepair(tx, workspaceId, assetId, repairsOn),
|
|
202
|
+
});
|
|
203
|
+
const [row] = await tx
|
|
204
|
+
.update(assets)
|
|
205
|
+
.set({
|
|
206
|
+
custodianUserId: userId,
|
|
207
|
+
custodySince: at,
|
|
208
|
+
status,
|
|
209
|
+
updatedAt: new Date(),
|
|
210
|
+
})
|
|
211
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
|
|
212
|
+
.returning();
|
|
213
|
+
if (!row)
|
|
214
|
+
throw KernError.notFound('Asset');
|
|
215
|
+
return row;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The latest instant this asset's custody trail already reaches.
|
|
219
|
+
*
|
|
220
|
+
* The end of the most recent closed period, or the start of the open one. One row answers it
|
|
221
|
+
* because the periods for an asset cannot overlap — that is what `inventory_custody_no_overlap`
|
|
222
|
+
* enforces — so the row that starts last also ends last.
|
|
223
|
+
*/
|
|
224
|
+
async boundary(tx, workspaceId, assetId) {
|
|
225
|
+
const [row] = await tx
|
|
226
|
+
.select({ from: custodyPeriods.effectiveFrom, to: custodyPeriods.effectiveTo })
|
|
227
|
+
.from(custodyPeriods)
|
|
228
|
+
.where(and(eq(custodyPeriods.workspaceId, workspaceId), eq(custodyPeriods.assetId, assetId)))
|
|
229
|
+
.orderBy(desc(custodyPeriods.effectiveFrom), desc(custodyPeriods.id))
|
|
230
|
+
.limit(1);
|
|
231
|
+
return row ? (row.to ?? row.from) : null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The instant this change happens, for both halves of it.
|
|
235
|
+
*
|
|
236
|
+
* One value, so the period that closes and the period that opens abut exactly: `'[)'` ranges
|
|
237
|
+
* `[…, at)` and `[at, …)` do not overlap, where two `now()` calls a microsecond apart would leave
|
|
238
|
+
* a gap during which the asset was held by nobody.
|
|
239
|
+
*
|
|
240
|
+
* **It is a millisecond past the trail's own end, not `now()`, and both halves of that are a
|
|
241
|
+
* defect this replaced.**
|
|
242
|
+
*
|
|
243
|
+
* - `max(now, open.effectiveFrom)` produced a **zero-length period** whenever the two were equal,
|
|
244
|
+
* which two handovers inside one millisecond make them — a JS `Date` resolves no finer. `[t, t)`
|
|
245
|
+
* is empty, an empty range overlaps nothing, so the exclusion constraint waves it through and
|
|
246
|
+
* the trail permanently records somebody holding the item for no time at all. Anybody reading
|
|
247
|
+
* "who had this in March" gets a name that was never true. Strictly after the boundary, there
|
|
248
|
+
* is no such row to write.
|
|
249
|
+
* - `assign` used plain `now()`, which a clock that steps backwards puts *before* the end of the
|
|
250
|
+
* last closed period — so the new period overlapped a finished one, Postgres refused it with
|
|
251
|
+
* `23P01`, and the person was told "somebody changed who is holding this a moment before you
|
|
252
|
+
* did. Reload." Nobody had; reloading changes nothing; the handover is refused again every time.
|
|
253
|
+
* Reading the trail's own end rather than the clock makes that unreachable instead of merely
|
|
254
|
+
* better explained: the next period always starts after the last one ended, whatever the clock
|
|
255
|
+
* says.
|
|
256
|
+
*
|
|
257
|
+
* `now` still wins whenever it is ahead, which is every ordinary case — this only ever moves the
|
|
258
|
+
* instant forward, never back, so it cannot manufacture a period that starts before its asset was
|
|
259
|
+
* bought.
|
|
260
|
+
*/
|
|
261
|
+
async instant(tx, workspaceId, assetId) {
|
|
262
|
+
const boundary = await this.boundary(tx, workspaceId, assetId);
|
|
263
|
+
return new Date(Math.max(Date.now(), boundary ? boundary.getTime() + TICK_MS : 0));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Hand a free item to a member.
|
|
267
|
+
*
|
|
268
|
+
* Refuses when somebody already has it, rather than quietly taking it off them: `transfer` is the
|
|
269
|
+
* procedure that means "hand it on", and collapsing the two would make a mistyped assignment
|
|
270
|
+
* indistinguishable from a deliberate handover in the timeline everyone reads afterwards.
|
|
271
|
+
*/
|
|
272
|
+
async assign(tx, workspaceId, actorId, assetId, userId, note, repairsOn) {
|
|
273
|
+
// Called for its two refusals — a row in another workspace is a 404, an archived one a
|
|
274
|
+
// conflict — and not for the row, which `stamp` returns below in its post-handover shape.
|
|
275
|
+
await this.asset(tx, workspaceId, assetId);
|
|
276
|
+
const open = await this.open(tx, workspaceId, assetId);
|
|
277
|
+
if (open)
|
|
278
|
+
throw KernError.conflict(open.userId === userId
|
|
279
|
+
? 'They are already holding this item.'
|
|
280
|
+
: 'Somebody else is holding this item. Hand it on, or take it back first.', 'inventory.custody.already_held');
|
|
281
|
+
const at = await this.instant(tx, workspaceId, assetId);
|
|
282
|
+
const period = await this.openPeriod(tx, workspaceId, assetId, userId, note, actorId, at);
|
|
283
|
+
const row = await this.stamp(tx, workspaceId, assetId, userId, at, repairsOn);
|
|
284
|
+
const activity = {
|
|
285
|
+
workspaceId,
|
|
286
|
+
assetId,
|
|
287
|
+
actorId,
|
|
288
|
+
action: 'assigned',
|
|
289
|
+
data: { userId, ...(note ? { note } : {}) },
|
|
290
|
+
};
|
|
291
|
+
await this.notify.history(tx, activity);
|
|
292
|
+
return {
|
|
293
|
+
asset: row,
|
|
294
|
+
period,
|
|
295
|
+
userId,
|
|
296
|
+
previousUserId: null,
|
|
297
|
+
activity,
|
|
298
|
+
// Never the person doing the handing: telling somebody what they just did is the notification
|
|
299
|
+
// everybody switches the type off over.
|
|
300
|
+
notifyUserId: userId === actorId ? null : userId,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Hand it straight on. One transaction, not a return followed by an assign.
|
|
305
|
+
*
|
|
306
|
+
* Two calls would leave the asset `in_stock` with no custodian in between — visible to anybody
|
|
307
|
+
* reading the list at that moment, and permanently visible in the timeline as a return nobody
|
|
308
|
+
* performed and a stock period nobody spent.
|
|
309
|
+
*/
|
|
310
|
+
async transfer(tx, workspaceId, actorId, assetId, userId, note, repairsOn) {
|
|
311
|
+
await this.asset(tx, workspaceId, assetId);
|
|
312
|
+
const open = await this.open(tx, workspaceId, assetId);
|
|
313
|
+
if (!open)
|
|
314
|
+
throw KernError.conflict('Nobody is holding this item, so there is nothing to hand on. Assign it instead.', 'inventory.custody.not_held');
|
|
315
|
+
if (open.userId === userId)
|
|
316
|
+
throw KernError.conflict('They are already holding this item.', 'inventory.custody.already_held');
|
|
317
|
+
const at = await this.instant(tx, workspaceId, assetId);
|
|
318
|
+
await this.close(tx, workspaceId, open.id, at);
|
|
319
|
+
const period = await this.openPeriod(tx, workspaceId, assetId, userId, note, actorId, at);
|
|
320
|
+
const row = await this.stamp(tx, workspaceId, assetId, userId, at, repairsOn);
|
|
321
|
+
const activity = {
|
|
322
|
+
workspaceId,
|
|
323
|
+
assetId,
|
|
324
|
+
actorId,
|
|
325
|
+
action: 'transferred',
|
|
326
|
+
data: { userId, previousUserId: open.userId, ...(note ? { note } : {}) },
|
|
327
|
+
};
|
|
328
|
+
await this.notify.history(tx, activity);
|
|
329
|
+
return {
|
|
330
|
+
asset: row,
|
|
331
|
+
period,
|
|
332
|
+
userId,
|
|
333
|
+
previousUserId: open.userId,
|
|
334
|
+
activity,
|
|
335
|
+
notifyUserId: userId === actorId ? null : userId,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Take it back. Closes the open period and puts the item back in stock. */
|
|
339
|
+
async return(tx, workspaceId, actorId, assetId, note, repairsOn) {
|
|
340
|
+
await this.asset(tx, workspaceId, assetId);
|
|
341
|
+
const open = await this.open(tx, workspaceId, assetId);
|
|
342
|
+
if (!open)
|
|
343
|
+
throw KernError.conflict('Nobody is holding this item, so there is nothing to take back.', 'inventory.custody.not_held');
|
|
344
|
+
const at = await this.instant(tx, workspaceId, assetId);
|
|
345
|
+
await this.close(tx, workspaceId, open.id, at);
|
|
346
|
+
const row = await this.stamp(tx, workspaceId, assetId, null, null, repairsOn);
|
|
347
|
+
const activity = {
|
|
348
|
+
workspaceId,
|
|
349
|
+
assetId,
|
|
350
|
+
actorId,
|
|
351
|
+
action: 'returned',
|
|
352
|
+
data: { previousUserId: open.userId, ...(note ? { note } : {}) },
|
|
353
|
+
};
|
|
354
|
+
await this.notify.history(tx, activity);
|
|
355
|
+
return {
|
|
356
|
+
asset: row,
|
|
357
|
+
period: null,
|
|
358
|
+
userId: null,
|
|
359
|
+
previousUserId: open.userId,
|
|
360
|
+
activity,
|
|
361
|
+
// A return is not news to anybody: the person who had it knows they handed it back, and the
|
|
362
|
+
// person taking it back is the one making the call.
|
|
363
|
+
notifyUserId: null,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=custody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custody.js","sourceRoot":"","sources":["../../../src/server/services/custody.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The constraint's name, if a named one refused the write. */
|
|
2
|
+
export declare const constraintOf: (err: unknown) => string | undefined;
|
|
3
|
+
/** The SQLSTATE, if one reached us. `23P01` is an exclusion violation, `23505` a unique one. */
|
|
4
|
+
export declare const sqlStateOf: (err: unknown) => string | undefined;
|
|
5
|
+
/** Did this write lose to `name`, whichever way Postgres reported it? */
|
|
6
|
+
export declare const violated: (err: unknown, name: string) => boolean;
|
|
7
|
+
//# sourceMappingURL=db-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-errors.d.ts","sourceRoot":"","sources":["../../../src/server/services/db-errors.ts"],"names":[],"mappings":"AAgCA,+DAA+D;AAC/D,eAAO,MAAM,YAAY,GAAI,KAAK,OAAO,KAAG,MAAM,GAAG,SAC4B,CAAA;AAEjF,gGAAgG;AAChG,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,KAAG,MAAM,GAAG,SACkD,CAAA;AAErG,yEAAyE;AACzE,eAAO,MAAM,QAAQ,GAAI,KAAK,OAAO,EAAE,MAAM,MAAM,KAAG,OAAqC,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which constraint Postgres refused a write with, dug out of what drizzle hands back.
|
|
3
|
+
*
|
|
4
|
+
* A database constraint is only half a feature. `inventory_custody_no_overlap` makes two open
|
|
5
|
+
* custody periods for one asset impossible — but two people pressing *Hand over* on the same laptop
|
|
6
|
+
* at the same instant means the loser is shown drizzle's own text, "Failed query: insert into
|
|
7
|
+
* mod_inventory.custody_periods …", which tells a person nothing they can act on and looks like the
|
|
8
|
+
* product broke rather than like somebody else got there first.
|
|
9
|
+
*
|
|
10
|
+
* So the service catches, asks these two questions, and throws a `KernError.conflict` with a
|
|
11
|
+
* sentence. Reaching through `cause` rather than reading the top frame is the load-bearing part:
|
|
12
|
+
* drizzle wraps the driver's error, and the driver's error is the only object carrying `code` and
|
|
13
|
+
* `constraint`. `src/server/inventory.int.test.ts` walks the same chain for the same reason.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_DEPTH = 5;
|
|
16
|
+
function walk(err, read) {
|
|
17
|
+
let cursor = err;
|
|
18
|
+
for (let depth = 0; depth < MAX_DEPTH && cursor; depth++) {
|
|
19
|
+
const found = read(cursor);
|
|
20
|
+
if (found)
|
|
21
|
+
return found;
|
|
22
|
+
cursor = cursor.cause;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The constraint's name, if a named one refused the write. */
|
|
27
|
+
export const constraintOf = (err) => walk(err, (e) => (typeof e.constraint === 'string' ? e.constraint : undefined));
|
|
28
|
+
/** The SQLSTATE, if one reached us. `23P01` is an exclusion violation, `23505` a unique one. */
|
|
29
|
+
export const sqlStateOf = (err) => walk(err, (e) => (typeof e.code === 'string' && /^[0-9A-Z]{5}$/.test(e.code) ? e.code : undefined));
|
|
30
|
+
/** Did this write lose to `name`, whichever way Postgres reported it? */
|
|
31
|
+
export const violated = (err, name) => constraintOf(err) === name;
|
|
32
|
+
//# sourceMappingURL=db-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-errors.js","sourceRoot":"","sources":["../../../src/server/services/db-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,SAAS,GAAG,CAAC,CAAA;AAQnB,SAAS,IAAI,CAAC,GAAY,EAAE,IAA4C;IACtE,IAAI,MAAM,GAAY,GAAG,CAAA;IACzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,IAAI,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QACzD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAqB,CAAC,CAAA;QACzC,IAAI,KAAK;YAAE,OAAO,KAAK,CAAA;QACvB,MAAM,GAAI,MAAsB,CAAC,KAAK,CAAA;IACxC,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAY,EAAsB,EAAE,CAC/D,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAEjF,gGAAgG;AAChG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAY,EAAsB,EAAE,CAC7D,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAErG,yEAAyE;AACzE,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAAY,EAAE,IAAY,EAAW,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA"}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import type { Kernel } from '@kernhq/kernel';
|
|
2
2
|
import { AssetService } from './assets.js';
|
|
3
|
+
import { AttachmentService } from './attachments.js';
|
|
4
|
+
import { CategoryService } from './categories.js';
|
|
5
|
+
import { CustodyService } from './custody.js';
|
|
3
6
|
import { NotifyService } from './notify.js';
|
|
7
|
+
import { OffboardingService } from './offboarding.js';
|
|
8
|
+
import { RepairService } from './repairs.js';
|
|
9
|
+
import { SearchService } from './search.js';
|
|
10
|
+
import { StatsService } from './stats.js';
|
|
4
11
|
export interface InventoryServices {
|
|
5
12
|
notify: NotifyService;
|
|
6
13
|
assets: AssetService;
|
|
14
|
+
categories: CategoryService;
|
|
15
|
+
custody: CustodyService;
|
|
16
|
+
repairs: RepairService;
|
|
17
|
+
attachments: AttachmentService;
|
|
18
|
+
stats: StatsService;
|
|
19
|
+
/** The workspace-wide index, and the documents behind `inventory:asset:<id>`. */
|
|
20
|
+
search: SearchService;
|
|
21
|
+
/** Somebody left holding company property. Raises a checklist; never moves custody. */
|
|
22
|
+
offboarding: OffboardingService;
|
|
7
23
|
}
|
|
8
24
|
/** One service graph per kernel instance; the router, jobs and procedures all share it. */
|
|
9
25
|
export declare function inventoryServices(kernel: Kernel): InventoryServices;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEzC,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,eAAe,CAAA;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,OAAO,EAAE,aAAa,CAAA;IACtB,WAAW,EAAE,iBAAiB,CAAA;IAC9B,KAAK,EAAE,YAAY,CAAA;IACnB,iFAAiF;IACjF,MAAM,EAAE,aAAa,CAAA;IACrB,uFAAuF;IACvF,WAAW,EAAE,kBAAkB,CAAA;CAChC;AAID,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CA2BnE"}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { AssetService } from './assets.js';
|
|
2
|
+
import { AttachmentService } from './attachments.js';
|
|
3
|
+
import { CategoryService } from './categories.js';
|
|
4
|
+
import { CustodyService } from './custody.js';
|
|
2
5
|
import { NotifyService } from './notify.js';
|
|
6
|
+
import { OffboardingService } from './offboarding.js';
|
|
7
|
+
import { RepairService } from './repairs.js';
|
|
8
|
+
import { SearchService } from './search.js';
|
|
9
|
+
import { StatsService } from './stats.js';
|
|
3
10
|
const cache = new WeakMap();
|
|
4
11
|
/** One service graph per kernel instance; the router, jobs and procedures all share it. */
|
|
5
12
|
export function inventoryServices(kernel) {
|
|
@@ -8,7 +15,24 @@ export function inventoryServices(kernel) {
|
|
|
8
15
|
return existing;
|
|
9
16
|
const notify = new NotifyService(kernel);
|
|
10
17
|
const assets = new AssetService(kernel, notify);
|
|
11
|
-
const
|
|
18
|
+
const categories = new CategoryService();
|
|
19
|
+
const custody = new CustodyService(notify);
|
|
20
|
+
const repairs = new RepairService(notify);
|
|
21
|
+
const attachments = new AttachmentService(kernel, notify);
|
|
22
|
+
const stats = new StatsService();
|
|
23
|
+
const search = new SearchService(kernel, notify);
|
|
24
|
+
const offboarding = new OffboardingService(kernel, notify);
|
|
25
|
+
const services = {
|
|
26
|
+
notify,
|
|
27
|
+
assets,
|
|
28
|
+
categories,
|
|
29
|
+
custody,
|
|
30
|
+
repairs,
|
|
31
|
+
attachments,
|
|
32
|
+
stats,
|
|
33
|
+
search,
|
|
34
|
+
offboarding,
|
|
35
|
+
};
|
|
12
36
|
cache.set(kernel, services);
|
|
13
37
|
return services;
|
|
14
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAgBzC,MAAM,KAAK,GAAG,IAAI,OAAO,EAA6B,CAAA;AAEtD,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAClC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAE7B,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/C,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAA;IAC1C,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAA;IACzC,MAAM,WAAW,GAAG,IAAI,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACzD,MAAM,KAAK,GAAG,IAAI,YAAY,EAAE,CAAA;IAChC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChD,MAAM,WAAW,GAAG,IAAI,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE1D,MAAM,QAAQ,GAAsB;QAClC,MAAM;QACN,MAAM;QACN,UAAU;QACV,OAAO;QACP,OAAO;QACP,WAAW;QACX,KAAK;QACL,MAAM;QACN,WAAW;KACZ,CAAA;IACD,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC3B,OAAO,QAAQ,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Kernel } from '@kernhq/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* Is this person somebody this workspace can hand a laptop to?
|
|
4
|
+
*
|
|
5
|
+
* **`custody.assign` and `custody.transfer` took any uuid.** The id arrives in the request, nothing
|
|
6
|
+
* looked it up, and the module then wrote it into `custody_periods.user_id` and
|
|
7
|
+
* `assets.custodian_user_id` — the two columns that answer "who is answerable for this" for the rest
|
|
8
|
+
* of the item's life — and sent a notification to it. So a member of one workspace could record a
|
|
9
|
+
* *stranger* as holding company property, and the product would then chase that stranger about it.
|
|
10
|
+
* Every screen renders an id it cannot resolve as "a former member", which is the honest thing for a
|
|
11
|
+
* screen to say and exactly what hides this: the register looks plausible and names nobody real.
|
|
12
|
+
*
|
|
13
|
+
* The check is a question for core, because membership is core's fact and this module has no copy of
|
|
14
|
+
* it. `core.users.principal` answers with one person rather than the workspace's whole roll, which
|
|
15
|
+
* matters on an instance where a workspace has thousands of seats and this runs on every handover.
|
|
16
|
+
*
|
|
17
|
+
* **`active`, not merely present.** An invitation nobody has accepted is not somebody who can be
|
|
18
|
+
* handed a thing, and a suspended account is one the workspace has deliberately shut out.
|
|
19
|
+
*
|
|
20
|
+
* **A failure here refuses rather than waves through.** Every other cross-service call this module
|
|
21
|
+
* makes is best-effort, and this one deliberately is not: swallowing the failure would mean the
|
|
22
|
+
* check silently stops existing exactly while core is unwell, which is indistinguishable from not
|
|
23
|
+
* having written it. `UNAVAILABLE` is 503 and says the true thing — the answer is not knowable right
|
|
24
|
+
* now — where a 400 would tell somebody their colleague is not a member.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireWorkspaceMember(kernel: Kernel, workspaceId: string, userId: string): Promise<void>;
|
|
27
|
+
//# sourceMappingURL=members.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"members.d.ts","sourceRoot":"","sources":["../../../src/server/services/members.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAsBf"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { KernError } from '@kernhq/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* Is this person somebody this workspace can hand a laptop to?
|
|
4
|
+
*
|
|
5
|
+
* **`custody.assign` and `custody.transfer` took any uuid.** The id arrives in the request, nothing
|
|
6
|
+
* looked it up, and the module then wrote it into `custody_periods.user_id` and
|
|
7
|
+
* `assets.custodian_user_id` — the two columns that answer "who is answerable for this" for the rest
|
|
8
|
+
* of the item's life — and sent a notification to it. So a member of one workspace could record a
|
|
9
|
+
* *stranger* as holding company property, and the product would then chase that stranger about it.
|
|
10
|
+
* Every screen renders an id it cannot resolve as "a former member", which is the honest thing for a
|
|
11
|
+
* screen to say and exactly what hides this: the register looks plausible and names nobody real.
|
|
12
|
+
*
|
|
13
|
+
* The check is a question for core, because membership is core's fact and this module has no copy of
|
|
14
|
+
* it. `core.users.principal` answers with one person rather than the workspace's whole roll, which
|
|
15
|
+
* matters on an instance where a workspace has thousands of seats and this runs on every handover.
|
|
16
|
+
*
|
|
17
|
+
* **`active`, not merely present.** An invitation nobody has accepted is not somebody who can be
|
|
18
|
+
* handed a thing, and a suspended account is one the workspace has deliberately shut out.
|
|
19
|
+
*
|
|
20
|
+
* **A failure here refuses rather than waves through.** Every other cross-service call this module
|
|
21
|
+
* makes is best-effort, and this one deliberately is not: swallowing the failure would mean the
|
|
22
|
+
* check silently stops existing exactly while core is unwell, which is indistinguishable from not
|
|
23
|
+
* having written it. `UNAVAILABLE` is 503 and says the true thing — the answer is not knowable right
|
|
24
|
+
* now — where a 400 would tell somebody their colleague is not a member.
|
|
25
|
+
*/
|
|
26
|
+
export async function requireWorkspaceMember(kernel, workspaceId, userId) {
|
|
27
|
+
let principal;
|
|
28
|
+
try {
|
|
29
|
+
principal = await kernel.call('core.users.principal', { userId });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
kernel.log.warn({ err: err instanceof Error ? err.message : String(err), workspaceId }, 'inventory: could not check whether the new custodian is a member');
|
|
33
|
+
throw new KernError('UNAVAILABLE', 'Who belongs to this workspace cannot be checked right now. Try again in a moment.');
|
|
34
|
+
}
|
|
35
|
+
const member = (principal?.memberships ?? []).some((m) => m.workspaceId === workspaceId && m.status === 'active');
|
|
36
|
+
if (!member)
|
|
37
|
+
throw KernError.badRequest('That person is not a member of this workspace, so nothing can be handed to them.');
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=members.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"members.js","sourceRoot":"","sources":["../../../src/server/services/members.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAe,MAAM,gBAAgB,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAc,EACd,WAAmB,EACnB,MAAc;IAEd,IAAI,SAA2B,CAAA;IAC/B,IAAI,CAAC;QACH,SAAS,GAAG,MAAM,MAAM,CAAC,IAAI,CAAmB,sBAAsB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IACrF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,CAAC,IAAI,CACb,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,EACtE,kEAAkE,CACnE,CAAA;QACD,MAAM,IAAI,SAAS,CACjB,aAAa,EACb,mFAAmF,CACpF,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,CAChD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,WAAW,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAC9D,CAAA;IACD,IAAI,CAAC,MAAM;QACT,MAAM,SAAS,CAAC,UAAU,CACxB,kFAAkF,CACnF,CAAA;AACL,CAAC"}
|