@cosmicdrift/kumiko-types 0.159.0 → 0.160.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +149 -1
- package/src/concurrency-mode.ts +2 -0
- package/src/config-scope.ts +2 -0
- package/src/config.ts +498 -0
- package/src/cursor-types.ts +18 -0
- package/src/define-handler.ts +115 -0
- package/src/entity-cache.ts +35 -0
- package/src/entity-handlers.ts +30 -0
- package/src/entity-table-meta-types.ts +92 -0
- package/src/envelope-cipher-types.ts +6 -0
- package/src/event-store-errors.ts +35 -0
- package/src/event-store-executor-types.ts +93 -0
- package/src/event-store-types.ts +41 -0
- package/src/executor-brand.ts +19 -0
- package/src/feature.ts +1040 -0
- package/src/fields.ts +685 -0
- package/src/file-handle-types.ts +19 -0
- package/src/file-provider-resolver-types.ts +7 -0
- package/src/file-storage-provider-types.ts +39 -0
- package/src/handlers.ts +836 -0
- package/src/hook-phase.ts +2 -0
- package/src/hooks.ts +170 -0
- package/src/kms-adapter-types.ts +118 -0
- package/src/multi-stream-apply-context-types.ts +38 -0
- package/src/nav.ts +67 -0
- package/src/ownership.ts +83 -0
- package/src/projection.ts +164 -0
- package/src/rate-limit-types.ts +30 -0
- package/src/schema-table-types.ts +24 -0
- package/src/screen.ts +747 -0
- package/src/secrets-types.ts +177 -0
- package/src/snapshot-types.ts +36 -0
- package/src/step.ts +334 -0
- package/src/tenant-db-types.ts +58 -0
- package/src/tz-context.ts +63 -0
- package/src/where-clause-types.ts +25 -0
- package/src/workspace.ts +42 -0
- package/src/write-error-info-types.ts +22 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityId, TenantId } from "./identifiers";
|
|
2
|
+
|
|
3
|
+
export type CursorQueryOptions = {
|
|
4
|
+
tenantId: TenantId;
|
|
5
|
+
cursor?: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
filterIds?: readonly EntityId[];
|
|
8
|
+
sort?: string;
|
|
9
|
+
sortDirection?: "asc" | "desc";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CursorResult<T> = {
|
|
13
|
+
rows: T[];
|
|
14
|
+
nextCursor: string | null;
|
|
15
|
+
/** Optional total row count — only present when the caller sets
|
|
16
|
+
* `totalCount: true` on the query. */
|
|
17
|
+
total?: number;
|
|
18
|
+
};
|
|
@@ -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,35 @@
|
|
|
1
|
+
import type { EntityId, TenantId } from "./identifiers";
|
|
2
|
+
|
|
3
|
+
export type EntityCache = {
|
|
4
|
+
/** Get a single cached entity. Returns null on miss. */
|
|
5
|
+
get(
|
|
6
|
+
tenantId: TenantId,
|
|
7
|
+
entityName: string,
|
|
8
|
+
id: EntityId,
|
|
9
|
+
): Promise<Record<string, unknown> | null>;
|
|
10
|
+
|
|
11
|
+
/** Get multiple cached entities. Returns a Map of id → data (misses are absent). */
|
|
12
|
+
mget(
|
|
13
|
+
tenantId: TenantId,
|
|
14
|
+
entityName: string,
|
|
15
|
+
ids: readonly EntityId[],
|
|
16
|
+
): Promise<Map<EntityId, Record<string, unknown>>>;
|
|
17
|
+
|
|
18
|
+
/** Cache a single entity. */
|
|
19
|
+
set(
|
|
20
|
+
tenantId: TenantId,
|
|
21
|
+
entityName: string,
|
|
22
|
+
id: EntityId,
|
|
23
|
+
data: Record<string, unknown>,
|
|
24
|
+
): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/** Cache multiple entities at once. */
|
|
27
|
+
mset(
|
|
28
|
+
tenantId: TenantId,
|
|
29
|
+
entityName: string,
|
|
30
|
+
entries: ReadonlyArray<{ id: EntityId; data: Record<string, unknown> }>,
|
|
31
|
+
): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/** Invalidate a single cached entity. */
|
|
34
|
+
del(tenantId: TenantId, entityName: string, id: EntityId): Promise<void>;
|
|
35
|
+
};
|
|
@@ -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,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
|
+
};
|