@checkstack/test-utils-backend 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @checkstack/test-utils-backend
2
2
 
3
+ ## 0.1.46
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [8cad340]
8
+ - Updated dependencies [8cad340]
9
+ - Updated dependencies [8cad340]
10
+ - Updated dependencies [8cad340]
11
+ - Updated dependencies [8cad340]
12
+ - Updated dependencies [8cad340]
13
+ - Updated dependencies [8cad340]
14
+ - @checkstack/backend-api@0.25.0
15
+ - @checkstack/common@0.17.0
16
+ - @checkstack/queue-api@0.3.14
17
+ - @checkstack/signal-common@0.2.11
18
+
19
+ ## 0.1.45
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [2ec8f64]
24
+ - @checkstack/backend-api@0.24.1
25
+
3
26
  ## 0.1.44
4
27
 
5
28
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/test-utils-backend",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "license": "Elastic-2.0",
5
5
  "checkstack": {
6
6
  "type": "tooling"
@@ -13,15 +13,18 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/backend-api": "0.24.0",
17
- "@checkstack/common": "0.16.0",
18
- "@checkstack/queue-api": "0.3.13",
19
- "@checkstack/signal-common": "0.2.10"
16
+ "@checkstack/backend-api": "0.25.0",
17
+ "@checkstack/common": "0.17.0",
18
+ "@checkstack/queue-api": "0.3.14",
19
+ "@checkstack/signal-common": "0.2.11",
20
+ "drizzle-orm": "^0.45.0",
21
+ "pg": "^8.21.0"
20
22
  },
21
23
  "devDependencies": {
24
+ "@checkstack/scripts": "0.6.3",
22
25
  "@checkstack/tsconfig": "0.0.7",
23
- "@checkstack/scripts": "0.6.2",
24
26
  "@types/bun": "latest",
27
+ "@types/pg": "^8.20.0",
25
28
  "zod": "^4.0.0"
26
29
  }
27
30
  }
package/src/index.ts CHANGED
@@ -12,3 +12,9 @@ export {
12
12
  type MockEventBus,
13
13
  type EmittedEvent,
14
14
  } from "./mock-event-bus";
15
+ export {
16
+ withTestDb,
17
+ isIntegrationEnabled,
18
+ type TestDb,
19
+ type WithTestDbOptions,
20
+ } from "./with-test-db";
@@ -0,0 +1,130 @@
1
+ import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
3
+ import { Pool } from "pg";
4
+ import type { SafeDatabase } from "@checkstack/backend-api";
5
+
6
+ /**
7
+ * A real-Postgres test database, isolated in a throwaway schema, for
8
+ * `*.it.test.ts` query-correctness tests. Use this when the SQL itself is the
9
+ * risk (team-scoping/visibility filters, joins, aggregations) — the
10
+ * `createMockDb()` stub returns canned empty results and cannot verify query
11
+ * shape, so it proves branch logic only, not the SQL.
12
+ *
13
+ * Mirrors `core/e2e/scripts/start-e2e-server.ts` and the existing IT tests'
14
+ * per-run schema isolation: every call creates a uniquely named schema, runs
15
+ * the package's Drizzle migrations into it with a strict `search_path`, hands
16
+ * back a `SafeDatabase` bound to that schema, and tears it all down on
17
+ * `dispose()`.
18
+ */
19
+ export interface TestDb<S extends Record<string, unknown>> {
20
+ /** A `SafeDatabase` bound to the isolated schema (relational API omitted). */
21
+ readonly db: SafeDatabase<S>;
22
+ /** The unique, throwaway schema name this database lives in. */
23
+ readonly schema: string;
24
+ /** Drop the schema and close the pool. Always call this in `afterAll`/`afterEach`. */
25
+ dispose(): Promise<void>;
26
+ }
27
+
28
+ export interface WithTestDbOptions<S extends Record<string, unknown>> {
29
+ /**
30
+ * The package's Drizzle schema object (the `* as schema` import). Passed to
31
+ * `drizzle({ schema })` so the returned `db` is fully typed.
32
+ */
33
+ schema: S;
34
+ /**
35
+ * Absolute path to the package's `drizzle/` migrations folder (the one with
36
+ * the `.sql` files + `meta/`). Run the package generator to keep it in sync;
37
+ * never hand-edit applied migrations (see `.claude/rules/migrations.md`).
38
+ */
39
+ migrationsFolder: string;
40
+ /**
41
+ * Postgres connection string. Defaults to `CHECKSTACK_IT_PG_URL`, matching the
42
+ * existing IT lane + the CI `integration` job.
43
+ */
44
+ connectionString?: string;
45
+ /** Override the generated schema name (mostly for deterministic debugging). */
46
+ schemaName?: string;
47
+ }
48
+
49
+ const DEFAULT_PG_URL = "postgres://checkstack:checkstack@localhost:5432/checkstack";
50
+
51
+ /**
52
+ * True only when the integration lane is enabled. Suites that use
53
+ * {@link withTestDb} MUST wrap themselves in
54
+ * `describe.skipIf(!isIntegrationEnabled())(...)` (or the `CHECKSTACK_IT`
55
+ * env check) so the default `bun test` never tries to reach a real database.
56
+ */
57
+ export function isIntegrationEnabled(): boolean {
58
+ return Boolean(process.env.CHECKSTACK_IT);
59
+ }
60
+
61
+ /**
62
+ * Provision an isolated, migration-fresh test database. Throws if the
63
+ * integration lane is not enabled, so a misconfigured suite fails loudly
64
+ * instead of silently hitting a developer's working database.
65
+ */
66
+ export async function withTestDb<S extends Record<string, unknown>>(
67
+ options: WithTestDbOptions<S>,
68
+ ): Promise<TestDb<S>> {
69
+ if (!isIntegrationEnabled()) {
70
+ throw new Error(
71
+ "withTestDb() requires CHECKSTACK_IT=1 and a real Postgres. Wrap the " +
72
+ "suite in describe.skipIf(!isIntegrationEnabled()) so the default " +
73
+ "`bun test` lane never reaches a database.",
74
+ );
75
+ }
76
+
77
+ const connectionString =
78
+ options.connectionString ??
79
+ process.env.CHECKSTACK_IT_PG_URL ??
80
+ DEFAULT_PG_URL;
81
+
82
+ const schema =
83
+ options.schemaName ??
84
+ `it_${crypto.randomUUID().replaceAll("-", "")}`;
85
+
86
+ // A dedicated pool whose every connection resolves the throwaway schema
87
+ // first. Binding via the pool `options` keeps the search_path on every
88
+ // physical connection the pool hands out (a session-level SET on a Pool is
89
+ // not guaranteed to land on the same connection a later query checks out).
90
+ const pool = new Pool({
91
+ connectionString,
92
+ options: `-c search_path=${schema}`,
93
+ });
94
+
95
+ // The migrator must run with the schema present and the search_path pinned to
96
+ // a single connection (Drizzle wraps all pending migrations in one
97
+ // transaction), mirroring runPluginMigrations().
98
+ const migrationClient = await pool.connect();
99
+ try {
100
+ await migrationClient.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
101
+ await migrationClient.query(`SET search_path = "${schema}"`);
102
+ await migrate(drizzle(migrationClient), {
103
+ migrationsFolder: options.migrationsFolder,
104
+ migrationsSchema: schema,
105
+ });
106
+ } finally {
107
+ migrationClient.release();
108
+ }
109
+
110
+ // `drizzle()` returns a NodePgDatabase<S>; SafeDatabase<S> is that minus the
111
+ // relational `query` API. The cast strips it at the type level (the same
112
+ // pattern the existing IT tests use); the scoped-db runtime proxy blocks the
113
+ // relational API in production, and tests simply never call it.
114
+ const fullDb: NodePgDatabase<S> = drizzle({ client: pool, schema: options.schema });
115
+ const db = fullDb as unknown as SafeDatabase<S>;
116
+
117
+ return {
118
+ db,
119
+ schema,
120
+ dispose: async () => {
121
+ const cleanup = await pool.connect();
122
+ try {
123
+ await cleanup.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
124
+ } finally {
125
+ cleanup.release();
126
+ }
127
+ await pool.end();
128
+ },
129
+ };
130
+ }