@cosmicdrift/kumiko-framework 0.379.0 → 0.381.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.
Files changed (37) hide show
  1. package/dist/db/apply-entity-event.d.ts +3 -2
  2. package/dist/db/apply-entity-event.js +1 -1
  3. package/dist/db/dialect.d.ts +2 -1
  4. package/dist/db/dialect.js +2 -2
  5. package/dist/db/entity-table-from-registry.d.ts +1 -2
  6. package/dist/db/entity-table-from-registry.js +0 -2
  7. package/dist/db/entity-table-meta.d.ts +5 -1
  8. package/dist/db/entity-table-meta.js +5 -12
  9. package/dist/db/executor-managed-tables.d.ts +3 -1
  10. package/dist/db/executor-managed-tables.js +9 -1
  11. package/dist/db/index.d.ts +2 -2
  12. package/dist/db/query.d.ts +22 -11
  13. package/dist/db/query.js +13 -36
  14. package/dist/db/table-builder.d.ts +2 -3
  15. package/dist/db/table-introspection.d.ts +2 -0
  16. package/dist/db/table-introspection.js +4 -0
  17. package/dist/db/tenant-db.d.ts +8 -4
  18. package/dist/db/tenant-db.js +35 -10
  19. package/dist/engine/extensions/tenant-data.d.ts +2 -2
  20. package/dist/engine/projection-helpers.d.ts +3 -3
  21. package/dist/engine/steps/unsafe-projection-delete.js +3 -2
  22. package/dist/es-ops/operations-schema.d.ts +1 -1
  23. package/dist/event-store/archive.d.ts +1 -1
  24. package/dist/event-store/events-schema.d.ts +1 -1
  25. package/dist/event-store/rebuild-dead-letter.d.ts +1 -1
  26. package/dist/event-store/snapshot.d.ts +1 -1
  27. package/dist/event-store/upcaster-dead-letter.d.ts +1 -1
  28. package/dist/files/storage-tracking.d.ts +1 -1
  29. package/dist/files/upload-ledger.d.ts +2 -2
  30. package/dist/internal/testing.d.ts +6 -1
  31. package/dist/internal/testing.js +1 -2
  32. package/dist/migrations/pending-rebuilds.d.ts +1 -1
  33. package/dist/migrations/pii-event-backfill.d.ts +1 -1
  34. package/dist/pipeline/event-consumer-state.d.ts +1 -1
  35. package/dist/pipeline/projection-state.d.ts +1 -1
  36. package/package.json +3 -3
  37. package/src/changes.json +7 -0
@@ -1,6 +1,7 @@
1
+ import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
1
2
  import type { EntityDefinition } from "../engine/types/index.ts";
2
3
  import type { StoredEvent } from "../event-store/index.ts";
3
- import type { DbRow, DbRunner } from "./connection.ts";
4
+ import type { DbRow } from "./connection.ts";
4
5
  import type { TableColumns } from "./dialect.ts";
5
6
  type Table = TableColumns<any>;
6
7
  export type AutoVerb = "created" | "updated" | "deleted" | "restored" | "forgotten";
@@ -17,5 +18,5 @@ export declare function parseAutoVerb(event: StoredEvent): AutoVerb | null;
17
18
  /** Idempotente Anwendung eines Auto-Events auf die Entity-Tabelle.
18
19
  * Wird sowohl beim Live-Append (innerhalb der Write-TX) als auch beim
19
20
  * Rebuild (innerhalb der Rebuild-TX) gerufen — identische Logik. */
20
- export declare function applyEntityEvent(event: StoredEvent, table: Table, entity: EntityDefinition, tx: DbRunner): Promise<ApplyResult>;
21
+ export declare function applyEntityEvent(event: StoredEvent, table: Table, entity: EntityDefinition, tx: ExecutorManagedRunner): Promise<ApplyResult>;
21
22
  export {};
@@ -14,7 +14,7 @@
14
14
  // (DEK erased → Wert unlesbar, bidx NULL). Load-bearing Test:
15
15
  // db/__tests__/implicit-projection-equivalence.integration.test.ts.
16
16
  //
17
- // Tenant isolation: applyEntityEvent expects a raw DbRunner (tx or pool), NOT
17
+ // Tenant isolation: applyEntityEvent expects an ExecutorManagedRunner (tx or pool), NOT
18
18
  // a TenantDb wrapper. Two sources keep it safe:
19
19
  // 1. The live path reads the target row through the tenant-scoped TenantDb
20
20
  // before writing — loadById() for update/delete/forget, db.fetchOne() for
@@ -1,3 +1,4 @@
1
+ import type { WritableSchemaTable } from "@cosmicdrift/kumiko-types/executor-brand";
1
2
  import { type ColumnHandle, type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
2
3
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
4
  import type { PgType } from "./entity-table-meta.ts";
@@ -94,7 +95,7 @@ export type ColumnMap = Record<string, ColumnBuilder<unknown>>;
94
95
  type IndexOrPk = IndexBuilderWithCols | PrimaryKeyDescriptor;
95
96
  export declare function table<TCols extends ColumnMap>(tableName: string, cols: TCols, optsFn?: (t: {
96
97
  [K in keyof TCols]: ColumnHandle;
97
- }) => Record<string, IndexOrPk> | ReadonlyArray<IndexOrPk>): SchemaTable;
98
+ }) => Record<string, IndexOrPk> | ReadonlyArray<IndexOrPk>): WritableSchemaTable;
98
99
  /** Reads the `kumiko:schema:Name` symbol from a table object.
99
100
  * Throws with `context` in the message so callers don't have to duplicate the guard. */
100
101
  export declare function extractTableName(table: unknown, context?: string): string;
@@ -18,7 +18,7 @@
18
18
  // crashed on timestamptz writes without it (#1480).
19
19
  import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
20
20
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
- import { schemaTableName } from "./table-introspection.js";
21
+ import { brandWritable, schemaTableName } from "./table-introspection.js";
22
22
  function isNumericPgType(t) {
23
23
  return t.startsWith("numeric(");
24
24
  }
@@ -376,7 +376,7 @@ export function table(tableName, cols, optsFn) {
376
376
  [KUMIKO_COLUMNS_SYMBOL]: handles,
377
377
  [KUMIKO_META_SYMBOL]: base,
378
378
  });
379
- return out;
379
+ return brandWritable(out);
380
380
  }
381
381
  /** Reads the `kumiko:schema:Name` symbol from a table object.
382
382
  * Throws with `context` in the message so callers don't have to duplicate the guard. */
@@ -1,5 +1,4 @@
1
- import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
2
1
  import type { Registry } from "../engine/index.ts";
3
2
  import type { SchemaTable } from "./dialect.ts";
4
3
  import { buildEntityTable } from "./table-builder.ts";
5
- export declare function entityTableFromRegistry(registry: Registry, entityName: string, entity: Parameters<typeof buildEntityTable>[1]): SchemaTable & ExecutorOnly;
4
+ export declare function entityTableFromRegistry(registry: Registry, entityName: string, entity: Parameters<typeof buildEntityTable>[1]): SchemaTable;
@@ -17,8 +17,6 @@ export function entityTableFromRegistry(registry, entityName, entity) {
17
17
  continue;
18
18
  if (projection.source !== entityName)
19
19
  continue;
20
- // @cast-boundary projection-table — ProjectionDefinition.table is the
21
- // untyped table object every projection registrar accepts.
22
20
  return projection.table;
23
21
  }
24
22
  return buildEntityTable(entityName, entity);
@@ -1,3 +1,4 @@
1
+ import type { Writable } from "@cosmicdrift/kumiko-types/executor-brand";
1
2
  import type { EntityTenancy, TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
2
3
  import type { EntityDefinition } from "../engine/types/index.ts";
3
4
  import type { BuildEntityTableMetaOptions, EntityTableMeta, UnmanagedTableInput } from "./entity-table-meta-types.ts";
@@ -12,6 +13,9 @@ export declare function resolveTableName(entityName: string, entity: EntityDefin
12
13
  * Named `derive*` (not `build*Meta`) so it is not mistaken for the unmanaged
13
14
  * escape hatch (#1208).
14
15
  */
16
+ export declare function deriveEntityTableMeta(entityName: string, entity: EntityDefinition, options: BuildEntityTableMetaOptions & {
17
+ readonly source: "unmanaged";
18
+ }): EntityTableMeta & Writable;
15
19
  export declare function deriveEntityTableMeta(entityName: string, entity: EntityDefinition, options?: BuildEntityTableMetaOptions): EntityTableMeta;
16
20
  export declare function assertBackingTableSuperset(entityName: string, fieldMeta: EntityTableMeta, tableMeta: EntityTableMeta): void;
17
21
  /**
@@ -25,4 +29,4 @@ export declare function assertBackingTableSuperset(entityName: string, fieldMeta
25
29
  */
26
30
  export declare function defineUnmanagedTable<const T extends EntityTenancy = "tenant">(input: UnmanagedTableInput & {
27
31
  readonly tenancy?: T;
28
- }): EntityTableMeta & TenancyBrand<T>;
32
+ }): EntityTableMeta & TenancyBrand<T> & Writable;
@@ -18,6 +18,7 @@
18
18
  import { collectPiiEncryptedFields, isRecordKeyedField } from "../crypto/index.js";
19
19
  import { isSqlExpression } from "./dialect.js";
20
20
  import { READ_MODEL_PREFIX, SOFT_DELETE_LIVE_ROW_PREDICATE, toSnakeCase, toTableName, } from "./table-builder.js";
21
+ import { brandWritable } from "./table-introspection.js";
21
22
  // Standard base columns for event-sourced read-model tables. Mirrors
22
23
  // `buildBaseColumns()` from table-builder.ts (drizzle variant).
23
24
  function fullBaseColumns(idType, softDelete) {
@@ -206,15 +207,6 @@ export function resolveTableName(entityName, entity, featureName) {
206
207
  }
207
208
  return `${featureName}_${baseName}`;
208
209
  }
209
- /**
210
- * Derive EntityTableMeta from an EntityDefinition (base columns + field DDL).
211
- * Defaults to `source: "managed"` (rebuildable projection). For direct-write
212
- * stores pass `{ source: "unmanaged" }` and use a non-`read_` table name
213
- * (convention: `store_*`) — or build columns by hand with `defineUnmanagedTable`.
214
- *
215
- * Named `derive*` (not `build*Meta`) so it is not mistaken for the unmanaged
216
- * escape hatch (#1208).
217
- */
218
210
  export function deriveEntityTableMeta(entityName, entity, options) {
219
211
  const tableName = resolveTableName(entityName, entity, options?.featureName);
220
212
  const source = options?.source ?? "managed";
@@ -345,7 +337,7 @@ export function deriveEntityTableMeta(entityName, entity, options) {
345
337
  }
346
338
  }
347
339
  const piiSubjectFields = collectPiiEncryptedFields(entity);
348
- return {
340
+ const meta = {
349
341
  tableName,
350
342
  columns,
351
343
  indexes,
@@ -353,6 +345,7 @@ export function deriveEntityTableMeta(entityName, entity, options) {
353
345
  ...(piiSubjectFields.length > 0 && { piiSubjectFields }),
354
346
  ...(entity.tenancy === "global" && { tenancy: "global" }),
355
347
  };
348
+ return source === "unmanaged" ? brandWritable(meta) : meta;
356
349
  }
357
350
  function sqlExpressionText(where) {
358
351
  return isSqlExpression(where) ? where.text : undefined;
@@ -405,7 +398,7 @@ export function defineUnmanagedTable(input) {
405
398
  "have a tenant_id column — a global table's rows carry the system tenant, not a per-row " +
406
399
  "tenant identity.");
407
400
  }
408
- return {
401
+ return brandWritable({
409
402
  tableName: input.tableName,
410
403
  columns: input.columns,
411
404
  indexes: input.indexes ?? [],
@@ -415,7 +408,7 @@ export function defineUnmanagedTable(input) {
415
408
  source: "unmanaged",
416
409
  ...(input.tenancy === "global" && { tenancy: "global" }),
417
410
  // @cast-boundary phantom-brand — TenancyBrand<T> is a phantom marker with no runtime representation.
418
- };
411
+ });
419
412
  }
420
413
  function assertUnmanagedTableName(tableName, via) {
421
414
  if (tableName.startsWith("read_")) {
@@ -1,6 +1,8 @@
1
+ import type { ExecutorManagedWriteCapability, Writable } from "@cosmicdrift/kumiko-types/executor-brand";
1
2
  import { AccessDeniedError } from "../errors/index.ts";
2
3
  export declare function markExecutorManagedTable(table: object): void;
3
4
  export declare function isExecutorManagedTable(table: unknown): boolean;
4
5
  export declare function registerExecutorManagedWriteRunner(runner: object): void;
5
- export declare function hasExecutorManagedWriteCapability(runner: unknown): boolean;
6
+ export declare function hasExecutorManagedWriteCapability(runner: unknown): runner is ExecutorManagedWriteCapability;
6
7
  export declare function executorManagedWriteDenied(runner: unknown, table: unknown, operation: string): AccessDeniedError | undefined;
8
+ export declare function assertWritableTable<T extends object>(table: T): asserts table is T & Writable;
@@ -1,4 +1,4 @@
1
- import { AccessDeniedError } from "../errors/index.js";
1
+ import { AccessDeniedError, InternalError } from "../errors/index.js";
2
2
  import { asEntityTableMeta, tableNameOf } from "./table-introspection.js";
3
3
  const executorManagedTables = new WeakSet();
4
4
  const executorManagedWriteRunners = new WeakSet();
@@ -38,3 +38,11 @@ export function executorManagedWriteDenied(runner, table, operation) {
38
38
  "projection apply; a direct write drifts from the event stream and a rebuild drops or resurrects the row.",
39
39
  });
40
40
  }
41
+ // Unmanaged tables are the Writable ones; callers invoke this only after executorManagedWriteDenied passed without a capable runner.
42
+ export function assertWritableTable(table) {
43
+ if (isExecutorManagedTable(table)) {
44
+ throw new InternalError({
45
+ message: `assertWritableTable: "${tableNameOf(table)}" is executor-managed and the runner holds no write capability.`,
46
+ });
47
+ }
48
+ }
@@ -36,6 +36,6 @@ export { renderTableDdl, renderTablesDdl } from "./render-ddl.ts";
36
36
  export { diffReplayAgainstSnapshot, type ReplayedSchema, type ReplayedTable, type ReplayMismatch, replayMigrationsDir, } from "./replay-migration-sql.ts";
37
37
  export { findCommentedDropTables, isRetiredFrameworkTable, type RetiredFrameworkTable, retiredFrameworkTables, } from "./retired-framework-tables.ts";
38
38
  export { tableExists } from "./schema-inspection.ts";
39
- export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, type EntityRow, type EntityTable, physicalColumnName, toSnakeCase, toTableName, } from "./table-builder.ts";
40
- export type { CreateTenantDbOptions, OutsideTransactionContext, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.ts";
39
+ export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, type EntityRow, type EntityTable, type ExecutorManagedRunner, type ExecutorManagedWriteCapability, physicalColumnName, toSnakeCase, toTableName, type Writable, type WritableSchemaTable, type WritableTable, } from "./table-builder.ts";
40
+ export type { CreateTenantDbOptions, ExecutorManagedTenantDb, OutsideTransactionContext, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.ts";
41
41
  export { castTenantRows, createSystemDbView, createTenantDataDestroyDb, createTenantDb, hasOutsideTransactionDb, requireOutsideTransactionDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.ts";
@@ -1,9 +1,9 @@
1
1
  import type { AggregateRow, AggregateSpec } from "@cosmicdrift/kumiko-types/aggregate-types";
2
2
  import type { DbRunner, DbTx } from "@cosmicdrift/kumiko-types/db-connection";
3
+ import type { ExecutorManagedRunner, ExecutorManagedWriteCapability, WritableTable } from "@cosmicdrift/kumiko-types/executor-brand";
3
4
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
5
  import type { SelectOptions, WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
5
6
  import type { EntityTableMeta } from "./entity-table-meta.ts";
6
- import { type NotExecutorOnly } from "./table-builder.ts";
7
7
  import { asEntityTableMeta, requireEntityTableMeta } from "./table-introspection.ts";
8
8
  export { asEntityTableMeta, requireEntityTableMeta };
9
9
  export type ReservedClient = {
@@ -23,12 +23,14 @@ export type RawClientWithBegin = RawClient & {
23
23
  export declare function hasBegin(client: RawClient): client is RawClientWithBegin;
24
24
  export declare function asRawClient(db: unknown): RawClient;
25
25
  export declare function runnerCanBeginTransaction(db: unknown): boolean;
26
+ export declare function runInSavepoint<T>(tx: ExecutorManagedRunner, fn: (sp: ExecutorManagedRunner) => Promise<T>): Promise<T>;
26
27
  export declare function runInSavepoint<T>(tx: unknown, fn: (sp: unknown) => Promise<T>): Promise<T>;
28
+ export declare function runInSavepointIfSupported<T>(db: ExecutorManagedRunner, fn: (sp: ExecutorManagedRunner) => Promise<T>): Promise<T>;
27
29
  export declare function runInSavepointIfSupported<T>(db: unknown, fn: (sp: DbRunner) => Promise<T>): Promise<T>;
28
30
  export type AnyDb = DbRunner | unknown;
31
+ type AnyRow = any;
29
32
  export type { JsonTextMatch, OrderByClause, SelectOptions, WhereObject, WhereOperator, WhereValue, } from "@cosmicdrift/kumiko-types/where-clause-types";
30
33
  type TableLike = EntityTableMeta | any;
31
- type WritableTable = EntityTableMeta & NotExecutorOnly;
32
34
  export type TableInfo = {
33
35
  readonly name: string;
34
36
  readonly columnOf: (field: string) => string;
@@ -43,8 +45,8 @@ export declare function instantToTimestamptzParam(instant: Temporal.Instant): st
43
45
  export declare function isClosedConnectionError(err: unknown): boolean;
44
46
  export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
45
47
  export declare function unsafeReadRetrying<TRow>(db: AnyDb, sqlText: string, params: readonly unknown[]): Promise<readonly TRow[]>;
46
- export declare function selectMany<TRow = any>(db: AnyDb, table: TableLike, where?: WhereObject, options?: SelectOptions): Promise<readonly TRow[]>;
47
- export declare function fetchOne<TRow = any>(db: AnyDb, table: TableLike, where: WhereObject): Promise<TRow | undefined>;
48
+ export declare function selectMany<TRow = AnyRow>(db: AnyDb, table: TableLike, where?: WhereObject, options?: SelectOptions): Promise<readonly TRow[]>;
49
+ export declare function fetchOne<TRow = AnyRow>(db: AnyDb, table: TableLike, where: WhereObject): Promise<TRow | undefined>;
48
50
  export type JoinColumnPair = {
49
51
  readonly left: string;
50
52
  readonly right: string;
@@ -65,10 +67,14 @@ export type InnerJoinRow<TLeft, TRight> = {
65
67
  readonly right: TRight;
66
68
  };
67
69
  export declare function selectInnerJoin<TLeft = Record<string, unknown>, TRight = Record<string, unknown>>(db: AnyDb, spec: InnerJoinSpec): Promise<readonly InnerJoinRow<TLeft, TRight>[]>;
68
- export declare function insertMany<TRow = any>(db: AnyDb, table: WritableTable, rows: ReadonlyArray<Record<string, unknown>>): Promise<readonly TRow[]>;
69
- export declare function insertOne<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>): Promise<TRow | undefined>;
70
- export declare function updateMany<TRow = any>(db: AnyDb, table: WritableTable, set: Record<string, unknown>, where: WhereObject): Promise<readonly TRow[]>;
70
+ export declare function insertMany<TRow = AnyRow>(db: AnyDb, table: WritableTable, rows: ReadonlyArray<Record<string, unknown>>): Promise<readonly TRow[]>;
71
+ export declare function insertMany<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, rows: ReadonlyArray<Record<string, unknown>>): Promise<readonly TRow[]>;
72
+ export declare function insertOne<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>): Promise<TRow | undefined>;
73
+ export declare function insertOne<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>): Promise<TRow | undefined>;
74
+ export declare function updateMany<TRow = AnyRow>(db: AnyDb, table: WritableTable, set: Record<string, unknown>, where: WhereObject): Promise<readonly TRow[]>;
75
+ export declare function updateMany<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, set: Record<string, unknown>, where: WhereObject): Promise<readonly TRow[]>;
71
76
  export declare function deleteMany(db: AnyDb, table: WritableTable, where: WhereObject): Promise<number>;
77
+ export declare function deleteMany(db: ExecutorManagedWriteCapability, table: EntityTableMeta, where: WhereObject): Promise<number>;
72
78
  export declare function resolveConflictColumns(table: TableLike, info: TableInfo, conflictKeys: readonly string[] | undefined): readonly string[];
73
79
  export type UpsertOnConflictOptions = {
74
80
  readonly conflictKeys: readonly string[];
@@ -88,18 +94,23 @@ export type DeleteManyBatchedResult = {
88
94
  export declare function countWhere(db: AnyDb, table: TableLike, where?: WhereObject): Promise<number>;
89
95
  export declare function isTimestamptzType(pgType: string | undefined): boolean;
90
96
  export declare function aggregateWhere(db: AnyDb, table: TableLike, spec: AggregateSpec, where?: WhereObject): Promise<readonly AggregateRow[]>;
91
- export declare function upsertOnConflict<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options: UpsertOnConflictOptions): Promise<TRow | undefined>;
92
- export declare function upsertByPk<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, updateOnConflict?: Record<string, unknown>): Promise<TRow | undefined>;
97
+ export declare function upsertOnConflict<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options: UpsertOnConflictOptions): Promise<TRow | undefined>;
98
+ export declare function upsertOnConflict<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, options: UpsertOnConflictOptions): Promise<TRow | undefined>;
99
+ export declare function upsertByPk<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, updateOnConflict?: Record<string, unknown>): Promise<TRow | undefined>;
100
+ export declare function upsertByPk<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, updateOnConflict?: Record<string, unknown>): Promise<TRow | undefined>;
93
101
  export type InsertOnConflictDoNothingOptions = {
94
102
  readonly conflictKeys?: readonly string[];
95
103
  };
96
- export declare function insertOnConflictDoNothing<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options?: InsertOnConflictDoNothingOptions): Promise<TRow | undefined>;
104
+ export declare function insertOnConflictDoNothing<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options?: InsertOnConflictDoNothingOptions): Promise<TRow | undefined>;
105
+ export declare function insertOnConflictDoNothing<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, options?: InsertOnConflictDoNothingOptions): Promise<TRow | undefined>;
97
106
  export type IncrementCounterOptions = {
98
107
  readonly conflictKeys?: readonly string[];
99
108
  /** Extra SET clauses on conflict (e.g. lastUpdatedAt: sql\`NOW()\`). */
100
109
  readonly set?: Record<string, unknown>;
101
110
  };
102
- export declare function incrementCounter<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, increments: Record<string, number>, options?: IncrementCounterOptions): Promise<TRow | undefined>;
111
+ export declare function incrementCounter<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, increments: Record<string, number>, options?: IncrementCounterOptions): Promise<TRow | undefined>;
112
+ export declare function incrementCounter<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, increments: Record<string, number>, options?: IncrementCounterOptions): Promise<TRow | undefined>;
103
113
  export declare function deleteManyBatched(db: AnyDb, table: WritableTable, where: WhereObject, options: DeleteManyBatchedOptions): Promise<DeleteManyBatchedResult>;
114
+ export declare function deleteManyBatched(db: ExecutorManagedWriteCapability, table: EntityTableMeta, where: WhereObject, options: DeleteManyBatchedOptions): Promise<DeleteManyBatchedResult>;
104
115
  export declare function transaction<T>(db: AnyDb, fn: (tx: DbTx) => Promise<T>): Promise<T>;
105
116
  export declare function runInNewTransaction<T>(db: unknown, fn: (tx: DbRunner) => Promise<T>): Promise<T>;
package/dist/db/query.js CHANGED
@@ -78,42 +78,27 @@ export function asRawClient(db) {
78
78
  export function runnerCanBeginTransaction(db) {
79
79
  return hasBegin(asRawClient(db));
80
80
  }
81
- // postgres.js rejects the WHOLE begin() when any statement inside
82
- // it errored — even if the JS error was caught (the driver tracks per-block
83
- // query failures). Error confinement therefore must go through the driver's
84
- // own savepoint(), not manual SAVEPOINT statements. The callback receives
85
- // the savepoint-scoped tx; run every confined statement on THAT handle.
81
+ function savepointRunnerOf(sp) {
82
+ // @cast-boundary db-runner savepoint handle → TRunner at the single seam (boundRunner re-wraps savepoint handles with the caller's capability)
83
+ return sp;
84
+ }
86
85
  export async function runInSavepoint(tx, fn) {
87
86
  const raw = asRawClient(tx);
88
87
  if (typeof raw.savepoint !== "function") {
89
88
  throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (postgres.js TransactionSql).");
90
89
  }
91
- return raw.savepoint(fn);
90
+ return raw.savepoint((sp) => fn(savepointRunnerOf(sp)));
92
91
  }
93
- // Same error-confinement as runInSavepoint, but for call sites that don't
94
- // know whether `db` is a bare pool connection or an active transaction
95
- // (e.g. the CRUD executor, invoked both from a dispatcher tx and directly
96
- // against the pool by seeds/tests). A pool connection has no savepoint() —
97
- // and doesn't need one, since each statement there is its own auto-committed
98
- // unit and a failed statement can't poison anything downstream.
99
- //
100
- // `.savepoint` being present isn't proof the transaction is still open: a
101
- // committed TransactionSql-shaped db still exposes `.savepoint`, and calling
102
- // it fails with PG 25P01 ("no active sql transaction") since there's no
103
- // BEGIN left to nest into. That is now a programming error, not something
104
- // to paper over by re-running `fn` without the savepoint's isolation.
105
92
  export async function runInSavepointIfSupported(db, fn) {
106
93
  const raw = asRawClient(db);
107
- // @cast-boundary db-runner savepoint handle → DbRunner at the single seam
108
- const asRunner = (sp) => sp;
109
94
  if (typeof raw.savepoint !== "function")
110
- return fn(asRunner(db));
95
+ return fn(savepointRunnerOf(db));
111
96
  // An error from inside fn must never be remapped, even if it carries a closed-connection code.
112
97
  let fnStarted = false;
113
98
  try {
114
99
  return await raw.savepoint((sp) => {
115
100
  fnStarted = true;
116
- return fn(asRunner(sp));
101
+ return fn(savepointRunnerOf(sp));
117
102
  });
118
103
  }
119
104
  catch (e) {
@@ -870,7 +855,6 @@ function selectColumnList(info, fields) {
870
855
  })
871
856
  .join(", ");
872
857
  }
873
- // biome-ignore lint/suspicious/noExplicitAny: opt-in default loosens row type for unannotated test fixtures
874
858
  export async function selectMany(db, table, where, options) {
875
859
  const scoped = tenantDbDelegate(db);
876
860
  if (scoped) {
@@ -905,7 +889,6 @@ export async function selectMany(db, table, where, options) {
905
889
  const raw = await unsafeReadRetrying(db, sqlText, values);
906
890
  return coerceRows(raw, info);
907
891
  }
908
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
909
892
  export async function fetchOne(db, table, where) {
910
893
  const rows = await selectMany(db, table, where, { limit: 1 });
911
894
  return rows[0];
@@ -980,10 +963,6 @@ export async function selectInnerJoin(db, spec) {
980
963
  };
981
964
  });
982
965
  }
983
- // Bulk INSERT — same shape as insertOne but takes an array of rows and
984
- // produces one multi-VALUES statement. Mirrors drizzle's
985
- // `db.insert(t).values(rows[])`. Empty input is a no-op.
986
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
987
966
  export async function insertMany(db, table, rows) {
988
967
  if (rows.length === 0)
989
968
  return [];
@@ -1026,7 +1005,6 @@ export async function insertMany(db, table, rows) {
1026
1005
  });
1027
1006
  return coerceRows(raw, info);
1028
1007
  }
1029
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1030
1008
  export async function insertOne(db, table, values) {
1031
1009
  const scoped = tenantDbDelegate(db);
1032
1010
  if (scoped) {
@@ -1065,7 +1043,6 @@ export async function insertOne(db, table, values) {
1065
1043
  return undefined;
1066
1044
  return coerceRow(first, info);
1067
1045
  }
1068
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1069
1046
  export async function updateMany(db, table, set, where) {
1070
1047
  const scoped = tenantDbDelegate(db);
1071
1048
  if (scoped) {
@@ -1290,8 +1267,11 @@ export async function aggregateWhere(db, table, spec, where = {}) {
1290
1267
  };
1291
1268
  });
1292
1269
  }
1293
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1294
- export async function upsertOnConflict(db, table, values, options) {
1270
+ export function upsertOnConflict(db, table, values, options) {
1271
+ return upsertOnConflictChecked(db, table, values, options);
1272
+ }
1273
+ // upsertByPk shares this body; the checks inside gate the write, so it needs no overload pair.
1274
+ async function upsertOnConflictChecked(db, table, values, options) {
1295
1275
  assertNotTenantScoped(db, "upsertOnConflict");
1296
1276
  const managedDenied = executorManagedWriteDenied(db, table, "upsertOnConflict");
1297
1277
  if (managedDenied)
@@ -1337,7 +1317,6 @@ export async function upsertOnConflict(db, table, values, options) {
1337
1317
  return undefined;
1338
1318
  return coerceRow(first, info);
1339
1319
  }
1340
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1341
1320
  export async function upsertByPk(db, table, values, updateOnConflict) {
1342
1321
  const managedDenied = executorManagedWriteDenied(db, table, "upsertByPk");
1343
1322
  if (managedDenied)
@@ -1345,12 +1324,11 @@ export async function upsertByPk(db, table, values, updateOnConflict) {
1345
1324
  const info = extractTableInfo(table);
1346
1325
  const conflictKeys = resolveConflictColumns(table, info, undefined);
1347
1326
  const fieldKeys = conflictKeys.map((col) => info.fieldOf(col));
1348
- return upsertOnConflict(db, table, values, {
1327
+ return upsertOnConflictChecked(db, table, values, {
1349
1328
  conflictKeys: fieldKeys,
1350
1329
  ...(updateOnConflict !== undefined ? { update: updateOnConflict } : {}),
1351
1330
  });
1352
1331
  }
1353
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1354
1332
  export async function insertOnConflictDoNothing(db, table, values, options) {
1355
1333
  assertNotTenantScoped(db, "insertOnConflictDoNothing");
1356
1334
  const managedDenied = executorManagedWriteDenied(db, table, "insertOnConflictDoNothing");
@@ -1371,7 +1349,6 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
1371
1349
  return undefined;
1372
1350
  return coerceRow(first, info);
1373
1351
  }
1374
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1375
1352
  export async function incrementCounter(db, table, values, increments, options = {}) {
1376
1353
  assertNotTenantScoped(db, "incrementCounter");
1377
1354
  const managedDenied = executorManagedWriteDenied(db, table, "incrementCounter");
@@ -1,5 +1,4 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
3
2
  import type { TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
4
3
  import type { EntityDefinition, EntityRelations, FieldDefinition, FieldsMap } from "../engine/types/index.ts";
5
4
  import type { Table } from "./event-store-executor-context.ts";
@@ -194,11 +193,11 @@ type Flatten<T> = {
194
193
  /** The row `executor.list()` / `executor.detail()` return for entity `E`. A wide
195
194
  * `EntityDefinition` (fields without literal keys) yields `Record<string, unknown>`. */
196
195
  export type EntityRow<E extends EntityDefinition = EntityDefinition> = string extends keyof E["fields"] ? Record<string, unknown> : Flatten<RowOfColumns<BaseColumnsType<E>> & SoftDeleteRow<E> & RowForFields<E["fields"]>>;
197
- export type { ExecutorOnly, NotExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
196
+ export type { ExecutorManagedRunner, ExecutorManagedWriteCapability, Writable, WritableSchemaTable, WritableTable, } from "@cosmicdrift/kumiko-types/executor-brand";
198
197
  export type { EntityTenancy, TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
199
198
  type EntityTableTenancy<E extends EntityDefinition> = [NonNullable<E["tenancy"]>] extends [never] ? "tenant" : [NonNullable<E["tenancy"]>] extends ["global"] ? "global" : "tenant";
200
199
  export type EntityTable<E extends EntityDefinition = EntityDefinition> = TableColumns<// biome-ignore lint/suspicious/noExplicitAny: drizzle's internal table-config stays generic; we layer typed columns on top via the intersection below.
201
- any> & BaseColumnsType<E> & SoftDeleteColumnsType & ColumnsForEntity<E["fields"]> & ExecutorOnly & TenancyBrand<EntityTableTenancy<E>>;
200
+ any> & BaseColumnsType<E> & SoftDeleteColumnsType & ColumnsForEntity<E["fields"]> & TenancyBrand<EntityTableTenancy<E>>;
202
201
  export declare function buildBaseColumns(softDelete: boolean, idType?: "serial" | "uuid"): {
203
202
  id: ColumnBuilder<string> | ColumnBuilder<number>;
204
203
  tenantId: ColumnBuilder<string>;
@@ -1,4 +1,5 @@
1
1
  import type { EntityTableMeta } from "@cosmicdrift/kumiko-types/entity-table-meta-types";
2
+ import type { Writable } from "@cosmicdrift/kumiko-types/executor-brand";
2
3
  export declare function isEntityTableMeta(value: unknown): value is EntityTableMeta;
3
4
  export declare function asEntityTableMeta(table: unknown): EntityTableMeta | undefined;
4
5
  export declare function requireEntityTableMeta(table: unknown, tableName: string): EntityTableMeta;
@@ -14,3 +15,4 @@ export declare function columnHandleName(table: unknown, field: string): string
14
15
  export declare function hasFieldProperty(table: unknown, field: string): boolean;
15
16
  /** SQL column name of a JS field via the column-handle map; undefined when not mapped. */
16
17
  export declare function columnSqlNameOf(table: unknown, field: string): string | undefined;
18
+ export declare function brandWritable<T extends EntityTableMeta>(table: T): T & Writable;
@@ -74,3 +74,7 @@ export function columnSqlNameOf(table, field) {
74
74
  const name = handle["name"];
75
75
  return typeof name === "string" ? name : undefined;
76
76
  }
77
+ export function brandWritable(table) {
78
+ // @cast-boundary phantom-brand — Writable is a phantom marker with no runtime representation.
79
+ return table;
80
+ }
@@ -1,7 +1,8 @@
1
1
  import type { EntityTableMeta } from "@cosmicdrift/kumiko-types/entity-table-meta-types";
2
+ import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
2
3
  import type { EscapeHatchDeclaration, EscapeHatchReporter, EscapeHatchTarget } from "@cosmicdrift/kumiko-types/handlers";
3
4
  import type { SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
4
- import { type TenantDb, type TenantDbMode, type UncheckedSystemDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
5
+ import { type ExecutorManagedTenantDb, type TenantDb, type TenantDbMode, type UncheckedSystemDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
5
6
  import { asRawClient } from "../db/query.ts";
6
7
  import type { EntityDefinition } from "../engine/types/fields.ts";
7
8
  import { type TenantId } from "../engine/types/identifiers.ts";
@@ -10,8 +11,8 @@ import { type Meter, type Tracer } from "../observability/index.ts";
10
11
  import type { DbRunner } from "./connection.ts";
11
12
  import { tableNameOf } from "./table-introspection.ts";
12
13
  type Table = SchemaTable;
13
- export { SYSTEM_SCOPE_CHECK_BRAND, type TenantDb, type TenantDbMode, type UncheckedSystemDb, } from "@cosmicdrift/kumiko-types/tenant-db-types";
14
- export declare function withExecutorManagedWrites(runner: DbRunner): DbRunner;
14
+ export { type ExecutorManagedTenantDb, SYSTEM_SCOPE_CHECK_BRAND, type TenantDb, type TenantDbMode, type UncheckedSystemDb, } from "@cosmicdrift/kumiko-types/tenant-db-types";
15
+ export declare function withExecutorManagedWrites(runner: DbRunner): ExecutorManagedRunner;
15
16
  export declare function withoutExecutorManagedWrites(runner: DbRunner): DbRunner;
16
17
  export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
17
18
  export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSystemDb, reason: string): DbRunner;
@@ -55,7 +56,10 @@ export type CreateTenantDbOptions = {
55
56
  readonly signal?: AbortSignal;
56
57
  readonly grants?: TenantDbGrants;
57
58
  };
59
+ export declare function createTenantDb(options: CreateTenantDbOptions & {
60
+ readonly db: ExecutorManagedRunner;
61
+ }): ExecutorManagedTenantDb;
58
62
  export declare function createTenantDb(options: CreateTenantDbOptions): TenantDb;
59
- export declare function createTenantDataDestroyDb(options: Omit<CreateTenantDbOptions, "mode">): TenantDb;
63
+ export declare function createTenantDataDestroyDb(options: Omit<CreateTenantDbOptions, "mode">): ExecutorManagedTenantDb;
60
64
  export declare function createProjectingTenantDb(db: DbRunner, tenantId: TenantId, projectionRegistry: Registry | undefined): TenantDb;
61
65
  export { asRawClient };
@@ -4,7 +4,7 @@ import { asEntityTableMeta, asRawClient, aggregateWhere as bunAggregateWhere, co
4
4
  import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
5
5
  import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
6
6
  import { emitDbQuery, fallbackEscapeHatchReporter, registerStandardMetrics, } from "../observability/index.js";
7
- import { executorManagedWriteDenied, registerExecutorManagedWriteRunner, } from "./executor-managed-tables.js";
7
+ import { assertWritableTable, executorManagedWriteDenied, hasExecutorManagedWriteCapability, registerExecutorManagedWriteRunner, } from "./executor-managed-tables.js";
8
8
  import { bindProjectionRegistry, projectionRegistryOf } from "./projection-registry-binding.js";
9
9
  import { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
10
10
  import { queryDatabaseNow } from "./queries/database-now.js";
@@ -73,12 +73,17 @@ const capableRunnerByInner = new WeakMap();
73
73
  // Framework-private (not re-exported from db/index.ts): the only way to obtain a runner that may
74
74
  // write executor-managed tables. Only the framework's own event-apply paths call it.
75
75
  export function withExecutorManagedWrites(runner) {
76
- if (capableRunnerInners.has(runner))
76
+ if (hasExecutorManagedWriteCapability(runner))
77
77
  return runner;
78
78
  const cached = capableRunnerByInner.get(runner);
79
79
  if (cached)
80
80
  return cached;
81
81
  const capable = boundRunner(runner, runnerPersonalDataGates.get(runner), projectionRegistryOf(runner), true);
82
+ if (!hasExecutorManagedWriteCapability(capable)) {
83
+ throw new InternalError({
84
+ message: "boundRunner did not register the executor write capability",
85
+ });
86
+ }
82
87
  capableRunnerByInner.set(runner, capable);
83
88
  return capable;
84
89
  }
@@ -353,7 +358,7 @@ export function createTenantDb(options) {
353
358
  }
354
359
  // Tenant-destroy hooks purge their own tenant's event-fed rows; the pinned tenant is the only reach and unsafeRaw stays without the write capability.
355
360
  export function createTenantDataDestroyDb(options) {
356
- return buildTenantDb({ ...options, db: withExecutorManagedWrites(options.db), mode: "tenant" });
361
+ return createTenantDb({ ...options, db: withExecutorManagedWrites(options.db), mode: "tenant" });
357
362
  }
358
363
  // Untyped JS callers or an `as` cast can still hand in undefined; a scoped handle
359
364
  // without a tenant would bind `tenant_id = NULL` instead of failing loudly.
@@ -488,6 +493,26 @@ function buildTenantDb(options) {
488
493
  "use db.global(table) with escapeHatch instead.",
489
494
  });
490
495
  }
496
+ // Callers ran executorManagedWriteDenied first: either db holds the capability or the table is
497
+ // unmanaged. The split only proves to the compiler which overload of the free writers applies.
498
+ function insertRow(table, values) {
499
+ if (hasExecutorManagedWriteCapability(db))
500
+ return bunInsertOne(db, table, values);
501
+ assertWritableTable(table);
502
+ return bunInsertOne(db, table, values);
503
+ }
504
+ function updateRows(table, set, where) {
505
+ if (hasExecutorManagedWriteCapability(db))
506
+ return bunUpdateMany(db, table, set, where);
507
+ assertWritableTable(table);
508
+ return bunUpdateMany(db, table, set, where);
509
+ }
510
+ function deleteRows(table, where) {
511
+ if (hasExecutorManagedWriteCapability(db))
512
+ return bunDeleteMany(db, table, where);
513
+ assertWritableTable(table);
514
+ return bunDeleteMany(db, table, where);
515
+ }
491
516
  function globalTable(table) {
492
517
  const meta = asEntityTableMeta(table);
493
518
  if (meta?.tenancy !== "global") {
@@ -511,7 +536,7 @@ function buildTenantDb(options) {
511
536
  if (denied)
512
537
  return Promise.reject(denied);
513
538
  report("global-write", globalWriteReason());
514
- return withDbSpan("insert", table, async () => bunInsertOne(db, table, values));
539
+ return withDbSpan("insert", table, async () => insertRow(table, values));
515
540
  },
516
541
  updateMany(set, where) {
517
542
  const denied = writesDenied("ctx.db.global().updateMany") ??
@@ -525,7 +550,7 @@ function buildTenantDb(options) {
525
550
  return Promise.reject(new Error("db.global().updateMany without where would mass-update every tenant's rows. Pass at least one where condition."));
526
551
  }
527
552
  report("global-write", globalWriteReason());
528
- return withDbSpan("update", table, async () => bunUpdateMany(db, table, set, where));
553
+ return withDbSpan("update", table, async () => updateRows(table, set, where));
529
554
  },
530
555
  deleteMany(where) {
531
556
  const denied = writesDenied("ctx.db.global().deleteMany") ??
@@ -538,10 +563,10 @@ function buildTenantDb(options) {
538
563
  }
539
564
  report("global-write", globalWriteReason());
540
565
  return withDbSpan("delete", table, async () => {
541
- await bunDeleteMany(db, table, where);
566
+ await deleteRows(table, where);
542
567
  });
543
568
  },
544
- // @cast-boundary phantom-brand — GlobalWrites is only present in the type when TTable is not ExecutorOnly; the object above always carries the methods.
569
+ // @cast-boundary phantom-brand — GlobalWrites is only present in the type when TTable carries Writable; the object above always carries the methods.
545
570
  };
546
571
  }
547
572
  function grantedUnsafeRawRunner(declaredStepReason) {
@@ -605,7 +630,7 @@ function buildTenantDb(options) {
605
630
  if (managedDenied)
606
631
  return Promise.reject(managedDenied);
607
632
  const data = insertValues(table, values);
608
- return withDbSpan("insert", table, async () => bunInsertOne(db, table, data));
633
+ return withDbSpan("insert", table, async () => insertRow(table, data));
609
634
  },
610
635
  updateMany(table, set, where) {
611
636
  const writeDenied = writesDenied("ctx.db.updateMany");
@@ -624,7 +649,7 @@ function buildTenantDb(options) {
624
649
  if (managedDenied)
625
650
  return Promise.reject(managedDenied);
626
651
  const filter = writeWhere(table, where);
627
- return withDbSpan("update", table, async () => bunUpdateMany(db, table, set, filter));
652
+ return withDbSpan("update", table, async () => updateRows(table, set, filter));
628
653
  },
629
654
  deleteMany(table, where) {
630
655
  const writeDenied = writesDenied("ctx.db.deleteMany");
@@ -640,7 +665,7 @@ function buildTenantDb(options) {
640
665
  if (managedDenied)
641
666
  return Promise.reject(managedDenied);
642
667
  const filter = writeWhere(table, where);
643
- return withDbSpan("delete", table, async () => bunDeleteMany(db, table, filter));
668
+ return withDbSpan("delete", table, async () => deleteRows(table, filter));
644
669
  },
645
670
  };
646
671
  declaredUnsafeRawRunners.set(tenantDb, grantedUnsafeRawRunner);
@@ -1,9 +1,9 @@
1
1
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
2
- import type { TenantDb } from "../../db/tenant-db.ts";
2
+ import type { ExecutorManagedTenantDb } from "../../db/tenant-db.ts";
3
3
  import type { Registry, TenantId } from "../types/index.ts";
4
4
  import type { TenantDestroyHookResult } from "./tenant-resource.ts";
5
5
  export interface TenantDataHookCtx {
6
- readonly db: TenantDb;
6
+ readonly db: ExecutorManagedTenantDb;
7
7
  readonly registry: Registry;
8
8
  readonly tenantId: TenantId;
9
9
  readonly deadlineAt?: number;
@@ -1,7 +1,7 @@
1
- import type { DbRunner } from "../db/connection.ts";
1
+ import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
2
2
  import type { StoredEvent } from "../event-store/event-store.ts";
3
3
  import type { MultiStreamApplyContext } from "../pipeline/multi-stream-apply-context.ts";
4
4
  import type { MultiStreamApplyFn, ProjectionTable, SingleStreamApplyFn } from "./types/projection.ts";
5
- export declare function defineApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx: DbRunner, table: ProjectionTable) => Promise<void>): SingleStreamApplyFn;
6
- export declare function defineMspApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx: DbRunner, ctx: MultiStreamApplyContext) => Promise<void>): MultiStreamApplyFn;
5
+ export declare function defineApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx: ExecutorManagedRunner, table: ProjectionTable) => Promise<void>): SingleStreamApplyFn;
6
+ export declare function defineMspApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx: ExecutorManagedRunner, ctx: MultiStreamApplyContext) => Promise<void>): MultiStreamApplyFn;
7
7
  export declare function setFields(table: ProjectionTable, fields: Record<string, unknown> | ((event: Parameters<SingleStreamApplyFn>[0]) => Record<string, unknown>)): SingleStreamApplyFn;
@@ -16,6 +16,7 @@
16
16
  // must commit in the same TX as the aggregate-mutation that triggered
17
17
  // it (stronger consistency than an async projection). Reviewer judges.
18
18
  import { deleteMany } from "../../db/query.js";
19
+ import { requireEntityTableMeta } from "../../db/table-introspection.js";
19
20
  import { withExecutorManagedWrites } from "../../db/tenant-db.js";
20
21
  import { tenantDbWriteRunner } from "../../db/tenant-db-runner.js";
21
22
  import { defineStep } from "../define-step.js";
@@ -27,8 +28,8 @@ defineStep({
27
28
  const where = resolveRequired(args.where, ctx);
28
29
  // The `unsafe*`-step IS the sanctioned direct projection write — it exists
29
30
  // to bypass the executor deliberately (boot-validated allowlist + reviewer
30
- // gate). The brand-strip cast makes that explicit at the one blessed seam.
31
- await deleteMany(withExecutorManagedWrites(tenantDbWriteRunner(ctx.db, "unsafe projection delete")), args.table, where);
31
+ // gate).
32
+ await deleteMany(withExecutorManagedWrites(tenantDbWriteRunner(ctx.db, "unsafe projection delete")), requireEntityTableMeta(args.table, "unsafeProjectionDelete"), where);
32
33
  },
33
34
  });
34
35
  export function buildUnsafeProjectionDeleteStep(args) {
@@ -1,5 +1,5 @@
1
1
  import { type DbConnection } from "../db/index.ts";
2
2
  export type EsOperationType = "seed-migration";
3
3
  export type EsOperationAppliedBy = "boot" | "cli" | "ci-pipeline";
4
- export declare const esOperationsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
4
+ export declare const esOperationsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
5
5
  export declare function createEsOperationsTable(db: DbConnection): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import type { DbConnection, DbRunner } from "../db/connection.ts";
2
2
  import type { TenantId } from "../engine/types/index.ts";
3
- export declare const archivedStreamsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
3
+ export declare const archivedStreamsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
4
4
  export declare function createArchivedStreamsTable(db: DbConnection): Promise<void>;
5
5
  export type ArchiveStreamArgs = {
6
6
  readonly tenantId: TenantId;
@@ -1,3 +1,3 @@
1
1
  import { type DbConnection } from "../db/index.ts";
2
- export declare const eventsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
2
+ export declare const eventsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
3
3
  export declare function createEventsTable(db: DbConnection): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import type { DbConnection, DbRunner } from "../db/connection.ts";
2
2
  import type { StoredEvent } from "./event-store.ts";
3
- export declare const rebuildDeadLetterTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
3
+ export declare const rebuildDeadLetterTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
4
4
  export declare function createRebuildDeadLetterTable(db: DbConnection): Promise<void>;
5
5
  export type SkippedApply = {
6
6
  readonly event: StoredEvent;
@@ -2,7 +2,7 @@
2
2
  import type { LoadAggregateWithSnapshotOptions, LoadAggregateWithSnapshotResult, SnapshotReducer } from "@cosmicdrift/kumiko-types/snapshot-types";
3
3
  import type { DbConnection, DbRunner } from "../db/connection.ts";
4
4
  import type { TenantId } from "../engine/types/index.ts";
5
- export declare const snapshotsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
5
+ export declare const snapshotsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
6
6
  export declare function createSnapshotsTable(db: DbConnection): Promise<void>;
7
7
  export declare function effectiveSnapshotGeneration(callerVersion: number, entityEventVersion: number | undefined): number;
8
8
  export type Snapshot<TState extends Record<string, unknown> = Record<string, unknown>> = {
@@ -1,6 +1,6 @@
1
1
  import type { DbConnection, DbRunner } from "../db/connection.ts";
2
2
  import type { StoredEvent } from "./event-store.ts";
3
- export declare const upcasterDeadLetterTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
3
+ export declare const upcasterDeadLetterTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
4
4
  export declare function createUpcasterDeadLetterTable(db: DbConnection): Promise<void>;
5
5
  export declare function recordUpcasterDeadLetter(db: DbRunner, args: {
6
6
  event: StoredEvent;
@@ -7,7 +7,7 @@ export declare function fileRefStorageDelta(event: {
7
7
  readonly bytes: number;
8
8
  readonly files: number;
9
9
  } | null;
10
- export declare const tenantStorageUsageTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
10
+ export declare const tenantStorageUsageTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
11
11
  export declare const filesStorageTrackingFeature: import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
12
12
  readonly exports: void;
13
13
  };
@@ -1,10 +1,10 @@
1
+ import type { WritableTable } from "@cosmicdrift/kumiko-types/executor-brand";
1
2
  import type { FileProviderResolver, FileUploadLedger } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
2
3
  import type { FileStorageProvider } from "@cosmicdrift/kumiko-types/file-storage-provider-types";
3
4
  import type { TenantId } from "@cosmicdrift/kumiko-types/identifiers";
4
5
  import type { DbConnection } from "../db/connection.ts";
5
- import { type EntityTableMeta } from "../db/entity-table-meta.ts";
6
6
  import { type TenantDb } from "../db/tenant-db.ts";
7
- export declare const fileUploadLedgerTable: EntityTableMeta;
7
+ export declare const fileUploadLedgerTable: WritableTable;
8
8
  export declare function createFileUploadLedger(db: DbConnection | TenantDb, tenantId: TenantId): FileUploadLedger;
9
9
  export declare function withUploadLedger(resolver: (tenantId: TenantId) => Promise<FileStorageProvider>, db: DbConnection | TenantDb): FileProviderResolver;
10
10
  export declare function takeUploadLedgerEntry(txDb: TenantDb, storageKey: string): Promise<boolean>;
@@ -1,6 +1,11 @@
1
+ import type { DbRunner } from "@cosmicdrift/kumiko-types/db-connection";
2
+ import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
1
3
  import type { AnyDb } from "../db/query.ts";
4
+ import { type ExecutorManagedTenantDb, type TenantDb } from "../db/tenant-db.ts";
2
5
  export { resetBlindIndexKeyForTests } from "../crypto/blind-index.ts";
3
6
  export { resetEventPiiCatalogForTests } from "../crypto/event-pii.ts";
4
7
  export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.ts";
5
8
  export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.ts";
6
- export declare function withExecutorManagedWritesForTests(db: AnyDb): AnyDb;
9
+ export declare function withExecutorManagedWritesForTests(db: TenantDb): ExecutorManagedTenantDb;
10
+ export declare function withExecutorManagedWritesForTests(db: DbRunner): ExecutorManagedRunner;
11
+ export declare function withExecutorManagedWritesForTests(db: AnyDb): ExecutorManagedTenantDb | ExecutorManagedRunner;
@@ -1,10 +1,9 @@
1
- import { createTenantDb, withExecutorManagedWrites } from "../db/tenant-db.js";
1
+ import { createTenantDb, withExecutorManagedWrites, } from "../db/tenant-db.js";
2
2
  import { isBoundTenantDb, isDbRunner, tenantDbRunner } from "../db/tenant-db-runner.js";
3
3
  export { resetBlindIndexKeyForTests } from "../crypto/blind-index.js";
4
4
  export { resetEventPiiCatalogForTests } from "../crypto/event-pii.js";
5
5
  export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.js";
6
6
  export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.js";
7
- // Seeds and teardown write read-model rows without events; the capability is the sanctioned test seam.
8
7
  export function withExecutorManagedWritesForTests(db) {
9
8
  if (isBoundTenantDb(db)) {
10
9
  return createTenantDb({
@@ -3,7 +3,7 @@ import type { Registry } from "../engine/types/index.ts";
3
3
  import type { JobRunner } from "../jobs/index.ts";
4
4
  import type { Logger } from "../logging/types.ts";
5
5
  import { type RebuildResult } from "../pipeline/index.ts";
6
- export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
6
+ export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
7
7
  export declare function createPendingRebuildsTable(db: DbConnection): Promise<void>;
8
8
  /** Liest die rebuild-Marker der frisch applizierten Migrations und queued
9
9
  * die betroffenen Tabellen. Upsert: ein bereits pending-er Tisch behält
@@ -2,7 +2,7 @@ import type { DbConnection } from "../db/connection.ts";
2
2
  import { type PiiBackfillFailure } from "../db/queries/backfill-pii.ts";
3
3
  import type { Registry } from "../engine/types/index.ts";
4
4
  import type { Logger } from "../logging/types.ts";
5
- export declare const piiBackfillStateTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
5
+ export declare const piiBackfillStateTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
6
6
  export declare function createPiiBackfillStateTable(db: DbConnection): Promise<void>;
7
7
  export declare function piiAnnotationEntries(registry: Registry): readonly string[];
8
8
  export declare function piiAnnotationFingerprint(registry: Registry): string;
@@ -5,7 +5,7 @@ export type PendingGapEntry = {
5
5
  readonly to: string;
6
6
  readonly xmax: string;
7
7
  };
8
- export declare const eventConsumerStateTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
8
+ export declare const eventConsumerStateTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
9
9
  export declare const ConsumerStatuses: {
10
10
  readonly idle: "idle";
11
11
  readonly processing: "processing";
@@ -1,5 +1,5 @@
1
1
  import type { DbConnection } from "../db/connection.ts";
2
- export declare const projectionStateTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
2
+ export declare const projectionStateTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
3
3
  export declare const ProjectionStatuses: {
4
4
  readonly idle: "idle";
5
5
  readonly rebuilding: "rebuilding";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.379.0",
3
+ "version": "0.381.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -199,7 +199,7 @@
199
199
  "./package.json": "./package.json"
200
200
  },
201
201
  "dependencies": {
202
- "@cosmicdrift/kumiko-types": "0.379.0",
202
+ "@cosmicdrift/kumiko-types": "0.381.0",
203
203
  "bullmq": "^5.76.7",
204
204
  "bun-types": "^1.3.13",
205
205
  "hono": "^4.13.1",
@@ -215,7 +215,7 @@
215
215
  "zod": "^4.4.3"
216
216
  },
217
217
  "devDependencies": {
218
- "@cosmicdrift/kumiko-testing": "0.379.0",
218
+ "@cosmicdrift/kumiko-testing": "0.381.0",
219
219
  "bun-types": "^1.3.13",
220
220
  "pino-pretty": "^13.1.3"
221
221
  },
package/src/changes.json CHANGED
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.380.0",
4
+ "type": "breaking",
5
+ "title": "Writes compile only for Writable tables or capable runners",
6
+ "detail": "The negative ExecutorOnly brand is replaced by positive brands. table(), defineUnmanagedTable and deriveEntityTableMeta with source \"unmanaged\" return Writable tables; buildEntityTable and r.projection tables do not. insertOne, updateMany, deleteMany, the upsert and counter helpers and the TenantDb and db.global() write methods accept a Writable table, or any table when the runner is an ExecutorManagedRunner or ExecutorManagedTenantDb. Widening a table to SchemaTable or EntityTableMeta drops the brand, so erased tables no longer compile. Projection apply functions receive tx as ExecutorManagedRunner. The runtime lock is unchanged.",
7
+ "migration": "Apply functions that annotate `tx` as `DbRunner`, inline or standalone, type it as `ExecutorManagedRunner` (or drop the annotation / annotate the function with SingleStreamApplyFn/MultiStreamApplyFn). Own write helpers take `WritableTable` for direct-write tables, or `ExecutorManagedRunner` / `ExecutorManagedTenantDb` when they write event-fed tables from an apply, rebuild or tenant-destroy context, instead of `SchemaTable`/`EntityTableMeta`. Tables annotated as `SchemaTable` lose the `Writable` brand; drop the annotation or add `& Writable`. Tenant-destroy hooks keep using the db they receive. Affects kumiko-enterprise, solon, money-horse, kumiko-studio, publicstatus."
8
+ },
2
9
  {
3
10
  "version": "0.379.0",
4
11
  "type": "improvement",