@fragno-dev/test 0.0.0-canary-20251030115355

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,406 @@
1
+ import { Kysely } from "kysely";
2
+ import { SQLocalKysely } from "sqlocal/kysely";
3
+ import { KyselyPGlite } from "kysely-pglite";
4
+ import { drizzle } from "drizzle-orm/pglite";
5
+ import { PGlite } from "@electric-sql/pglite";
6
+ import { KyselyAdapter } from "@fragno-dev/db/adapters/kysely";
7
+ import { DrizzleAdapter } from "@fragno-dev/db/adapters/drizzle";
8
+ import type { AnySchema } from "@fragno-dev/db/schema";
9
+ import type { DatabaseAdapter } from "@fragno-dev/db/adapters";
10
+ import type { AbstractQuery } from "@fragno-dev/db/query";
11
+ import { createRequire } from "node:module";
12
+ import { mkdir, writeFile, rm } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { existsSync } from "node:fs";
15
+
16
+ // Adapter configuration types
17
+ export interface KyselySqliteAdapter {
18
+ type: "kysely-sqlite";
19
+ }
20
+
21
+ export interface KyselyPgliteAdapter {
22
+ type: "kysely-pglite";
23
+ databasePath?: string;
24
+ }
25
+
26
+ export interface DrizzlePgliteAdapter {
27
+ type: "drizzle-pglite";
28
+ databasePath?: string;
29
+ }
30
+
31
+ export type SupportedAdapter = KyselySqliteAdapter | KyselyPgliteAdapter | DrizzlePgliteAdapter;
32
+
33
+ // Conditional return types based on adapter
34
+ export type TestContext<T extends SupportedAdapter> = T extends
35
+ | KyselySqliteAdapter
36
+ | KyselyPgliteAdapter
37
+ ? {
38
+ readonly db: AbstractQuery<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
39
+ 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
+ }
44
+ : T extends DrizzlePgliteAdapter
45
+ ? {
46
+ readonly db: AbstractQuery<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
47
+ 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
+ }
52
+ : never;
53
+
54
+ // Factory function return type
55
+ interface AdapterFactoryResult<T extends SupportedAdapter> {
56
+ testContext: TestContext<T>;
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ adapter: DatabaseAdapter<any>;
59
+ }
60
+
61
+ /**
62
+ * Create Kysely + SQLite adapter using SQLocalKysely (always in-memory)
63
+ */
64
+ export async function createKyselySqliteAdapter(
65
+ _config: KyselySqliteAdapter,
66
+ schema: AnySchema,
67
+ namespace: string,
68
+ migrateToVersion?: number,
69
+ ): Promise<AdapterFactoryResult<KyselySqliteAdapter>> {
70
+ // Helper to create a new database instance and run migrations
71
+ const createDatabase = async () => {
72
+ // Create SQLocalKysely instance (always in-memory for tests)
73
+ const { dialect } = new SQLocalKysely(":memory:");
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ const kysely = new Kysely<any>({
76
+ dialect,
77
+ });
78
+
79
+ // Create KyselyAdapter
80
+ const adapter = new KyselyAdapter({
81
+ db: kysely,
82
+ provider: "sqlite",
83
+ });
84
+
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
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ const orm = adapter.createQueryEngine(schema, namespace) as AbstractQuery<any>;
99
+
100
+ return { kysely, adapter, orm };
101
+ };
102
+
103
+ // Create initial database
104
+ let { kysely, adapter, orm } = await createDatabase();
105
+
106
+ // Reset database function - creates a fresh in-memory database and re-runs migrations
107
+ 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;
116
+ };
117
+
118
+ // Cleanup function - closes connections (no files to delete for in-memory)
119
+ const cleanup = async () => {
120
+ await kysely.destroy();
121
+ };
122
+
123
+ return {
124
+ testContext: {
125
+ get db() {
126
+ return orm;
127
+ },
128
+ get kysely() {
129
+ return kysely;
130
+ },
131
+ get adapter() {
132
+ return adapter;
133
+ },
134
+ resetDatabase,
135
+ cleanup,
136
+ },
137
+ get adapter() {
138
+ return adapter;
139
+ },
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Create Kysely + PGLite adapter using kysely-pglite
145
+ */
146
+ export async function createKyselyPgliteAdapter(
147
+ config: KyselyPgliteAdapter,
148
+ schema: AnySchema,
149
+ namespace: string,
150
+ migrateToVersion?: number,
151
+ ): Promise<AdapterFactoryResult<KyselyPgliteAdapter>> {
152
+ const databasePath = config.databasePath;
153
+
154
+ // Helper to create a new database instance and run migrations
155
+ const createDatabase = async () => {
156
+ // Create KyselyPGlite instance
157
+ const kyselyPglite = await KyselyPGlite.create(databasePath);
158
+
159
+ // Create Kysely instance with PGlite dialect
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ const kysely = new Kysely<any>({
162
+ dialect: kyselyPglite.dialect,
163
+ });
164
+
165
+ // Create KyselyAdapter
166
+ const adapter = new KyselyAdapter({
167
+ db: kysely,
168
+ provider: "postgresql",
169
+ });
170
+
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
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ const orm = adapter.createQueryEngine(schema, namespace) as AbstractQuery<any>;
185
+
186
+ return { kysely, adapter, kyselyPglite, orm };
187
+ };
188
+
189
+ // Create initial database
190
+ let { kysely, adapter, kyselyPglite, orm } = await createDatabase();
191
+
192
+ // Reset database function - creates a fresh database and re-runs migrations
193
+ 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
201
+ }
202
+
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;
209
+ };
210
+
211
+ // Cleanup function - closes connections and deletes database directory
212
+ const cleanup = async () => {
213
+ await kysely.destroy();
214
+
215
+ try {
216
+ await kyselyPglite.client.close();
217
+ } catch {
218
+ // Ignore if already closed
219
+ }
220
+
221
+ // Delete the database directory if it exists and is a file path
222
+ if (databasePath && databasePath !== ":memory:" && existsSync(databasePath)) {
223
+ await rm(databasePath, { recursive: true, force: true });
224
+ }
225
+ };
226
+
227
+ return {
228
+ testContext: {
229
+ get db() {
230
+ return orm;
231
+ },
232
+ get kysely() {
233
+ return kysely;
234
+ },
235
+ get adapter() {
236
+ return adapter;
237
+ },
238
+ resetDatabase,
239
+ cleanup,
240
+ },
241
+ get adapter() {
242
+ return adapter;
243
+ },
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Create Drizzle + PGLite adapter using drizzle-orm/pglite
249
+ */
250
+ export async function createDrizzlePgliteAdapter(
251
+ config: DrizzlePgliteAdapter,
252
+ schema: AnySchema,
253
+ namespace: string,
254
+ _migrateToVersion?: number,
255
+ ): Promise<AdapterFactoryResult<DrizzlePgliteAdapter>> {
256
+ const databasePath = config.databasePath;
257
+
258
+ // Import drizzle-kit for migrations
259
+ const require = createRequire(import.meta.url);
260
+ const { generateDrizzleJson, generateMigration } =
261
+ require("drizzle-kit/api") as typeof import("drizzle-kit/api");
262
+
263
+ // Import generateSchema from the properly exported module
264
+ const { generateSchema } = await import("@fragno-dev/db/adapters/drizzle/generate");
265
+
266
+ // Helper to write schema to file and dynamically import it
267
+ const writeAndLoadSchema = async () => {
268
+ const testDir = join(import.meta.dirname, "_generated", "drizzle-test");
269
+ await mkdir(testDir, { recursive: true }).catch(() => {
270
+ // Ignore error if directory already exists
271
+ });
272
+
273
+ const schemaFilePath = join(
274
+ testDir,
275
+ `test-schema-${Date.now()}-${Math.random().toString(36).slice(2, 9)}.ts`,
276
+ );
277
+
278
+ // Generate and write the Drizzle schema to file
279
+ const drizzleSchemaTs = generateSchema([{ namespace: namespace ?? "", schema }], "postgresql");
280
+ await writeFile(schemaFilePath, drizzleSchemaTs, "utf-8");
281
+
282
+ // Dynamically import the generated schema (with cache busting)
283
+ const schemaModule = await import(`${schemaFilePath}?t=${Date.now()}`);
284
+
285
+ const cleanup = async () => {
286
+ await rm(testDir, { recursive: true, force: true });
287
+ };
288
+
289
+ return { schemaModule, cleanup };
290
+ };
291
+
292
+ // Helper to create a new database instance and run migrations
293
+ const createDatabase = async () => {
294
+ // Write schema to file and load it
295
+ const { schemaModule, cleanup } = await writeAndLoadSchema();
296
+
297
+ // Create PGlite instance
298
+ const pglite = new PGlite(databasePath);
299
+
300
+ // Create Drizzle instance with PGlite
301
+ const db = drizzle(pglite, {
302
+ schema: schemaModule,
303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ }) as any;
305
+
306
+ // Generate and run migrations
307
+ const migrationStatements = await generateMigration(
308
+ generateDrizzleJson({}), // Empty schema (starting state)
309
+ generateDrizzleJson(schemaModule), // Target schema
310
+ );
311
+
312
+ // Execute migration SQL
313
+ for (const statement of migrationStatements) {
314
+ await db.execute(statement);
315
+ }
316
+
317
+ // Create DrizzleAdapter
318
+ const adapter = new DrizzleAdapter({
319
+ db: () => db,
320
+ provider: "postgresql",
321
+ });
322
+
323
+ // Create ORM instance
324
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
325
+ const orm = adapter.createQueryEngine(schema, namespace) as AbstractQuery<any>;
326
+
327
+ return { drizzle: db, adapter, pglite, cleanup, orm };
328
+ };
329
+
330
+ // 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
334
+ const resetDatabase = async () => {
335
+ // Close the old instances and cleanup
336
+ await pglite.close();
337
+ await schemaCleanup();
338
+
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;
346
+ };
347
+
348
+ // Cleanup function - closes connections and deletes generated files and database directory
349
+ const cleanup = async () => {
350
+ await pglite.close();
351
+ await schemaCleanup();
352
+
353
+ // Delete the database directory if it exists and is a file path
354
+ if (databasePath && databasePath !== ":memory:" && existsSync(databasePath)) {
355
+ await rm(databasePath, { recursive: true, force: true });
356
+ }
357
+ };
358
+
359
+ return {
360
+ testContext: {
361
+ get db() {
362
+ return orm;
363
+ },
364
+ get drizzle() {
365
+ return drizzleDb;
366
+ },
367
+ get adapter() {
368
+ return adapter;
369
+ },
370
+ resetDatabase,
371
+ cleanup,
372
+ },
373
+ get adapter() {
374
+ return adapter;
375
+ },
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Create adapter based on configuration
381
+ */
382
+ export async function createAdapter<T extends SupportedAdapter>(
383
+ adapterConfig: T,
384
+ schema: AnySchema,
385
+ namespace: string,
386
+ migrateToVersion?: number,
387
+ ): Promise<AdapterFactoryResult<T>> {
388
+ if (adapterConfig.type === "kysely-sqlite") {
389
+ return createKyselySqliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
390
+ AdapterFactoryResult<T>
391
+ >;
392
+ } else if (adapterConfig.type === "kysely-pglite") {
393
+ return createKyselyPgliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
394
+ AdapterFactoryResult<T>
395
+ >;
396
+ } else if (adapterConfig.type === "drizzle-pglite") {
397
+ return createDrizzlePgliteAdapter(
398
+ adapterConfig,
399
+ schema,
400
+ namespace,
401
+ migrateToVersion,
402
+ ) as Promise<AdapterFactoryResult<T>>;
403
+ }
404
+
405
+ throw new Error(`Unsupported adapter type: ${(adapterConfig as SupportedAdapter).type}`);
406
+ }