@fragno-dev/test 0.1.4 → 0.1.6

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,379 @@
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 { createRequire } from "node:module";
11
+ import { mkdir, writeFile, rm } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { existsSync } from "node:fs";
14
+
15
+ // Adapter configuration types
16
+ export interface KyselySqliteAdapter {
17
+ type: "kysely-sqlite";
18
+ }
19
+
20
+ export interface KyselyPgliteAdapter {
21
+ type: "kysely-pglite";
22
+ databasePath?: string;
23
+ }
24
+
25
+ export interface DrizzlePgliteAdapter {
26
+ type: "drizzle-pglite";
27
+ databasePath?: string;
28
+ }
29
+
30
+ export type SupportedAdapter = KyselySqliteAdapter | KyselyPgliteAdapter | DrizzlePgliteAdapter;
31
+
32
+ // Conditional return types based on adapter
33
+ export type TestContext<T extends SupportedAdapter> = T extends
34
+ | KyselySqliteAdapter
35
+ | KyselyPgliteAdapter
36
+ ? {
37
+ readonly kysely: Kysely<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
38
+ readonly adapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
39
+ resetDatabase: () => Promise<void>;
40
+ cleanup: () => Promise<void>;
41
+ }
42
+ : T extends DrizzlePgliteAdapter
43
+ ? {
44
+ readonly drizzle: ReturnType<typeof drizzle<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any
45
+ readonly adapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
46
+ resetDatabase: () => Promise<void>;
47
+ cleanup: () => Promise<void>;
48
+ }
49
+ : never;
50
+
51
+ // Factory function return type
52
+ interface AdapterFactoryResult<T extends SupportedAdapter> {
53
+ testContext: TestContext<T>;
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ adapter: DatabaseAdapter<any>;
56
+ }
57
+
58
+ /**
59
+ * Create Kysely + SQLite adapter using SQLocalKysely (always in-memory)
60
+ */
61
+ export async function createKyselySqliteAdapter(
62
+ _config: KyselySqliteAdapter,
63
+ schema: AnySchema,
64
+ namespace: string,
65
+ migrateToVersion?: number,
66
+ ): Promise<AdapterFactoryResult<KyselySqliteAdapter>> {
67
+ // Helper to create a new database instance and run migrations
68
+ const createDatabase = async () => {
69
+ // Create SQLocalKysely instance (always in-memory for tests)
70
+ const { dialect } = new SQLocalKysely(":memory:");
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ const kysely = new Kysely<any>({
73
+ dialect,
74
+ });
75
+
76
+ // Create KyselyAdapter
77
+ const adapter = new KyselyAdapter({
78
+ db: kysely,
79
+ provider: "sqlite",
80
+ });
81
+
82
+ // Run migrations
83
+ const migrator = adapter.createMigrationEngine(schema, namespace);
84
+ const preparedMigration = migrateToVersion
85
+ ? await migrator.prepareMigrationTo(migrateToVersion, {
86
+ updateSettings: false,
87
+ })
88
+ : await migrator.prepareMigration({
89
+ updateSettings: false,
90
+ });
91
+ await preparedMigration.execute();
92
+
93
+ return { kysely, adapter };
94
+ };
95
+
96
+ // Create initial database
97
+ let { kysely, adapter } = await createDatabase();
98
+
99
+ // Reset database function - creates a fresh in-memory database and re-runs migrations
100
+ const resetDatabase = async () => {
101
+ // Destroy the old Kysely instance
102
+ await kysely.destroy();
103
+
104
+ // Create a new database instance
105
+ const newDb = await createDatabase();
106
+ kysely = newDb.kysely;
107
+ adapter = newDb.adapter;
108
+ };
109
+
110
+ // Cleanup function - closes connections (no files to delete for in-memory)
111
+ const cleanup = async () => {
112
+ await kysely.destroy();
113
+ };
114
+
115
+ return {
116
+ testContext: {
117
+ get kysely() {
118
+ return kysely;
119
+ },
120
+ get adapter() {
121
+ return adapter;
122
+ },
123
+ resetDatabase,
124
+ cleanup,
125
+ },
126
+ get adapter() {
127
+ return adapter;
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Create Kysely + PGLite adapter using kysely-pglite
134
+ */
135
+ export async function createKyselyPgliteAdapter(
136
+ config: KyselyPgliteAdapter,
137
+ schema: AnySchema,
138
+ namespace: string,
139
+ migrateToVersion?: number,
140
+ ): Promise<AdapterFactoryResult<KyselyPgliteAdapter>> {
141
+ const databasePath = config.databasePath;
142
+
143
+ // Helper to create a new database instance and run migrations
144
+ const createDatabase = async () => {
145
+ // Create KyselyPGlite instance
146
+ const kyselyPglite = await KyselyPGlite.create(databasePath);
147
+
148
+ // Create Kysely instance with PGlite dialect
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ const kysely = new Kysely<any>({
151
+ dialect: kyselyPglite.dialect,
152
+ });
153
+
154
+ // Create KyselyAdapter
155
+ const adapter = new KyselyAdapter({
156
+ db: kysely,
157
+ provider: "postgresql",
158
+ });
159
+
160
+ // Run migrations
161
+ const migrator = adapter.createMigrationEngine(schema, namespace);
162
+ const preparedMigration = migrateToVersion
163
+ ? await migrator.prepareMigrationTo(migrateToVersion, {
164
+ updateSettings: false,
165
+ })
166
+ : await migrator.prepareMigration({
167
+ updateSettings: false,
168
+ });
169
+ await preparedMigration.execute();
170
+
171
+ return { kysely, adapter, kyselyPglite };
172
+ };
173
+
174
+ // Create initial database
175
+ let { kysely, adapter, kyselyPglite } = await createDatabase();
176
+
177
+ // Reset database function - creates a fresh database and re-runs migrations
178
+ const resetDatabase = async () => {
179
+ // Close the old instances
180
+ await kysely.destroy();
181
+
182
+ try {
183
+ await kyselyPglite.client.close();
184
+ } catch {
185
+ // Ignore if already closed
186
+ }
187
+
188
+ // Create a new database instance
189
+ const newDb = await createDatabase();
190
+ kysely = newDb.kysely;
191
+ adapter = newDb.adapter;
192
+ kyselyPglite = newDb.kyselyPglite;
193
+ };
194
+
195
+ // Cleanup function - closes connections and deletes database directory
196
+ const cleanup = async () => {
197
+ await kysely.destroy();
198
+
199
+ try {
200
+ await kyselyPglite.client.close();
201
+ } catch {
202
+ // Ignore if already closed
203
+ }
204
+
205
+ // Delete the database directory if it exists and is a file path
206
+ if (databasePath && databasePath !== ":memory:" && existsSync(databasePath)) {
207
+ await rm(databasePath, { recursive: true, force: true });
208
+ }
209
+ };
210
+
211
+ return {
212
+ testContext: {
213
+ get kysely() {
214
+ return kysely;
215
+ },
216
+ get adapter() {
217
+ return adapter;
218
+ },
219
+ resetDatabase,
220
+ cleanup,
221
+ },
222
+ get adapter() {
223
+ return adapter;
224
+ },
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Create Drizzle + PGLite adapter using drizzle-orm/pglite
230
+ */
231
+ export async function createDrizzlePgliteAdapter(
232
+ config: DrizzlePgliteAdapter,
233
+ schema: AnySchema,
234
+ namespace: string,
235
+ _migrateToVersion?: number,
236
+ ): Promise<AdapterFactoryResult<DrizzlePgliteAdapter>> {
237
+ const databasePath = config.databasePath;
238
+
239
+ // Import drizzle-kit for migrations
240
+ const require = createRequire(import.meta.url);
241
+ const { generateDrizzleJson, generateMigration } =
242
+ require("drizzle-kit/api") as typeof import("drizzle-kit/api");
243
+
244
+ // Import generateSchema from the properly exported module
245
+ const { generateSchema } = await import("@fragno-dev/db/adapters/drizzle/generate");
246
+
247
+ // Helper to write schema to file and dynamically import it
248
+ const writeAndLoadSchema = async () => {
249
+ const testDir = join(import.meta.dirname, "_generated", "drizzle-test");
250
+ await mkdir(testDir, { recursive: true }).catch(() => {
251
+ // Ignore error if directory already exists
252
+ });
253
+
254
+ const schemaFilePath = join(
255
+ testDir,
256
+ `test-schema-${Date.now()}-${Math.random().toString(36).slice(2, 9)}.ts`,
257
+ );
258
+
259
+ // Generate and write the Drizzle schema to file
260
+ const drizzleSchemaTs = generateSchema([{ namespace: namespace ?? "", schema }], "postgresql");
261
+ await writeFile(schemaFilePath, drizzleSchemaTs, "utf-8");
262
+
263
+ // Dynamically import the generated schema (with cache busting)
264
+ const schemaModule = await import(`${schemaFilePath}?t=${Date.now()}`);
265
+
266
+ const cleanup = async () => {
267
+ await rm(testDir, { recursive: true, force: true });
268
+ };
269
+
270
+ return { schemaModule, cleanup };
271
+ };
272
+
273
+ // Helper to create a new database instance and run migrations
274
+ const createDatabase = async () => {
275
+ // Write schema to file and load it
276
+ const { schemaModule, cleanup } = await writeAndLoadSchema();
277
+
278
+ // Create PGlite instance
279
+ const pglite = new PGlite(databasePath);
280
+
281
+ // Create Drizzle instance with PGlite
282
+ const db = drizzle(pglite, {
283
+ schema: schemaModule,
284
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
+ }) as any;
286
+
287
+ // Generate and run migrations
288
+ const migrationStatements = await generateMigration(
289
+ generateDrizzleJson({}), // Empty schema (starting state)
290
+ generateDrizzleJson(schemaModule), // Target schema
291
+ );
292
+
293
+ // Execute migration SQL
294
+ for (const statement of migrationStatements) {
295
+ await db.execute(statement);
296
+ }
297
+
298
+ // Create DrizzleAdapter
299
+ const adapter = new DrizzleAdapter({
300
+ db: () => db,
301
+ provider: "postgresql",
302
+ });
303
+
304
+ return { drizzle: db, adapter, pglite, cleanup };
305
+ };
306
+
307
+ // Create initial database
308
+ let { drizzle: drizzleDb, adapter, pglite, cleanup: schemaCleanup } = await createDatabase();
309
+
310
+ // Reset database function - creates a fresh database and re-runs migrations
311
+ const resetDatabase = async () => {
312
+ // Close the old instances and cleanup
313
+ await pglite.close();
314
+ await schemaCleanup();
315
+
316
+ // Create a new database instance
317
+ const newDb = await createDatabase();
318
+ drizzleDb = newDb.drizzle;
319
+ adapter = newDb.adapter;
320
+ pglite = newDb.pglite;
321
+ schemaCleanup = newDb.cleanup;
322
+ };
323
+
324
+ // Cleanup function - closes connections and deletes generated files and database directory
325
+ const cleanup = async () => {
326
+ await pglite.close();
327
+ await schemaCleanup();
328
+
329
+ // Delete the database directory if it exists and is a file path
330
+ if (databasePath && databasePath !== ":memory:" && existsSync(databasePath)) {
331
+ await rm(databasePath, { recursive: true, force: true });
332
+ }
333
+ };
334
+
335
+ return {
336
+ testContext: {
337
+ get drizzle() {
338
+ return drizzleDb;
339
+ },
340
+ get adapter() {
341
+ return adapter;
342
+ },
343
+ resetDatabase,
344
+ cleanup,
345
+ },
346
+ get adapter() {
347
+ return adapter;
348
+ },
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Create adapter based on configuration
354
+ */
355
+ export async function createAdapter<T extends SupportedAdapter>(
356
+ adapterConfig: T,
357
+ schema: AnySchema,
358
+ namespace: string,
359
+ migrateToVersion?: number,
360
+ ): Promise<AdapterFactoryResult<T>> {
361
+ if (adapterConfig.type === "kysely-sqlite") {
362
+ return createKyselySqliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
363
+ AdapterFactoryResult<T>
364
+ >;
365
+ } else if (adapterConfig.type === "kysely-pglite") {
366
+ return createKyselyPgliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
367
+ AdapterFactoryResult<T>
368
+ >;
369
+ } else if (adapterConfig.type === "drizzle-pglite") {
370
+ return createDrizzlePgliteAdapter(
371
+ adapterConfig,
372
+ schema,
373
+ namespace,
374
+ migrateToVersion,
375
+ ) as Promise<AdapterFactoryResult<T>>;
376
+ }
377
+
378
+ throw new Error(`Unsupported adapter type: ${(adapterConfig as SupportedAdapter).type}`);
379
+ }
package/src/index.test.ts CHANGED
@@ -55,7 +55,9 @@ describe("createDatabaseFragmentForTest", () => {
55
55
  });
56
56
 
57
57
  it("should use in-memory database by default", async () => {
58
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
58
+ const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
59
+ adapter: { type: "kysely-sqlite" },
60
+ });
59
61
 
60
62
  // Should be able to create and query users
61
63
  const user = await fragment.services.createUser({
@@ -77,8 +79,8 @@ describe("createDatabaseFragmentForTest", () => {
77
79
  });
78
80
 
79
81
  it("should create database at specified path", async () => {
80
- const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
81
- databasePath: testDbPath,
82
+ const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
83
+ adapter: { type: "kysely-sqlite", databasePath: testDbPath },
82
84
  });
83
85
 
84
86
  // Create a user
@@ -101,7 +103,9 @@ describe("createDatabaseFragmentForTest", () => {
101
103
 
102
104
  describe("migrateToVersion option", () => {
103
105
  it("should migrate to latest version by default", async () => {
104
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
106
+ const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
107
+ adapter: { type: "kysely-sqlite" },
108
+ });
105
109
 
106
110
  // Should have the 'age' column from version 2
107
111
  const user = await fragment.services.createUser({
@@ -120,14 +124,15 @@ describe("createDatabaseFragmentForTest", () => {
120
124
 
121
125
  it("should migrate to specific version when specified", async () => {
122
126
  // Migrate to version 1 (before 'age' column was added)
123
- const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
127
+ const { test } = await createDatabaseFragmentForTest(testFragmentDef, {
128
+ adapter: { type: "kysely-sqlite" },
124
129
  migrateToVersion: 1,
125
130
  });
126
131
 
127
132
  // Query the database directly to check schema
128
133
  // In version 1, we should be able to insert without the age column
129
134
  const tableName = "users_test-fragment-db";
130
- await fragment.kysely
135
+ await test.kysely
131
136
  .insertInto(tableName)
132
137
  .values({
133
138
  id: "test-id-1",
@@ -136,7 +141,7 @@ describe("createDatabaseFragmentForTest", () => {
136
141
  })
137
142
  .execute();
138
143
 
139
- const result = await fragment.kysely.selectFrom(tableName).selectAll().execute();
144
+ const result = await test.kysely.selectFrom(tableName).selectAll().execute();
140
145
 
141
146
  expect(result).toHaveLength(1);
142
147
  expect(result[0]).toMatchObject({
@@ -150,7 +155,8 @@ describe("createDatabaseFragmentForTest", () => {
150
155
 
151
156
  it("should allow creating user with age when migrated to version 2", async () => {
152
157
  // Explicitly migrate to version 2
153
- const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
158
+ const { fragment, test } = await createDatabaseFragmentForTest(testFragmentDef, {
159
+ adapter: { type: "kysely-sqlite" },
154
160
  migrateToVersion: 2,
155
161
  });
156
162
 
@@ -169,7 +175,7 @@ describe("createDatabaseFragmentForTest", () => {
169
175
  });
170
176
 
171
177
  const tableName = "users_test-fragment-db";
172
- const result = await fragment.kysely.selectFrom(tableName).selectAll().execute();
178
+ const result = await test.kysely.selectFrom(tableName).selectAll().execute();
173
179
 
174
180
  expect(result).toHaveLength(1);
175
181
  expect(result[0]).toMatchObject({
@@ -194,8 +200,8 @@ describe("createDatabaseFragmentForTest", () => {
194
200
  });
195
201
 
196
202
  it("should work with both databasePath and migrateToVersion", async () => {
197
- const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
198
- databasePath: testDbPath,
203
+ const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
204
+ adapter: { type: "kysely-sqlite", databasePath: testDbPath },
199
205
  migrateToVersion: 2,
200
206
  });
201
207
 
@@ -225,21 +231,27 @@ describe("createDatabaseFragmentForTest", () => {
225
231
 
226
232
  describe("fragment initialization", () => {
227
233
  it("should provide kysely instance", async () => {
228
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
234
+ const { test } = await createDatabaseFragmentForTest(testFragmentDef, {
235
+ adapter: { type: "kysely-sqlite" },
236
+ });
229
237
 
230
- expect(fragment.kysely).toBeDefined();
231
- expect(typeof fragment.kysely.selectFrom).toBe("function");
238
+ expect(test.kysely).toBeDefined();
239
+ expect(typeof test.kysely.selectFrom).toBe("function");
232
240
  });
233
241
 
234
242
  it("should provide adapter instance", async () => {
235
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
243
+ const { test } = await createDatabaseFragmentForTest(testFragmentDef, {
244
+ adapter: { type: "kysely-sqlite" },
245
+ });
236
246
 
237
- expect(fragment.adapter).toBeDefined();
238
- expect(typeof fragment.adapter.createMigrationEngine).toBe("function");
247
+ expect(test.adapter).toBeDefined();
248
+ expect(typeof test.adapter.createMigrationEngine).toBe("function");
239
249
  });
240
250
 
241
251
  it("should have all standard fragment test properties", async () => {
242
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
252
+ const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
253
+ adapter: { type: "kysely-sqlite" },
254
+ });
243
255
 
244
256
  expect(fragment.services).toBeDefined();
245
257
  expect(fragment.initRoutes).toBeDefined();
@@ -258,14 +270,18 @@ describe("createDatabaseFragmentForTest", () => {
258
270
 
259
271
  await expect(
260
272
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
261
- createDatabaseFragmentForTest(nonDbFragment as any),
273
+ createDatabaseFragmentForTest(nonDbFragment as any, {
274
+ adapter: { type: "kysely-sqlite" },
275
+ }),
262
276
  ).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
263
277
  });
264
278
  });
265
279
 
266
280
  describe("route handling with defineRoutes", () => {
267
281
  it("should handle route factory with multiple routes", async () => {
268
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
282
+ const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
283
+ adapter: { type: "kysely-sqlite" },
284
+ });
269
285
 
270
286
  type Config = {};
271
287
  type Deps = {};
@@ -357,33 +373,36 @@ describe("createDatabaseFragmentForTest", () => {
357
373
 
358
374
  describe("resetDatabase", () => {
359
375
  it("should clear all data and recreate a fresh database", async () => {
360
- const fragment = await createDatabaseFragmentForTest(testFragmentDef);
376
+ // Don't destructure so we can access the updated fragment through getters after reset
377
+ const result = await createDatabaseFragmentForTest(testFragmentDef, {
378
+ adapter: { type: "kysely-sqlite" },
379
+ });
361
380
 
362
381
  // Create some users
363
- await fragment.services.createUser({
382
+ await result.services.createUser({
364
383
  name: "User 1",
365
384
  email: "user1@example.com",
366
385
  age: 25,
367
386
  });
368
- await fragment.services.createUser({
387
+ await result.services.createUser({
369
388
  name: "User 2",
370
389
  email: "user2@example.com",
371
390
  age: 30,
372
391
  });
373
392
 
374
393
  // Verify users exist
375
- let users = await fragment.services.getUsers();
394
+ let users = await result.services.getUsers();
376
395
  expect(users).toHaveLength(2);
377
396
 
378
397
  // Reset the database
379
- await fragment.resetDatabase();
398
+ await result.test.resetDatabase();
380
399
 
381
- // Verify database is empty
382
- users = await fragment.services.getUsers();
400
+ // Verify database is empty (accessing through result to get updated fragment)
401
+ users = await result.services.getUsers();
383
402
  expect(users).toHaveLength(0);
384
403
 
385
404
  // Verify we can still create new users after reset
386
- const newUser = await fragment.services.createUser({
405
+ const newUser = await result.services.createUser({
387
406
  name: "User After Reset",
388
407
  email: "after@example.com",
389
408
  age: 35,
@@ -396,9 +415,118 @@ describe("createDatabaseFragmentForTest", () => {
396
415
  age: 35,
397
416
  });
398
417
 
399
- users = await fragment.services.getUsers();
418
+ users = await result.services.getUsers();
400
419
  expect(users).toHaveLength(1);
401
420
  expect(users[0]).toMatchObject(newUser);
402
421
  });
403
422
  });
423
+
424
+ describe("multiple adapters with auth-like schema", () => {
425
+ // Simplified auth schema for testing
426
+ const authSchema = schema((s) => {
427
+ return s
428
+ .addTable("user", (t) => {
429
+ return t
430
+ .addColumn("id", idColumn())
431
+ .addColumn("email", column("string"))
432
+ .addColumn("passwordHash", column("string"))
433
+ .createIndex("idx_user_email", ["email"]);
434
+ })
435
+ .addTable("session", (t) => {
436
+ return t
437
+ .addColumn("id", idColumn())
438
+ .addColumn("userId", column("string"))
439
+ .addColumn("expiresAt", column("timestamp"))
440
+ .createIndex("idx_session_user", ["userId"]);
441
+ });
442
+ });
443
+
444
+ const authFragmentDef = defineFragmentWithDatabase<{}>("auth-test")
445
+ .withDatabase(authSchema)
446
+ .withServices(({ orm }) => {
447
+ return {
448
+ createUser: async (email: string, passwordHash: string) => {
449
+ const id = await orm.create("user", { email, passwordHash });
450
+ return { id: id.valueOf(), email, passwordHash };
451
+ },
452
+ createSession: async (userId: string) => {
453
+ const expiresAt = new Date();
454
+ expiresAt.setDate(expiresAt.getDate() + 30);
455
+ const id = await orm.create("session", { userId, expiresAt });
456
+ return { id: id.valueOf(), userId, expiresAt };
457
+ },
458
+ getUserByEmail: async (email: string) => {
459
+ const user = await orm.findFirst("user", (b) =>
460
+ b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)),
461
+ );
462
+ if (!user) {
463
+ return null;
464
+ }
465
+ return { id: user.id.valueOf(), email: user.email, passwordHash: user.passwordHash };
466
+ },
467
+ };
468
+ });
469
+
470
+ const adapters = [
471
+ { name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
472
+ { name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
473
+ { name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
474
+ ];
475
+
476
+ for (const { name, adapter } of adapters) {
477
+ describe(name, () => {
478
+ it(
479
+ "should create user and session",
480
+ async () => {
481
+ const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, {
482
+ adapter,
483
+ });
484
+
485
+ // Create a user
486
+ const user = await fragment.services.createUser("test@test.com", "hashed-password");
487
+ expect(user).toMatchObject({
488
+ id: expect.any(String),
489
+ email: "test@test.com",
490
+ passwordHash: "hashed-password",
491
+ });
492
+
493
+ // Create a session for the user
494
+ const session = await fragment.services.createSession(user.id);
495
+ expect(session).toMatchObject({
496
+ id: expect.any(String),
497
+ userId: user.id,
498
+ expiresAt: expect.any(Date),
499
+ });
500
+
501
+ // Find user by email
502
+ const foundUser = await fragment.services.getUserByEmail("test@test.com");
503
+ expect(foundUser).toMatchObject({
504
+ id: user.id,
505
+ email: "test@test.com",
506
+ passwordHash: "hashed-password",
507
+ });
508
+
509
+ // Cleanup
510
+ await test.cleanup();
511
+ },
512
+ { timeout: 10000 },
513
+ );
514
+
515
+ it(
516
+ "should return null when user not found",
517
+ async () => {
518
+ const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, {
519
+ adapter,
520
+ });
521
+
522
+ const notFound = await fragment.services.getUserByEmail("nonexistent@test.com");
523
+ expect(notFound).toBeNull();
524
+
525
+ await test.cleanup();
526
+ },
527
+ { timeout: 10000 },
528
+ );
529
+ });
530
+ }
531
+ });
404
532
  });