@cosmicdrift/kumiko-types 0.388.0 → 0.390.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.
@@ -11,11 +11,15 @@ import type { KumikoEventTypeMap } from "./event-type-map.ts";
11
11
  import type { FileContext } from "./file-handle-types.ts";
12
12
  import type { FileProviderResolver } from "./file-provider-resolver-types.ts";
13
13
  import type { GeoTzProvider } from "./geo-tz.ts";
14
+ import type { BeforeTransactionContext, HandlerAuth, HandlerDb, HandlerDispatch, HandlerEvents, HandlerObservability, JobDispatch, JobEvents, JobObservability, ReservationContext } from "./handler-context-facets.ts";
15
+ import type { ContextServices, HandlerLifecycle, HandlerTenants, JobLifecycle } from "./handler-context-services.ts";
14
16
  import type { Logger } from "./logger.ts";
15
- import type { Meter, MetricsHandle, Tracer } from "./observability/index.ts";
17
+ import type { Meter, Tracer } from "./observability/index.ts";
16
18
  import type { SearchAdapter } from "./search-adapter.ts";
17
19
  import type { CrossTenantReads, TenantDb } from "./tenant-db-types.ts";
18
20
  import type { Clock, TzContext } from "./tz-context.ts";
21
+ export type { BeforeTransactionContext, HandlerAuth, HandlerDb, HandlerDispatch, HandlerEvents, HandlerObservability, JobDispatch, JobEvents, JobObservability, ReservationContext, } from "./handler-context-facets.ts";
22
+ export type { ContextServices, HandlerLifecycle, HandlerTenants, JobLifecycle, } from "./handler-context-services.ts";
19
23
  export type OpenToAllPersonalData = "tenant-members";
20
24
  export type OpenToAllDeclaration = {
21
25
  readonly reason: string;
@@ -214,27 +218,12 @@ type SharedContextFields = {
214
218
  readonly jobRunner?: JobRunnerRef;
215
219
  readonly configResolver?: ConfigResolver;
216
220
  readonly config?: ConfigAccessor;
217
- /**
218
- * Config accessor resolved for ANOTHER tenant, identity = that tenant's system
219
- * user (the caller's user-scope config never leaks in). Cross-tenant calls
220
- * throw AccessDeniedError unless the caller is a system identity or has the
221
- * SystemAdmin role; the caller's own tenant needs no privilege. Present when
222
- * the config feature wired its accessor factory.
223
- */
221
+ /** Becomes `ctx.tenants.config`. */
224
222
  readonly configFor?: (tenantId: TenantId) => ConfigAccessor;
225
- /**
226
- * Write target for ANOTHER tenant: a tenant-mode db bound to it plus the
227
- * `streamTenantId` to hand the executor, so the event actor stays the
228
- * operator. Cross-tenant calls throw AccessDeniedError unless the caller is
229
- * SystemAdmin (the tenantIdOverride gate); the caller's own tenant needs no
230
- * privilege and yields `streamTenantId: undefined`. Write handlers only;
231
- * absent for r.systemScope() handlers and without a db.
232
- */
223
+ /** Becomes `ctx.tenants.writeTarget`. */
233
224
  readonly forTenant?: (tenantId: TenantId) => TenantWriteTarget;
234
225
  /** Mints the per-request `ctx.config`; set by the boot (`addConfigAccessorFactory`) from the effective `configResolver`, overriding an app-supplied value. */
235
226
  readonly configAccessorFactory?: ConfigAccessorFactory;
236
- /** @deprecated Use `configAccessorFactory`; the underscore name is removed in 0.390.0. */
237
- readonly _configAccessorFactory?: ConfigAccessorFactory;
238
227
  readonly configEncryption?: import("./envelope-cipher-types.ts").EnvelopeCipher;
239
228
  readonly rateLimit?: import("./rate-limit-types.ts").RateLimitResolver;
240
229
  readonly _rateLimitPayloadDigest?: (value: string) => string;
@@ -255,10 +244,13 @@ type SharedContextFields = {
255
244
  readonly meter?: Meter;
256
245
  readonly signal?: AbortSignal;
257
246
  readonly effectiveFeatures?: (tenantId: TenantId) => ReadonlySet<string>;
247
+ /** Becomes `ctx.lifecycle.afterCommit`. */
258
248
  readonly scheduleAfterCommit?: (hook: () => Promise<void>) => void;
259
- readonly queryAsMember?: MemberReader;
249
+ /** Becomes `ctx.lifecycle.afterRollback`. */
250
+ readonly scheduleAfterRollback?: (hook: () => Promise<void>) => void;
260
251
  };
261
252
  export type AppContext = SharedContextFields & {
253
+ readonly services?: never;
262
254
  readonly db?: DbConnection | TenantDb;
263
255
  readonly systemDb?: UncheckedSystemDb;
264
256
  readonly registry?: Registry;
@@ -284,7 +276,7 @@ export type AppContext = SharedContextFields & {
284
276
  /** Tenant of the current pipeline call. The dispatcher mirrors
285
277
  * `user.tenantId` here when building HandlerContext so the lifecycle
286
278
  * pipeline and system hooks can access it without the user object.
287
- * `effectiveFeatures(ctx._tenantId)` uses it at hook-filter points. */
279
+ * `effectiveFeatures(ctx.user.tenantId)` uses it at hook-filter points. */
288
280
  readonly _tenantId?: TenantId;
289
281
  /** Boot-configured default locale (BCP-47). Last fallback in ctx.locale's
290
282
  * Request-Locale → Boot-Default chain (dispatch-shared.ts) — used when
@@ -292,18 +284,8 @@ export type AppContext = SharedContextFields & {
292
284
  * Absent falls back to "en". */
293
285
  readonly defaultLocale?: string;
294
286
  };
295
- export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedContextFields & {
296
- readonly db: TenantDb;
297
- /**
298
- * Same tenant scoping as `db`, but never bound to the handler's tx: writes through it survive
299
- * a rollback. `undefined` only in the denial phases (member-resolution, before-transaction)
300
- * and in platform-metering tenant mode; narrow with `hasOutsideTransactionDb(ctx)` or take it
301
- * via `requireOutsideTransactionDb(ctx)` (`@cosmicdrift/kumiko-framework/db`). For atomic
302
- * multi-statement writes outside the tx use `runInOwnTransaction(ctx.dbOutsideTransaction, fn)`; `fn`
303
- * must not touch rows the handler's own tx has locked. For
304
- * r.systemScope() handlers it is a fail-closed Proxy; use `systemDb.outsideTransaction`.
305
- */
306
- readonly dbOutsideTransaction: TenantDb | undefined;
287
+ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = {
288
+ readonly db: HandlerDb;
307
289
  readonly systemDb?: UncheckedSystemDb;
308
290
  readonly crossTenantReads?: CrossTenantReads;
309
291
  readonly registry: Registry;
@@ -313,80 +295,35 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
313
295
  * The value is identical to `event.user`; identity switches still use
314
296
  * `queryAs`/`writeAs`. */
315
297
  readonly user: SessionUser;
316
- readonly systemUser?: SessionUser;
317
- readonly log?: Logger;
318
- readonly triggeredBy?: {
319
- readonly id: string;
320
- readonly tenantId: TenantId;
321
- } | null;
322
298
  /** Keyed digest for handler-owned rate-limit keys, so Redis never holds a
323
299
  * plaintext or plainly hashed identifier. Scoped to the running handler:
324
300
  * the same input yields a different digest per handler and never matches a
325
301
  * dispatcher payload-bucket digest. Absent when the app wires no digest key. */
326
302
  readonly rateLimitDigest?: (value: string) => string;
327
303
  readonly includeDeleted?: boolean;
328
- readonly runPreSave?: (handlerName: string, changes: Record<string, unknown>, previous: Readonly<Record<string, unknown>>, isNew: boolean) => Promise<Record<string, unknown>>;
329
- readonly query: (qn: string, payload: unknown) => Promise<unknown>;
330
- readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
331
- readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
332
- readonly writeAs: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
304
+ readonly dispatch: HandlerDispatch;
333
305
  readonly hasFeature: (featureName: string) => Promise<boolean>;
334
- /**
335
- * Default append: a typed domain event on one aggregate stream in the current tx. The runtime
336
- * bumps the stream version and fires matching projections in the same transaction. A version
337
- * conflict throws and poisons the transaction.
338
- */
339
- readonly appendEvent: AppendEventFn<TMap>;
340
- /**
341
- * Untyped append (`payload: unknown`) with the same runtime as `appendEvent`. Only for
342
- * migrations and upcasters, where the event type is not knowable at compile time.
343
- */
344
- readonly unsafeAppendEvent: UnsafeAppendEventFn;
345
- /**
346
- * Optimistic append: runs in a savepoint and returns a version conflict as a result instead of
347
- * throwing, so the handler can continue after losing a race on the same aggregate stream.
348
- */
349
- readonly tryAppendEvent: TryAppendEventFn;
350
- readonly fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
351
- readonly loadAggregate: (aggregateId: string, options?: {
352
- readonly asOf?: Temporal.Instant;
353
- }) => Promise<readonly import("./event-store-types.ts").StoredEvent[]>;
354
- readonly archiveStream: (aggregateId: string, args: {
355
- readonly aggregateType: string;
356
- readonly reason?: string;
357
- }) => Promise<void>;
358
- readonly restoreStream: (aggregateId: string) => Promise<void>;
359
- readonly isStreamArchived: (aggregateId: string) => Promise<boolean>;
360
- readonly snapshotAggregate: (args: {
361
- readonly aggregateId: string;
362
- readonly aggregateType: string;
363
- readonly version: number;
364
- readonly state: Record<string, unknown>;
365
- readonly snapshotVersion?: number;
366
- }) => Promise<void>;
367
- readonly loadAggregateWithSnapshot: <TState extends Record<string, unknown>>(aggregateId: string, reducer: import("./snapshot-types.ts").SnapshotReducer<TState>, initial: TState, options?: Omit<import("./snapshot-types.ts").LoadAggregateWithSnapshotOptions, "upcastEvent">) => Promise<import("./snapshot-types.ts").LoadAggregateWithSnapshotResult<TState>>;
368
- readonly queryProjection: <T = Record<string, unknown>>(qualifiedName: string, options?: {
369
- readonly unsafeAllTenants?: boolean;
370
- }) => Promise<readonly T[]>;
371
- readonly metrics: MetricsHandle;
372
- readonly metricsFor: (featureName: string) => MetricsHandle;
373
- readonly tracer: Tracer;
306
+ readonly events: HandlerEvents<TMap>;
307
+ readonly obs: HandlerObservability;
374
308
  readonly tz: TzContext;
375
309
  readonly locale: string;
376
- readonly resolveAuthClaims: (user: SessionUser) => Promise<Record<string, unknown>>;
377
- readonly resolveActiveMembership: (userId: string, tenantId: TenantId) => Promise<ActiveMembershipResult>;
378
- readonly queryAsMember: MemberReader;
310
+ readonly auth: HandlerAuth;
311
+ readonly config?: ConfigAccessor;
312
+ readonly notify?: NotifyFn;
313
+ readonly services: ContextServices;
314
+ readonly lifecycle: HandlerLifecycle;
315
+ readonly tenants: HandlerTenants;
379
316
  };
380
317
  export type JobDeferral = {
381
318
  readonly kind: "job-deferral";
382
319
  readonly delayMs: number;
383
320
  };
384
- export type JobContext = SharedContextFields & {
321
+ export type JobContext = {
385
322
  readonly db: TenantDb;
323
+ readonly events: JobEvents;
386
324
  readonly registry: Registry;
387
325
  readonly systemUser: SessionUser;
388
- readonly log: Logger;
389
- readonly metricsFor: (featureName: string) => MetricsHandle;
326
+ readonly obs: JobObservability;
390
327
  readonly tz: TzContext;
391
328
  readonly triggeredBy: {
392
329
  readonly id: string;
@@ -395,22 +332,18 @@ export type JobContext = SharedContextFields & {
395
332
  readonly attempt: number;
396
333
  readonly finalAttempt: boolean;
397
334
  readonly defer: (delayMs: number) => JobDeferral;
335
+ readonly canDefer: boolean;
398
336
  readonly systemDb?: UncheckedSystemDb;
399
337
  readonly crossTenantReads?: CrossTenantReads;
400
- readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
401
- readonly writeAs: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
402
- readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
403
- readonly queryAsMember: MemberReader;
338
+ readonly dispatch: JobDispatch;
404
339
  readonly triggerName?: string;
405
340
  readonly triggerEvent?: JobTriggerEvent;
406
341
  readonly _tenantId?: TenantId;
342
+ readonly config?: ConfigAccessor;
343
+ readonly notify?: NotifyFn;
344
+ readonly services: ContextServices;
345
+ readonly lifecycle: JobLifecycle;
407
346
  };
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
347
  export type WriteHandlerFn<TPayload = unknown, TData = unknown> = (event: WriteEvent<TPayload>, context: HandlerContext) => Promise<WriteResult<TData>>;
415
348
  export type QueryHandlerFn<TPayload = unknown, TResult = unknown> = (query: QueryEvent<TPayload>, context: HandlerContext) => Promise<TResult>;
416
349
  export type StreamHandlerFn<TPayload = unknown, TChunk = unknown> = (query: QueryEvent<TPayload>, context: HandlerContext) => AsyncGenerator<TChunk>;
@@ -419,7 +352,7 @@ export type StreamHandlerFn<TPayload = unknown, TChunk = unknown> = (query: Quer
419
352
  * → kebab-case. Drives the literal-type of `EventDef.name`, so that
420
353
  * `r.defineEvent("foo", schema)` inside `defineFeature("driverOrders")`
421
354
  * carries `name: "driver-orders:event:foo"` as a literal — strict-mode
422
- * for `ctx.appendEvent({ type: eventDef.name, ... })` lights up.
355
+ * for `ctx.events.append({ type: eventDef.name, ... })` lights up.
423
356
  *
424
357
  * Algorithm mirrors the runtime regex pipeline:
425
358
  * 1. `.` → `-` (dot acts as word-boundary)
@@ -607,14 +540,14 @@ export type ReservationHandle = {
607
540
  * its own short write instead of holding a second connection while the transaction is open.
608
541
  * The returned release (or handle.release) runs when the transaction does not commit (failure
609
542
  * result, throw, rollback, failed commit). Throwing a KumikoError rejects the write. */
610
- export type PreTransactionReservation = (event: WriteEvent, ctx: HandlerContext) => Promise<ReservationRelease | ReservationHandle | undefined>;
543
+ export type PreTransactionReservation = (event: WriteEvent, ctx: ReservationContext) => Promise<ReservationRelease | ReservationHandle | undefined>;
611
544
  /** Outcome of a write handler's pre-transaction phase. `release` runs when the handler transaction does not commit (failure result, throw, rollback, failed commit with unknown outcome), so it must itself tolerate a commit that actually happened. */
612
545
  export type BeforeTransactionResult<TPrepared = unknown> = {
613
546
  readonly isSuccess: true;
614
547
  readonly data: TPrepared;
615
548
  readonly release?: ReservationRelease;
616
549
  } | WriteFailure;
617
- export type BeforeTransactionPhase<TPayload = unknown, TPrepared = unknown> = (event: WriteEvent<TPayload>, ctx: HandlerContext) => Promise<BeforeTransactionResult<TPrepared>>;
550
+ export type BeforeTransactionPhase<TPayload = unknown, TPrepared = unknown> = (event: WriteEvent<TPayload>, ctx: BeforeTransactionContext) => Promise<BeforeTransactionResult<TPrepared>>;
618
551
  /** `perCommand`: the handler runs without a transaction of its own. */
619
552
  export type WriteHandlerTransactionMode = "perCommand";
620
553
  export type WriteHandlerDef = {
@@ -636,17 +569,17 @@ export type WriteHandlerDef = {
636
569
  * payload (e.g. `(p) => p.files.length`). Replaces the options' `cost`; results below 1 count as 1. */
637
570
  readonly rateLimitCost?: (payload: unknown) => number;
638
571
  readonly escapeHatch?: EscapeHatchDeclaration;
639
- /** Reserves before the handler transaction opens. Reached through a nested ctx.write the
572
+ /** Reserves before the handler transaction opens. Reached through a nested ctx.dispatch.write the
640
573
  * reservation cannot run, so the dispatcher rejects it unless the top-level batch reserved it. */
641
574
  readonly reserveBeforeTransaction?: PreTransactionReservation;
642
575
  /** Runs after all pre-handler gates (and after reserveBeforeTransaction) and before the
643
576
  * transaction opens: no transaction and no connection held, for external I/O and reads only.
644
577
  * Its `data` reaches the handler as `event.prepared`; a failure result or throw rejects the
645
- * write before anything is written. A handler with this phase cannot run as a nested ctx.write.
646
- * `ctx.forTenant` is available but read-only here: its db denies writes for the own and a
578
+ * write before anything is written. A handler with this phase cannot run as a nested ctx.dispatch.write.
579
+ * `ctx.tenants.writeTarget` is available but read-only here: its db denies writes for the own and a
647
580
  * foreign tenant alike (a foreign tenant stays gated by the SystemAdmin check). */
648
581
  readonly beforeTransaction?: BeforeTransactionPhase;
649
- /** `"perCommand"`: the handler runs without a transaction of its own. Every ctx.write/writeAs it
582
+ /** `"perCommand"`: the handler runs without a transaction of its own. Every ctx.dispatch.write/writeAs it
650
583
  * issues runs as its own top-level command with its own transaction and afterCommit hooks;
651
584
  * ctx.db writes and event appends throw. It cannot run as a nested write or inside a
652
585
  * multi-command batch, and cannot be combined with beforeTransaction/reserveBeforeTransaction. */
@@ -693,4 +626,3 @@ export type StreamHandlerDef = {
693
626
  readonly rateLimit?: RateLimitDeclaration;
694
627
  readonly escapeHatch?: EscapeHatchDeclaration;
695
628
  };
696
- export {};
package/dist/hooks.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { StoredEvent } from "./event-store-types.ts";
2
- import type { AppContext } from "./handlers.ts";
2
+ import type { HandlerContext } from "./handlers.ts";
3
3
  import type { HookPhase } from "./hook-phase.ts";
4
4
  import type { EntityId } from "./identifiers.ts";
5
5
  export type ValidationError = {
@@ -26,23 +26,22 @@ export type DeleteContext = {
26
26
  readonly lockGuardBypassed?: boolean | undefined;
27
27
  };
28
28
  export type LifecycleResult = SaveContext | DeleteContext;
29
- export type PreSaveHookFn = (changes: Record<string, unknown>, context: AppContext & {
29
+ export type PreSaveHookFn = (changes: Record<string, unknown>, context: HandlerContext & {
30
30
  readonly previous: Readonly<Record<string, unknown>>;
31
31
  readonly isNew: boolean;
32
32
  }) => Promise<Record<string, unknown>>;
33
- export type PostSaveHookFn = (result: SaveContext, context: AppContext) => Promise<void>;
34
- export type PostSaveBatchHookFn = (results: readonly SaveContext[], context: AppContext) => Promise<void>;
35
- export type PreDeleteHookFn = (payload: DeleteContext, context: AppContext) => Promise<void>;
36
- export type PostDeleteHookFn = (payload: DeleteContext, context: AppContext) => Promise<void>;
37
- export type PostDeleteBatchHookFn = (payloads: readonly DeleteContext[], context: AppContext) => Promise<void>;
38
- export type PreQueryHookFn = (payload: Record<string, unknown>, context: AppContext) => Promise<Record<string, unknown>>;
33
+ export type PostSaveHookFn = (result: SaveContext, context: HandlerContext) => Promise<void>;
34
+ export type PostSaveBatchHookFn = (results: readonly SaveContext[], context: HandlerContext) => Promise<void>;
35
+ export type PreDeleteHookFn = (payload: DeleteContext, context: HandlerContext) => Promise<void>;
36
+ export type PostDeleteHookFn = (payload: DeleteContext, context: HandlerContext) => Promise<void>;
37
+ export type PostDeleteBatchHookFn = (payloads: readonly DeleteContext[], context: HandlerContext) => Promise<void>;
39
38
  export type PostQueryHookFn = (result: {
40
39
  readonly entityName: string | undefined;
41
40
  readonly rows: ReadonlyArray<Record<string, unknown>>;
42
- }, context: AppContext) => Promise<{
41
+ }, context: HandlerContext) => Promise<{
43
42
  rows: ReadonlyArray<Record<string, unknown>>;
44
43
  }>;
45
- export type LifecycleHookFn = PreSaveHookFn | PostSaveHookFn | PreDeleteHookFn | PostDeleteHookFn | PreQueryHookFn | PostQueryHookFn;
44
+ export type LifecycleHookFn = PreSaveHookFn | PostSaveHookFn | PreDeleteHookFn | PostDeleteHookFn | PostQueryHookFn;
46
45
  export type { HookPhase } from "./hook-phase.ts";
47
46
  export type HookOwner = {
48
47
  readonly featureName?: string;
@@ -60,7 +59,6 @@ export type HookMap = {
60
59
  readonly postSave?: Readonly<Record<string, readonly PhasedHook<PostSaveHookFn>[]>>;
61
60
  readonly preDelete?: Readonly<Record<string, readonly PhasedHook<PreDeleteHookFn>[]>>;
62
61
  readonly postDelete?: Readonly<Record<string, readonly PhasedHook<PostDeleteHookFn>[]>>;
63
- readonly preQuery?: Readonly<Record<string, readonly OwnedFn<PreQueryHookFn>[]>>;
64
62
  readonly postQuery?: Readonly<Record<string, readonly OwnedFn<PostQueryHookFn>[]>>;
65
63
  };
66
64
  export type EntityHookMap = {
@@ -23,7 +23,12 @@ export type ListAggregatePayload = {
23
23
  readonly kind: ListAggregateMeasureKind;
24
24
  }[];
25
25
  };
26
- export type ListAggregateValue = number | string | null;
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[];
27
32
  export type ListAggregateGroup = {
28
33
  readonly key: AggregateKey;
29
34
  readonly label?: string | null;
@@ -9,6 +9,9 @@ export const listAggregatePayloadSchema = z.object({
9
9
  .max(MAX_LIST_AGGREGATE_MEASURES)
10
10
  .default([]),
11
11
  });
12
+ export function isListAggregateCurrencyAmounts(value) {
13
+ return Array.isArray(value);
14
+ }
12
15
  const LIST_AGGREGATE_RESULT_KEYS = ["groups", "truncated", "searchTruncated"];
13
16
  // Closed key set: a row that merely has a `groups` field (jsonb column) must not
14
17
  // be mistaken for an aggregate and skip the row-level read filter.
@@ -1,3 +1,3 @@
1
- // Metric + Meter contract. Feature code interacts through ctx.metrics
1
+ // Metric + Meter contract. Feature code interacts through ctx.obs.metrics
2
2
  // (MetricsHandle); the framework wires Counter/Histogram/Gauge behind that.
3
3
  export {};
@@ -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
+ };
@@ -0,0 +1,3 @@
1
+ export const LIST_BULK_ACTION_KEYS = ["id", "rowAction", "confirm"];
2
+ /** Most rows one selection can hold; the loaded page caps it further. */
3
+ export const MAX_BULK_SELECTION = 100;
@@ -0,0 +1,70 @@
1
+ import type { ListAggregateMeasureKind } from "./list-aggregate-types.ts";
2
+ export declare const LIST_TABLE_DENSITIES: readonly ["comfortable", "compact"];
3
+ export type ListTableDensity = (typeof LIST_TABLE_DENSITIES)[number];
4
+ export declare const LIST_COLUMN_PINS: readonly ["left", "right"];
5
+ export type ListColumnPin = (typeof LIST_COLUMN_PINS)[number];
6
+ export declare const LIST_TABLE_OPTION_KEYS: readonly ["rememberState", "resetAll", "columnPicker", "density", "pinning", "resizing", "keyboardNavigation", "export", "grouping", "totals"];
7
+ export declare const LIST_TABLE_GROUPING_KEYS: readonly ["field", "label", "labels", "collapsedWhen", "userChoice", "fields"];
8
+ export declare const LIST_TABLE_TOTAL_KEYS: readonly ["field", "kind"];
9
+ export declare const LIST_TABLE_EXPORT_KEYS: readonly ["xlsx", "maxRows"];
10
+ export declare const DEFAULT_EXPORT_MAX_ROWS = 10000;
11
+ /** Hard cap: the server builds the whole workbook in memory. */
12
+ export declare const MAX_EXPORT_MAX_ROWS = 100000;
13
+ export declare const LIST_TABLE_DENSITY_KEYS: readonly ["default", "userChoice"];
14
+ /** Opt-in table behaviour of a list. Without the block a list renders and behaves exactly as before. */
15
+ export type ListTableOptions = {
16
+ /** Keeps search, sort, page size, facets and date range for the browser session and restores them when the screen is opened without list parameters in the URL. A link with parameters always wins. */
17
+ readonly rememberState?: boolean;
18
+ /** Replaces the facet reset with one button that resets search, sort, page size, facets and date range, and drops the remembered list parameters (column and density choices stay). */
19
+ readonly resetAll?: boolean;
20
+ /** Lets the user hide and reorder columns in the view menu. Columns marked `hidden` start hidden. */
21
+ readonly columnPicker?: boolean;
22
+ /** Lets the user pin columns left or right in the view menu; columns may start pinned via `pinned`. Pinned columns stay sticky while scrolling horizontally. No effect in the card layout below `md`. */
23
+ readonly pinning?: boolean;
24
+ /** Switches the table to table-layout fixed; the user drags or uses the arrow keys on the header edge to resize. `width` and `minWidth` on a column are start values; widths are remembered per session. No effect in the card layout. */
25
+ readonly resizing?: boolean;
26
+ /** Rows become one tab stop; ArrowUp/ArrowDown move, Home/End jump, Enter runs the row click. Space stays reserved for selection. */
27
+ readonly keyboardNavigation?: boolean;
28
+ readonly density?: {
29
+ /** Row density on first visit. Default "comfortable". */
30
+ readonly default?: ListTableDensity;
31
+ /** Shows the density choice in the view menu and remembers it per session. */
32
+ readonly userChoice?: boolean;
33
+ };
34
+ /** Shows an export button; the server reads the whole filtered list (not just the visible page). */
35
+ readonly export?: ListTableExportOptions;
36
+ /** Server-side grouping: group headers with count and the `totals` measures over all matching rows; each open group loads its own rows. */
37
+ readonly grouping?: ListTableGrouping;
38
+ /** Footer row under the rows, computed by the server over all rows matching the current filters and search. In grouped mode the same measures show in each group header. */
39
+ readonly totals?: readonly ListTableTotal[];
40
+ };
41
+ export type ListTableGrouping = {
42
+ /** Start grouping. Without it the list starts ungrouped and the user picks from `fields`. */
43
+ readonly field?: string;
44
+ /** Groups whose key equals this start collapsed and load no rows until opened. */
45
+ readonly collapsedWhen?: string | number | boolean | null;
46
+ /** i18n key of the group header. Placeholders: `{count}` rows in the group, `{value}` the group's value. Default: the value itself. */
47
+ readonly label?: string;
48
+ /** Header i18n key per group key (as string), overriding `label`. */
49
+ readonly labels?: Readonly<Record<string, string>>;
50
+ /** Shows "Group by" in the view menu (needs `fields` or `field`) and remembers the choice per session. */
51
+ readonly userChoice?: boolean;
52
+ /** Fields offered under "Group by"; `field` is always offered. */
53
+ readonly fields?: readonly string[];
54
+ };
55
+ export type ListTableTotal = {
56
+ readonly field: string;
57
+ /** sum and avg on number, decimal, money and bigInt; min and max also on date and timestamp. Money is shown per currency, never added across currencies. */
58
+ readonly kind: ListAggregateMeasureKind;
59
+ };
60
+ export type ListTableExportOptions = {
61
+ readonly xlsx: true;
62
+ /** Export is refused above this many rows. Default 10000, hard cap 100000. Encrypted, sensitive and virtual columns are never written. */
63
+ readonly maxRows?: number;
64
+ };
65
+ /** One `table` block for every entityList, projectionList and relatedList of the bundled features. A screen's own `table` keys win per key. `grouping` and `totals` are not accepted here. */
66
+ export type BundledListsOptions = {
67
+ readonly table: ListTableOptions;
68
+ /** Pins the first column left on every bundled list where no column is pinned. Needs `table.pinning: true`. */
69
+ readonly pinFirstColumn?: boolean;
70
+ };
@@ -0,0 +1,28 @@
1
+ export const LIST_TABLE_DENSITIES = ["comfortable", "compact"];
2
+ export const LIST_COLUMN_PINS = ["left", "right"];
3
+ export const LIST_TABLE_OPTION_KEYS = [
4
+ "rememberState",
5
+ "resetAll",
6
+ "columnPicker",
7
+ "density",
8
+ "pinning",
9
+ "resizing",
10
+ "keyboardNavigation",
11
+ "export",
12
+ "grouping",
13
+ "totals",
14
+ ];
15
+ export const LIST_TABLE_GROUPING_KEYS = [
16
+ "field",
17
+ "label",
18
+ "labels",
19
+ "collapsedWhen",
20
+ "userChoice",
21
+ "fields",
22
+ ];
23
+ export const LIST_TABLE_TOTAL_KEYS = ["field", "kind"];
24
+ export const LIST_TABLE_EXPORT_KEYS = ["xlsx", "maxRows"];
25
+ export const DEFAULT_EXPORT_MAX_ROWS = 10000;
26
+ /** Hard cap: the server builds the whole workbook in memory. */
27
+ export const MAX_EXPORT_MAX_ROWS = 100000;
28
+ export const LIST_TABLE_DENSITY_KEYS = ["default", "userChoice"];