@cosmicdrift/kumiko-types 0.399.0 → 0.400.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.
@@ -0,0 +1,26 @@
1
+ export type ParentRefDef = {
2
+ /** Field holding the name of the host entity this row hangs off. */
3
+ readonly entityTypeField: string;
4
+ /** Field holding the host row's id. */
5
+ readonly entityIdField: string;
6
+ /** Narrow which registered entities may act as host. Omitted means every
7
+ * registered entity is a candidate, which also widens the read-gate's SQL. */
8
+ readonly allowedTypes?: readonly string[];
9
+ };
10
+ export type ReadGateDef = {
11
+ /** Row readable only if at least one live row of `entity` with `field` = row.id is readable
12
+ * through that entity's own parentRef gate (host visibility). */
13
+ readonly anyLink?: {
14
+ readonly entity: string;
15
+ readonly field: string;
16
+ };
17
+ /** Row readable only if the file_ref in `field` is live and readable through its owner: owner
18
+ * row readable (ownership, parentRef, handler gate) and the field read access on the file_ref's
19
+ * fieldName passes. A file owned by this row itself passes this part. */
20
+ readonly fileOwner?: {
21
+ readonly field: string;
22
+ };
23
+ /** Roles that skip `anyLink` (they also see unlinked rows). `fileOwner` still applies; only
24
+ * the framework SYSTEM user skips both. */
25
+ readonly linkExemptRoles?: readonly string[];
26
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -4,10 +4,13 @@ import type { SessionUser, WriteResult } from "./handlers.ts";
4
4
  import type { DeleteContext, SaveContext } from "./hooks.ts";
5
5
  import type { EntityId, TenantId } from "./identifiers.ts";
6
6
  import type { ListAggregatePayload, ListAggregateResult } from "./list-aggregate-types.ts";
7
+ import type { SchemaTable } from "./schema-table-types.ts";
7
8
  import type { SearchAdapter } from "./search-adapter.ts";
8
9
  import type { TenantDb } from "./tenant-db-types.ts";
9
10
  export type ParentVisibilityOption = {
10
11
  readonly entities: ReadonlyMap<string, EntityDefinition>;
12
+ readonly canReadEntity?: (entityName: string) => boolean;
13
+ readonly resolveTable?: (entityName: string) => SchemaTable;
11
14
  };
12
15
  export type ParentVisibilityGateOption = {
13
16
  readonly parentVisibility?: ParentVisibilityOption;
@@ -35,6 +38,7 @@ export type ListRuntimeOptions = {
35
38
  readonly labelField: string;
36
39
  }>;
37
40
  readonly resolveEntity: (entityName: string) => EntityDefinition | undefined;
41
+ readonly resolveTable?: (entityName: string) => SchemaTable;
38
42
  };
39
43
  readonly referenceSort?: {
40
44
  readonly fields: ReadonlyArray<{
@@ -43,8 +47,16 @@ export type ListRuntimeOptions = {
43
47
  readonly labelField: string;
44
48
  }>;
45
49
  readonly resolveEntity: (entityName: string) => EntityDefinition | undefined;
50
+ readonly resolveTable?: (entityName: string) => SchemaTable;
46
51
  };
47
52
  readonly parentVisibility?: ParentVisibilityOption;
53
+ readonly derivedSql?: {
54
+ readonly resolveEntity: (entityName: string) => EntityDefinition | undefined;
55
+ readonly resolveTable: (entityName: string) => SchemaTable;
56
+ readonly canReadEntity: (entityName: string) => boolean;
57
+ readonly today: string;
58
+ readonly now: string;
59
+ };
48
60
  };
49
61
  type ListFilterOption = {
50
62
  readonly field: string;
@@ -0,0 +1,48 @@
1
+ export type FieldCondition = boolean | {
2
+ readonly field: string;
3
+ readonly eq: unknown;
4
+ } | {
5
+ readonly field: string;
6
+ readonly ne: unknown;
7
+ } | {
8
+ readonly field: string;
9
+ readonly in: readonly unknown[];
10
+ } | {
11
+ readonly field: string;
12
+ readonly notIn: readonly unknown[];
13
+ };
14
+ export type FieldStateConditions = {
15
+ /** Field is shown only while the condition holds on the record values. Hidden values are kept, never dropped. */
16
+ readonly visibleWhen?: Exclude<FieldCondition, boolean>;
17
+ /** Field is required while the condition holds (and the field is visible). Enforced by the server write path, not only the form. */
18
+ readonly requiredWhen?: Exclude<FieldCondition, boolean>;
19
+ };
20
+ /** Value is an array of FileRef ids; `file_refs.fieldName` stays the top-level embedded field name. */
21
+ export type EmbeddedFilesSubFieldProps = {
22
+ readonly maxSize?: string;
23
+ readonly accept?: readonly string[];
24
+ readonly maxCount?: number;
25
+ };
26
+ export type EmbeddedImagesSubFieldProps = EmbeddedFilesSubFieldProps & {
27
+ readonly capture?: "environment" | "user";
28
+ };
29
+ /** A cell computed from other cells of the same row. `from` names the
30
+ * sub-fields the operation reads (order matters for `subtract`). Renderer
31
+ * metadata — the derived value is rounded server- and client-side to the
32
+ * precision its target sub-field in `schema` declares (money → integer,
33
+ * decimal → `scale`) before it's validated. */
34
+ export type EmbeddedDerivedCellDef = {
35
+ /** Row-level condition on a sibling sub-field. When it does not hold for a row, the cell is not
36
+ * derived there: it stays a normal input and the row value is left untouched. */
37
+ readonly when?: Exclude<FieldCondition, boolean>;
38
+ } & ({
39
+ /** `percent` = `from[0] * from[1] / 100` (exactly two sources, e.g. amount and rate). */
40
+ readonly op: "multiply" | "sum" | "subtract" | "percent";
41
+ readonly from: readonly string[];
42
+ } | {
43
+ /** `from` is exactly `[value, min, max]`; bounds are inclusive and each may be missing. */
44
+ readonly op: "inRange";
45
+ readonly from: readonly string[];
46
+ readonly inside: string;
47
+ readonly outside: string;
48
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -1,27 +1,81 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  /** Display type a derived value formats as — drives the column's renderer
3
- * choice in the view-model, parallel to FieldDefinition["type"]. Single-column
4
- * types only: `money` is excluded because it needs a `<name>Currency`
5
- * companion column a derived field has no place to put — use `number`/
6
- * `decimal` plus a `{ format: "currency" }` column renderer instead. */
7
- export type DerivedValueType = "text" | "number" | "decimal" | "boolean" | "date" | "timestamp";
3
+ * choice in the view-model, parallel to FieldDefinition["type"]. `money` is
4
+ * SQL-only and only for a `related` + `first` measure of a money field: the
5
+ * value is the stored money read shape (`{ amount, currency, ... }`), the
6
+ * currency read next to the amount. A JS `derive` cannot return money. */
7
+ export type DerivedValueType = "text" | "number" | "decimal" | "boolean" | "date" | "timestamp" | "money";
8
8
  /** Clock injected into `derive` — never read `Temporal.Now`/`Date` inside a
9
9
  * derive body (no-date-api guard + testability). The list-query handler passes
10
10
  * the read-time instant; unit tests pass a fixed one. */
11
11
  export type DeriveContext = {
12
12
  readonly asOf: Temporal.Instant;
13
13
  };
14
- export type DerivedFieldDef = {
14
+ export type JsDerivedFieldDef = {
15
15
  readonly valueType: DerivedValueType;
16
16
  /** Pure function of the stored row + clock. Returns the JSON-safe display
17
17
  * value (e.g. integer minor units for a currency column, ISO string for a
18
- * `date`). */
18
+ * `date`). Runs after SQL derived values are on the row. */
19
19
  readonly derive: (row: Readonly<Record<string, unknown>>, ctx: DeriveContext) => unknown;
20
20
  };
21
+ export type SqlDerivedFieldDef = {
22
+ readonly valueType: DerivedValueType;
23
+ readonly sortable?: boolean;
24
+ readonly sql: DerivedSqlSpec;
25
+ readonly derive?: undefined;
26
+ };
27
+ export type DerivedFieldDef = JsDerivedFieldDef | SqlDerivedFieldDef;
28
+ export type DerivedSqlSpec = {
29
+ readonly kind: "coalesce";
30
+ readonly fields: readonly string[];
31
+ } | {
32
+ readonly kind: "related";
33
+ /** Same convention as a reference field's `entity` ("feature:entity"). */
34
+ readonly entity: string;
35
+ /** related.<field> = outer.<outer ?? "id"> */
36
+ readonly on: {
37
+ readonly field: string;
38
+ readonly outer?: string;
39
+ };
40
+ readonly measure: DerivedRelatedMeasure;
41
+ /** AND-ed. */
42
+ readonly where?: readonly DerivedCondition[];
43
+ };
44
+ export type DerivedRelatedMeasure = {
45
+ readonly kind: "count";
46
+ } | {
47
+ readonly kind: "countDistinct";
48
+ readonly field: string;
49
+ } | {
50
+ readonly kind: "first";
51
+ readonly field: string;
52
+ readonly orderBy: string;
53
+ readonly direction: "asc" | "desc";
54
+ /** Only for a reference field with labelField: yields the referenced row's label. */
55
+ readonly label?: boolean;
56
+ };
57
+ export type DerivedConditionValue = string | number | boolean | null | readonly (string | number)[] | {
58
+ readonly today: true;
59
+ } | {
60
+ readonly now: true;
61
+ };
62
+ export type DerivedCondition = {
63
+ readonly field: string;
64
+ readonly op: "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
65
+ readonly value: DerivedConditionValue;
66
+ }
67
+ /** from <= today AND (to IS NULL OR to >= today) */
68
+ | {
69
+ readonly activeOn: {
70
+ readonly from: string;
71
+ readonly to: string;
72
+ };
73
+ };
74
+ export declare function isSqlDerivedField(def: DerivedFieldDef): def is SqlDerivedFieldDef;
21
75
  export type DerivedFieldsMap = Readonly<Record<string, DerivedFieldDef>>;
22
- /** Client-facing projection of DerivedFieldDef — `derive` is server-only and
23
- * not JSON-safe (would trip the output-walk guard), so the browser schema
24
- * only ever carries `valueType`. A real `Pick`, not a same-shape cast: TS
25
- * itself proves `derive` isn't there instead of a `{ valueType } as
26
- * DerivedFieldDef` cast lying about a field that's actually missing. */
27
- export type ClientDerivedFieldDef = Pick<DerivedFieldDef, "valueType">;
76
+ /** Client-facing projection of DerivedFieldDef: `derive`/`sql` are server-only
77
+ * (not JSON-safe), so the browser schema only carries `valueType` and `sortable`. */
78
+ export type ClientDerivedFieldDef = {
79
+ readonly valueType: DerivedValueType;
80
+ readonly sortable?: true;
81
+ };
@@ -1,20 +1,13 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  // --- Derived (computed) fields ---
3
3
  //
4
- // A derived field is read-time only: its value is computed from the stored row
5
- // (and the clock) when an entityList query runs (676/2 — NOT detail; only the
6
- // "list" case in entity-handlers.ts calls augmentDerivedFields), never
7
- // persisted. It lives in
8
- // `EntityDefinition.derivedFields` — deliberately NOT in `fields`, so it
9
- // produces no DB column, never enters a write schema, and can't be the target
10
- // of an entityEdit. A declarative `entityList` can name it like any column and
11
- // the view-model renders the appended value.
12
- //
13
- // LIMIT: derived columns are DISPLAY ONLY. A declarative `entityList` loads its
14
- // rows server-side and a column-header sort round-trips to the server, where
15
- // `executor.list` sorts/filters/searches over real SQL columns — so a derived
16
- // field (no column) silently no-ops. There is no client-side sort path. Need a
17
- // derived value sortable/searchable? Materialize it as a stored field; it then
18
- // rides the existing `searchable`/`sortable` machinery. Time-dependent values
19
- // (as-of-today) can't be materialized without a daily re-index anyway.
20
- export {};
4
+ // A derived field is read-time only: never persisted, no DB column, never in a
5
+ // write schema. It lives in `EntityDefinition.derivedFields`, not in `fields`.
6
+ // JS `derive` fields run in the list handler after the SQL fetch and are
7
+ // display-only. `sql` fields are computed inside the list SELECT as correlated
8
+ // subqueries (identifiers come from registry-validated field names, values are
9
+ // always bound params) and can opt into `sortable`. Time-dependent values stay
10
+ // read-time; neither kind enters the entity cache.
11
+ export function isSqlDerivedField(def) {
12
+ return "sql" in def;
13
+ }
package/dist/fields.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  import type { VariantSpec } from "./derivatives-types.ts";
3
+ import type { EmbeddedDerivedCellDef, EmbeddedFilesSubFieldProps, EmbeddedImagesSubFieldProps, FieldCondition, FieldStateConditions } from "./field-state-conditions.ts";
4
+ export type { EmbeddedDerivedCellDef, EmbeddedFilesSubFieldProps, EmbeddedImagesSubFieldProps, FieldCondition, FieldStateConditions, } from "./field-state-conditions.ts";
5
+ import type { ParentRefDef, ReadGateDef } from "./entity-row-visibility.ts";
3
6
  import type { DerivedFieldsMap } from "./fields-derived.ts";
4
7
  import type { EventUpcastCtx } from "./handlers.ts";
5
8
  import type { OwnershipMap } from "./ownership.ts";
6
9
  import type { EntityTenancy } from "./tenancy-brand.ts";
7
- export type { ClientDerivedFieldDef, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, } from "./fields-derived.ts";
10
+ export type { ClientDerivedFieldDef, DeriveContext, DerivedCondition, DerivedConditionValue, DerivedFieldDef, DerivedFieldsMap, DerivedRelatedMeasure, DerivedSqlSpec, DerivedValueType, JsDerivedFieldDef, SqlDerivedFieldDef, } from "./fields-derived.ts";
11
+ export { isSqlDerivedField } from "./fields-derived.ts";
8
12
  export type FieldAccess = {
9
13
  readonly read?: OwnershipMap | readonly string[];
10
14
  readonly write?: OwnershipMap | readonly string[];
@@ -30,6 +34,7 @@ export type ResolvedPiiFlags = {
30
34
  * that lives purely in an FK column. */
31
35
  readonly subjectRef?: true;
32
36
  };
37
+ type PiiAndFieldState = ResolvedPiiFlags & FieldStateConditions;
33
38
  export type EntityPiiAbsentSubject = "tenant" | "self";
34
39
  export type PersonalSubject = "self" | {
35
40
  readonly of: string;
@@ -137,7 +142,7 @@ export type TextFieldDef = {
137
142
  readonly multiline?: boolean | {
138
143
  readonly rows?: number;
139
144
  };
140
- } & ResolvedPiiFlags;
145
+ } & PiiAndFieldState;
141
146
  /**
142
147
  * Long-form text content — source code, markdown, blog posts, email
143
148
  * templates, and anything that can be megabytes large. Its surface is
@@ -176,7 +181,7 @@ export type LongTextFieldDef = {
176
181
  readonly multiline?: boolean | {
177
182
  readonly rows?: number;
178
183
  };
179
- } & ResolvedPiiFlags;
184
+ } & PiiAndFieldState;
180
185
  export type BooleanFieldDef = {
181
186
  readonly type: "boolean";
182
187
  readonly description?: string;
@@ -187,7 +192,7 @@ export type BooleanFieldDef = {
187
192
  readonly sensitive?: boolean;
188
193
  readonly default?: boolean;
189
194
  readonly access?: FieldAccess;
190
- };
195
+ } & FieldStateConditions;
191
196
  export declare const SELECT_OPTION_TONES: readonly ["ok", "warn", "bad", "critical", "accent", "neutral"];
192
197
  export type SelectOptionTone = (typeof SELECT_OPTION_TONES)[number];
193
198
  /** One `optionsQueryPayload` entry: a literal, or `{ field }` = the current value of
@@ -200,19 +205,6 @@ export declare function isOptionsQueryFieldRef(value: OptionsQueryPayloadValue):
200
205
  readonly field: string;
201
206
  };
202
207
  export declare function optionsQueryFieldRefs(payload: OptionsQueryPayload | undefined): string[];
203
- export type FieldCondition = boolean | {
204
- readonly field: string;
205
- readonly eq: unknown;
206
- } | {
207
- readonly field: string;
208
- readonly ne: unknown;
209
- } | {
210
- readonly field: string;
211
- readonly in: readonly unknown[];
212
- } | {
213
- readonly field: string;
214
- readonly notIn: readonly unknown[];
215
- };
216
208
  /** Options of a select field that are only available while `when` holds for the row values. */
217
209
  export type SelectConditionalOptions<TOption extends string = string> = {
218
210
  readonly options: readonly TOption[];
@@ -262,7 +254,7 @@ export type SelectFieldDef<TOptions extends readonly string[] = readonly string[
262
254
  * in no rule are always available. The write path enforces it, the renderer only
263
255
  * filters. Only valid on entity fields. */
264
256
  readonly conditionalOptions?: readonly SelectConditionalOptions<TOptions[number]>[];
265
- } & ResolvedPiiFlags;
257
+ } & PiiAndFieldState;
266
258
  export type MultiSelectFieldDef<TOptions extends readonly string[] = readonly string[]> = {
267
259
  readonly type: "multiSelect";
268
260
  readonly description?: string;
@@ -291,7 +283,7 @@ export type MultiSelectFieldDef<TOptions extends readonly string[] = readonly st
291
283
  * `hint` is ignored. Static `options` stay authoritative and the write path must
292
284
  * enforce the same rule, the renderer only mirrors it. Called with an empty payload. */
293
285
  readonly optionsAvailabilityQuery?: string;
294
- } & ResolvedPiiFlags;
286
+ } & PiiAndFieldState;
295
287
  /**
296
288
  * Storage: `integer` flag decides the Postgres column type, not just Zod
297
289
  * validation. `integer: true` → `integer` column (32-bit, ~±2.1 billion),
@@ -325,7 +317,7 @@ export type NumberFieldDef = {
325
317
  * Default `true`. Set `false` for values that read wrong grouped (e.g. a
326
318
  * model year: "2021", not "2.021"). Never affects the stored value. */
327
319
  readonly grouping?: boolean;
328
- } & ResolvedPiiFlags;
320
+ } & PiiAndFieldState;
329
321
  /**
330
322
  * 64-bit integer field for audit counters, byte sizes, event IDs, and other
331
323
  * values that can exceed 2^31 (~2.1 billion). Stored as Postgres `bigint`,
@@ -347,7 +339,7 @@ export type BigIntFieldDef = {
347
339
  readonly sensitive?: boolean;
348
340
  readonly default?: number;
349
341
  readonly access?: FieldAccess;
350
- } & ResolvedPiiFlags;
342
+ } & PiiAndFieldState;
351
343
  /**
352
344
  * Exact decimal — Postgres `numeric(precision, scale)`. For values that need
353
345
  * fractional precision the integer `number` field (32-bit int) and `money`
@@ -372,7 +364,7 @@ export type DecimalFieldDef = {
372
364
  readonly sensitive?: boolean;
373
365
  readonly default?: number;
374
366
  readonly access?: FieldAccess;
375
- } & ResolvedPiiFlags;
367
+ } & PiiAndFieldState;
376
368
  /** Where a money field's currency comes from. `"tenant"` is the
377
369
  * tenant-settings bundle's per-tenant currency (`tenant-settings:config:
378
370
  * currency`); `"literal"` names a fixed ISO code and
@@ -405,7 +397,7 @@ export type MoneyFieldDef = {
405
397
  * (`entity.defaultCurrency ?? "EUR"`); a stored value always keeps its
406
398
  * own currency regardless of this declaration. */
407
399
  readonly currency?: MoneyCurrencySource;
408
- };
400
+ } & FieldStateConditions;
409
401
  export type ReferenceFieldDef = {
410
402
  readonly type: "reference";
411
403
  readonly description?: string;
@@ -460,13 +452,13 @@ export type ReferenceFieldDef = {
460
452
  * reference then deliberately crosses the tenant boundary after the
461
453
  * handover. Use it for an entity that must not move with its host. */
462
454
  readonly handover?: "stay";
463
- } & ResolvedPiiFlags;
455
+ } & PiiAndFieldState;
464
456
  export type DefaultCurrency = "EUR" | "USD" | "GBP" | "CHF" | "JPY" | "SEK" | "NOK" | "DKK" | "PLN" | "CZK" | "CAD" | "AUD" | "NZD" | "CNY" | "INR";
465
457
  type EmbeddedSubFieldBase = {
466
458
  readonly required?: boolean;
467
459
  readonly searchable?: boolean;
468
460
  readonly access?: FieldAccess;
469
- };
461
+ } & FieldStateConditions;
470
462
  /** `money` is a signed integer amount in minor units (cents); which currency
471
463
  * it means is defined by the owning aggregate (a head-level field or tenant
472
464
  * config), not per row. Unlike the top-level `money` field (BIGINT + currency
@@ -487,23 +479,23 @@ export type EmbeddedSubFieldDef = (EmbeddedSubFieldBase & {
487
479
  /** Same contract as `SelectFieldDef.display`; in a table cell the
488
480
  * default stays a dropdown. */
489
481
  readonly display?: "radio" | "dropdown";
482
+ readonly optionTones?: {
483
+ readonly [option: string]: SelectOptionTone;
484
+ };
490
485
  }) | (EmbeddedSubFieldBase & {
491
486
  readonly type: "reference";
492
487
  readonly entity: string;
493
488
  readonly labelField?: string;
494
489
  /** Same contract as `ReferenceFieldDef.optionsQuery`. */
495
490
  readonly optionsQuery?: string;
491
+ }) | (EmbeddedSubFieldBase & EmbeddedImagesSubFieldProps & {
492
+ readonly type: "images";
493
+ }) | (EmbeddedSubFieldBase & EmbeddedFilesSubFieldProps & {
494
+ readonly type: "files";
496
495
  });
497
- /** A cell computed from other cells of the same row. `from` names the
498
- * sub-fields the operation reads (order matters for `subtract`). Renderer
499
- * metadata — the derived value is rounded server- and client-side to the
500
- * precision its target sub-field in `schema` declares (money → integer,
501
- * decimal → `scale`) before it's validated. */
502
- export type EmbeddedDerivedCellDef = {
503
- /** `percent` = `from[0] * from[1] / 100` (exactly two sources, e.g. amount and rate). */
504
- readonly op: "multiply" | "sum" | "subtract" | "percent";
505
- readonly from: readonly string[];
506
- };
496
+ export type EmbeddedFileSubFieldDef = Extract<EmbeddedSubFieldDef, {
497
+ type: "images" | "files";
498
+ }>;
507
499
  export type EmbeddedTotalDef = string | {
508
500
  readonly label: string;
509
501
  readonly from: readonly string[];
@@ -547,7 +539,7 @@ export type EmbeddedFieldDef = {
547
539
  * rounded `derived` cells, i.e. "sum-of-rounded" not "round-of-sum" —
548
540
  * see the embedded-list totals-matching behavior. */
549
541
  readonly totalsMatch?: Readonly<Record<string, string>>;
550
- } & ResolvedPiiFlags;
542
+ } & PiiAndFieldState;
551
543
  export type JsonbFieldDef = {
552
544
  readonly type: "jsonb";
553
545
  readonly description?: string;
@@ -561,7 +553,7 @@ export type JsonbFieldDef = {
561
553
  * run that already read the compromised value. See irreversible-operation-gate. */
562
554
  readonly readAsInstruction?: boolean;
563
555
  readonly access?: FieldAccess;
564
- } & ResolvedPiiFlags;
556
+ } & PiiAndFieldState;
565
557
  export type JsonbEmptyValue = "object" | "array";
566
558
  export type DateFieldDef = {
567
559
  readonly type: "date";
@@ -580,7 +572,7 @@ export type DateFieldDef = {
580
572
  /** Format/locale override for display and input parsing (for example,
581
573
  * "de-DE"). Default: app locale. */
582
574
  readonly locale?: string;
583
- } & ResolvedPiiFlags;
575
+ } & PiiAndFieldState;
584
576
  export type TimestampFieldDef = {
585
577
  readonly type: "timestamp";
586
578
  readonly description?: string;
@@ -606,14 +598,14 @@ export type TimestampFieldDef = {
606
598
  /** Format/locale override for display and input parsing. Default: app
607
599
  * locale. */
608
600
  readonly locale?: string;
609
- } & ResolvedPiiFlags;
601
+ } & PiiAndFieldState;
610
602
  export type TzFieldDef = {
611
603
  readonly type: "tz";
612
604
  readonly description?: string;
613
605
  readonly required?: boolean;
614
606
  readonly sensitive?: boolean;
615
607
  readonly access?: FieldAccess;
616
- } & ResolvedPiiFlags;
608
+ } & PiiAndFieldState;
617
609
  export type LocatedTimestampFieldDef = {
618
610
  readonly type: "locatedTimestamp";
619
611
  readonly description?: string;
@@ -631,7 +623,7 @@ export type LocatedTimestampFieldDef = {
631
623
  /** Format/locale override for display and input parsing. Default: app
632
624
  * locale. */
633
625
  readonly locale?: string;
634
- } & ResolvedPiiFlags;
626
+ } & PiiAndFieldState;
635
627
  export type FileFieldDef = {
636
628
  readonly type: "file";
637
629
  readonly description?: string;
@@ -642,7 +634,7 @@ export type FileFieldDef = {
642
634
  readonly anonymousUpload?: boolean;
643
635
  readonly accept?: readonly string[];
644
636
  readonly access?: FieldAccess;
645
- } & ResolvedPiiFlags;
637
+ } & PiiAndFieldState;
646
638
  export type ImageFieldDef = {
647
639
  readonly type: "image";
648
640
  readonly description?: string;
@@ -660,7 +652,7 @@ export type ImageFieldDef = {
660
652
  * camera instead of the file picker. */
661
653
  readonly capture?: "environment" | "user";
662
654
  readonly access?: FieldAccess;
663
- } & ResolvedPiiFlags;
655
+ } & PiiAndFieldState;
664
656
  export type FilesFieldDef = {
665
657
  readonly type: "files";
666
658
  readonly description?: string;
@@ -675,7 +667,7 @@ export type FilesFieldDef = {
675
667
  * `maxCount`/`accept`/`maxSize` as hints. The stored value is the same array of FileRef ids. */
676
668
  readonly display?: "list" | "dropzone";
677
669
  readonly access?: FieldAccess;
678
- } & ResolvedPiiFlags;
670
+ } & PiiAndFieldState;
679
671
  export type ImagesFieldDef = {
680
672
  readonly type: "images";
681
673
  readonly description?: string;
@@ -694,7 +686,7 @@ export type ImagesFieldDef = {
694
686
  * camera instead of the file picker. */
695
687
  readonly capture?: "environment" | "user";
696
688
  readonly access?: FieldAccess;
697
- } & ResolvedPiiFlags;
689
+ } & PiiAndFieldState;
698
690
  export type FieldDefinition = TextFieldDef | LongTextFieldDef | BooleanFieldDef | SelectFieldDef | MultiSelectFieldDef | NumberFieldDef | BigIntFieldDef | DecimalFieldDef | MoneyFieldDef | ReferenceFieldDef | EmbeddedFieldDef | JsonbFieldDef | DateFieldDef | TimestampFieldDef | TzFieldDef | LocatedTimestampFieldDef | FileFieldDef | ImageFieldDef | FilesFieldDef | ImagesFieldDef;
699
691
  export declare const FIELD_TYPE_NAMES: readonly FieldDefinition["type"][];
700
692
  /** FieldDefinition as an entity-less inline form screen (actionForm,
@@ -758,15 +750,7 @@ export type EntityTextFieldDef = TextFieldDef & ResolvedPiiStance;
758
750
  export type EntityLongTextFieldDef = LongTextFieldDef & ResolvedPiiStance;
759
751
  export type EntityFieldDefinition = Exclude<FieldDefinition, TextFieldDef | LongTextFieldDef> | EntityTextFieldDef | EntityLongTextFieldDef;
760
752
  export type FieldsMap = Readonly<Record<string, EntityFieldDefinition>>;
761
- export type ParentRefDef = {
762
- /** Field holding the name of the host entity this row hangs off. */
763
- readonly entityTypeField: string;
764
- /** Field holding the host row's id. */
765
- readonly entityIdField: string;
766
- /** Narrow which registered entities may act as host. Omitted means every
767
- * registered entity is a candidate, which also widens the read-gate's SQL. */
768
- readonly allowedTypes?: readonly string[];
769
- };
753
+ export type { ParentRefDef, ReadGateDef } from "./entity-row-visibility.ts";
770
754
  export type EntityEventMigration = {
771
755
  readonly fromVersion: number;
772
756
  readonly toVersion: number;
@@ -841,6 +825,13 @@ export type EntityDefinition<F extends FieldsMap = FieldsMap, T extends EntityTe
841
825
  * registered entity is denied.
842
826
  */
843
827
  readonly parentRef?: ParentRefDef;
828
+ /**
829
+ * Declarative read gate for rows readable only through other rows (a link to a visible host,
830
+ * a file whose owner is readable). AND-ed with `access.read` and `parentRef` in list, detail,
831
+ * count, aggregate, live signals and the writers' readability check. Without the request-time
832
+ * registry the gate denies every non-system caller.
833
+ */
834
+ readonly readGate?: ReadGateDef;
844
835
  /**
845
836
  * Default retention policy for this entity. Tenant administrators can
846
837
  * override it through compliance profiles and tenant overrides.
package/dist/fields.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  // --- Field Types ---
3
+ export { isSqlDerivedField } from "./fields-derived.js";
3
4
  export const SELECT_OPTION_TONES = ["ok", "warn", "bad", "critical", "accent", "neutral"];
4
5
  export function isOptionsQueryFieldRef(value) {
5
6
  return typeof value === "object" && value !== null;
@@ -117,10 +117,14 @@ export type QueryEvent<TPayload = unknown> = {
117
117
  readonly payload: TPayload;
118
118
  readonly user: SessionUser;
119
119
  };
120
+ import type { LifecycleResult } from "./hooks.ts";
120
121
  import type { WriteFailure } from "./write-error-info-types.ts";
121
122
  export type WriteResult<TData = unknown> = {
122
123
  readonly isSuccess: true;
123
124
  readonly data: TData;
125
+ /** The executor result behind a handler that answers with its own `data`. Hooks and
126
+ * projections run on it; the dispatcher removes it from what leaves the pipeline. */
127
+ readonly lifecycle?: LifecycleResult;
124
128
  } | WriteFailure;
125
129
  import type { Registry } from "./feature.ts";
126
130
  import type { TenantId } from "./identifiers.ts";
@@ -79,6 +79,9 @@ export type FieldFillAction = {
79
79
  export type EmbeddedListEditSpec = {
80
80
  /** Sub-fields shown as read-only cells; every other cell stays editable. */
81
81
  readonly readOnlyCells?: readonly string[];
82
+ /** Sub-fields that only steer a row (e.g. the kind that decides which controls
83
+ * appear) and are not shown on phone cards; the desktop table still shows them. */
84
+ readonly cardHiddenCells?: readonly string[];
82
85
  /** `true` keeps the rows as they are: no add, remove, duplicate, move or
83
86
  * paste-appended rows. Cells stay editable (unless in `readOnlyCells`). */
84
87
  readonly fixedRows?: boolean;
@@ -195,6 +198,10 @@ export type EditRelatedListSection = {
195
198
  readonly id?: string;
196
199
  readonly kind: "relatedList";
197
200
  readonly title: string;
201
+ /** Rows are aggregates or other values with no record of their own, so they lead nowhere on
202
+ * purpose. Declares that to the screen-conventions guard; with `rowClick`/`rowActions` it is a
203
+ * boot error. */
204
+ readonly readOnly?: boolean;
198
205
  /** Fully qualified query QN. Same paged envelope as projectionList.query:
199
206
  * `{ rows, nextCursor, total? }`. */
200
207
  readonly query: string;
@@ -282,6 +289,11 @@ export type EditRelatedListSection = {
282
289
  * Ignored outside tabs mode or when the field's value is not a finite
283
290
  * number. */
284
291
  readonly countField?: string;
292
+ /** Tab badge counted from this section's own query (`limit: 1, totalCount: true`
293
+ * → `total`), so it matches the tab's rows and stays live. Tabs mode only,
294
+ * exclusive with `countField`; the query's Zod schema must accept `totalCount`
295
+ * and `limit`. */
296
+ readonly count?: true;
285
297
  /** Rendered top-right in the section's Card title row — same
286
298
  * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
287
299
  * evaluated against the parent record. Every action must resolve an icon
@@ -9,7 +9,7 @@ export type ExpandableRowGrouping = Omit<ListTableGrouping, "field" | "userChoic
9
9
  * layout and are therefore omitted. `kind` stays the discriminator so further
10
10
  * expansion kinds can be added without a breaking change. `table` takes only
11
11
  * `grouping`; the other table options belong on the entityList itself. */
12
- export type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField" | "table" | "bulkActions"> & {
12
+ export type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField" | "count" | "table" | "bulkActions"> & {
13
13
  readonly table?: {
14
14
  readonly grouping?: ExpandableRowGrouping;
15
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-types",
3
- "version": "0.399.0",
3
+ "version": "0.400.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": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",