/** * Test database utilities for SMRT tests * * Automatically uses PostgreSQL when DATABASE_URL is set (CI environment), * otherwise creates unique SQLite temp files to avoid concurrency issues. * * Supports transaction-based test isolation: each test runs in a transaction * that gets rolled back, ensuring clean state between tests. * * @example Basic usage * ```typescript * import { getTestDbConfig, createTestDb } from '@happyvertical/smrt-vitest'; * * const { config, cleanup } = await createTestDb(); * // Use config... * await cleanup(); * ``` * * @example Transaction isolation (recommended for parallel tests) * ```typescript * import { createIsolatedTestDb } from '@happyvertical/smrt-vitest'; * * const { db, cleanup } = await createIsolatedTestDb({ schema: mySchema }); * // All operations run in a transaction * await db.insert('users', { id: '1', name: 'Test' }); * // cleanup() rolls back the transaction - no data persists * await cleanup(); * ``` * * @packageDocumentation */ import type { DatabaseInterfaceWithTransaction, TransactionHandle } from './types.js'; /** * Options for {@link createIsolatedTestDbFromManifest}. */ export interface ManifestTestDbOptions { /** * Explicit path to a manifest JSON file. * * When omitted the function searches the following locations in order: * 1. `.smrt/manifest.json` — output of `smrtVitestPlugin()` / `smrt generate:test` * 2. `dist/manifest.json` — production build manifest * 3. `src/manifest/manifest.json` — legacy location */ manifestPath?: string; /** * Restrict schema creation to a subset of class names. * * Accepts either simple class names (`'Product'`) or fully-qualified * manifest keys (`'@my-org/smrt-models:Product'`). When omitted, every * object that has a schema in the manifest is included. * * @example `['Product', 'Order', 'OrderItem']` */ includeObjects?: string[]; /** * Prefix for the SQLite temp-file name. Ignored for PostgreSQL. * @default 'smrt-manifest' */ prefix?: string; } /** * Supported test database adapters. * * - `'sqlite'` — file-based or in-memory SQLite (default for local development) * - `'postgres'` — PostgreSQL, used when `DATABASE_URL` is set (CI) */ export type TestDbAdapter = 'sqlite' | 'postgres'; /** * Database connection configuration for a test run. * * Returned by {@link getTestDbConfig} and {@link getInMemoryDbConfig}. * Pass to `@happyvertical/sql`'s `getDatabase()` to open a connection. */ export interface TestDbConfig { /** Adapter type — determines the driver used to open the connection. */ type: 'sqlite' | 'postgres'; /** * Connection URL. * * - SQLite: absolute path to a `.db` file, or `':memory:'` * - PostgreSQL: `postgresql://user:pass@host:port/dbname` */ url: string; } /** * Detect the database adapter to use based on the current environment. * * Resolution order: * 1. `TEST_DB_ADAPTER` env var — explicit override (`'sqlite'` or `'postgres'`) * 2. `DATABASE_URL` env var set → `'postgres'` * 3. Default → `'sqlite'` * * @returns The adapter identifier for the current environment. * @see {@link getTestDbConfig} to obtain a full {@link TestDbConfig}. */ export declare function getTestAdapter(): TestDbAdapter; /** * Check whether PostgreSQL is available for the current test run. * * Returns `true` when the `DATABASE_URL` environment variable is set, * which is the signal used by CI environments to opt into PostgreSQL. * * @returns `true` if `DATABASE_URL` is set, `false` otherwise. * @see {@link getTestAdapter} for full adapter resolution logic. */ export declare function isPostgresAvailable(): boolean; /** * Get a {@link TestDbConfig} appropriate for the current environment. * * Uses PostgreSQL when `DATABASE_URL` is set (CI), otherwise generates * a unique SQLite temp-file path to prevent concurrency conflicts between * parallel test workers. * * @param prefix - Optional prefix used in the SQLite temp-file name. * Ignored when using PostgreSQL. Defaults to `'smrt-test'`. * @returns A {@link TestDbConfig} ready to pass to `getDatabase()`. * @see {@link getInMemoryDbConfig} for a non-persistent SQLite alternative. * @see {@link createTestDb} to obtain the config alongside a cleanup function. */ export declare function getTestDbConfig(prefix?: string): TestDbConfig; /** * Get a {@link TestDbConfig} backed by an in-memory SQLite database. * * In-memory databases are isolated per connection — safe for concurrent * tests within the same process, but the database cannot be shared across * connections or workers. No temp files are created or cleaned up. * * Prefer {@link getTestDbConfig} (file-based SQLite or PostgreSQL) when * tests need to be shared or inspected after the run. * * @returns A `TestDbConfig` with `url: ':memory:'`. */ export declare function getInMemoryDbConfig(): TestDbConfig; /** * Create a test database and return a cleanup function. * * Determines the adapter automatically via {@link getTestDbConfig}. For * SQLite, a unique temp file is created; `cleanup()` removes it along with * any WAL/SHM sidecar files. For PostgreSQL, `cleanup()` is a no-op * (table isolation must be handled by the test itself). * * Unlike {@link createIsolatedTestDb}, this function does **not** wrap * operations in a transaction — mutations made during the test persist until * the temp file is deleted. Prefer {@link createIsolatedTestDb} for * isolated, parallel-safe tests. * * @param prefix - Prefix for the SQLite temp-file name. Ignored for * PostgreSQL. Defaults to `'smrt-test'`. * @returns An object containing the resolved {@link TestDbConfig} and an * async `cleanup()` function that removes the temp file on SQLite. * * @example * ```typescript * import { createTestDb } from '@happyvertical/smrt-vitest'; * * const { config, cleanup } = await createTestDb(); * const db = await getDatabase(config); * // ... run tests ... * await cleanup(); * ``` * * @see {@link createIsolatedTestDb} for transaction-isolated test databases. */ export declare function createTestDb(prefix?: string): Promise<{ config: TestDbConfig; cleanup: () => Promise; }>; /** * Get a human-readable display name for the current test database adapter. * * Useful for labelling `describe` blocks or test output so logs make clear * which backend is under test. * * @returns `'PostgreSQL'` when the adapter is `'postgres'`, otherwise `'SQLite'`. * * @example * ```typescript * import { getAdapterDisplayName } from '@happyvertical/smrt-vitest'; * * describe(`Product (${getAdapterDisplayName()})`, () => { * // ... * }); * ``` * * @see {@link getTestAdapter} to obtain the raw adapter identifier. */ export declare function getAdapterDisplayName(): string; /** * Options for {@link createIsolatedTestDb}. */ export interface IsolatedTestDbOptions { /** * Raw SQL DDL to execute against the database before the transaction begins. * * The DDL is applied outside the transaction (required for DDL on SQLite and * some PostgreSQL configurations), so it persists for the lifetime of the * temp database. The transaction wraps only the DML that follows. * * @example * ```typescript * schema: ` * CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL); * CREATE TABLE orders (id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id)); * ` * ``` */ schema?: string; /** * Prefix for the SQLite temp-file name. Ignored for PostgreSQL. * @default 'smrt-isolated' */ prefix?: string; } /** * Result returned by {@link createIsolatedTestDb} and * {@link createIsolatedTestDbFromManifest}. */ export interface IsolatedTestDbResult { /** * Transaction-scoped database handle. * * Use this for all DML inside your test. All operations run within the * open transaction and are rolled back when `cleanup()` is called. */ db: TransactionHandle; /** * The underlying database connection, opened before the transaction began. * * Use only for operations that must run outside the transaction (e.g., * reading sequences or checking schema state). Most tests should use * `db` instead. */ baseDb: DatabaseInterfaceWithTransaction; /** * The resolved {@link TestDbConfig} used to open the connection. * * Useful for introspection (e.g., logging which adapter is under test). */ config: TestDbConfig; /** * Roll back the transaction, close the connection, and delete any SQLite * temp files. * * **Always** call this in `afterEach()` or a `finally` block to prevent * connection leaks and temp-file accumulation. */ cleanup: () => Promise; } /** * Create a test database with transaction isolation. * * Each test runs in a transaction that gets rolled back on `cleanup()`, * ensuring complete isolation between tests without the overhead of * creating or dropping tables between runs. Parallel test workers each * receive their own temp database file (SQLite) or an independent * transaction (PostgreSQL). * * File-backed SQLite schemas are prepared once per process, snapshotted, and * copied into each later database. The template contains schema only; test * data remains isolated in the per-test transaction and database file. * * Requires `@happyvertical/sql` with `beginTransaction()` support * (SDK PR #722). Throws if the adapter does not implement it. * * @param options - Optional schema DDL and SQLite prefix. * Pass `schema` to have the DDL applied before the transaction begins. * @returns An {@link IsolatedTestDbResult} containing the transaction handle, * base connection, resolved config, and a `cleanup()` function. * * @see {@link createIsolatedTestDbFromManifest} to derive the schema * automatically from the generated manifest file. * * @example * ```typescript * import { createIsolatedTestDb } from '@happyvertical/smrt-vitest'; * import { beforeEach, afterEach, it } from 'vitest'; * * let db: TransactionHandle; * let cleanup: () => Promise; * * beforeEach(async () => { * const result = await createIsolatedTestDb({ * schema: `CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)` * }); * db = result.db; * cleanup = result.cleanup; * }); * * afterEach(async () => { * await cleanup(); // Rolls back - no data persists * }); * * it('should insert and query', async () => { * await db.insert('users', { id: '1', name: 'Alice' }); * const user = await db.get('users', { id: '1' }); * expect(user?.name).toBe('Alice'); * // After this test, cleanup() rolls back - Alice doesn't exist * }); * * it('should start with clean state', async () => { * // This test runs with a fresh transaction * const users = await db.list('users', {}); * expect(users).toHaveLength(0); // Clean! * }); * ``` */ export declare function createIsolatedTestDb(options?: IsolatedTestDbOptions): Promise; /** * Create an isolated test database with schema derived from a manifest file. * * Eliminates the need to manually write or maintain DDL in test files by * reading table definitions directly from the generated manifest. Handles: * * - **STI deduplication** — multiple classes that share the same table are * merged into a single `CREATE TABLE` statement that includes all columns. * - **FK dependency ordering** — tables are created in topological order so * `REFERENCES` constraints are always satisfied. * - **Auto-detection** — searches `.smrt/manifest.json`, `dist/manifest.json`, * and `src/manifest/manifest.json` when no `manifestPath` is given. * * @param options - Optional manifest path, class filter, and SQLite prefix. * @returns An {@link IsolatedTestDbResult} — same shape as * {@link createIsolatedTestDb}, with a transaction-scoped `db` handle and * a `cleanup()` that rolls back and removes temp files. * * @throws When no manifest is found at any of the checked locations. * @throws When the manifest contains no objects with a database schema (or * none of the filtered `includeObjects` have a schema). * * @see {@link createIsolatedTestDb} if you prefer to supply raw DDL directly. * @see {@link ManifestTestDbOptions} for all available options. * * @example Basic usage * ```typescript * import { createIsolatedTestDbFromManifest } from '@happyvertical/smrt-vitest'; * * let db, cleanup; * * beforeEach(async () => { * ({ db, cleanup } = await createIsolatedTestDbFromManifest()); * }); * * afterEach(async () => { * await cleanup(); * }); * ``` * * @example With tenant scoping * ```typescript * import { createIsolatedTestDbFromManifest } from '@happyvertical/smrt-vitest'; * import { withTenant, resetTenancy, setupTestTenancy } from '@happyvertical/smrt-tenancy'; * * // In setup file * setupTestTenancy({ enableInterceptors: true, rawQueryPolicy: 'allow' }); * * // In test file * let db, cleanup; * * beforeEach(async () => { * ({ db, cleanup } = await createIsolatedTestDbFromManifest()); * }); * * afterEach(async () => { * resetTenancy(); * await cleanup(); * }); * * it('should auto-populate tenantId', async () => { * await withTenant({ tenantId: 'test-tenant' }, async () => { * const product = await collection.create({ name: 'Widget' }); * expect(product.tenantId).toBe('test-tenant'); * }); * }); * ``` * * @example Filter to specific objects * ```typescript * const { db, cleanup } = await createIsolatedTestDbFromManifest({ * includeObjects: ['Product', 'Order', 'OrderItem'], * }); * ``` */ export declare function createIsolatedTestDbFromManifest(options?: ManifestTestDbOptions): Promise; //# sourceMappingURL=test-db.d.ts.map