@cosmicdrift/kumiko-framework 0.367.0 → 0.369.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 (240) hide show
  1. package/AGENTS.md +1 -7
  2. package/README.md +1 -1
  3. package/agents/examples/access-control.md +2 -2
  4. package/agents/examples/anonymous-access-multitenant.md +2 -2
  5. package/agents/examples/anonymous-access.md +3 -3
  6. package/agents/examples/apex-landing.md +1 -1
  7. package/agents/examples/apex-surface-auth.md +6 -7
  8. package/agents/examples/api-worker-split.md +7 -8
  9. package/agents/examples/auth-claims.md +2 -1
  10. package/agents/examples/auth-foundation-providers.md +3 -3
  11. package/agents/examples/auth-mfa.md +3 -4
  12. package/agents/examples/basic-entity.md +3 -4
  13. package/agents/examples/chat-channels.md +4 -4
  14. package/agents/examples/compliance-profiles-demo.md +2 -3
  15. package/agents/examples/cross-feature-events.md +4 -4
  16. package/agents/examples/crypto-shredding-hr.md +14 -9
  17. package/agents/examples/currencies-global.md +3 -4
  18. package/agents/examples/currencies-per-tenant.md +3 -4
  19. package/agents/examples/custom-fields-basic.md +2 -3
  20. package/agents/examples/custom-handlers.md +3 -5
  21. package/agents/examples/dashboard-stats.md +2 -3
  22. package/agents/examples/delivery-notifications.md +5 -6
  23. package/agents/examples/e2e-generator.md +1 -1
  24. package/agents/examples/embedded-entity-form.md +2 -3
  25. package/agents/examples/embedded.md +2 -3
  26. package/agents/examples/encrypted-tenant-config.md +5 -6
  27. package/agents/examples/error-contract.md +3 -4
  28. package/agents/examples/event-sourcing.md +19 -23
  29. package/agents/examples/feature-toggles.md +4 -4
  30. package/agents/examples/field-access.md +3 -4
  31. package/agents/examples/files-post-processing.md +12 -13
  32. package/agents/examples/golden-path.md +208 -0
  33. package/agents/examples/idempotency.md +2 -2
  34. package/agents/examples/inbound-mail-triage.md +7 -7
  35. package/agents/examples/lane-routing.md +3 -3
  36. package/agents/examples/ledger-banking.md +2 -3
  37. package/agents/examples/ledger-invoicing.md +2 -3
  38. package/agents/examples/legal-pages.md +1 -1
  39. package/agents/examples/lifecycle-hooks.md +3 -3
  40. package/agents/examples/managed-config.md +3 -4
  41. package/agents/examples/managed-pages.md +2 -2
  42. package/agents/examples/notes-history-basic.md +4 -5
  43. package/agents/examples/ownership.md +3 -5
  44. package/agents/examples/pdf-audit-export.md +2 -3
  45. package/agents/examples/pipeline-basics.md +2 -2
  46. package/agents/examples/public-share-token.md +3 -3
  47. package/agents/examples/rate-limiting.md +2 -1
  48. package/agents/examples/realtime-sse.md +2 -3
  49. package/agents/examples/record-detail-layout.md +1 -1
  50. package/agents/examples/reference-data.md +3 -3
  51. package/agents/examples/relations.md +2 -2
  52. package/agents/examples/screens-nav.md +1 -1
  53. package/agents/examples/search.md +2 -2
  54. package/agents/examples/secrets-demo.md +8 -8
  55. package/agents/examples/security-baseline.md +2 -2
  56. package/agents/examples/session-revocation.md +7 -8
  57. package/agents/examples/state-machine.md +181 -165
  58. package/agents/examples/tags-basic.md +2 -3
  59. package/agents/examples/tenant-isolation.md +2 -3
  60. package/agents/examples/tenant-settings.md +2 -2
  61. package/agents/examples/tier-admin.md +3 -4
  62. package/agents/examples/timezones.md +3 -3
  63. package/agents/examples/user-data-rights.md +12 -8
  64. package/agents/examples/user-profile.md +3 -3
  65. package/agents/examples/webhook-step.md +2 -3
  66. package/agents/examples/wizard-form.md +4 -5
  67. package/agents/examples/workflow-engine.md +7 -3
  68. package/agents/examples/writeform-section.md +1 -1
  69. package/agents/examples.md +1 -0
  70. package/agents/features.md +4 -4
  71. package/dist/consumer-cli.js +2 -3
  72. package/dist/crypto/index.d.ts +1 -1
  73. package/dist/crypto/index.js +1 -1
  74. package/dist/crypto/owner-transition-rekey.js +2 -0
  75. package/dist/crypto/pg-kms-adapter.js +1 -1
  76. package/dist/crypto/subject-resolver.d.ts +2 -0
  77. package/dist/crypto/subject-resolver.js +30 -15
  78. package/dist/db/bun-db-deprecated.d.ts +10 -0
  79. package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
  80. package/dist/db/config-seed.js +1 -1
  81. package/dist/db/connection.d.ts +23 -5
  82. package/dist/db/connection.js +10 -5
  83. package/dist/db/dialect.js +1 -1
  84. package/dist/db/entity-field-encryption.d.ts +5 -0
  85. package/dist/db/entity-field-encryption.js +29 -0
  86. package/dist/db/entity-table-meta.js +6 -5
  87. package/dist/db/event-store-executor-read.js +26 -5
  88. package/dist/db/index.d.ts +4 -4
  89. package/dist/db/index.js +1 -1
  90. package/dist/db/migrate-runner.js +1 -1
  91. package/dist/db/queries/backfill-pii.js +16 -9
  92. package/dist/db/queries/ddl.js +1 -1
  93. package/dist/db/queries/event-store.d.ts +2 -0
  94. package/dist/db/queries/event-store.js +26 -0
  95. package/dist/db/queries/test-stack.js +1 -1
  96. package/dist/db/query.d.ts +105 -1
  97. package/dist/db/query.js +1450 -3
  98. package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
  99. package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
  100. package/dist/db/tenant-db.d.ts +11 -0
  101. package/dist/db/tenant-db.js +51 -12
  102. package/dist/derivatives/derivatives-context.d.ts +1 -1
  103. package/dist/derivatives/derivatives-context.js +1 -1
  104. package/dist/engine/boot-validator/gdpr-storage.js +3 -3
  105. package/dist/engine/boot-validator/pii-retention.js +3 -3
  106. package/dist/engine/boot-validator/record-owned.js +3 -3
  107. package/dist/engine/boot-validator/screens.js +37 -1
  108. package/dist/engine/bridge-stub.d.ts +4 -0
  109. package/dist/engine/bridge-stub.js +62 -0
  110. package/dist/engine/build-config-feature-schema.js +197 -65
  111. package/dist/engine/event-helpers.d.ts +2 -1
  112. package/dist/engine/event-helpers.js +1 -0
  113. package/dist/engine/extension-selector-plugins.d.ts +3 -0
  114. package/dist/engine/extension-selector-plugins.js +14 -0
  115. package/dist/engine/feature-config-events-jobs.d.ts +2 -0
  116. package/dist/engine/feature-config-events-jobs.js +3 -2
  117. package/dist/engine/index.d.ts +1 -0
  118. package/dist/engine/index.js +1 -0
  119. package/dist/es-ops/operations-schema.js +1 -1
  120. package/dist/event-store/admin-api.js +1 -1
  121. package/dist/event-store/archive.js +1 -1
  122. package/dist/event-store/errors.d.ts +2 -1
  123. package/dist/event-store/errors.js +6 -2
  124. package/dist/event-store/event-store.js +17 -9
  125. package/dist/event-store/events-schema.js +1 -1
  126. package/dist/event-store/rebuild-dead-letter.js +3 -3
  127. package/dist/event-store/snapshot.js +1 -1
  128. package/dist/event-store/upcaster-dead-letter.js +3 -3
  129. package/dist/files/create-file-ref-from-bytes.js +1 -1
  130. package/dist/files/file-routes.js +1 -1
  131. package/dist/i18n/settings-hub-keys.js +1 -2
  132. package/dist/internal/stack-support.d.ts +12 -0
  133. package/dist/internal/stack-support.js +12 -0
  134. package/dist/internal/testing.d.ts +4 -0
  135. package/dist/internal/testing.js +5 -0
  136. package/dist/jobs/job-runner.js +23 -11
  137. package/dist/migrations/pending-rebuilds.js +1 -1
  138. package/dist/migrations/pii-event-backfill.js +2 -2
  139. package/dist/pipeline/dispatch-shared.js +43 -23
  140. package/dist/pipeline/dispatch-write.js +6 -2
  141. package/dist/pipeline/dispatcher-utils.js +6 -2
  142. package/dist/pipeline/event-consumer-state.js +1 -1
  143. package/dist/pipeline/projection-rebuild.d.ts +1 -0
  144. package/dist/pipeline/projection-rebuild.js +1 -1
  145. package/dist/pipeline/projection-state.js +1 -1
  146. package/dist/pipeline/system-hooks.js +1 -1
  147. package/dist/schema-cli.js +3 -3
  148. package/dist/search/purge-subject.js +20 -10
  149. package/dist/seeding/index.d.ts +1 -0
  150. package/dist/seeding/index.js +1 -0
  151. package/dist/stack/index.d.ts +10 -9
  152. package/dist/stack/index.js +11 -19
  153. package/dist/testing/e2e-generator.d.ts +5 -49
  154. package/dist/testing/e2e-generator.js +5 -382
  155. package/dist/testing/handler-context.d.ts +5 -4
  156. package/dist/testing/handler-context.js +5 -69
  157. package/dist/testing/index.d.ts +10 -26
  158. package/dist/testing/index.js +10 -34
  159. package/dist/testing/real-providers.d.ts +5 -3
  160. package/dist/testing/real-providers.js +5 -21
  161. package/dist/upgrade-cli.js +6 -4
  162. package/package.json +37 -9
  163. package/src/changes.json +64 -0
  164. package/src/scripts/codemod/README.md +43 -0
  165. package/src/scripts/codemod/codemod-support.ts +276 -0
  166. package/src/scripts/codemod/db-factory.ts +810 -0
  167. package/src/scripts/codemod/feature-factory.ts +429 -0
  168. package/src/scripts/codemod/s3-imports.ts +274 -0
  169. package/src/scripts/codemod/tenant-db-options.ts +454 -0
  170. package/src/scripts/codemod/testing-move-targets.ts +161 -0
  171. package/src/scripts/codemod/testing-move.ts +520 -0
  172. package/dist/bun-db/connection.d.ts +0 -17
  173. package/dist/bun-db/connection.js +0 -40
  174. package/dist/bun-db/index.d.ts +0 -4
  175. package/dist/bun-db/query.d.ts +0 -106
  176. package/dist/bun-db/query.js +0 -1457
  177. package/dist/db/api.d.ts +0 -19
  178. package/dist/db/api.js +0 -23
  179. package/dist/db/bun-provider.d.ts +0 -2
  180. package/dist/db/bun-provider.js +0 -28
  181. package/dist/db/postgres-provider.d.ts +0 -2
  182. package/dist/db/postgres-provider.js +0 -21
  183. package/dist/db/query-api.d.ts +0 -2
  184. package/dist/db/query-api.js +0 -6
  185. package/dist/stack/db.d.ts +0 -22
  186. package/dist/stack/db.js +0 -67
  187. package/dist/stack/drain-event-consumers.d.ts +0 -4
  188. package/dist/stack/drain-event-consumers.js +0 -51
  189. package/dist/stack/drain-jobs.d.ts +0 -22
  190. package/dist/stack/drain-jobs.js +0 -141
  191. package/dist/stack/event-collector.d.ts +0 -9
  192. package/dist/stack/event-collector.js +0 -12
  193. package/dist/stack/push-entity-projection-tables.d.ts +0 -24
  194. package/dist/stack/push-entity-projection-tables.js +0 -74
  195. package/dist/stack/redis.d.ts +0 -17
  196. package/dist/stack/redis.js +0 -52
  197. package/dist/stack/request-helper.d.ts +0 -35
  198. package/dist/stack/request-helper.js +0 -144
  199. package/dist/stack/test-stack.d.ts +0 -192
  200. package/dist/stack/test-stack.js +0 -429
  201. package/dist/testing/access-assertions.d.ts +0 -2
  202. package/dist/testing/access-assertions.js +0 -13
  203. package/dist/testing/assertions.d.ts +0 -10
  204. package/dist/testing/assertions.js +0 -22
  205. package/dist/testing/boot-validator-fixture.d.ts +0 -3
  206. package/dist/testing/boot-validator-fixture.js +0 -101
  207. package/dist/testing/closed-connection-error.d.ts +0 -7
  208. package/dist/testing/closed-connection-error.js +0 -46
  209. package/dist/testing/db-cleanup.d.ts +0 -9
  210. package/dist/testing/db-cleanup.js +0 -31
  211. package/dist/testing/expect-error.d.ts +0 -2
  212. package/dist/testing/expect-error.js +0 -22
  213. package/dist/testing/file-provider-contract.d.ts +0 -2
  214. package/dist/testing/file-provider-contract.js +0 -102
  215. package/dist/testing/http-cookies.d.ts +0 -7
  216. package/dist/testing/http-cookies.js +0 -47
  217. package/dist/testing/late-bound.d.ts +0 -9
  218. package/dist/testing/late-bound.js +0 -31
  219. package/dist/testing/manual-clock.d.ts +0 -14
  220. package/dist/testing/manual-clock.js +0 -12
  221. package/dist/testing/multipart-helper.d.ts +0 -22
  222. package/dist/testing/multipart-helper.js +0 -77
  223. package/dist/testing/mutable-master-key-provider.d.ts +0 -7
  224. package/dist/testing/mutable-master-key-provider.js +0 -35
  225. package/dist/testing/observability-recorder.d.ts +0 -9
  226. package/dist/testing/observability-recorder.js +0 -29
  227. package/dist/testing/seed.d.ts +0 -9
  228. package/dist/testing/seed.js +0 -21
  229. package/dist/testing/shared-entities.d.ts +0 -286
  230. package/dist/testing/shared-entities.js +0 -56
  231. package/dist/testing/utils.d.ts +0 -1
  232. package/dist/testing/utils.js +0 -1
  233. package/dist/testing/wait-for.d.ts +0 -20
  234. package/dist/testing/wait-for.js +0 -47
  235. package/dist/testing/without-ambient-temporal.d.ts +0 -5
  236. package/dist/testing/without-ambient-temporal.js +0 -17
  237. package/dist/testing/zip-fixture.d.ts +0 -1
  238. package/dist/testing/zip-fixture.js +0 -33
  239. /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
  240. /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
package/dist/db/api.d.ts DELETED
@@ -1,19 +0,0 @@
1
- import type { PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
2
- export type DbConnectionOptions = {
3
- readonly maxConnections?: number;
4
- readonly idleTimeoutSeconds?: number;
5
- readonly connectTimeoutSeconds?: number;
6
- readonly closeTimeoutSeconds?: number;
7
- };
8
- export declare const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
9
- export type DbPoolHandle = {
10
- /** Provider Connection — Calls gehen über asRawClient() oder direkt. */
11
- readonly db: any;
12
- /** Raw driver client (postgres-js or Bun.SQL) */
13
- readonly client: any;
14
- /** LISTEN-capable client; Bun.SQL serves it from its own dedicated connection */
15
- readonly listenClient?: PgListenClient;
16
- /** Pool schliessen */
17
- close: () => Promise<void>;
18
- };
19
- export declare function createConnection(url: string, options?: DbConnectionOptions): Promise<DbPoolHandle>;
package/dist/db/api.js DELETED
@@ -1,23 +0,0 @@
1
- // DB-API: Unified Types und Connection-Factory.
2
- // Provider-agnostic code verwendet ausschliesslich diese Types.
3
- // Provider-Implementierungen: postgres-provider.ts, bun-provider.ts.
4
- //
5
- // `asRawClient(db)` aus bun-db/query.ts normalisiert beide Provider
6
- // zu { unsafe, begin } — business code ist provider-neutral.
7
- //
8
- // Default: postgres-js. Set DB_PROVIDER=bun für Bun.SQL (experimentell).
9
- // A DB outage can leave an in-flight query referenced on a dead connection,
10
- // so a plain end() (no timeout = wait for queries to finish) hangs forever —
11
- // close() must always bound how long shutdown waits.
12
- export const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
13
- let _provider;
14
- export async function createConnection(url, options = {}) {
15
- const p = process.env["DB_PROVIDER"];
16
- if (p === "bun" || p === "bun-sql") {
17
- const { createBunConnection } = await import("./bun-provider.js");
18
- return createBunConnection(url, options);
19
- }
20
- // postgres-js: sync, kein async import nötig
21
- const { createPgConnection } = await import("./postgres-provider.js");
22
- return createPgConnection(url, options);
23
- }
@@ -1,2 +0,0 @@
1
- import { type DbConnectionOptions, type DbPoolHandle } from "./api.js";
2
- export declare function createBunConnection(url: string, options?: DbConnectionOptions): DbPoolHandle;
@@ -1,28 +0,0 @@
1
- // Bun.SQL provider, opt-in via DB_PROVIDER=bun; postgres-js stays the default.
2
- // Known bug (Bun <= 1.4.3, oven-sh/bun#29484): a cached plan is not re-prepared
3
- // after DDL, so e.g. `SELECT *` after ADD COLUMN fails on that connection with
4
- // 0A000 "cached plan must not change result type". prepare:false is no fix,
5
- // it changes how jsonb params are bound.
6
- //
7
- // LISTEN runs on Bun.SQL's own dedicated connection, which is exempt from the
8
- // pool's idleTimeout and maxLifetime and re-LISTENs after a drop.
9
- import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS, } from "./api.js";
10
- export function createBunConnection(url, options = {}) {
11
- const bunOpts = {};
12
- if (options.maxConnections !== undefined)
13
- bunOpts.max = options.maxConnections;
14
- if (options.idleTimeoutSeconds !== undefined)
15
- bunOpts.idleTimeout = options.idleTimeoutSeconds;
16
- if (options.connectTimeoutSeconds !== undefined)
17
- bunOpts.connectionTimeout = options.connectTimeoutSeconds;
18
- const db = new Bun.SQL(url, bunOpts);
19
- const closeTimeout = options.closeTimeoutSeconds ?? DEFAULT_DB_CLOSE_TIMEOUT_SECONDS;
20
- return {
21
- db,
22
- client: db,
23
- listenClient: db,
24
- close: async () => {
25
- await db.end({ timeout: closeTimeout });
26
- },
27
- };
28
- }
@@ -1,2 +0,0 @@
1
- import { type DbConnectionOptions, type DbPoolHandle } from "./api.js";
2
- export declare function createPgConnection(url: string, options?: DbConnectionOptions): DbPoolHandle;
@@ -1,21 +0,0 @@
1
- // PostgreSQL Provider: postgres-js.
2
- // Standard-Provider — stabil, keine bekannten Protocol-Level-Bugs.
3
- // `asRawClient(db)` wrappt .unsafe und .begin transparent.
4
- import postgres from "postgres";
5
- import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS, } from "./api.js";
6
- export function createPgConnection(url, options = {}) {
7
- const pgOptions = {};
8
- if (options.maxConnections !== undefined)
9
- pgOptions.max = options.maxConnections;
10
- if (options.idleTimeoutSeconds !== undefined)
11
- pgOptions.idle_timeout = options.idleTimeoutSeconds;
12
- if (options.connectTimeoutSeconds !== undefined) {
13
- pgOptions.connect_timeout = options.connectTimeoutSeconds;
14
- }
15
- const client = postgres(url, pgOptions);
16
- return {
17
- db: client,
18
- client,
19
- close: () => client.end({ timeout: options.closeTimeoutSeconds ?? DEFAULT_DB_CLOSE_TIMEOUT_SECONDS }),
20
- };
21
- }
@@ -1,2 +0,0 @@
1
- export type { JsonTextMatch, SelectOptions, WhereObject, WhereOperator, WhereValue, } from "../db/query.js";
2
- export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
@@ -1,6 +0,0 @@
1
- // Legacy re-export shim — query-api.ts hat früher drizzle gewrapped.
2
- // Heute liegen die Helpers in src/bun-db/. Wir re-exportieren von dort
3
- // damit existing imports `@cosmicdrift/kumiko-framework/db` weiterhin
4
- // funktionieren während wir die Callers schrittweise auf den direkten
5
- // bun-db-Import migrieren.
6
- export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
@@ -1,22 +0,0 @@
1
- import type { DbConnection, PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
2
- declare function requireEnv(name: string): string;
3
- export type TestDb = {
4
- db: DbConnection;
5
- outsideTransactionDb: DbConnection;
6
- client: PgListenClient;
7
- dbName: string;
8
- cleanup: () => Promise<void>;
9
- };
10
- export type CreateTestDbOptions = {
11
- readonly baseUrl?: string;
12
- readonly dbName?: string;
13
- readonly persistent?: boolean;
14
- readonly poolMax?: number;
15
- readonly outsideTransactionPoolMax?: number;
16
- };
17
- /**
18
- * Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
19
- * Für Bun.SQL: DB_PROVIDER=bun setzen (experimentell — siehe db/bun-provider.ts).
20
- */
21
- export declare function createTestDb(arg?: string | CreateTestDbOptions): Promise<TestDb>;
22
- export { requireEnv };
package/dist/stack/db.js DELETED
@@ -1,67 +0,0 @@
1
- // Test-DB Factory: CREATE DATABASE → connect → createEventsTable.
2
- // Provider-agnostic via createConnection (DB_PROVIDER env).
3
- // postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
4
- import { createConnection } from "../db/api.js";
5
- import { DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX } from "../db/connection.js";
6
- import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack.js";
7
- import { ensureTemporalPolyfill } from "../time/polyfill.js";
8
- import { generateId } from "../utils/index.js";
9
- function requireEnv(name) {
10
- const value = process.env[name];
11
- if (!value) {
12
- throw new Error(`Missing required env var: ${name}. Copy .env.example to .env and fill in values. Tests also need the test services (Postgres, Redis) running, e.g. "docker compose up -d".`);
13
- }
14
- return value;
15
- }
16
- /**
17
- * Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
18
- * Für Bun.SQL: DB_PROVIDER=bun setzen (experimentell — siehe db/bun-provider.ts).
19
- */
20
- export async function createTestDb(arg) {
21
- await ensureTemporalPolyfill();
22
- const opts = typeof arg === "string" ? { baseUrl: arg } : (arg ?? {});
23
- const url = opts.baseUrl ?? requireEnv("TEST_DATABASE_URL");
24
- const dbName = opts.dbName ?? `kumiko_test_${generateId().slice(-8)}`;
25
- const adminUrl = url.replace(/\/[^/]+$/, "/postgres");
26
- const admin = await createConnection(adminUrl, { maxConnections: 1 });
27
- try {
28
- if (opts.persistent) {
29
- if (!(await databaseExists(admin.db, dbName))) {
30
- await createDatabase(admin.db, dbName);
31
- }
32
- }
33
- else {
34
- await createDatabase(admin.db, dbName);
35
- }
36
- }
37
- finally {
38
- await admin.close();
39
- }
40
- const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
41
- const conn = await createConnection(testUrl, opts.poolMax !== undefined ? { maxConnections: opts.poolMax } : {});
42
- const outsideConn = await createConnection(testUrl, {
43
- maxConnections: opts.outsideTransactionPoolMax ?? DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX,
44
- });
45
- const { createEventsTable } = await import("../event-store/index.js");
46
- await createEventsTable(conn.db);
47
- return {
48
- db: conn.db,
49
- outsideTransactionDb: outsideConn.db,
50
- client: conn.client,
51
- dbName,
52
- cleanup: async () => {
53
- await conn.close();
54
- await outsideConn.close();
55
- if (!opts.persistent) {
56
- const admin2 = await createConnection(adminUrl, { maxConnections: 1 });
57
- try {
58
- await dropDatabaseIfExists(admin2.db, dbName);
59
- }
60
- finally {
61
- await admin2.close();
62
- }
63
- }
64
- },
65
- };
66
- }
67
- export { requireEnv };
@@ -1,4 +0,0 @@
1
- import type { TestStack } from "./test-stack.js";
2
- export declare function drainEventConsumers(stack: Pick<TestStack, "db" | "eventDispatcher">, consumerNames: readonly [string, ...string[]], options?: {
3
- readonly maxPasses?: number;
4
- }): Promise<void>;
@@ -1,51 +0,0 @@
1
- import { getEventsHighWaterMark } from "../event-store/index.js";
2
- import { getConsumerState } from "../pipeline/index.js";
3
- const DEFAULT_MAX_PASSES = 25;
4
- // Drives eventDispatcher.runOnce() until every named consumer has caught up
5
- // to the events high-water mark captured at call time. No "no progress ⇒
6
- // throw" shortcut: a pass can legitimately advance zero cursors even though
7
- // work remains — runOnce's FOR UPDATE SKIP LOCKED lets a concurrent pass
8
- // (the test stack's own 50ms poll timer) hold a consumer's row, and a
9
- // consumer coming off a thrown-pass backoff sits idle until its retryAtMs
10
- // elapses. A bounded pass budget tolerates both without pretending to detect
11
- // "stuck" from a single empty pass. The target is a snapshot: events a
12
- // consumer's own handler writes afterward (cascades) are not waited on.
13
- export async function drainEventConsumers(stack, consumerNames, options) {
14
- const { db, eventDispatcher } = stack;
15
- if (!eventDispatcher) {
16
- throw new Error("drainEventConsumers: stack.eventDispatcher is undefined — no system consumer or " +
17
- "multiStreamProjection is wired for this stack.");
18
- }
19
- const maxPasses = options?.maxPasses ?? DEFAULT_MAX_PASSES;
20
- if (!Number.isInteger(maxPasses) || maxPasses < 1) {
21
- throw new Error("drainEventConsumers: maxPasses must be a positive integer");
22
- }
23
- const target = await getEventsHighWaterMark(db);
24
- for (let pass = 0; pass < maxPasses; pass++) {
25
- await eventDispatcher.runOnce();
26
- const states = await loadConsumerStates(db, consumerNames);
27
- // skip: every named consumer has already caught up to the target snapshot — the
28
- // remaining budgeted passes are unnecessary, not a swallowed error.
29
- if (states.every((state) => state.lastProcessedEventId >= target))
30
- return;
31
- if (pass === maxPasses - 1) {
32
- throw budgetExhaustedError(maxPasses, target, states);
33
- }
34
- }
35
- }
36
- async function loadConsumerStates(db, consumerNames) {
37
- return Promise.all(consumerNames.map(async (name) => {
38
- const state = await getConsumerState(db, name);
39
- if (!state) {
40
- throw new Error(`drainEventConsumers: consumer "${name}" not registered or per-instance`);
41
- }
42
- return state;
43
- }));
44
- }
45
- function budgetExhaustedError(maxPasses, target, states) {
46
- const unfinished = states.filter((state) => state.lastProcessedEventId < target);
47
- const lines = unfinished.map((state) => ` ${state.name}: lastProcessedEventId=${state.lastProcessedEventId} target=${target} ` +
48
- `status=${state.status} attempts=${state.attempts} lastError=${state.lastError ?? "null"}`);
49
- return new Error(`drainEventConsumers: budget of ${maxPasses} passes exhausted, consumers still behind target:\n` +
50
- lines.join("\n"));
51
- }
@@ -1,22 +0,0 @@
1
- import type { DbConnection } from "../db/index.js";
2
- import type { JobRunner } from "../jobs/index.js";
3
- import { type EventDispatcher } from "../pipeline/index.js";
4
- export type JobFailureTracker = {
5
- readonly onJobComplete: (jobName: string, jobId: string) => void;
6
- readonly onJobFailed: (jobName: string, jobId: string, error: string) => void;
7
- waitForActivity(): Promise<void>;
8
- readonly failures: ReadonlyMap<string, {
9
- readonly jobName: string;
10
- readonly error: string;
11
- }>;
12
- clear(): void;
13
- };
14
- export declare function createJobFailureTracker(): JobFailureTracker;
15
- export type DrainJobsStack = {
16
- readonly db: DbConnection;
17
- readonly eventDispatcher?: EventDispatcher;
18
- readonly jobRunner?: JobRunner;
19
- };
20
- export declare function drainJobs(stack: DrainJobsStack, tracker: JobFailureTracker, options?: {
21
- readonly timeoutMs?: number;
22
- }): Promise<void>;
@@ -1,141 +0,0 @@
1
- import { getEventsHighWaterMark } from "../event-store/index.js";
2
- import { getConsumerState } from "../pipeline/index.js";
3
- // Gap between idle-checks while waiting for a job hook to fire. Short enough
4
- // that a test never feels it, long enough that consecutive idle passes don't
5
- // hammer Postgres/Redis with pointless polling between hook-driven wakeups.
6
- const DRAIN_IDLE_POLL_MS = 25;
7
- // Two consecutive idle passes (not one) before declaring the stack drained —
8
- // a single "pending===0 and consumers caught up" pass can be a race: a job
9
- // hook fires and enqueues a follow-up between the countPendingJobs() read
10
- // and the consumer check of the SAME pass. A DRAIN_IDLE_POLL_MS wait sits
11
- // between every pass, including the first and second idle one, so an
12
- // enqueue that lands just after the first idle read still gets picked up
13
- // by the second pass instead of racing straight past it.
14
- const REQUIRED_IDLE_PASSES = 2;
15
- // Below the 15s per-test budget so a stuck drain reports what is open rather
16
- // than surfacing as a bare test timeout.
17
- const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
18
- function sleep(ms) {
19
- return new Promise((resolve) => setTimeout(resolve, ms));
20
- }
21
- export function createJobFailureTracker() {
22
- const failures = new Map();
23
- let wake;
24
- let armed;
25
- function fire() {
26
- wake?.();
27
- armed = undefined;
28
- wake = undefined;
29
- }
30
- return {
31
- onJobComplete(_jobName, jobId) {
32
- // A retry that eventually succeeds must not leave a stale failure
33
- // behind from its earlier attempt(s).
34
- failures.delete(jobId);
35
- fire();
36
- },
37
- onJobFailed(jobName, jobId, error) {
38
- failures.set(jobId, { jobName, error });
39
- fire();
40
- },
41
- waitForActivity() {
42
- armed ??= new Promise((resolve) => {
43
- wake = resolve;
44
- });
45
- return armed;
46
- },
47
- failures,
48
- clear() {
49
- failures.clear();
50
- },
51
- };
52
- }
53
- // Diagnostic only: lets the deadline error name what was still open.
54
- async function describeLaggingConsumers(db, consumerNames) {
55
- // skip: no shared consumers wired — nothing to catch up on.
56
- if (consumerNames.length === 0)
57
- return [];
58
- const target = await getEventsHighWaterMark(db);
59
- const lagging = [];
60
- for (const name of consumerNames) {
61
- const state = await getConsumerState(db, name);
62
- if (state && state.lastProcessedEventId < target) {
63
- lagging.push(`${name} (status=${state.status}, lastProcessedEventId=${state.lastProcessedEventId}, target=${target})`);
64
- }
65
- }
66
- return lagging;
67
- }
68
- async function drainTimeoutError(db, consumerNames, timeoutMs, pending) {
69
- const lagging = await describeLaggingConsumers(db, consumerNames);
70
- return new Error(`drainJobs: not drained after ${timeoutMs}ms — pending jobs: ${pending}, ` +
71
- `consumers behind: ${lagging.length === 0 ? "none" : lagging.join("; ")}`);
72
- }
73
- async function sharedConsumersCaughtUp(db, consumerNames) {
74
- // skip: no shared consumers wired — nothing to catch up on.
75
- if (consumerNames.length === 0)
76
- return true;
77
- const target = await getEventsHighWaterMark(db);
78
- let allCaughtUp = true;
79
- for (const name of consumerNames) {
80
- const state = await getConsumerState(db, name);
81
- if (!state) {
82
- throw new Error(`drainJobs: consumer "${name}" not registered or per-instance`);
83
- }
84
- if (state.status === "dead") {
85
- throw new Error(`drainJobs: consumer "${state.name}" is dead — lastError: ${state.lastError ?? "unknown"}`);
86
- }
87
- if (state.lastProcessedEventId < target)
88
- allCaughtUp = false;
89
- }
90
- return allCaughtUp;
91
- }
92
- // Fails a stuck drain with a diagnosis instead of a generic test-runner timeout.
93
- // It only bounds the loop; it never replaces waiting for real work.
94
- export async function drainJobs(stack, tracker, options) {
95
- const { db, eventDispatcher, jobRunner } = stack;
96
- const timeoutMs = options?.timeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
97
- const deadline = Date.now() + timeoutMs;
98
- if (!jobRunner) {
99
- throw new Error("drainJobs: stack.jobRunner is undefined — pass `jobs: {...}` to setupTestStack and " +
100
- "register at least one r.job(...) so a runner is built.");
101
- }
102
- const sharedConsumerNames = (eventDispatcher?.consumers ?? [])
103
- .filter((consumer) => (consumer.delivery ?? "shared") === "shared")
104
- .map((consumer) => consumer.name);
105
- try {
106
- try {
107
- let consecutiveIdlePasses = 0;
108
- while (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
109
- await eventDispatcher?.runOnce();
110
- const pending = await jobRunner.countPendingJobs();
111
- const caughtUp = await sharedConsumersCaughtUp(db, sharedConsumerNames);
112
- consecutiveIdlePasses = pending === 0 && caughtUp ? consecutiveIdlePasses + 1 : 0;
113
- if (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
114
- if (Date.now() > deadline) {
115
- throw await drainTimeoutError(db, sharedConsumerNames, timeoutMs, pending);
116
- }
117
- await Promise.race([tracker.waitForActivity(), sleep(DRAIN_IDLE_POLL_MS)]);
118
- }
119
- }
120
- }
121
- catch (err) {
122
- // A failed job often kills its consumer, so the drain error is only the follow-up symptom.
123
- if (tracker.failures.size === 0)
124
- throw err;
125
- const message = err instanceof Error ? err.message : String(err);
126
- throw new Error(`${message}; ${describeJobFailures(tracker)}`, { cause: err });
127
- }
128
- if (tracker.failures.size > 0) {
129
- throw new Error(`drainJobs: ${describeJobFailures(tracker)}`);
130
- }
131
- }
132
- finally {
133
- tracker.clear();
134
- }
135
- }
136
- function describeJobFailures(tracker) {
137
- const details = [...tracker.failures.values()]
138
- .map((failure) => `${failure.jobName}: ${failure.error}`)
139
- .join("; ");
140
- return `job(s) failed: ${details}`;
141
- }
@@ -1,9 +0,0 @@
1
- import type { SseEvent } from "../api/sse-broker.js";
2
- import type { SaveContext } from "../engine/types/index.js";
3
- export type EventCollector = {
4
- readonly sse: SseEvent[];
5
- readonly postSave: SaveContext[];
6
- /** Clears all collected events — call in beforeEach for per-test isolation */
7
- reset(): void;
8
- };
9
- export declare function createEventCollector(): EventCollector;
@@ -1,12 +0,0 @@
1
- export function createEventCollector() {
2
- const sse = [];
3
- const postSave = [];
4
- return {
5
- sse,
6
- postSave,
7
- reset() {
8
- sse.length = 0;
9
- postSave.length = 0;
10
- },
11
- };
12
- }
@@ -1,24 +0,0 @@
1
- import type { DbConnection } from "../db/connection.js";
2
- import type { Registry } from "../engine/types/index.js";
3
- type DbHolder = {
4
- readonly db: DbConnection;
5
- };
6
- /**
7
- * Push all implicit-projection tables — one per `r.entity()` — that the
8
- * registry knows about. setupTestStack already handles explicit
9
- * projections, MSPs, and `r.storeTable()` declarations in its own loop;
10
- * implicit projections are the missing piece for a fresh boot.
11
- *
12
- * Idempotent via `tableExists` so a persistent dev DB
13
- * (`KUMIKO_DEV_DB_NAME`) reuses existing tables on reboot. One batched
14
- * push at the end so drizzle-kit's `generateMigration` runs once over
15
- * the whole missing set.
16
- *
17
- * Lives next to `setupTestStack` because both are stack-bootstrap
18
- * helpers that legitimately speak the `unsafe*`-DDL layer; the
19
- * Table-DDL Guard's stack/** allowlist is the single shared exemption
20
- * site. Apps still declare data via `r.entity()` / `r.storeTable()` and
21
- * never call this directly.
22
- */
23
- export declare function pushEntityProjectionTables(stack: DbHolder, registry: Registry): Promise<void>;
24
- export {};
@@ -1,74 +0,0 @@
1
- import { columnNamesOf, tableExists } from "../db/schema-inspection.js";
2
- import { schemaTableName } from "../db/table-introspection.js";
3
- import { addMissingColumns, addMissingIndexes, tableToMeta, unsafePushTables, } from "./table-helpers.js";
4
- // biome-ignore lint/suspicious/noConsole: stack-internal status logging
5
- const logInfo = (msg) => console.log(msg);
6
- /**
7
- * Push all implicit-projection tables — one per `r.entity()` — that the
8
- * registry knows about. setupTestStack already handles explicit
9
- * projections, MSPs, and `r.storeTable()` declarations in its own loop;
10
- * implicit projections are the missing piece for a fresh boot.
11
- *
12
- * Idempotent via `tableExists` so a persistent dev DB
13
- * (`KUMIKO_DEV_DB_NAME`) reuses existing tables on reboot. One batched
14
- * push at the end so drizzle-kit's `generateMigration` runs once over
15
- * the whole missing set.
16
- *
17
- * Lives next to `setupTestStack` because both are stack-bootstrap
18
- * helpers that legitimately speak the `unsafe*`-DDL layer; the
19
- * Table-DDL Guard's stack/** allowlist is the single shared exemption
20
- * site. Apps still declare data via `r.entity()` / `r.storeTable()` and
21
- * never call this directly.
22
- */
23
- export async function pushEntityProjectionTables(stack, registry) {
24
- const seen = new Set();
25
- const missing = {};
26
- for (const [projName, proj] of registry.getAllProjections()) {
27
- if (!proj.isImplicit)
28
- continue;
29
- if (seen.has(proj.table))
30
- continue;
31
- seen.add(proj.table);
32
- const physical = schemaTableName(proj.table) ?? tableToMeta(proj.table).tableName;
33
- if (!(await tableExists(stack.db, `public.${physical}`))) {
34
- missing[projName] = proj.table;
35
- continue;
36
- }
37
- await syncExistingProjectionTable(stack, physical, proj.table);
38
- }
39
- if (Object.keys(missing).length > 0) {
40
- await unsafePushTables(stack.db, missing);
41
- }
42
- }
43
- function isUnsafeToAutoAdd(col) {
44
- return col.notNull && !col.primaryKey && col.defaultSql === undefined;
45
- }
46
- // Backfills columns and indexes a persistent dev DB predates; a required column with no
47
- // default can't be added safely on a table that may hold rows, so that
48
- // stays a boot error instead.
49
- async function syncExistingProjectionTable(stack, physical, table) {
50
- const meta = tableToMeta(table);
51
- const liveColumns = await columnNamesOf(stack.db, physical);
52
- const missingColumns = meta.columns.filter((c) => !liveColumns.has(c.name));
53
- if (missingColumns.length === 0) {
54
- logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
55
- await addMissingIndexes(stack.db, physical, meta.indexes);
56
- // skip: table is already in sync, only indexes needed backfilling
57
- return;
58
- }
59
- const unsafeColumns = missingColumns.filter(isUnsafeToAutoAdd);
60
- if (unsafeColumns.length > 0) {
61
- const devDbName = process.env["KUMIKO_DEV_DB_NAME"] ?? "<KUMIKO_DEV_DB_NAME>";
62
- throw new Error(`[kumiko-stack] table "${physical}" is missing required column(s) ` +
63
- `${unsafeColumns.map((c) => `"${c.name}"`).join(", ")} with no default. ` +
64
- "Auto-sync only backfills nullable columns or columns with a default on an " +
65
- "existing dev DB — a required column with no default can't be added safely " +
66
- `once the table may hold rows. Drop the persistent dev database "${devDbName}" ` +
67
- `(e.g. \`dropdb ${devDbName}\`, adjusted for your local Postgres host/port/user) ` +
68
- "so the next boot recreates it from scratch.");
69
- }
70
- logInfo(`[kumiko-stack] table ${physical} is missing column(s) ` +
71
- `${missingColumns.map((c) => `"${c.name}"`).join(", ")} — adding`);
72
- await addMissingColumns(stack.db, physical, missingColumns);
73
- await addMissingIndexes(stack.db, physical, meta.indexes);
74
- }
@@ -1,17 +0,0 @@
1
- export type TestRedis = {
2
- redis: import("ioredis").Redis;
3
- redisUrl: string;
4
- /** The per-test `keyPrefix` on `redis`, e.g. `kt:1a2b3c4d:`. */
5
- keyPrefix: string;
6
- /** Delete every key this test created (prefix-scoped). Replaces the old
7
- * `redis.flushdb()` — that wiped other parallel tests' BullMQ state. */
8
- flushNamespace: () => Promise<void>;
9
- cleanup: () => Promise<void>;
10
- };
11
- export declare function queueNamePrefixForTestRedis(keyPrefix: string): string;
12
- export type CreateTestRedisOptions = {
13
- /** Reuse another stack's keyPrefix on a fresh connection. cleanup() then only
14
- * disconnects — the owning stack flushes the namespace and its queues. */
15
- readonly borrowKeyPrefix?: string;
16
- };
17
- export declare function createTestRedis(opts?: CreateTestRedisOptions): Promise<TestRedis>;
@@ -1,52 +0,0 @@
1
- import { generateId } from "../utils/index.js";
2
- import { requireEnv } from "./db.js";
3
- // BullMQ queues live on the raw redisUrl, outside the test Redis keyPrefix, so
4
- // parallel stacks sharing the prod default queue name would consume each
5
- // other's jobs. BullMQ rejects `:` in queue names.
6
- export function queueNamePrefixForTestRedis(keyPrefix) {
7
- return keyPrefix.split(":").filter(Boolean).join("-");
8
- }
9
- export async function createTestRedis(opts = {}) {
10
- const Redis = (await import("ioredis")).Redis;
11
- const redisUrl = requireEnv("REDIS_URL");
12
- // Every test gets a per-file key prefix on a shared DB (no DB-pool-of-15
13
- // round-robin). Collisions at birthday-paradox rates are gone — the
14
- // prefix space is unbounded. See Track B.3 in docs/plans/tests-refactor.
15
- const prefix = opts.borrowKeyPrefix ?? `kt:${generateId().slice(-8)}:`;
16
- const redis = new Redis(redisUrl, { keyPrefix: prefix });
17
- async function deleteKeysMatching(pattern) {
18
- // Open a prefix-less client for the scan — ioredis' keyPrefix is applied
19
- // per-command but SCAN's returned keys are full names, so managing the
20
- // del set with the prefix already on the connection is error-prone.
21
- const raw = new Redis(redisUrl);
22
- try {
23
- const stream = raw.scanStream({ match: pattern, count: 500 });
24
- const keys = [];
25
- for await (const batch of stream)
26
- keys.push(...batch);
27
- if (keys.length > 0)
28
- await raw.del(...keys);
29
- }
30
- finally {
31
- raw.disconnect();
32
- }
33
- }
34
- const flushNamespace = () => deleteKeysMatching(`${prefix}*`);
35
- return {
36
- redis,
37
- redisUrl,
38
- keyPrefix: prefix,
39
- flushNamespace,
40
- cleanup: async () => {
41
- if (opts.borrowKeyPrefix !== undefined) {
42
- redis.disconnect();
43
- // skip: the owning stack flushes the shared namespace and its queues
44
- return;
45
- }
46
- await flushNamespace();
47
- // The derived per-stack JobRunner queues sit outside the keyPrefix.
48
- await deleteKeysMatching(`bull:${queueNamePrefixForTestRedis(prefix)}-*`);
49
- redis.disconnect();
50
- },
51
- };
52
- }