@kernhq/module-inventory 0.2.0 → 0.4.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 +106 -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 +345 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +245 -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 +1108 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +253 -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 +1392 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +385 -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 +169 -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 +157 -0
- package/dist/server/services/categories.d.ts.map +1 -0
- package/dist/server/services/categories.js +309 -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/0008_category_order_unique.sql +71 -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 +49 -0
- package/package.json +2 -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 +395 -0
- package/src/client/errors.ts +229 -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 +1106 -18
- package/src/client/mock.test.ts +465 -1
- package/src/client/mock.ts +1105 -79
- package/src/client/module.ts +73 -1
- 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/reorder.test.ts +100 -0
- package/src/client/reorder.ts +79 -0
- package/src/client/repairs.test.ts +38 -0
- package/src/client/repairs.ts +38 -0
- package/src/client/sequence.test.ts +248 -0
- package/src/client/sequence.ts +185 -0
- package/src/client/settings/CategoriesSettings.svelte +746 -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 +270 -0
- package/src/contract/notifications.ts +73 -0
- package/src/contract/permissions.ts +55 -2
- package/src/contract/router.ts +284 -1
- package/src/contract/settings.ts +24 -4
- package/src/module.test.ts +217 -4
- package/src/server/index.ts +311 -4
- package/src/server/inventory.int.test.ts +4109 -19
- package/src/server/jobs.ts +444 -0
- package/src/server/migrations.test.ts +253 -2
- package/src/server/router.ts +486 -6
- package/src/server/schema.ts +171 -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 +337 -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,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"}
|
|
@@ -38,6 +38,15 @@ export declare class NotifyService {
|
|
|
38
38
|
private readonly kernel;
|
|
39
39
|
constructor(kernel: Kernel);
|
|
40
40
|
private best;
|
|
41
|
+
/**
|
|
42
|
+
* `best`, asked the other question: **did the call actually happen**, rather than what it returned.
|
|
43
|
+
*
|
|
44
|
+
* `best` cannot answer that — a procedure that legitimately answers `null` or `undefined` is
|
|
45
|
+
* indistinguishable from one that threw and was swallowed, and a caller writing "I have told
|
|
46
|
+
* them" into a database on the strength of that is writing a lie. So the value is thrown away
|
|
47
|
+
* here and the boolean is the whole answer.
|
|
48
|
+
*/
|
|
49
|
+
private attempted;
|
|
41
50
|
/**
|
|
42
51
|
* Append to the asset's own history, inside the caller's transaction.
|
|
43
52
|
*
|
|
@@ -51,12 +60,45 @@ export declare class NotifyService {
|
|
|
51
60
|
history(tx: Tx, input: HistoryInput): Promise<void>;
|
|
52
61
|
/** The workspace-wide activity feed's copy. Best-effort, and only ever after the commit. */
|
|
53
62
|
activity(input: HistoryInput): Promise<void>;
|
|
54
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Tell these people, and answer **who it was meant for and how many of them were actually
|
|
65
|
+
* written** — both numbers, because one of them cannot be checked without the other.
|
|
66
|
+
*
|
|
67
|
+
* Everything this class does is best-effort, so a failed `core.notifications.create` is swallowed
|
|
68
|
+
* and logged; a caller that then records "this one has been notified" has recorded something that
|
|
69
|
+
* never happened. The two nightly sweeps write exactly such a marker, once per row for ever, so a
|
|
70
|
+
* swallowed failure there is not a delayed notice but a notice nobody will ever receive.
|
|
71
|
+
*
|
|
72
|
+
* This used to answer a single count, and the sweeps marked the row whenever it was above zero —
|
|
73
|
+
* so a notice that reached one of three recipients was stamped *told*, and the two who missed it
|
|
74
|
+
* never heard. **A partial delivery is a failure for the people it did not reach**, and the row
|
|
75
|
+
* cannot record two answers, so the honest one is the pessimistic one: the sweeps mark nothing
|
|
76
|
+
* unless `delivered === targeted`, and say it again in the morning. The cost is that whoever did
|
|
77
|
+
* hear it hears it twice; `groupKey` collapses that in the notification centre, and it is a
|
|
78
|
+
* strictly smaller price than one person never being told at all.
|
|
79
|
+
*
|
|
80
|
+
* `targeted` is what is left after the de-duplication and the exclusions, which is why the caller
|
|
81
|
+
* cannot work it out from the list it passed in. `targeted: 0` means there was nobody to tell,
|
|
82
|
+
* which is also not "told".
|
|
83
|
+
*/
|
|
84
|
+
notify(input: NotifyInput): Promise<{
|
|
85
|
+
targeted: number;
|
|
86
|
+
delivered: number;
|
|
87
|
+
}>;
|
|
55
88
|
/** Realtime cache invalidation for connected clients. */
|
|
56
89
|
change(workspaceId: string, entity: string, id: string, op: EntityChange['op'], opts?: {
|
|
57
90
|
patch?: Record<string, unknown>;
|
|
58
91
|
scope?: Record<string, string>;
|
|
59
92
|
}): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* The workspace-wide search index, written to by `SearchService` and by nothing else.
|
|
95
|
+
*
|
|
96
|
+
* These two spent a release with no caller: `objectTypes` had been taken off the module
|
|
97
|
+
* definition, because a declared type with no indexer and no resolver renders a link to nothing.
|
|
98
|
+
* Both halves exist now — `src/server/services/search.ts` builds the documents, `src/server/
|
|
99
|
+
* index.ts` declares the indexer and the resolver — and every asset mutation reindexes after it
|
|
100
|
+
* has committed.
|
|
101
|
+
*/
|
|
60
102
|
index(documents: core.SearchDocument[]): Promise<void>;
|
|
61
103
|
unindex(workspaceId: string, type: string, ids: string[]): Promise<void>;
|
|
62
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAI7D,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oDAAoD;IACpD,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CAC9C;AAED;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;YAE7B,IAAI;IAYlB;;;;;;;;;OASG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYzD,4FAA4F;IACtF,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAI7D,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oDAAoD;IACpD,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CAC9C;AAED;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;YAE7B,IAAI;IAYlB;;;;;;;OAOG;YACW,SAAS;IASvB;;;;;;;;;OASG;IACG,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYzD,4FAA4F;IACtF,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BlF,yDAAyD;IACnD,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAO,GAC7E,OAAO,CAAC,IAAI,CAAC;IAahB;;;;;;;;OAQG;IACG,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtD,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ/E"}
|
|
@@ -23,6 +23,20 @@ export class NotifyService {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* `best`, asked the other question: **did the call actually happen**, rather than what it returned.
|
|
28
|
+
*
|
|
29
|
+
* `best` cannot answer that — a procedure that legitimately answers `null` or `undefined` is
|
|
30
|
+
* indistinguishable from one that threw and was swallowed, and a caller writing "I have told
|
|
31
|
+
* them" into a database on the strength of that is writing a lie. So the value is thrown away
|
|
32
|
+
* here and the boolean is the whole answer.
|
|
33
|
+
*/
|
|
34
|
+
async attempted(what, fn) {
|
|
35
|
+
return ((await this.best(what, async () => {
|
|
36
|
+
await fn();
|
|
37
|
+
return true;
|
|
38
|
+
})) === true);
|
|
39
|
+
}
|
|
26
40
|
/**
|
|
27
41
|
* Append to the asset's own history, inside the caller's transaction.
|
|
28
42
|
*
|
|
@@ -56,12 +70,33 @@ export class NotifyService {
|
|
|
56
70
|
data: input.data ?? {},
|
|
57
71
|
}));
|
|
58
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Tell these people, and answer **who it was meant for and how many of them were actually
|
|
75
|
+
* written** — both numbers, because one of them cannot be checked without the other.
|
|
76
|
+
*
|
|
77
|
+
* Everything this class does is best-effort, so a failed `core.notifications.create` is swallowed
|
|
78
|
+
* and logged; a caller that then records "this one has been notified" has recorded something that
|
|
79
|
+
* never happened. The two nightly sweeps write exactly such a marker, once per row for ever, so a
|
|
80
|
+
* swallowed failure there is not a delayed notice but a notice nobody will ever receive.
|
|
81
|
+
*
|
|
82
|
+
* This used to answer a single count, and the sweeps marked the row whenever it was above zero —
|
|
83
|
+
* so a notice that reached one of three recipients was stamped *told*, and the two who missed it
|
|
84
|
+
* never heard. **A partial delivery is a failure for the people it did not reach**, and the row
|
|
85
|
+
* cannot record two answers, so the honest one is the pessimistic one: the sweeps mark nothing
|
|
86
|
+
* unless `delivered === targeted`, and say it again in the morning. The cost is that whoever did
|
|
87
|
+
* hear it hears it twice; `groupKey` collapses that in the notification centre, and it is a
|
|
88
|
+
* strictly smaller price than one person never being told at all.
|
|
89
|
+
*
|
|
90
|
+
* `targeted` is what is left after the de-duplication and the exclusions, which is why the caller
|
|
91
|
+
* cannot work it out from the list it passed in. `targeted: 0` means there was nobody to tell,
|
|
92
|
+
* which is also not "told".
|
|
93
|
+
*/
|
|
59
94
|
async notify(input) {
|
|
60
95
|
const excluded = new Set([...(input.exclude ?? [])].filter(Boolean));
|
|
61
96
|
const targets = [...new Set(input.userIds)].filter((id) => id && !excluded.has(id));
|
|
62
97
|
if (!targets.length)
|
|
63
|
-
return;
|
|
64
|
-
await Promise.all(targets.map((userId) => this.
|
|
98
|
+
return { targeted: 0, delivered: 0 };
|
|
99
|
+
const written = await Promise.all(targets.map((userId) => this.attempted('notifications.create', () => this.kernel.call('core.notifications.create', {
|
|
65
100
|
userId,
|
|
66
101
|
workspaceId: input.workspaceId,
|
|
67
102
|
module: MODULE_ID,
|
|
@@ -74,6 +109,7 @@ export class NotifyService {
|
|
|
74
109
|
groupKey: input.groupKey ?? null,
|
|
75
110
|
actorId: input.actorId ?? null,
|
|
76
111
|
}))));
|
|
112
|
+
return { targeted: targets.length, delivered: written.filter(Boolean).length };
|
|
77
113
|
}
|
|
78
114
|
/** Realtime cache invalidation for connected clients. */
|
|
79
115
|
async change(workspaceId, entity, id, op, opts = {}) {
|
|
@@ -86,9 +122,15 @@ export class NotifyService {
|
|
|
86
122
|
...(opts.scope ? { scope: opts.scope } : {}),
|
|
87
123
|
}));
|
|
88
124
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The workspace-wide search index, written to by `SearchService` and by nothing else.
|
|
127
|
+
*
|
|
128
|
+
* These two spent a release with no caller: `objectTypes` had been taken off the module
|
|
129
|
+
* definition, because a declared type with no indexer and no resolver renders a link to nothing.
|
|
130
|
+
* Both halves exist now — `src/server/services/search.ts` builds the documents, `src/server/
|
|
131
|
+
* index.ts` declares the indexer and the resolver — and every asset mutation reindexes after it
|
|
132
|
+
* has committed.
|
|
133
|
+
*/
|
|
92
134
|
async index(documents) {
|
|
93
135
|
if (!documents.length)
|
|
94
136
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AA0B3C;;;;;;;GAOG;AACH,MAAM,OAAO,aAAa;IACK;IAA7B,YAA6B,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAEvC,KAAK,CAAC,IAAI,CAAI,IAAY,EAAE,EAAoB;QACtD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAA;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAClB,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EACvD,8BAA8B,CAC/B,CAAA;YACD,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,OAAO,CAAC,EAAM,EAAE,KAAmB;QACvC,MAAM,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC;YACnC,EAAE,EAAE,MAAM,EAAE;YACZ,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE;YAC5B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;SACvB,CAAC,CAAA;IACJ,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,QAAQ,CAAC,KAAmB;QAChC,MAAM,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,EAAE,CACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE;YACvC,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE;YAC/D,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE;YAC5B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;SACvB,CAAC,CACH,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAAkB;QAC7B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAa,CAAC,CAAA;QAChF,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QACnF,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/server/services/notify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Kernel } from '@kernhq/kernel';
|
|
2
|
+
import type { NotifyService } from './notify.js';
|
|
3
|
+
/** Why the checklist is being raised, which is the only difference between the two callers. */
|
|
4
|
+
export type Departure =
|
|
5
|
+
/** `core.member.removed` — the account is out of the workspace already. */
|
|
6
|
+
'removed'
|
|
7
|
+
/** `hr.person.status_changed` to a status that ends employment — they may still be at their desk. */
|
|
8
|
+
| 'leaving';
|
|
9
|
+
export interface ReturnList {
|
|
10
|
+
items: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
code: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Who was told. Empty when there was nothing to return, or nobody who could act on it. */
|
|
16
|
+
recipients: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Somebody is leaving, and the register still says things are theirs.
|
|
20
|
+
*
|
|
21
|
+
* **This raises a checklist and notifies. It never moves anything.** Custody changes because a
|
|
22
|
+
* person did something — handed an item over, handed it on, took it back — and every one of those
|
|
23
|
+
* writes a period row and a line of history saying who did it. A hook that quietly returned an
|
|
24
|
+
* item on somebody's last day would write a handover nobody performed, into the one record the
|
|
25
|
+
* company later argues from; and the laptop would still be in their bag. So the answer is a message
|
|
26
|
+
* to the people who can take it back, and the register stays exactly as true as it was.
|
|
27
|
+
*
|
|
28
|
+
* **It is inert without HR.** Nothing here imports HR, depends on it, or assumes it is installed:
|
|
29
|
+
* `hr.person.status_changed` simply never arrives in a workspace that has no HR, and the second
|
|
30
|
+
* caller — `core.member.removed` — is core's own event and needs nothing at all. Both paths check
|
|
31
|
+
* the *inventory* module is switched on for the workspace before doing anything, because an event
|
|
32
|
+
* bus is instance-wide and a workspace that has never enabled this module must not be sent its
|
|
33
|
+
* notifications.
|
|
34
|
+
*/
|
|
35
|
+
export declare class OffboardingService {
|
|
36
|
+
private readonly kernel;
|
|
37
|
+
private readonly notify;
|
|
38
|
+
constructor(kernel: Kernel, notify: NotifyService);
|
|
39
|
+
/**
|
|
40
|
+
* What one person is still recorded as holding.
|
|
41
|
+
*
|
|
42
|
+
* Read from `assets.custodian_user_id` — denormalised inside the transaction that writes the
|
|
43
|
+
* custody period, and indexed — rather than from an open-period join, which is the same choice
|
|
44
|
+
* `custody.byUser` makes and for the same reason. Archived rows cannot appear: an item somebody
|
|
45
|
+
* holds cannot be archived at all, because `assets.archive` refuses it.
|
|
46
|
+
*/
|
|
47
|
+
held(workspaceId: string, userId: string): Promise<ReturnList['items']>;
|
|
48
|
+
/**
|
|
49
|
+
* Raise the return list, if there is one.
|
|
50
|
+
*
|
|
51
|
+
* Answers with what it found and who it told, so a test can assert both and a caller can log the
|
|
52
|
+
* count. Doing nothing is the ordinary case — most people leave holding nothing — and it is not
|
|
53
|
+
* an error.
|
|
54
|
+
*
|
|
55
|
+
* Redelivery sends the same message again: an event handler is retried, and `core.notifications
|
|
56
|
+
* .create` has no idempotency key. The two events behind this are rare enough that the cost is a
|
|
57
|
+
* duplicate row rather than a stream, and `groupKey` collapses them where a client groups. That
|
|
58
|
+
* is the same trade `module-tracker`'s `due-soon` makes, written down rather than assumed.
|
|
59
|
+
*/
|
|
60
|
+
raise(workspaceId: string, userId: string, departure: Departure): Promise<ReturnList>;
|
|
61
|
+
/**
|
|
62
|
+
* What to call the person in the sentence.
|
|
63
|
+
*
|
|
64
|
+
* Core is asked, and a failure falls back to "a former member" rather than to the uuid: a
|
|
65
|
+
* notification with a uuid in the middle of it is the product admitting it does not know who it
|
|
66
|
+
* is talking about, and this module already refuses to print one on a screen for the same reason.
|
|
67
|
+
*/
|
|
68
|
+
private nameOf;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=offboarding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offboarding.d.ts","sourceRoot":"","sources":["../../../src/server/services/offboarding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAK5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAYhD,+FAA+F;AAC/F,MAAM,MAAM,SAAS;AACnB,2EAA2E;AACzE,SAAS;AACX,qGAAqG;GACnG,SAAS,CAAA;AAEb,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,2FAA2F;IAC3F,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAkB;IAE3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa;IAGxC;;;;;;;OAOG;IACG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAiB7E;;;;;;;;;;;OAWG;IACG,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA8C3F;;;;;;OAMG;YACW,MAAM;CAMrB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { and, asc, eq, isNull } from 'drizzle-orm';
|
|
2
|
+
import { MODULE_ID } from '../../contract/models.js';
|
|
3
|
+
import { assets } from '../schema.js';
|
|
4
|
+
import { membersWithPermission } from './audience.js';
|
|
5
|
+
import { assetUrl } from './search.js';
|
|
6
|
+
/** How many tags fit in one sentence before it stops being readable. */
|
|
7
|
+
const TAGS_IN_BODY = 5;
|
|
8
|
+
/**
|
|
9
|
+
* More than anybody is plausibly holding, and a bound all the same: this reads every live asset of
|
|
10
|
+
* one person, and an unbounded query in an event handler is an unbounded query.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_HELD = 200;
|
|
13
|
+
/**
|
|
14
|
+
* Somebody is leaving, and the register still says things are theirs.
|
|
15
|
+
*
|
|
16
|
+
* **This raises a checklist and notifies. It never moves anything.** Custody changes because a
|
|
17
|
+
* person did something — handed an item over, handed it on, took it back — and every one of those
|
|
18
|
+
* writes a period row and a line of history saying who did it. A hook that quietly returned an
|
|
19
|
+
* item on somebody's last day would write a handover nobody performed, into the one record the
|
|
20
|
+
* company later argues from; and the laptop would still be in their bag. So the answer is a message
|
|
21
|
+
* to the people who can take it back, and the register stays exactly as true as it was.
|
|
22
|
+
*
|
|
23
|
+
* **It is inert without HR.** Nothing here imports HR, depends on it, or assumes it is installed:
|
|
24
|
+
* `hr.person.status_changed` simply never arrives in a workspace that has no HR, and the second
|
|
25
|
+
* caller — `core.member.removed` — is core's own event and needs nothing at all. Both paths check
|
|
26
|
+
* the *inventory* module is switched on for the workspace before doing anything, because an event
|
|
27
|
+
* bus is instance-wide and a workspace that has never enabled this module must not be sent its
|
|
28
|
+
* notifications.
|
|
29
|
+
*/
|
|
30
|
+
export class OffboardingService {
|
|
31
|
+
kernel;
|
|
32
|
+
notify;
|
|
33
|
+
constructor(kernel, notify) {
|
|
34
|
+
this.kernel = kernel;
|
|
35
|
+
this.notify = notify;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What one person is still recorded as holding.
|
|
39
|
+
*
|
|
40
|
+
* Read from `assets.custodian_user_id` — denormalised inside the transaction that writes the
|
|
41
|
+
* custody period, and indexed — rather than from an open-period join, which is the same choice
|
|
42
|
+
* `custody.byUser` makes and for the same reason. Archived rows cannot appear: an item somebody
|
|
43
|
+
* holds cannot be archived at all, because `assets.archive` refuses it.
|
|
44
|
+
*/
|
|
45
|
+
async held(workspaceId, userId) {
|
|
46
|
+
return this.kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
47
|
+
.select({ id: assets.id, code: assets.code, name: assets.name })
|
|
48
|
+
.from(assets)
|
|
49
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.custodianUserId, userId), isNull(assets.archivedAt)))
|
|
50
|
+
.orderBy(asc(assets.code))
|
|
51
|
+
.limit(MAX_HELD));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Raise the return list, if there is one.
|
|
55
|
+
*
|
|
56
|
+
* Answers with what it found and who it told, so a test can assert both and a caller can log the
|
|
57
|
+
* count. Doing nothing is the ordinary case — most people leave holding nothing — and it is not
|
|
58
|
+
* an error.
|
|
59
|
+
*
|
|
60
|
+
* Redelivery sends the same message again: an event handler is retried, and `core.notifications
|
|
61
|
+
* .create` has no idempotency key. The two events behind this are rare enough that the cost is a
|
|
62
|
+
* duplicate row rather than a stream, and `groupKey` collapses them where a client groups. That
|
|
63
|
+
* is the same trade `module-tracker`'s `due-soon` makes, written down rather than assumed.
|
|
64
|
+
*/
|
|
65
|
+
async raise(workspaceId, userId, departure) {
|
|
66
|
+
if (!(await this.kernel.isModuleEnabled(workspaceId, MODULE_ID).catch(() => false)))
|
|
67
|
+
return { items: [], recipients: [] };
|
|
68
|
+
const items = await this.held(workspaceId, userId);
|
|
69
|
+
if (!items.length)
|
|
70
|
+
return { items: [], recipients: [] };
|
|
71
|
+
// Whoever may take an item back, which is the thing this message is asking for. Not "the
|
|
72
|
+
// admins": a workspace that gave its office manager a custom role did that on purpose.
|
|
73
|
+
const recipients = await membersWithPermission(this.kernel, workspaceId, 'inventory.custody.manage');
|
|
74
|
+
if (!recipients.length) {
|
|
75
|
+
this.kernel.log.warn({ module: MODULE_ID, workspaceId, held: items.length }, 'inventory: somebody left holding items and nobody in the workspace may take them back');
|
|
76
|
+
return { items, recipients: [] };
|
|
77
|
+
}
|
|
78
|
+
const who = await this.nameOf(userId);
|
|
79
|
+
const tags = items.slice(0, TAGS_IN_BODY).map((item) => `${item.code} ${item.name}`);
|
|
80
|
+
const rest = items.length - tags.length;
|
|
81
|
+
await this.notify.notify({
|
|
82
|
+
workspaceId,
|
|
83
|
+
userIds: recipients,
|
|
84
|
+
type: 'inventory.custody.return_due',
|
|
85
|
+
title: departure === 'removed'
|
|
86
|
+
? `${who} has left the workspace still holding ${items.length === 1 ? 'an item' : `${items.length} items`}`
|
|
87
|
+
: `${who} is leaving and still holds ${items.length === 1 ? 'an item' : `${items.length} items`}`,
|
|
88
|
+
body: rest > 0 ? `${tags.join(', ')} and ${rest} more` : tags.join(', '),
|
|
89
|
+
// One item has an object worth pointing at; several do not, and naming the first would send
|
|
90
|
+
// everybody to one laptop out of five. The URL below is the list either way.
|
|
91
|
+
object: items.length === 1 ? { module: MODULE_ID, type: 'asset', id: items[0].id } : null,
|
|
92
|
+
url: items.length === 1 ? assetUrl(items[0].id) : `/inventory?custodian=${userId}`,
|
|
93
|
+
data: { userId, departure, assetIds: items.map((item) => item.id) },
|
|
94
|
+
// One group per person, so the two events cannot stack two separate piles about one leaver.
|
|
95
|
+
groupKey: `inventory.return_due.${userId}`,
|
|
96
|
+
// Never to the person leaving: they are being *chased*, and on the `removed` path they are no
|
|
97
|
+
// longer a member of this workspace at all.
|
|
98
|
+
exclude: [userId],
|
|
99
|
+
});
|
|
100
|
+
return { items, recipients };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What to call the person in the sentence.
|
|
104
|
+
*
|
|
105
|
+
* Core is asked, and a failure falls back to "a former member" rather than to the uuid: a
|
|
106
|
+
* notification with a uuid in the middle of it is the product admitting it does not know who it
|
|
107
|
+
* is talking about, and this module already refuses to print one on a screen for the same reason.
|
|
108
|
+
*/
|
|
109
|
+
async nameOf(userId) {
|
|
110
|
+
const user = await this.kernel
|
|
111
|
+
.call('core.users.get', { id: userId })
|
|
112
|
+
.catch(() => null);
|
|
113
|
+
return user?.name?.trim() || user?.email?.trim() || 'A former member';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=offboarding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offboarding.js","sourceRoot":"","sources":["../../../src/server/services/offboarding.ts"],"names":[],"mappings":"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"}
|