@cosmicdrift/kumiko-framework 0.388.0 → 0.389.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 (194) hide show
  1. package/AGENTS.md +3 -0
  2. package/agents/examples/golden-path.md +13 -1
  3. package/agents/examples/lifecycle-hooks.md +55 -2
  4. package/agents/examples/list-table-options.md +466 -0
  5. package/agents/examples/pdf-audit-export.md +4 -2
  6. package/agents/examples/state-machine.md +3 -2
  7. package/agents/examples/tenant-isolation.md +73 -1
  8. package/agents/examples.md +1 -0
  9. package/agents/widgets.md +1 -0
  10. package/dist/api/api-constants.d.ts +1 -0
  11. package/dist/api/api-constants.js +2 -0
  12. package/dist/api/auth-routes-shared.d.ts +2 -0
  13. package/dist/api/auth-routes-shared.js +5 -0
  14. package/dist/api/client-error-route.js +2 -3
  15. package/dist/api/entity-read-access.d.ts +2 -1
  16. package/dist/api/entity-read-access.js +14 -0
  17. package/dist/api/export/export-cells-reference.d.ts +9 -0
  18. package/dist/api/export/export-cells-reference.js +21 -0
  19. package/dist/api/export/export-cells.d.ts +11 -0
  20. package/dist/api/export/export-cells.js +136 -0
  21. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  22. package/dist/api/export/export-ciphertext-guard.js +27 -0
  23. package/dist/api/export/export-columns.d.ts +29 -0
  24. package/dist/api/export/export-columns.js +85 -0
  25. package/dist/api/export/export-errors.d.ts +4 -0
  26. package/dist/api/export/export-errors.js +14 -0
  27. package/dist/api/export/export-route.d.ts +4 -0
  28. package/dist/api/export/export-route.js +74 -0
  29. package/dist/api/export/export-rows.d.ts +13 -0
  30. package/dist/api/export/export-rows.js +62 -0
  31. package/dist/api/export/export-screen.d.ts +12 -0
  32. package/dist/api/export/export-screen.js +47 -0
  33. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  34. package/dist/api/export/export-xlsx-writer.js +21 -0
  35. package/dist/api/extra-route.d.ts +4 -0
  36. package/dist/api/pii-leak-guard.d.ts +3 -0
  37. package/dist/api/pii-leak-guard.js +3 -2
  38. package/dist/api/routes.d.ts +16 -0
  39. package/dist/api/routes.js +2 -2
  40. package/dist/api/server-extra-routes.js +3 -0
  41. package/dist/api/server-routes.d.ts +1 -1
  42. package/dist/api/server-routes.js +8 -2
  43. package/dist/api/server-wiring.js +2 -0
  44. package/dist/api/sse-route.d.ts +2 -1
  45. package/dist/api/sse-route.js +11 -1
  46. package/dist/config/index.d.ts +3 -1
  47. package/dist/crypto/pg-kms-adapter.d.ts +1 -0
  48. package/dist/crypto/pg-kms-adapter.js +12 -18
  49. package/dist/db/aggregate-sql.d.ts +2 -0
  50. package/dist/db/aggregate-sql.js +10 -0
  51. package/dist/db/closed-connection.js +12 -19
  52. package/dist/db/connection.d.ts +4 -5
  53. package/dist/db/connection.js +29 -20
  54. package/dist/db/driver-row-coercion.js +4 -4
  55. package/dist/db/driver-value-preparation.js +6 -4
  56. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  57. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  58. package/dist/db/event-store-executor-read-aggregate.js +36 -5
  59. package/dist/db/event-store-executor-read-filter.js +6 -3
  60. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  61. package/dist/db/event-store-executor-write-atomic.js +41 -0
  62. package/dist/db/event-store-executor-write-steps.d.ts +0 -3
  63. package/dist/db/event-store-executor-write-steps.js +0 -5
  64. package/dist/db/event-store-executor-write.js +15 -15
  65. package/dist/db/event-store-executor.js +2 -1
  66. package/dist/db/executor-managed-tables.js +1 -1
  67. package/dist/db/index.d.ts +1 -1
  68. package/dist/db/index.js +1 -1
  69. package/dist/db/migrate-runner.js +3 -3
  70. package/dist/db/pg-error.js +26 -21
  71. package/dist/db/queries/backfill-pii.js +3 -6
  72. package/dist/db/queries/database-now.js +2 -1
  73. package/dist/db/queries/shadow-swap.js +2 -3
  74. package/dist/db/query.d.ts +14 -0
  75. package/dist/db/query.js +68 -43
  76. package/dist/db/reference-data.js +14 -2
  77. package/dist/db/statement-timeout.js +2 -1
  78. package/dist/db/tenant-db-upsert.d.ts +5 -0
  79. package/dist/db/tenant-db-upsert.js +91 -0
  80. package/dist/db/tenant-db.js +11 -1
  81. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  82. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  83. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  84. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  85. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  86. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  87. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  88. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  89. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  90. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  91. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  92. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  93. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  94. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  95. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  96. package/dist/engine/boot-validator/list-table-options.js +77 -0
  97. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  98. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  99. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  100. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  101. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  102. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  103. package/dist/engine/boot-validator/screens-kind-entity-list.js +7 -0
  104. package/dist/engine/boot-validator/screens-kind-projection-list.js +4 -0
  105. package/dist/engine/boot-validator/screens-lists.js +12 -2
  106. package/dist/engine/build-app-schema.d.ts +2 -1
  107. package/dist/engine/build-app-schema.js +9 -1
  108. package/dist/engine/build-config-feature-schema.js +6 -1
  109. package/dist/engine/define-feature.js +4 -0
  110. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  111. package/dist/engine/feature-ast/extractors/index.js +1 -1
  112. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  113. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  114. package/dist/engine/feature-ast/index.d.ts +1 -1
  115. package/dist/engine/feature-ast/parse.js +3 -1
  116. package/dist/engine/feature-ast/patch.d.ts +2 -0
  117. package/dist/engine/feature-ast/patch.js +1 -0
  118. package/dist/engine/feature-ast/pattern-change-schema.d.ts +7 -245
  119. package/dist/engine/feature-ast/pattern-change-schema.js +9 -163
  120. package/dist/engine/feature-ast/pattern-id-matchers.js +1 -0
  121. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  122. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  123. package/dist/engine/feature-ast/patterns.d.ts +5 -1
  124. package/dist/engine/feature-ast/patterns.js +1 -0
  125. package/dist/engine/feature-ast/render.js +4 -0
  126. package/dist/engine/feature-builder-state.d.ts +1 -0
  127. package/dist/engine/feature-builder-state.js +1 -0
  128. package/dist/engine/feature-manifest.d.ts +1 -0
  129. package/dist/engine/feature-manifest.js +1 -0
  130. package/dist/engine/field-access.d.ts +1 -0
  131. package/dist/engine/field-access.js +12 -0
  132. package/dist/engine/index.d.ts +2 -2
  133. package/dist/engine/index.js +1 -1
  134. package/dist/engine/pattern-library/library.js +2 -1
  135. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  136. package/dist/engine/pattern-library/static-schemas.js +9 -0
  137. package/dist/engine/screen-filter-ops.js +2 -0
  138. package/dist/engine/strip-disabled-features-from-app-schema.js +7 -3
  139. package/dist/engine/types/feature.d.ts +1 -0
  140. package/dist/engine/types/index.d.ts +4 -3
  141. package/dist/engine/types/screen.d.ts +1 -1
  142. package/dist/engine/types/screen.js +1 -1
  143. package/dist/entrypoint/index.d.ts +2 -0
  144. package/dist/entrypoint/index.js +3 -0
  145. package/dist/event-store/event-store.d.ts +1 -0
  146. package/dist/event-store/event-store.js +7 -4
  147. package/dist/event-store/provenance-append.js +1 -1
  148. package/dist/files/create-file-ref-from-bytes.js +3 -0
  149. package/dist/files/create-file-ref.d.ts +3 -0
  150. package/dist/files/create-file-ref.js +11 -2
  151. package/dist/files/file-routes.d.ts +2 -0
  152. package/dist/files/file-routes.js +29 -2
  153. package/dist/files/provider-resolver.d.ts +1 -0
  154. package/dist/files/provider-resolver.js +1 -1
  155. package/dist/files/record-write-check.d.ts +6 -0
  156. package/dist/files/record-write-check.js +13 -0
  157. package/dist/files/upload-ledger.d.ts +2 -2
  158. package/dist/files/upload-ledger.js +4 -4
  159. package/dist/i18n/required-surface-keys.js +7 -2
  160. package/dist/internal/testing.js +1 -1
  161. package/dist/jobs/job-runner-scope.js +6 -6
  162. package/dist/jobs/job-runner.js +3 -0
  163. package/dist/observability/escape-hatch-report.js +9 -2
  164. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  165. package/dist/pipeline/after-rollback-sink.js +32 -0
  166. package/dist/pipeline/dispatch-batch.js +8 -1
  167. package/dist/pipeline/dispatch-shared.js +4 -0
  168. package/dist/pipeline/event-dispatcher-delivery.js +3 -1
  169. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  170. package/dist/pipeline/event-dispatcher.js +6 -9
  171. package/dist/pipeline/handler-context-dispatch.d.ts +2 -0
  172. package/dist/pipeline/handler-context-dispatch.js +13 -1
  173. package/dist/pipeline/handler-context-events.d.ts +2 -4
  174. package/dist/pipeline/handler-context-events.js +6 -4
  175. package/dist/pipeline/system-hooks.d.ts +3 -1
  176. package/dist/pipeline/system-hooks.js +55 -6
  177. package/dist/pipeline/write-result-masking.js +13 -10
  178. package/dist/schema-cli/baseline-command.js +1 -1
  179. package/dist/schema-cli/status-command.js +1 -1
  180. package/dist/search/reindex-entity.js +15 -5
  181. package/dist/ui-types/index.d.ts +2 -2
  182. package/dist/ui-types/index.js +1 -1
  183. package/dist/upgrade-changelog.d.ts +2 -0
  184. package/dist/upgrade-changelog.js +14 -0
  185. package/dist/upgrade-cli.d.ts +3 -12
  186. package/dist/upgrade-cli.js +22 -76
  187. package/dist/upgrade-marker-parse.d.ts +32 -0
  188. package/dist/upgrade-marker-parse.js +73 -0
  189. package/package.json +9 -6
  190. package/src/changes.json +245 -0
  191. package/src/scripts/codemod/codemod-support.ts +1 -1
  192. package/src/scripts/codemod/db-factory.ts +1 -1
  193. package/dist/engine/provider-plugin-context.d.ts +0 -3
  194. package/dist/engine/provider-plugin-context.js +0 -28
@@ -5,7 +5,7 @@ import { extractPgError } from "./pg-error.js";
5
5
  // A pool can briefly keep handing out connections the server just closed.
6
6
  // Each failed attempt discards one, so retries are bounded by pool size.
7
7
  const CLOSED_CONNECTION_CODES = new Set([
8
- "CONNECTION_CLOSED", // postgres-js
8
+ "ERR_POSTGRES_CONNECTION_CLOSED", // Bun.SQL
9
9
  "57P01", // PG SQLSTATE: admin_shutdown / terminated backend
10
10
  ]);
11
11
  export function isClosedConnectionError(err) {
@@ -21,40 +21,33 @@ export function isPooledClient(raw) {
21
21
  typeof propertyOf(raw, "savepoint") !== "function" &&
22
22
  typeof propertyOf(raw, "release") !== "function");
23
23
  }
24
- const closedConnectionLog = createFallbackLogger("postgres-js");
25
- // The driver rejects in-flight queries with this AbortError (no code) when the
26
- // pool closes under them, so isClosedConnectionError can't see it.
27
- const DRIVER_CLOSED_ABORT_MESSAGE = "The connection was closed.";
28
- function isDriverClosedAbort(err) {
29
- return (err instanceof Error && err.name === "AbortError" && err.message === DRIVER_CLOSED_ABORT_MESSAGE);
30
- }
24
+ const closedConnectionLog = createFallbackLogger("bun-sql");
31
25
  function isHandleLike(raw) {
32
26
  return raw !== null && (typeof raw === "object" || typeof raw === "function");
33
27
  }
34
- // postgres-js pool and tx handles both carry `typed`.
28
+ // Bun.SQL pool and tx handles both carry `flush`.
35
29
  function driverOf(raw) {
36
30
  if (!isHandleLike(raw))
37
31
  return "unknown";
38
- if (typeof raw["typed"] === "function")
39
- return "postgres-js";
32
+ if (typeof raw["flush"] === "function")
33
+ return "bun-sql";
40
34
  return "unknown";
41
35
  }
42
36
  function positiveNumber(v) {
43
37
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
44
38
  }
45
- // postgres-js configures timeouts in seconds.
39
+ // Bun.SQL reports its timeout options in milliseconds.
46
40
  function timeoutOptionMs(options, driver, optionName) {
47
- if (driver !== "postgres-js")
41
+ if (driver !== "bun-sql")
48
42
  return undefined;
49
- const seconds = positiveNumber(options[optionName]);
50
- return seconds === undefined ? undefined : seconds * 1000;
43
+ return positiveNumber(options[optionName]);
51
44
  }
52
45
  function errorProp(err, name) {
53
46
  return isHandleLike(err) ? err[name] : undefined;
54
47
  }
55
48
  // Deliberately excludes SQL text and parameters: app SQL can carry PII.
56
49
  export function describeClosedConnectionFailure(raw, err, attempts) {
57
- if (!isClosedConnectionError(err) && !isDriverClosedAbort(err))
50
+ if (!isClosedConnectionError(err))
58
51
  return undefined;
59
52
  const driver = driverOf(raw);
60
53
  const rawOptions = isHandleLike(raw) ? raw["options"] : undefined;
@@ -64,9 +57,9 @@ export function describeClosedConnectionFailure(raw, err, attempts) {
64
57
  pooled: isPooledClient(raw),
65
58
  attempts,
66
59
  poolMax: positiveNumber(options["max"]),
67
- idleTimeoutMs: timeoutOptionMs(options, driver, "idle_timeout"),
68
- maxLifetimeMs: timeoutOptionMs(options, driver, "max_lifetime"),
69
- connectionTimeoutMs: timeoutOptionMs(options, driver, "connect_timeout"),
60
+ idleTimeoutMs: timeoutOptionMs(options, driver, "idleTimeout"),
61
+ maxLifetimeMs: timeoutOptionMs(options, driver, "maxLifetime"),
62
+ connectionTimeoutMs: timeoutOptionMs(options, driver, "connectionTimeout"),
70
63
  errorName: errorProp(err, "name"),
71
64
  errorCode: extractPgError(err)?.code ?? errorProp(err, "code"),
72
65
  errorErrno: errorProp(err, "errno"),
@@ -1,14 +1,12 @@
1
- import type { DbConnection, PgClient } from "@cosmicdrift/kumiko-types/db-connection";
2
- import postgres from "postgres";
1
+ import type { DbConnection, PgClient, PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
3
2
  export type * from "@cosmicdrift/kumiko-types/db-connection";
4
- type PostgresDriverOptions = NonNullable<Parameters<typeof postgres>[1]>;
3
+ type BunSqlOptions = Bun.SQL.PostgresOrMySQLOptions;
5
4
  export type DbConnectionOptions = {
6
5
  readonly maxConnections?: number;
7
6
  readonly idleTimeoutSeconds?: number;
8
7
  readonly connectTimeoutSeconds?: number;
9
8
  readonly closeTimeoutSeconds?: number;
10
- readonly ssl?: PostgresDriverOptions["ssl"];
11
- readonly onNotice?: (notice: postgres.Notice) => void;
9
+ readonly ssl?: BunSqlOptions["tls"];
12
10
  readonly statementTimeoutMs?: number;
13
11
  readonly idleInTransactionSessionTimeoutMs?: number;
14
12
  readonly lockTimeoutMs?: number;
@@ -19,6 +17,7 @@ export type CreateDbConnectionOptions = DbConnectionOptions & {
19
17
  export type DbConnectionHandle = {
20
18
  db: DbConnection;
21
19
  client: PgClient;
20
+ listenClient: PgListenClient;
22
21
  close: () => Promise<void>;
23
22
  };
24
23
  export declare const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
@@ -1,7 +1,6 @@
1
- import postgres from "postgres";
2
1
  import { readPositiveIntEnv } from "../utils/env-parse.js";
3
2
  // A DB outage can leave an in-flight query referenced on a dead connection,
4
- // so a plain end() (no timeout = wait for queries to finish) hangs forever —
3
+ // so a plain close() (no timeout = wait for queries to finish) hangs forever —
5
4
  // close() must always bound how long shutdown waits.
6
5
  export const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
7
6
  // Keyed by the pool so withoutStatementTimeout can open a sibling with identical connect options.
@@ -11,20 +10,28 @@ export function openUnboundedSiblingPool(db) {
11
10
  return undefined;
12
11
  return unboundedSiblingFactories.get(db)?.();
13
12
  }
13
+ // Bun.SQL treats `true` and a TLS object without rejectUnauthorized as sslmode=require, which
14
+ // skips certificate checks; postgres-js verified in both cases, so keep that unless turned off.
15
+ function verifiedTls(ssl) {
16
+ if (ssl === true)
17
+ return "verify-full";
18
+ if (typeof ssl === "object" && !(ssl instanceof Blob) && ssl.rejectUnauthorized === undefined) {
19
+ return { ...ssl, rejectUnauthorized: true };
20
+ }
21
+ return ssl;
22
+ }
14
23
  export function createDbConnection({ url, ...options }) {
15
- const pgOptions = {};
24
+ const sqlOptions = {};
16
25
  if (options.maxConnections !== undefined)
17
- pgOptions.max = options.maxConnections;
26
+ sqlOptions.max = options.maxConnections;
18
27
  if (options.idleTimeoutSeconds !== undefined) {
19
- pgOptions.idle_timeout = options.idleTimeoutSeconds;
28
+ sqlOptions.idleTimeout = options.idleTimeoutSeconds;
20
29
  }
21
30
  if (options.connectTimeoutSeconds !== undefined) {
22
- pgOptions.connect_timeout = options.connectTimeoutSeconds;
31
+ sqlOptions.connectionTimeout = options.connectTimeoutSeconds;
23
32
  }
24
33
  if (options.ssl !== undefined)
25
- pgOptions.ssl = options.ssl;
26
- if (options.onNotice !== undefined)
27
- pgOptions.onnotice = options.onNotice;
34
+ sqlOptions.tls = verifiedTls(options.ssl);
28
35
  const serverParameters = {};
29
36
  if (options.statementTimeoutMs !== undefined) {
30
37
  serverParameters["statement_timeout"] = options.statementTimeoutMs;
@@ -36,48 +43,50 @@ export function createDbConnection({ url, ...options }) {
36
43
  if (options.lockTimeoutMs !== undefined)
37
44
  serverParameters["lock_timeout"] = options.lockTimeoutMs;
38
45
  if (Object.keys(serverParameters).length > 0)
39
- pgOptions.connection = serverParameters;
40
- const client = postgres(url, pgOptions);
46
+ sqlOptions.connection = serverParameters;
47
+ // Bun.SQL opens all `max` connections on the first query, so a pool always costs `max` backends.
48
+ const client = new Bun.SQL(url, sqlOptions);
41
49
  // A run that never settles (a caller gave up on it via its own timeout) never reaches the
42
50
  // sibling's own close; close() ends it so shutdown and DROP DATABASE are not blocked by it.
43
51
  const openSiblings = new Set();
44
52
  unboundedSiblingFactories.set(client, () => {
45
53
  // Two connections are enough for lock-in-tx plus DDL; the cap keeps parallel rebuilds from exhausting max_connections.
46
- const sibling = postgres(url, {
47
- ...pgOptions,
54
+ const sibling = new Bun.SQL(url, {
55
+ ...sqlOptions,
48
56
  max: 4,
49
- connection: { ...pgOptions.connection, statement_timeout: 0 },
57
+ connection: { ...sqlOptions.connection, statement_timeout: 0 },
50
58
  });
51
59
  openSiblings.add(sibling);
52
60
  return {
53
61
  client: sibling,
54
62
  close: async (timeoutSeconds) => {
55
63
  openSiblings.delete(sibling);
56
- await sibling.end({ timeout: timeoutSeconds });
64
+ await sibling.close({ timeout: timeoutSeconds });
57
65
  },
58
66
  };
59
67
  });
60
68
  return {
61
69
  db: client,
62
70
  client,
71
+ listenClient: client,
63
72
  close: async () => {
64
73
  const timeout = options.closeTimeoutSeconds ?? DEFAULT_DB_CLOSE_TIMEOUT_SECONDS;
65
74
  unboundedSiblingFactories.delete(client);
66
75
  const pools = [client, ...openSiblings];
67
76
  openSiblings.clear();
68
- const results = await Promise.allSettled(pools.map((pool) => pool.end({ timeout })));
77
+ const results = await Promise.allSettled(pools.map((pool) => pool.close({ timeout })));
69
78
  const failure = results.find((result) => result.status === "rejected");
70
79
  if (failure !== undefined)
71
80
  throw failure.reason;
72
81
  },
73
82
  };
74
83
  }
75
- // DB_PROVIDER=bun selected the removed Bun.SQL driver; ignoring it would boot
76
- // postgres-js under a config that still claims Bun.SQL.
84
+ // DB_PROVIDER once selected between drivers; a config that still sets it is stale,
85
+ // so fail loudly instead of silently ignoring it.
77
86
  function assertNoRemovedDbProvider(env) {
78
87
  const provider = env["DB_PROVIDER"];
79
- if (provider !== undefined && provider !== "" && provider !== "postgres") {
80
- throw new Error(`DB_PROVIDER="${provider}" is no longer supported: postgres-js is the only driver. Remove DB_PROVIDER from the environment.`);
88
+ if (provider !== undefined && provider !== "") {
89
+ throw new Error(`DB_PROVIDER="${provider}" is no longer supported: Bun.SQL is the only driver. Remove DB_PROVIDER from the environment.`);
81
90
  }
82
91
  }
83
92
  export function dbConnectionOptionsFromEnv(env = process.env) {
@@ -4,13 +4,13 @@ import { parseJsonSafe } from "../utils/safe-json.js";
4
4
  import { isTemporalInstant, isTemporalPlainDate } from "./temporal-value-guards.js";
5
5
  // --- Value coercion at the driver boundary ------------------------------
6
6
  //
7
- // postgres-js returns timestamptz as JS Date (or ISO string depending on
7
+ // The driver returns timestamptz as JS Date (or ISO string depending on
8
8
  // driver config). Framework contract says timestamptz surfaces as
9
9
  // Temporal.Instant — without coercion every read hands callers a Date and
10
10
  // downstream code that does .epochMilliseconds / .add(...) crashes.
11
11
  //
12
- // Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
13
- // params — convert to ISO string when the column pgType is timestamptz.
12
+ // Symmetric on write: Temporal.Instant doesn't bind directly as a driver
13
+ // param — convert to ISO string when the column pgType is timestamptz.
14
14
  // Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
15
15
  // drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
16
16
  // unmanaged columns). Uses BigInt equality check so 9007199254740993n never
@@ -72,7 +72,7 @@ function instantFromDriver(value) {
72
72
  // the day, so say so once per column instead of returning the wrong day quietly.
73
73
  const warnedNonMidnightDateColumns = new Set();
74
74
  const MS_PER_DAY = 86_400_000;
75
- const dateColumnDriftLog = createFallbackLogger("postgres-js");
75
+ const dateColumnDriftLog = createFallbackLogger("bun-sql");
76
76
  function warnIfNotDateColumn(epochMs, column) {
77
77
  // skip: midnight value is a correct date column, and an already-warned column is deduped
78
78
  if (epochMs % MS_PER_DAY === 0 || warnedNonMidnightDateColumns.has(column))
@@ -33,7 +33,9 @@ function prepareDateValue(value) {
33
33
  // structured values bind directly with an ::jsonb cast; JSON.stringify first
34
34
  // would produce a JSON-string scalar instead of the structured value.
35
35
  export function prepareJsonbValue(value) {
36
- if (typeof value === "boolean") {
36
+ // Bun.SQL types JS numbers as int4/float8 params, which Postgres refuses for a jsonb column.
37
+ // Non-finite numbers fall through so Postgres rejects them instead of storing JSON null.
38
+ if (typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) {
37
39
  return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
38
40
  }
39
41
  if (typeof value !== "object" || isTemporalInstant(value)) {
@@ -42,7 +44,7 @@ export function prepareJsonbValue(value) {
42
44
  if (!Array.isArray(value)) {
43
45
  return { kind: "param", sql: "::jsonb", bound: value };
44
46
  }
45
- // All-boolean arrays are inferred as boolean[] by postgres-js; route via text::jsonb.
47
+ // All-boolean arrays are inferred as boolean[] by the driver; route via text::jsonb.
46
48
  if (value.length > 0 && value.every((entry) => typeof entry === "boolean")) {
47
49
  return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
48
50
  }
@@ -59,7 +61,7 @@ const INTEGER_PG_TYPES = new Set([
59
61
  "bigserial",
60
62
  ]);
61
63
  // Postgres rounds sub-millisecond params into timestamptz(3); truncating to ms here keeps the
62
- // stored instant identical across drivers and equal to what postgres.js sends via Date.
64
+ // stored instant identical across drivers and equal to what a Date param would store.
63
65
  export function instantToTimestamptzParam(instant) {
64
66
  return Temporal.Instant.fromEpochMilliseconds(instant.epochMilliseconds).toString();
65
67
  }
@@ -67,7 +69,7 @@ function prepareDriverNeutralScalar(value, pgType) {
67
69
  if ((pgType === "timestamptz" || pgType === "timestamptz(3)") && isTemporalInstant(value)) {
68
70
  return { kind: "param", sql: "", bound: instantToTimestamptzParam(value) };
69
71
  }
70
- // postgres.js sends text, so Postgres rejects 3.5 for an integer column instead of rounding it.
72
+ // Sent as text, so Postgres rejects 3.5 for an integer column instead of rounding it.
71
73
  if (typeof value === "number" && pgType !== undefined && INTEGER_PG_TYPES.has(pgType)) {
72
74
  return { kind: "param", sql: "", bound: String(value) };
73
75
  }
@@ -0,0 +1,17 @@
1
+ import type { FieldDefinition } from "@cosmicdrift/kumiko-types/fields";
2
+ import type { ListAggregateCurrencyAmount, ListAggregateMeasureKind } from "@cosmicdrift/kumiko-types/list-aggregate-types";
3
+ import type { OwnershipClause } from "../engine/ownership.ts";
4
+ import type { ExecutorContext } from "./event-store-executor-context.ts";
5
+ import { type ListWhere } from "./event-store-executor-read-filter.ts";
6
+ import type { TenantDb } from "./tenant-db.ts";
7
+ export type CurrencyMeasureCandidate = {
8
+ readonly kind: ListAggregateMeasureKind;
9
+ readonly field: string;
10
+ readonly def: FieldDefinition;
11
+ readonly clause: OwnershipClause;
12
+ };
13
+ export type MeasureCurrencies = ReadonlyMap<string, readonly string[]>;
14
+ export declare function currencyColumnOf(field: string): string;
15
+ export declare function loadMeasureCurrencies(ctx: ExecutorContext, measures: readonly CurrencyMeasureCandidate[], where: ListWhere, db: TenantDb): Promise<MeasureCurrencies>;
16
+ export declare function currencyMeasureAlias(measureIndex: number, currencyIndex: number): string;
17
+ export declare function decodeCurrencyAmounts(row: Readonly<Record<string, unknown>>, measureIndex: number, currencies: readonly string[]): readonly ListAggregateCurrencyAmount[];
@@ -0,0 +1,40 @@
1
+ import { isPerCurrencyMeasure } from "./aggregate-sql.js";
2
+ import { bindShiftedClause } from "./event-store-executor-read-filter.js";
3
+ import { executeRawQueryRead } from "./queries/raw-sql.js";
4
+ import { schemaTableName } from "./table-introspection.js";
5
+ import { tenantDbRunner } from "./tenant-db-runner.js";
6
+ // Above the ~180 ISO 4217 codes, so the cap only bounds malformed data.
7
+ const MAX_AGGREGATE_CURRENCIES = 200;
8
+ export function currencyColumnOf(field) {
9
+ return `${field}Currency`;
10
+ }
11
+ // Runs before the measure query mutates `where`, on a copy of its params, and
12
+ // under the money field's own read clause: an unreadable amount must not reveal
13
+ // which currencies exist.
14
+ export async function loadMeasureCurrencies(ctx, measures, where, db) {
15
+ const byField = new Map();
16
+ const tableName = String(schemaTableName(ctx.table));
17
+ for (const m of measures) {
18
+ if (!isPerCurrencyMeasure(m.kind, m.def.type) || byField.has(m.field))
19
+ continue;
20
+ if (!Object.hasOwn(ctx.table, currencyColumnOf(m.field)))
21
+ continue;
22
+ const params = [...where.params];
23
+ const conditions = [...where.whereSql, `${where.colSql(m.field)} IS NOT NULL`];
24
+ if (m.clause.kind === "sql")
25
+ conditions.push(`(${bindShiftedClause(params, m.clause)})`);
26
+ const currencySql = where.colSql(currencyColumnOf(m.field));
27
+ const rows = await executeRawQueryRead(tenantDbRunner(db), `SELECT DISTINCT ${currencySql} AS cur FROM "${tableName}" WHERE ${conditions.join(" AND ")} AND ${currencySql} IS NOT NULL ORDER BY 1 LIMIT ${MAX_AGGREGATE_CURRENCIES}`, params);
28
+ byField.set(m.field, rows.map((row) => String(row.cur)));
29
+ }
30
+ return byField;
31
+ }
32
+ export function currencyMeasureAlias(measureIndex, currencyIndex) {
33
+ return `m${measureIndex}_${currencyIndex}`;
34
+ }
35
+ export function decodeCurrencyAmounts(row, measureIndex, currencies) {
36
+ return currencies.flatMap((currency, j) => {
37
+ const raw = row[currencyMeasureAlias(measureIndex, j)];
38
+ return raw === null || raw === undefined ? [] : [{ currency, value: Number(raw) }];
39
+ });
40
+ }
@@ -1,6 +1,7 @@
1
1
  import { ValidationError } from "../errors/index.js";
2
- import { aggregateKeyOf, decodeListAggregateValue, emptyListAggregateValue, groupedKeyOrderLimitSql, isListAggregateGroupable, isListAggregateMeasureSupported, listAggregateKeySql, listAggregateMeasureSql, } from "./aggregate-sql.js";
2
+ import { aggregateKeyOf, decodeListAggregateValue, emptyListAggregateValue, groupedKeyOrderLimitSql, isListAggregateGroupable, isListAggregateMeasureSupported, isPerCurrencyMeasure, listAggregateKeySql, listAggregateMeasureSql, listCurrencyMeasureSql, } from "./aggregate-sql.js";
3
3
  import { holdsCiphertextOrSecret } from "./entity-field-encryption.js";
4
+ import { currencyColumnOf, currencyMeasureAlias, decodeCurrencyAmounts, loadMeasureCurrencies, } from "./event-store-executor-read-aggregate-currencies.js";
4
5
  import { appendShiftedClause, bindShiftedClause, } from "./event-store-executor-read-filter.js";
5
6
  import { buildListWhere, createFieldReadClause, } from "./event-store-executor-read-where.js";
6
7
  import { executeRawQueryRead } from "./queries/raw-sql.js";
@@ -61,9 +62,26 @@ function planAggregate(ctx, fieldReadClause, aggregate) {
61
62
  return { groupBy, measures };
62
63
  }
63
64
  function emptyMeasureValues(plan) {
64
- return plan.measures.map((m) => m.gate.kind === "silent" ? null : emptyListAggregateValue(m.kind));
65
+ return plan.measures.map((m) => {
66
+ if (m.gate.kind === "silent")
67
+ return null;
68
+ return isPerCurrencyMeasure(m.kind, m.gate.def.type) ? [] : emptyListAggregateValue(m.kind);
69
+ });
70
+ }
71
+ function currencyCandidates(plan) {
72
+ return plan.measures.flatMap((m) => m.gate.kind === "ok"
73
+ ? [{ kind: m.kind, field: m.field, def: m.gate.def, clause: m.gate.clause }]
74
+ : []);
65
75
  }
66
- function renderSelectList(plan, where) {
76
+ function currencyMeasureSelects(measureIndex, kind, field, readFilterSql, currencies, where) {
77
+ return currencies.map((currency, j) => {
78
+ where.params.push(currency);
79
+ const currencyMatch = `${where.colSql(currencyColumnOf(field))} = $${where.params.length}`;
80
+ const filterSql = readFilterSql === undefined ? currencyMatch : `${currencyMatch} AND (${readFilterSql})`;
81
+ return `${listCurrencyMeasureSql(kind, where.colSql(field), filterSql)} AS ${currencyMeasureAlias(measureIndex, j)}`;
82
+ });
83
+ }
84
+ function renderSelectList(plan, where, currencies) {
67
85
  const parts = [];
68
86
  if (plan.groupBy?.gate.kind === "ok") {
69
87
  const { def, clause } = plan.groupBy.gate;
@@ -77,6 +95,11 @@ function renderSelectList(plan, where) {
77
95
  if (m.gate.kind === "silent")
78
96
  continue; // skip: an unreadable measure stays out of the SELECT and decodes to null
79
97
  const filterSql = m.gate.clause.kind === "sql" ? bindShiftedClause(where.params, m.gate.clause) : undefined;
98
+ if (isPerCurrencyMeasure(m.kind, m.gate.def.type)) {
99
+ const fieldCurrencies = currencies.get(m.field) ?? [];
100
+ parts.push(...currencyMeasureSelects(i, m.kind, m.field, filterSql, fieldCurrencies, where));
101
+ continue;
102
+ }
80
103
  parts.push(`${listAggregateMeasureSql(m.kind, m.gate.def.type, where.colSql(m.field), filterSql)} AS m${i}`);
81
104
  }
82
105
  return parts.join(", ");
@@ -93,7 +116,8 @@ export async function aggregateList(ctx, payload, user, db, runtimeOptions) {
93
116
  };
94
117
  }
95
118
  const { where, searchTruncated } = built;
96
- const selectList = renderSelectList(plan, where);
119
+ const currencies = await loadMeasureCurrencies(ctx, currencyCandidates(plan), where, db);
120
+ const selectList = renderSelectList(plan, where, currencies);
97
121
  const whereText = where.whereSql.length > 0 ? ` WHERE ${where.whereSql.join(" AND ")}` : "";
98
122
  const groupText = grouped ? groupedKeyOrderLimitSql(MAX_AGGREGATE_GROUPS + 1) : "";
99
123
  const tableName = String(schemaTableName(ctx.table));
@@ -102,7 +126,14 @@ export async function aggregateList(ctx, payload, user, db, runtimeOptions) {
102
126
  const groups = rows.slice(0, MAX_AGGREGATE_GROUPS).map((row) => ({
103
127
  key: grouped ? aggregateKeyOf(row["k0"]) : null,
104
128
  count: Number(row["c"]),
105
- measures: plan.measures.map((m, i) => m.gate.kind === "silent" ? null : decodeListAggregateValue(row[`m${i}`])),
129
+ measures: plan.measures.map((m, i) => {
130
+ if (m.gate.kind === "silent")
131
+ return null;
132
+ if (isPerCurrencyMeasure(m.kind, m.gate.def.type)) {
133
+ return decodeCurrencyAmounts(row, i, currencies.get(m.field) ?? []);
134
+ }
135
+ return decodeListAggregateValue(row[`m${i}`]);
136
+ }),
106
137
  }));
107
138
  return {
108
139
  groups,
@@ -58,7 +58,7 @@ function applyMultiSelectFilter(colSql, whereSql, params, f) {
58
58
  // skip: empty candidate list is unsatisfiable
59
59
  return;
60
60
  }
61
- // Bound as text then cast: a JS array makes postgres.js infer boolean[]/int4[]
61
+ // Bound as text then cast: a JS array makes the driver infer boolean[]/int4[]
62
62
  // for client-supplied non-string values, and `boolean[]::jsonb` is rejected
63
63
  // (500 on client input). An array value means "contains all of these".
64
64
  params.push(JSON.stringify(candidates));
@@ -163,6 +163,9 @@ export function collectListFilters(payload) {
163
163
  const single = payload.filter !== undefined ? [payload.filter] : [];
164
164
  return payload.filters !== undefined ? [...single, ...payload.filters] : single;
165
165
  }
166
+ function isJsonbArrayField(field) {
167
+ return field?.type === "multiSelect" || (field?.type === "reference" && field.multiple === true);
168
+ }
166
169
  export function applyListFilter(where, f, table, entity, fieldReadClause) {
167
170
  if (!Object.hasOwn(table, f.field)) {
168
171
  // skip: unknown field — not a real column, drop the filter (injection guard)
@@ -194,9 +197,9 @@ export function applyListFilter(where, f, table, entity, fieldReadClause) {
194
197
  }
195
198
  if (readClause.kind === "sql")
196
199
  appendShiftedClause(where, readClause);
197
- if (entity.fields[f.field]?.type === "multiSelect") {
200
+ if (isJsonbArrayField(entity.fields[f.field])) {
198
201
  applyMultiSelectFilter(where.colSql, where.whereSql, where.params, f);
199
- // skip: multiSelect already handled — don't fall through to applyScreenFilter
202
+ // skip: jsonb-array field already handled — don't fall through to applyScreenFilter
200
203
  return;
201
204
  }
202
205
  applyScreenFilter(table, where.colSql, where.whereSql, where.params, f);
@@ -0,0 +1,4 @@
1
+ import type { EventStoreExecutor } from "@cosmicdrift/kumiko-types/event-store-executor-types";
2
+ type WriteVerbs = Pick<EventStoreExecutor, "create" | "update" | "delete" | "forget" | "restore">;
3
+ export declare function withOwnTransactionOnPool(verbs: WriteVerbs): WriteVerbs;
4
+ export {};
@@ -0,0 +1,41 @@
1
+ import { runnerCanBeginTransaction } from "./query.js";
2
+ import { runInOwnTransaction } from "./tenant-db.js";
3
+ import { isBoundTenantDb, tenantDbRunner, tenantDbWritesDeniedReason } from "./tenant-db-runner.js";
4
+ // Aborts the surrounding transaction when a verb returns a failure instead of throwing.
5
+ class RollbackOnFailure extends Error {
6
+ }
7
+ // Pool runners have no savepoint, so append, projection and hooks would otherwise commit as
8
+ // separate autocommit statements. Tx and savepoint runners already provide atomicity.
9
+ async function runVerbAtomically(db, run) {
10
+ // Unbound and denied handles keep their original error, a fresh transaction would reword it.
11
+ if (!isBoundTenantDb(db) ||
12
+ tenantDbWritesDeniedReason(db) !== undefined ||
13
+ !runnerCanBeginTransaction(tenantDbRunner(db))) {
14
+ return run(db);
15
+ }
16
+ let failure;
17
+ try {
18
+ return await runInOwnTransaction(db, async (txDb) => {
19
+ const result = await run(txDb);
20
+ if (!result.isSuccess) {
21
+ failure = result;
22
+ throw new RollbackOnFailure();
23
+ }
24
+ return result;
25
+ });
26
+ }
27
+ catch (error) {
28
+ if (error instanceof RollbackOnFailure && failure !== undefined)
29
+ return failure;
30
+ throw error;
31
+ }
32
+ }
33
+ export function withOwnTransactionOnPool(verbs) {
34
+ return {
35
+ create: (payload, user, db, options) => runVerbAtomically(db, (txDb) => verbs.create(payload, user, txDb, options)),
36
+ update: (payload, user, db, options) => runVerbAtomically(db, (txDb) => verbs.update(payload, user, txDb, options)),
37
+ delete: (payload, user, db, options) => runVerbAtomically(db, (txDb) => verbs.delete(payload, user, txDb, options)),
38
+ forget: (payload, user, db, options) => runVerbAtomically(db, (txDb) => verbs.forget(payload, user, txDb, options)),
39
+ restore: (payload, user, db, options) => runVerbAtomically(db, (txDb) => verbs.restore(payload, user, txDb, options)),
40
+ };
41
+ }
@@ -5,9 +5,6 @@ import type { EntityId } from "../engine/types/index.ts";
5
5
  import { type UnprocessableOpts, type WriteFailure, writeFailure } from "../errors/index.ts";
6
6
  import type { StoredEvent } from "../event-store/index.ts";
7
7
  import type { DbRow, DbRunner } from "./connection.ts";
8
- export declare function withoutEventInJson<T extends {
9
- readonly event: unknown;
10
- }>(result: T): T;
11
8
  export declare function projectEntityEvent(db: TenantDb, runner: DbRunner, event: StoredEvent): Promise<void>;
12
9
  export declare function selectOptionNotAvailableFailure(entries: readonly UnavailableSelectOption[]): WriteFailure | undefined;
13
10
  export type PreSaveFn = (changes: Record<string, unknown>, previous: Record<string, unknown>, isNew: boolean) => Promise<Record<string, unknown>>;
@@ -2,11 +2,6 @@ import { VersionConflictError as FrameworkVersionConflict, UnprocessableError, w
2
2
  import { hasLiveProjectionsForEvent, runProjectionsForEvent, } from "../pipeline/projections-runner.js";
3
3
  import { projectionRegistryOf } from "./projection-registry-binding.js";
4
4
  import { runInSavepointIfSupported } from "./query.js";
5
- // The echoed event carries entity PII and must never reach a handler's JSON response, while
6
- // hooks and projections still read it by property. Non-enumerable keeps it out of spread/stringify.
7
- export function withoutEventInJson(result) {
8
- return Object.defineProperty(result, "event", { value: result.event, enumerable: false });
9
- }
10
5
  // Custom projections for a write that did not go through the dispatcher (hook or
11
6
  // job writing a second aggregate). Savepoint like applyEntityEvent: a throwing
12
7
  // projection fails the write and the caller's transaction rolls it back.
@@ -12,7 +12,7 @@ import { applyEntityEvent } from "./apply-entity-event.js";
12
12
  import { flattenCompoundTypes, rehydrateCompoundTypes } from "./compound-types.js";
13
13
  import { WritePurposes, } from "./event-store-executor.js";
14
14
  import { buildEventMetadata, entityEventName, isForeignTenantOnGlobalEntity, ownershipSubject, tryMapUniqueViolation, } from "./event-store-executor-context.js";
15
- import { optimisticLockFailure, ownershipDeniedFailure, projectEntityEvent, runPreSave, selectOptionNotAvailableFailure, withoutEventInJson, } from "./event-store-executor-write-steps.js";
15
+ import { optimisticLockFailure, ownershipDeniedFailure, projectEntityEvent, runPreSave, selectOptionNotAvailableFailure, } from "./event-store-executor-write-steps.js";
16
16
  import { isFrameworkSystemUser, isLockGuardBypassed } from "./lock-guard.js";
17
17
  import { runInSavepointIfSupported } from "./query.js";
18
18
  import { assertPersonalDataWrite, tableNameOf, withExecutorManagedWrites } from "./tenant-db.js";
@@ -244,8 +244,8 @@ export function createWriteVerbs(ctx) {
244
244
  // legitimately collide — the same ID has an existing stream — so return
245
245
  // version_conflict instead of internal_error. Update uses the same catch.
246
246
  try {
247
- // Savepoint-scoped: postgres.js poisons the WHOLE surrounding
248
- // begin() once any statement inside it errors, even if the JS error
247
+ // Savepoint-scoped: Postgres aborts the WHOLE surrounding
248
+ // transaction once any statement inside it errors, even if the JS error
249
249
  // is caught — a losing concurrent create's unique-violation
250
250
  // would otherwise abort the caller's outer transaction and surface as
251
251
  // internal_error at commit time instead of the version_conflict this
@@ -287,8 +287,8 @@ export function createWriteVerbs(ctx) {
287
287
  // designer/frontend can show a clear "duplicate" message instead of a cryptic
288
288
  // "internal server error".
289
289
  // Savepoint like the append: an unclassified DB error from the projection
290
- // write would otherwise poison the enclosing transaction (postgres-js rejects the
291
- // whole begin() once any statement in it fails). The savepoint rolls back the failed
290
+ // write would otherwise poison the enclosing transaction (Postgres aborts the
291
+ // whole transaction once any statement in it fails). The savepoint rolls back the failed
292
292
  // write while allowing the error to propagate and remain catchable.
293
293
  async function applyProjection(runner, event, noRowMessage) {
294
294
  let result;
@@ -462,7 +462,7 @@ export function createWriteVerbs(ctx) {
462
462
  await projectEntityEvent(db, runner, echoEvent);
463
463
  return {
464
464
  isSuccess: true,
465
- data: withoutEventInJson({
465
+ data: {
466
466
  kind: "save",
467
467
  id: aggregateId,
468
468
  data: projection,
@@ -473,7 +473,7 @@ export function createWriteVerbs(ctx) {
473
473
  // Persisted event carries ciphertext by design — the caller-facing
474
474
  // echo must be plaintext like every other response field.
475
475
  event: echoEvent,
476
- }),
476
+ },
477
477
  };
478
478
  },
479
479
  async update(payload, user, db, updateOptions) {
@@ -526,7 +526,7 @@ export function createWriteVerbs(ctx) {
526
526
  const { data, echoEvent } = persisted;
527
527
  return {
528
528
  isSuccess: true,
529
- data: withoutEventInJson({
529
+ data: {
530
530
  kind: "save",
531
531
  id: data["id"], // @cast-boundary engine-payload
532
532
  data,
@@ -535,7 +535,7 @@ export function createWriteVerbs(ctx) {
535
535
  isNew: false,
536
536
  entityName,
537
537
  event: echoEvent,
538
- }),
538
+ },
539
539
  };
540
540
  }
541
541
  catch (e) {
@@ -622,14 +622,14 @@ export function createWriteVerbs(ctx) {
622
622
  await projectEntityEvent(db, runner, echoEvent);
623
623
  return {
624
624
  isSuccess: true,
625
- data: withoutEventInJson({
625
+ data: {
626
626
  kind: "delete",
627
627
  id: payload.id,
628
628
  data: existing,
629
629
  entityName,
630
630
  event: echoEvent,
631
631
  ...(isLockGuardBypassed(user, options) && { lockGuardBypassed: true }),
632
- }),
632
+ },
633
633
  };
634
634
  },
635
635
  // Hard-purge (Art. 17). Same shape as delete(), but emits `forgotten` which
@@ -695,14 +695,14 @@ export function createWriteVerbs(ctx) {
695
695
  await projectEntityEvent(db, runner, echoEvent);
696
696
  return {
697
697
  isSuccess: true,
698
- data: withoutEventInJson({
698
+ data: {
699
699
  kind: "delete",
700
700
  id: payload.id,
701
701
  data: existing,
702
702
  entityName,
703
703
  event: echoEvent,
704
704
  lockGuardBypassed: true,
705
- }),
705
+ },
706
706
  };
707
707
  },
708
708
  async restore(payload, user, db, options) {
@@ -761,7 +761,7 @@ export function createWriteVerbs(ctx) {
761
761
  await projectEntityEvent(db, runner, echoEvent);
762
762
  return {
763
763
  isSuccess: true,
764
- data: withoutEventInJson({
764
+ data: {
765
765
  kind: "save",
766
766
  id: payload.id,
767
767
  data: restoredHydrated,
@@ -770,7 +770,7 @@ export function createWriteVerbs(ctx) {
770
770
  isNew: false,
771
771
  entityName,
772
772
  event: echoEvent,
773
- }),
773
+ },
774
774
  };
775
775
  },
776
776
  };
@@ -1,12 +1,13 @@
1
1
  import { buildExecutorContext } from "./event-store-executor-context.js";
2
2
  import { createReadVerbs } from "./event-store-executor-read.js";
3
3
  import { createWriteVerbs } from "./event-store-executor-write.js";
4
+ import { withOwnTransactionOnPool } from "./event-store-executor-write-atomic.js";
4
5
  export { WritePurposes } from "@cosmicdrift/kumiko-types/event-store-executor-types";
5
6
  export { entityEventName } from "./event-store-executor-context.js";
6
7
  export function createEventStoreExecutor(table, entity, options) {
7
8
  const ctx = buildExecutorContext(table, entity, options);
8
9
  return {
9
- ...createWriteVerbs(ctx),
10
+ ...withOwnTransactionOnPool(createWriteVerbs(ctx)),
10
11
  ...createReadVerbs(ctx),
11
12
  };
12
13
  }