@happyvertical/smrt-vitest 0.30.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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Test database utilities for SMRT tests
3
+ *
4
+ * Automatically uses PostgreSQL when DATABASE_URL is set (CI environment),
5
+ * otherwise creates unique SQLite temp files to avoid concurrency issues.
6
+ *
7
+ * Supports transaction-based test isolation: each test runs in a transaction
8
+ * that gets rolled back, ensuring clean state between tests.
9
+ *
10
+ * @example Basic usage
11
+ * ```typescript
12
+ * import { getTestDbConfig, createTestDb } from '@happyvertical/smrt-vitest';
13
+ *
14
+ * const { config, cleanup } = await createTestDb();
15
+ * // Use config...
16
+ * await cleanup();
17
+ * ```
18
+ *
19
+ * @example Transaction isolation (recommended for parallel tests)
20
+ * ```typescript
21
+ * import { createIsolatedTestDb } from '@happyvertical/smrt-vitest';
22
+ *
23
+ * const { db, cleanup } = await createIsolatedTestDb({ schema: mySchema });
24
+ * // All operations run in a transaction
25
+ * await db.insert('users', { id: '1', name: 'Test' });
26
+ * // cleanup() rolls back the transaction - no data persists
27
+ * await cleanup();
28
+ * ```
29
+ *
30
+ * @packageDocumentation
31
+ */
32
+ import type { DatabaseInterfaceWithTransaction, TransactionHandle } from './types.js';
33
+ /**
34
+ * Options for {@link createIsolatedTestDbFromManifest}.
35
+ */
36
+ export interface ManifestTestDbOptions {
37
+ /**
38
+ * Explicit path to a manifest JSON file.
39
+ *
40
+ * When omitted the function searches the following locations in order:
41
+ * 1. `.smrt/manifest.json` — output of `smrtVitestPlugin()` / `smrt generate:test`
42
+ * 2. `dist/manifest.json` — production build manifest
43
+ * 3. `src/manifest/manifest.json` — legacy location
44
+ */
45
+ manifestPath?: string;
46
+ /**
47
+ * Restrict schema creation to a subset of class names.
48
+ *
49
+ * Accepts either simple class names (`'Product'`) or fully-qualified
50
+ * manifest keys (`'@my-org/smrt-models:Product'`). When omitted, every
51
+ * object that has a schema in the manifest is included.
52
+ *
53
+ * @example `['Product', 'Order', 'OrderItem']`
54
+ */
55
+ includeObjects?: string[];
56
+ /**
57
+ * Prefix for the SQLite temp-file name. Ignored for PostgreSQL.
58
+ * @default 'smrt-manifest'
59
+ */
60
+ prefix?: string;
61
+ }
62
+ /**
63
+ * Supported test database adapters.
64
+ *
65
+ * - `'sqlite'` — file-based or in-memory SQLite (default for local development)
66
+ * - `'postgres'` — PostgreSQL, used when `DATABASE_URL` is set (CI)
67
+ */
68
+ export type TestDbAdapter = 'sqlite' | 'postgres';
69
+ /**
70
+ * Database connection configuration for a test run.
71
+ *
72
+ * Returned by {@link getTestDbConfig} and {@link getInMemoryDbConfig}.
73
+ * Pass to `@happyvertical/sql`'s `getDatabase()` to open a connection.
74
+ */
75
+ export interface TestDbConfig {
76
+ /** Adapter type — determines the driver used to open the connection. */
77
+ type: 'sqlite' | 'postgres';
78
+ /**
79
+ * Connection URL.
80
+ *
81
+ * - SQLite: absolute path to a `.db` file, or `':memory:'`
82
+ * - PostgreSQL: `postgresql://user:pass@host:port/dbname`
83
+ */
84
+ url: string;
85
+ }
86
+ /**
87
+ * Detect the database adapter to use based on the current environment.
88
+ *
89
+ * Resolution order:
90
+ * 1. `TEST_DB_ADAPTER` env var — explicit override (`'sqlite'` or `'postgres'`)
91
+ * 2. `DATABASE_URL` env var set → `'postgres'`
92
+ * 3. Default → `'sqlite'`
93
+ *
94
+ * @returns The adapter identifier for the current environment.
95
+ * @see {@link getTestDbConfig} to obtain a full {@link TestDbConfig}.
96
+ */
97
+ export declare function getTestAdapter(): TestDbAdapter;
98
+ /**
99
+ * Check whether PostgreSQL is available for the current test run.
100
+ *
101
+ * Returns `true` when the `DATABASE_URL` environment variable is set,
102
+ * which is the signal used by CI environments to opt into PostgreSQL.
103
+ *
104
+ * @returns `true` if `DATABASE_URL` is set, `false` otherwise.
105
+ * @see {@link getTestAdapter} for full adapter resolution logic.
106
+ */
107
+ export declare function isPostgresAvailable(): boolean;
108
+ /**
109
+ * Get a {@link TestDbConfig} appropriate for the current environment.
110
+ *
111
+ * Uses PostgreSQL when `DATABASE_URL` is set (CI), otherwise generates
112
+ * a unique SQLite temp-file path to prevent concurrency conflicts between
113
+ * parallel test workers.
114
+ *
115
+ * @param prefix - Optional prefix used in the SQLite temp-file name.
116
+ * Ignored when using PostgreSQL. Defaults to `'smrt-test'`.
117
+ * @returns A {@link TestDbConfig} ready to pass to `getDatabase()`.
118
+ * @see {@link getInMemoryDbConfig} for a non-persistent SQLite alternative.
119
+ * @see {@link createTestDb} to obtain the config alongside a cleanup function.
120
+ */
121
+ export declare function getTestDbConfig(prefix?: string): TestDbConfig;
122
+ /**
123
+ * Get a {@link TestDbConfig} backed by an in-memory SQLite database.
124
+ *
125
+ * In-memory databases are isolated per connection — safe for concurrent
126
+ * tests within the same process, but the database cannot be shared across
127
+ * connections or workers. No temp files are created or cleaned up.
128
+ *
129
+ * Prefer {@link getTestDbConfig} (file-based SQLite or PostgreSQL) when
130
+ * tests need to be shared or inspected after the run.
131
+ *
132
+ * @returns A `TestDbConfig` with `url: ':memory:'`.
133
+ */
134
+ export declare function getInMemoryDbConfig(): TestDbConfig;
135
+ /**
136
+ * Create a test database and return a cleanup function.
137
+ *
138
+ * Determines the adapter automatically via {@link getTestDbConfig}. For
139
+ * SQLite, a unique temp file is created; `cleanup()` removes it along with
140
+ * any WAL/SHM sidecar files. For PostgreSQL, `cleanup()` is a no-op
141
+ * (table isolation must be handled by the test itself).
142
+ *
143
+ * Unlike {@link createIsolatedTestDb}, this function does **not** wrap
144
+ * operations in a transaction — mutations made during the test persist until
145
+ * the temp file is deleted. Prefer {@link createIsolatedTestDb} for
146
+ * isolated, parallel-safe tests.
147
+ *
148
+ * @param prefix - Prefix for the SQLite temp-file name. Ignored for
149
+ * PostgreSQL. Defaults to `'smrt-test'`.
150
+ * @returns An object containing the resolved {@link TestDbConfig} and an
151
+ * async `cleanup()` function that removes the temp file on SQLite.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * import { createTestDb } from '@happyvertical/smrt-vitest';
156
+ *
157
+ * const { config, cleanup } = await createTestDb();
158
+ * const db = await getDatabase(config);
159
+ * // ... run tests ...
160
+ * await cleanup();
161
+ * ```
162
+ *
163
+ * @see {@link createIsolatedTestDb} for transaction-isolated test databases.
164
+ */
165
+ export declare function createTestDb(prefix?: string): Promise<{
166
+ config: TestDbConfig;
167
+ cleanup: () => Promise<void>;
168
+ }>;
169
+ /**
170
+ * Get a human-readable display name for the current test database adapter.
171
+ *
172
+ * Useful for labelling `describe` blocks or test output so logs make clear
173
+ * which backend is under test.
174
+ *
175
+ * @returns `'PostgreSQL'` when the adapter is `'postgres'`, otherwise `'SQLite'`.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * import { getAdapterDisplayName } from '@happyvertical/smrt-vitest';
180
+ *
181
+ * describe(`Product (${getAdapterDisplayName()})`, () => {
182
+ * // ...
183
+ * });
184
+ * ```
185
+ *
186
+ * @see {@link getTestAdapter} to obtain the raw adapter identifier.
187
+ */
188
+ export declare function getAdapterDisplayName(): string;
189
+ /**
190
+ * Options for {@link createIsolatedTestDb}.
191
+ */
192
+ export interface IsolatedTestDbOptions {
193
+ /**
194
+ * Raw SQL DDL to execute against the database before the transaction begins.
195
+ *
196
+ * The DDL is applied outside the transaction (required for DDL on SQLite and
197
+ * some PostgreSQL configurations), so it persists for the lifetime of the
198
+ * temp database. The transaction wraps only the DML that follows.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * schema: `
203
+ * CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL);
204
+ * CREATE TABLE orders (id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id));
205
+ * `
206
+ * ```
207
+ */
208
+ schema?: string;
209
+ /**
210
+ * Prefix for the SQLite temp-file name. Ignored for PostgreSQL.
211
+ * @default 'smrt-isolated'
212
+ */
213
+ prefix?: string;
214
+ }
215
+ /**
216
+ * Result returned by {@link createIsolatedTestDb} and
217
+ * {@link createIsolatedTestDbFromManifest}.
218
+ */
219
+ export interface IsolatedTestDbResult {
220
+ /**
221
+ * Transaction-scoped database handle.
222
+ *
223
+ * Use this for all DML inside your test. All operations run within the
224
+ * open transaction and are rolled back when `cleanup()` is called.
225
+ */
226
+ db: TransactionHandle;
227
+ /**
228
+ * The underlying database connection, opened before the transaction began.
229
+ *
230
+ * Use only for operations that must run outside the transaction (e.g.,
231
+ * reading sequences or checking schema state). Most tests should use
232
+ * `db` instead.
233
+ */
234
+ baseDb: DatabaseInterfaceWithTransaction;
235
+ /**
236
+ * The resolved {@link TestDbConfig} used to open the connection.
237
+ *
238
+ * Useful for introspection (e.g., logging which adapter is under test).
239
+ */
240
+ config: TestDbConfig;
241
+ /**
242
+ * Roll back the transaction, close the connection, and delete any SQLite
243
+ * temp files.
244
+ *
245
+ * **Always** call this in `afterEach()` or a `finally` block to prevent
246
+ * connection leaks and temp-file accumulation.
247
+ */
248
+ cleanup: () => Promise<void>;
249
+ }
250
+ /**
251
+ * Create a test database with transaction isolation.
252
+ *
253
+ * Each test runs in a transaction that gets rolled back on `cleanup()`,
254
+ * ensuring complete isolation between tests without the overhead of
255
+ * creating or dropping tables between runs. Parallel test workers each
256
+ * receive their own temp database file (SQLite) or an independent
257
+ * transaction (PostgreSQL).
258
+ *
259
+ * Requires `@happyvertical/sql` with `beginTransaction()` support
260
+ * (SDK PR #722). Throws if the adapter does not implement it.
261
+ *
262
+ * @param options - Optional schema DDL and SQLite prefix.
263
+ * Pass `schema` to have the DDL applied before the transaction begins.
264
+ * @returns An {@link IsolatedTestDbResult} containing the transaction handle,
265
+ * base connection, resolved config, and a `cleanup()` function.
266
+ *
267
+ * @see {@link createIsolatedTestDbFromManifest} to derive the schema
268
+ * automatically from the generated manifest file.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * import { createIsolatedTestDb } from '@happyvertical/smrt-vitest';
273
+ * import { beforeEach, afterEach, it } from 'vitest';
274
+ *
275
+ * let db: TransactionHandle;
276
+ * let cleanup: () => Promise<void>;
277
+ *
278
+ * beforeEach(async () => {
279
+ * const result = await createIsolatedTestDb({
280
+ * schema: `CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)`
281
+ * });
282
+ * db = result.db;
283
+ * cleanup = result.cleanup;
284
+ * });
285
+ *
286
+ * afterEach(async () => {
287
+ * await cleanup(); // Rolls back - no data persists
288
+ * });
289
+ *
290
+ * it('should insert and query', async () => {
291
+ * await db.insert('users', { id: '1', name: 'Alice' });
292
+ * const user = await db.get('users', { id: '1' });
293
+ * expect(user?.name).toBe('Alice');
294
+ * // After this test, cleanup() rolls back - Alice doesn't exist
295
+ * });
296
+ *
297
+ * it('should start with clean state', async () => {
298
+ * // This test runs with a fresh transaction
299
+ * const users = await db.list('users', {});
300
+ * expect(users).toHaveLength(0); // Clean!
301
+ * });
302
+ * ```
303
+ */
304
+ export declare function createIsolatedTestDb(options?: IsolatedTestDbOptions): Promise<IsolatedTestDbResult>;
305
+ /**
306
+ * Create an isolated test database with schema derived from a manifest file.
307
+ *
308
+ * Eliminates the need to manually write or maintain DDL in test files by
309
+ * reading table definitions directly from the generated manifest. Handles:
310
+ *
311
+ * - **STI deduplication** — multiple classes that share the same table are
312
+ * merged into a single `CREATE TABLE` statement that includes all columns.
313
+ * - **FK dependency ordering** — tables are created in topological order so
314
+ * `REFERENCES` constraints are always satisfied.
315
+ * - **Auto-detection** — searches `.smrt/manifest.json`, `dist/manifest.json`,
316
+ * and `src/manifest/manifest.json` when no `manifestPath` is given.
317
+ *
318
+ * @param options - Optional manifest path, class filter, and SQLite prefix.
319
+ * @returns An {@link IsolatedTestDbResult} — same shape as
320
+ * {@link createIsolatedTestDb}, with a transaction-scoped `db` handle and
321
+ * a `cleanup()` that rolls back and removes temp files.
322
+ *
323
+ * @throws When no manifest is found at any of the checked locations.
324
+ * @throws When the manifest contains no objects with a database schema (or
325
+ * none of the filtered `includeObjects` have a schema).
326
+ *
327
+ * @see {@link createIsolatedTestDb} if you prefer to supply raw DDL directly.
328
+ * @see {@link ManifestTestDbOptions} for all available options.
329
+ *
330
+ * @example Basic usage
331
+ * ```typescript
332
+ * import { createIsolatedTestDbFromManifest } from '@happyvertical/smrt-vitest';
333
+ *
334
+ * let db, cleanup;
335
+ *
336
+ * beforeEach(async () => {
337
+ * ({ db, cleanup } = await createIsolatedTestDbFromManifest());
338
+ * });
339
+ *
340
+ * afterEach(async () => {
341
+ * await cleanup();
342
+ * });
343
+ * ```
344
+ *
345
+ * @example With tenant scoping
346
+ * ```typescript
347
+ * import { createIsolatedTestDbFromManifest } from '@happyvertical/smrt-vitest';
348
+ * import { withTenant, resetTenancy, setupTestTenancy } from '@happyvertical/smrt-tenancy';
349
+ *
350
+ * // In setup file
351
+ * setupTestTenancy({ enableInterceptors: true, rawQueryPolicy: 'allow' });
352
+ *
353
+ * // In test file
354
+ * let db, cleanup;
355
+ *
356
+ * beforeEach(async () => {
357
+ * ({ db, cleanup } = await createIsolatedTestDbFromManifest());
358
+ * });
359
+ *
360
+ * afterEach(async () => {
361
+ * resetTenancy();
362
+ * await cleanup();
363
+ * });
364
+ *
365
+ * it('should auto-populate tenantId', async () => {
366
+ * await withTenant({ tenantId: 'test-tenant' }, async () => {
367
+ * const product = await collection.create({ name: 'Widget' });
368
+ * expect(product.tenantId).toBe('test-tenant');
369
+ * });
370
+ * });
371
+ * ```
372
+ *
373
+ * @example Filter to specific objects
374
+ * ```typescript
375
+ * const { db, cleanup } = await createIsolatedTestDbFromManifest({
376
+ * includeObjects: ['Product', 'Order', 'OrderItem'],
377
+ * });
378
+ * ```
379
+ */
380
+ export declare function createIsolatedTestDbFromManifest(options?: ManifestTestDbOptions): Promise<IsolatedTestDbResult>;
381
+ //# sourceMappingURL=test-db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-db.d.ts","sourceRoot":"","sources":["../src/test-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAcH,OAAO,KAAK,EACV,gCAAgC,EAChC,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAwCpB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,CAAC;AAElD;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,IAAI,aAAa,CAa9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,MAAM,SAAc,GAAG,YAAY,CAmBlE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,IAAI,YAAY,CAKlD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,YAAY,CAAC,MAAM,SAAc,GAAG,OAAO,CAAC;IAChE,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CA2BD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAQ9C;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,EAAE,EAAE,iBAAiB,CAAC;IAEtB;;;;;;OAMG;IACH,MAAM,EAAE,gCAAgC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CA4E/B;AA2mBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,wBAAsB,gCAAgC,CACpD,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CA4D/B"}