@cosmicdrift/kumiko-types 0.159.1 → 0.161.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,115 @@
1
+ import type { ZodType, z } from "zod";
2
+ import type { KumikoEventTypeMap } from "./event-type-map";
3
+ import type {
4
+ AccessRule,
5
+ HandlerContext,
6
+ QueryEvent,
7
+ RateLimitOption,
8
+ WriteEvent,
9
+ WriteResult,
10
+ } from "./handlers";
11
+ import type { PipelineDef } from "./step";
12
+
13
+ // --- Write Handler Definition ---
14
+ //
15
+ // TMap propagates the strict event-type-map through the handler's
16
+ // HandlerContext. CRITICAL: TMap is declared as a generic parameter on the
17
+ // FUNCTION (defineWriteHandler), not just on the type. Generic-functions
18
+ // substitute TMap at the USE-site (the caller's compile context, where
19
+ // the augmentation is visible); generic-type-aliases substitute at the
20
+ // definition-site (framework's compile, where the augmentation isn't
21
+ // visible) and collapse `keyof TMap` to `never`. See the spike-findings
22
+ // memory for the empirical proof.
23
+ //
24
+ // Two authoring forms — `handler` (free-form) or `perform: stepsPipeline(...)`
25
+ // (step-pipeline). A `perform` is compiled to a handler-function at
26
+ // definition time; the dispatcher only ever sees `handler`.
27
+
28
+ export type WriteHandlerDefinition<
29
+ TName extends string = string,
30
+ TSchema extends ZodType = ZodType,
31
+ TData = unknown,
32
+ TMap extends object = KumikoEventTypeMap,
33
+ > = {
34
+ readonly name: TName;
35
+ readonly schema: TSchema;
36
+ readonly access?: AccessRule;
37
+ readonly unsafeSkipTransitionGuard?: boolean;
38
+ readonly rateLimit?: RateLimitOption;
39
+ readonly handler: (
40
+ event: WriteEvent<z.infer<TSchema>>,
41
+ context: HandlerContext<TMap>,
42
+ ) => Promise<WriteResult<TData>>;
43
+ // Preserved when the author wrote a `perform` block — the original
44
+ // PipelineDef. Designer/AI/AST tools read this when present; the
45
+ // dispatcher ignores it and just calls `handler`. Absent on free-form
46
+ // handlers.
47
+ readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
48
+ };
49
+
50
+ // Author-facing input — accepts either the free-form `handler` or the
51
+ // pipeline-form `perform`. defineWriteHandler narrows them to the
52
+ // canonical WriteHandlerDefinition shape.
53
+ export type WriteHandlerInput<
54
+ TName extends string = string,
55
+ TSchema extends ZodType = ZodType,
56
+ TData = unknown,
57
+ TMap extends object = KumikoEventTypeMap,
58
+ > = {
59
+ readonly name: TName;
60
+ readonly schema: TSchema;
61
+ readonly access?: AccessRule;
62
+ readonly unsafeSkipTransitionGuard?: boolean;
63
+ readonly rateLimit?: RateLimitOption;
64
+ } & (
65
+ | {
66
+ readonly handler: (
67
+ event: WriteEvent<z.infer<TSchema>>,
68
+ context: HandlerContext<TMap>,
69
+ ) => Promise<WriteResult<TData>>;
70
+ readonly perform?: never;
71
+ }
72
+ | {
73
+ readonly perform: PipelineDef<z.infer<TSchema>, TData>;
74
+ readonly handler?: never;
75
+ }
76
+ );
77
+
78
+ // --- Query Handler Definition ---
79
+
80
+ export type QueryHandlerDefinition<
81
+ TName extends string = string,
82
+ TSchema extends ZodType = ZodType,
83
+ TResult = unknown,
84
+ TMap extends object = KumikoEventTypeMap,
85
+ > = {
86
+ readonly name: TName;
87
+ readonly schema: TSchema;
88
+ readonly access?: AccessRule;
89
+ readonly rateLimit?: RateLimitOption;
90
+ readonly handler: (
91
+ query: QueryEvent<z.infer<TSchema>>,
92
+ context: HandlerContext<TMap>,
93
+ ) => Promise<TResult>;
94
+ };
95
+
96
+ // --- Stream Handler Definition ---
97
+ //
98
+ // Scaffolding only (#1376) — access/rateLimit read by the boot-validator
99
+ // (#1377), dispatch-time consumption (async-generator, SSE) lands in #1378.
100
+
101
+ export type StreamHandlerDefinition<
102
+ TName extends string = string,
103
+ TSchema extends ZodType = ZodType,
104
+ TChunk = unknown,
105
+ TMap extends object = KumikoEventTypeMap,
106
+ > = {
107
+ readonly name: TName;
108
+ readonly schema: TSchema;
109
+ readonly access?: AccessRule;
110
+ readonly rateLimit?: RateLimitOption;
111
+ readonly handler: (
112
+ query: QueryEvent<z.infer<TSchema>>,
113
+ context: HandlerContext<TMap>,
114
+ ) => AsyncGenerator<TChunk>;
115
+ };
@@ -0,0 +1,30 @@
1
+ import type { EntityDefinition } from "./fields";
2
+ import type { AccessRule, QueryHandlerDef, WriteHandlerDef } from "./handlers";
3
+
4
+ export type EntityHandlerOptions = { readonly access?: AccessRule };
5
+
6
+ export type EntityQueryHandlerOptions = EntityHandlerOptions & {
7
+ /** Reads across every tenant instead of the caller's own — for a
8
+ * SystemAdmin-only operator inspector over an otherwise tenant-scoped
9
+ * entity. Scope this to the ONE handler that needs it rather than making
10
+ * the whole feature r.systemScope(), which would drop tenant isolation
11
+ * from every other handler the feature registers too. */
12
+ readonly crossTenant?: boolean;
13
+ };
14
+
15
+ export type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
16
+
17
+ export type RegisterEntityCrudOptions = {
18
+ readonly write?: EntityHandlerOptions;
19
+ readonly read?: EntityQueryHandlerOptions;
20
+ readonly verbs?: Partial<Record<EntityCrudVerb, boolean>>;
21
+ /** Default true. Set false when the entity was already registered (e.g. before r.relation). */
22
+ readonly registerEntity?: boolean;
23
+ };
24
+
25
+ /** Minimal registrar surface — keeps entity-handlers free of define-feature imports. */
26
+ export type EntityCrudRegistrar = {
27
+ entity(name: string, definition: EntityDefinition): unknown;
28
+ writeHandler(def: WriteHandlerDef): unknown;
29
+ queryHandler(def: QueryHandlerDef): unknown;
30
+ };
@@ -0,0 +1,92 @@
1
+ // Plain-data types for EntityTableMeta — split from the runtime
2
+ // (buildEntityTableMeta, resolveTableName, defineUnmanagedTable) in
3
+ // entity-table-meta.ts. Prep step for the types-only package extraction
4
+ // (#1283) — this file must have ONLY `import type`, no value imports
5
+ // (crypto/DB deps).
6
+
7
+ import type { EntityRelations } from "./relations";
8
+
9
+ // PG type repertoire the read-model tables need. Deliberately narrow — no
10
+ // vendor-specific types (TSVECTOR, HSTORE, etc.). An app-author who needs
11
+ // those reaches into the reviewed SQL migration by hand, not the generator.
12
+ export type PgType =
13
+ | "uuid"
14
+ | "text"
15
+ | "boolean"
16
+ | "integer"
17
+ | "double precision"
18
+ | "bigint"
19
+ | "serial"
20
+ | "bigserial"
21
+ | "jsonb"
22
+ | "timestamptz"
23
+ | "timestamptz(3)"
24
+ // Exact decimal — precision/scale are encoded in the type string so the
25
+ // DDL renderer and read-coercion need no side-channel metadata.
26
+ | `numeric(${number},${number})`;
27
+
28
+ export type ColumnMeta = {
29
+ readonly name: string; // snake_case PG column name
30
+ readonly pgType: PgType;
31
+ readonly notNull: boolean;
32
+ // Raw SQL-default-expression (e.g. `now()`, `gen_random_uuid()`,
33
+ // `'[]'::jsonb`). undefined = no DEFAULT clause.
34
+ readonly defaultSql?: string;
35
+ readonly primaryKey?: boolean;
36
+ readonly identity?: boolean;
37
+ // bigint/bigserial only: JS round-trip mode. `number` = createBigIntField /
38
+ // drizzle mode:"number" (safe ≤2^53). `bigint` = money cents, raw unmanaged.
39
+ readonly bigintJsMode?: "number" | "bigint";
40
+ };
41
+
42
+ export type IndexMeta = {
43
+ readonly name: string;
44
+ readonly columns: readonly string[]; // snake_case PG column names
45
+ readonly unique?: boolean;
46
+ // Raw SQL-where-expression for partial indexes. Caller is responsible
47
+ // for safety — emitted verbatim.
48
+ readonly whereSql?: string;
49
+ // Set when the EntityDefinition has a partial index (def.where as a
50
+ // drizzle SQL AST) the generator can't reliably render. The renderer
51
+ // emits the statement COMMENTED OUT with a warning hint — the app-author
52
+ // has to add the WHERE manually in the generated SQL.
53
+ readonly needsManualWhere?: boolean;
54
+ };
55
+
56
+ export type CompositePrimaryKeyMeta = {
57
+ readonly name: string;
58
+ readonly columns: readonly string[];
59
+ };
60
+
61
+ export type EntityTableMeta = {
62
+ readonly tableName: string;
63
+ readonly columns: readonly ColumnMeta[];
64
+ readonly indexes: readonly IndexMeta[];
65
+ // For tables with composite PK (no single id column, e.g. snapshots
66
+ // keyed by aggregate_id+version). When set, no column should have
67
+ // primaryKey:true; the constraint is emitted at table-level.
68
+ readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
69
+ // Source hint for diagnostics/tests — not used functionally.
70
+ // "managed" = from EntityDefinition (with base-columns + audit trail).
71
+ // "unmanaged" = via defineUnmanagedTable — no standard audit, the app
72
+ // carries the responsibility. Migration-generator + tooling can use the
73
+ // discriminator to render warnings ("X tables are unmanaged").
74
+ readonly source: "managed" | "unmanaged";
75
+ // PII-subject-annotated field names (pii/userOwned/tenantOwned). Set by
76
+ // buildEntityTableMeta so the registry can reject r.storeTable stores
77
+ // whose direct writes would skip the executor's encryption (#820).
78
+ readonly piiSubjectFields?: readonly string[];
79
+ };
80
+
81
+ export type BuildEntityTableMetaOptions = {
82
+ readonly featureName?: string;
83
+ readonly relations?: EntityRelations;
84
+ readonly source?: "managed" | "unmanaged";
85
+ };
86
+
87
+ export type UnmanagedTableInput = {
88
+ readonly tableName: string;
89
+ readonly columns: readonly ColumnMeta[];
90
+ readonly indexes?: readonly IndexMeta[];
91
+ readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
92
+ };
@@ -0,0 +1,6 @@
1
+ import type { KeyScope } from "./secrets-types";
2
+
3
+ export type EnvelopeCipher = {
4
+ encrypt(plaintext: string, scope?: KeyScope): Promise<string>;
5
+ decrypt(stored: string, scope?: KeyScope): Promise<string>;
6
+ };
@@ -0,0 +1,35 @@
1
+ // Failure modes of the event-store's append() path. Surfaced as typed
2
+ // errors so the executor layer can map them to the framework's
3
+ // WriteResult error contract (version_conflict).
4
+
5
+ export class VersionConflictError extends Error {
6
+ public readonly aggregateId: string;
7
+ public readonly expectedVersion: number;
8
+ constructor(aggregateId: string, expectedVersion: number) {
9
+ super(
10
+ `Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`,
11
+ );
12
+ this.name = "VersionConflictError";
13
+ this.aggregateId = aggregateId;
14
+ this.expectedVersion = expectedVersion;
15
+ }
16
+ }
17
+
18
+ // Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
19
+ // are read-only — restoreStream() makes them writable again. The archive
20
+ // state is not carried on the events themselves; it lives on the sparse
21
+ // kumiko_archived_streams table. Handlers that need to branch on archive
22
+ // state should call ctx.isStreamArchived(id) first.
23
+ export class ArchivedStreamError extends Error {
24
+ public readonly tenantId: string;
25
+ public readonly aggregateId: string;
26
+ constructor(tenantId: string, aggregateId: string) {
27
+ super(
28
+ `Aggregate ${aggregateId} on tenant ${tenantId} is archived — appendEvent is blocked. ` +
29
+ `Call restoreStream() to re-open the stream before writing.`,
30
+ );
31
+ this.name = "ArchivedStreamError";
32
+ this.tenantId = tenantId;
33
+ this.aggregateId = aggregateId;
34
+ }
35
+ }
@@ -0,0 +1,93 @@
1
+ import type { CursorResult } from "./cursor-types";
2
+ import type { SessionUser, WriteResult } from "./handlers";
3
+ import type { DeleteContext, SaveContext } from "./hooks";
4
+ import type { EntityId } from "./identifiers";
5
+ import type { SearchAdapter } from "./search-adapter";
6
+ import type { TenantDb } from "./tenant-db-types";
7
+
8
+ export type EventStoreExecutor = {
9
+ create: (
10
+ payload: Record<string, unknown>,
11
+ user: SessionUser,
12
+ db: TenantDb,
13
+ ) => Promise<WriteResult<SaveContext>>;
14
+
15
+ update: (
16
+ payload: { id: EntityId; version?: number | undefined; changes: Record<string, unknown> },
17
+ user: SessionUser,
18
+ db: TenantDb,
19
+ options?: { skipOptimisticLock?: boolean; skipUnchanged?: boolean },
20
+ ) => Promise<WriteResult<SaveContext>>;
21
+
22
+ delete: (
23
+ payload: { id: EntityId },
24
+ user: SessionUser,
25
+ db: TenantDb,
26
+ ) => Promise<WriteResult<DeleteContext>>;
27
+
28
+ // Hard-purge (Art. 17 erasure). Like delete, but emits `<entity>.forgotten`
29
+ // which hard-deletes the row even for softDelete entities — and, being an
30
+ // auto-verb replayed by the implicit projection, the erasure survives a
31
+ // rebuild (created → forgotten → row gone). Reaches soft-deleted rows too.
32
+ forget: (
33
+ payload: { id: EntityId },
34
+ user: SessionUser,
35
+ db: TenantDb,
36
+ ) => Promise<WriteResult<DeleteContext>>;
37
+
38
+ restore: (
39
+ payload: { id: EntityId },
40
+ user: SessionUser,
41
+ db: TenantDb,
42
+ ) => Promise<WriteResult<SaveContext>>;
43
+
44
+ list: (
45
+ payload: {
46
+ cursor?: string | undefined;
47
+ limit?: number | undefined;
48
+ search?: string | undefined;
49
+ sort?: string | undefined;
50
+ sortDirection?: "asc" | "desc" | undefined;
51
+ offset?: number | undefined;
52
+ totalCount?: boolean | undefined;
53
+ filter?:
54
+ | {
55
+ readonly field: string;
56
+ readonly op: "eq" | "ne" | "lt" | "gt" | "in";
57
+ readonly value: unknown;
58
+ }
59
+ | undefined;
60
+ // User-chosen faceted filters (dynamic, additive to the static
61
+ // `filter`). All combined with AND.
62
+ filters?:
63
+ | ReadonlyArray<{
64
+ readonly field: string;
65
+ readonly op: "eq" | "ne" | "lt" | "gt" | "in";
66
+ readonly value: unknown;
67
+ }>
68
+ | undefined;
69
+ },
70
+ user: SessionUser,
71
+ db: TenantDb,
72
+ /** Tier 2.7e audit fix: per-call SearchAdapter override. When the
73
+ * executor didn't get a SearchAdapter via Options at build time
74
+ * (defaultEntityQueryHandler path), the caller (handler) can pass
75
+ * one from ctx.searchAdapter here at runtime.
76
+ * options.searchAdapter (build-time) wins — the runtime override
77
+ * is the fallback for the default wrapper. */
78
+ runtimeOptions?: {
79
+ readonly searchAdapter?: SearchAdapter;
80
+ // Trash query: skip the implicit `isDeleted = FALSE` filter so soft-
81
+ // deleted rows are returned too. Tenant + ownership clauses still apply
82
+ // — includeDeleted only relaxes the soft-delete predicate, never the
83
+ // visibility ones, so it can ride untrusted query input safely.
84
+ readonly includeDeleted?: boolean;
85
+ },
86
+ ) => Promise<CursorResult<Record<string, unknown>>>;
87
+
88
+ detail: (
89
+ payload: { id: EntityId },
90
+ user: SessionUser,
91
+ db: TenantDb,
92
+ ) => Promise<Record<string, unknown> | null>;
93
+ };
@@ -0,0 +1,41 @@
1
+ import type { TenantId } from "./identifiers";
2
+
3
+ export type EventMetadata = {
4
+ readonly userId: string;
5
+ readonly requestId?: string;
6
+ // End-to-end business-operation id. Root HTTP requests get it from the
7
+ // x-correlation-id header (default: requestId). MSP-applies inherit it
8
+ // from the triggering event. Lets you trace "which user click caused
9
+ // this email 3 streams later?".
10
+ readonly correlationId?: string;
11
+ // Stored event id that triggered this write. Null for root commands;
12
+ // set to event.id when an MSP-apply runs ctx.appendEvent. Together with
13
+ // correlationId forms a causation DAG across aggregate streams.
14
+ readonly causationId?: string;
15
+ // Marten-conform free key/value space for app-specific metadata that
16
+ // doesn't deserve its own EventMetadata field. Examples: A/B-test bucket,
17
+ // feature-flag snapshot, geo-region, client SDK version. Persisted into
18
+ // events.metadata jsonb (no schema change — it's already a free-form
19
+ // jsonb column), survives upcasters untouched, available on every
20
+ // StoredEvent.metadata.headers. Framework does not interpret values; the
21
+ // app reads them when filtering/auditing. Keep values JSON-primitive
22
+ // (string|number|boolean) so JSON serialization stays bulletproof.
23
+ readonly headers?: Readonly<Record<string, string | number | boolean>>;
24
+ };
25
+
26
+ // Generic over the payload shape. Default = Record<string, unknown> keeps
27
+ // all existing consumers backwards-compatible; annotate `StoredEvent<MyEventPayload>`
28
+ // where a typed payload read is needed.
29
+ export type StoredEvent<TPayload = Record<string, unknown>> = {
30
+ readonly id: string;
31
+ readonly aggregateId: string;
32
+ readonly aggregateType: string;
33
+ readonly tenantId: TenantId;
34
+ readonly version: number;
35
+ readonly type: string;
36
+ readonly eventVersion: number;
37
+ readonly payload: TPayload;
38
+ readonly metadata: EventMetadata;
39
+ readonly createdAt: Temporal.Instant;
40
+ readonly createdBy: string;
41
+ };
@@ -0,0 +1,19 @@
1
+ // A managed projection is only writable through the executor (event →
2
+ // rebuild-safe). To make a direct write a *compile* error rather than a
3
+ // convention, EntityTable carries a phantom `unique symbol` prop; the public
4
+ // write helpers reject anything that has it (see NotExecutorOnly + query.ts).
5
+ // The symbol key dodges SchemaTable's `[field: string]: unknown` index
6
+ // signature (string index sigs don't cover symbol keys), so the brand
7
+ // survives the type-erasure that would swallow a plain marker prop — and the
8
+ // executor seam (applyEntityEvent) erases `table` to TableColumns<any>, which
9
+ // carries no such prop, so the one legitimate writer stays green.
10
+ declare const EXECUTOR_ONLY: unique symbol;
11
+ export interface ExecutorOnly {
12
+ readonly [EXECUTOR_ONLY]: true;
13
+ }
14
+ // Negative brand for write-helper params: a branded EntityTable is NOT
15
+ // assignable (its `true` clashes with `never`), while unmanaged EntityTableMeta
16
+ // and erased SchemaTable pass (they lack the prop, so `?: never` is satisfied).
17
+ export type NotExecutorOnly = {
18
+ readonly [EXECUTOR_ONLY]?: never;
19
+ };