@stratal/testing 0.0.0-canary-ff3a406 → 0.0.0-canary-ccb3f17
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/dist/database/index.d.mts +2 -2
- package/dist/database/index.mjs +2 -2
- package/dist/{database-B02eYKhE.mjs → database-DeMI4Igi.mjs} +158 -91
- package/dist/database-DeMI4Igi.mjs.map +1 -0
- package/dist/{decorate-B7nr7eBl.mjs → decorate-ZDdbRmsv.mjs} +1 -1
- package/dist/feature-flags/index.mjs +1 -1
- package/dist/{feature-flags-BiLhfSGh.mjs → feature-flags-BMYurC1B.mjs} +2 -2
- package/dist/{feature-flags-BiLhfSGh.mjs.map → feature-flags-BMYurC1B.mjs.map} +1 -1
- package/dist/index-DUScdrKC.d.mts +162 -0
- package/dist/index-DUScdrKC.d.mts.map +1 -0
- package/dist/{index-CrHzUDKX.d.mts → index-Dm6e2N61.d.mts} +1 -2
- package/dist/index-Dm6e2N61.d.mts.map +1 -0
- package/dist/index-qgWNJRdC.d.mts.map +1 -1
- package/dist/index.d.mts +57 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -77
- package/dist/index.mjs.map +1 -1
- package/dist/mocks/cloudflare-workers.d.mts +25 -0
- package/dist/mocks/cloudflare-workers.d.mts.map +1 -0
- package/dist/mocks/cloudflare-workers.mjs +28 -0
- package/dist/mocks/cloudflare-workers.mjs.map +1 -0
- package/dist/mocks/index.d.mts +2 -2
- package/dist/mocks/index.mjs +2 -2
- package/dist/mocks/noop-rate-limiter-store.d.mts +0 -1
- package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -1
- package/dist/mocks/zenstack-language.d.mts +5 -8
- package/dist/mocks/zenstack-language.d.mts.map +1 -1
- package/dist/storage/index.d.mts +1 -1
- package/dist/storage/index.mjs +1 -1
- package/dist/{storage-DhoxWqyF.mjs → storage-q8RFahgU.mjs} +6 -6
- package/dist/storage-q8RFahgU.mjs.map +1 -0
- package/dist/test-worker-exports-DMct0Egv.d.mts +143 -0
- package/dist/test-worker-exports-DMct0Egv.d.mts.map +1 -0
- package/dist/test-workers-cache-eiGMw9Yj.mjs +182 -0
- package/dist/test-workers-cache-eiGMw9Yj.mjs.map +1 -0
- package/dist/vitest-plugin/index.d.mts +8 -22
- package/dist/vitest-plugin/index.d.mts.map +1 -1
- package/dist/vitest-plugin/index.mjs +18 -13
- package/dist/vitest-plugin/index.mjs.map +1 -1
- package/package.json +21 -15
- package/dist/database-B02eYKhE.mjs.map +0 -1
- package/dist/index-BIr5nLof.d.mts +0 -122
- package/dist/index-BIr5nLof.d.mts.map +0 -1
- package/dist/index-CrHzUDKX.d.mts.map +0 -1
- package/dist/storage-DhoxWqyF.mjs.map +0 -1
- package/dist/test-email-provider-B7cjj97-.d.mts +0 -21
- package/dist/test-email-provider-B7cjj97-.d.mts.map +0 -1
- package/dist/test-email-provider-Dr-nhE0x.mjs +0 -34
- package/dist/test-email-provider-Dr-nhE0x.mjs.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { BINDING_ENV_VAR, DEFAULT_DB_BINDING, type
|
|
1
|
+
import { a as BINDING_ENV_VAR, c as buildConnectionString, d as deriveAdminConnectionString, f as deriveFileDbName, i as resetWorkerDatabase, l as createTestDatabaseGlobalSetup, m as ensureWorkerDatabase, n as buildTableDiscoverySql, o as DEFAULT_DB_BINDING, p as deriveTemplateName, r as buildTruncateSql, s as TestDatabaseGlobalSetupOptions, t as ResetOptions, u as databasePrefix } from "../index-DUScdrKC.mjs";
|
|
2
|
+
export { BINDING_ENV_VAR, DEFAULT_DB_BINDING, type ResetOptions, type TestDatabaseGlobalSetupOptions, buildConnectionString, buildTableDiscoverySql, buildTruncateSql, createTestDatabaseGlobalSetup, databasePrefix, deriveAdminConnectionString, deriveFileDbName, deriveTemplateName, ensureWorkerDatabase, resetWorkerDatabase };
|
package/dist/database/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { BINDING_ENV_VAR, DEFAULT_DB_BINDING,
|
|
1
|
+
import { a as DEFAULT_DB_BINDING, c as databasePrefix, d as deriveTemplateName, f as ensureWorkerDatabase, i as BINDING_ENV_VAR, l as deriveAdminConnectionString, n as buildTruncateSql, o as buildConnectionString, r as resetWorkerDatabase, s as createTestDatabaseGlobalSetup, t as buildTableDiscoverySql, u as deriveFileDbName } from "../database-DeMI4Igi.mjs";
|
|
2
|
+
export { BINDING_ENV_VAR, DEFAULT_DB_BINDING, buildConnectionString, buildTableDiscoverySql, buildTruncateSql, createTestDatabaseGlobalSetup, databasePrefix, deriveAdminConnectionString, deriveFileDbName, deriveTemplateName, ensureWorkerDatabase, resetWorkerDatabase };
|
|
@@ -1,33 +1,23 @@
|
|
|
1
|
-
import { createHash
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
//#region src/database/test-database.ts
|
|
5
5
|
/**
|
|
6
6
|
* Test database isolation helpers.
|
|
7
7
|
*
|
|
8
|
-
* Implements database-per-
|
|
8
|
+
* Implements database-per-worker isolation for parallel e2e runs against
|
|
9
9
|
* Postgres. A migrated **template** database is built once in global setup;
|
|
10
|
-
* each
|
|
11
|
-
*
|
|
10
|
+
* each worker clones it once into a per-worker database that is reused (and
|
|
11
|
+
* reset between tests, not dropped per file). See `@stratal/testing/database`.
|
|
12
12
|
*
|
|
13
13
|
* `pg` is imported dynamically so this module loads without it — consumers
|
|
14
14
|
* that don't use a database never pay the dependency.
|
|
15
15
|
*/
|
|
16
|
-
/** Env var that selects the isolation mode (single source of truth). */
|
|
17
|
-
const ISOLATION_ENV_VAR = "STRATAL_TEST_DB_ISOLATION";
|
|
18
16
|
/** Env var carrying the name of the Hyperdrive binding to isolate. */
|
|
19
17
|
const BINDING_ENV_VAR = "STRATAL_TEST_DB_BINDING";
|
|
20
18
|
/** Default Hyperdrive binding name when none is configured. */
|
|
21
19
|
const DEFAULT_DB_BINDING = "DB";
|
|
22
20
|
/**
|
|
23
|
-
* Normalize an isolation value (from an option or env var) to a mode.
|
|
24
|
-
* Anything other than the literal `'database'` resolves to `'shared'` — the
|
|
25
|
-
* default — so parallel isolation is strictly opt-in.
|
|
26
|
-
*/
|
|
27
|
-
function normalizeIsolation(value) {
|
|
28
|
-
return value === "database" ? "database" : "shared";
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
21
|
* Postgres' identifier length limit. Names longer than this are silently
|
|
32
22
|
* truncated by the server, which would cause distinct logical names to collide
|
|
33
23
|
* on the same physical database. We assert against it everywhere a name is
|
|
@@ -35,7 +25,7 @@ function normalizeIsolation(value) {
|
|
|
35
25
|
*/
|
|
36
26
|
const MAX_IDENTIFIER_LENGTH = 63;
|
|
37
27
|
/** Quote a Postgres identifier for safe interpolation into DDL. */
|
|
38
|
-
function quoteIdent(name) {
|
|
28
|
+
function quoteIdent$1(name) {
|
|
39
29
|
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
40
30
|
}
|
|
41
31
|
/** Quote a Postgres string literal for safe interpolation into SQL. */
|
|
@@ -47,7 +37,7 @@ function quoteLiteral(value) {
|
|
|
47
37
|
* instead of a raw module-not-found when database isolation is requested
|
|
48
38
|
* without the dependency installed.
|
|
49
39
|
*/
|
|
50
|
-
async function importPg() {
|
|
40
|
+
async function importPg$1() {
|
|
51
41
|
try {
|
|
52
42
|
const { default: pg } = await import("pg");
|
|
53
43
|
return pg;
|
|
@@ -83,31 +73,31 @@ function buildConnectionString(base, dbName) {
|
|
|
83
73
|
url.pathname = `/${dbName}`;
|
|
84
74
|
return url.toString();
|
|
85
75
|
}
|
|
86
|
-
/** Length of the random token appended by {@link deriveDbName}. */
|
|
87
|
-
const DB_NAME_TOKEN_LENGTH = 12;
|
|
88
76
|
/** The shared prefix for per-file databases, used as the leak-sweep key. */
|
|
89
77
|
function databasePrefix(base) {
|
|
90
|
-
|
|
91
|
-
assertIdentifierLength(`${prefix}${"x".repeat(DB_NAME_TOKEN_LENGTH)}`, "per-file database name");
|
|
92
|
-
return prefix;
|
|
78
|
+
return `${databaseNameOf(base).replace(/[^a-z0-9_]/gi, "_")}_f_`;
|
|
93
79
|
}
|
|
94
|
-
/** Name of the migrated template database cloned per
|
|
80
|
+
/** Name of the migrated template database cloned per worker. */
|
|
95
81
|
function deriveTemplateName(base) {
|
|
96
82
|
const name = `${databaseNameOf(base).replace(/[^a-z0-9_]/gi, "_")}_template`;
|
|
97
83
|
assertIdentifierLength(name, "template database name");
|
|
98
84
|
return name;
|
|
99
85
|
}
|
|
100
86
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
87
|
+
* Name of the database owned by a single test FILE, keyed by a per-file `token`.
|
|
88
|
+
* Each test file clones the template into its OWN database — no two files ever
|
|
89
|
+
* share one — because `@cloudflare/vitest-pool-workers` isolates per file and can
|
|
90
|
+
* run a worker's files concurrently; per-file databases make cross-file
|
|
91
|
+
* contamination impossible by construction. Asserted against Postgres' 63-char
|
|
92
|
+
* identifier limit (keep the base name short so `_f_<token>` fits).
|
|
104
93
|
*/
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
|
|
94
|
+
function deriveFileDbName(base, token) {
|
|
95
|
+
const name = `${databaseNameOf(base).replace(/[^a-z0-9_]/gi, "_")}_f_${token}`;
|
|
96
|
+
assertIdentifierLength(name, "per-file database name");
|
|
97
|
+
return name;
|
|
108
98
|
}
|
|
109
99
|
async function withAdminClient(adminConn, fn) {
|
|
110
|
-
const client = new (await (importPg())).Client({ connectionString: adminConn });
|
|
100
|
+
const client = new (await (importPg$1())).Client({ connectionString: adminConn });
|
|
111
101
|
await client.connect();
|
|
112
102
|
try {
|
|
113
103
|
return await fn((sql) => client.query(sql));
|
|
@@ -115,43 +105,63 @@ async function withAdminClient(adminConn, fn) {
|
|
|
115
105
|
await client.end();
|
|
116
106
|
}
|
|
117
107
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const e = error;
|
|
122
|
-
return e?.code === "55006" || /is being accessed by other users/i.test(e?.message ?? "");
|
|
108
|
+
/** True when the error is Postgres' "duplicate database" (concurrent create). */
|
|
109
|
+
function isDuplicateDatabase(error) {
|
|
110
|
+
return error?.code === "42P04";
|
|
123
111
|
}
|
|
124
112
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
113
|
+
* True for SQLSTATE 55006 ("source database is being accessed by other users") —
|
|
114
|
+
* what `CREATE DATABASE ... TEMPLATE t` raises while another session is using the
|
|
115
|
+
* template (e.g. a concurrent clone of the same template).
|
|
128
116
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
function isTemplateInUse(error) {
|
|
118
|
+
return error?.code === "55006";
|
|
119
|
+
}
|
|
120
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
121
|
+
/** True when a database with `dbName` already exists. */
|
|
122
|
+
async function databaseExists(query, dbName) {
|
|
123
|
+
const { rows } = await query(`SELECT 1 AS one FROM pg_database WHERE datname = ${quoteLiteral(dbName)}`);
|
|
124
|
+
return rows.length > 0;
|
|
131
125
|
}
|
|
132
126
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
127
|
+
* Ensure worker database `dbName` exists, cloned from `template`. Idempotent and
|
|
128
|
+
* lock-frugal: it first checks `pg_database`, so the common "already exists" path
|
|
129
|
+
* (every file after a slot's first) never issues `CREATE DATABASE ... TEMPLATE`
|
|
130
|
+
* and never takes the clone lock.
|
|
131
|
+
*
|
|
132
|
+
* The clone is serialized across all workers and processes by one Postgres
|
|
133
|
+
* advisory lock keyed on the template. Postgres permits only one
|
|
134
|
+
* `CREATE DATABASE ... TEMPLATE t` at a time — a concurrent one fails with
|
|
135
|
+
* SQLSTATE 55006. On a fresh run every worker's first compile races to clone the
|
|
136
|
+
* same template, so without this they'd all fire `CREATE ... TEMPLATE` at once
|
|
137
|
+
* and all but one would fail their first file. The lock funnels them one-at-a-
|
|
138
|
+
* time, each blocking *in Postgres* (not busy-waiting) until its turn. A create
|
|
139
|
+
* that still hits a transient 55006 is retried with backoff; one that loses the
|
|
140
|
+
* race for the same name (SQLSTATE 42P04) is treated as success.
|
|
137
141
|
*/
|
|
138
|
-
async function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
async function ensureWorkerDatabase(adminConn, dbName, template) {
|
|
143
|
+
await withAdminClient(adminConn, async (query) => {
|
|
144
|
+
if (await databaseExists(query, dbName)) return;
|
|
145
|
+
const lockKey = quoteLiteral(`stratal:worker-db-clone:${template}`);
|
|
146
|
+
await query(`SELECT pg_advisory_lock(hashtext(${lockKey}))`);
|
|
147
|
+
try {
|
|
148
|
+
if (await databaseExists(query, dbName)) return;
|
|
149
|
+
const create = `CREATE DATABASE ${quoteIdent$1(dbName)} TEMPLATE ${quoteIdent$1(template)}`;
|
|
150
|
+
for (let attempt = 1;; attempt++) try {
|
|
151
|
+
await query(create);
|
|
152
|
+
return;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (isDuplicateDatabase(error)) return;
|
|
155
|
+
if (isTemplateInUse(error) && attempt < 5) {
|
|
156
|
+
await sleep(250 * attempt);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
await query(`SELECT pg_advisory_unlock(hashtext(${lockKey}))`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
155
165
|
}
|
|
156
166
|
/**
|
|
157
167
|
* Drop leaked per-file databases matching the prefix while leaving a concurrent
|
|
@@ -169,7 +179,7 @@ async function sweepStaleDatabases(adminConn, prefix) {
|
|
|
169
179
|
const likePrefix = prefix.replace(/'/g, "''").replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
170
180
|
await withAdminClient(adminConn, async (query) => {
|
|
171
181
|
const { rows } = await query(`SELECT d.datname FROM pg_database d WHERE d.datname LIKE '${likePrefix}%' ESCAPE '\\' AND NOT EXISTS (SELECT 1 FROM pg_stat_activity a WHERE a.datname = d.datname)`);
|
|
172
|
-
for (const { datname } of rows) await query(`DROP DATABASE IF EXISTS ${quoteIdent(datname)} WITH (FORCE)`);
|
|
182
|
+
for (const { datname } of rows) await query(`DROP DATABASE IF EXISTS ${quoteIdent$1(datname)} WITH (FORCE)`);
|
|
173
183
|
});
|
|
174
184
|
}
|
|
175
185
|
/**
|
|
@@ -236,12 +246,15 @@ function collectSchemaFiles(path) {
|
|
|
236
246
|
return [path];
|
|
237
247
|
}
|
|
238
248
|
/**
|
|
239
|
-
* A content-derived fingerprint of the schema source(s) plus the migrate
|
|
240
|
-
*
|
|
241
|
-
* to a schema file — or to how migration
|
|
242
|
-
* Uses file basenames (not absolute paths)
|
|
249
|
+
* A content-derived fingerprint of the schema source(s) plus the migrate and
|
|
250
|
+
* prepare routines. The template is reused across runs while this is
|
|
251
|
+
* unchanged; any edit to a schema file — or to how migration/preparation run
|
|
252
|
+
* — changes it and forces a rebuild. Uses file basenames (not absolute paths)
|
|
253
|
+
* so it is stable across checkouts.
|
|
254
|
+
*
|
|
255
|
+
* @internal exported for fingerprint unit tests
|
|
243
256
|
*/
|
|
244
|
-
function computeSchemaFingerprint(schema, migrate) {
|
|
257
|
+
function computeSchemaFingerprint(schema, migrate, prepare) {
|
|
245
258
|
const roots = Array.isArray(schema) ? schema : [schema];
|
|
246
259
|
const files = roots.flatMap(collectSchemaFiles).sort();
|
|
247
260
|
if (files.length === 0) throw new Error(`[stratal-testing] No schema files found for fingerprinting under: ${roots.join(", ")}`);
|
|
@@ -253,6 +266,8 @@ function computeSchemaFingerprint(schema, migrate) {
|
|
|
253
266
|
hash.update("\0");
|
|
254
267
|
}
|
|
255
268
|
hash.update(migrate.toString());
|
|
269
|
+
hash.update("\0");
|
|
270
|
+
hash.update(prepare ? prepare.toString() : "");
|
|
256
271
|
return hash.digest("hex");
|
|
257
272
|
}
|
|
258
273
|
/**
|
|
@@ -268,24 +283,23 @@ async function readTemplateFingerprint(query, template) {
|
|
|
268
283
|
/**
|
|
269
284
|
* Build a Vitest `globalSetup` default export that prepares the test database.
|
|
270
285
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* cloned per test file.
|
|
286
|
+
* Under a Postgres advisory lock (so concurrent setups across CI shards /
|
|
287
|
+
* multiple e2e projects don't clobber each other), sweeps leaked per-worker
|
|
288
|
+
* databases, then ensures a migrated (and optionally prepared) template exists
|
|
289
|
+
* — ready to be cloned per worker.
|
|
276
290
|
*
|
|
277
291
|
* **Template reuse.** The template is fingerprinted from the `schema` source(s)
|
|
278
|
-
* + the `migrate` routine
|
|
279
|
-
* database COMMENT. On each run, a matching
|
|
280
|
-
*
|
|
281
|
-
* on the first run after
|
|
282
|
-
* is stamped only after a successful
|
|
283
|
-
*
|
|
284
|
-
* driven.
|
|
292
|
+
* + the `migrate` routine + the `prepare` routine, and the fingerprint is
|
|
293
|
+
* stored as the template's database COMMENT. On each run, a matching
|
|
294
|
+
* fingerprint means nothing has changed and the existing template is reused
|
|
295
|
+
* as-is — `migrate`/`prepare` run **only** on the first run after an edit (or on
|
|
296
|
+
* a fresh database). The fingerprint is stamped only after a successful
|
|
297
|
+
* migrate + prepare, so a match always implies a complete template; there is no
|
|
298
|
+
* force/skip flag — reuse is purely fingerprint-driven.
|
|
285
299
|
*
|
|
286
300
|
* **Concurrency model.** The reuse check + rebuild runs under
|
|
287
301
|
* `pg_advisory_lock(hashtext(<template>))`, so only one process rebuilds the
|
|
288
|
-
* template at a time. The stale-database sweep only drops per-
|
|
302
|
+
* template at a time. The stale-database sweep only drops per-worker databases
|
|
289
303
|
* with **no active connections**, leaving a sibling process's live databases
|
|
290
304
|
* intact. Teardown deliberately does **not** sweep or drop the template: a
|
|
291
305
|
* concurrent process may still be using both, and the next run's setup sweep is
|
|
@@ -307,28 +321,81 @@ function createTestDatabaseGlobalSetup(opts) {
|
|
|
307
321
|
return async () => {
|
|
308
322
|
const base = opts.connectionString ?? process.env.DATABASE_URL;
|
|
309
323
|
if (!base) throw new Error("[stratal-testing] No connection string for test database setup. Set process.env.DATABASE_URL or pass `connectionString`.");
|
|
310
|
-
if (normalizeIsolation(opts.isolation ?? process.env["STRATAL_TEST_DB_ISOLATION"]) === "shared") {
|
|
311
|
-
await opts.migrate(base);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (!opts.schema) throw new Error("[stratal-testing] `schema` is required for database isolation. Pass the schema file(s) or directory so the migrated template can be reused across runs when unchanged (and rebuilt when it changes).");
|
|
315
324
|
const adminConn = deriveAdminConnectionString(base);
|
|
316
325
|
const template = opts.templateName ?? deriveTemplateName(base);
|
|
317
326
|
const prefix = databasePrefix(base);
|
|
318
|
-
const fingerprint = computeSchemaFingerprint(opts.schema, opts.migrate);
|
|
327
|
+
const fingerprint = computeSchemaFingerprint(opts.schema, opts.migrate, opts.prepare);
|
|
319
328
|
await withAdvisoryLock(adminConn, template, async () => {
|
|
320
329
|
await sweepStaleDatabases(adminConn, prefix);
|
|
321
330
|
if (await withAdminClient(adminConn, (query) => readTemplateFingerprint(query, template)) === fingerprint) return;
|
|
322
331
|
await withAdminClient(adminConn, async (query) => {
|
|
323
|
-
await query(`DROP DATABASE IF EXISTS ${quoteIdent(template)} WITH (FORCE)`);
|
|
324
|
-
await query(`CREATE DATABASE ${quoteIdent(template)}`);
|
|
332
|
+
await query(`DROP DATABASE IF EXISTS ${quoteIdent$1(template)} WITH (FORCE)`);
|
|
333
|
+
await query(`CREATE DATABASE ${quoteIdent$1(template)}`);
|
|
325
334
|
});
|
|
326
|
-
|
|
327
|
-
await
|
|
335
|
+
const templateConn = buildConnectionString(base, template);
|
|
336
|
+
await opts.migrate(templateConn);
|
|
337
|
+
if (opts.prepare) await opts.prepare(templateConn);
|
|
338
|
+
await withAdminClient(adminConn, (query) => query(`COMMENT ON DATABASE ${quoteIdent$1(template)} IS ${quoteLiteral(fingerprint)}`));
|
|
328
339
|
});
|
|
329
340
|
};
|
|
330
341
|
}
|
|
331
342
|
//#endregion
|
|
332
|
-
|
|
343
|
+
//#region src/database/reset.ts
|
|
344
|
+
function quoteIdent(ident) {
|
|
345
|
+
return `"${ident.replace(/"/g, "\"\"")}"`;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Build a single `TRUNCATE ... RESTART IDENTITY CASCADE` (empty when no tables).
|
|
349
|
+
* Takes `{ schema, table }` pairs and quotes each part independently, so a
|
|
350
|
+
* dotted identifier (a legal quoted name, e.g. `"my.table"`) is preserved
|
|
351
|
+
* verbatim rather than mis-split into extra qualifier segments.
|
|
352
|
+
*/
|
|
353
|
+
function buildTruncateSql(tables) {
|
|
354
|
+
if (tables.length === 0) return "";
|
|
355
|
+
return `TRUNCATE ${tables.map((t) => `${quoteIdent(t.schema)}.${quoteIdent(t.table)}`).join(", ")} RESTART IDENTITY CASCADE`;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Sentinel meaning "the active search-path schema" (`current_schema()`). A
|
|
359
|
+
* Symbol so it can never collide with a real schema name — a schema literally
|
|
360
|
+
* named `current` is passed through as the string literal `'current'`.
|
|
361
|
+
*/
|
|
362
|
+
const CURRENT_SCHEMA = Symbol("current_schema");
|
|
363
|
+
/**
|
|
364
|
+
* Build the SQL that discovers truncatable tables: every base table in the
|
|
365
|
+
* given schemas ({@link CURRENT_SCHEMA} → `current_schema()`), excluding
|
|
366
|
+
* migration bookkeeping (`_prisma%`) and any caller-supplied preserve patterns.
|
|
367
|
+
* Returns a SELECT of `schemaname, tablename`. Escaping lives here so both reset
|
|
368
|
+
* paths share one implementation.
|
|
369
|
+
*/
|
|
370
|
+
function buildTableDiscoverySql(schemas, preserve) {
|
|
371
|
+
return `SELECT schemaname::text AS schemaname, tablename::text AS tablename FROM pg_tables WHERE schemaname IN (${[CURRENT_SCHEMA, ...schemas].map((s) => typeof s === "symbol" ? "current_schema()" : `'${s.replace(/'/g, "''")}'`).join(", ")}) AND ${["_prisma%", ...preserve].map((p) => `tablename NOT LIKE '${p.replace(/'/g, "''")}'`).join(" AND ")}`;
|
|
372
|
+
}
|
|
373
|
+
async function importPg() {
|
|
374
|
+
const { default: mod } = await import("pg");
|
|
375
|
+
return mod;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Reset a worker database between tests: discover every non-preserved table in
|
|
379
|
+
* the target schemas and truncate them in one statement. Fast because the
|
|
380
|
+
* schema/DDL is baked into the worker DB — reset only clears mutable rows and
|
|
381
|
+
* resets identities. Opens and closes a short-lived direct `pg` connection.
|
|
382
|
+
*/
|
|
383
|
+
async function resetWorkerDatabase(connectionString, opts = {}) {
|
|
384
|
+
const client = new (await (importPg())).Client({ connectionString });
|
|
385
|
+
await client.connect();
|
|
386
|
+
try {
|
|
387
|
+
const sql = buildTableDiscoverySql(opts.schemas ?? [], opts.preserve ?? []);
|
|
388
|
+
const { rows } = await client.query(sql);
|
|
389
|
+
const truncateSql = buildTruncateSql(rows.map((r) => ({
|
|
390
|
+
schema: r.schemaname,
|
|
391
|
+
table: r.tablename
|
|
392
|
+
})));
|
|
393
|
+
if (truncateSql) await client.query(truncateSql);
|
|
394
|
+
} finally {
|
|
395
|
+
await client.end();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
export { DEFAULT_DB_BINDING as a, databasePrefix as c, deriveTemplateName as d, ensureWorkerDatabase as f, BINDING_ENV_VAR as i, deriveAdminConnectionString as l, buildTruncateSql as n, buildConnectionString as o, resetWorkerDatabase as r, createTestDatabaseGlobalSetup as s, buildTableDiscoverySql as t, deriveFileDbName as u };
|
|
333
400
|
|
|
334
|
-
//# sourceMappingURL=database-
|
|
401
|
+
//# sourceMappingURL=database-DeMI4Igi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-DeMI4Igi.mjs","names":["quoteIdent","importPg"],"sources":["../src/database/test-database.ts","../src/database/reset.ts"],"sourcesContent":["/**\n * Test database isolation helpers.\n *\n * Implements database-per-worker isolation for parallel e2e runs against\n * Postgres. A migrated **template** database is built once in global setup;\n * each worker clones it once into a per-worker database that is reused (and\n * reset between tests, not dropped per file). See `@stratal/testing/database`.\n *\n * `pg` is imported dynamically so this module loads without it — consumers\n * that don't use a database never pay the dependency.\n */\n\nimport { createHash } from 'node:crypto'\nimport { readFileSync, readdirSync, statSync } from 'node:fs'\nimport { basename, dirname, join, resolve } from 'node:path'\n\nimport type pg from 'pg'\n\n/** Env var carrying the name of the Hyperdrive binding to isolate. */\nexport const BINDING_ENV_VAR = 'STRATAL_TEST_DB_BINDING'\n\n/** Default Hyperdrive binding name when none is configured. */\nexport const DEFAULT_DB_BINDING = 'DB'\n\n/**\n * Postgres' identifier length limit. Names longer than this are silently\n * truncated by the server, which would cause distinct logical names to collide\n * on the same physical database. We assert against it everywhere a name is\n * derived.\n */\nconst MAX_IDENTIFIER_LENGTH = 63\n\n/** Quote a Postgres identifier for safe interpolation into DDL. */\nfunction quoteIdent(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`\n}\n\n/** Quote a Postgres string literal for safe interpolation into SQL. */\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Dynamically import `pg` (an optional peer), surfacing an actionable error\n * instead of a raw module-not-found when database isolation is requested\n * without the dependency installed.\n */\nasync function importPg(): Promise<typeof pg> {\n try {\n const { default: pg } = await import('pg')\n return pg\n } catch (error) {\n throw new Error(\n \"[stratal-testing] `pg` is required for database isolation but is not installed. \" +\n 'Install it: `npm install --save-dev pg` (or `yarn add -D pg`).',\n { cause: error },\n )\n }\n}\n\n/**\n * Assert a derived identifier fits within Postgres' {@link MAX_IDENTIFIER_LENGTH}\n * limit. Throws a clear, actionable error instead of letting the server\n * silently truncate and collide names.\n */\nfunction assertIdentifierLength(name: string, kind: string): void {\n if (name.length > MAX_IDENTIFIER_LENGTH) {\n throw new Error(\n `[stratal-testing] Derived ${kind} \"${name}\" is ${name.length} characters, ` +\n `exceeding Postgres' ${MAX_IDENTIFIER_LENGTH}-character identifier limit. ` +\n 'Use a shorter base database name so the test-isolation suffix fits.',\n )\n }\n}\n\n/** Parse the database name out of a Postgres connection URL. */\nfunction databaseNameOf(connectionString: string): string {\n const name = new URL(connectionString).pathname.replace(/^\\//, '')\n return name || 'postgres'\n}\n\n/**\n * Derive an admin connection string pointing at the `postgres` maintenance\n * database. `CREATE`/`DROP DATABASE` cannot run on a connection bound to the\n * target database, so administration always goes through `postgres`.\n */\nexport function deriveAdminConnectionString(connectionString: string): string {\n const url = new URL(connectionString)\n url.pathname = '/postgres'\n return url.toString()\n}\n\n/** Build a connection string identical to `base` but pointing at `dbName`. */\nexport function buildConnectionString(base: string, dbName: string): string {\n const url = new URL(base)\n url.pathname = `/${dbName}`\n return url.toString()\n}\n\n/** The shared prefix for per-file databases, used as the leak-sweep key. */\nexport function databasePrefix(base: string): string {\n const baseName = databaseNameOf(base).replace(/[^a-z0-9_]/gi, '_')\n return `${baseName}_f_`\n}\n\n/** Name of the migrated template database cloned per worker. */\nexport function deriveTemplateName(base: string): string {\n const name = `${databaseNameOf(base).replace(/[^a-z0-9_]/gi, '_')}_template`\n assertIdentifierLength(name, 'template database name')\n return name\n}\n\n/**\n * Name of the database owned by a single test FILE, keyed by a per-file `token`.\n * Each test file clones the template into its OWN database — no two files ever\n * share one — because `@cloudflare/vitest-pool-workers` isolates per file and can\n * run a worker's files concurrently; per-file databases make cross-file\n * contamination impossible by construction. Asserted against Postgres' 63-char\n * identifier limit (keep the base name short so `_f_<token>` fits).\n */\nexport function deriveFileDbName(base: string, token: string): string {\n const baseName = databaseNameOf(base).replace(/[^a-z0-9_]/gi, '_')\n const name = `${baseName}_f_${token}`\n assertIdentifierLength(name, 'per-file database name')\n return name\n}\n\nasync function withAdminClient<T>(adminConn: string, fn: (query: (sql: string) => Promise<unknown>) => Promise<T>): Promise<T> {\n const pg = await importPg()\n const client = new pg.Client({ connectionString: adminConn })\n await client.connect()\n try {\n return await fn((sql) => client.query(sql))\n } finally {\n await client.end()\n }\n}\n\n/** True when the error is Postgres' \"duplicate database\" (concurrent create). */\nfunction isDuplicateDatabase(error: unknown): boolean {\n return (error as { code?: string })?.code === '42P04'\n}\n\n/**\n * True for SQLSTATE 55006 (\"source database is being accessed by other users\") —\n * what `CREATE DATABASE ... TEMPLATE t` raises while another session is using the\n * template (e.g. a concurrent clone of the same template).\n */\nfunction isTemplateInUse(error: unknown): boolean {\n return (error as { code?: string })?.code === '55006'\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** True when a database with `dbName` already exists. */\nasync function databaseExists(\n query: (sql: string) => Promise<unknown>,\n dbName: string,\n): Promise<boolean> {\n const { rows } = (await query(\n `SELECT 1 AS one FROM pg_database WHERE datname = ${quoteLiteral(dbName)}`,\n )) as { rows: unknown[] }\n return rows.length > 0\n}\n\n/**\n * Ensure worker database `dbName` exists, cloned from `template`. Idempotent and\n * lock-frugal: it first checks `pg_database`, so the common \"already exists\" path\n * (every file after a slot's first) never issues `CREATE DATABASE ... TEMPLATE`\n * and never takes the clone lock.\n *\n * The clone is serialized across all workers and processes by one Postgres\n * advisory lock keyed on the template. Postgres permits only one\n * `CREATE DATABASE ... TEMPLATE t` at a time — a concurrent one fails with\n * SQLSTATE 55006. On a fresh run every worker's first compile races to clone the\n * same template, so without this they'd all fire `CREATE ... TEMPLATE` at once\n * and all but one would fail their first file. The lock funnels them one-at-a-\n * time, each blocking *in Postgres* (not busy-waiting) until its turn. A create\n * that still hits a transient 55006 is retried with backoff; one that loses the\n * race for the same name (SQLSTATE 42P04) is treated as success.\n */\nexport async function ensureWorkerDatabase(\n adminConn: string,\n dbName: string,\n template: string,\n): Promise<void> {\n await withAdminClient(adminConn, async (query) => {\n if (await databaseExists(query, dbName)) return\n\n const lockKey = quoteLiteral(`stratal:worker-db-clone:${template}`)\n await query(`SELECT pg_advisory_lock(hashtext(${lockKey}))`)\n try {\n // A sibling worker may have cloned it while we waited for the lock.\n if (await databaseExists(query, dbName)) return\n const create = `CREATE DATABASE ${quoteIdent(dbName)} TEMPLATE ${quoteIdent(template)}`\n for (let attempt = 1; ; attempt++) {\n try {\n await query(create)\n return\n } catch (error) {\n if (isDuplicateDatabase(error)) return\n if (isTemplateInUse(error) && attempt < 5) {\n await sleep(250 * attempt)\n continue\n }\n throw error\n }\n }\n } finally {\n await query(`SELECT pg_advisory_unlock(hashtext(${lockKey}))`)\n }\n })\n}\n\n/**\n * Drop leaked per-file databases matching the prefix while leaving a concurrent\n * process's **live** databases intact.\n *\n * Multiple setups can run at once (CI sharding, several e2e projects). A blanket\n * \"drop everything matching the prefix\" sweep would delete a sibling process's\n * in-flight per-file databases. So we only drop databases that currently have\n * **no active backend connections** — i.e. true leaks from a crashed prior run.\n * A live per-file database always has the test worker's pool connected, so it is\n * skipped. The `WITH (FORCE)` covers the narrow race where a connection appears\n * between the check and the drop.\n */\nasync function sweepStaleDatabases(adminConn: string, prefix: string): Promise<void> {\n // Escape both the SQL-string quote and the LIKE metacharacters (`\\`, `%`, `_`)\n // so a prefix containing `_` (a single-char wildcard) can't over-match and drop\n // an unrelated database. `\\` is the explicit ESCAPE character below.\n const likePrefix = prefix\n .replace(/'/g, \"''\")\n .replace(/[\\\\%_]/g, (c) => `\\\\${c}`)\n await withAdminClient(adminConn, async (query) => {\n const { rows } = (await query(\n `SELECT d.datname FROM pg_database d ` +\n `WHERE d.datname LIKE '${likePrefix}%' ESCAPE '\\\\' ` +\n `AND NOT EXISTS (SELECT 1 FROM pg_stat_activity a WHERE a.datname = d.datname)`,\n )) as { rows: { datname: string }[] }\n for (const { datname } of rows) {\n await query(`DROP DATABASE IF EXISTS ${quoteIdent(datname)} WITH (FORCE)`)\n }\n })\n}\n\n/**\n * Run `fn` while holding a session-level Postgres advisory lock keyed to\n * `lockKey`, serializing template setup across concurrent processes (CI\n * sharding, multiple e2e projects) so they don't drop/recreate the template out\n * from under each other. The lock is released in `finally`.\n */\nasync function withAdvisoryLock<T>(adminConn: string, lockKey: string, fn: () => Promise<T>): Promise<T> {\n return withAdminClient(adminConn, async (query) => {\n await query(`SELECT pg_advisory_lock(hashtext(${quoteLiteral(lockKey)}))`)\n try {\n return await fn()\n } finally {\n await query(`SELECT pg_advisory_unlock(hashtext(${quoteLiteral(lockKey)}))`)\n }\n })\n}\n\n/** Schema source file extensions hashed into the template fingerprint. */\nconst SCHEMA_FILE_RE = /\\.(zmodel|prisma|sql)$/\n\n/** Matches ZModel `import \"...\"` / `import '...'` statements. */\nconst ZMODEL_IMPORT_RE = /^\\s*import\\s+['\"]([^'\"]+)['\"]/gm\n\n/**\n * Follow a ZModel file's `import` graph, collecting the root plus every\n * transitively imported `.zmodel` file — ZenStack supports multi-file schemas,\n * so editing an imported file must invalidate the fingerprint. Import paths\n * resolve relative to the importing file; the `.zmodel` extension is optional.\n * A missing import target is skipped (migration surfaces the real error).\n */\nfunction collectZmodelImports(file: string, seen: Set<string>): void {\n if (seen.has(file)) return\n let content: string\n try {\n content = readFileSync(file, 'utf8')\n } catch {\n return // missing import target — not part of the fingerprint\n }\n seen.add(file)\n for (const [, importPath] of content.matchAll(ZMODEL_IMPORT_RE)) {\n const target = resolve(dirname(file), importPath)\n collectZmodelImports(target.endsWith('.zmodel') ? target : `${target}.zmodel`, seen)\n }\n}\n\n/**\n * Expand a schema path into the concrete files to hash. A directory contributes\n * every schema file in its tree; a single `.zmodel` file contributes its whole\n * `import` graph; any other file contributes itself.\n */\nfunction collectSchemaFiles(path: string): string[] {\n if (statSync(path).isDirectory()) {\n const out: string[] = []\n for (const entry of readdirSync(path, { withFileTypes: true })) {\n const full = join(path, entry.name)\n if (entry.isDirectory()) out.push(...collectSchemaFiles(full))\n else if (SCHEMA_FILE_RE.test(entry.name)) out.push(full)\n }\n return out\n }\n if (path.endsWith('.zmodel')) {\n const seen = new Set<string>()\n collectZmodelImports(path, seen)\n return [...seen]\n }\n return [path]\n}\n\n/**\n * A content-derived fingerprint of the schema source(s) plus the migrate and\n * prepare routines. The template is reused across runs while this is\n * unchanged; any edit to a schema file — or to how migration/preparation run\n * — changes it and forces a rebuild. Uses file basenames (not absolute paths)\n * so it is stable across checkouts.\n *\n * @internal exported for fingerprint unit tests\n */\nexport function computeSchemaFingerprint(\n schema: string | string[],\n migrate: TestDatabaseGlobalSetupOptions['migrate'],\n prepare?: TestDatabaseGlobalSetupOptions['prepare'],\n): string {\n const roots = Array.isArray(schema) ? schema : [schema]\n const files = roots.flatMap(collectSchemaFiles).sort()\n if (files.length === 0) {\n throw new Error(\n `[stratal-testing] No schema files found for fingerprinting under: ${roots.join(', ')}`,\n )\n }\n const hash = createHash('sha256')\n for (const file of files) {\n hash.update(basename(file))\n hash.update('\\0')\n hash.update(readFileSync(file))\n hash.update('\\0')\n }\n hash.update(migrate.toString())\n hash.update('\\0')\n hash.update(prepare ? prepare.toString() : '')\n return hash.digest('hex')\n}\n\n/**\n * Read the template database's stored schema fingerprint (kept as the database\n * COMMENT). Returns `null` when the template does not exist or carries no\n * fingerprint. Database comments are NOT copied by `CREATE DATABASE ...\n * TEMPLATE`, so per-file clones never inherit it.\n */\nasync function readTemplateFingerprint(\n query: (sql: string) => Promise<unknown>,\n template: string,\n): Promise<string | null> {\n const { rows } = (await query(\n `SELECT shobj_description(oid, 'pg_database') AS fingerprint ` +\n `FROM pg_database WHERE datname = ${quoteLiteral(template)}`,\n )) as { rows: { fingerprint: string | null }[] }\n return rows.length === 0 ? null : rows[0].fingerprint\n}\n\n/** Options for {@link createTestDatabaseGlobalSetup}. */\nexport interface TestDatabaseGlobalSetupOptions {\n /** Run migrations against the template connection string. */\n migrate: (connectionString: string) => void | Promise<void>\n /**\n * Schema source(s) — file or directory path(s). Contents + `migrate` + `prepare`\n * are hashed into the template fingerprint; the template is reused across runs\n * while unchanged. For a ZenStack multi-file schema pass the root `.zmodel`.\n */\n schema: string | string[]\n /**\n * One-time preparation run against the template **after** migrations (before\n * the fingerprint is stamped). Bake expensive baseline state here — seed data,\n * a default tenant schema, reference rows — so every worker database inherits\n * it via the clone instead of rebuilding it per test.\n *\n * The fingerprint hashes this hook's **source text only** — if `prepare` reads\n * external data files (seed JSON/SQL) at runtime, changing only those files\n * will NOT invalidate the template. Bump the hook's source (e.g. a version\n * comment) or drop the template manually when seed data changes.\n */\n prepare?: (connectionString: string) => void | Promise<void>\n /** Base/admin connection string. Defaults to `process.env.DATABASE_URL`. */\n connectionString?: string\n /** Template database name. Defaults to `<baseDbName>_template`. */\n templateName?: string\n}\n\n/**\n * Build a Vitest `globalSetup` default export that prepares the test database.\n *\n * Under a Postgres advisory lock (so concurrent setups across CI shards /\n * multiple e2e projects don't clobber each other), sweeps leaked per-worker\n * databases, then ensures a migrated (and optionally prepared) template exists\n * — ready to be cloned per worker.\n *\n * **Template reuse.** The template is fingerprinted from the `schema` source(s)\n * + the `migrate` routine + the `prepare` routine, and the fingerprint is\n * stored as the template's database COMMENT. On each run, a matching\n * fingerprint means nothing has changed and the existing template is reused\n * as-is — `migrate`/`prepare` run **only** on the first run after an edit (or on\n * a fresh database). The fingerprint is stamped only after a successful\n * migrate + prepare, so a match always implies a complete template; there is no\n * force/skip flag — reuse is purely fingerprint-driven.\n *\n * **Concurrency model.** The reuse check + rebuild runs under\n * `pg_advisory_lock(hashtext(<template>))`, so only one process rebuilds the\n * template at a time. The stale-database sweep only drops per-worker databases\n * with **no active connections**, leaving a sibling process's live databases\n * intact. Teardown deliberately does **not** sweep or drop the template: a\n * concurrent process may still be using both, and the next run's setup sweep is\n * the backstop for any leak.\n *\n * @example\n * ```ts\n * // test/global-setup.ts\n * import { createTestDatabaseGlobalSetup } from '@stratal/testing/database'\n *\n * export default createTestDatabaseGlobalSetup({\n * schema: schemaPath, // file or directory — reused-when-unchanged fingerprint\n * migrate: (conn) => execFileSync(zenstackBin, ['db', 'push', '--force-reset', `--schema=${schemaPath}`, '--accept-data-loss'],\n * { env: { ...process.env, DATABASE_URL: conn }, stdio: 'inherit' }),\n * })\n * ```\n */\nexport function createTestDatabaseGlobalSetup(\n opts: TestDatabaseGlobalSetupOptions,\n): () => Promise<void> {\n return async () => {\n const base = opts.connectionString ?? process.env.DATABASE_URL\n if (!base) {\n throw new Error(\n '[stratal-testing] No connection string for test database setup. Set process.env.DATABASE_URL or pass `connectionString`.',\n )\n }\n\n const adminConn = deriveAdminConnectionString(base)\n const template = opts.templateName ?? deriveTemplateName(base)\n const prefix = databasePrefix(base)\n const fingerprint = computeSchemaFingerprint(opts.schema, opts.migrate, opts.prepare)\n\n // Serialize template rebuild across concurrent setups so they don't drop or\n // migrate the template out from under each other.\n await withAdvisoryLock(adminConn, template, async () => {\n await sweepStaleDatabases(adminConn, prefix)\n\n // Reuse the existing template when its stored fingerprint matches — the\n // schema is unchanged, so the migrated template is ready to clone. Only a\n // fingerprint mismatch (or a missing template) pays the migration cost.\n const current = await withAdminClient(adminConn, (query) =>\n readTemplateFingerprint(query, template),\n )\n if (current === fingerprint) return\n\n await withAdminClient(adminConn, async (query) => {\n await query(`DROP DATABASE IF EXISTS ${quoteIdent(template)} WITH (FORCE)`)\n await query(`CREATE DATABASE ${quoteIdent(template)}`)\n })\n const templateConn = buildConnectionString(base, template)\n await opts.migrate(templateConn)\n if (opts.prepare) await opts.prepare(templateConn)\n // Stamp the fingerprint only after a successful migrate + prepare, so a\n // matching fingerprint always implies a complete, ready template. Database\n // COMMENTs are not copied by CREATE DATABASE ... TEMPLATE, so clones stay\n // clean.\n await withAdminClient(adminConn, (query) =>\n query(`COMMENT ON DATABASE ${quoteIdent(template)} IS ${quoteLiteral(fingerprint)}`),\n )\n })\n\n // No teardown hook: anything destructive here (sweeping per-worker databases\n // or dropping the template) could clobber a concurrent process that is still\n // running. The next run's setup sweep (connection-guarded) reclaims leaks.\n }\n}\n","import type pg from 'pg'\n\n/** Options controlling which tables a per-test reset truncates. */\nexport interface ResetOptions {\n /** Extra schemas to include beyond `current_schema()` (e.g. a tenant schema). */\n schemas?: string[]\n /**\n * Bare table names or SQL LIKE patterns (matched against `tablename` across\n * all target schemas) that must survive the reset — reference/seed data baked\n * into the template. Not schema-qualified: a `schema.table` entry matches a\n * table literally named `schema.table`, not `table` in `schema`. Migration\n * bookkeeping (`_prisma%`) is always preserved.\n */\n preserve?: string[]\n}\n\nfunction quoteIdent(ident: string): string {\n return `\"${ident.replace(/\"/g, '\"\"')}\"`\n}\n\n/**\n * Build a single `TRUNCATE ... RESTART IDENTITY CASCADE` (empty when no tables).\n * Takes `{ schema, table }` pairs and quotes each part independently, so a\n * dotted identifier (a legal quoted name, e.g. `\"my.table\"`) is preserved\n * verbatim rather than mis-split into extra qualifier segments.\n */\nexport function buildTruncateSql(tables: { schema: string; table: string }[]): string {\n if (tables.length === 0) return ''\n const quoted = tables.map((t) => `${quoteIdent(t.schema)}.${quoteIdent(t.table)}`)\n return `TRUNCATE ${quoted.join(', ')} RESTART IDENTITY CASCADE`\n}\n\n/**\n * Sentinel meaning \"the active search-path schema\" (`current_schema()`). A\n * Symbol so it can never collide with a real schema name — a schema literally\n * named `current` is passed through as the string literal `'current'`.\n */\nconst CURRENT_SCHEMA = Symbol('current_schema')\n\n/**\n * Build the SQL that discovers truncatable tables: every base table in the\n * given schemas ({@link CURRENT_SCHEMA} → `current_schema()`), excluding\n * migration bookkeeping (`_prisma%`) and any caller-supplied preserve patterns.\n * Returns a SELECT of `schemaname, tablename`. Escaping lives here so both reset\n * paths share one implementation.\n */\nexport function buildTableDiscoverySql(schemas: string[], preserve: string[]): string {\n const schemaFilter = [CURRENT_SCHEMA, ...schemas]\n .map((s) => (typeof s === 'symbol' ? 'current_schema()' : `'${s.replace(/'/g, \"''\")}'`))\n .join(', ')\n const notLike = ['_prisma%', ...preserve]\n .map((p) => `tablename NOT LIKE '${p.replace(/'/g, \"''\")}'`)\n .join(' AND ')\n return `SELECT schemaname::text AS schemaname, tablename::text AS tablename FROM pg_tables WHERE schemaname IN (${schemaFilter}) AND ${notLike}`\n}\n\nasync function importPg(): Promise<typeof pg> {\n const { default: mod } = await import('pg')\n return mod\n}\n\n/**\n * Reset a worker database between tests: discover every non-preserved table in\n * the target schemas and truncate them in one statement. Fast because the\n * schema/DDL is baked into the worker DB — reset only clears mutable rows and\n * resets identities. Opens and closes a short-lived direct `pg` connection.\n */\nexport async function resetWorkerDatabase(connectionString: string, opts: ResetOptions = {}): Promise<void> {\n const pgMod = await importPg()\n const client = new pgMod.Client({ connectionString })\n await client.connect()\n try {\n const sql = buildTableDiscoverySql(opts.schemas ?? [], opts.preserve ?? [])\n const { rows } = await client.query<{ schemaname: string; tablename: string }>(sql)\n const tables = rows.map((r) => ({ schema: r.schemaname, table: r.tablename }))\n const truncateSql = buildTruncateSql(tables)\n if (truncateSql) await client.query(truncateSql)\n } finally {\n await client.end()\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.140.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "../feature-flags-
|
|
1
|
+
import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "../feature-flags-BMYurC1B.mjs";
|
|
2
2
|
export { FEATURE_FLAG_SERVICE_TOKEN, FakeFeatureFlagService };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as __decorate } from "./decorate-
|
|
1
|
+
import { t as __decorate } from "./decorate-ZDdbRmsv.mjs";
|
|
2
2
|
import { Singleton } from "stratal/di";
|
|
3
3
|
//#region src/feature-flags/fake-feature-flag.service.ts
|
|
4
4
|
/**
|
|
@@ -83,4 +83,4 @@ FakeFeatureFlagService = __decorate([Singleton(FEATURE_FLAG_SERVICE_TOKEN)], Fak
|
|
|
83
83
|
//#endregion
|
|
84
84
|
export { FakeFeatureFlagService as n, FEATURE_FLAG_SERVICE_TOKEN as t };
|
|
85
85
|
|
|
86
|
-
//# sourceMappingURL=feature-flags-
|
|
86
|
+
//# sourceMappingURL=feature-flags-BMYurC1B.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flags-
|
|
1
|
+
{"version":3,"file":"feature-flags-BMYurC1B.mjs","names":[],"sources":["../src/feature-flags/fake-feature-flag.service.ts"],"sourcesContent":["import { Singleton } from 'stratal/di';\n\n/**\n * Global DI token for the feature-flags service.\n *\n * Mirrors `FEATURE_FLAG_TOKENS.FeatureFlagService` from `@stratal/feature-flags`\n * — `Symbol.for(...)` resolves to the same symbol across packages via the global\n * registry. Declared here so `@stratal/testing` needs no dependency on the\n * optional feature-flags package. Keep this string in sync with\n * `packages/feature-flags/src/feature-flags.tokens.ts`.\n */\nexport const FEATURE_FLAG_SERVICE_TOKEN = Symbol.for('stratal:feature-flags:service')\n\n/** A value a feature flag can resolve to. */\nexport type FlagValue = boolean | string | number | object\n\ninterface FakeFlagDetails<T> {\n flagKey: string\n value: T\n reason: string\n}\n\n/**\n * FakeFeatureFlagService\n *\n * In-memory stand-in for `@stratal/feature-flags`'s request-scoped\n * `FeatureFlagService`, auto-registered by the testing module so feature-gated\n * code resolves without a real Cloudflare Flagship binding (which only exists at\n * runtime). Mirrors the real service's public evaluation surface.\n *\n * Unset flags return the per-call default (or the type's zero value). Configure\n * values with {@link set} / {@link setAll}; access it in tests via\n * `module.featureFlags`.\n *\n * @example\n * ```typescript\n * module.featureFlags.set('new-checkout', true)\n * const enabled = await flags.getBooleanValue('new-checkout') // true\n * ```\n */\n@Singleton(FEATURE_FLAG_SERVICE_TOKEN)\nexport class FakeFeatureFlagService {\n private readonly flags = new Map<string, FlagValue>()\n\n // ==================== CONFIGURATION ====================\n\n /** Set a single flag value. */\n set(flagKey: string, value: FlagValue): this {\n this.flags.set(flagKey, value)\n return this\n }\n\n /** Replace all configured flags with the given map. */\n setAll(flags: Record<string, FlagValue>): this {\n this.reset()\n for (const [key, value] of Object.entries(flags)) this.flags.set(key, value)\n return this\n }\n\n /** Clear every configured flag. */\n reset(): this {\n this.flags.clear()\n return this\n }\n\n // ==================== EVALUATION ====================\n\n async get(flagKey: string, defaultValue?: unknown): Promise<unknown> {\n return Promise.resolve(this.flags.has(flagKey) ? this.flags.get(flagKey) : defaultValue)\n }\n\n async getBooleanValue(flagKey: string, defaultValue = false): Promise<boolean> {\n return this.resolve(flagKey, defaultValue)\n }\n\n async getStringValue(flagKey: string, defaultValue = ''): Promise<string> {\n return this.resolve(flagKey, defaultValue)\n }\n\n async getNumberValue(flagKey: string, defaultValue = 0): Promise<number> {\n return this.resolve(flagKey, defaultValue)\n }\n\n async getObjectValue<T extends object>(flagKey: string, defaultValue: T = {} as T): Promise<T> {\n return this.resolve(flagKey, defaultValue)\n }\n\n async getBooleanDetails(flagKey: string, defaultValue = false): Promise<FakeFlagDetails<boolean>> {\n return this.details(flagKey, await this.getBooleanValue(flagKey, defaultValue))\n }\n\n async getStringDetails(flagKey: string, defaultValue = ''): Promise<FakeFlagDetails<string>> {\n return this.details(flagKey, await this.getStringValue(flagKey, defaultValue))\n }\n\n async getNumberDetails(flagKey: string, defaultValue = 0): Promise<FakeFlagDetails<number>> {\n return this.details(flagKey, await this.getNumberValue(flagKey, defaultValue))\n }\n\n async getObjectDetails<T extends object>(flagKey: string, defaultValue: T = {} as T): Promise<FakeFlagDetails<T>> {\n return this.details(flagKey, await this.getObjectValue(flagKey, defaultValue))\n }\n\n /** Returns every configured flag as a `{ key: value }` map. */\n async all(): Promise<Record<string, FlagValue>> {\n return Promise.resolve(Object.fromEntries(this.flags))\n }\n\n /** Switching Flagship apps is a no-op in the fake. */\n use(): this {\n return this\n }\n\n /** The binding name this instance targets. */\n // oxlint-disable-next-line typescript/class-literal-property-style\n get app(): string {\n return 'fake'\n }\n\n // ==================== INTERNAL ====================\n\n private resolve<T extends FlagValue>(flagKey: string, defaultValue: T): Promise<T> {\n return Promise.resolve(this.flags.has(flagKey) ? (this.flags.get(flagKey) as T) : defaultValue)\n }\n\n private details<T>(flagKey: string, value: T): FakeFlagDetails<T> {\n return { flagKey, value, reason: 'fake' }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAa,6BAA6B,OAAO,IAAI,+BAA+B;AA8B7E,IAAM,yBAAN,MAAM,uBAAuB;CAClC,wBAAyB,IAAI,IAAuB;;CAKpD,IAAI,SAAiB,OAAwB;EAC3C,KAAK,MAAM,IAAI,SAAS,KAAK;EAC7B,OAAO;CACT;;CAGA,OAAO,OAAwC;EAC7C,KAAK,MAAM;EACX,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK;EAC3E,OAAO;CACT;;CAGA,QAAc;EACZ,KAAK,MAAM,MAAM;EACjB,OAAO;CACT;CAIA,MAAM,IAAI,SAAiB,cAA0C;EACnE,OAAO,QAAQ,QAAQ,KAAK,MAAM,IAAI,OAAO,IAAI,KAAK,MAAM,IAAI,OAAO,IAAI,YAAY;CACzF;CAEA,MAAM,gBAAgB,SAAiB,eAAe,OAAyB;EAC7E,OAAO,KAAK,QAAQ,SAAS,YAAY;CAC3C;CAEA,MAAM,eAAe,SAAiB,eAAe,IAAqB;EACxE,OAAO,KAAK,QAAQ,SAAS,YAAY;CAC3C;CAEA,MAAM,eAAe,SAAiB,eAAe,GAAoB;EACvE,OAAO,KAAK,QAAQ,SAAS,YAAY;CAC3C;CAEA,MAAM,eAAiC,SAAiB,eAAkB,CAAC,GAAoB;EAC7F,OAAO,KAAK,QAAQ,SAAS,YAAY;CAC3C;CAEA,MAAM,kBAAkB,SAAiB,eAAe,OAA0C;EAChG,OAAO,KAAK,QAAQ,SAAS,MAAM,KAAK,gBAAgB,SAAS,YAAY,CAAC;CAChF;CAEA,MAAM,iBAAiB,SAAiB,eAAe,IAAsC;EAC3F,OAAO,KAAK,QAAQ,SAAS,MAAM,KAAK,eAAe,SAAS,YAAY,CAAC;CAC/E;CAEA,MAAM,iBAAiB,SAAiB,eAAe,GAAqC;EAC1F,OAAO,KAAK,QAAQ,SAAS,MAAM,KAAK,eAAe,SAAS,YAAY,CAAC;CAC/E;CAEA,MAAM,iBAAmC,SAAiB,eAAkB,CAAC,GAAqC;EAChH,OAAO,KAAK,QAAQ,SAAS,MAAM,KAAK,eAAe,SAAS,YAAY,CAAC;CAC/E;;CAGA,MAAM,MAA0C;EAC9C,OAAO,QAAQ,QAAQ,OAAO,YAAY,KAAK,KAAK,CAAC;CACvD;;CAGA,MAAY;EACV,OAAO;CACT;;CAIA,IAAI,MAAc;EAChB,OAAO;CACT;CAIA,QAAqC,SAAiB,cAA6B;EACjF,OAAO,QAAQ,QAAQ,KAAK,MAAM,IAAI,OAAO,IAAK,KAAK,MAAM,IAAI,OAAO,IAAU,YAAY;CAChG;CAEA,QAAmB,SAAiB,OAA8B;EAChE,OAAO;GAAE;GAAS;GAAO,QAAQ;EAAO;CAC1C;AACF;AAxFC,yBAAA,WAAA,CAAA,UAAU,0BAA0B,CAAA,GAAA,sBAAA"}
|