@cosmicdrift/kumiko-types 0.249.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.249.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
@@ -275,7 +275,9 @@ export type RowActionNavigateBase = {
275
275
  readonly params?: RowFieldExtractor;
276
276
  /** Conditional Visibility pro Row. */
277
277
  readonly visible?: FieldCondition;
278
- 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";
279
281
  /** Wenn true, löst ein Klick auf die ganze Zeile (nicht nur das Aktionsmenü)
280
282
  * diese navigate-Action aus. Max. eine pro Liste (Boot-Validator prüft). Nur
281
283
  * auf navigate — ein Row-Klick darf keinen (evtl. destruktiven, unbestätigten)
@@ -320,7 +322,9 @@ export type RowActionDrawer = {
320
322
  readonly params?: RowFieldExtractor;
321
323
  /** Per-row conditional visibility. */
322
324
  readonly visible?: FieldCondition;
323
- 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";
324
328
  /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
325
329
  * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
326
330
  * (renderer-web), analogous to EditFieldSpec.icon. */
@@ -338,7 +342,10 @@ export type ToolbarAction =
338
342
  readonly label: string;
339
343
  /** Screen-id (kurz, unqualified) zu dem navigiert wird. */
340
344
  readonly screen: string;
341
- 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";
342
349
  }
343
350
  | {
344
351
  readonly kind: "writeHandler";
@@ -364,7 +371,10 @@ export type ToolbarAction =
364
371
  * vs. drawer, not the form (see docs/plans/bundled-features-screen-
365
372
  * standardisierung.md §2.6c). */
366
373
  readonly screen: string;
367
- 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";
368
378
  };
369
379
 
370
380
  export type EntityListScreenDefinition = {
@@ -1004,6 +1014,10 @@ export type ActionFormScreenDefinition = {
1004
1014
  /** i18n-key für den Submit-Button. Default: i18n-Default des
1005
1015
  * Renderers (typischerweise "actions.submit"). */
1006
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";
1007
1021
  /** Navigate to this screen ID after a successful submit: either a short
1008
1022
  * ID (e.g. "item-list" — same feature, the nav-router resolves to the
1009
1023
  * full path) or a fully-qualified cross-feature QN