@cosmicdrift/kumiko-types 0.248.0 → 0.250.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 +1 -1
- package/src/event-store-executor-types.ts +11 -0
- package/src/feature.ts +14 -6
- package/src/fields.ts +5 -0
- package/src/handlers.ts +30 -9
- package/src/screen.ts +24 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.250.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>",
|
|
@@ -109,6 +109,17 @@ export type EventStoreExecutor = {
|
|
|
109
109
|
}>;
|
|
110
110
|
readonly resolveEntity: (entityName: string) => EntityDefinition | undefined;
|
|
111
111
|
};
|
|
112
|
+
// fw#2741 — `sortable: true` reference fields order by the target row's
|
|
113
|
+
// labelField instead of the raw FK column. Same request-time registry
|
|
114
|
+
// dependency (and the same fields/resolveEntity pairing) as referenceSearch.
|
|
115
|
+
readonly referenceSort?: {
|
|
116
|
+
readonly fields: ReadonlyArray<{
|
|
117
|
+
readonly fieldName: string;
|
|
118
|
+
readonly targetEntityName: string;
|
|
119
|
+
readonly labelField: string;
|
|
120
|
+
}>;
|
|
121
|
+
readonly resolveEntity: (entityName: string) => EntityDefinition | undefined;
|
|
122
|
+
};
|
|
112
123
|
},
|
|
113
124
|
) => Promise<CursorResult<Record<string, unknown>>>;
|
|
114
125
|
|
package/src/feature.ts
CHANGED
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
EntityRef,
|
|
38
38
|
EventDef,
|
|
39
39
|
EventMigrationDef,
|
|
40
|
-
|
|
40
|
+
EventPiiStance,
|
|
41
41
|
EventUpcastFn,
|
|
42
42
|
HandlerRef,
|
|
43
43
|
NameOrRef,
|
|
@@ -580,15 +580,18 @@ export type FeatureRegistrar<TFeature extends string = string> = {
|
|
|
580
580
|
// payload out) and run once per read, not once per event persisted, so
|
|
581
581
|
// keep them cheap.
|
|
582
582
|
//
|
|
583
|
-
// `options.piiFields`
|
|
584
|
-
//
|
|
585
|
-
//
|
|
583
|
+
// `options.piiFields` is a mandatory, explicit PII stance (fw#2558) —
|
|
584
|
+
// registration fails without one. Pass a map of payload fields to their
|
|
585
|
+
// owning `subjectField` for fields carrying personal data (encrypted
|
|
586
|
+
// under that user's DEK, crypto-shredding #799), or `"none"` to declare
|
|
587
|
+
// the payload holds no personal data. append() enforces the resulting
|
|
588
|
+
// catalog on every write path.
|
|
586
589
|
defineEvent<const TInner extends string, TPayload>(
|
|
587
590
|
name: TInner,
|
|
588
591
|
schema: ZodType<TPayload>,
|
|
589
|
-
options
|
|
592
|
+
options: {
|
|
590
593
|
readonly version?: number;
|
|
591
|
-
readonly piiFields
|
|
594
|
+
readonly piiFields: EventPiiStance;
|
|
592
595
|
readonly migrations?: readonly {
|
|
593
596
|
readonly fromVersion: number;
|
|
594
597
|
readonly toVersion: number;
|
|
@@ -888,6 +891,11 @@ export type Registry = {
|
|
|
888
891
|
targetEntityName: string;
|
|
889
892
|
labelField: string;
|
|
890
893
|
}>;
|
|
894
|
+
getSortableReferences(entityName: string): ReadonlyArray<{
|
|
895
|
+
fieldName: string;
|
|
896
|
+
targetEntityName: string;
|
|
897
|
+
labelField: string;
|
|
898
|
+
}>;
|
|
891
899
|
getIncomingRelations(entityName: string): ReadonlyArray<{
|
|
892
900
|
sourceEntity: string;
|
|
893
901
|
relationName: string;
|
package/src/fields.ts
CHANGED
|
@@ -483,6 +483,11 @@ export type ReferenceFieldDef = {
|
|
|
483
483
|
* validator rejects it otherwise, since the default "id" is a UUID column
|
|
484
484
|
* and ILIKE against it would crash at runtime. */
|
|
485
485
|
readonly searchable?: true;
|
|
486
|
+
/** Order a list by the referenced entity's `labelField` instead of by the raw
|
|
487
|
+
* UUID column (fw#2741). Requires an explicit, non-"id" `labelField` — the
|
|
488
|
+
* boot validator rejects it otherwise, since ordering by a UUID produces a
|
|
489
|
+
* sequence that looks meaningful to the user but is arbitrary. */
|
|
490
|
+
readonly sortable?: true;
|
|
486
491
|
} & ResolvedPiiFlags;
|
|
487
492
|
|
|
488
493
|
// --- Currency ---
|
package/src/handlers.ts
CHANGED
|
@@ -589,15 +589,29 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
|
|
|
589
589
|
// Job execution: db + registry + systemUser + logging guaranteed, plus a
|
|
590
590
|
// write path into the same dispatcher pipeline write-handlers use.
|
|
591
591
|
//
|
|
592
|
-
// write/queryAs throw until JobRunner.attachDispatcher() has run — a
|
|
593
|
-
// this early is a framework boot bug, not a job-author mistake.
|
|
592
|
+
// write/writeAs/queryAs throw until JobRunner.attachDispatcher() has run — a
|
|
593
|
+
// call this early is a framework boot bug, not a job-author mistake.
|
|
594
594
|
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
595
|
+
// `write` runs as the job's own systemUser, tenant-scoped via the job's
|
|
596
|
+
// _tenantId (falls back to SYSTEM_TENANT_ID for jobs with no tenant context,
|
|
597
|
+
// e.g. plain cron jobs). A job that writes without fanning out per-tenant
|
|
598
|
+
// (`_perTenant:` trigger) silently writes into the system tenant instead of
|
|
599
|
+
// each tenant it may have meant to reach — no guard against this, only this
|
|
600
|
+
// warning.
|
|
601
|
+
//
|
|
602
|
+
// `writeAs` is the explicit-identity counterpart, symmetric with `queryAs`:
|
|
603
|
+
// `hasAccess` has no system bypass, so a write handler whose `access` lists
|
|
604
|
+
// concrete roles is unreachable through `write` — the job's systemUser only
|
|
605
|
+
// carries SYSTEM_ROLE and the dispatch comes back `access_denied`
|
|
606
|
+
// (kumiko-framework#2585). The job has to assemble a full SessionUser for it:
|
|
607
|
+
// `triggeredBy` alone is not enough, it carries only id + tenantId and no
|
|
608
|
+
// roles, so the roles have to come from the job's own lookup.
|
|
609
|
+
//
|
|
610
|
+
// The passed identity also decides the target tenant — `writeAs` hands
|
|
611
|
+
// `user.tenantId` straight to the dispatcher with nothing cross-checking it
|
|
612
|
+
// against the job's own tenant, exactly as `queryAs` and the raw `db` above
|
|
613
|
+
// already do. Building an identity from job payload data is therefore a
|
|
614
|
+
// cross-tenant write path; derive it from a trusted lookup instead.
|
|
601
615
|
export type JobContext = SharedContextFields & {
|
|
602
616
|
readonly db: DbConnection;
|
|
603
617
|
readonly registry: Registry;
|
|
@@ -614,6 +628,7 @@ export type JobContext = SharedContextFields & {
|
|
|
614
628
|
// pass it to code that trusts the connection to already be scoped.
|
|
615
629
|
readonly systemDb?: UncheckedSystemDb;
|
|
616
630
|
readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
|
|
631
|
+
readonly writeAs: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
|
|
617
632
|
readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
|
|
618
633
|
// Multi-trigger jobs (`on: [...]`) use this to tell which trigger fired —
|
|
619
634
|
// undefined for cron/manual jobs. Mirrors AppContext.triggerName.
|
|
@@ -722,6 +737,12 @@ export type QualifiedEventName<
|
|
|
722
737
|
// no user key to shred for system-triggered events.
|
|
723
738
|
export type EventPiiFields = Readonly<Record<string, { readonly subjectField: string }>>;
|
|
724
739
|
|
|
740
|
+
// The full set of PII stances a defineEvent() call may declare. "none" is a
|
|
741
|
+
// distinct value from `{}` — an empty object is indistinguishable from "the
|
|
742
|
+
// author forgot to list fields", while "none" is an explicit assertion that
|
|
743
|
+
// the payload carries no personal data.
|
|
744
|
+
export type EventPiiStance = EventPiiFields | "none";
|
|
745
|
+
|
|
725
746
|
export type EventDef<TPayload = unknown, TName extends string = string> = {
|
|
726
747
|
readonly name: TName;
|
|
727
748
|
readonly schema: ZodType<TPayload>;
|
|
@@ -730,7 +751,7 @@ export type EventDef<TPayload = unknown, TName extends string = string> = {
|
|
|
730
751
|
// upcasts older stored events. Reads consult this to decide if upcasters
|
|
731
752
|
// need to run before the payload hits consumer code.
|
|
732
753
|
readonly version: number;
|
|
733
|
-
readonly piiFields?:
|
|
754
|
+
readonly piiFields?: EventPiiStance;
|
|
734
755
|
};
|
|
735
756
|
|
|
736
757
|
// Args for ctx.appendEvent — explicit aggregate target, Marten-style.
|
package/src/screen.ts
CHANGED
|
@@ -89,15 +89,19 @@ export type FormatSpec = {
|
|
|
89
89
|
export type FieldRenderer = PlatformComponent | string | FormatSpec;
|
|
90
90
|
|
|
91
91
|
// Declarative field-state condition. Evaluated by the renderer against the
|
|
92
|
-
// current row/form values.
|
|
92
|
+
// current row/form values. Five forms:
|
|
93
93
|
// boolean — static on/off (e.g. readOnly: true)
|
|
94
94
|
// { field, eq } — true when row[field] === eq
|
|
95
95
|
// { field, ne } — true when row[field] !== ne
|
|
96
|
+
// { field, in } — true when row[field] is one of the given values
|
|
97
|
+
// { field, notIn } — true when row[field] is none of the given values
|
|
96
98
|
// JSON-safe: survives buildAppSchema → window.__KUMIKO_SCHEMA__ stringify.
|
|
97
99
|
export type FieldCondition =
|
|
98
100
|
| boolean
|
|
99
101
|
| { readonly field: string; readonly eq: unknown }
|
|
100
|
-
| { readonly field: string; readonly ne: unknown }
|
|
102
|
+
| { readonly field: string; readonly ne: unknown }
|
|
103
|
+
| { readonly field: string; readonly in: readonly unknown[] }
|
|
104
|
+
| { readonly field: string; readonly notIn: readonly unknown[] };
|
|
101
105
|
|
|
102
106
|
// --- entityList ---
|
|
103
107
|
|
|
@@ -271,7 +275,9 @@ export type RowActionNavigateBase = {
|
|
|
271
275
|
readonly params?: RowFieldExtractor;
|
|
272
276
|
/** Conditional Visibility pro Row. */
|
|
273
277
|
readonly visible?: FieldCondition;
|
|
274
|
-
|
|
278
|
+
/** Visual style only — unlike the writeHandler variants, "danger" does not
|
|
279
|
+
* force a confirm dialog here: the target form is the confirmation. */
|
|
280
|
+
readonly style?: "primary" | "secondary" | "danger";
|
|
275
281
|
/** Wenn true, löst ein Klick auf die ganze Zeile (nicht nur das Aktionsmenü)
|
|
276
282
|
* diese navigate-Action aus. Max. eine pro Liste (Boot-Validator prüft). Nur
|
|
277
283
|
* auf navigate — ein Row-Klick darf keinen (evtl. destruktiven, unbestätigten)
|
|
@@ -316,7 +322,9 @@ export type RowActionDrawer = {
|
|
|
316
322
|
readonly params?: RowFieldExtractor;
|
|
317
323
|
/** Per-row conditional visibility. */
|
|
318
324
|
readonly visible?: FieldCondition;
|
|
319
|
-
|
|
325
|
+
/** Visual style only — unlike the writeHandler variants, "danger" does not
|
|
326
|
+
* force a confirm dialog here: the target form is the confirmation. */
|
|
327
|
+
readonly style?: "primary" | "secondary" | "danger";
|
|
320
328
|
/** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
|
|
321
329
|
* kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
|
|
322
330
|
* (renderer-web), analogous to EditFieldSpec.icon. */
|
|
@@ -334,7 +342,10 @@ export type ToolbarAction =
|
|
|
334
342
|
readonly label: string;
|
|
335
343
|
/** Screen-id (kurz, unqualified) zu dem navigiert wird. */
|
|
336
344
|
readonly screen: string;
|
|
337
|
-
|
|
345
|
+
/** Visual style only — unlike the writeHandler variant, "danger" does
|
|
346
|
+
* not force a confirm dialog here: the target form is the
|
|
347
|
+
* confirmation. */
|
|
348
|
+
readonly style?: "primary" | "secondary" | "danger";
|
|
338
349
|
}
|
|
339
350
|
| {
|
|
340
351
|
readonly kind: "writeHandler";
|
|
@@ -360,7 +371,10 @@ export type ToolbarAction =
|
|
|
360
371
|
* vs. drawer, not the form (see docs/plans/bundled-features-screen-
|
|
361
372
|
* standardisierung.md §2.6c). */
|
|
362
373
|
readonly screen: string;
|
|
363
|
-
|
|
374
|
+
/** Visual style only — unlike the writeHandler variant, "danger" does
|
|
375
|
+
* not force a confirm dialog here: the target form is the
|
|
376
|
+
* confirmation. */
|
|
377
|
+
readonly style?: "primary" | "secondary" | "danger";
|
|
364
378
|
};
|
|
365
379
|
|
|
366
380
|
export type EntityListScreenDefinition = {
|
|
@@ -1000,6 +1014,10 @@ export type ActionFormScreenDefinition = {
|
|
|
1000
1014
|
/** i18n-key für den Submit-Button. Default: i18n-Default des
|
|
1001
1015
|
* Renderers (typischerweise "actions.submit"). */
|
|
1002
1016
|
readonly submitLabel?: string;
|
|
1017
|
+
/** Visual style of the submit button. "danger" renders it red — for a form
|
|
1018
|
+
* whose handler is destructive (terminate, revoke, delete), so the warning
|
|
1019
|
+
* isn't lost on the way from the calling screen to this one. Default "primary". */
|
|
1020
|
+
readonly submitStyle?: "primary" | "danger";
|
|
1003
1021
|
/** Navigate to this screen ID after a successful submit: either a short
|
|
1004
1022
|
* ID (e.g. "item-list" — same feature, the nav-router resolves to the
|
|
1005
1023
|
* full path) or a fully-qualified cross-feature QN
|