@cosmicdrift/kumiko-framework 0.378.0 → 0.380.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 (116) hide show
  1. package/AGENTS.md +4 -1
  2. package/agents/examples/compose-stacks.md +1 -2
  3. package/agents/examples/currencies-global.md +2 -3
  4. package/agents/examples/embedded.md +23 -37
  5. package/agents/examples/event-sourcing.md +6 -5
  6. package/agents/examples/legal-pages.md +5 -5
  7. package/agents/examples/reference-data.md +26 -21
  8. package/agents/examples/relations.md +2 -1
  9. package/agents/features.md +4 -4
  10. package/dist/api/entity-read-access.d.ts +2 -0
  11. package/dist/api/entity-read-access.js +27 -0
  12. package/dist/api/index.d.ts +1 -0
  13. package/dist/api/index.js +1 -0
  14. package/dist/api/server.js +10 -3
  15. package/dist/api/sse-route.d.ts +5 -0
  16. package/dist/api/sse-route.js +59 -9
  17. package/dist/db/apply-entity-event.d.ts +3 -2
  18. package/dist/db/apply-entity-event.js +1 -1
  19. package/dist/db/dialect.d.ts +2 -1
  20. package/dist/db/dialect.js +2 -2
  21. package/dist/db/entity-table-from-registry.js +0 -2
  22. package/dist/db/entity-table-meta.d.ts +5 -3
  23. package/dist/db/entity-table-meta.js +6 -15
  24. package/dist/db/event-store-executor-write.js +21 -16
  25. package/dist/db/executor-managed-tables.d.ts +8 -0
  26. package/dist/db/executor-managed-tables.js +48 -0
  27. package/dist/db/index.d.ts +6 -4
  28. package/dist/db/index.js +4 -2
  29. package/dist/db/money.d.ts +1 -1
  30. package/dist/db/queries/backfill-pii.js +22 -12
  31. package/dist/db/queries/database-now.d.ts +5 -0
  32. package/dist/db/queries/database-now.js +12 -0
  33. package/dist/db/queries/event-store-orphan-rows.d.ts +8 -0
  34. package/dist/db/queries/event-store-orphan-rows.js +32 -0
  35. package/dist/db/query.d.ts +22 -11
  36. package/dist/db/query.js +41 -36
  37. package/dist/db/reference-data.d.ts +5 -4
  38. package/dist/db/reference-data.js +23 -21
  39. package/dist/db/table-builder.d.ts +2 -3
  40. package/dist/db/table-builder.js +2 -0
  41. package/dist/db/table-introspection.d.ts +2 -0
  42. package/dist/db/table-introspection.js +4 -0
  43. package/dist/db/tenant-db-runner.d.ts +2 -0
  44. package/dist/db/tenant-db-runner.js +7 -0
  45. package/dist/db/tenant-db.d.ts +9 -2
  46. package/dist/db/tenant-db.js +82 -25
  47. package/dist/engine/boot-validator/entity-handler.js +9 -4
  48. package/dist/engine/boot-validator/query-refs.js +8 -0
  49. package/dist/engine/boot-validator/screens.js +34 -9
  50. package/dist/engine/extensions/tenant-data.d.ts +2 -2
  51. package/dist/engine/feature-ui-extensions.js +4 -0
  52. package/dist/engine/projection-helpers.d.ts +3 -3
  53. package/dist/engine/registry-state.js +2 -0
  54. package/dist/engine/registry-validate.js +4 -1
  55. package/dist/engine/schema-builder.js +18 -4
  56. package/dist/engine/screen-helpers.d.ts +1 -0
  57. package/dist/engine/screen-helpers.js +3 -0
  58. package/dist/engine/soft-delete-cleanup.js +53 -24
  59. package/dist/engine/steps/unsafe-projection-delete.js +4 -2
  60. package/dist/entrypoint/index.d.ts +1 -3
  61. package/dist/errors/reasons.d.ts +1 -0
  62. package/dist/errors/reasons.js +4 -1
  63. package/dist/errors/serialize.d.ts +1 -0
  64. package/dist/errors/serialize.js +9 -5
  65. package/dist/errors/write-error-info.js +3 -2
  66. package/dist/errors/zod-bridge.js +7 -0
  67. package/dist/es-ops/operations-schema.d.ts +1 -1
  68. package/dist/event-store/archive.d.ts +1 -1
  69. package/dist/event-store/events-schema.d.ts +1 -1
  70. package/dist/event-store/index.d.ts +1 -0
  71. package/dist/event-store/index.js +1 -0
  72. package/dist/event-store/rebuild-dead-letter.d.ts +1 -1
  73. package/dist/event-store/snapshot.d.ts +1 -1
  74. package/dist/event-store/upcaster-dead-letter.d.ts +1 -1
  75. package/dist/files/action-form-upload.d.ts +16 -0
  76. package/dist/files/action-form-upload.js +45 -0
  77. package/dist/files/bind-pending-file-refs.d.ts +14 -0
  78. package/dist/files/bind-pending-file-refs.js +44 -4
  79. package/dist/files/create-file-ref.d.ts +3 -0
  80. package/dist/files/create-file-ref.js +41 -13
  81. package/dist/files/file-routes.js +29 -2
  82. package/dist/files/index.d.ts +2 -1
  83. package/dist/files/index.js +1 -0
  84. package/dist/files/purge-expired-file-ref-bytes.d.ts +3 -2
  85. package/dist/files/purge-expired-file-ref-bytes.js +11 -5
  86. package/dist/files/storage-tracking.d.ts +1 -1
  87. package/dist/files/upload-ledger.d.ts +2 -2
  88. package/dist/internal/testing.d.ts +7 -0
  89. package/dist/internal/testing.js +14 -1
  90. package/dist/migrations/pending-rebuilds.d.ts +1 -1
  91. package/dist/migrations/pii-event-backfill.d.ts +1 -1
  92. package/dist/migrations/pii-event-backfill.js +14 -7
  93. package/dist/observability/escape-hatch-report.d.ts +1 -1
  94. package/dist/observability/escape-hatch-report.js +14 -3
  95. package/dist/pipeline/cascade-handler.js +79 -10
  96. package/dist/pipeline/dispatch-shared.js +1 -9
  97. package/dist/pipeline/event-consumer-state.d.ts +1 -1
  98. package/dist/pipeline/msp-rebuild.js +5 -3
  99. package/dist/pipeline/projection-rebuild.js +4 -2
  100. package/dist/pipeline/projection-state.d.ts +1 -1
  101. package/dist/pipeline/projections-runner.js +2 -1
  102. package/dist/rate-limit/index.d.ts +1 -1
  103. package/dist/rate-limit/index.js +1 -1
  104. package/dist/rate-limit/middleware.d.ts +4 -0
  105. package/dist/rate-limit/middleware.js +5 -2
  106. package/dist/ui-types/index.d.ts +2 -2
  107. package/dist/ui-types/index.js +2 -2
  108. package/dist/ui-types/list-row-meta.d.ts +3 -0
  109. package/dist/ui-types/list-row-meta.js +12 -1
  110. package/kumiko-es-tables.json +6 -0
  111. package/package.json +4 -3
  112. package/src/changes.json +117 -0
  113. package/src/scripts/codemod/README.md +20 -0
  114. package/src/scripts/codemod/codemod-support.ts +1 -1
  115. package/src/scripts/codemod/feature-layout.ts +416 -0
  116. package/src/scripts/codemod/package-merge.ts +0 -11
@@ -0,0 +1,12 @@
1
+ import { executeRawQueryRead } from "./raw-sql.js";
2
+ // Called by createTenantDb with its inner runner; keeps raw SQL out of consumers.
3
+ export async function queryDatabaseNow(runner) {
4
+ const [row] = await executeRawQueryRead(runner, "SELECT now() AS now");
5
+ if (!row)
6
+ throw new Error("SELECT now() returned no row");
7
+ return row.now;
8
+ }
9
+ /** Postgres clock; inside a transaction `now()` is the transaction start, so repeated reads agree. */
10
+ export function readDatabaseNow(db) {
11
+ return db.databaseNow();
12
+ }
@@ -0,0 +1,8 @@
1
+ import type { TenantId } from "../../engine/types/index.ts";
2
+ import { type AnyDb } from "../query.ts";
3
+ export type DeleteRowsWithoutReplayableStreamArgs = {
4
+ readonly table: unknown;
5
+ readonly ids: readonly string[];
6
+ readonly tenantId?: TenantId;
7
+ };
8
+ export declare function deleteRowsWithoutReplayableStream(db: AnyDb, args: DeleteRowsWithoutReplayableStreamArgs): Promise<readonly string[]>;
@@ -0,0 +1,32 @@
1
+ import { asRawClient } from "../query.js";
2
+ import { tableNameOf } from "../table-introspection.js";
3
+ import { isBoundTenantDb, tenantDbWriteRunner } from "../tenant-db-runner.js";
4
+ import { toPostgresArrayLiteral } from "./postgres-array-literal.js";
5
+ import { quoteTableIdent } from "./table-ops.js";
6
+ // Deletes only rows a projection rebuild would not recreate: the row's stream has no events, or is archived.
7
+ // One statement, so a stream cannot gain events between check and delete.
8
+ export async function deleteRowsWithoutReplayableStream(db, args) {
9
+ if (args.ids.length === 0)
10
+ return [];
11
+ const tenantBound = isBoundTenantDb(db);
12
+ const tenantId = tenantBound ? db.tenantId : args.tenantId;
13
+ const runner = tenantBound ? tenantDbWriteRunner(db, "deleteRowsWithoutReplayableStream") : db;
14
+ const params = [toPostgresArrayLiteral(args.ids)];
15
+ let tenantFilter = "";
16
+ if (tenantId !== undefined) {
17
+ params.push(tenantId);
18
+ tenantFilter = `AND t."tenant_id" = $2`;
19
+ }
20
+ const rows = await asRawClient(runner).unsafe(`DELETE FROM ${quoteTableIdent(tableNameOf(args.table))} t
21
+ WHERE t."id" = ANY($1::uuid[]) ${tenantFilter}
22
+ AND NOT EXISTS (
23
+ SELECT 1 FROM "kumiko_events" e
24
+ WHERE e."tenant_id" = t."tenant_id" AND e."aggregate_id" = t."id"
25
+ AND NOT EXISTS (
26
+ SELECT 1 FROM "kumiko_archived_streams" a
27
+ WHERE a."tenant_id" = e."tenant_id" AND a."aggregate_id" = e."aggregate_id"
28
+ )
29
+ )
30
+ RETURNING t."id"`, params);
31
+ return rows.map((row) => row.id);
32
+ }
@@ -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
@@ -24,6 +24,7 @@ import { InternalError } from "../errors/index.js";
24
24
  import { createFallbackLogger } from "../logging/index.js";
25
25
  import { parseJsonSafe } from "../utils/safe-json.js";
26
26
  import { SQL_EXPR_BRAND } from "./dialect.js";
27
+ import { executorManagedWriteDenied } from "./executor-managed-tables.js";
27
28
  import { extractPgError } from "./pg-error.js";
28
29
  import { toSnakeCase } from "./table-builder.js";
29
30
  import { asEntityTableMeta, requireEntityTableMeta } from "./table-introspection.js";
@@ -77,42 +78,27 @@ export function asRawClient(db) {
77
78
  export function runnerCanBeginTransaction(db) {
78
79
  return hasBegin(asRawClient(db));
79
80
  }
80
- // postgres.js rejects the WHOLE begin() when any statement inside
81
- // it errored — even if the JS error was caught (the driver tracks per-block
82
- // query failures). Error confinement therefore must go through the driver's
83
- // own savepoint(), not manual SAVEPOINT statements. The callback receives
84
- // 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
+ }
85
85
  export async function runInSavepoint(tx, fn) {
86
86
  const raw = asRawClient(tx);
87
87
  if (typeof raw.savepoint !== "function") {
88
88
  throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (postgres.js TransactionSql).");
89
89
  }
90
- return raw.savepoint(fn);
90
+ return raw.savepoint((sp) => fn(savepointRunnerOf(sp)));
91
91
  }
92
- // Same error-confinement as runInSavepoint, but for call sites that don't
93
- // know whether `db` is a bare pool connection or an active transaction
94
- // (e.g. the CRUD executor, invoked both from a dispatcher tx and directly
95
- // against the pool by seeds/tests). A pool connection has no savepoint() —
96
- // and doesn't need one, since each statement there is its own auto-committed
97
- // unit and a failed statement can't poison anything downstream.
98
- //
99
- // `.savepoint` being present isn't proof the transaction is still open: a
100
- // committed TransactionSql-shaped db still exposes `.savepoint`, and calling
101
- // it fails with PG 25P01 ("no active sql transaction") since there's no
102
- // BEGIN left to nest into. That is now a programming error, not something
103
- // to paper over by re-running `fn` without the savepoint's isolation.
104
92
  export async function runInSavepointIfSupported(db, fn) {
105
93
  const raw = asRawClient(db);
106
- // @cast-boundary db-runner savepoint handle → DbRunner at the single seam
107
- const asRunner = (sp) => sp;
108
94
  if (typeof raw.savepoint !== "function")
109
- return fn(asRunner(db));
95
+ return fn(savepointRunnerOf(db));
110
96
  // An error from inside fn must never be remapped, even if it carries a closed-connection code.
111
97
  let fnStarted = false;
112
98
  try {
113
99
  return await raw.savepoint((sp) => {
114
100
  fnStarted = true;
115
- return fn(asRunner(sp));
101
+ return fn(savepointRunnerOf(sp));
116
102
  });
117
103
  }
118
104
  catch (e) {
@@ -869,7 +855,6 @@ function selectColumnList(info, fields) {
869
855
  })
870
856
  .join(", ");
871
857
  }
872
- // biome-ignore lint/suspicious/noExplicitAny: opt-in default loosens row type for unannotated test fixtures
873
858
  export async function selectMany(db, table, where, options) {
874
859
  const scoped = tenantDbDelegate(db);
875
860
  if (scoped) {
@@ -904,7 +889,6 @@ export async function selectMany(db, table, where, options) {
904
889
  const raw = await unsafeReadRetrying(db, sqlText, values);
905
890
  return coerceRows(raw, info);
906
891
  }
907
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
908
892
  export async function fetchOne(db, table, where) {
909
893
  const rows = await selectMany(db, table, where, { limit: 1 });
910
894
  return rows[0];
@@ -979,14 +963,13 @@ export async function selectInnerJoin(db, spec) {
979
963
  };
980
964
  });
981
965
  }
982
- // Bulk INSERT — same shape as insertOne but takes an array of rows and
983
- // produces one multi-VALUES statement. Mirrors drizzle's
984
- // `db.insert(t).values(rows[])`. Empty input is a no-op.
985
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
986
966
  export async function insertMany(db, table, rows) {
987
967
  if (rows.length === 0)
988
968
  return [];
989
969
  assertNotTenantScoped(db, "insertMany");
970
+ const managedDenied = executorManagedWriteDenied(db, table, "insertMany");
971
+ if (managedDenied)
972
+ throw managedDenied;
990
973
  const info = extractTableInfo(table);
991
974
  // Use the column-set from the first row; assume all rows share keys.
992
975
  const firstRow = rows[0];
@@ -1022,12 +1005,14 @@ export async function insertMany(db, table, rows) {
1022
1005
  });
1023
1006
  return coerceRows(raw, info);
1024
1007
  }
1025
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1026
1008
  export async function insertOne(db, table, values) {
1027
1009
  const scoped = tenantDbDelegate(db);
1028
1010
  if (scoped) {
1029
1011
  return scoped.insertOne(table, values);
1030
1012
  }
1013
+ const managedDenied = executorManagedWriteDenied(db, table, "insertOne");
1014
+ if (managedDenied)
1015
+ throw managedDenied;
1031
1016
  const info = extractTableInfo(table);
1032
1017
  const entries = Object.entries(values)
1033
1018
  .filter(([k]) => info.hasColumn(k))
@@ -1058,12 +1043,14 @@ export async function insertOne(db, table, values) {
1058
1043
  return undefined;
1059
1044
  return coerceRow(first, info);
1060
1045
  }
1061
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1062
1046
  export async function updateMany(db, table, set, where) {
1063
1047
  const scoped = tenantDbDelegate(db);
1064
1048
  if (scoped) {
1065
1049
  return scoped.updateMany(table, set, where);
1066
1050
  }
1051
+ const managedDenied = executorManagedWriteDenied(db, table, "updateMany");
1052
+ if (managedDenied)
1053
+ throw managedDenied;
1067
1054
  const info = extractTableInfo(table);
1068
1055
  const setEntries = Object.entries(set).map(([k, v]) => {
1069
1056
  const col = info.columnOf(k);
@@ -1103,6 +1090,9 @@ export async function deleteMany(db, table, where) {
1103
1090
  if (scoped) {
1104
1091
  return scoped.deleteMany(table, where);
1105
1092
  }
1093
+ const managedDenied = executorManagedWriteDenied(db, table, "deleteMany");
1094
+ if (managedDenied)
1095
+ throw managedDenied;
1106
1096
  const info = extractTableInfo(table);
1107
1097
  const w = buildWhereClause(info, where, 1);
1108
1098
  let sqlText = `DELETE FROM ${quoteIdent(info.name)}`;
@@ -1277,9 +1267,15 @@ export async function aggregateWhere(db, table, spec, where = {}) {
1277
1267
  };
1278
1268
  });
1279
1269
  }
1280
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1281
- 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) {
1282
1275
  assertNotTenantScoped(db, "upsertOnConflict");
1276
+ const managedDenied = executorManagedWriteDenied(db, table, "upsertOnConflict");
1277
+ if (managedDenied)
1278
+ throw managedDenied;
1283
1279
  const info = extractTableInfo(table);
1284
1280
  const entries = insertEntries(info, values);
1285
1281
  const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
@@ -1321,19 +1317,23 @@ export async function upsertOnConflict(db, table, values, options) {
1321
1317
  return undefined;
1322
1318
  return coerceRow(first, info);
1323
1319
  }
1324
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1325
1320
  export async function upsertByPk(db, table, values, updateOnConflict) {
1321
+ const managedDenied = executorManagedWriteDenied(db, table, "upsertByPk");
1322
+ if (managedDenied)
1323
+ throw managedDenied;
1326
1324
  const info = extractTableInfo(table);
1327
1325
  const conflictKeys = resolveConflictColumns(table, info, undefined);
1328
1326
  const fieldKeys = conflictKeys.map((col) => info.fieldOf(col));
1329
- return upsertOnConflict(db, table, values, {
1327
+ return upsertOnConflictChecked(db, table, values, {
1330
1328
  conflictKeys: fieldKeys,
1331
1329
  ...(updateOnConflict !== undefined ? { update: updateOnConflict } : {}),
1332
1330
  });
1333
1331
  }
1334
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1335
1332
  export async function insertOnConflictDoNothing(db, table, values, options) {
1336
1333
  assertNotTenantScoped(db, "insertOnConflictDoNothing");
1334
+ const managedDenied = executorManagedWriteDenied(db, table, "insertOnConflictDoNothing");
1335
+ if (managedDenied)
1336
+ throw managedDenied;
1337
1337
  const info = extractTableInfo(table);
1338
1338
  const entries = insertEntries(info, values);
1339
1339
  const conflictCols = resolveConflictColumns(table, info, options?.conflictKeys);
@@ -1349,9 +1349,11 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
1349
1349
  return undefined;
1350
1350
  return coerceRow(first, info);
1351
1351
  }
1352
- // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
1353
1352
  export async function incrementCounter(db, table, values, increments, options = {}) {
1354
1353
  assertNotTenantScoped(db, "incrementCounter");
1354
+ const managedDenied = executorManagedWriteDenied(db, table, "incrementCounter");
1355
+ if (managedDenied)
1356
+ throw managedDenied;
1355
1357
  const info = extractTableInfo(table);
1356
1358
  const entries = insertEntries(info, values);
1357
1359
  const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
@@ -1402,6 +1404,9 @@ export async function incrementCounter(db, table, values, increments, options =
1402
1404
  return coerceRow(first, info);
1403
1405
  }
1404
1406
  export async function deleteManyBatched(db, table, where, options) {
1407
+ const managedDenied = executorManagedWriteDenied(db, table, "deleteManyBatched");
1408
+ if (managedDenied)
1409
+ throw managedDenied;
1405
1410
  const info = extractTableInfo(table);
1406
1411
  const w = buildWhereClause(info, where, 1);
1407
1412
  if (!w.sqlText) {
@@ -1,14 +1,15 @@
1
+ import type { Registry } from "../engine/index.ts";
1
2
  import type { ReferenceDataDef } from "../engine/types/index.ts";
2
3
  import type { DbConnection } from "./connection.ts";
3
- import type { TableColumns } from "./dialect.ts";
4
- type Table = TableColumns<any>;
5
4
  /**
6
5
  * Seed reference data at boot time.
7
6
  * For each ReferenceDataDef: upsert rows (insert missing, update changed, never delete).
8
7
  * Upsert key defaults to the first field in the data object.
8
+ *
9
+ * Rows go through the entity's event-store executor as the system user in the
10
+ * SYSTEM_TENANT_ID stream, so the seed is replayable on a projection rebuild.
9
11
  */
10
- export declare function seedReferenceData(defs: readonly ReferenceDataDef[], tables: ReadonlyMap<string, Table>, db: DbConnection): Promise<{
12
+ export declare function seedReferenceData(defs: readonly ReferenceDataDef[], registry: Registry, db: DbConnection): Promise<{
11
13
  inserted: number;
12
14
  updated: number;
13
15
  }>;
14
- export {};
@@ -1,21 +1,26 @@
1
- import { fetchOne, insertOne, updateMany } from "../db/query.js";
1
+ import { fetchOne } from "../db/query.js";
2
+ import { createSystemUser } from "../engine/system-user.js";
2
3
  import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
3
- import { tableColumnsOf } from "./table-introspection.js";
4
- function hasColumn(table, field) {
5
- const cols = tableColumnsOf(table);
6
- return cols !== undefined && field in cols;
7
- }
4
+ import { reraiseAsKumikoError } from "../errors/index.js";
5
+ import { entityTableFromRegistry } from "./entity-table-from-registry.js";
6
+ import { createEventStoreExecutor } from "./event-store-executor.js";
7
+ import { createTenantDb } from "./tenant-db.js";
8
8
  /**
9
9
  * Seed reference data at boot time.
10
10
  * For each ReferenceDataDef: upsert rows (insert missing, update changed, never delete).
11
11
  * Upsert key defaults to the first field in the data object.
12
+ *
13
+ * Rows go through the entity's event-store executor as the system user in the
14
+ * SYSTEM_TENANT_ID stream, so the seed is replayable on a projection rebuild.
12
15
  */
13
- export async function seedReferenceData(defs, tables, db) {
16
+ export async function seedReferenceData(defs, registry, db) {
14
17
  let inserted = 0;
15
18
  let updated = 0;
19
+ const by = createSystemUser(SYSTEM_TENANT_ID);
20
+ const tdb = createTenantDb({ db, tenantId: SYSTEM_TENANT_ID, mode: "system" });
16
21
  for (const def of defs) {
17
- const table = tables.get(def.entityName);
18
- if (!table)
22
+ const entity = registry.getEntity(def.entityName);
23
+ if (!entity)
19
24
  continue;
20
25
  if (def.data.length === 0)
21
26
  continue;
@@ -26,11 +31,13 @@ export async function seedReferenceData(defs, tables, db) {
26
31
  if (!firstKey)
27
32
  continue;
28
33
  const upsertKey = def.upsertKey ?? firstKey;
34
+ const table = entityTableFromRegistry(registry, def.entityName, entity);
35
+ const executor = createEventStoreExecutor(table, entity, { entityName: def.entityName });
29
36
  for (const row of def.data) {
30
37
  const keyValue = row[upsertKey];
31
38
  if (keyValue === undefined)
32
39
  continue;
33
- const existing = (await fetchOne(db, table, { [upsertKey]: keyValue }));
40
+ const existing = await fetchOne(db, table, { [upsertKey]: keyValue });
34
41
  if (existing) {
35
42
  const changes = {};
36
43
  for (const [field, value] of Object.entries(row)) {
@@ -41,21 +48,16 @@ export async function seedReferenceData(defs, tables, db) {
41
48
  }
42
49
  }
43
50
  if (Object.keys(changes).length > 0) {
44
- await updateMany(db, table, changes, { [upsertKey]: keyValue });
51
+ const result = await executor.update({ id: existing.id, version: existing.version, changes }, by, tdb);
52
+ if (!result.isSuccess)
53
+ throw reraiseAsKumikoError(result.error);
45
54
  updated++;
46
55
  }
47
56
  }
48
57
  else {
49
- // Only add framework columns if the table actually has them.
50
- // Drizzle used to filter extra fields silently; bunInsertOne doesn't.
51
- const values = { ...row };
52
- if (hasColumn(table, "tenantId"))
53
- values["tenantId"] = SYSTEM_TENANT_ID;
54
- if (hasColumn(table, "version"))
55
- values["version"] = 1;
56
- if (hasColumn(table, "insertedAt"))
57
- values["insertedAt"] = Temporal.Now.instant();
58
- await insertOne(db, table, values);
58
+ const result = await executor.create({ ...row, tenantId: SYSTEM_TENANT_ID }, by, tdb);
59
+ if (!result.isSuccess)
60
+ throw reraiseAsKumikoError(result.error);
59
61
  inserted++;
60
62
  }
61
63
  }
@@ -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,6 +1,7 @@
1
1
  import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
2
2
  import { toSnakeCase } from "../utils/case.js";
3
3
  import { assertUnreachable } from "../utils/index.js";
4
+ import { markExecutorManagedTable } from "./executor-managed-tables.js";
4
5
  import { columnHandleName } from "./table-introspection.js";
5
6
  export { toSnakeCase } from "../utils/case.js";
6
7
  import { bigint, boolean, decimalColumn, doublePrecision, index, instant, integer, isSqlExpression, jsonb, moneyAmount, table as pgTable, plainDate, serial, sql, text, uniqueIndex, uuid, } from "./dialect.js";
@@ -420,5 +421,6 @@ export function buildEntityTable(entityName, entity, options) {
420
421
  return indexes;
421
422
  });
422
423
  stampEntityTableMeta(built, entity);
424
+ markExecutorManagedTable(built);
423
425
  return built;
424
426
  }
@@ -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,6 +1,8 @@
1
1
  import type { TenantDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
2
2
  import { InternalError } from "../errors/index.ts";
3
3
  import type { DbRunner } from "./connection.ts";
4
+ export declare function isBoundTenantDb(value: unknown): value is TenantDb;
5
+ export declare function isDbRunner(value: unknown): value is DbRunner;
4
6
  export declare function bindTenantDbRunner(tenantDb: TenantDb, runner: DbRunner): void;
5
7
  export declare function tenantDbRunner(tenantDb: TenantDb): DbRunner;
6
8
  export declare function denyTenantDbWrites(tenantDb: TenantDb, reason: string): void;
@@ -1,5 +1,12 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { InternalError } from "../errors/index.js";
2
3
  const tenantDbRunners = new WeakMap();
4
+ export function isBoundTenantDb(value) {
5
+ return typeof value === "object" && value !== null && tenantDbRunners.has(value);
6
+ }
7
+ export function isDbRunner(value) {
8
+ return typeof propertyOf(value, "unsafe") === "function";
9
+ }
3
10
  export function bindTenantDbRunner(tenantDb, runner) {
4
11
  tenantDbRunners.set(tenantDb, runner);
5
12
  }
@@ -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,7 +11,9 @@ 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 { 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;
16
+ export declare function withoutExecutorManagedWrites(runner: DbRunner): DbRunner;
14
17
  export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
15
18
  export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSystemDb, reason: string): DbRunner;
16
19
  export declare function withSystemDbUnsafeRawGrant(systemDb: UncheckedSystemDb, grant: EscapeHatchDeclaration | undefined, deniedCallerLabel: string): UncheckedSystemDb;
@@ -53,6 +56,10 @@ export type CreateTenantDbOptions = {
53
56
  readonly signal?: AbortSignal;
54
57
  readonly grants?: TenantDbGrants;
55
58
  };
59
+ export declare function createTenantDb(options: CreateTenantDbOptions & {
60
+ readonly db: ExecutorManagedRunner;
61
+ }): ExecutorManagedTenantDb;
56
62
  export declare function createTenantDb(options: CreateTenantDbOptions): TenantDb;
63
+ export declare function createTenantDataDestroyDb(options: Omit<CreateTenantDbOptions, "mode">): ExecutorManagedTenantDb;
57
64
  export declare function createProjectingTenantDb(db: DbRunner, tenantId: TenantId, projectionRegistry: Registry | undefined): TenantDb;
58
65
  export { asRawClient };