@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/index.test.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { describe, expect, it, afterEach } from "vitest";
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
2
  import { column, idColumn, schema } from "@fragno-dev/db/schema";
3
- import { defineFragmentWithDatabase } from "@fragno-dev/db/fragment";
4
- import { createDatabaseFragmentForTest } from "./index";
5
- import { unlinkSync, existsSync } from "node:fs";
6
- import { defineRoute, defineRoutes } from "@fragno-dev/core";
7
- import { z } from "zod";
3
+ import { withDatabase } from "@fragno-dev/db";
4
+ import { defineFragment } from "@fragno-dev/core";
5
+ import { instantiate } from "@fragno-dev/core";
6
+ import { buildDatabaseFragmentsTest } from "./db-test";
7
+ import type { ExtractFragmentServices } from "@fragno-dev/core/route";
8
8
 
9
9
  // Test schema with multiple versions
10
10
  const testSchema = schema((s) => {
@@ -22,518 +22,132 @@ const testSchema = schema((s) => {
22
22
  });
23
23
 
24
24
  // Test fragment definition
25
- const testFragmentDef = defineFragmentWithDatabase<{}>("test-fragment")
26
- .withDatabase(testSchema)
27
- .withServices(({ orm }) => {
25
+ const testFragmentDef = defineFragment<{}>("test-fragment")
26
+ .extend(withDatabase(testSchema))
27
+ .providesBaseService(({ deps }) => {
28
28
  return {
29
29
  createUser: async (data: { name: string; email: string; age?: number | null }) => {
30
- const id = await orm.create("users", data);
30
+ const id = await deps.db.create("users", data);
31
31
  return { ...data, id: id.valueOf() };
32
32
  },
33
33
  getUsers: async () => {
34
- const users = await orm.find("users", (b) =>
34
+ const users = await deps.db.find("users", (b) =>
35
35
  b.whereIndex("idx_users_all", (eb) => eb("id", "!=", "")),
36
36
  );
37
37
  return users.map((u) => ({ ...u, id: u.id.valueOf() }));
38
38
  },
39
39
  };
40
- });
41
-
42
- describe("createDatabaseFragmentForTest", () => {
43
- describe("databasePath option", () => {
44
- const testDbPath = "./test-fragno.pglite";
45
-
46
- afterEach(() => {
47
- // Clean up test database files
48
- if (existsSync(testDbPath)) {
49
- try {
50
- unlinkSync(testDbPath);
51
- } catch {
52
- // Ignore cleanup errors
53
- }
54
- }
55
- });
56
-
57
- it("should use in-memory database by default", async () => {
58
- const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
59
- adapter: { type: "kysely-sqlite" },
60
- });
61
-
62
- // Should be able to create and query users
63
- const user = await fragment.services.createUser({
64
- name: "Test User",
65
- email: "test@example.com",
66
- age: 25,
67
- });
68
-
69
- expect(user).toMatchObject({
70
- id: expect.any(String),
71
- name: "Test User",
72
- email: "test@example.com",
73
- age: 25,
74
- });
75
-
76
- const users = await fragment.services.getUsers();
77
- expect(users).toHaveLength(1);
78
- expect(users[0]).toMatchObject(user);
79
- });
80
-
81
- it("should create database at specified path", async () => {
82
- const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
83
- adapter: { type: "kysely-sqlite", databasePath: testDbPath },
84
- });
85
-
86
- // Create a user
87
- await fragment.services.createUser({
88
- name: "Persisted User",
89
- email: "persisted@example.com",
90
- age: 30,
91
- });
92
-
93
- // Verify data exists in this instance
94
- const users = await fragment.services.getUsers();
95
- expect(users).toHaveLength(1);
96
- expect(users[0]).toMatchObject({
97
- name: "Persisted User",
98
- email: "persisted@example.com",
99
- age: 30,
100
- });
101
- });
102
- });
103
-
104
- describe("migrateToVersion option", () => {
105
- it("should migrate to latest version by default", async () => {
106
- const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
107
- adapter: { type: "kysely-sqlite" },
108
- });
109
-
110
- // Should have the 'age' column from version 2
111
- const user = await fragment.services.createUser({
112
- name: "Test User",
113
- email: "test@example.com",
114
- age: 25,
115
- });
116
-
117
- expect(user).toMatchObject({
118
- id: expect.any(String),
119
- name: "Test User",
120
- email: "test@example.com",
121
- age: 25,
122
- });
40
+ })
41
+ .build();
42
+
43
+ describe("buildDatabaseFragmentsTest", () => {
44
+ it("should create and use a database fragment", async () => {
45
+ const { fragments, test } = await buildDatabaseFragmentsTest()
46
+ .withTestAdapter({ type: "kysely-sqlite" })
47
+ .withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
48
+ .build();
49
+
50
+ const fragment = fragments.test;
51
+
52
+ // Should be able to create and query users
53
+ const user = await fragment.services.createUser({
54
+ name: "Test User",
55
+ email: "test@example.com",
56
+ age: 25,
123
57
  });
124
58
 
125
- it("should migrate to specific version when specified", async () => {
126
- // Migrate to version 1 (before 'age' column was added)
127
- const { test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
128
- adapter: { type: "kysely-sqlite" },
129
- migrateToVersion: 1,
130
- });
131
-
132
- // Query the database directly to check schema
133
- // In version 1, we should be able to insert without the age column
134
- const tableName = "users_test-fragment-db";
135
- await test.kysely
136
- .insertInto(tableName)
137
- .values({
138
- id: "test-id-1",
139
- name: "V1 User",
140
- email: "v1@example.com",
141
- })
142
- .execute();
143
-
144
- const result = await test.kysely.selectFrom(tableName).selectAll().execute();
145
-
146
- expect(result).toHaveLength(1);
147
- expect(result[0]).toMatchObject({
148
- id: "test-id-1",
149
- name: "V1 User",
150
- email: "v1@example.com",
151
- });
152
- // In version 1, the age column should not exist
153
- expect(result[0]).not.toHaveProperty("age");
59
+ expect(user).toMatchObject({
60
+ id: expect.any(String),
61
+ name: "Test User",
62
+ email: "test@example.com",
63
+ age: 25,
154
64
  });
155
65
 
156
- it("should allow creating user with age when migrated to version 2", async () => {
157
- // Explicitly migrate to version 2
158
- const { fragment, test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
159
- adapter: { type: "kysely-sqlite" },
160
- migrateToVersion: 2,
161
- });
162
-
163
- // Should be able to use age column
164
- const user = await fragment.services.createUser({
165
- name: "V2 User",
166
- email: "v2@example.com",
167
- age: 35,
168
- });
169
-
170
- expect(user).toMatchObject({
171
- id: expect.any(String),
172
- name: "V2 User",
173
- email: "v2@example.com",
174
- age: 35,
175
- });
176
-
177
- const tableName = "users_test-fragment-db";
178
- const result = await test.kysely.selectFrom(tableName).selectAll().execute();
66
+ const users = await fragment.services.getUsers();
67
+ expect(users).toHaveLength(1);
68
+ expect(users[0]).toMatchObject(user);
179
69
 
180
- expect(result).toHaveLength(1);
181
- expect(result[0]).toMatchObject({
182
- name: "V2 User",
183
- email: "v2@example.com",
184
- age: 35,
185
- });
186
- });
70
+ await test.cleanup();
187
71
  });
188
72
 
189
- describe("combined options", () => {
190
- const testDbPath = "./test-combined.pglite";
191
-
192
- afterEach(() => {
193
- if (existsSync(testDbPath)) {
194
- try {
195
- unlinkSync(testDbPath);
196
- } catch {
197
- // Ignore cleanup errors
198
- }
199
- }
200
- });
201
-
202
- it("should work with both databasePath and migrateToVersion", async () => {
203
- const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
204
- adapter: { type: "kysely-sqlite", databasePath: testDbPath },
205
- migrateToVersion: 2,
206
- });
207
-
208
- // Create user at version 2 (with age support)
209
- const user = await fragment.services.createUser({
210
- name: "Combined Test",
211
- email: "combined@example.com",
212
- age: 40,
213
- });
214
-
215
- expect(user).toMatchObject({
216
- id: expect.any(String),
217
- name: "Combined Test",
218
- email: "combined@example.com",
219
- age: 40,
220
- });
221
-
222
- const users = await fragment.services.getUsers();
223
- expect(users).toHaveLength(1);
224
- expect(users[0]).toMatchObject({
225
- name: "Combined Test",
226
- email: "combined@example.com",
227
- age: 40,
228
- });
229
- });
73
+ it("should throw error for non-database fragment", async () => {
74
+ const nonDbFragmentDef = defineFragment<{}>("non-db-fragment")
75
+ .providesBaseService(() => ({}))
76
+ .build();
77
+
78
+ await expect(
79
+ buildDatabaseFragmentsTest()
80
+ .withTestAdapter({ type: "kysely-sqlite" })
81
+ .withFragment("nonDb", instantiate(nonDbFragmentDef).withConfig({}).withRoutes([]))
82
+ .build(),
83
+ ).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
230
84
  });
231
85
 
232
- describe("fragment initialization", () => {
233
- it("should provide kysely instance", async () => {
234
- const { test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
235
- adapter: { type: "kysely-sqlite" },
236
- });
237
-
238
- expect(test.kysely).toBeDefined();
239
- expect(typeof test.kysely.selectFrom).toBe("function");
240
- });
86
+ it("should reset database by truncating tables", async () => {
87
+ const { fragments, test } = await buildDatabaseFragmentsTest()
88
+ .withTestAdapter({ type: "kysely-sqlite" })
89
+ .withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
90
+ .build();
241
91
 
242
- it("should provide adapter instance", async () => {
243
- const { test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
244
- adapter: { type: "kysely-sqlite" },
245
- });
92
+ const fragment = fragments.test;
246
93
 
247
- expect(test.adapter).toBeDefined();
248
- expect(typeof test.adapter.createMigrationEngine).toBe("function");
94
+ // Create some users
95
+ await fragment.services.createUser({
96
+ name: "User 1",
97
+ email: "user1@example.com",
98
+ age: 25,
249
99
  });
250
100
 
251
- it("should have all standard fragment test properties", async () => {
252
- const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
253
- adapter: { type: "kysely-sqlite" },
254
- });
101
+ // Verify users exist
102
+ let users = await fragment.services.getUsers();
103
+ expect(users).toHaveLength(1);
255
104
 
256
- expect(fragment.services).toBeDefined();
257
- });
105
+ // Reset the database
106
+ await test.resetDatabase();
258
107
 
259
- it("should throw error for non-database fragment", async () => {
260
- // Create a fragment without database
261
- const nonDbFragment = {
262
- definition: {
263
- name: "non-db-fragment",
264
- additionalContext: {},
265
- },
266
- $requiredOptions: {},
267
- };
108
+ // Verify database is empty
109
+ users = await fragment.services.getUsers();
110
+ expect(users).toHaveLength(0);
268
111
 
269
- await expect(
270
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
- createDatabaseFragmentForTest(nonDbFragment as any, [], {
272
- adapter: { type: "kysely-sqlite" },
273
- }),
274
- ).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
275
- });
112
+ // Cleanup
113
+ await test.cleanup();
276
114
  });
277
115
 
278
- describe("route handling with defineRoutes", () => {
279
- it("should handle route factory with multiple routes", async () => {
280
- type Config = {};
281
- type Deps = {};
282
- type Services = {
283
- createUser: (data: { name: string; email: string; age?: number | null }) => Promise<{
284
- name: string;
285
- email: string;
286
- age?: number | null;
287
- id: string;
288
- }>;
289
- getUsers: () => Promise<{ name: string; email: string; age: number | null; id: string }[]>;
290
- };
116
+ it("should expose db property for direct ORM queries", async () => {
117
+ const { fragments, test } = await buildDatabaseFragmentsTest()
118
+ .withTestAdapter({ type: "kysely-sqlite" })
119
+ .withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
120
+ .build();
291
121
 
292
- const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
293
- defineRoute({
294
- method: "POST",
295
- path: "/users",
296
- inputSchema: z.object({
297
- name: z.string(),
298
- email: z.string(),
299
- age: z.number().nullable().optional(),
300
- }),
301
- outputSchema: z.object({
302
- id: z.string(),
303
- name: z.string(),
304
- email: z.string(),
305
- age: z.number().nullable().optional(),
306
- }),
307
- handler: async ({ input }, { json }) => {
308
- if (input) {
309
- const data = await input.valid();
310
- const user = await services.createUser(data);
311
- return json(user);
312
- }
313
- return json({ id: "", name: "", email: "", age: null });
314
- },
315
- }),
316
- defineRoute({
317
- method: "GET",
318
- path: "/users",
319
- outputSchema: z.array(
320
- z.object({
321
- id: z.string(),
322
- name: z.string(),
323
- email: z.string(),
324
- age: z.number().nullable(),
325
- }),
326
- ),
327
- handler: async (_ctx, { json }) => {
328
- const users = await services.getUsers();
329
- return json(users);
330
- },
331
- }),
332
- ]);
122
+ const fragment = fragments.test;
333
123
 
334
- const routes = [routeFactory] as const;
335
- const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, routes, {
336
- adapter: { type: "kysely-sqlite" },
337
- });
338
- // Test creating a user
339
- const createResponse = await fragment.callRoute("POST", "/users", {
340
- body: { name: "John Doe", email: "john@example.com", age: 30 },
341
- });
342
-
343
- expect(createResponse.type).toBe("json");
344
- if (createResponse.type === "json") {
345
- expect(createResponse.data).toMatchObject({
346
- id: expect.any(String),
347
- name: "John Doe",
348
- email: "john@example.com",
349
- age: 30,
350
- });
351
- }
352
-
353
- // Test getting users
354
- const getUsersResponse = await fragment.callRoute("GET", "/users");
355
-
356
- expect(getUsersResponse.type).toBe("json");
357
- if (getUsersResponse.type === "json") {
358
- expect(getUsersResponse.data).toHaveLength(1);
359
- expect(getUsersResponse.data[0]).toMatchObject({
360
- id: expect.any(String),
361
- name: "John Doe",
362
- email: "john@example.com",
363
- age: 30,
364
- });
365
- }
124
+ // Test creating a record directly using test.db
125
+ const userId = await fragment.db.create("users", {
126
+ name: "Direct DB User",
127
+ email: "direct@example.com",
128
+ age: 28,
366
129
  });
367
- });
368
130
 
369
- describe("resetDatabase", () => {
370
- const adapters = [
371
- { name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
372
- { name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
373
- { name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
374
- ];
375
-
376
- for (const { name, adapter } of adapters) {
377
- describe(name, () => {
378
- it("should clear all data and recreate a fresh database", async () => {
379
- // Don't destructure so we can access the updated fragment through getters after reset
380
- const result = await createDatabaseFragmentForTest(testFragmentDef, [], {
381
- adapter,
382
- });
383
-
384
- // Create some users
385
- await result.services.createUser({
386
- name: "User 1",
387
- email: "user1@example.com",
388
- age: 25,
389
- });
390
- await result.services.createUser({
391
- name: "User 2",
392
- email: "user2@example.com",
393
- age: 30,
394
- });
395
-
396
- // Verify users exist
397
- let users = await result.services.getUsers();
398
- expect(users).toHaveLength(2);
399
-
400
- // Reset the database
401
- await result.test.resetDatabase();
402
-
403
- // Verify database is empty (accessing through result to get updated fragment)
404
- users = await result.services.getUsers();
405
- expect(users).toHaveLength(0);
406
-
407
- // Verify we can still create new users after reset
408
- const newUser = await result.services.createUser({
409
- name: "User After Reset",
410
- email: "after@example.com",
411
- age: 35,
412
- });
413
-
414
- expect(newUser).toMatchObject({
415
- id: expect.any(String),
416
- name: "User After Reset",
417
- email: "after@example.com",
418
- age: 35,
419
- });
420
-
421
- users = await result.services.getUsers();
422
- expect(users).toHaveLength(1);
423
- expect(users[0]).toMatchObject(newUser);
424
-
425
- // Cleanup
426
- await result.test.cleanup();
427
- }, 10000);
428
- });
429
- }
430
- });
131
+ expect(userId).toBeDefined();
132
+ expect(typeof userId.valueOf()).toBe("string");
431
133
 
432
- describe("db property access", () => {
433
- const adapters = [
434
- { name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
435
- { name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
436
- { name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
437
- ];
438
-
439
- for (const { name, adapter } of adapters) {
440
- describe(name, () => {
441
- it("should expose db property for direct ORM queries", async () => {
442
- const { test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
443
- adapter,
444
- });
445
-
446
- // Test creating a record directly using test.db
447
- const userId = await test.db.create("users", {
448
- name: "Direct DB User",
449
- email: "direct@example.com",
450
- age: 28,
451
- });
452
-
453
- expect(userId).toBeDefined();
454
- expect(typeof userId.valueOf()).toBe("string");
455
-
456
- // Test finding records using test.db
457
- const users = await test.db.find("users", (b) =>
458
- b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
459
- );
134
+ // Test finding records using test.db
135
+ const users = await fragment.db.find("users", (b) =>
136
+ b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
137
+ );
460
138
 
461
- expect(users).toHaveLength(1);
462
- expect(users[0]).toMatchObject({
463
- id: userId,
464
- name: "Direct DB User",
465
- email: "direct@example.com",
466
- age: 28,
467
- });
468
-
469
- // Test findFirst using test.db
470
- const user = await test.db.findFirst("users", (b) =>
471
- b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
472
- );
139
+ expect(users).toHaveLength(1);
140
+ expect(users[0]).toMatchObject({
141
+ id: userId,
142
+ name: "Direct DB User",
143
+ email: "direct@example.com",
144
+ age: 28,
145
+ });
473
146
 
474
- expect(user).toMatchObject({
475
- id: userId,
476
- name: "Direct DB User",
477
- email: "direct@example.com",
478
- age: 28,
479
- });
480
-
481
- // Cleanup
482
- await test.cleanup();
483
- }, 10000);
484
-
485
- it("should maintain db property after resetDatabase", async () => {
486
- const result = await createDatabaseFragmentForTest(testFragmentDef, [], {
487
- adapter,
488
- });
489
-
490
- // Create initial data using test.db
491
- await result.test.db.create("users", {
492
- name: "Before Reset",
493
- email: "before@example.com",
494
- age: 25,
495
- });
496
-
497
- // Verify db works before reset
498
- expect(result.test.db).toBeDefined();
499
- expect(typeof result.test.db.create).toBe("function");
500
-
501
- // Verify data exists
502
- let users = await result.test.db.find("users");
503
- expect(users).toHaveLength(1);
504
-
505
- // Reset database
506
- await result.test.resetDatabase();
507
-
508
- // Verify database was actually reset (no data)
509
- users = await result.test.db.find("users");
510
- expect(users).toHaveLength(0);
511
-
512
- // Verify we can still use the ORM after reset
513
- const newUserId = await result.test.db.create("users", {
514
- name: "After Reset",
515
- email: "after@example.com",
516
- age: 30,
517
- });
518
-
519
- expect(newUserId).toBeDefined();
520
-
521
- const newUsers = await result.test.db.find("users");
522
- expect(newUsers).toHaveLength(1);
523
- expect(newUsers[0]).toMatchObject({
524
- name: "After Reset",
525
- email: "after@example.com",
526
- age: 30,
527
- });
528
-
529
- // Cleanup
530
- await result.test.cleanup();
531
- }, 10000);
532
- });
533
- }
147
+ await test.cleanup();
534
148
  });
535
149
 
536
- describe("multiple adapters with auth-like schema", () => {
150
+ it("should work with multi-table schema", async () => {
537
151
  // Simplified auth schema for testing
538
152
  const authSchema = schema((s) => {
539
153
  return s
@@ -553,84 +167,211 @@ describe("createDatabaseFragmentForTest", () => {
553
167
  });
554
168
  });
555
169
 
556
- const authFragmentDef = defineFragmentWithDatabase<{}>("auth-test")
557
- .withDatabase(authSchema)
558
- .withServices(({ orm }) => {
170
+ const authFragmentDef = defineFragment<{}>("auth-test")
171
+ .extend(withDatabase(authSchema))
172
+ .providesBaseService(({ deps }) => {
559
173
  return {
560
174
  createUser: async (email: string, passwordHash: string) => {
561
- const id = await orm.create("user", { email, passwordHash });
175
+ const id = await deps.db.create("user", { email, passwordHash });
562
176
  return { id: id.valueOf(), email, passwordHash };
563
177
  },
564
178
  createSession: async (userId: string) => {
565
179
  const expiresAt = new Date();
566
180
  expiresAt.setDate(expiresAt.getDate() + 30);
567
- const id = await orm.create("session", { userId, expiresAt });
181
+ const id = await deps.db.create("session", { userId, expiresAt });
568
182
  return { id: id.valueOf(), userId, expiresAt };
569
183
  },
570
- getUserByEmail: async (email: string) => {
571
- const user = await orm.findFirst("user", (b) =>
572
- b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)),
573
- );
574
- if (!user) {
575
- return null;
576
- }
577
- return { id: user.id.valueOf(), email: user.email, passwordHash: user.passwordHash };
578
- },
579
184
  };
185
+ })
186
+ .build();
187
+
188
+ const { fragments, test } = await buildDatabaseFragmentsTest()
189
+ .withTestAdapter({ type: "kysely-sqlite" })
190
+ .withFragment("auth", instantiate(authFragmentDef).withConfig({}).withRoutes([]))
191
+ .build();
192
+
193
+ const fragment = fragments.auth;
194
+
195
+ // Create a user
196
+ const user = await fragment.services.createUser("test@test.com", "hashed-password");
197
+ expect(user).toMatchObject({
198
+ id: expect.any(String),
199
+ email: "test@test.com",
200
+ passwordHash: "hashed-password",
201
+ });
202
+
203
+ // Create a session for the user
204
+ const session = await fragment.services.createSession(user.id);
205
+ expect(session).toMatchObject({
206
+ id: expect.any(String),
207
+ userId: user.id,
208
+ expiresAt: expect.any(Date),
209
+ });
210
+
211
+ await test.cleanup();
212
+ });
213
+ });
214
+
215
+ describe("multi-fragment tests", () => {
216
+ // Create two different schemas
217
+ const userSchema = schema((s) => {
218
+ return s.addTable("user", (t) => {
219
+ return t
220
+ .addColumn("id", idColumn())
221
+ .addColumn("name", column("string"))
222
+ .addColumn("email", column("string"))
223
+ .createIndex("idx_user_all", ["id"]);
224
+ });
225
+ });
226
+
227
+ const postSchema = schema((s) => {
228
+ return s.addTable("post", (t) => {
229
+ return t
230
+ .addColumn("id", idColumn())
231
+ .addColumn("title", column("string"))
232
+ .addColumn("userId", column("string"))
233
+ .createIndex("idx_post_all", ["id"]);
234
+ });
235
+ });
236
+
237
+ const userFragmentDef = defineFragment<{}>("user-fragment")
238
+ .extend(withDatabase(userSchema))
239
+ .providesBaseService(({ deps }) => {
240
+ return {
241
+ createUser: async (data: { name: string; email: string }) => {
242
+ const id = await deps.db.create("user", data);
243
+ return { ...data, id: id.valueOf() };
244
+ },
245
+ getUsers: async () => {
246
+ const users = await deps.db.find("user", (b) =>
247
+ b.whereIndex("idx_user_all", (eb) => eb("id", "!=", "")),
248
+ );
249
+ return users.map((u) => ({ ...u, id: u.id.valueOf() }));
250
+ },
251
+ };
252
+ })
253
+ .build();
254
+
255
+ const postFragmentDef = defineFragment<{}>("post-fragment")
256
+ .extend(withDatabase(postSchema))
257
+ .providesBaseService(({ deps }) => {
258
+ return {
259
+ createPost: async (data: { title: string; userId: string }) => {
260
+ const id = await deps.db.create("post", data);
261
+ return { ...data, id: id.valueOf() };
262
+ },
263
+ getPosts: async () => {
264
+ const posts = await deps.db.find("post", (b) =>
265
+ b.whereIndex("idx_post_all", (eb) => eb("id", "!=", "")),
266
+ );
267
+ return posts.map((p) => ({ ...p, id: p.id.valueOf() }));
268
+ },
269
+ };
270
+ })
271
+ .build();
272
+
273
+ const adapters = [
274
+ { name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
275
+ { name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
276
+ { name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
277
+ ];
278
+
279
+ for (const { name, adapter } of adapters) {
280
+ it(`should allow multiple fragments to share the same database adapter - ${name}`, async () => {
281
+ // Create both fragments with shared adapter
282
+ const { fragments, test } = await buildDatabaseFragmentsTest()
283
+ .withTestAdapter(adapter)
284
+ .withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
285
+ .withFragment("post", instantiate(postFragmentDef).withConfig({}).withRoutes([]))
286
+ .build();
287
+
288
+ // Create a user
289
+ const user = await fragments.user.services.createUser({
290
+ name: "John Doe",
291
+ email: "john@example.com",
292
+ });
293
+
294
+ expect(user).toMatchObject({
295
+ id: expect.any(String),
296
+ name: "John Doe",
297
+ email: "john@example.com",
580
298
  });
581
299
 
582
- const adapters = [
583
- { name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
584
- { name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
585
- { name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
586
- ];
587
-
588
- for (const { name, adapter } of adapters) {
589
- describe(name, () => {
590
- it("should create user and session", async () => {
591
- const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, [], {
592
- adapter,
593
- });
594
-
595
- // Create a user
596
- const user = await fragment.services.createUser("test@test.com", "hashed-password");
597
- expect(user).toMatchObject({
598
- id: expect.any(String),
599
- email: "test@test.com",
600
- passwordHash: "hashed-password",
601
- });
602
-
603
- // Create a session for the user
604
- const session = await fragment.services.createSession(user.id);
605
- expect(session).toMatchObject({
606
- id: expect.any(String),
607
- userId: user.id,
608
- expiresAt: expect.any(Date),
609
- });
610
-
611
- // Find user by email
612
- const foundUser = await fragment.services.getUserByEmail("test@test.com");
613
- expect(foundUser).toMatchObject({
614
- id: user.id,
615
- email: "test@test.com",
616
- passwordHash: "hashed-password",
617
- });
618
-
619
- // Cleanup
620
- await test.cleanup();
621
- }, 10000);
622
-
623
- it("should return null when user not found", async () => {
624
- const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, [], {
625
- adapter,
626
- });
627
-
628
- const notFound = await fragment.services.getUserByEmail("nonexistent@test.com");
629
- expect(notFound).toBeNull();
630
-
631
- await test.cleanup();
632
- }, 10000);
300
+ // Create a post with the user's ID
301
+ const post = await fragments.post.services.createPost({
302
+ title: "My First Post",
303
+ userId: user.id,
633
304
  });
305
+
306
+ expect(post).toMatchObject({
307
+ id: expect.any(String),
308
+ title: "My First Post",
309
+ userId: user.id,
310
+ });
311
+
312
+ // Verify data exists
313
+ const users = await fragments.user.services.getUsers();
314
+ expect(users).toHaveLength(1);
315
+
316
+ const posts = await fragments.post.services.getPosts();
317
+ expect(posts).toHaveLength(1);
318
+ expect(posts[0]!.userId).toBe(user.id);
319
+
320
+ // Cleanup (centralized - cleans up all fragments)
321
+ await test.cleanup();
322
+ }, 10000);
323
+ }
324
+ });
325
+
326
+ describe("ExtractFragmentServices", () => {
327
+ it("extracts provided services from database fragment with new API", () => {
328
+ const testSchema = schema((s) => s);
329
+
330
+ interface ITestService {
331
+ doSomething: (input: string) => Promise<string>;
332
+ doSomethingElse: (input: number) => Promise<number>;
634
333
  }
334
+
335
+ const fragment = defineFragment<{}>("test-db-fragment")
336
+ .extend(withDatabase(testSchema))
337
+ .providesService(
338
+ "test",
339
+ (): ITestService => ({
340
+ doSomething: async (input: string) => input.toUpperCase(),
341
+ doSomethingElse: async (input: number) => input * 2,
342
+ }),
343
+ )
344
+ .build();
345
+
346
+ type Services = ExtractFragmentServices<typeof fragment>;
347
+
348
+ // Should include the provided service
349
+ expectTypeOf<Services>().toMatchObjectType<{
350
+ test: ITestService;
351
+ }>();
352
+ });
353
+
354
+ it("merges base services and provided services in database fragment", () => {
355
+ const testSchema = schema((s) => s);
356
+
357
+ const fragment = defineFragment<{}>("test-db-fragment")
358
+ .extend(withDatabase(testSchema))
359
+ .providesBaseService(() => ({
360
+ internalService: async () => "internal",
361
+ }))
362
+ .providesService("externalService", () => ({
363
+ publicMethod: async () => "public",
364
+ }))
365
+ .build();
366
+
367
+ type Services = ExtractFragmentServices<typeof fragment>;
368
+
369
+ // Should include both base services and provided services
370
+ expectTypeOf<Services>().toMatchObjectType<{
371
+ internalService: () => Promise<string>;
372
+ externalService: {
373
+ publicMethod: () => Promise<string>;
374
+ };
375
+ }>();
635
376
  });
636
377
  });