@jterrazz/test 7.1.0 → 8.0.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.
@@ -1,11 +1,11 @@
1
1
  const require_chunk = require("./chunk.cjs");
2
- let node_fs = require("node:fs");
3
- let node_os = require("node:os");
4
2
  let node_path = require("node:path");
3
+ let node_fs = require("node:fs");
5
4
  let pg = require("pg");
5
+ let node_os = require("node:os");
6
6
  let better_sqlite3 = require("better-sqlite3");
7
7
  better_sqlite3 = require_chunk.__toESM(better_sqlite3, 1);
8
- //#region src/adapters/postgres.adapter.ts
8
+ //#region src/services/postgres.ts
9
9
  var PostgresHandle = class {
10
10
  type = "postgres";
11
11
  composeName;
@@ -129,7 +129,7 @@ function postgres(options = {}) {
129
129
  return new PostgresHandle(options);
130
130
  }
131
131
  //#endregion
132
- //#region src/adapters/redis.adapter.ts
132
+ //#region src/services/redis.ts
133
133
  var RedisHandle = class {
134
134
  type = "redis";
135
135
  composeName;
@@ -201,7 +201,7 @@ function redis(options = {}) {
201
201
  return new RedisHandle(options);
202
202
  }
203
203
  //#endregion
204
- //#region src/adapters/sqlite.adapter.ts
204
+ //#region src/services/sqlite.ts
205
205
  var SqliteHandle = class {
206
206
  type = "sqlite";
207
207
  composeName = null;
@@ -347,4 +347,4 @@ Object.defineProperty(exports, "sqlite", {
347
347
  }
348
348
  });
349
349
 
350
- //# sourceMappingURL=sqlite.adapter.cjs.map
350
+ //# sourceMappingURL=sqlite.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.cjs","names":["Client","Database"],"sources":["../src/services/postgres.ts","../src/services/redis.ts","../src/services/sqlite.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { IsolationStrategy } from '../spec/ports/isolation.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { IsolationStrategy } from '../spec/ports/isolation.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { IsolationStrategy } from '../spec/ports/isolation.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Each test run gets a fresh template; workers share it via lock\n this.templatePath = resolve(tmpdir(), 'jterrazz-test-sqlite-template.sqlite');\n const lockPath = `${this.templatePath}.lock`;\n\n if (existsSync(lockPath)) {\n // Another worker is creating it — wait for it\n const start = Date.now();\n while (existsSync(lockPath) && Date.now() - start < 30_000) {\n await new Promise((r) => setTimeout(r, 100));\n }\n }\n\n if (existsSync(this.templatePath)) {\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n return;\n }\n\n // Acquire lock\n const { writeFileSync } = await import('node:fs');\n writeFileSync(lockPath, process.pid.toString());\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes',\n },\n stdio: 'pipe',\n });\n\n // Checkpoint WAL so the template is a single file (safe to copy)\n const tmpDb = new Database(this.templatePath);\n tmpDb.pragma('wal_checkpoint(TRUNCATE)');\n tmpDb.close();\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n // Release lock\n try {\n unlinkSync(lockPath);\n } catch {\n /* Ignore */\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- //#region src/adapters/ports/database.port.d.ts
1
+ //#region src/spec/ports/database.port.d.ts
2
2
  /**
3
3
  * Abstract database interface for specification runners.
4
4
  * Implement this to plug in your database stack (e.g. Postgres, SQLite).
@@ -12,7 +12,7 @@ interface DatabasePort {
12
12
  reset(): Promise<void>;
13
13
  }
14
14
  //#endregion
15
- //#region src/infra/ports/isolation.port.d.ts
15
+ //#region src/spec/ports/isolation.port.d.ts
16
16
  /**
17
17
  * Strategy for isolating service state across parallel test workers.
18
18
  *
@@ -44,7 +44,7 @@ interface IsolationStrategy {
44
44
  release(): Promise<void>;
45
45
  }
46
46
  //#endregion
47
- //#region src/infra/ports/service.port.d.ts
47
+ //#region src/spec/ports/service.port.d.ts
48
48
  /**
49
49
  * A service handle — returned by factory functions like postgres(), redis().
50
50
  * Mutable: connectionString is populated after the orchestrator starts containers.
@@ -78,7 +78,7 @@ interface ServiceHandle {
78
78
  isolation(): IsolationStrategy;
79
79
  }
80
80
  //#endregion
81
- //#region src/adapters/postgres.adapter.d.ts
81
+ //#region src/services/postgres.d.ts
82
82
  interface PostgresOptions {
83
83
  /** Map to a service in docker-compose.test.yaml. */
84
84
  compose?: string;
@@ -118,7 +118,7 @@ declare class PostgresHandle implements DatabasePort, ServiceHandle {
118
118
  */
119
119
  declare function postgres(options?: PostgresOptions): PostgresHandle;
120
120
  //#endregion
121
- //#region src/adapters/redis.adapter.d.ts
121
+ //#region src/services/redis.d.ts
122
122
  interface RedisOptions {
123
123
  /** Map to a service in docker-compose.test.yaml. */
124
124
  compose?: string;
@@ -151,7 +151,7 @@ declare class RedisHandle implements ServiceHandle {
151
151
  */
152
152
  declare function redis(options?: RedisOptions): RedisHandle;
153
153
  //#endregion
154
- //#region src/adapters/sqlite.adapter.d.ts
154
+ //#region src/services/sqlite.d.ts
155
155
  interface SqliteOptions {
156
156
  /**
157
157
  * Path to a SQL file used to initialize the database schema.
@@ -206,4 +206,4 @@ declare class SqliteHandle implements DatabasePort, ServiceHandle {
206
206
  declare function sqlite(options?: SqliteOptions): SqliteHandle;
207
207
  //#endregion
208
208
  export { PostgresOptions as a, IsolationStrategy as c, redis as i, DatabasePort as l, sqlite as n, postgres as o, RedisOptions as r, ServiceHandle as s, SqliteOptions as t };
209
- //# sourceMappingURL=sqlite.adapter.d.ts.map
209
+ //# sourceMappingURL=sqlite.d.cts.map
@@ -1,4 +1,4 @@
1
- //#region src/adapters/ports/database.port.d.ts
1
+ //#region src/spec/ports/database.port.d.ts
2
2
  /**
3
3
  * Abstract database interface for specification runners.
4
4
  * Implement this to plug in your database stack (e.g. Postgres, SQLite).
@@ -12,7 +12,7 @@ interface DatabasePort {
12
12
  reset(): Promise<void>;
13
13
  }
14
14
  //#endregion
15
- //#region src/infra/ports/isolation.port.d.ts
15
+ //#region src/spec/ports/isolation.port.d.ts
16
16
  /**
17
17
  * Strategy for isolating service state across parallel test workers.
18
18
  *
@@ -44,7 +44,7 @@ interface IsolationStrategy {
44
44
  release(): Promise<void>;
45
45
  }
46
46
  //#endregion
47
- //#region src/infra/ports/service.port.d.ts
47
+ //#region src/spec/ports/service.port.d.ts
48
48
  /**
49
49
  * A service handle — returned by factory functions like postgres(), redis().
50
50
  * Mutable: connectionString is populated after the orchestrator starts containers.
@@ -78,7 +78,7 @@ interface ServiceHandle {
78
78
  isolation(): IsolationStrategy;
79
79
  }
80
80
  //#endregion
81
- //#region src/adapters/postgres.adapter.d.ts
81
+ //#region src/services/postgres.d.ts
82
82
  interface PostgresOptions {
83
83
  /** Map to a service in docker-compose.test.yaml. */
84
84
  compose?: string;
@@ -118,7 +118,7 @@ declare class PostgresHandle implements DatabasePort, ServiceHandle {
118
118
  */
119
119
  declare function postgres(options?: PostgresOptions): PostgresHandle;
120
120
  //#endregion
121
- //#region src/adapters/redis.adapter.d.ts
121
+ //#region src/services/redis.d.ts
122
122
  interface RedisOptions {
123
123
  /** Map to a service in docker-compose.test.yaml. */
124
124
  compose?: string;
@@ -151,7 +151,7 @@ declare class RedisHandle implements ServiceHandle {
151
151
  */
152
152
  declare function redis(options?: RedisOptions): RedisHandle;
153
153
  //#endregion
154
- //#region src/adapters/sqlite.adapter.d.ts
154
+ //#region src/services/sqlite.d.ts
155
155
  interface SqliteOptions {
156
156
  /**
157
157
  * Path to a SQL file used to initialize the database schema.
@@ -206,4 +206,4 @@ declare class SqliteHandle implements DatabasePort, ServiceHandle {
206
206
  declare function sqlite(options?: SqliteOptions): SqliteHandle;
207
207
  //#endregion
208
208
  export { PostgresOptions as a, IsolationStrategy as c, redis as i, DatabasePort as l, sqlite as n, postgres as o, RedisOptions as r, ServiceHandle as s, SqliteOptions as t };
209
- //# sourceMappingURL=sqlite.adapter.d.cts.map
209
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -1,9 +1,9 @@
1
- import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
1
  import { resolve } from "node:path";
2
+ import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
4
3
  import { Client } from "pg";
4
+ import { tmpdir } from "node:os";
5
5
  import Database from "better-sqlite3";
6
- //#region src/adapters/postgres.adapter.ts
6
+ //#region src/services/postgres.ts
7
7
  var PostgresHandle = class {
8
8
  type = "postgres";
9
9
  composeName;
@@ -127,7 +127,7 @@ function postgres(options = {}) {
127
127
  return new PostgresHandle(options);
128
128
  }
129
129
  //#endregion
130
- //#region src/adapters/redis.adapter.ts
130
+ //#region src/services/redis.ts
131
131
  var RedisHandle = class {
132
132
  type = "redis";
133
133
  composeName;
@@ -199,7 +199,7 @@ function redis(options = {}) {
199
199
  return new RedisHandle(options);
200
200
  }
201
201
  //#endregion
202
- //#region src/adapters/sqlite.adapter.ts
202
+ //#region src/services/sqlite.ts
203
203
  var SqliteHandle = class {
204
204
  type = "sqlite";
205
205
  composeName = null;
@@ -328,4 +328,4 @@ function sqlite(options = {}) {
328
328
  //#endregion
329
329
  export { redis as n, postgres as r, sqlite as t };
330
330
 
331
- //# sourceMappingURL=sqlite.adapter.js.map
331
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.js","names":[],"sources":["../src/services/postgres.ts","../src/services/redis.ts","../src/services/sqlite.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { IsolationStrategy } from '../spec/ports/isolation.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { IsolationStrategy } from '../spec/ports/isolation.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { IsolationStrategy } from '../spec/ports/isolation.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Each test run gets a fresh template; workers share it via lock\n this.templatePath = resolve(tmpdir(), 'jterrazz-test-sqlite-template.sqlite');\n const lockPath = `${this.templatePath}.lock`;\n\n if (existsSync(lockPath)) {\n // Another worker is creating it — wait for it\n const start = Date.now();\n while (existsSync(lockPath) && Date.now() - start < 30_000) {\n await new Promise((r) => setTimeout(r, 100));\n }\n }\n\n if (existsSync(this.templatePath)) {\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n return;\n }\n\n // Acquire lock\n const { writeFileSync } = await import('node:fs');\n writeFileSync(lockPath, process.pid.toString());\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes',\n },\n stdio: 'pipe',\n });\n\n // Checkpoint WAL so the template is a single file (safe to copy)\n const tmpDb = new Database(this.templatePath);\n tmpDb.pragma('wal_checkpoint(TRUNCATE)');\n tmpDb.close();\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n // Release lock\n try {\n unlinkSync(lockPath);\n } catch {\n /* Ignore */\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
package/dist/types.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/builder/common/intercept/types.d.ts
1
+ //#region src/spec/intercept/types.d.ts
2
2
  /**
3
3
  * An intercept trigger describes which HTTP request to match.
4
4
  */
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/builder/common/intercept/types.d.ts
1
+ //#region src/spec/intercept/types.d.ts
2
2
  /**
3
3
  * An intercept trigger describes which HTTP request to match.
4
4
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "7.1.0",
3
+ "version": "8.0.0",
4
4
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"file":"sqlite.adapter.cjs","names":["Client","Database"],"sources":["../src/adapters/postgres.adapter.ts","../src/adapters/redis.adapter.ts","../src/adapters/sqlite.adapter.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { IsolationStrategy } from '../infra/ports/isolation.port.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { DatabasePort } from './ports/database.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { IsolationStrategy } from '../infra/ports/isolation.port.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { DatabasePort } from './ports/database.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { IsolationStrategy } from '../infra/ports/isolation.port.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { DatabasePort } from './ports/database.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Each test run gets a fresh template; workers share it via lock\n this.templatePath = resolve(tmpdir(), 'jterrazz-test-sqlite-template.sqlite');\n const lockPath = `${this.templatePath}.lock`;\n\n if (existsSync(lockPath)) {\n // Another worker is creating it — wait for it\n const start = Date.now();\n while (existsSync(lockPath) && Date.now() - start < 30_000) {\n await new Promise((r) => setTimeout(r, 100));\n }\n }\n\n if (existsSync(this.templatePath)) {\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n return;\n }\n\n // Acquire lock\n const { writeFileSync } = await import('node:fs');\n writeFileSync(lockPath, process.pid.toString());\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes',\n },\n stdio: 'pipe',\n });\n\n // Checkpoint WAL so the template is a single file (safe to copy)\n const tmpDb = new Database(this.templatePath);\n tmpDb.pragma('wal_checkpoint(TRUNCATE)');\n tmpDb.close();\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n // Release lock\n try {\n unlinkSync(lockPath);\n } catch {\n /* Ignore */\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"sqlite.adapter.js","names":[],"sources":["../src/adapters/postgres.adapter.ts","../src/adapters/redis.adapter.ts","../src/adapters/sqlite.adapter.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { Client } from 'pg';\n\nimport type { IsolationStrategy } from '../infra/ports/isolation.port.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { DatabasePort } from './ports/database.port.js';\n\nexport interface PostgresOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n /** Override environment variables. */\n env?: Record<string, string>;\n}\n\nexport class PostgresHandle implements DatabasePort, ServiceHandle {\n readonly type = 'postgres';\n readonly composeName: null | string;\n readonly defaultPort = 5432;\n readonly defaultImage: string;\n readonly environment: Record<string, string>;\n\n connectionString = '';\n started = false;\n\n private client: Client | null = null;\n private originalConnectionString = '';\n private schema = 'public';\n\n constructor(options: PostgresOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'postgres:17';\n this.environment = {\n POSTGRES_DB: 'test',\n POSTGRES_PASSWORD: 'test',\n POSTGRES_USER: 'test',\n ...options.env,\n };\n }\n\n buildConnectionString(host: string, port: number): string {\n const user = this.environment.POSTGRES_USER ?? 'test';\n const password = this.environment.POSTGRES_PASSWORD ?? 'test';\n const db = this.environment.POSTGRES_DB ?? 'test';\n return `postgresql://${user}:${password}@${host}:${port}/${db}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('postgres: cannot healthcheck — no connection string');\n }\n\n // Healthcheck uses a throwaway client (connection might not be established yet)\n try {\n const client = new Client({ connectionString: this.connectionString });\n await client.connect();\n await client.query('SELECT 1');\n await client.end();\n } catch (error: any) {\n throw new Error(\n `postgres healthcheck failed: ${error.message || error.code || String(error)}`,\n { cause: error },\n );\n }\n }\n\n async initialize(composeDir: string): Promise<void> {\n if (!this.composeName) {\n return;\n }\n\n const initPaths = [\n resolve(composeDir, `${this.composeName}/init.sql`),\n resolve(composeDir, 'postgres/init.sql'),\n ];\n\n for (const initPath of initPaths) {\n if (existsSync(initPath)) {\n const sql = readFileSync(initPath, 'utf8');\n try {\n await this.seed(sql);\n } catch (error: any) {\n throw new Error(\n `postgres init script failed (${initPath}):\\n${error.message}`,\n {\n cause: error,\n },\n );\n }\n return;\n }\n }\n }\n\n private async getClient(): Promise<Client> {\n if (this.client) {\n return this.client;\n }\n const client = new Client({ connectionString: this.connectionString });\n client.on('error', () => {\n // Connection dropped (container stopped) — reset so next call reconnects\n this.client = null;\n });\n await client.connect();\n this.client = client;\n return client;\n }\n\n async seed(sql: string): Promise<void> {\n const client = await this.getClient();\n await client.query(sql);\n }\n\n async reset(): Promise<void> {\n const client = await this.getClient();\n const result = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = '${this.schema}'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of result.rows) {\n await client.query(`TRUNCATE \"${this.schema}\".\"${row.tablename}\" CASCADE`);\n }\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const client = await this.getClient();\n const columnList = columns.join(', ');\n const result = await client.query(\n `SELECT ${columnList} FROM \"${this.schema}\".\"${table}\" ORDER BY 1`,\n );\n return result.rows.map((row: Record<string, unknown>) => columns.map((col) => row[col]));\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n const workerSchema = `worker_${workerId}`;\n this.originalConnectionString = this.connectionString;\n const client = await this.getClient();\n\n // Create schema by cloning all tables from public\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n await client.query(`CREATE SCHEMA \"${workerSchema}\"`);\n\n // Copy table structures (no data) from public\n const tables = await client.query(`\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND tablename NOT LIKE '_prisma%'\n `);\n for (const row of tables.rows) {\n await client.query(\n `CREATE TABLE \"${workerSchema}\".\"${row.tablename}\" (LIKE \"public\".\"${row.tablename}\" INCLUDING ALL)`,\n );\n }\n\n // Switch this handle to use the worker schema\n this.schema = workerSchema;\n await client.query(`SET search_path TO \"${workerSchema}\", public`);\n\n // Update connectionString so app connections also use the worker schema\n const url = new URL(this.connectionString);\n url.searchParams.set('options', `-c search_path=${workerSchema},public`);\n this.connectionString = url.toString();\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n const client = await this.getClient();\n const workerSchema = this.schema;\n this.schema = 'public';\n this.connectionString = this.originalConnectionString;\n await client.query(`SET search_path TO public`);\n await client.query(`DROP SCHEMA IF EXISTS \"${workerSchema}\" CASCADE`);\n },\n };\n }\n}\n\n/**\n * Create a PostgreSQL service handle.\n *\n * @example\n * const db = postgres({ compose: \"db\" });\n * // After start: db.connectionString is populated\n */\nexport function postgres(options: PostgresOptions = {}): PostgresHandle {\n return new PostgresHandle(options);\n}\n","import type { IsolationStrategy } from '../infra/ports/isolation.port.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { DatabasePort } from './ports/database.port.js';\n\nexport interface RedisOptions {\n /** Map to a service in docker-compose.test.yaml. */\n compose?: string;\n /** Override image. */\n image?: string;\n}\n\nexport class RedisHandle implements ServiceHandle {\n readonly type = 'redis';\n readonly composeName: null | string;\n readonly defaultPort = 6379;\n readonly defaultImage: string;\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private dbIndex = 0;\n\n constructor(options: RedisOptions = {}) {\n this.composeName = options.compose ?? null;\n this.defaultImage = options.image ?? 'redis:7';\n }\n\n buildConnectionString(host: string, port: number): string {\n return `redis://${host}:${port}`;\n }\n\n createDatabaseAdapter(): DatabasePort | null {\n return null;\n }\n\n async healthcheck(): Promise<void> {\n if (!this.connectionString) {\n throw new Error('redis: cannot healthcheck — no connection string');\n }\n\n try {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString });\n await client.connect();\n await client.ping();\n await client.disconnect();\n } catch (error: any) {\n throw new Error(\n `redis healthcheck failed: ${error.message || error.code || String(error)}`,\n {\n cause: error,\n },\n );\n }\n }\n\n async initialize(): Promise<void> {\n // Redis doesn't need initialization scripts\n }\n\n async reset(): Promise<void> {\n const { createClient } = await import('redis');\n const client = createClient({ url: this.connectionString, database: this.dbIndex });\n await client.connect();\n try {\n await client.flushDb();\n } finally {\n await client.disconnect();\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n // Use Redis database index 1-15 for workers (0 is default/shared)\n const numericId = Number.parseInt(workerId, 10) || 0;\n this.dbIndex = (numericId % 15) + 1;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n await this.reset();\n this.dbIndex = 0;\n },\n };\n }\n}\n\n/**\n * Create a Redis service handle.\n *\n * @example\n * const cache = redis({ compose: \"cache\" });\n * // After start: cache.connectionString is populated\n */\nexport function redis(options: RedisOptions = {}): RedisHandle {\n return new RedisHandle(options);\n}\n","import Database from 'better-sqlite3';\nimport { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { IsolationStrategy } from '../infra/ports/isolation.port.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { DatabasePort } from './ports/database.port.js';\n\nexport interface SqliteOptions {\n /**\n * Path to a SQL file used to initialize the database schema.\n * Mutually exclusive with `prismaSchema`.\n */\n init?: string;\n /**\n * Path to a Prisma schema directory or file.\n * The adapter runs `prisma db push` to create the template.\n * Mutually exclusive with `init`.\n */\n prismaSchema?: string;\n}\n\nexport class SqliteHandle implements DatabasePort, ServiceHandle {\n readonly type = 'sqlite';\n readonly composeName = null;\n readonly defaultPort = 0;\n readonly defaultImage = '';\n readonly environment: Record<string, string> = {};\n\n connectionString = '';\n started = false;\n\n private db: Database.Database | null = null;\n private templatePath = '';\n private workerDbPath = '';\n private initSql: null | string;\n private prismaSchema: null | string;\n\n constructor(options: SqliteOptions = {}) {\n this.initSql = options.init ?? null;\n this.prismaSchema = options.prismaSchema ?? null;\n }\n\n buildConnectionString(): string {\n return `file:${this.workerDbPath || this.templatePath}`;\n }\n\n createDatabaseAdapter(): DatabasePort {\n return this;\n }\n\n async healthcheck(): Promise<void> {\n // SQLite is always ready — it's a file\n }\n\n async initialize(): Promise<void> {\n // Each test run gets a fresh template; workers share it via lock\n this.templatePath = resolve(tmpdir(), 'jterrazz-test-sqlite-template.sqlite');\n const lockPath = `${this.templatePath}.lock`;\n\n if (existsSync(lockPath)) {\n // Another worker is creating it — wait for it\n const start = Date.now();\n while (existsSync(lockPath) && Date.now() - start < 30_000) {\n await new Promise((r) => setTimeout(r, 100));\n }\n }\n\n if (existsSync(this.templatePath)) {\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n return;\n }\n\n // Acquire lock\n const { writeFileSync } = await import('node:fs');\n writeFileSync(lockPath, process.pid.toString());\n\n if (this.prismaSchema) {\n // Use Prisma to create schema\n const { execSync } = await import('node:child_process');\n execSync('npx prisma db push --force-reset', {\n env: {\n ...process.env,\n DATABASE_URL: `file:${this.templatePath}`,\n PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes',\n },\n stdio: 'pipe',\n });\n\n // Checkpoint WAL so the template is a single file (safe to copy)\n const tmpDb = new Database(this.templatePath);\n tmpDb.pragma('wal_checkpoint(TRUNCATE)');\n tmpDb.close();\n } else if (this.initSql) {\n // Use raw SQL to create schema\n const sql = readFileSync(this.initSql, 'utf8');\n const templateDb = new Database(this.templatePath);\n templateDb.exec(sql);\n templateDb.close();\n } else {\n // Empty database — consumer will seed\n const templateDb = new Database(this.templatePath);\n templateDb.close();\n }\n\n // Release lock\n try {\n unlinkSync(lockPath);\n } catch {\n /* Ignore */\n }\n\n this.connectionString = `file:${this.templatePath}`;\n this.started = true;\n }\n\n private getDb(): Database.Database {\n const dbPath = this.workerDbPath || this.templatePath;\n if (!this.db) {\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n }\n return this.db;\n }\n\n private closeDb(): void {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n async seed(sql: string): Promise<void> {\n this.getDb().exec(sql);\n }\n\n async query(table: string, columns: string[]): Promise<unknown[][]> {\n const columnList = columns.join(', ');\n const rows = this.getDb()\n .prepare(`SELECT ${columnList} FROM \"${table}\" ORDER BY 1`)\n .all() as Record<string, unknown>[];\n return rows.map((row) => columns.map((col) => row[col]));\n }\n\n async reset(): Promise<void> {\n const db = this.getDb();\n const tables = db\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`,\n )\n .all() as { name: string }[];\n for (const { name } of tables) {\n db.exec(`DELETE FROM \"${name}\"`);\n }\n }\n\n isolation(): IsolationStrategy {\n return {\n acquire: async (workerId: string) => {\n this.closeDb();\n this.workerDbPath = resolve(\n tmpdir(),\n `test-worker-${workerId}-${Date.now()}.sqlite`,\n );\n copyFileSync(this.templatePath, this.workerDbPath);\n this.connectionString = `file:${this.workerDbPath}`;\n },\n\n reset: async () => {\n await this.reset();\n },\n\n release: async () => {\n this.closeDb();\n if (this.workerDbPath && existsSync(this.workerDbPath)) {\n unlinkSync(this.workerDbPath);\n }\n this.workerDbPath = '';\n this.connectionString = `file:${this.templatePath}`;\n },\n };\n }\n}\n\n/**\n * Create a SQLite service handle. Uses file-copy isolation for parallel tests.\n *\n * @example\n * // With Prisma schema\n * const db = sqlite({ prismaSchema: './prisma/schema' });\n *\n * // With raw SQL init\n * const db = sqlite({ init: './schema.sql' });\n *\n * // Empty database\n * const db = sqlite();\n */\nexport function sqlite(options: SqliteOptions = {}): SqliteHandle {\n return new SqliteHandle(options);\n}\n"],"mappings":"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"}