@fragno-dev/test 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/adapters.ts CHANGED
@@ -12,6 +12,8 @@ import { createRequire } from "node:module";
12
12
  import { mkdir, writeFile, rm } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
  import { existsSync } from "node:fs";
15
+ import type { BaseTestContext } from ".";
16
+ import { createCommonTestContextMethods } from ".";
15
17
 
16
18
  // Adapter configuration types
17
19
  export interface KyselySqliteAdapter {
@@ -30,44 +32,47 @@ export interface DrizzlePgliteAdapter {
30
32
 
31
33
  export type SupportedAdapter = KyselySqliteAdapter | KyselyPgliteAdapter | DrizzlePgliteAdapter;
32
34
 
33
- // Conditional return types based on adapter
34
- export type TestContext<T extends SupportedAdapter> = T extends
35
+ // Schema configuration for multi-schema adapters
36
+ export interface SchemaConfig {
37
+ schema: AnySchema;
38
+ namespace: string;
39
+ migrateToVersion?: number;
40
+ }
41
+
42
+ // Internal test context extends BaseTestContext with getOrm (not exposed publicly)
43
+ interface InternalTestContext extends BaseTestContext {
44
+ getOrm: <TSchema extends AnySchema>(namespace: string) => AbstractQuery<TSchema>;
45
+ }
46
+
47
+ // Conditional return types based on adapter (adapter-specific properties only)
48
+ export type AdapterContext<T extends SupportedAdapter> = T extends
35
49
  | KyselySqliteAdapter
36
50
  | KyselyPgliteAdapter
37
51
  ? {
38
- readonly db: AbstractQuery<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
39
52
  readonly kysely: Kysely<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
40
- readonly adapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
41
- resetDatabase: () => Promise<void>;
42
- cleanup: () => Promise<void>;
43
53
  }
44
54
  : T extends DrizzlePgliteAdapter
45
55
  ? {
46
- readonly db: AbstractQuery<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
47
56
  readonly drizzle: ReturnType<typeof drizzle<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any
48
- readonly adapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
49
- resetDatabase: () => Promise<void>;
50
- cleanup: () => Promise<void>;
51
57
  }
52
58
  : never;
53
59
 
54
60
  // Factory function return type
55
61
  interface AdapterFactoryResult<T extends SupportedAdapter> {
56
- testContext: TestContext<T>;
62
+ testContext: InternalTestContext & AdapterContext<T>;
57
63
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
64
  adapter: DatabaseAdapter<any>;
59
65
  }
60
66
 
61
67
  /**
62
68
  * Create Kysely + SQLite adapter using SQLocalKysely (always in-memory)
69
+ * Supports multiple schemas with separate namespaces
63
70
  */
64
71
  export async function createKyselySqliteAdapter(
65
72
  _config: KyselySqliteAdapter,
66
- schema: AnySchema,
67
- namespace: string,
68
- migrateToVersion?: number,
73
+ schemas: SchemaConfig[],
69
74
  ): Promise<AdapterFactoryResult<KyselySqliteAdapter>> {
70
- // Helper to create a new database instance and run migrations
75
+ // Helper to create a new database instance and run migrations for all schemas
71
76
  const createDatabase = async () => {
72
77
  // Create SQLocalKysely instance (always in-memory for tests)
73
78
  const { dialect } = new SQLocalKysely(":memory:");
@@ -82,37 +87,43 @@ export async function createKyselySqliteAdapter(
82
87
  provider: "sqlite",
83
88
  });
84
89
 
85
- // Run migrations
86
- const migrator = adapter.createMigrationEngine(schema, namespace);
87
- const preparedMigration = migrateToVersion
88
- ? await migrator.prepareMigrationTo(migrateToVersion, {
89
- updateSettings: false,
90
- })
91
- : await migrator.prepareMigration({
92
- updateSettings: false,
93
- });
94
- await preparedMigration.execute();
95
-
96
- // Create ORM instance
90
+ // Run migrations for all schemas in order
97
91
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
- const orm = adapter.createQueryEngine(schema, namespace) as AbstractQuery<any>;
92
+ const ormMap = new Map<string, AbstractQuery<any, any>>();
93
+
94
+ for (const { schema, namespace, migrateToVersion } of schemas) {
95
+ // Run migrations
96
+ const migrator = adapter.createMigrationEngine(schema, namespace);
97
+ const preparedMigration = migrateToVersion
98
+ ? await migrator.prepareMigrationTo(migrateToVersion, {
99
+ updateSettings: false,
100
+ })
101
+ : await migrator.prepareMigration({
102
+ updateSettings: false,
103
+ });
104
+ await preparedMigration.execute();
105
+
106
+ // Create ORM instance and store in map
107
+ const orm = adapter.createQueryEngine(schema, namespace);
108
+ ormMap.set(namespace, orm);
109
+ }
99
110
 
100
- return { kysely, adapter, orm };
111
+ return { kysely, adapter, ormMap };
101
112
  };
102
113
 
103
114
  // Create initial database
104
- let { kysely, adapter, orm } = await createDatabase();
115
+ let { kysely, adapter, ormMap } = await createDatabase();
105
116
 
106
- // Reset database function - creates a fresh in-memory database and re-runs migrations
117
+ // Reset database function - truncates all tables (only supported for in-memory databases)
107
118
  const resetDatabase = async () => {
108
- // Destroy the old Kysely instance
109
- await kysely.destroy();
110
-
111
- // Create a new database instance
112
- const newDb = await createDatabase();
113
- kysely = newDb.kysely;
114
- adapter = newDb.adapter;
115
- orm = newDb.orm;
119
+ // For SQLite, truncate all tables by deleting rows
120
+ for (const { schema, namespace } of schemas) {
121
+ const mapper = adapter.createTableNameMapper(namespace);
122
+ for (const tableName of Object.keys(schema.tables)) {
123
+ const physicalTableName = mapper.toPhysical(tableName);
124
+ await kysely.deleteFrom(physicalTableName).execute();
125
+ }
126
+ }
116
127
  };
117
128
 
118
129
  // Cleanup function - closes connections (no files to delete for in-memory)
@@ -120,17 +131,17 @@ export async function createKyselySqliteAdapter(
120
131
  await kysely.destroy();
121
132
  };
122
133
 
134
+ const commonMethods = createCommonTestContextMethods(ormMap);
135
+
123
136
  return {
124
137
  testContext: {
125
- get db() {
126
- return orm;
127
- },
128
138
  get kysely() {
129
139
  return kysely;
130
140
  },
131
141
  get adapter() {
132
142
  return adapter;
133
143
  },
144
+ ...commonMethods,
134
145
  resetDatabase,
135
146
  cleanup,
136
147
  },
@@ -142,16 +153,15 @@ export async function createKyselySqliteAdapter(
142
153
 
143
154
  /**
144
155
  * Create Kysely + PGLite adapter using kysely-pglite
156
+ * Supports multiple schemas with separate namespaces
145
157
  */
146
158
  export async function createKyselyPgliteAdapter(
147
159
  config: KyselyPgliteAdapter,
148
- schema: AnySchema,
149
- namespace: string,
150
- migrateToVersion?: number,
160
+ schemas: SchemaConfig[],
151
161
  ): Promise<AdapterFactoryResult<KyselyPgliteAdapter>> {
152
162
  const databasePath = config.databasePath;
153
163
 
154
- // Helper to create a new database instance and run migrations
164
+ // Helper to create a new database instance and run migrations for all schemas
155
165
  const createDatabase = async () => {
156
166
  // Create KyselyPGlite instance
157
167
  const kyselyPglite = await KyselyPGlite.create(databasePath);
@@ -168,44 +178,47 @@ export async function createKyselyPgliteAdapter(
168
178
  provider: "postgresql",
169
179
  });
170
180
 
171
- // Run migrations
172
- const migrator = adapter.createMigrationEngine(schema, namespace);
173
- const preparedMigration = migrateToVersion
174
- ? await migrator.prepareMigrationTo(migrateToVersion, {
175
- updateSettings: false,
176
- })
177
- : await migrator.prepareMigration({
178
- updateSettings: false,
179
- });
180
- await preparedMigration.execute();
181
-
182
- // Create ORM instance
181
+ // Run migrations for all schemas in order
183
182
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
- const orm = adapter.createQueryEngine(schema, namespace) as AbstractQuery<any>;
183
+ const ormMap = new Map<string, AbstractQuery<any, any>>();
184
+
185
+ for (const { schema, namespace, migrateToVersion } of schemas) {
186
+ // Run migrations
187
+ const migrator = adapter.createMigrationEngine(schema, namespace);
188
+ const preparedMigration = migrateToVersion
189
+ ? await migrator.prepareMigrationTo(migrateToVersion, {
190
+ updateSettings: false,
191
+ })
192
+ : await migrator.prepareMigration({
193
+ updateSettings: false,
194
+ });
195
+ await preparedMigration.execute();
196
+
197
+ // Create ORM instance and store in map
198
+ const orm = adapter.createQueryEngine(schema, namespace);
199
+ ormMap.set(namespace, orm);
200
+ }
185
201
 
186
- return { kysely, adapter, kyselyPglite, orm };
202
+ return { kysely, adapter, kyselyPglite, ormMap };
187
203
  };
188
204
 
189
205
  // Create initial database
190
- let { kysely, adapter, kyselyPglite, orm } = await createDatabase();
206
+ const { kysely, adapter, kyselyPglite, ormMap } = await createDatabase();
191
207
 
192
- // Reset database function - creates a fresh database and re-runs migrations
208
+ // Reset database function - truncates all tables (only supported for in-memory databases)
193
209
  const resetDatabase = async () => {
194
- // Close the old instances
195
- await kysely.destroy();
196
-
197
- try {
198
- await kyselyPglite.client.close();
199
- } catch {
200
- // Ignore if already closed
210
+ if (databasePath && databasePath !== ":memory:") {
211
+ throw new Error("resetDatabase is only supported for in-memory databases");
201
212
  }
202
213
 
203
- // Create a new database instance
204
- const newDb = await createDatabase();
205
- kysely = newDb.kysely;
206
- adapter = newDb.adapter;
207
- kyselyPglite = newDb.kyselyPglite;
208
- orm = newDb.orm;
214
+ // Truncate all tables
215
+ for (const { schema, namespace } of schemas) {
216
+ const mapper = adapter.createTableNameMapper(namespace);
217
+ for (const tableName of Object.keys(schema.tables)) {
218
+ const physicalTableName = mapper.toPhysical(tableName);
219
+ await kysely.deleteFrom(physicalTableName).execute();
220
+ }
221
+ }
209
222
  };
210
223
 
211
224
  // Cleanup function - closes connections and deletes database directory
@@ -224,17 +237,17 @@ export async function createKyselyPgliteAdapter(
224
237
  }
225
238
  };
226
239
 
240
+ const commonMethods = createCommonTestContextMethods(ormMap);
241
+
227
242
  return {
228
243
  testContext: {
229
- get db() {
230
- return orm;
231
- },
232
244
  get kysely() {
233
245
  return kysely;
234
246
  },
235
247
  get adapter() {
236
248
  return adapter;
237
249
  },
250
+ ...commonMethods,
238
251
  resetDatabase,
239
252
  cleanup,
240
253
  },
@@ -246,12 +259,11 @@ export async function createKyselyPgliteAdapter(
246
259
 
247
260
  /**
248
261
  * Create Drizzle + PGLite adapter using drizzle-orm/pglite
262
+ * Supports multiple schemas with separate namespaces
249
263
  */
250
264
  export async function createDrizzlePgliteAdapter(
251
265
  config: DrizzlePgliteAdapter,
252
- schema: AnySchema,
253
- namespace: string,
254
- _migrateToVersion?: number,
266
+ schemas: SchemaConfig[],
255
267
  ): Promise<AdapterFactoryResult<DrizzlePgliteAdapter>> {
256
268
  const databasePath = config.databasePath;
257
269
 
@@ -275,8 +287,13 @@ export async function createDrizzlePgliteAdapter(
275
287
  `test-schema-${Date.now()}-${Math.random().toString(36).slice(2, 9)}.ts`,
276
288
  );
277
289
 
278
- // Generate and write the Drizzle schema to file
279
- const drizzleSchemaTs = generateSchema([{ namespace: namespace ?? "", schema }], "postgresql");
290
+ // Combine all schemas into a single Drizzle schema with namespaced tables
291
+ const schemaConfigs = schemas.map(({ schema, namespace }) => ({
292
+ namespace: namespace ?? "",
293
+ schema,
294
+ }));
295
+
296
+ const drizzleSchemaTs = generateSchema(schemaConfigs, "postgresql");
280
297
  await writeFile(schemaFilePath, drizzleSchemaTs, "utf-8");
281
298
 
282
299
  // Dynamically import the generated schema (with cache busting)
@@ -289,7 +306,7 @@ export async function createDrizzlePgliteAdapter(
289
306
  return { schemaModule, cleanup };
290
307
  };
291
308
 
292
- // Helper to create a new database instance and run migrations
309
+ // Helper to create a new database instance and run migrations for all schemas
293
310
  const createDatabase = async () => {
294
311
  // Write schema to file and load it
295
312
  const { schemaModule, cleanup } = await writeAndLoadSchema();
@@ -320,29 +337,41 @@ export async function createDrizzlePgliteAdapter(
320
337
  provider: "postgresql",
321
338
  });
322
339
 
323
- // Create ORM instance
340
+ // Create ORM instances for each schema and store in map
324
341
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
325
- const orm = adapter.createQueryEngine(schema, namespace) as AbstractQuery<any>;
342
+ const ormMap = new Map<string, AbstractQuery<any, any>>();
343
+
344
+ for (const { schema, namespace } of schemas) {
345
+ const orm = adapter.createQueryEngine(schema, namespace);
346
+ ormMap.set(namespace, orm);
347
+ }
326
348
 
327
- return { drizzle: db, adapter, pglite, cleanup, orm };
349
+ return { drizzle: db, adapter, pglite, cleanup, ormMap };
328
350
  };
329
351
 
330
352
  // Create initial database
331
- let { drizzle: drizzleDb, adapter, pglite, cleanup: schemaCleanup, orm } = await createDatabase();
332
-
333
- // Reset database function - creates a fresh database and re-runs migrations
353
+ const {
354
+ drizzle: drizzleDb,
355
+ adapter,
356
+ pglite,
357
+ cleanup: schemaCleanup,
358
+ ormMap,
359
+ } = await createDatabase();
360
+
361
+ // Reset database function - truncates all tables (only supported for in-memory databases)
334
362
  const resetDatabase = async () => {
335
- // Close the old instances and cleanup
336
- await pglite.close();
337
- await schemaCleanup();
363
+ if (databasePath && databasePath !== ":memory:") {
364
+ throw new Error("resetDatabase is only supported for in-memory databases");
365
+ }
338
366
 
339
- // Create a new database instance
340
- const newDb = await createDatabase();
341
- drizzleDb = newDb.drizzle;
342
- adapter = newDb.adapter;
343
- pglite = newDb.pglite;
344
- schemaCleanup = newDb.cleanup;
345
- orm = newDb.orm;
367
+ // Truncate all tables by deleting rows
368
+ for (const { schema, namespace } of schemas) {
369
+ const mapper = adapter.createTableNameMapper(namespace);
370
+ for (const tableName of Object.keys(schema.tables)) {
371
+ const physicalTableName = mapper.toPhysical(tableName);
372
+ await drizzleDb.execute(`DELETE FROM "${physicalTableName}"`);
373
+ }
374
+ }
346
375
  };
347
376
 
348
377
  // Cleanup function - closes connections and deletes generated files and database directory
@@ -356,17 +385,17 @@ export async function createDrizzlePgliteAdapter(
356
385
  }
357
386
  };
358
387
 
388
+ const commonMethods = createCommonTestContextMethods(ormMap);
389
+
359
390
  return {
360
391
  testContext: {
361
- get db() {
362
- return orm;
363
- },
364
392
  get drizzle() {
365
393
  return drizzleDb;
366
394
  },
367
395
  get adapter() {
368
396
  return adapter;
369
397
  },
398
+ ...commonMethods,
370
399
  resetDatabase,
371
400
  cleanup,
372
401
  },
@@ -378,28 +407,18 @@ export async function createDrizzlePgliteAdapter(
378
407
 
379
408
  /**
380
409
  * Create adapter based on configuration
410
+ * Supports multiple schemas with separate namespaces
381
411
  */
382
412
  export async function createAdapter<T extends SupportedAdapter>(
383
413
  adapterConfig: T,
384
- schema: AnySchema,
385
- namespace: string,
386
- migrateToVersion?: number,
414
+ schemas: SchemaConfig[],
387
415
  ): Promise<AdapterFactoryResult<T>> {
388
416
  if (adapterConfig.type === "kysely-sqlite") {
389
- return createKyselySqliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
390
- AdapterFactoryResult<T>
391
- >;
417
+ return createKyselySqliteAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
392
418
  } else if (adapterConfig.type === "kysely-pglite") {
393
- return createKyselyPgliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
394
- AdapterFactoryResult<T>
395
- >;
419
+ return createKyselyPgliteAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
396
420
  } else if (adapterConfig.type === "drizzle-pglite") {
397
- return createDrizzlePgliteAdapter(
398
- adapterConfig,
399
- schema,
400
- namespace,
401
- migrateToVersion,
402
- ) as Promise<AdapterFactoryResult<T>>;
421
+ return createDrizzlePgliteAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
403
422
  }
404
423
 
405
424
  throw new Error(`Unsupported adapter type: ${(adapterConfig as SupportedAdapter).type}`);