@cosmicdrift/kumiko-types 0.387.0 → 0.389.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/dist/feature.d.ts CHANGED
@@ -1,20 +1,16 @@
1
1
  import type * as z from "zod";
2
2
  import type { ZodType } from "zod";
3
- import type { ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigSeedDef, ExtensionSelectorDef, ExtensionSelectorPanel, JobDefinition, JobHandlerFn, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, TemplateVariables, TranslationKeys, TranslationsDef } from "./config.ts";
4
- import type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition } from "./define-handler.ts";
5
- import type { RegisterEntityCrudOptions } from "./entity-handlers.ts";
3
+ import type { ConfigKeyDefinition, ConfigSeedDef, ExtensionSelectorDef, ExtensionSelectorPanel, JobDefinition, NotificationDefinition, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, TranslationKeys } from "./config.ts";
6
4
  import type { EntityTableMeta } from "./entity-table-meta-types.ts";
7
- import type { KumikoEventTypeMap } from "./event-type-map.ts";
8
- import type { ExtensionOptionsArgs, ExtensionOptionsFor } from "./extension-options-map.ts";
9
5
  import type { EntityDefinition } from "./fields.ts";
10
- import type { AccessRule, AuthClaimsFn, AuthClaimsHookDef, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyType, DeclarativeEventMigration, EntityRef, EscapeHatchDeclaration, EventDef, EventMigrationDef, EventPiiStance, EventUpcastFn, HandlerRef, NameOrRef, QualifiedEventName, QueryHandlerDef, RateLimitDeclaration, StreamHandlerDef, StreamHandlerFn, WriteHandlerDef } from "./handlers.ts";
11
- import type { EntityHookMap, HookMap, HookPhase, OwnedFn, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SearchPayloadContributorFn, ValidationHookFn } from "./hooks.ts";
6
+ import type { AuthClaimsFn, AuthClaimsHookDef, ClaimKeyDefinition, EventDef, EventMigrationDef, EventUpcastFn, QueryHandlerDef, StreamHandlerDef, WriteHandlerDef } from "./handlers.ts";
7
+ import type { EntityHookMap, HookMap, HookPhase, OwnedFn, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SearchPayloadContributorFn } from "./hooks.ts";
12
8
  import type { HttpRouteDefinition } from "./http-route.ts";
13
9
  import type { ContentCollectionDefinition, NavDefinition } from "./nav.ts";
14
10
  import type { EntityProjectionExtension, MultiStreamProjectionDefinition, ProjectionDefinition } from "./projection.ts";
15
11
  import type { EntityRelations, RelationDefinition } from "./relations.ts";
16
12
  import type { ScreenDefinition } from "./screen.ts";
17
- import type { TreeActionDef, TreeActionsHandle } from "./tree-node.ts";
13
+ import type { TreeActionDef } from "./tree-node.ts";
18
14
  import type { WebSocketRouteDefinition } from "./websocket-route.ts";
19
15
  import type { WorkspaceDefinition } from "./workspace.ts";
20
16
  export type FeatureMetricType = "counter" | "histogram" | "gauge";
@@ -116,7 +112,7 @@ export type BootCheckContext = {
116
112
  };
117
113
  export type BootCheckFn = (ctx: BootCheckContext) => void;
118
114
  /** runProdApp options a stack can depend on; runProdApp checks the set at boot. */
119
- export type BootOptionName = "mail" | "kmsMode" | "fileStorage" | "billing" | "featureToggles" | "demoMode" | "seeds" | "signals" | "rateLimit";
115
+ export type BootOptionName = "mail" | "kmsMode" | "fileStorage" | "billing" | "featureToggles" | "rateLimit";
120
116
  export type StackBootRequirement = {
121
117
  readonly stack: string;
122
118
  readonly options: readonly BootOptionName[];
@@ -133,6 +129,7 @@ export type FeatureDefinition = {
133
129
  readonly requiredProjections: ReadonlySet<string>;
134
130
  readonly requiredSteps: ReadonlySet<string>;
135
131
  readonly toggleableDefault?: boolean;
132
+ readonly adoptsBundledLists?: true;
136
133
  readonly uiHints?: UiHints;
137
134
  readonly entities?: Readonly<Record<string, EntityDefinition>>;
138
135
  readonly entityTables?: Readonly<Record<string, unknown>>;
@@ -198,310 +195,6 @@ export type FeatureDefinition = {
198
195
  readonly envSchema?: z.ZodObject<z.ZodRawShape>;
199
196
  readonly dedupeOptions?: Readonly<Record<string, unknown>>;
200
197
  };
201
- type RefOrRefs = NameOrRef | readonly NameOrRef[];
202
- type HookTarget = RefOrRefs | {
203
- readonly allOf: NameOrRef;
204
- };
205
- export type EntityObjectDefinition = {
206
- readonly name: string;
207
- readonly backingTable?: unknown;
208
- } & EntityDefinition;
209
- type HookEscapeHatch = {
210
- readonly escapeHatch?: EscapeHatchDeclaration;
211
- };
212
- type HookPhaseOption = {
213
- readonly phase?: HookPhase;
214
- };
215
- export type HookDefinition = {
216
- readonly type: "validation";
217
- readonly target: RefOrRefs;
218
- readonly handler: ValidationHookFn;
219
- readonly escapeHatch?: never;
220
- readonly phase?: never;
221
- } | ({
222
- readonly type: "preSave";
223
- readonly target: RefOrRefs;
224
- readonly handler: PreSaveHookFn;
225
- readonly phase?: never;
226
- } & HookEscapeHatch) | ({
227
- readonly type: "postSave";
228
- readonly target: HookTarget;
229
- readonly handler: PostSaveHookFn;
230
- } & HookEscapeHatch & HookPhaseOption) | ({
231
- readonly type: "preDelete";
232
- readonly target: HookTarget;
233
- readonly handler: PreDeleteHookFn;
234
- readonly phase?: never;
235
- } & HookEscapeHatch) | ({
236
- readonly type: "postDelete";
237
- readonly target: HookTarget;
238
- readonly handler: PostDeleteHookFn;
239
- } & HookEscapeHatch & HookPhaseOption) | ({
240
- readonly type: "preQuery";
241
- readonly target: RefOrRefs;
242
- readonly handler: PreQueryHookFn;
243
- readonly phase?: never;
244
- } & HookEscapeHatch) | ({
245
- readonly type: "postQuery";
246
- readonly target: HookTarget;
247
- readonly handler: PostQueryHookFn;
248
- readonly phase?: never;
249
- } & HookEscapeHatch);
250
- /**
251
- * `r.requires({ features })` declares the features this one depends on; the
252
- * `.projection` extension declares read-side projection tables that this
253
- * feature's pipeline steps are allowed to write via
254
- * `r.step.unsafeProjectionUpsert` (hard-required for any unsafeProjection-*
255
- * step usage, see Q10).
256
- *
257
- * @example r.requires({ features: ["auth", "tenant"] });
258
- */
259
- export type RequiresApi = ((options: {
260
- readonly features: readonly string[];
261
- }) => void) & {
262
- readonly projection: (tableName: string) => void;
263
- readonly step: (stepKind: string) => void;
264
- };
265
- /**
266
- * `TFeature` is the literal feature-name from `defineFeature("foo", ...)` —
267
- * default-`string` keeps every existing usage zero-config. Strict-typed
268
- * features (apps that opt into the literal-name flavour) get propagated
269
- * through to `defineEvent` so the returned `EventDef.name` is a literal
270
- * `${CamelToKebab<TFeature>}:event:${CamelToKebab<TInner>}`. That literal
271
- * threads through `ctx.appendEvent({ type: eventDef.name, ... })`,
272
- * keeping strict-mode alive even when handlers route via `eventDef.name`
273
- * instead of hand-typed string literals.
274
- */
275
- export type FeatureRegistrar<TFeature extends string = string> = {
276
- systemScope(): void;
277
- describe(text: string): void;
278
- requires: RequiresApi;
279
- optionalRequires(...featureNames: string[]): void;
280
- optionalRequires(options: {
281
- readonly features: readonly string[];
282
- }): void;
283
- toggleable(options: {
284
- default: boolean;
285
- }): void;
286
- uiHints(hints: UiHints): void;
287
- /**
288
- * @example r.entity({ name: "note", fields: { title: createTextField({ required: true }) } });
289
- */
290
- entity(definition: EntityObjectDefinition): EntityRef;
291
- crud(entityName: string, entity: EntityDefinition, options?: RegisterEntityCrudOptions): void;
292
- /**
293
- * @example r.writeHandler({ name: "note:archive", schema, handler, access: { roles: ["User"] } });
294
- */
295
- writeHandler<TName extends string, TSchema extends ZodType, TPrepared = undefined>(def: WriteHandlerDefinition<TName, TSchema, unknown, KumikoEventTypeMap, TPrepared>): HandlerRef;
296
- /**
297
- * @example r.queryHandler({ name: "note:list", schema, handler, access: { roles: ["User"] } });
298
- */
299
- queryHandler<TName extends string, TSchema extends ZodType>(def: QueryHandlerDefinition<TName, TSchema>): HandlerRef;
300
- streamHandler<TName extends string, TSchema extends ZodType>(def: StreamHandlerDefinition<TName, TSchema>): HandlerRef;
301
- streamHandler<TSchema extends ZodType>(name: string, schema: TSchema, handler: StreamHandlerFn<z.infer<TSchema>>, options: {
302
- access: AccessRule;
303
- rateLimit?: RateLimitDeclaration;
304
- escapeHatch?: EscapeHatchDeclaration;
305
- }): HandlerRef;
306
- relation(entity: NameOrRef, relationName: string, definition: RelationDefinition): void;
307
- relation<TDef extends RelationDefinition>(definition: {
308
- readonly entity: NameOrRef;
309
- readonly name: string;
310
- } & TDef): void;
311
- /**
312
- * @example r.hook({ type: "postSave", target: { allOf: noteRef }, handler: notifyOwner });
313
- */
314
- hook(definition: HookDefinition): void;
315
- searchPayloadExtension(entity: NameOrRef, fn: SearchPayloadContributorFn): void;
316
- config<T extends ConfigKeyType>(keyName: string, def: ConfigKeyDefinition<T>): ConfigKeyHandle<T>;
317
- config<TKeys extends Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>>(definition: {
318
- readonly keys: TKeys;
319
- readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
320
- }): {
321
- readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]>;
322
- };
323
- job(name: string, options: Omit<JobDefinition, "name" | "handler">, handler: JobHandlerFn): void;
324
- job(definition: JobDefinition): void;
325
- notification(name: string, definition: {
326
- readonly trigger?: {
327
- readonly on: NameOrRef;
328
- };
329
- readonly recipient?: NotificationRecipientFn;
330
- readonly data?: NotificationDataFn;
331
- readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
332
- readonly variables?: TemplateVariables;
333
- readonly record?: NotificationRecordMapping;
334
- readonly template?: Readonly<Record<string, NotificationTemplateBody>>;
335
- readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
336
- readonly localizedTemplate?: () => NotificationLocalizedTemplate;
337
- readonly renamedFrom?: readonly string[];
338
- readonly systemOnly?: boolean;
339
- readonly unsubscribe?: boolean;
340
- readonly tenantTransport?: boolean;
341
- }): void;
342
- notification(definition: {
343
- readonly name: string;
344
- readonly trigger?: {
345
- readonly on: NameOrRef;
346
- };
347
- readonly recipient?: NotificationRecipientFn;
348
- readonly data?: NotificationDataFn;
349
- readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
350
- readonly variables?: TemplateVariables;
351
- readonly record?: NotificationRecordMapping;
352
- readonly template?: Readonly<Record<string, NotificationTemplateBody>>;
353
- readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
354
- readonly localizedTemplate?: () => NotificationLocalizedTemplate;
355
- readonly renamedFrom?: readonly string[];
356
- readonly systemOnly?: boolean;
357
- readonly unsubscribe?: boolean;
358
- readonly tenantTransport?: boolean;
359
- }): void;
360
- translations(def: TranslationsDef): void;
361
- defineEvent<const TInner extends string, TPayload>(name: TInner, schema: ZodType<TPayload>, options: {
362
- readonly version?: number;
363
- readonly piiFields: EventPiiStance;
364
- readonly migrations?: readonly {
365
- readonly fromVersion: number;
366
- readonly toVersion: number;
367
- readonly transform: EventUpcastFn | DeclarativeEventMigration;
368
- }[];
369
- }): EventDef<TPayload, QualifiedEventName<TFeature, TInner>>;
370
- readsConfig(...qualifiedKeys: string[]): void;
371
- readsConfig(options: {
372
- readonly keys: readonly string[];
373
- }): void;
374
- referenceData(entity: NameOrRef, data: readonly Record<string, unknown>[], options?: {
375
- upsertKey?: string;
376
- }): void;
377
- referenceData(definition: {
378
- readonly entity: NameOrRef;
379
- readonly data: readonly Record<string, unknown>[];
380
- readonly upsertKey?: string;
381
- }): void;
382
- extendsRegistrar(name: string, def: RegistrarExtensionDef): void;
383
- useExtension<N extends string>(extensionName: N, entity: NameOrRef, ...options: ExtensionOptionsArgs<N>): void;
384
- useExtension<N extends string>(definition: {
385
- readonly name: N;
386
- readonly entity: NameOrRef;
387
- } & ExtensionOptionsFor<N>): void;
388
- /**
389
- * Declares which config key selects the active provider under an
390
- * extension point — called by the point-owning foundation (e.g.
391
- * `r.extensionSelector("mailTransport", configKeys.provider)`).
392
- * Readiness gating counts a provider-feature's `required` keys and
393
- * secrets only while that provider is the selected one. Registry-build
394
- * fails on duplicate declarations per extension and on selector keys
395
- * that no mounted feature declares. A masked tenant selector key makes the
396
- * Settings-Hub render one tenant dashboard for the owner: the selector as a
397
- * select of the mounted plugin ids, the selected plugin's config and secrets
398
- * as panels, and plugin features lose their own tenant nav. `config:write:set`
399
- * rejects unknown plugin ids (`""` clears). `options.panels` adds the owner's own
400
- * `custom`/`screen` panels to that dashboard, after the selection and before the
401
- * plugin panels (a short `screen` id means a screen of the declaring feature).
402
- */
403
- extensionSelector(extensionName: string, key: {
404
- readonly name: string;
405
- } | string, options?: {
406
- readonly panels?: readonly ExtensionSelectorPanel[];
407
- }): void;
408
- /**
409
- * Marker declaration: this feature provides a cross-feature API under the
410
- * given name. The implementation is registered separately as a query or
411
- * write handler under the QN pattern; `r.exposesApi` is only a boot-check
412
- * surface.
413
- *
414
- * The boot validator checks that every `r.usesApi(name)` finds a matching
415
- * exposer, that the exposer feature is listed in requires/optionalRequires,
416
- * and that no API name is exposed more than once.
417
- *
418
- * ```ts
419
- * defineFeature("compliance-profiles", (r) => {
420
- * r.exposesApi("compliance.forTenant");
421
- * r.queryHandler({
422
- * name: "compliance:query:for-tenant",
423
- * // ... actual implementation
424
- * });
425
- * });
426
- * ```
427
- */
428
- exposesApi(apiName: string): void;
429
- /**
430
- * Declares that this feature calls a cross-feature API. The boot validator
431
- * checks that another feature provides `r.exposesApi(apiName)` and that
432
- * this feature lists it in `r.requires/optionalRequires`.
433
- *
434
- * ```ts
435
- * defineFeature("user-data-rights", (r) => {
436
- * r.requires({ features: ["compliance-profiles"] });
437
- * r.usesApi("compliance.forTenant");
438
- * });
439
- * ```
440
- */
441
- usesApi(apiName: string): void;
442
- /**
443
- * Declares a boot-time mount-invariant for this feature. `fn` runs once
444
- * at boot with a ctx exposing every mounted feature — throw to fail the
445
- * boot with a clear message. Multiple calls per feature are allowed.
446
- *
447
- * Use for cross-feature invariants that `r.requires` can't express
448
- * because they're conditional (only fail when the feature has a
449
- * specific shape), e.g. "if this feature has PII entities, some
450
- * user-data-hook feature must be mounted" (the prompt-store trap —
451
- * a UserData feature was written but never mounted, and nothing caught
452
- * it at boot):
453
- *
454
- * ```ts
455
- * defineFeature("prompt-store", (r) => {
456
- * const promptFields = { text: createTextField({ personal: "self", find: "none" }) };
457
- * r.entity({ name: "prompt", fields: promptFields });
458
- * r.bootCheck(({ features }) => {
459
- * // Conditional on this feature's own shape (has a pii field) —
460
- * // r.requires({ features: ["user-data-hook"] }) can't express that, it would fail
461
- * // even for a prompt-store variant with no PII fields at all.
462
- * const hasPiiField = Object.values(promptFields).some((f) => f.pii);
463
- * const hasUserDataHook = features.some((f) => f.name === "user-data-hook");
464
- * if (hasPiiField && !hasUserDataHook) {
465
- * throw new Error("prompt-store has PII fields but no user-data-hook feature is mounted");
466
- * }
467
- * });
468
- * });
469
- * ```
470
- */
471
- bootCheck(fn: BootCheckFn): void;
472
- metric(shortName: string, options: MetricOptions): void;
473
- metric(definition: {
474
- readonly name: string;
475
- } & MetricOptions): void;
476
- secret(shortName: string, options: SecretOptions): SecretKeyHandle;
477
- secret(definition: {
478
- readonly name: string;
479
- } & SecretOptions): SecretKeyHandle;
480
- secretNamespace(shortName: string, options: SecretNamespaceOptions): SecretNamespaceHandle;
481
- secretNamespace(definition: {
482
- readonly name: string;
483
- } & SecretNamespaceOptions): SecretNamespaceHandle;
484
- projection(definition: ProjectionDefinition): void;
485
- multiStreamProjection(definition: MultiStreamProjectionDefinition): void;
486
- extendEntityProjection(entityName: string, extension: EntityProjectionExtension): void;
487
- authClaims(fn: AuthClaimsFn): void;
488
- claimKey<T extends ClaimKeyType>(shortName: string, options: {
489
- readonly type: T;
490
- }): ClaimKeyHandle<T>;
491
- claimKey<T extends ClaimKeyType>(definition: {
492
- readonly name: string;
493
- readonly type: T;
494
- }): ClaimKeyHandle<T>;
495
- screen(definition: ScreenDefinition): void;
496
- nav(definition: NavDefinition): void;
497
- contentCollection(definition: ContentCollectionDefinition): string;
498
- workspace(definition: WorkspaceDefinition): void;
499
- httpRoute(definition: HttpRouteDefinition): void;
500
- webSocketRoute(definition: WebSocketRouteDefinition): void;
501
- storeTable(meta: EntityTableMeta, options: StoreTableOptions): void;
502
- treeActions<const TActions extends Record<string, TreeActionDef>>(actions: TActions): TreeActionsHandle<TFeature, TActions>;
503
- envSchema(schema: z.ZodObject<z.ZodRawShape>): void;
504
- };
505
198
  export type Registry = {
506
199
  readonly features: ReadonlyMap<string, FeatureDefinition>;
507
200
  getFeature(name: string): FeatureDefinition | undefined;
@@ -595,4 +288,3 @@ export type Registry = {
595
288
  getDefaultWorkspace(): WorkspaceDefinition | undefined;
596
289
  getTreeActions(featureName: string): Readonly<Record<string, TreeActionDef>> | undefined;
597
290
  };
598
- export {};
package/dist/fields.js CHANGED
@@ -47,7 +47,7 @@ function isFieldTypeName(name) {
47
47
  export const FIELD_TYPE_NAMES = Object.keys(FIELD_TYPE_NAME_FLAGS).filter(isFieldTypeName);
48
48
  // Field types with no bound, editable widget on the auto-wired entityEdit
49
49
  // path (render-field.tsx renders a read-only banner instead) — shared by
50
- // the boot validator (packages/framework/src/engine/boot-validator/screens.ts)
50
+ // the boot validator (packages/framework/src/engine/boot-validator/screens-forms.ts)
51
51
  // and the client-side required-field check (packages/renderer/src/app/form-schema.ts).
52
52
  // Embedded LIST fields (`multiple: true`) are the one exception — they get
53
53
  // their own EmbeddedListField grid widget — callers must check that
@@ -256,6 +256,7 @@ type SharedContextFields = {
256
256
  readonly signal?: AbortSignal;
257
257
  readonly effectiveFeatures?: (tenantId: TenantId) => ReadonlySet<string>;
258
258
  readonly scheduleAfterCommit?: (hook: () => Promise<void>) => void;
259
+ readonly scheduleAfterRollback?: (hook: () => Promise<void>) => void;
259
260
  readonly queryAsMember?: MemberReader;
260
261
  };
261
262
  export type AppContext = SharedContextFields & {
@@ -350,6 +351,10 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
350
351
  readonly fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
351
352
  readonly loadAggregate: (aggregateId: string, options?: {
352
353
  readonly asOf?: Temporal.Instant;
354
+ readonly atVersion?: never;
355
+ } | {
356
+ readonly atVersion?: number;
357
+ readonly asOf?: never;
353
358
  }) => Promise<readonly import("./event-store-types.ts").StoredEvent[]>;
354
359
  readonly archiveStream: (aggregateId: string, args: {
355
360
  readonly aggregateType: string;
@@ -405,12 +410,6 @@ export type JobContext = SharedContextFields & {
405
410
  readonly triggerEvent?: JobTriggerEvent;
406
411
  readonly _tenantId?: TenantId;
407
412
  };
408
- /**
409
- * What a provider plugin (`build`, `capabilitiesFor`, ...) may read. Both write/query handlers
410
- * and jobs can produce one (`providerPluginContextFromHandler`, `providerPluginContextFromJob`),
411
- * so a plugin never depends on a request-bound field.
412
- */
413
- export type ProviderPluginContext = Pick<HandlerContext, "db" | "dbOutsideTransaction" | "systemDb" | "registry" | "user" | "log" | "triggeredBy" | "config" | "secrets">;
414
413
  export type WriteHandlerFn<TPayload = unknown, TData = unknown> = (event: WriteEvent<TPayload>, context: HandlerContext) => Promise<WriteResult<TData>>;
415
414
  export type QueryHandlerFn<TPayload = unknown, TResult = unknown> = (query: QueryEvent<TPayload>, context: HandlerContext) => Promise<TResult>;
416
415
  export type StreamHandlerFn<TPayload = unknown, TChunk = unknown> = (query: QueryEvent<TPayload>, context: HandlerContext) => AsyncGenerator<TChunk>;
@@ -632,6 +631,9 @@ export type WriteHandlerDef = {
632
631
  /** Per-payload-field limits, e.g. per recipient address. Complements
633
632
  * `rateLimit`; anonymous handlers still need an ip-keyed `rateLimit`. */
634
633
  readonly additionalRateLimits?: readonly PayloadRateLimitOption[];
634
+ /** Tokens each request books on every declared rateLimit bucket, computed from the validated
635
+ * payload (e.g. `(p) => p.files.length`). Replaces the options' `cost`; results below 1 count as 1. */
636
+ readonly rateLimitCost?: (payload: unknown) => number;
635
637
  readonly escapeHatch?: EscapeHatchDeclaration;
636
638
  /** Reserves before the handler transaction opens. Reached through a nested ctx.write the
637
639
  * reservation cannot run, so the dispatcher rejects it unless the top-level batch reserved it. */
@@ -0,0 +1,43 @@
1
+ import * as z from "zod";
2
+ import type { AggregateKey } from "./aggregate-types.ts";
3
+ export declare const MAX_LIST_AGGREGATE_MEASURES = 20;
4
+ export declare const LIST_AGGREGATE_MEASURE_KINDS: readonly ["sum", "avg", "count", "min", "max"];
5
+ export type ListAggregateMeasureKind = (typeof LIST_AGGREGATE_MEASURE_KINDS)[number];
6
+ export declare const listAggregatePayloadSchema: z.ZodObject<{
7
+ groupBy: z.ZodOptional<z.ZodString>;
8
+ measures: z.ZodDefault<z.ZodArray<z.ZodObject<{
9
+ field: z.ZodString;
10
+ kind: z.ZodEnum<{
11
+ avg: "avg";
12
+ count: "count";
13
+ max: "max";
14
+ min: "min";
15
+ sum: "sum";
16
+ }>;
17
+ }, z.core.$strip>>>;
18
+ }, z.core.$strip>;
19
+ export type ListAggregatePayload = {
20
+ readonly groupBy?: string | undefined;
21
+ readonly measures: readonly {
22
+ readonly field: string;
23
+ readonly kind: ListAggregateMeasureKind;
24
+ }[];
25
+ };
26
+ export type ListAggregateCurrencyAmount = {
27
+ readonly currency: string;
28
+ readonly value: number;
29
+ };
30
+ export type ListAggregateValue = number | string | null | readonly ListAggregateCurrencyAmount[];
31
+ export declare function isListAggregateCurrencyAmounts(value: ListAggregateValue | undefined): value is readonly ListAggregateCurrencyAmount[];
32
+ export type ListAggregateGroup = {
33
+ readonly key: AggregateKey;
34
+ readonly label?: string | null;
35
+ readonly count: number;
36
+ readonly measures: readonly ListAggregateValue[];
37
+ };
38
+ export type ListAggregateResult = {
39
+ readonly groups: readonly ListAggregateGroup[];
40
+ readonly truncated?: true;
41
+ readonly searchTruncated?: true;
42
+ };
43
+ export declare function isListAggregateResult(value: unknown): value is ListAggregateResult;
@@ -0,0 +1,22 @@
1
+ import * as z from "zod";
2
+ import { isPlainObject } from "./type-guards.js";
3
+ export const MAX_LIST_AGGREGATE_MEASURES = 20;
4
+ export const LIST_AGGREGATE_MEASURE_KINDS = ["sum", "avg", "count", "min", "max"];
5
+ export const listAggregatePayloadSchema = z.object({
6
+ groupBy: z.string().optional(),
7
+ measures: z
8
+ .array(z.object({ field: z.string(), kind: z.enum(LIST_AGGREGATE_MEASURE_KINDS) }))
9
+ .max(MAX_LIST_AGGREGATE_MEASURES)
10
+ .default([]),
11
+ });
12
+ export function isListAggregateCurrencyAmounts(value) {
13
+ return Array.isArray(value);
14
+ }
15
+ const LIST_AGGREGATE_RESULT_KEYS = ["groups", "truncated", "searchTruncated"];
16
+ // Closed key set: a row that merely has a `groups` field (jsonb column) must not
17
+ // be mistaken for an aggregate and skip the row-level read filter.
18
+ export function isListAggregateResult(value) {
19
+ return (isPlainObject(value) &&
20
+ Array.isArray(value["groups"]) &&
21
+ Object.keys(value).every((key) => LIST_AGGREGATE_RESULT_KEYS.includes(key)));
22
+ }
@@ -6,6 +6,8 @@ import type { FieldRenderer, PlatformComponent, ScreenNavSugar, ScreenSlots } fr
6
6
  import type { ActionFormRedirect } from "./screen-action-form.ts";
7
7
  import type { DashboardPanelVisibility } from "./screen-dashboard.ts";
8
8
  import type { ListColumnSpec, ListSortSpec, RelatedListToolbarAction, RowAction, RowActionMode, RowFieldExtractor } from "./screen-entity-list.ts";
9
+ import type { ListBulkAction } from "./screen-list-bulk.ts";
10
+ import type { ListTableGrouping, ListTableOptions } from "./screen-list-table.ts";
9
11
  import type { ListFacetSpec } from "./screen-projection.ts";
10
12
  export type EditFieldSpec = string | {
11
13
  readonly field: string;
@@ -168,25 +170,9 @@ export type EditExtensionSection = {
168
170
  * (declared or id-derived); the boot-validator rejects one that doesn't. */
169
171
  readonly actions?: readonly RowAction[];
170
172
  };
171
- /** Rows are grouped by `field` in order of first appearance. Every group gets
172
- * a collapsible header. */
173
- export type RelatedListGroupBy = {
174
- readonly field: string;
175
- /** Groups whose field value equals this start collapsed; the others start open. */
176
- readonly collapsedWhen?: string | number | boolean | null;
177
- /** i18n key of the group header. Placeholders: `{count}` rows in the group,
178
- * `{value}` the group's field value, `{lastDate}` the group's latest
179
- * `dateField` value as a locale date. A group with neither `label` nor an
180
- * entry in `labels` renders its rows without a header and never collapses. */
181
- readonly label?: string;
182
- /** Header i18n key per group value (as string), overriding `label`. */
183
- readonly labels?: Readonly<Record<string, string>>;
184
- /** Row field (ISO date or timestamp) behind `{lastDate}`. */
185
- readonly dateField?: string;
186
- };
187
- /** Group key of a field value; `collapsedWhen` and row values both go through it. */
173
+ /** Group key of a field value; `collapsedWhen` and server group keys both go through it. */
188
174
  export declare function relatedListGroupKey(value: unknown): string;
189
- export declare function relatedListGroupHeaderLabel(groupBy: RelatedListGroupBy, groupKey: string): string | undefined;
175
+ export declare function relatedListGroupHeaderLabel(grouping: Pick<ListTableGrouping, "label" | "labels">, groupKey: string): string | undefined;
190
176
  /** Row tint rule: rows where the `FieldCondition` (`{ field, eq }`, `{ field,
191
177
  * in }` …) holds get the tone. `bad` renders red. */
192
178
  export type RelatedListRowTone = Exclude<FieldCondition, boolean> & {
@@ -240,6 +226,8 @@ export type EditRelatedListSection = {
240
226
  * "3 Positionen"). Omitted: generic "entries" wording. */
241
227
  readonly itemNoun?: string;
242
228
  readonly pageSize?: number;
229
+ /** Opt-in table behaviour: remembered state, reset, column picker, density. */
230
+ readonly table?: ListTableOptions;
243
231
  /** Initial sort on mount, applied client-side over the already-loaded rows
244
232
  * (this section has no pager; if the fetch is truncated, `nextCursor !== null`,
245
233
  * the section shows a hint banner that the sort covers only the loaded rows).
@@ -278,8 +266,8 @@ export type EditRelatedListSection = {
278
266
  readonly rowActions?: readonly RowAction[];
279
267
  /** Layout of the row-action column — see `RowActionMode`. */
280
268
  readonly rowActionMode?: RowActionMode;
281
- /** Groups rows by the value of a field under collapsible group headers. */
282
- readonly groupBy?: RelatedListGroupBy;
269
+ /** Bulk actions over checkbox-selected rows, each pointing at a writeHandler row action. Adds a checkbox column and a selection bar; without it the list has no checkboxes. */
270
+ readonly bulkActions?: readonly ListBulkAction[];
283
271
  /** Tints rows whose field matches — a declarative rule, not a function, so
284
272
  * the screen definition stays serializable. */
285
273
  readonly rowTone?: RelatedListRowTone;
@@ -1,8 +1,10 @@
1
- /** Group key of a field value; `collapsedWhen` and row values both go through it. */
1
+ /** Group key of a field value; `collapsedWhen` and server group keys both go through it. */
2
2
  export function relatedListGroupKey(value) {
3
3
  return value === undefined || value === null ? "" : String(value);
4
4
  }
5
- export function relatedListGroupHeaderLabel(groupBy, groupKey) {
6
- const labels = groupBy.labels;
7
- return labels !== undefined && Object.hasOwn(labels, groupKey) ? labels[groupKey] : groupBy.label;
5
+ export function relatedListGroupHeaderLabel(grouping, groupKey) {
6
+ const labels = grouping.labels;
7
+ return labels !== undefined && Object.hasOwn(labels, groupKey)
8
+ ? labels[groupKey]
9
+ : grouping.label;
8
10
  }
@@ -4,7 +4,10 @@ import type { IconKey } from "./nav-icon.ts";
4
4
  import type { FieldRenderer, PlatformComponent, ScreenNavSugar, ScreenSlots } from "./screen.ts";
5
5
  import type { ActionFormRedirect } from "./screen-action-form.ts";
6
6
  import type { DashboardPanelVisibility } from "./screen-dashboard.ts";
7
- import type { EditRelatedListSection, ListEmptyStateSpec } from "./screen-entity-edit.ts";
7
+ import type { ListEmptyStateSpec } from "./screen-entity-edit.ts";
8
+ import type { EntityListExpandableRow } from "./screen-expandable-row.ts";
9
+ import type { ListBulkAction } from "./screen-list-bulk.ts";
10
+ import type { ListColumnPin, ListTableOptions } from "./screen-list-table.ts";
8
11
  export type ListColumnSpec = string | {
9
12
  readonly field: string;
10
13
  readonly renderer?: FieldRenderer;
@@ -49,6 +52,16 @@ export type ListColumnSpec = string | {
49
52
  * `md` breakpoint (tables keep it). For columns that only make sense
50
53
  * next to the others, like a sort key. */
51
54
  readonly hideOnNarrow?: boolean;
55
+ /** Start state for the column picker: the column begins hidden and the
56
+ * user can show it. Only valid together with `table.columnPicker`. */
57
+ readonly hidden?: boolean;
58
+ /** Start pin for the column: it sticks to this edge while the table scrolls
59
+ * horizontally. Only valid together with `table.pinning`. */
60
+ readonly pinned?: ListColumnPin;
61
+ /** Start width in px. Only valid together with `table.resizing`. */
62
+ readonly width?: number;
63
+ /** Smallest width in px the user can resize to. Only valid together with `table.resizing`. */
64
+ readonly minWidth?: number;
52
65
  /** Dashboard list panels only. "bar": field holds a 0..1 fraction, drawn
53
66
  * as bar + percent. "badge": field holds a label, drawn as a pill whose
54
67
  * tone comes from `badgeToneField`. "datetime": field holds an ISO string or
@@ -321,11 +334,6 @@ export type RelatedListToolbarAction = (Extract<ToolbarAction, {
321
334
  }> & {
322
335
  readonly visible?: FieldCondition;
323
336
  });
324
- /** Expandable area of an `entityList` row: a relatedList of the row's record.
325
- * `id` (tab id) and `countField` (tab badge) have no meaning outside a tabs
326
- * layout and are therefore omitted. `kind` stays the discriminator so further
327
- * expansion kinds can be added without a breaking change. */
328
- export type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField">;
329
337
  /** An extra choice on a facet next to the field's own options: a named
330
338
  * bundle of values ("Running" = several statuses) or "no filter" (`values:
331
339
  * []`, e.g. "All"). Chips only. */
@@ -340,8 +348,9 @@ export type EntityListFacetExtraOption = {
340
348
  };
341
349
  export type EntityListFacetConfig = {
342
350
  /** `"select"` (default): dropdown with checkboxes. `"chips"`: one toggle
343
- * chip per option, single choice. */
344
- readonly display?: "select" | "chips";
351
+ * chip per option, single choice. `"range"`: "from" and "to" inputs on a
352
+ * number, decimal, money or bigInt field, sent as `gte`/`lte`. */
353
+ readonly display?: "select" | "chips" | "range";
345
354
  /** Chips show how many rows each option matches (other facets and the
346
355
  * search apply). Counts come from the list query itself, one `limit: 1`
347
356
  * count request per chip. */
@@ -387,6 +396,8 @@ export type EntityListScreenDefinition = {
387
396
  readonly rowActions?: readonly RowAction[];
388
397
  /** Layout of the row-action column — see `RowActionMode`. */
389
398
  readonly rowActionMode?: RowActionMode;
399
+ /** Bulk actions over checkbox-selected rows, each pointing at a writeHandler row action. Adds a checkbox column and a selection bar; without it the list has no checkboxes. */
400
+ readonly bulkActions?: readonly ListBulkAction[];
390
401
  /** Toolbar actions (list header). "Open Incident", "Schedule Maintenance"
391
402
  * etc. — next to "+ New" if present. Array order = UI order,
392
403
  * primary-style links. */
@@ -403,6 +414,8 @@ export type EntityListScreenDefinition = {
403
414
  readonly filter?: ScreenFilter;
404
415
  readonly pagination?: ListPaginationMode;
405
416
  readonly pageSize?: number;
417
+ /** Opt-in table behaviour: remembered state, reset, column picker, density. */
418
+ readonly table?: ListTableOptions;
406
419
  readonly defaultSort?: ListSortSpec;
407
420
  /** Initial facet selection, per filterable field: option values (string
408
421
  * facets) or a boolean. Applies only while the URL carries no value for
@@ -0,0 +1,16 @@
1
+ import type { EditRelatedListSection } from "./screen-entity-edit.ts";
2
+ import type { ListTableGrouping } from "./screen-list-table.ts";
3
+ /** Server-side grouping of an expandable row's sub-list: a fixed `field`, no "Group by" choice. */
4
+ export type ExpandableRowGrouping = Omit<ListTableGrouping, "field" | "userChoice" | "fields"> & {
5
+ readonly field: string;
6
+ };
7
+ /** Expandable area of an `entityList` row: a relatedList of the row's record.
8
+ * `id` (tab id) and `countField` (tab badge) have no meaning outside a tabs
9
+ * layout and are therefore omitted. `kind` stays the discriminator so further
10
+ * expansion kinds can be added without a breaking change. `table` takes only
11
+ * `grouping`; the other table options belong on the entityList itself. */
12
+ export type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField" | "table" | "bulkActions"> & {
13
+ readonly table?: {
14
+ readonly grouping?: ExpandableRowGrouping;
15
+ };
16
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export declare const LIST_BULK_ACTION_KEYS: readonly ["id", "rowAction", "confirm"];
2
+ /** Most rows one selection can hold; the loaded page caps it further. */
3
+ export declare const MAX_BULK_SELECTION = 100;
4
+ /** An action that runs a writeHandler row action once per checkbox-selected row. One `/api/write` per row, so access, validation and audit stay per row. */
5
+ export type ListBulkAction = {
6
+ /** Stable id, kebab-case, unique among the screen's bulk actions. */
7
+ readonly id: string;
8
+ /** Id of a writeHandler entry in the same screen's `rowActions`; it supplies handler, payload, label, style and `visible`. Rows the action's `visible` hides are skipped. */
9
+ readonly rowAction: string;
10
+ /** i18n key of the one confirm dialog for the whole selection; gets `{count}` and `{action}`. Without it a `confirm` or `style: "danger"` on the row action still asks once with the default text. */
11
+ readonly confirm?: string;
12
+ };