@kernhq/module-inventory 0.1.2 → 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 +134 -9
- package/dist/contract/capabilities.d.ts +49 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +94 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +76 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +62 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +16 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +468 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +320 -0
- package/dist/contract/models.js.map +1 -0
- 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 +72 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +79 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/contract/router.d.ts +1378 -0
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +266 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/contract/settings.d.ts +20 -0
- package/dist/contract/settings.d.ts.map +1 -0
- package/dist/contract/settings.js +49 -0
- package/dist/contract/settings.js.map +1 -0
- package/dist/server/index.d.ts +8 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +288 -8
- 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 +1789 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +439 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +156 -9
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +183 -11
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +139 -0
- package/dist/server/services/assets.d.ts.map +1 -0
- package/dist/server/services/assets.js +429 -0
- package/dist/server/services/assets.js.map +1 -0
- 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 +26 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +39 -0
- package/dist/server/services/index.js.map +1 -0
- 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 +105 -0
- package/dist/server/services/notify.d.ts.map +1 -0
- package/dist/server/services/notify.js +147 -0
- package/dist/server/services/notify.js.map +1 -0
- 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/0000_init.sql +12 -3
- package/migrations/0001_rls.sql +24 -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/0000_snapshot.json +40 -13
- 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 +44 -2
- package/package.json +5 -4
- package/src/client/api-instance.ts +27 -2
- package/src/client/api.ts +1 -1
- 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 +191 -59
- 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/i18n.ts +11 -166
- package/src/client/index.ts +8 -1
- 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 +296 -0
- package/src/client/messages.ts +1424 -0
- package/src/client/mock.test.ts +555 -0
- package/src/client/mock.ts +1261 -52
- package/src/client/module.ts +76 -2
- package/src/client/pages/AssetsPage.svelte +638 -145
- package/src/client/permissions.ts +8 -1
- package/src/client/price.test.ts +254 -0
- package/src/client/price.ts +279 -0
- package/src/client/query.test.ts +58 -0
- package/src/client/query.ts +51 -2
- 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 +403 -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 +140 -26
- package/src/client/widgets/RepairsWidget.svelte +124 -0
- package/src/contract/capabilities.ts +99 -0
- package/src/contract/events.ts +83 -0
- package/src/contract/index.ts +16 -0
- package/src/contract/models.ts +360 -0
- package/src/contract/notifications.ts +73 -0
- package/src/contract/permissions.ts +79 -0
- package/src/contract/router.ts +300 -0
- package/src/contract/settings.ts +50 -0
- package/src/module.test.ts +330 -7
- package/src/server/index.ts +318 -8
- package/src/server/inventory.int.test.ts +4374 -0
- package/src/server/jobs.ts +444 -0
- package/src/server/migrations.test.ts +251 -0
- package/src/server/router.ts +574 -0
- package/src/server/schema.ts +184 -10
- package/src/server/services/assets.ts +528 -0
- 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 +56 -0
- package/src/server/services/members.ts +54 -0
- package/src/server/services/notify.ts +196 -0
- 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/tsconfig.base.json +22 -0
- package/tsconfig.client.json +1 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +18 -3
- package/dist/contract.d.ts +0 -387
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js +0 -119
- package/dist/contract.js.map +0 -1
- package/dist/server/_impl.d.ts +0 -427
- package/dist/server/_impl.d.ts.map +0 -1
- package/dist/server/_impl.js +0 -204
- package/dist/server/_impl.js.map +0 -1
- package/src/contract.ts +0 -143
- package/src/server/_impl.ts +0 -275
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type Kernel, type Tx } from '@kernhq/kernel';
|
|
2
|
+
import type { AssetCreateInput, AssetHistoryEntry, Asset as AssetModel, AssetPatchInput, AssetSort } from '../../contract/models.js';
|
|
3
|
+
import { assetHistory, assets } from '../schema.js';
|
|
4
|
+
import type { HistoryInput, NotifyService } from './notify.js';
|
|
5
|
+
type Row = typeof assets.$inferSelect;
|
|
6
|
+
/**
|
|
7
|
+
* What a mutation wrote, and what may only leave the module once it has committed.
|
|
8
|
+
*
|
|
9
|
+
* `asset_history` is written inside the caller's transaction and is authoritative; the mirror of it
|
|
10
|
+
* in core's activity feed is not, and used to be fired off with `void` while the transaction was
|
|
11
|
+
* still open. A rollback then left the workspace's feed showing an event for an id that does not
|
|
12
|
+
* exist. Everything else this module announces — the event, the realtime change — already waits for
|
|
13
|
+
* the commit, so the activity record is handed back here and flushed beside them.
|
|
14
|
+
*/
|
|
15
|
+
export interface Written {
|
|
16
|
+
row: Row;
|
|
17
|
+
/** `null` when nothing happened worth recording, which is what an update that changed nothing is. */
|
|
18
|
+
activity: HistoryInput | null;
|
|
19
|
+
}
|
|
20
|
+
/** How this workspace spells an asset tag. Read before the transaction opens — see `codeFormat`. */
|
|
21
|
+
export interface CodeFormat {
|
|
22
|
+
prefix: string;
|
|
23
|
+
pad: number;
|
|
24
|
+
}
|
|
25
|
+
/** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
|
|
26
|
+
export declare function toAsset(row: Row): AssetModel;
|
|
27
|
+
/**
|
|
28
|
+
* The wire shape of one timeline entry. Drizzle's `jsonb` columns are nullable in the row and never
|
|
29
|
+
* null in the contract — `changes: []` and `data: {}` are what "nothing to say" looks like to a
|
|
30
|
+
* screen, where `null` is one more thing every caller has to remember to handle.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toHistoryEntry(row: typeof assetHistory.$inferSelect): AssetHistoryEntry;
|
|
33
|
+
export interface ListInput {
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
limit: number;
|
|
36
|
+
cursor?: string;
|
|
37
|
+
q?: string;
|
|
38
|
+
categoryId?: string;
|
|
39
|
+
status?: string;
|
|
40
|
+
custodianUserId?: string;
|
|
41
|
+
archived: boolean;
|
|
42
|
+
sort: AssetSort;
|
|
43
|
+
}
|
|
44
|
+
export declare class AssetService {
|
|
45
|
+
private readonly kernel;
|
|
46
|
+
private readonly notify;
|
|
47
|
+
constructor(kernel: Kernel, notify: NotifyService);
|
|
48
|
+
/**
|
|
49
|
+
* How this workspace spells an asset tag.
|
|
50
|
+
*
|
|
51
|
+
* **Called before the transaction opens, never inside one.** `kernel.settings.module` is a
|
|
52
|
+
* `core.settings.getModule` call over the broker, and awaiting a remote service while holding a
|
|
53
|
+
* pooled connection *and* the counter row lock is two failures waiting: the pool starves under
|
|
54
|
+
* concurrent creates, and a create fails outright whenever core is briefly away — which is the
|
|
55
|
+
* one thing `NotifyService`'s own docblock says a mutation here must never do.
|
|
56
|
+
*/
|
|
57
|
+
codeFormat(workspaceId: string): Promise<CodeFormat>;
|
|
58
|
+
/**
|
|
59
|
+
* The next asset tag for a workspace.
|
|
60
|
+
*
|
|
61
|
+
* One narrow row per workspace and key, incremented under the insert's conflict lock — two
|
|
62
|
+
* concurrent creates each read the value their own statement returned, so codes stay unique
|
|
63
|
+
* without a retry loop. `INV-0042`, because people say asset tags out loud. Nothing in here
|
|
64
|
+
* leaves the database, which is what keeps the lock short.
|
|
65
|
+
*/
|
|
66
|
+
nextCode(tx: Tx, workspaceId: string, format: CodeFormat): Promise<string>;
|
|
67
|
+
list(tx: Tx, input: ListInput): Promise<{
|
|
68
|
+
items: AssetModel[];
|
|
69
|
+
nextCursor: string | null;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* The asset's own timeline, newest first, paged on the sequence the entries carry.
|
|
73
|
+
*
|
|
74
|
+
* **The ordering is `seq`, not `occurred_at` and not the id**, and the middle one of those three
|
|
75
|
+
* is the interesting correction.
|
|
76
|
+
*
|
|
77
|
+
* `occurred_at` was never a candidate: it defaults to `now()`, which is the *transaction*
|
|
78
|
+
* timestamp, so `create` writes the asset and its `created` entry with the same value and a page
|
|
79
|
+
* boundary between two entries that share a sort key repeats one and drops the other.
|
|
80
|
+
*
|
|
81
|
+
* The id looked like the answer and was only half of one. A uuidv7 is unique, so paging on it is
|
|
82
|
+
* gapless — but the kernel's `uuidv7()` carries the clock only to the millisecond and fills the
|
|
83
|
+
* rest from `randomUUID()` with no counter, so two entries written inside one millisecond sort by
|
|
84
|
+
* ten random bytes. Stable, so nothing was ever dropped or repeated; and arbitrary, so a timeline
|
|
85
|
+
* could show "Bruno removed the file" above "Bruno added the file". `seq` is a sequence: strictly
|
|
86
|
+
* increasing whatever the clock does, at whatever rate. `schema.ts` argues it at the column.
|
|
87
|
+
*
|
|
88
|
+
* The comparison is a single column because the sort key *is* the bookmark, so there is no
|
|
89
|
+
* bookmarked-row subquery here. The bookmark is still checked as a bounded positive integer and
|
|
90
|
+
* still bound to the sort it was issued under, because that is the codec's job and not this
|
|
91
|
+
* query's.
|
|
92
|
+
*/
|
|
93
|
+
history(tx: Tx, workspaceId: string, assetId: string, input: {
|
|
94
|
+
limit: number;
|
|
95
|
+
cursor?: string;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
items: AssetHistoryEntry[];
|
|
98
|
+
nextCursor: string | null;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* A category id in a request is a claim about a row in this workspace, and it is checked before
|
|
102
|
+
* it is stored.
|
|
103
|
+
*
|
|
104
|
+
* The same defect `photoFileId` had, one field over: `assets.create` and `assets.update` took
|
|
105
|
+
* `categoryId` on trust, so any uuid at all went into the column. Two things follow from that,
|
|
106
|
+
* and neither is theoretical. An id belonging to **another workspace** files an asset under a
|
|
107
|
+
* category this one cannot see, name or unfile — the picker resolves names from its own
|
|
108
|
+
* `categories.list`, so the field renders blank and the filter it sits behind matches nothing
|
|
109
|
+
* anybody can select. An id belonging to **nobody** does the same thing with no other workspace
|
|
110
|
+
* involved. Either way the register holds a reference to a row that is not there, which is
|
|
111
|
+
* exactly the state `categories.archive` exists to prevent from the other end.
|
|
112
|
+
*
|
|
113
|
+
* **Checked inside the caller's transaction, unlike `checkPhoto`.** A file lives in core and has
|
|
114
|
+
* to be asked for over the broker, so that check happens before a connection is taken — the rule
|
|
115
|
+
* `codeFormat` documents. A category is this module's own table one join away, so the honest
|
|
116
|
+
* place for it is the transaction that writes the row: it costs one indexed read, and it cannot
|
|
117
|
+
* be raced by somebody deleting the category, because nothing deletes one.
|
|
118
|
+
*
|
|
119
|
+
* An **archived** category is deliberately allowed. Archiving takes it out of every picker and
|
|
120
|
+
* leaves every asset already filed under it still naming it; refusing it here would mean an edit
|
|
121
|
+
* to an asset's location failed because of a category somebody tidied away last year, and it
|
|
122
|
+
* would make re-filing a batch of assets under an old category impossible for no gain.
|
|
123
|
+
*/
|
|
124
|
+
private requireCategory;
|
|
125
|
+
get(tx: Tx, workspaceId: string, assetId: string): Promise<Row>;
|
|
126
|
+
create(tx: Tx, workspaceId: string, actorId: string | null, input: AssetCreateInput, format: CodeFormat): Promise<Written>;
|
|
127
|
+
/** Fields a person edits directly, and therefore the fields the timeline reports a diff for. */
|
|
128
|
+
private static readonly EDITABLE;
|
|
129
|
+
update(tx: Tx, workspaceId: string, actorId: string | null, assetId: string, input: AssetPatchInput): Promise<Written>;
|
|
130
|
+
/**
|
|
131
|
+
* @param repairsOn whether the workspace records repairs, read before the transaction opened.
|
|
132
|
+
* The "it is away for repair" refusal below tells somebody to go and use `repairs.complete`, and
|
|
133
|
+
* that procedure answers 404 in a workspace with the capability off — so the refusal would be an
|
|
134
|
+
* instruction to open a door that is not there, and the asset could never leave the register.
|
|
135
|
+
*/
|
|
136
|
+
archive(tx: Tx, workspaceId: string, actorId: string | null, assetId: string, archived: boolean, repairsOn: boolean): Promise<Written>;
|
|
137
|
+
}
|
|
138
|
+
export {};
|
|
139
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../../src/server/services/assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { KernError, uuidv7 } from '@kernhq/kernel';
|
|
2
|
+
import { and, asc, desc, eq, ilike, isNull, lt, or, sql } from 'drizzle-orm';
|
|
3
|
+
import { alias } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { InventorySettings } from '../../contract/settings.js';
|
|
5
|
+
import { assetHistory, assets, categories, counters } from '../schema.js';
|
|
6
|
+
import { decodeMark, decodeSeqMark, encodeMark, encodeSeqMark } from './cursor.js';
|
|
7
|
+
import { openRepairId } from './status.js';
|
|
8
|
+
/** The wire shape: drizzle gives Date objects for timestamps, the contract promises ISO strings. */
|
|
9
|
+
export function toAsset(row) {
|
|
10
|
+
return {
|
|
11
|
+
id: row.id,
|
|
12
|
+
workspaceId: row.workspaceId,
|
|
13
|
+
code: row.code,
|
|
14
|
+
name: row.name,
|
|
15
|
+
description: row.description,
|
|
16
|
+
categoryId: row.categoryId,
|
|
17
|
+
status: row.status,
|
|
18
|
+
custodianUserId: row.custodianUserId,
|
|
19
|
+
custodySince: row.custodySince?.toISOString() ?? null,
|
|
20
|
+
serialNumber: row.serialNumber,
|
|
21
|
+
location: row.location,
|
|
22
|
+
purchasedOn: row.purchasedOn ?? null,
|
|
23
|
+
purchasedFrom: row.purchasedFrom,
|
|
24
|
+
priceMinor: row.priceMinor,
|
|
25
|
+
currency: row.currency,
|
|
26
|
+
warrantyUntil: row.warrantyUntil ?? null,
|
|
27
|
+
photoFileId: row.photoFileId,
|
|
28
|
+
custom: row.custom ?? {},
|
|
29
|
+
createdAt: row.createdAt.toISOString(),
|
|
30
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
31
|
+
archivedAt: row.archivedAt?.toISOString() ?? null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The wire shape of one timeline entry. Drizzle's `jsonb` columns are nullable in the row and never
|
|
36
|
+
* null in the contract — `changes: []` and `data: {}` are what "nothing to say" looks like to a
|
|
37
|
+
* screen, where `null` is one more thing every caller has to remember to handle.
|
|
38
|
+
*/
|
|
39
|
+
export function toHistoryEntry(row) {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
assetId: row.assetId,
|
|
43
|
+
actorId: row.actorId,
|
|
44
|
+
action: row.action,
|
|
45
|
+
changes: row.changes ?? [],
|
|
46
|
+
data: row.data ?? {},
|
|
47
|
+
occurredAt: row.occurredAt.toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `%`, `_` and `\` are pattern syntax to `ilike`, not text.
|
|
52
|
+
*
|
|
53
|
+
* Unescaped, a search for `50%` matched every row in the workspace and a search for `_hair` matched
|
|
54
|
+
* "Chair" — both of which read as a broken search rather than as a clever one. Escaped rather than
|
|
55
|
+
* stripped: somebody typing a per-cent sign means the character.
|
|
56
|
+
*/
|
|
57
|
+
const contains = (q) => `%${q.replace(/[\\%_]/g, '\\$&')}%`;
|
|
58
|
+
/**
|
|
59
|
+
* The one ordering a timeline has, named rather than left as a literal.
|
|
60
|
+
*
|
|
61
|
+
* The cursor codec binds a bookmark to the sort it was issued under and refuses it under any other,
|
|
62
|
+
* so a timeline needs a name for its sort even while there is only one — and the day a second one
|
|
63
|
+
* exists, every cursor already in a browser tab is refused rather than misread.
|
|
64
|
+
*/
|
|
65
|
+
const HISTORY_SORT = 'recent';
|
|
66
|
+
/** Which column an ordering sorts on, for the list itself and for the bookmarked row alike. */
|
|
67
|
+
const sortKey = (t, sort) => sort === 'name' ? t.name : sort === 'code' ? t.code : t.id;
|
|
68
|
+
export class AssetService {
|
|
69
|
+
kernel;
|
|
70
|
+
notify;
|
|
71
|
+
constructor(kernel, notify) {
|
|
72
|
+
this.kernel = kernel;
|
|
73
|
+
this.notify = notify;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* How this workspace spells an asset tag.
|
|
77
|
+
*
|
|
78
|
+
* **Called before the transaction opens, never inside one.** `kernel.settings.module` is a
|
|
79
|
+
* `core.settings.getModule` call over the broker, and awaiting a remote service while holding a
|
|
80
|
+
* pooled connection *and* the counter row lock is two failures waiting: the pool starves under
|
|
81
|
+
* concurrent creates, and a create fails outright whenever core is briefly away — which is the
|
|
82
|
+
* one thing `NotifyService`'s own docblock says a mutation here must never do.
|
|
83
|
+
*/
|
|
84
|
+
async codeFormat(workspaceId) {
|
|
85
|
+
const settings = await this.kernel.settings.module(workspaceId, 'inventory', InventorySettings);
|
|
86
|
+
return { prefix: settings.assetCodePrefix, pad: settings.assetCodePad };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The next asset tag for a workspace.
|
|
90
|
+
*
|
|
91
|
+
* One narrow row per workspace and key, incremented under the insert's conflict lock — two
|
|
92
|
+
* concurrent creates each read the value their own statement returned, so codes stay unique
|
|
93
|
+
* without a retry loop. `INV-0042`, because people say asset tags out loud. Nothing in here
|
|
94
|
+
* leaves the database, which is what keeps the lock short.
|
|
95
|
+
*/
|
|
96
|
+
async nextCode(tx, workspaceId, format) {
|
|
97
|
+
const [row] = await tx
|
|
98
|
+
.insert(counters)
|
|
99
|
+
.values({ workspaceId, key: 'asset_code', value: 1 })
|
|
100
|
+
.onConflictDoUpdate({
|
|
101
|
+
target: [counters.workspaceId, counters.key],
|
|
102
|
+
set: { value: sql `${counters.value} + 1` },
|
|
103
|
+
})
|
|
104
|
+
.returning();
|
|
105
|
+
return `${format.prefix}${String(row.value).padStart(format.pad, '0')}`;
|
|
106
|
+
}
|
|
107
|
+
async list(tx, input) {
|
|
108
|
+
const filters = [eq(assets.workspaceId, input.workspaceId)];
|
|
109
|
+
if (input.q) {
|
|
110
|
+
// A code is something somebody reads off a sticker; matching name, code and serial loosely
|
|
111
|
+
// matters more than word-splitting here. Full-text arrives with the core indexer.
|
|
112
|
+
const pattern = contains(input.q);
|
|
113
|
+
filters.push(or(ilike(assets.name, pattern), ilike(assets.code, pattern), ilike(sql `coalesce(${assets.serialNumber}, '')`, pattern)));
|
|
114
|
+
}
|
|
115
|
+
if (input.categoryId)
|
|
116
|
+
filters.push(eq(assets.categoryId, input.categoryId));
|
|
117
|
+
if (input.status)
|
|
118
|
+
filters.push(eq(assets.status, input.status));
|
|
119
|
+
if (input.custodianUserId)
|
|
120
|
+
filters.push(eq(assets.custodianUserId, input.custodianUserId));
|
|
121
|
+
if (!input.archived)
|
|
122
|
+
filters.push(isNull(assets.archivedAt));
|
|
123
|
+
/**
|
|
124
|
+
* `recent` sorts by id rather than by `created_at`: ids are uuidv7, so they carry the clock, and
|
|
125
|
+
* one indexed unique column is a cheaper and unambiguous page boundary than a timestamp two rows
|
|
126
|
+
* can share. Ordered to the millisecond and no finer — the kernel's `uuidv7()` fills its last
|
|
127
|
+
* ten bytes from `randomUUID()` with no counter — so two assets created inside one millisecond
|
|
128
|
+
* come back in a stable but arbitrary order relative to each other.
|
|
129
|
+
*
|
|
130
|
+
* **The timeline deliberately stopped accepting that and this list has not**, so the difference
|
|
131
|
+
* is worth stating rather than leaving as an inconsistency. `asset_history` is a record somebody
|
|
132
|
+
* argues from, and a millisecond there holds a create and its own first entry, so an arbitrary
|
|
133
|
+
* order is a lie about causation — it has a sequence of its own now (`schema.ts`, `seq`). A list
|
|
134
|
+
* is an ordering of things a person is browsing: nothing hangs on which of two assets imported
|
|
135
|
+
* in the same millisecond is shown first, and paging stays gapless either way because the id is
|
|
136
|
+
* unique. If that ever stops being true — a bulk import whose order somebody relies on — the
|
|
137
|
+
* fix is the same sequence, not a timestamp: `created_at` defaults to the *transaction's*
|
|
138
|
+
* `now()`, so one import shares it exactly.
|
|
139
|
+
*
|
|
140
|
+
* It is fatal for a test that expects creation order; see `inventory.int.test.ts`.
|
|
141
|
+
*/
|
|
142
|
+
const column = sortKey(assets, input.sort);
|
|
143
|
+
const descending = input.sort === 'recent';
|
|
144
|
+
if (input.cursor) {
|
|
145
|
+
const mark = decodeMark(input.cursor, input.sort);
|
|
146
|
+
/**
|
|
147
|
+
* The sort key comes back out of the bookmarked row rather than out of the cursor, which is
|
|
148
|
+
* what keeps the cursor small and stops it disagreeing with the row it names. Scoped by
|
|
149
|
+
* `workspace_id` as well as by id, so a cursor cannot be used to probe another workspace's
|
|
150
|
+
* ordering — this module reaches the database as a superuser in `core`, with RLS bypassed.
|
|
151
|
+
*
|
|
152
|
+
* A row deleted between two pages leaves the subquery empty, the row comparison NULL and the
|
|
153
|
+
* page empty: the list simply ends. That is deliberate, and preferred to a 400 — somebody
|
|
154
|
+
* else archiving a row while you read is an ordinary race, not a malformed request, and an
|
|
155
|
+
* error toast on "Load more" would be a worse answer than a list that has run out.
|
|
156
|
+
*/
|
|
157
|
+
const marker = alias(assets, 'page_marker');
|
|
158
|
+
const bookmarked = tx
|
|
159
|
+
.select({ key: sortKey(marker, input.sort), id: marker.id })
|
|
160
|
+
.from(marker)
|
|
161
|
+
.where(and(eq(marker.id, mark.i), eq(marker.workspaceId, input.workspaceId)));
|
|
162
|
+
filters.push(descending
|
|
163
|
+
? sql `(${column}, ${assets.id}) < (${bookmarked})`
|
|
164
|
+
: sql `(${column}, ${assets.id}) > (${bookmarked})`);
|
|
165
|
+
}
|
|
166
|
+
const order = descending ? [desc(column), desc(assets.id)] : [asc(column), asc(assets.id)];
|
|
167
|
+
const rows = await tx
|
|
168
|
+
.select()
|
|
169
|
+
.from(assets)
|
|
170
|
+
.where(and(...filters))
|
|
171
|
+
.orderBy(...order)
|
|
172
|
+
.limit(input.limit + 1);
|
|
173
|
+
const items = rows.slice(0, input.limit);
|
|
174
|
+
const last = items.at(-1);
|
|
175
|
+
const nextCursor = rows.length > input.limit && last ? encodeMark({ i: last.id, s: input.sort }) : null;
|
|
176
|
+
return { items: items.map(toAsset), nextCursor };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The asset's own timeline, newest first, paged on the sequence the entries carry.
|
|
180
|
+
*
|
|
181
|
+
* **The ordering is `seq`, not `occurred_at` and not the id**, and the middle one of those three
|
|
182
|
+
* is the interesting correction.
|
|
183
|
+
*
|
|
184
|
+
* `occurred_at` was never a candidate: it defaults to `now()`, which is the *transaction*
|
|
185
|
+
* timestamp, so `create` writes the asset and its `created` entry with the same value and a page
|
|
186
|
+
* boundary between two entries that share a sort key repeats one and drops the other.
|
|
187
|
+
*
|
|
188
|
+
* The id looked like the answer and was only half of one. A uuidv7 is unique, so paging on it is
|
|
189
|
+
* gapless — but the kernel's `uuidv7()` carries the clock only to the millisecond and fills the
|
|
190
|
+
* rest from `randomUUID()` with no counter, so two entries written inside one millisecond sort by
|
|
191
|
+
* ten random bytes. Stable, so nothing was ever dropped or repeated; and arbitrary, so a timeline
|
|
192
|
+
* could show "Bruno removed the file" above "Bruno added the file". `seq` is a sequence: strictly
|
|
193
|
+
* increasing whatever the clock does, at whatever rate. `schema.ts` argues it at the column.
|
|
194
|
+
*
|
|
195
|
+
* The comparison is a single column because the sort key *is* the bookmark, so there is no
|
|
196
|
+
* bookmarked-row subquery here. The bookmark is still checked as a bounded positive integer and
|
|
197
|
+
* still bound to the sort it was issued under, because that is the codec's job and not this
|
|
198
|
+
* query's.
|
|
199
|
+
*/
|
|
200
|
+
async history(tx, workspaceId, assetId, input) {
|
|
201
|
+
// 404 before paging: a timeline for an asset in another workspace must not answer with an
|
|
202
|
+
// empty list, which reads as "nothing has happened to it" rather than "it is not yours".
|
|
203
|
+
await this.get(tx, workspaceId, assetId);
|
|
204
|
+
const filters = [eq(assetHistory.workspaceId, workspaceId), eq(assetHistory.assetId, assetId)];
|
|
205
|
+
if (input.cursor)
|
|
206
|
+
filters.push(lt(assetHistory.seq, decodeSeqMark(input.cursor, HISTORY_SORT).n));
|
|
207
|
+
const rows = await tx
|
|
208
|
+
.select()
|
|
209
|
+
.from(assetHistory)
|
|
210
|
+
.where(and(...filters))
|
|
211
|
+
.orderBy(desc(assetHistory.seq))
|
|
212
|
+
.limit(input.limit + 1);
|
|
213
|
+
const items = rows.slice(0, input.limit);
|
|
214
|
+
const last = items.at(-1);
|
|
215
|
+
const nextCursor = rows.length > input.limit && last ? encodeSeqMark({ n: last.seq, s: HISTORY_SORT }) : null;
|
|
216
|
+
return { items: items.map(toHistoryEntry), nextCursor };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A category id in a request is a claim about a row in this workspace, and it is checked before
|
|
220
|
+
* it is stored.
|
|
221
|
+
*
|
|
222
|
+
* The same defect `photoFileId` had, one field over: `assets.create` and `assets.update` took
|
|
223
|
+
* `categoryId` on trust, so any uuid at all went into the column. Two things follow from that,
|
|
224
|
+
* and neither is theoretical. An id belonging to **another workspace** files an asset under a
|
|
225
|
+
* category this one cannot see, name or unfile — the picker resolves names from its own
|
|
226
|
+
* `categories.list`, so the field renders blank and the filter it sits behind matches nothing
|
|
227
|
+
* anybody can select. An id belonging to **nobody** does the same thing with no other workspace
|
|
228
|
+
* involved. Either way the register holds a reference to a row that is not there, which is
|
|
229
|
+
* exactly the state `categories.archive` exists to prevent from the other end.
|
|
230
|
+
*
|
|
231
|
+
* **Checked inside the caller's transaction, unlike `checkPhoto`.** A file lives in core and has
|
|
232
|
+
* to be asked for over the broker, so that check happens before a connection is taken — the rule
|
|
233
|
+
* `codeFormat` documents. A category is this module's own table one join away, so the honest
|
|
234
|
+
* place for it is the transaction that writes the row: it costs one indexed read, and it cannot
|
|
235
|
+
* be raced by somebody deleting the category, because nothing deletes one.
|
|
236
|
+
*
|
|
237
|
+
* An **archived** category is deliberately allowed. Archiving takes it out of every picker and
|
|
238
|
+
* leaves every asset already filed under it still naming it; refusing it here would mean an edit
|
|
239
|
+
* to an asset's location failed because of a category somebody tidied away last year, and it
|
|
240
|
+
* would make re-filing a batch of assets under an old category impossible for no gain.
|
|
241
|
+
*/
|
|
242
|
+
async requireCategory(tx, workspaceId, categoryId) {
|
|
243
|
+
const [row] = await tx
|
|
244
|
+
.select({ id: categories.id })
|
|
245
|
+
.from(categories)
|
|
246
|
+
.where(and(eq(categories.workspaceId, workspaceId), eq(categories.id, categoryId)));
|
|
247
|
+
if (!row)
|
|
248
|
+
throw KernError.badRequest('That category is not one this workspace has.');
|
|
249
|
+
}
|
|
250
|
+
async get(tx, workspaceId, assetId) {
|
|
251
|
+
const [row] = await tx
|
|
252
|
+
.select()
|
|
253
|
+
.from(assets)
|
|
254
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)));
|
|
255
|
+
if (!row)
|
|
256
|
+
throw KernError.notFound('Asset');
|
|
257
|
+
return row;
|
|
258
|
+
}
|
|
259
|
+
async create(tx, workspaceId, actorId, input, format) {
|
|
260
|
+
if (input.categoryId)
|
|
261
|
+
await this.requireCategory(tx, workspaceId, input.categoryId);
|
|
262
|
+
const [row] = await tx
|
|
263
|
+
.insert(assets)
|
|
264
|
+
.values({
|
|
265
|
+
id: uuidv7(),
|
|
266
|
+
workspaceId,
|
|
267
|
+
code: await this.nextCode(tx, workspaceId, format),
|
|
268
|
+
name: input.name,
|
|
269
|
+
description: input.description,
|
|
270
|
+
categoryId: input.categoryId ?? null,
|
|
271
|
+
serialNumber: input.serialNumber ?? null,
|
|
272
|
+
location: input.location ?? null,
|
|
273
|
+
purchasedFrom: input.purchasedFrom ?? null,
|
|
274
|
+
purchasedOn: input.purchasedOn ?? null,
|
|
275
|
+
warrantyUntil: input.warrantyUntil ?? null,
|
|
276
|
+
priceMinor: input.priceMinor ?? null,
|
|
277
|
+
currency: input.currency ?? null,
|
|
278
|
+
photoFileId: input.photoFileId ?? null,
|
|
279
|
+
})
|
|
280
|
+
.returning();
|
|
281
|
+
const activity = { workspaceId, assetId: row.id, actorId, action: 'created' };
|
|
282
|
+
await this.notify.history(tx, activity);
|
|
283
|
+
return { row: row, activity };
|
|
284
|
+
}
|
|
285
|
+
/** Fields a person edits directly, and therefore the fields the timeline reports a diff for. */
|
|
286
|
+
static EDITABLE = [
|
|
287
|
+
'name',
|
|
288
|
+
'description',
|
|
289
|
+
'categoryId',
|
|
290
|
+
'serialNumber',
|
|
291
|
+
'location',
|
|
292
|
+
'purchasedFrom',
|
|
293
|
+
'purchasedOn',
|
|
294
|
+
'warrantyUntil',
|
|
295
|
+
'priceMinor',
|
|
296
|
+
'currency',
|
|
297
|
+
'photoFileId',
|
|
298
|
+
];
|
|
299
|
+
async update(tx, workspaceId, actorId, assetId, input) {
|
|
300
|
+
// Before the row is locked, because a refusal here has nothing to do with this asset and
|
|
301
|
+
// holding a row lock across a check that can throw is a lock held for no reason. `undefined`
|
|
302
|
+
// means the patch never mentioned a category and `null` means "unfile it"; neither is an id.
|
|
303
|
+
if (input.categoryId)
|
|
304
|
+
await this.requireCategory(tx, workspaceId, input.categoryId);
|
|
305
|
+
const [prev] = await tx
|
|
306
|
+
.select()
|
|
307
|
+
.from(assets)
|
|
308
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
|
|
309
|
+
.for('update');
|
|
310
|
+
if (!prev)
|
|
311
|
+
throw KernError.notFound('Asset');
|
|
312
|
+
// `undefined` means "not mentioned"; `null` means "clear it". Collapsing the two is how an
|
|
313
|
+
// edit of one field quietly wipes the others.
|
|
314
|
+
const patch = { updatedAt: new Date() };
|
|
315
|
+
for (const field of AssetService.EDITABLE) {
|
|
316
|
+
const value = input[field];
|
|
317
|
+
patch[field] = value !== undefined ? (value ?? null) : prev[field];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* A new warranty date earns a new notice.
|
|
321
|
+
*
|
|
322
|
+
* `warranty_notified_at` is the marker that stops the sweep saying the same thing every morning
|
|
323
|
+
* for a month; left alone here, extending a warranty by two years would mean nobody is ever told
|
|
324
|
+
* about the new date, because the row is already marked. Cleared only when the date itself
|
|
325
|
+
* moved — an edit to the name must not re-arm a notice somebody has already had.
|
|
326
|
+
*/
|
|
327
|
+
if (patch.warrantyUntil !== prev.warrantyUntil)
|
|
328
|
+
patch.warrantyNotifiedAt = null;
|
|
329
|
+
// Filtered by workspace as well as by id. The `select … for update` three statements up already
|
|
330
|
+
// carries the predicate, so this is not reachable today — but `core` connects as a superuser
|
|
331
|
+
// with RLS bypassed, which makes the predicate in the statement the only barrier there is. A
|
|
332
|
+
// barrier that holds only because of what another statement happens to do is not one.
|
|
333
|
+
const [row] = await tx
|
|
334
|
+
.update(assets)
|
|
335
|
+
.set(patch)
|
|
336
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
|
|
337
|
+
.returning();
|
|
338
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : v) ?? null;
|
|
339
|
+
const changes = AssetService.EDITABLE.filter((f) => iso(patch[f]) !== iso(prev[f])).map((f) => ({
|
|
340
|
+
field: f,
|
|
341
|
+
from: iso(prev[f]),
|
|
342
|
+
to: iso(patch[f]),
|
|
343
|
+
}));
|
|
344
|
+
if (changes.length === 0)
|
|
345
|
+
return { row: row, activity: null };
|
|
346
|
+
const activity = { workspaceId, assetId, actorId, action: 'updated', changes };
|
|
347
|
+
await this.notify.history(tx, activity);
|
|
348
|
+
return { row: row, activity };
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* @param repairsOn whether the workspace records repairs, read before the transaction opened.
|
|
352
|
+
* The "it is away for repair" refusal below tells somebody to go and use `repairs.complete`, and
|
|
353
|
+
* that procedure answers 404 in a workspace with the capability off — so the refusal would be an
|
|
354
|
+
* instruction to open a door that is not there, and the asset could never leave the register.
|
|
355
|
+
*/
|
|
356
|
+
async archive(tx, workspaceId, actorId, assetId, archived, repairsOn) {
|
|
357
|
+
/**
|
|
358
|
+
* **Locked before either refusal is decided, because both of them are about a race.**
|
|
359
|
+
*
|
|
360
|
+
* The two checks below used to read the facts with a plain select, so the state they call
|
|
361
|
+
* impossible was reachable by simply doing the two things at once: an archive and a handover
|
|
362
|
+
* each read "nobody is holding it", both were allowed, and the register was left with an
|
|
363
|
+
* archived asset and an open custody period — the timeline showing a handover that never ended,
|
|
364
|
+
* and "what is Ada still holding?" quietly not counting it, because that question excludes
|
|
365
|
+
* archived rows. A check whose answer another transaction may already have changed is not a
|
|
366
|
+
* check; it is a comment.
|
|
367
|
+
*
|
|
368
|
+
* The lock is the same one `CustodyService.stamp` and `RepairService.restamp` take before they
|
|
369
|
+
* write `status`, which is what makes the ordering complete rather than one-sided: whichever
|
|
370
|
+
* transaction gets it second re-reads what the first committed, so an archive that arrives
|
|
371
|
+
* first makes the handover refuse, and a handover that arrives first makes the archive refuse.
|
|
372
|
+
* `lockAsset` in `status.ts` argues it in full.
|
|
373
|
+
*/
|
|
374
|
+
const [locked] = await tx
|
|
375
|
+
.select()
|
|
376
|
+
.from(assets)
|
|
377
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
|
|
378
|
+
.for('update');
|
|
379
|
+
if (!locked)
|
|
380
|
+
throw KernError.notFound('Asset');
|
|
381
|
+
if (archived) {
|
|
382
|
+
/**
|
|
383
|
+
* An item somebody is holding cannot leave the register.
|
|
384
|
+
*
|
|
385
|
+
* Somebody is answerable for the thing; taking it out of the register does not make them not
|
|
386
|
+
* answerable, it only stops anybody being able to find out. Two steps instead of one, and
|
|
387
|
+
* both of them mean something.
|
|
388
|
+
*/
|
|
389
|
+
if (locked.custodianUserId)
|
|
390
|
+
throw KernError.conflict('Somebody is still holding this item. Take it back before archiving it.', 'inventory.asset.still_held');
|
|
391
|
+
/**
|
|
392
|
+
* An item that is at a repairer cannot leave the register either, and for the same reason as
|
|
393
|
+
* custody: money is committed and the thing is out of the building, so taking it out of the
|
|
394
|
+
* register does not settle that — it only stops anybody being able to find out.
|
|
395
|
+
*
|
|
396
|
+
* It also keeps two counts that must agree from drifting. `stats.summary` reports
|
|
397
|
+
* `byStatus.under_repair` from the cached status column, over live rows only, and
|
|
398
|
+
* `outForRepair` from the repair rows themselves; an archived asset with an open repair
|
|
399
|
+
* would appear in one and not the other, and neither number would be wrong.
|
|
400
|
+
*
|
|
401
|
+
* **Only while the workspace records repairs.** With the `repairs` capability off, this
|
|
402
|
+
* refusal names a procedure that answers 404 — so it was not a two-step instruction, it was a
|
|
403
|
+
* dead end: the repair could not be completed, so the asset could not be archived, so the
|
|
404
|
+
* item was stuck in the register for as long as the capability stayed off. A refusal that
|
|
405
|
+
* points at a door has to be withdrawn when the door is taken away. `stats.away` counts only
|
|
406
|
+
* live assets for the same reason, so the two counts still cannot disagree.
|
|
407
|
+
*/
|
|
408
|
+
const open = repairsOn ? await openRepairId(tx, workspaceId, assetId) : undefined;
|
|
409
|
+
if (open)
|
|
410
|
+
throw KernError.conflict('This item is away for repair. Log the repair as returned before archiving it.', 'inventory.asset.under_repair');
|
|
411
|
+
}
|
|
412
|
+
const [row] = await tx
|
|
413
|
+
.update(assets)
|
|
414
|
+
.set({ archivedAt: archived ? new Date() : null, updatedAt: new Date() })
|
|
415
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, assetId)))
|
|
416
|
+
.returning();
|
|
417
|
+
if (!row)
|
|
418
|
+
throw KernError.notFound('Asset');
|
|
419
|
+
const activity = {
|
|
420
|
+
workspaceId,
|
|
421
|
+
assetId,
|
|
422
|
+
actorId,
|
|
423
|
+
action: archived ? 'retired' : 'restored',
|
|
424
|
+
};
|
|
425
|
+
await this.notify.history(tx, activity);
|
|
426
|
+
return { row, activity };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../src/server/services/assets.ts"],"names":[],"mappings":"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"}
|