@fragno-dev/db 0.1.13 → 0.1.14

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.
Files changed (75) hide show
  1. package/.turbo/turbo-build.log +48 -41
  2. package/CHANGELOG.md +6 -0
  3. package/dist/adapters/adapters.d.ts +13 -1
  4. package/dist/adapters/adapters.d.ts.map +1 -1
  5. package/dist/adapters/adapters.js.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +2 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +6 -1
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +6 -4
  11. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  12. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
  13. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.js +49 -36
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-decoder.js +1 -1
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  18. package/dist/adapters/drizzle/shared.d.ts +14 -1
  19. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  20. package/dist/adapters/kysely/kysely-adapter.d.ts +2 -0
  21. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  22. package/dist/adapters/kysely/kysely-adapter.js +7 -2
  23. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  24. package/dist/adapters/kysely/kysely-query.js +5 -3
  25. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  26. package/dist/adapters/kysely/kysely-shared.d.ts +11 -0
  27. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  28. package/dist/adapters/kysely/kysely-uow-compiler.js +38 -9
  29. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  30. package/dist/bind-services.d.ts +7 -0
  31. package/dist/bind-services.d.ts.map +1 -0
  32. package/dist/bind-services.js +14 -0
  33. package/dist/bind-services.js.map +1 -0
  34. package/dist/fragment.d.ts +131 -12
  35. package/dist/fragment.d.ts.map +1 -1
  36. package/dist/fragment.js +107 -8
  37. package/dist/fragment.js.map +1 -1
  38. package/dist/mod.d.ts +4 -2
  39. package/dist/mod.d.ts.map +1 -1
  40. package/dist/mod.js +3 -2
  41. package/dist/mod.js.map +1 -1
  42. package/dist/query/query.d.ts +2 -2
  43. package/dist/query/query.d.ts.map +1 -1
  44. package/dist/query/unit-of-work.d.ts +100 -15
  45. package/dist/query/unit-of-work.d.ts.map +1 -1
  46. package/dist/query/unit-of-work.js +214 -7
  47. package/dist/query/unit-of-work.js.map +1 -1
  48. package/package.json +3 -3
  49. package/src/adapters/adapters.ts +14 -0
  50. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +6 -1
  51. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +133 -5
  52. package/src/adapters/drizzle/drizzle-adapter.ts +16 -1
  53. package/src/adapters/drizzle/drizzle-query.ts +26 -15
  54. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +57 -57
  55. package/src/adapters/drizzle/drizzle-uow-compiler.ts +79 -39
  56. package/src/adapters/drizzle/drizzle-uow-decoder.ts +2 -5
  57. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +2 -2
  58. package/src/adapters/kysely/kysely-adapter.ts +16 -1
  59. package/src/adapters/kysely/kysely-query.ts +26 -15
  60. package/src/adapters/kysely/kysely-uow-compiler.test.ts +43 -43
  61. package/src/adapters/kysely/kysely-uow-compiler.ts +50 -14
  62. package/src/adapters/kysely/kysely-uow-joins.test.ts +30 -30
  63. package/src/bind-services.test.ts +214 -0
  64. package/src/bind-services.ts +37 -0
  65. package/src/db-fragment.test.ts +800 -0
  66. package/src/fragment.ts +557 -28
  67. package/src/mod.ts +19 -0
  68. package/src/query/query.ts +2 -2
  69. package/src/query/unit-of-work-multi-schema.test.ts +64 -0
  70. package/src/query/unit-of-work-types.test.ts +13 -0
  71. package/src/query/unit-of-work.test.ts +5 -9
  72. package/src/query/unit-of-work.ts +511 -62
  73. package/src/uow-context-integration.test.ts +102 -0
  74. package/src/uow-context.test.ts +182 -0
  75. package/src/fragment.test.ts +0 -341
@@ -0,0 +1,800 @@
1
+ import { test, expect, describe, expectTypeOf } from "vitest";
2
+ import { defineFragmentWithDatabase, type FragnoPublicConfigWithDatabase } from "./fragment";
3
+ import {
4
+ createFragment,
5
+ instantiateFragment,
6
+ type FragnoPublicClientConfig,
7
+ defineRoute,
8
+ defineRoutes,
9
+ } from "@fragno-dev/core";
10
+ import { createClientBuilder } from "@fragno-dev/core/client";
11
+ import { schema, idColumn, column } from "./schema/create";
12
+ import type { AbstractQuery } from "./query/query";
13
+ import type { DatabaseAdapter } from "./mod";
14
+ import type { DatabaseRequestThisContext } from "./fragment";
15
+ import { z } from "zod";
16
+ import {
17
+ fragnoDatabaseAdapterNameFakeSymbol,
18
+ fragnoDatabaseAdapterVersionFakeSymbol,
19
+ } from "./adapters/adapters";
20
+
21
+ type Empty = Record<never, never>;
22
+
23
+ const mockDatabaseAdapter: DatabaseAdapter = {
24
+ [fragnoDatabaseAdapterNameFakeSymbol]: "mock",
25
+ [fragnoDatabaseAdapterVersionFakeSymbol]: 0,
26
+ close: () => Promise.resolve(),
27
+ createQueryEngine: () => {
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ return {} as any;
30
+ },
31
+ getSchemaVersion: () => Promise.resolve("0"),
32
+ createMigrationEngine: () => {
33
+ throw new Error("Not implemented");
34
+ },
35
+ createSchemaGenerator: () => {
36
+ throw new Error("Not implemented");
37
+ },
38
+ createTableNameMapper: (namespace: string) => ({
39
+ toPhysical: (logicalName: string) => `${logicalName}_${namespace}`,
40
+ toLogical: (physicalName: string) => physicalName.replace(`_${namespace}`, ""),
41
+ }),
42
+ isConnectionHealthy: () => Promise.resolve(true),
43
+ };
44
+
45
+ describe("DatabaseFragmentBuilder", () => {
46
+ describe("Type inference", () => {
47
+ test("defineFragmentWithDatabase infers schema type from withDatabase", () => {
48
+ const _testSchema = schema((s) =>
49
+ s.addTable("users", (t) =>
50
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
51
+ ),
52
+ );
53
+
54
+ const _fragment = defineFragmentWithDatabase("test").withDatabase(_testSchema);
55
+
56
+ // Type check that withDatabase returns a builder with the schema
57
+ expectTypeOf(_fragment.definition.name).toEqualTypeOf<string>();
58
+ });
59
+
60
+ test("withDatabase correctly transforms schema type", () => {
61
+ const _testSchema1 = schema((s) =>
62
+ s.addTable("users", (t) =>
63
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
64
+ ),
65
+ );
66
+
67
+ const _testSchema2 = schema((s) =>
68
+ s.addTable("posts", (t) =>
69
+ t.addColumn("id", idColumn()).addColumn("title", column("string")),
70
+ ),
71
+ );
72
+
73
+ const fragment1 = defineFragmentWithDatabase("test").withDatabase(_testSchema1);
74
+ const fragment2 = fragment1.withDatabase(_testSchema2);
75
+
76
+ // Type check that we can chain withDatabase
77
+ expectTypeOf(fragment1.definition.name).toEqualTypeOf<string>();
78
+ expectTypeOf(fragment2.definition.name).toEqualTypeOf<string>();
79
+ });
80
+
81
+ test("withDependencies has access to config, fragnoConfig, and orm", () => {
82
+ const _testSchema = schema((s) =>
83
+ s.addTable("users", (t) =>
84
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
85
+ ),
86
+ );
87
+
88
+ const _fragment = defineFragmentWithDatabase("test")
89
+ .withDatabase(_testSchema)
90
+ .withDependencies(({ fragnoConfig, orm }) => {
91
+ expectTypeOf(fragnoConfig).toEqualTypeOf<{ mountRoute?: string }>();
92
+ expectTypeOf(orm).toEqualTypeOf<AbstractQuery<typeof _testSchema>>();
93
+
94
+ return {
95
+ userService: {
96
+ getUser: async (id: string) => ({ id, name: "Test" }),
97
+ },
98
+ };
99
+ });
100
+
101
+ // Type check that the fragment has the expected structure
102
+ expectTypeOf(_fragment.definition.name).toEqualTypeOf<string>();
103
+ });
104
+
105
+ test("providesService has access to config, fragnoConfig, deps, and db", () => {
106
+ const _testSchema = schema((s) =>
107
+ s.addTable("users", (t) =>
108
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
109
+ ),
110
+ );
111
+
112
+ const _fragment = defineFragmentWithDatabase("test")
113
+ .withDatabase(_testSchema)
114
+ .withDependencies(({ orm }) => ({
115
+ userRepo: {
116
+ create: (name: string) => orm.create("users", { name }),
117
+ },
118
+ }))
119
+ .providesService(({ fragnoConfig, deps, db }) => {
120
+ expectTypeOf(fragnoConfig).toEqualTypeOf<{ mountRoute?: string }>();
121
+ expectTypeOf(deps).toEqualTypeOf<{
122
+ userRepo: {
123
+ create: (name: string) => ReturnType<AbstractQuery<typeof _testSchema>["create"]>;
124
+ };
125
+ }>();
126
+ expectTypeOf(db).toEqualTypeOf<AbstractQuery<typeof _testSchema>>();
127
+
128
+ return {
129
+ cacheService: {
130
+ get: (_key: string) => "cached",
131
+ },
132
+ };
133
+ });
134
+
135
+ // Type check that the fragment has the expected structure
136
+ expectTypeOf(_fragment.definition.name).toEqualTypeOf<string>();
137
+ });
138
+ });
139
+
140
+ describe("Builder pattern", () => {
141
+ test("Builder methods return new instances", () => {
142
+ const _testSchema1 = schema((s) =>
143
+ s.addTable("users", (t) =>
144
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
145
+ ),
146
+ );
147
+
148
+ const _testSchema2 = schema((s) =>
149
+ s.addTable("posts", (t) =>
150
+ t.addColumn("id", idColumn()).addColumn("title", column("string")),
151
+ ),
152
+ );
153
+
154
+ const builder1 = defineFragmentWithDatabase("test");
155
+ const builder2 = builder1.withDatabase(_testSchema1);
156
+ const builder3 = builder2.withDatabase(_testSchema2);
157
+ const builder4 = builder3.withDependencies(() => ({ dep1: "value1" }));
158
+ const builder5 = builder4.providesService(({ defineService }) =>
159
+ defineService({ service1: "value1" }),
160
+ );
161
+
162
+ expect(builder1).not.toBe(builder2);
163
+ expect(builder2).not.toBe(builder3);
164
+ expect(builder3).not.toBe(builder4);
165
+ expect(builder4).not.toBe(builder5);
166
+ });
167
+
168
+ test("Each builder step preserves previous configuration", () => {
169
+ const _testSchema = schema((s) =>
170
+ s.addTable("users", (t) =>
171
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
172
+ ),
173
+ );
174
+
175
+ const fragment = defineFragmentWithDatabase("my-db-lib")
176
+ .withDatabase(_testSchema)
177
+ .withDependencies(({ orm }) => ({
178
+ client: "test client",
179
+ orm,
180
+ }))
181
+ .providesService(({ deps, defineService }) =>
182
+ defineService({
183
+ service: `Service using ${deps.client}`,
184
+ }),
185
+ );
186
+
187
+ expect(fragment.definition.name).toBe("my-db-lib");
188
+ expect(fragment.definition.dependencies).toBeDefined();
189
+ expect(fragment.definition.services).toBeDefined();
190
+ });
191
+ });
192
+
193
+ describe("Fragment instantiation", () => {
194
+ test("createFragment works with database adapter", async () => {
195
+ const testSchema = schema((s) =>
196
+ s.addTable("users", (t) =>
197
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
198
+ ),
199
+ );
200
+
201
+ const fragmentDef = defineFragmentWithDatabase("test-db")
202
+ .withDatabase(testSchema)
203
+ .withDependencies(({ orm }) => ({
204
+ userService: {
205
+ createUser: (name: string) => orm.create("users", { name }),
206
+ },
207
+ }))
208
+ .providesService(({ defineService }) =>
209
+ defineService({
210
+ logger: { log: (s: string) => console.log(s) },
211
+ }),
212
+ );
213
+
214
+ const options: FragnoPublicConfigWithDatabase = {
215
+ databaseAdapter: mockDatabaseAdapter,
216
+ };
217
+
218
+ const fragment = createFragment(fragmentDef, {}, [], options);
219
+
220
+ expect(fragment.config.name).toBe("test-db");
221
+ expect(fragment.deps).toHaveProperty("userService");
222
+ expect(fragment.services).toHaveProperty("logger");
223
+ });
224
+
225
+ test("throws error when database adapter is missing from dependencies", () => {
226
+ const testSchema = schema((s) =>
227
+ s.addTable("users", (t) =>
228
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
229
+ ),
230
+ );
231
+
232
+ const fragmentDef = defineFragmentWithDatabase("test-db")
233
+ .withDatabase(testSchema)
234
+ .withDependencies(() => ({
235
+ service: "test",
236
+ }));
237
+
238
+ expect(() => {
239
+ // @ts-expect-error - Test case
240
+ createFragment(fragmentDef, {}, [], {});
241
+ }).toThrow(/requires a database adapter/);
242
+ });
243
+
244
+ test("throws error when database adapter is missing from services", () => {
245
+ const testSchema = schema((s) =>
246
+ s.addTable("users", (t) =>
247
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
248
+ ),
249
+ );
250
+
251
+ const fragmentDef = defineFragmentWithDatabase("test-db")
252
+ .withDatabase(testSchema)
253
+ .withDependencies(() => ({
254
+ service: "test",
255
+ }))
256
+ .providesService(({ defineService }) =>
257
+ defineService({
258
+ serviceValue: "test",
259
+ }),
260
+ );
261
+
262
+ const options: FragnoPublicConfigWithDatabase = {
263
+ databaseAdapter: mockDatabaseAdapter,
264
+ };
265
+
266
+ // Services are called after dependencies, so this should work
267
+ const fragment = createFragment(fragmentDef, {}, [], options);
268
+ expect(fragment.services).toHaveProperty("serviceValue");
269
+ });
270
+
271
+ test("throws error when schema is not provided via withDatabase", () => {
272
+ const fragmentDef = defineFragmentWithDatabase<Empty>("test-db").withDependencies(() => ({
273
+ service: "test",
274
+ }));
275
+
276
+ const options: FragnoPublicConfigWithDatabase = {
277
+ databaseAdapter: mockDatabaseAdapter,
278
+ };
279
+
280
+ expect(() => {
281
+ createFragment(fragmentDef, {}, [], options);
282
+ }).toThrow(/requires a schema/);
283
+ });
284
+
285
+ test("orm is accessible in both dependencies and services", async () => {
286
+ const testSchema = schema((s) =>
287
+ s.addTable("users", (t) =>
288
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
289
+ ),
290
+ );
291
+
292
+ let depsOrm: AbstractQuery<typeof testSchema> | undefined;
293
+ let servicesOrm: AbstractQuery<typeof testSchema> | undefined;
294
+
295
+ const fragmentDef = defineFragmentWithDatabase("test-db")
296
+ .withDatabase(testSchema)
297
+ .withDependencies(({ orm }) => {
298
+ depsOrm = orm;
299
+ return { dep: "value" };
300
+ })
301
+ .providesService(({ db }) => {
302
+ servicesOrm = db;
303
+ return { service: "value" };
304
+ });
305
+
306
+ const options: FragnoPublicConfigWithDatabase = {
307
+ databaseAdapter: mockDatabaseAdapter,
308
+ };
309
+
310
+ createFragment(fragmentDef, {}, [], options);
311
+
312
+ expect(depsOrm).toBeDefined();
313
+ expect(servicesOrm).toBeDefined();
314
+ });
315
+ });
316
+
317
+ describe("Client builder integration", () => {
318
+ test("createClientBuilder works with database fragment", () => {
319
+ const testSchema = schema((s) =>
320
+ s.addTable("users", (t) =>
321
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
322
+ ),
323
+ );
324
+
325
+ const fragmentDef = defineFragmentWithDatabase("test-db")
326
+ .withDatabase(testSchema)
327
+ .providesService(({ db }) => {
328
+ return {
329
+ getUserById: (id: string) =>
330
+ db.findFirst("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", id))),
331
+ };
332
+ });
333
+
334
+ const routes = [
335
+ defineRoute({
336
+ method: "GET",
337
+ path: "/users",
338
+ outputSchema: z.array(
339
+ z.object({
340
+ id: z.string(),
341
+ name: z.string(),
342
+ }),
343
+ ),
344
+ handler: async (_ctx, { json }) => json([]),
345
+ }),
346
+ ] as const;
347
+
348
+ const clientConfig: FragnoPublicClientConfig = {
349
+ baseUrl: "http://localhost:3000",
350
+ };
351
+
352
+ const builder = createClientBuilder(fragmentDef, clientConfig, routes);
353
+
354
+ expect(builder).toBeDefined();
355
+ expectTypeOf(builder.createHook).toBeFunction();
356
+
357
+ const useUsers = builder.createHook("/users");
358
+ expect(useUsers).toHaveProperty("route");
359
+ expect(useUsers.route.path).toBe("/users");
360
+ });
361
+ });
362
+
363
+ describe("Route handler this context", () => {
364
+ test("this context has database functionality for database fragments", () => {
365
+ const testSchema = schema((s) =>
366
+ s.addTable("users", (t) =>
367
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
368
+ ),
369
+ );
370
+
371
+ const fragmentDef = defineFragmentWithDatabase("test-db").withDatabase(testSchema);
372
+
373
+ // Use defineRoutes with the fragment builder to get proper this typing
374
+ const routesFactory = defineRoutes(fragmentDef).create(({ defineRoute }) => {
375
+ return [
376
+ defineRoute({
377
+ method: "GET",
378
+ path: "/test",
379
+ handler: async function (_, { json }) {
380
+ // Type check that this has getUnitOfWork method
381
+ expectTypeOf(this).toHaveProperty("getUnitOfWork");
382
+ expectTypeOf(this.getUnitOfWork).toBeFunction();
383
+ return json({ ok: true });
384
+ },
385
+ }),
386
+ ];
387
+ });
388
+
389
+ const options: FragnoPublicConfigWithDatabase = {
390
+ databaseAdapter: mockDatabaseAdapter,
391
+ };
392
+
393
+ const fragment = createFragment(fragmentDef, {}, [routesFactory], options);
394
+ expect(fragment).toBeDefined();
395
+ });
396
+
397
+ test("database fragment routes have access to database this context", async () => {
398
+ const testSchema = schema((s) =>
399
+ s.addTable("users", (t) =>
400
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
401
+ ),
402
+ );
403
+
404
+ const fragmentDef = defineFragmentWithDatabase("test-db").withDatabase(testSchema);
405
+
406
+ // Use defineRoutes with fragment builder reference for proper this typing
407
+ const routesFactory = defineRoutes(fragmentDef).create(({ defineRoute }) => {
408
+ return [
409
+ defineRoute({
410
+ method: "GET",
411
+ path: "/test-uow",
412
+ outputSchema: z.object({ result: z.string() }),
413
+ handler: async function (_, { json }) {
414
+ // The type system ensures 'this' is DatabaseRequestThisContext
415
+ // which has getUnitOfWork method available
416
+ expectTypeOf(this).toHaveProperty("getUnitOfWork");
417
+ expectTypeOf(this.getUnitOfWork).toBeFunction();
418
+ return json({ result: "ok" });
419
+ },
420
+ }),
421
+ ];
422
+ });
423
+
424
+ const options: FragnoPublicConfigWithDatabase = {
425
+ databaseAdapter: mockDatabaseAdapter,
426
+ };
427
+
428
+ const fragment = createFragment(fragmentDef, {}, [routesFactory], options);
429
+
430
+ // Verify the fragment was created successfully
431
+ expect(fragment).toBeDefined();
432
+ expect(fragment.config.name).toBe("test-db");
433
+ });
434
+ });
435
+
436
+ describe("providesService with this context", () => {
437
+ test("providesService functions have access to DatabaseRequestThisContext", () => {
438
+ const testSchema = schema((s) =>
439
+ s.addTable("users", (t) =>
440
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
441
+ ),
442
+ );
443
+
444
+ // Define a service with a function that uses 'this'
445
+ const userService = {
446
+ getCurrentUser: function (this: DatabaseRequestThisContext, userId: string) {
447
+ // Type check that this has getUnitOfWork method
448
+ expectTypeOf(this).toHaveProperty("getUnitOfWork");
449
+ expectTypeOf(this.getUnitOfWork).toBeFunction();
450
+ return { id: userId, name: "Test User" };
451
+ },
452
+ };
453
+
454
+ // This should compile without errors because the function has the correct this type
455
+ const fragmentDef = defineFragmentWithDatabase("test-service")
456
+ .withDatabase(testSchema)
457
+ .providesService("userService", ({ defineService }) => defineService(userService));
458
+
459
+ expect(fragmentDef).toBeDefined();
460
+ expect(fragmentDef.definition.providedServices).toBeDefined();
461
+ expect(typeof fragmentDef.definition.providedServices).toBe("object");
462
+ });
463
+
464
+ test("providesService binds services correctly", async () => {
465
+ const testSchema = schema((s) =>
466
+ s.addTable("users", (t) =>
467
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
468
+ ),
469
+ );
470
+
471
+ const userService = {
472
+ testMethod: function (this: DatabaseRequestThisContext) {
473
+ // At runtime, this should have access to getUnitOfWork
474
+ const uow = this.getUnitOfWork();
475
+ return { hasUow: !!uow };
476
+ },
477
+ };
478
+
479
+ const fragmentDef = defineFragmentWithDatabase("test-service-runtime")
480
+ .withDatabase(testSchema)
481
+ .providesService("userService", ({ defineService }) => defineService(userService));
482
+
483
+ // Verify the definition has the provided service (now it's an object with factory functions)
484
+ expect(fragmentDef.definition.providedServices).toBeDefined();
485
+ expect(typeof fragmentDef.definition.providedServices).toBe("object");
486
+
487
+ // Type checking is the main test here - if the service functions
488
+ // don't have the correct `this` type, TypeScript will error at compile time
489
+ });
490
+
491
+ test("providesService with direct object (no factory)", () => {
492
+ const testSchema = schema((s) =>
493
+ s.addTable("users", (t) =>
494
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
495
+ ),
496
+ );
497
+
498
+ const fragmentDef = defineFragmentWithDatabase("test-direct")
499
+ .withDatabase(testSchema)
500
+ .providesService("helpers", {
501
+ slugify: (text: string) => text.toLowerCase().replace(/\s+/g, "-"),
502
+ capitalize: (text: string) => text.charAt(0).toUpperCase() + text.slice(1),
503
+ });
504
+
505
+ const options: FragnoPublicConfigWithDatabase = {
506
+ databaseAdapter: mockDatabaseAdapter,
507
+ };
508
+
509
+ const fragment = createFragment(fragmentDef, {}, [], options);
510
+
511
+ expect(fragment.services.helpers).toBeDefined();
512
+ expect(fragment.services.helpers.slugify("Hello World")).toBe("hello-world");
513
+ expect(fragment.services.helpers.capitalize("hello")).toBe("Hello");
514
+ });
515
+
516
+ test("providesService with 0-arity factory", () => {
517
+ const testSchema = schema((s) =>
518
+ s.addTable("users", (t) =>
519
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
520
+ ),
521
+ );
522
+
523
+ const fragmentDef = defineFragmentWithDatabase("test-zero-arity")
524
+ .withDatabase(testSchema)
525
+ .providesService("constants", () => ({
526
+ MAX_USERS: 100,
527
+ MIN_PASSWORD_LENGTH: 8,
528
+ }));
529
+
530
+ const options: FragnoPublicConfigWithDatabase = {
531
+ databaseAdapter: mockDatabaseAdapter,
532
+ };
533
+
534
+ const fragment = createFragment(fragmentDef, {}, [], options);
535
+
536
+ expect(fragment.services.constants).toBeDefined();
537
+ expect(fragment.services.constants.MAX_USERS).toBe(100);
538
+ expect(fragment.services.constants.MIN_PASSWORD_LENGTH).toBe(8);
539
+ });
540
+
541
+ test("chaining multiple provided services", () => {
542
+ const testSchema = schema((s) =>
543
+ s.addTable("users", (t) =>
544
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
545
+ ),
546
+ );
547
+
548
+ const fragmentDef = defineFragmentWithDatabase("test-chaining")
549
+ .withDatabase(testSchema)
550
+ .providesService("logger", {
551
+ log: (msg: string) => console.log(msg),
552
+ })
553
+ .providesService("validator", () => ({
554
+ validate: (value: string) => value.length > 0,
555
+ }))
556
+ .providesService("formatter", () => ({
557
+ format: (value: string) => value.trim(),
558
+ }));
559
+
560
+ const options: FragnoPublicConfigWithDatabase = {
561
+ databaseAdapter: mockDatabaseAdapter,
562
+ };
563
+
564
+ const fragment = createFragment(fragmentDef, {}, [], options);
565
+
566
+ expect(fragment.services.logger).toBeDefined();
567
+ expect(fragment.services.validator).toBeDefined();
568
+ expect(fragment.services.formatter).toBeDefined();
569
+ expect(fragment.services.logger.log).toBeDefined();
570
+ expect(fragment.services.validator.validate).toBeDefined();
571
+ expect(fragment.services.formatter.format).toBeDefined();
572
+ });
573
+ });
574
+
575
+ describe("usesService", () => {
576
+ test("should declare required service", () => {
577
+ const testSchema = schema((s) =>
578
+ s.addTable("users", (t) =>
579
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
580
+ ),
581
+ );
582
+
583
+ interface IEmailService {
584
+ sendEmail(to: string, subject: string): Promise<void>;
585
+ }
586
+
587
+ const fragment = defineFragmentWithDatabase("test-uses-service")
588
+ .withDatabase(testSchema)
589
+ .usesService<"email", IEmailService>("email");
590
+
591
+ expect(fragment.definition.usedServices).toBeDefined();
592
+ expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
593
+ });
594
+
595
+ test("should declare optional service", () => {
596
+ const testSchema = schema((s) =>
597
+ s.addTable("users", (t) =>
598
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
599
+ ),
600
+ );
601
+
602
+ interface ILogger {
603
+ log(message: string): void;
604
+ }
605
+
606
+ const fragment = defineFragmentWithDatabase("test-optional-service")
607
+ .withDatabase(testSchema)
608
+ .usesService<"logger", ILogger>("logger", { optional: true });
609
+
610
+ expect(fragment.definition.usedServices).toBeDefined();
611
+ expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: false });
612
+ });
613
+
614
+ test("should throw when required service not provided", () => {
615
+ const testSchema = schema((s) =>
616
+ s.addTable("users", (t) =>
617
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
618
+ ),
619
+ );
620
+
621
+ interface IEmailService {
622
+ sendEmail(to: string, subject: string): Promise<void>;
623
+ }
624
+
625
+ const fragment = defineFragmentWithDatabase("test-required")
626
+ .withDatabase(testSchema)
627
+ .usesService<"email", IEmailService>("email");
628
+
629
+ const options: FragnoPublicConfigWithDatabase = {
630
+ databaseAdapter: mockDatabaseAdapter,
631
+ };
632
+
633
+ expect(() => {
634
+ createFragment(fragment, {}, [], options);
635
+ }).toThrow("Fragment 'test-required' requires service 'email' but it was not provided");
636
+ });
637
+
638
+ test("should not throw when optional service not provided", () => {
639
+ const testSchema = schema((s) =>
640
+ s.addTable("users", (t) =>
641
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
642
+ ),
643
+ );
644
+
645
+ interface ILogger {
646
+ log(message: string): void;
647
+ }
648
+
649
+ const fragment = defineFragmentWithDatabase("test-optional")
650
+ .withDatabase(testSchema)
651
+ .usesService<"logger", ILogger>("logger", { optional: true });
652
+
653
+ const options: FragnoPublicConfigWithDatabase = {
654
+ databaseAdapter: mockDatabaseAdapter,
655
+ };
656
+
657
+ expect(() => {
658
+ createFragment(fragment, {}, [], options);
659
+ }).not.toThrow();
660
+ });
661
+
662
+ test("provided service can access used services", () => {
663
+ const testSchema = schema((s) =>
664
+ s.addTable("users", (t) =>
665
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
666
+ ),
667
+ );
668
+
669
+ interface IEmailService {
670
+ sendEmail(to: string, subject: string): Promise<void>;
671
+ }
672
+
673
+ const emailImpl: IEmailService = {
674
+ sendEmail: async () => {},
675
+ };
676
+
677
+ const fragment = defineFragmentWithDatabase("test-deps")
678
+ .withDatabase(testSchema)
679
+ .usesService<"email", IEmailService>("email")
680
+ .providesService(({ deps }) => ({
681
+ notifyUser: async (userId: string) => {
682
+ await deps.email.sendEmail(userId, "Notification");
683
+ },
684
+ }));
685
+
686
+ const options: FragnoPublicConfigWithDatabase = {
687
+ databaseAdapter: mockDatabaseAdapter,
688
+ };
689
+
690
+ const instance = createFragment(fragment, {}, [], options, { email: emailImpl });
691
+
692
+ expect(instance.services.notifyUser).toBeDefined();
693
+ expect(typeof instance.services.notifyUser).toBe("function");
694
+ });
695
+ });
696
+
697
+ describe("FragmentInstantiationBuilder with database fragments", () => {
698
+ test("works with database fragments using builder API", () => {
699
+ const testSchema = schema((s) =>
700
+ s.addTable("users", (t) =>
701
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
702
+ ),
703
+ );
704
+
705
+ const fragmentDef = defineFragmentWithDatabase("test-builder")
706
+ .withDatabase(testSchema)
707
+ .withDependencies(({ orm }) => ({
708
+ userService: {
709
+ createUser: (name: string) => orm.create("users", { name }),
710
+ },
711
+ }))
712
+ .providesService(({ defineService }) =>
713
+ defineService({
714
+ logger: { log: (s: string) => console.log(s) },
715
+ }),
716
+ );
717
+
718
+ const fragment = instantiateFragment(fragmentDef)
719
+ .withConfig({})
720
+ .withOptions({ databaseAdapter: mockDatabaseAdapter })
721
+ .build();
722
+
723
+ expect(fragment.config.name).toBe("test-builder");
724
+ expect(fragment.deps).toHaveProperty("userService");
725
+ expect(fragment.services).toHaveProperty("logger");
726
+ });
727
+
728
+ test("builder works with routes and database adapter", () => {
729
+ const testSchema = schema((s) =>
730
+ s.addTable("users", (t) =>
731
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
732
+ ),
733
+ );
734
+
735
+ const fragmentDef = defineFragmentWithDatabase("test-routes")
736
+ .withDatabase(testSchema)
737
+ .providesService(({ db }) => ({
738
+ getUserById: (id: string) =>
739
+ db.findFirst("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", id))),
740
+ }));
741
+
742
+ const route = defineRoute({
743
+ method: "GET",
744
+ path: "/users",
745
+ outputSchema: z.array(
746
+ z.object({
747
+ id: z.string(),
748
+ name: z.string(),
749
+ }),
750
+ ),
751
+ handler: async (_ctx, { json }) => json([]),
752
+ });
753
+
754
+ const fragment = instantiateFragment(fragmentDef)
755
+ .withConfig({})
756
+ .withRoutes([route])
757
+ .withOptions({ databaseAdapter: mockDatabaseAdapter })
758
+ .build();
759
+
760
+ expect(fragment.config.name).toBe("test-routes");
761
+ expect(fragment.services).toHaveProperty("getUserById");
762
+ expect(fragment.config.routes).toHaveLength(1);
763
+ expect(fragment.config.routes[0].path).toBe("/users");
764
+ });
765
+
766
+ test("builder works with used services in database fragments", () => {
767
+ const testSchema = schema((s) =>
768
+ s.addTable("users", (t) =>
769
+ t.addColumn("id", idColumn()).addColumn("name", column("string")),
770
+ ),
771
+ );
772
+
773
+ interface IEmailService {
774
+ sendEmail(to: string, subject: string): Promise<void>;
775
+ }
776
+
777
+ const emailImpl: IEmailService = {
778
+ sendEmail: async () => {},
779
+ };
780
+
781
+ const fragmentDef = defineFragmentWithDatabase("test-services")
782
+ .withDatabase(testSchema)
783
+ .usesService<"email", IEmailService>("email")
784
+ .providesService(({ deps }) => ({
785
+ notifyUser: async (userId: string) => {
786
+ await deps.email.sendEmail(userId, "Notification");
787
+ },
788
+ }));
789
+
790
+ const fragment = instantiateFragment(fragmentDef)
791
+ .withConfig({})
792
+ .withOptions({ databaseAdapter: mockDatabaseAdapter })
793
+ .withServices({ email: emailImpl })
794
+ .build();
795
+
796
+ expect(fragment.services.notifyUser).toBeDefined();
797
+ expect(fragment.services.email).toBeDefined();
798
+ });
799
+ });
800
+ });