@cosmicdrift/kumiko-types 0.197.1 → 0.199.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/package.json +5 -1
- package/src/config.ts +7 -6
- package/src/feature.ts +4 -0
- package/src/handlers.ts +46 -1
- package/src/identifiers.ts +9 -3
- package/src/nav-icon.ts +56 -0
- package/src/nav.ts +8 -5
- package/src/screen.ts +12 -1
- package/src/tenant-db-types.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.199.0",
|
|
4
4
|
"description": "Framework-Type-Definitions für Kumiko — FeatureDefinition, BootCheck-Types und die reinen Engine-Types. Erlaubt Downstream-Konsumenten, gegen die Type-Contracts zu bauen, ohne das ganze Framework-Package zu importieren. Enthaelt keine identitaets-sensitiven Runtime-Werte mehr (Error-Klassen leben seit #1629 in kumiko-framework, Brand-Symbole nutzen Symbol.for) und ist deshalb eine plain dependency, keine peerDependency.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -162,6 +162,10 @@
|
|
|
162
162
|
"types": "./src/nav.ts",
|
|
163
163
|
"default": "./src/nav.ts"
|
|
164
164
|
},
|
|
165
|
+
"./nav-icon": {
|
|
166
|
+
"types": "./src/nav-icon.ts",
|
|
167
|
+
"default": "./src/nav-icon.ts"
|
|
168
|
+
},
|
|
165
169
|
"./ownership": {
|
|
166
170
|
"types": "./src/ownership.ts",
|
|
167
171
|
"default": "./src/ownership.ts"
|
package/src/config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ConcurrencyMode } from "./concurrency-mode";
|
|
|
3
3
|
import type { ConfigScope } from "./config-scope";
|
|
4
4
|
import type { DbConnection } from "./db-connection";
|
|
5
5
|
import type { FieldDefinition } from "./fields";
|
|
6
|
-
import type {
|
|
6
|
+
import type { JobContext } from "./handlers";
|
|
7
7
|
import type {
|
|
8
8
|
PostDeleteHookFn,
|
|
9
9
|
PostSaveHookFn,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
PreSaveHookFn,
|
|
13
13
|
} from "./hooks";
|
|
14
14
|
import type { TenantId } from "./identifiers";
|
|
15
|
+
import type { NavIconKey } from "./nav-icon";
|
|
15
16
|
import type { TenantDb } from "./tenant-db-types";
|
|
16
17
|
|
|
17
18
|
// --- Config ---
|
|
@@ -154,12 +155,12 @@ export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
|
|
|
154
155
|
readonly group?: string;
|
|
155
156
|
};
|
|
156
157
|
|
|
157
|
-
// Label
|
|
158
|
-
//
|
|
159
|
-
//
|
|
158
|
+
// Label carrier for the settings hub. `title` is an i18n key (no literal —
|
|
159
|
+
// guarded), `icon` a NavIconKey for the nav entry, `order` the sort weight
|
|
160
|
+
// within its audience group.
|
|
160
161
|
export type ConfigMask = {
|
|
161
162
|
readonly title: string;
|
|
162
|
-
readonly icon?:
|
|
163
|
+
readonly icon?: NavIconKey;
|
|
163
164
|
readonly order?: number;
|
|
164
165
|
};
|
|
165
166
|
|
|
@@ -335,7 +336,7 @@ export type JobRunIn = Exclude<RunIn, "both">;
|
|
|
335
336
|
|
|
336
337
|
// --- Jobs ---
|
|
337
338
|
|
|
338
|
-
export type JobHandlerFn = (payload: Record<string, unknown>, context:
|
|
339
|
+
export type JobHandlerFn = (payload: Record<string, unknown>, context: JobContext) => Promise<void>;
|
|
339
340
|
|
|
340
341
|
export type JobTrigger =
|
|
341
342
|
// `on` akzeptiert ein einzelnes Handler-Ref ODER eine Liste. Multi-
|
package/src/feature.ts
CHANGED
|
@@ -932,6 +932,10 @@ export type Registry = {
|
|
|
932
932
|
): readonly SearchPayloadContributorFn[];
|
|
933
933
|
getHandlerEntity(qualifiedHandler: string): string | undefined;
|
|
934
934
|
isHandlerSystemScoped(qualifiedHandler: string): boolean;
|
|
935
|
+
// Job counterpart to isHandlerSystemScoped — a job's owning feature is
|
|
936
|
+
// tracked separately (jobFeatureMap) since jobs aren't write/query/stream
|
|
937
|
+
// handlers and don't populate handlerFeatureMap.
|
|
938
|
+
isJobSystemScoped(qualifiedJobName: string): boolean;
|
|
935
939
|
getHandlerFeature(qualifiedHandler: string): string | undefined;
|
|
936
940
|
// True iff at least one registered handler declares a `rateLimit`
|
|
937
941
|
// option. Pre-computed at registry-build so the boot path can skip
|
package/src/handlers.ts
CHANGED
|
@@ -177,6 +177,7 @@ export type WriteResult<TData = unknown> =
|
|
|
177
177
|
// Forward import: Registry is in feature.ts (circular type import — fine in TS)
|
|
178
178
|
import type { Registry } from "./feature";
|
|
179
179
|
import type { TenantId } from "./identifiers";
|
|
180
|
+
import type { UncheckedSystemDb } from "./tenant-db-types";
|
|
180
181
|
|
|
181
182
|
// Minimal interface for job event triggers (framework-owned, concrete type in jobs/)
|
|
182
183
|
export type JobRunnerRef = {
|
|
@@ -187,6 +188,17 @@ export type JobRunnerRef = {
|
|
|
187
188
|
): Promise<void>;
|
|
188
189
|
};
|
|
189
190
|
|
|
191
|
+
// Minimal interface a JobRunner attaches via attachDispatcher() to reach the
|
|
192
|
+
// write pipeline — framework-owned, concrete type wraps the dispatcher's
|
|
193
|
+
// executeWrite/executeQuery. Unlike HandlerContext.write (identity implicit
|
|
194
|
+
// via closure), both take an explicit user: a JobRunner is a boot-time
|
|
195
|
+
// singleton, not bound to one caller's identity. Only write+queryAs — no
|
|
196
|
+
// .query/.stream/.command/.batch/.resolveAuthClaims, that stays Dispatcher-only.
|
|
197
|
+
export type DispatchWriteRef = {
|
|
198
|
+
readonly write: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
|
|
199
|
+
readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
|
|
200
|
+
};
|
|
201
|
+
|
|
190
202
|
// Priority levels for notifications
|
|
191
203
|
export type NotifyPriority = "critical" | "normal" | "low";
|
|
192
204
|
|
|
@@ -347,6 +359,9 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
|
|
|
347
359
|
// outside the DB. `undefined` when the pipeline has no outside-tx source
|
|
348
360
|
// for this dispatch (see dispatch-shared.ts) — callers must check before use.
|
|
349
361
|
readonly dbOutsideTransaction: TenantDb | undefined;
|
|
362
|
+
// Only present for r.systemScope() handlers, bound to the same `db` as above.
|
|
363
|
+
// Non-system handlers never receive this — reach for `db` instead.
|
|
364
|
+
readonly systemDb?: UncheckedSystemDb;
|
|
350
365
|
readonly registry: Registry;
|
|
351
366
|
/** Aktiver SessionUser des Handler-Aufrufs — Convenience-Alias zu
|
|
352
367
|
* `event.user`. Existiert weil Handler intuitiv `ctx.user.tenantId`
|
|
@@ -544,13 +559,43 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
|
|
|
544
559
|
readonly resolveAuthClaims: (user: SessionUser) => Promise<Record<string, unknown>>;
|
|
545
560
|
};
|
|
546
561
|
|
|
547
|
-
// Job execution: db + registry + systemUser + logging guaranteed
|
|
562
|
+
// Job execution: db + registry + systemUser + logging guaranteed, plus a
|
|
563
|
+
// write path into the same dispatcher pipeline write-handlers use.
|
|
564
|
+
//
|
|
565
|
+
// write/queryAs throw until JobRunner.attachDispatcher() has run — a call
|
|
566
|
+
// this early is a framework boot bug, not a job-author mistake.
|
|
567
|
+
//
|
|
568
|
+
// Identity is implicit: write runs as the job's own systemUser, tenant-
|
|
569
|
+
// scoped via the job's _tenantId (falls back to SYSTEM_TENANT_ID for jobs
|
|
570
|
+
// with no tenant context, e.g. plain cron jobs). A job that writes without
|
|
571
|
+
// fanning out per-tenant (`_perTenant:` trigger) silently writes into the
|
|
572
|
+
// system tenant instead of each tenant it may have meant to reach — no
|
|
573
|
+
// guard against this, only this warning.
|
|
548
574
|
export type JobContext = SharedContextFields & {
|
|
549
575
|
readonly db: DbConnection;
|
|
550
576
|
readonly registry: Registry;
|
|
551
577
|
readonly systemUser: SessionUser;
|
|
552
578
|
readonly log: Logger;
|
|
553
579
|
readonly triggeredBy: { readonly id: string; readonly tenantId: TenantId } | null;
|
|
580
|
+
// Only present for jobs whose owning feature declares r.systemScope(),
|
|
581
|
+
// mirroring HandlerContext.systemDb (dispatch-shared.ts buildHandlerContext).
|
|
582
|
+
// assertTenantMatch()/acknowledgeCrossTenant() return a TenantDb, not the
|
|
583
|
+
// raw DbConnection above — job code that needs a DbRunner for a helper
|
|
584
|
+
// like reindexEntity() reaches through `.raw` on that TenantDb. `.raw`
|
|
585
|
+
// bypasses tenant filtering entirely, so it's only safe to hand to a
|
|
586
|
+
// helper that filters by tenantId itself (as reindexEntity does) — never
|
|
587
|
+
// pass it to code that trusts the connection to already be scoped.
|
|
588
|
+
readonly systemDb?: UncheckedSystemDb;
|
|
589
|
+
readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
|
|
590
|
+
readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
|
|
591
|
+
// Multi-trigger jobs (`on: [...]`) use this to tell which trigger fired —
|
|
592
|
+
// undefined for cron/manual jobs. Mirrors AppContext.triggerName.
|
|
593
|
+
readonly triggerName?: string;
|
|
594
|
+
// Fallback tenant/user when systemUser doesn't carry it (tenant-less cron
|
|
595
|
+
// jobs). Mirrors AppContext._tenantId/_userId — see the tenant-scoping
|
|
596
|
+
// footgun above.
|
|
597
|
+
readonly _tenantId?: TenantId;
|
|
598
|
+
readonly _userId?: string | undefined;
|
|
554
599
|
};
|
|
555
600
|
|
|
556
601
|
// --- Handler Functions ---
|
package/src/identifiers.ts
CHANGED
|
@@ -15,6 +15,14 @@ export type TenantId = string;
|
|
|
15
15
|
// the system boundary.
|
|
16
16
|
const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
17
17
|
|
|
18
|
+
// Shared shape check for any UUID-formatted identifier crossing an
|
|
19
|
+
// untrusted boundary (fileRefId, entity ids, ...) — same loose,
|
|
20
|
+
// version-agnostic regex as TENANT_ID_REGEX (v4/v7/nil all match), kept as
|
|
21
|
+
// one predicate so call sites don't grow their own copy of the shape.
|
|
22
|
+
export function isUuid(value: unknown): value is string {
|
|
23
|
+
return typeof value === "string" && TENANT_ID_REGEX.test(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
// Validates a candidate string against the tenantId format and returns it
|
|
19
27
|
// as a TenantId, or `null` when it doesn't match. Use at every system
|
|
20
28
|
// boundary that admits untrusted input (HTTP headers, cookies, query
|
|
@@ -22,9 +30,7 @@ const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-
|
|
|
22
30
|
// of the rejection shape — middleware returns 400, batch jobs may filter
|
|
23
31
|
// + log, and unit tests don't need a try/catch.
|
|
24
32
|
export function parseTenantId(value: unknown): TenantId | null {
|
|
25
|
-
|
|
26
|
-
if (!TENANT_ID_REGEX.test(value)) return null;
|
|
27
|
-
return value;
|
|
33
|
+
return isUuid(value) ? value : null;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
// "System-scope" tenant marker: handlers carry this tenantId when the event
|
package/src/nav-icon.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Closed vocabulary of nav icon keys. NavDefinition.icon, ScreenNavSugar.icon
|
|
2
|
+
// and ConfigMask.icon are all typed against this union, so an unregistered
|
|
3
|
+
// key is a compile error at the r.nav()/r.screen()/config-mask call site
|
|
4
|
+
// instead of a silent missing-icon at runtime.
|
|
5
|
+
//
|
|
6
|
+
// The renderer-web NAV_ICONS map (packages/renderer-web/src/layout/
|
|
7
|
+
// nav-tree.tsx) is checked against this same union via `satisfies`, so the
|
|
8
|
+
// two can't drift — add a key here only together with its lucide-react entry
|
|
9
|
+
// there, and vice versa.
|
|
10
|
+
export type NavIconKey =
|
|
11
|
+
| "dashboard"
|
|
12
|
+
| "layout-grid"
|
|
13
|
+
| "book-open"
|
|
14
|
+
| "clipboard-list"
|
|
15
|
+
| "package"
|
|
16
|
+
| "gauge"
|
|
17
|
+
| "list"
|
|
18
|
+
| "table"
|
|
19
|
+
| "layers"
|
|
20
|
+
| "building"
|
|
21
|
+
| "calculator"
|
|
22
|
+
| "wallet"
|
|
23
|
+
| "coins"
|
|
24
|
+
| "credit-card"
|
|
25
|
+
| "piggy-bank"
|
|
26
|
+
| "receipt"
|
|
27
|
+
| "chart"
|
|
28
|
+
| "bar-chart"
|
|
29
|
+
| "trending"
|
|
30
|
+
| "sparkles"
|
|
31
|
+
| "wand"
|
|
32
|
+
| "calendar"
|
|
33
|
+
| "file"
|
|
34
|
+
| "folder"
|
|
35
|
+
| "folder-open"
|
|
36
|
+
| "home"
|
|
37
|
+
| "bell"
|
|
38
|
+
| "shield"
|
|
39
|
+
| "settings"
|
|
40
|
+
| "users"
|
|
41
|
+
| "user"
|
|
42
|
+
| "search"
|
|
43
|
+
| "tag"
|
|
44
|
+
| "key"
|
|
45
|
+
| "link"
|
|
46
|
+
| "palette"
|
|
47
|
+
| "share"
|
|
48
|
+
| "server"
|
|
49
|
+
| "mail"
|
|
50
|
+
| "lock"
|
|
51
|
+
| "hash"
|
|
52
|
+
| "download"
|
|
53
|
+
| "upload"
|
|
54
|
+
| "rocket"
|
|
55
|
+
| "plus"
|
|
56
|
+
| "languages";
|
package/src/nav.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { AccessRule } from "./handlers";
|
|
2
|
+
import type { NavIconKey } from "./nav-icon";
|
|
2
3
|
import type { TargetRef } from "./target-ref";
|
|
3
4
|
import type { TreeAction } from "./tree-node";
|
|
4
5
|
|
|
6
|
+
export type { NavIconKey } from "./nav-icon";
|
|
7
|
+
|
|
5
8
|
// Nav entry declaration. Every feature that wants to appear in the app's
|
|
6
9
|
// navigation tree registers one or more entries via r.nav(). The engine
|
|
7
10
|
// keeps the list flat — ui-core's resolveNavigation assembles the parent/
|
|
@@ -22,10 +25,10 @@ export type NavDefinition = {
|
|
|
22
25
|
// i18n translation key. Resolved at render time by the renderer's
|
|
23
26
|
// useTranslation hook; engine keeps it opaque.
|
|
24
27
|
readonly label: string;
|
|
25
|
-
// Icon key
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
readonly icon?:
|
|
28
|
+
// Icon key from the closed NavIconKey vocabulary (see nav-icon.ts). Every
|
|
29
|
+
// renderer's icon map is checked against the same union, so a typo'd key
|
|
30
|
+
// is a compile error here rather than a missing icon at runtime.
|
|
31
|
+
readonly icon?: NavIconKey;
|
|
29
32
|
// Qualified name of a parent nav entry ("<feature>:nav:<id>"). Omit for
|
|
30
33
|
// top-level entries. Boot-validator rejects cycles + dangling refs.
|
|
31
34
|
readonly parent?: string;
|
|
@@ -113,7 +116,7 @@ export type ContentCollectionDefinition = {
|
|
|
113
116
|
readonly variableSchema?: Readonly<Record<string, string>>;
|
|
114
117
|
readonly nav: {
|
|
115
118
|
readonly label: string;
|
|
116
|
-
readonly icon?:
|
|
119
|
+
readonly icon?: NavIconKey;
|
|
117
120
|
readonly parent?: string;
|
|
118
121
|
readonly order?: number;
|
|
119
122
|
// Visibility of the nav node. Leave unset — it then follows the
|
package/src/screen.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FieldDefinition } from "./fields";
|
|
2
2
|
import type { AccessRule } from "./handlers";
|
|
3
|
+
import type { NavIconKey } from "./nav-icon";
|
|
3
4
|
|
|
4
5
|
// Screen definitions describe how a feature surfaces data to the user.
|
|
5
6
|
// Pure data — the engine stores these verbatim and ui-core / the renderer
|
|
@@ -704,6 +705,16 @@ export type CustomScreenDefinition = {
|
|
|
704
705
|
/** Parent list screen for breadcrumb when this detail is not in nav. */
|
|
705
706
|
readonly listScreenId?: string;
|
|
706
707
|
readonly access?: AccessRule;
|
|
708
|
+
/** This screen is registered without a self-owned `r.nav()` entry by
|
|
709
|
+
* design — an app opts in by navving it explicitly (e.g. a settings-area
|
|
710
|
+
* self-service screen). `createKumikoApp`'s boot diagnostic
|
|
711
|
+
* (kumiko-framework#2025) skips dormant screens instead of flagging
|
|
712
|
+
* every consumer that doesn't mount the client plugin as a false
|
|
713
|
+
* positive (kumiko-framework#2034). Only set this on screens the
|
|
714
|
+
* feature itself never navs — a screen the feature DOES nav still needs
|
|
715
|
+
* its client plugin mounted by every consumer, and should keep
|
|
716
|
+
* triggering the diagnostic if it's missing. */
|
|
717
|
+
readonly dormant?: boolean;
|
|
707
718
|
};
|
|
708
719
|
|
|
709
720
|
// --- configEdit ---
|
|
@@ -790,7 +801,7 @@ export type ScreenSlots = {
|
|
|
790
801
|
// declare a standalone `r.nav()` entry instead.
|
|
791
802
|
export type ScreenNavSugar = {
|
|
792
803
|
readonly label: string;
|
|
793
|
-
readonly icon?:
|
|
804
|
+
readonly icon?: NavIconKey;
|
|
794
805
|
readonly parent?: string;
|
|
795
806
|
readonly order?: number;
|
|
796
807
|
};
|
package/src/tenant-db-types.ts
CHANGED
|
@@ -56,3 +56,16 @@ export type TenantDb = {
|
|
|
56
56
|
): Promise<readonly T[]>;
|
|
57
57
|
deleteMany(table: WritableTable, where: WhereObject): Promise<void>;
|
|
58
58
|
};
|
|
59
|
+
|
|
60
|
+
// Symbol.for (global registry) so the brand identity matches even if kumiko-types
|
|
61
|
+
// resolves to two independent copies (workspace symlink vs. published npm) — a plain
|
|
62
|
+
// `Symbol()` per copy would make each resolution's `UncheckedSystemDb` structurally
|
|
63
|
+
// incompatible with the other, per the kumiko.secret precedent in secrets-types.ts.
|
|
64
|
+
export const SYSTEM_SCOPE_CHECK_BRAND: unique symbol = Symbol.for("kumiko.system-scope-check");
|
|
65
|
+
|
|
66
|
+
export type UncheckedSystemDb = {
|
|
67
|
+
readonly [SYSTEM_SCOPE_CHECK_BRAND]: true;
|
|
68
|
+
assertTenantMatch(tenantId: TenantId): TenantDb;
|
|
69
|
+
assertRowsTenant<T>(rows: readonly T[], tenantField: keyof T): readonly T[];
|
|
70
|
+
acknowledgeCrossTenant(reason: string): TenantDb;
|
|
71
|
+
};
|