@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-types",
3
- "version": "0.248.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
- EventPiiFields,
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` declares PII payload fields encrypted under the DEK
584
- // of the user named by `subjectField` (crypto-shredding, #799). append()
585
- // enforces the catalog on every write path.
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?: EventPiiFields;
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 call
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
- // Identity is implicit: write runs as the job's own systemUser, tenant-
596
- // scoped via the job's _tenantId (falls back to SYSTEM_TENANT_ID for jobs
597
- // with no tenant context, e.g. plain cron jobs). A job that writes without
598
- // fanning out per-tenant (`_perTenant:` trigger) silently writes into the
599
- // system tenant instead of each tenant it may have meant to reach — no
600
- // guard against this, only this warning.
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?: EventPiiFields;
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. Three forms:
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
- readonly style?: "primary" | "secondary";
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
- readonly style?: "primary" | "secondary";
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
- readonly style?: "primary" | "secondary";
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
- readonly style?: "primary" | "secondary";
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