@fragno-dev/db 0.1.13 → 0.1.15

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 (178) hide show
  1. package/.turbo/turbo-build.log +179 -132
  2. package/CHANGELOG.md +30 -0
  3. package/dist/adapters/adapters.d.ts +27 -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 +5 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +7 -5
  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 +76 -44
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  18. package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
  19. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
  20. package/dist/adapters/drizzle/generate.d.ts +4 -1
  21. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  22. package/dist/adapters/drizzle/generate.js +11 -18
  23. package/dist/adapters/drizzle/generate.js.map +1 -1
  24. package/dist/adapters/drizzle/shared.d.ts +14 -1
  25. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  26. package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
  27. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  28. package/dist/adapters/kysely/kysely-adapter.js +14 -3
  29. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  30. package/dist/adapters/kysely/kysely-query-builder.js +1 -1
  31. package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
  32. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
  33. package/dist/adapters/kysely/kysely-query.d.ts +1 -0
  34. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  35. package/dist/adapters/kysely/kysely-query.js +28 -19
  36. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  37. package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
  38. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  39. package/dist/adapters/kysely/kysely-shared.js +16 -1
  40. package/dist/adapters/kysely/kysely-shared.js.map +1 -1
  41. package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
  42. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  43. package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
  44. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  45. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  46. package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
  47. package/dist/db-fragment-definition-builder.d.ts +152 -0
  48. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  49. package/dist/db-fragment-definition-builder.js +137 -0
  50. package/dist/db-fragment-definition-builder.js.map +1 -0
  51. package/dist/fragments/internal-fragment.d.ts +19 -0
  52. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  53. package/dist/fragments/internal-fragment.js +39 -0
  54. package/dist/fragments/internal-fragment.js.map +1 -0
  55. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  56. package/dist/migration-engine/generation-engine.js +35 -15
  57. package/dist/migration-engine/generation-engine.js.map +1 -1
  58. package/dist/mod.d.ts +8 -18
  59. package/dist/mod.d.ts.map +1 -1
  60. package/dist/mod.js +7 -34
  61. package/dist/mod.js.map +1 -1
  62. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
  63. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
  64. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  65. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  66. package/dist/packages/fragno/dist/api/error.js +48 -0
  67. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  68. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  69. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  70. package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
  71. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  72. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  73. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  74. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  75. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  76. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  77. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  78. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  79. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  80. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  81. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  82. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  83. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  84. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  85. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  86. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  87. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  88. package/dist/packages/fragno/dist/api/route.js +17 -0
  89. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  90. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  91. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  92. package/dist/query/cursor.d.ts +10 -2
  93. package/dist/query/cursor.d.ts.map +1 -1
  94. package/dist/query/cursor.js +11 -4
  95. package/dist/query/cursor.js.map +1 -1
  96. package/dist/query/execute-unit-of-work.d.ts +123 -0
  97. package/dist/query/execute-unit-of-work.d.ts.map +1 -0
  98. package/dist/query/execute-unit-of-work.js +184 -0
  99. package/dist/query/execute-unit-of-work.js.map +1 -0
  100. package/dist/query/query.d.ts +3 -3
  101. package/dist/query/query.d.ts.map +1 -1
  102. package/dist/query/result-transform.js +4 -2
  103. package/dist/query/result-transform.js.map +1 -1
  104. package/dist/query/retry-policy.d.ts +88 -0
  105. package/dist/query/retry-policy.d.ts.map +1 -0
  106. package/dist/query/retry-policy.js +61 -0
  107. package/dist/query/retry-policy.js.map +1 -0
  108. package/dist/query/unit-of-work.d.ts +171 -32
  109. package/dist/query/unit-of-work.d.ts.map +1 -1
  110. package/dist/query/unit-of-work.js +530 -133
  111. package/dist/query/unit-of-work.js.map +1 -1
  112. package/dist/schema/serialize.js +12 -7
  113. package/dist/schema/serialize.js.map +1 -1
  114. package/dist/with-database.d.ts +28 -0
  115. package/dist/with-database.d.ts.map +1 -0
  116. package/dist/with-database.js +34 -0
  117. package/dist/with-database.js.map +1 -0
  118. package/package.json +10 -3
  119. package/src/adapters/adapters.ts +30 -0
  120. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
  121. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
  122. package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
  123. package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
  124. package/src/adapters/drizzle/drizzle-query.ts +25 -15
  125. package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
  126. package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
  127. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
  128. package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
  129. package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
  130. package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
  131. package/src/adapters/drizzle/generate.test.ts +102 -269
  132. package/src/adapters/drizzle/generate.ts +12 -30
  133. package/src/adapters/drizzle/test-utils.ts +36 -5
  134. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
  135. package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
  136. package/src/adapters/kysely/kysely-adapter.ts +25 -2
  137. package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
  138. package/src/adapters/kysely/kysely-query.ts +57 -37
  139. package/src/adapters/kysely/kysely-shared.ts +34 -0
  140. package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
  141. package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
  142. package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
  143. package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
  144. package/src/adapters/kysely/migration/execute-base.ts +1 -1
  145. package/src/db-fragment-definition-builder.test.ts +887 -0
  146. package/src/db-fragment-definition-builder.ts +506 -0
  147. package/src/db-fragment-instantiator.test.ts +467 -0
  148. package/src/db-fragment-integration.test.ts +408 -0
  149. package/src/fragments/internal-fragment.test.ts +160 -0
  150. package/src/fragments/internal-fragment.ts +85 -0
  151. package/src/migration-engine/generation-engine.test.ts +58 -15
  152. package/src/migration-engine/generation-engine.ts +78 -25
  153. package/src/mod.ts +35 -43
  154. package/src/query/cursor.test.ts +119 -0
  155. package/src/query/cursor.ts +17 -4
  156. package/src/query/execute-unit-of-work.test.ts +1310 -0
  157. package/src/query/execute-unit-of-work.ts +463 -0
  158. package/src/query/query.ts +4 -4
  159. package/src/query/result-transform.test.ts +129 -0
  160. package/src/query/result-transform.ts +4 -1
  161. package/src/query/retry-policy.test.ts +217 -0
  162. package/src/query/retry-policy.ts +141 -0
  163. package/src/query/unit-of-work-coordinator.test.ts +833 -0
  164. package/src/query/unit-of-work-types.test.ts +15 -2
  165. package/src/query/unit-of-work.test.ts +878 -200
  166. package/src/query/unit-of-work.ts +963 -321
  167. package/src/schema/serialize.ts +22 -11
  168. package/src/with-database.ts +140 -0
  169. package/tsdown.config.ts +1 -0
  170. package/dist/fragment.d.ts +0 -54
  171. package/dist/fragment.d.ts.map +0 -1
  172. package/dist/fragment.js +0 -92
  173. package/dist/fragment.js.map +0 -1
  174. package/dist/shared/settings-schema.js +0 -36
  175. package/dist/shared/settings-schema.js.map +0 -1
  176. package/src/fragment.test.ts +0 -341
  177. package/src/fragment.ts +0 -198
  178. package/src/shared/settings-schema.ts +0 -61
@@ -18,6 +18,21 @@ describe("generateMigrationsOrSchema - kysely", () => {
18
18
  beforeAll(() => {
19
19
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
20
  db = new Kysely({ dialect: new PostgresDialect({} as any) });
21
+
22
+ // Mock Kysely transaction to prevent actual database connections
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ vi.spyOn(db, "transaction").mockReturnValue({
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ execute: async (callback: any) => {
27
+ // Return a mock transaction executor that returns empty results
28
+ const mockTx = {
29
+ executeQuery: async () => ({ rows: [] }),
30
+ };
31
+ return callback(mockTx);
32
+ },
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ } as any);
35
+
21
36
  adapter = new KyselyAdapter({ db, provider: "postgresql" });
22
37
 
23
38
  // Mock the adapter methods
@@ -50,8 +65,8 @@ describe("generateMigrationsOrSchema - kysely", () => {
50
65
  const results = await generateMigrationsOrSchema([fragnoDb]);
51
66
 
52
67
  expect(results).toHaveLength(2); // Settings + test-db
53
- expect(results[0].namespace).toBe("fragno-db-settings");
54
- expect(results[0].path).toBe("20251024_001_f000_t001_fragno-db-settings.sql");
68
+ expect(results[0].namespace).toBe(""); // Empty namespace for settings table
69
+ expect(results[0].path).toBe("20251024_001_f000_t001_fragno_db_settings.sql");
55
70
  expect(results[0].schema).toContain("create table");
56
71
  expect(results[0].schema).toContain("fragno_db_settings");
57
72
 
@@ -101,8 +116,8 @@ describe("generateMigrationsOrSchema - kysely", () => {
101
116
  const results = await generateMigrationsOrSchema([fragnoDb1, fragnoDb2, fragnoDb3]);
102
117
 
103
118
  expect(results).toHaveLength(4); // Settings + 3 databases
104
- expect(results[0].namespace).toBe("fragno-db-settings");
105
- expect(results[0].path).toBe("20251024_001_f000_t001_fragno-db-settings.sql");
119
+ expect(results[0].namespace).toBe(""); // Empty namespace for settings table
120
+ expect(results[0].path).toBe("20251024_001_f000_t001_fragno_db_settings.sql");
106
121
 
107
122
  expect(results[1].namespace).toBe("apple-db");
108
123
  expect(results[1].path).toBe("20251024_002_f000_t001_apple-db.sql");
@@ -142,7 +157,7 @@ describe("generateMigrationsOrSchema - kysely", () => {
142
157
  // Settings table already at version 1, so no settings migration needed
143
158
  // But fragment migration is still generated
144
159
  expect(results).toHaveLength(2);
145
- expect(results[0].namespace).toBe("fragno-db-settings");
160
+ expect(results[0].namespace).toBe(""); // Empty namespace for settings table
146
161
  expect(results[1].namespace).toBe("test-db");
147
162
  expect(results[1].path).toBe("20251024_002_f000_t002_test-db.sql");
148
163
  expect(results[1].schema).toContain("create table");
@@ -170,7 +185,7 @@ describe("generateMigrationsOrSchema - kysely", () => {
170
185
 
171
186
  // Settings migration is generated, but no fragment migration (since toVersion=0)
172
187
  expect(results).toHaveLength(1);
173
- expect(results[0].namespace).toBe("fragno-db-settings");
188
+ expect(results[0].namespace).toBe(""); // Empty namespace for settings table
174
189
  });
175
190
 
176
191
  it("should throw error when no databases provided", async () => {
@@ -283,6 +298,20 @@ describe("generateMigrationsOrSchema - kysely", () => {
283
298
  it("should include MySQL-specific foreign key checks in generated SQL", async () => {
284
299
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
300
  const mysqlDb = new Kysely({ dialect: new PostgresDialect({} as any) });
301
+
302
+ // Mock Kysely transaction to prevent actual database connections
303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ vi.spyOn(mysqlDb, "transaction").mockReturnValue({
305
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
306
+ execute: async (callback: any) => {
307
+ const mockTx = {
308
+ executeQuery: async () => ({ rows: [] }),
309
+ };
310
+ return callback(mockTx);
311
+ },
312
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
313
+ } as any);
314
+
286
315
  const mysqlAdapter = new KyselyAdapter({ db: mysqlDb, provider: "mysql" });
287
316
 
288
317
  vi.spyOn(mysqlAdapter, "isConnectionHealthy").mockResolvedValue(true);
@@ -318,6 +347,20 @@ describe("generateMigrationsOrSchema - kysely", () => {
318
347
  it("should include SQLite-specific pragma in generated SQL", async () => {
319
348
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
349
  const sqliteDb = new Kysely({ dialect: new PostgresDialect({} as any) });
350
+
351
+ // Mock Kysely transaction to prevent actual database connections
352
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
+ vi.spyOn(sqliteDb, "transaction").mockReturnValue({
354
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
355
+ execute: async (callback: any) => {
356
+ const mockTx = {
357
+ executeQuery: async () => ({ rows: [] }),
358
+ };
359
+ return callback(mockTx);
360
+ },
361
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
+ } as any);
363
+
321
364
  const sqliteAdapter = new KyselyAdapter({ db: sqliteDb, provider: "sqlite" });
322
365
 
323
366
  vi.spyOn(sqliteAdapter, "isConnectionHealthy").mockResolvedValue(true);
@@ -377,7 +420,7 @@ describe("postProcessMigrationFilenames", () => {
377
420
  {
378
421
  schema: "schema2",
379
422
  path: "placeholder.sql",
380
- namespace: "fragno-db-settings",
423
+ namespace: "", // Empty namespace for settings table
381
424
  fromVersion: 0,
382
425
  toVersion: 1,
383
426
  },
@@ -393,8 +436,8 @@ describe("postProcessMigrationFilenames", () => {
393
436
  const result = postProcessMigrationFilenames(files);
394
437
 
395
438
  expect(result).toHaveLength(3);
396
- expect(result[0].namespace).toBe("fragno-db-settings");
397
- expect(result[0].path).toBe("20251024_001_f000_t001_fragno-db-settings.sql");
439
+ expect(result[0].namespace).toBe(""); // Empty namespace for settings table
440
+ expect(result[0].path).toBe("20251024_001_f000_t001_fragno_db_settings.sql");
398
441
  });
399
442
 
400
443
  it("should sort non-settings namespaces alphabetically", () => {
@@ -435,9 +478,9 @@ describe("postProcessMigrationFilenames", () => {
435
478
  {
436
479
  schema: "CREATE TABLE users;",
437
480
  path: "placeholder.sql",
438
- namespace: "fragno-db-settings",
481
+ namespace: "", // Empty namespace for settings table
439
482
  fromVersion: 0,
440
- toVersion: 1,
483
+ toVersion: 5,
441
484
  },
442
485
  {
443
486
  schema: "CREATE TABLE comments;",
@@ -450,7 +493,7 @@ describe("postProcessMigrationFilenames", () => {
450
493
 
451
494
  const result = postProcessMigrationFilenames(files);
452
495
 
453
- expect(result[0].path).toBe("20251024_001_f000_t001_fragno-db-settings.sql");
496
+ expect(result[0].path).toBe("20251024_001_f000_t005_fragno_db_settings.sql");
454
497
  expect(result[1].path).toBe("20251024_002_f005_t010_comment-db.sql");
455
498
  });
456
499
 
@@ -514,7 +557,7 @@ describe("postProcessMigrationFilenames", () => {
514
557
  {
515
558
  schema: "schema2",
516
559
  path: "placeholder.sql",
517
- namespace: "fragno-db-settings",
560
+ namespace: "", // Empty namespace for settings table
518
561
  fromVersion: 0,
519
562
  toVersion: 5,
520
563
  },
@@ -537,8 +580,8 @@ describe("postProcessMigrationFilenames", () => {
537
580
  const result = postProcessMigrationFilenames(files);
538
581
 
539
582
  expect(result).toHaveLength(4);
540
- expect(result[0].namespace).toBe("fragno-db-settings");
541
- expect(result[0].path).toBe("20251024_001_f000_t005_fragno-db-settings.sql");
583
+ expect(result[0].namespace).toBe(""); // Empty namespace for settings table
584
+ expect(result[0].path).toBe("20251024_001_f000_t005_fragno_db_settings.sql");
542
585
  expect(result[1].namespace).toBe("apple-db");
543
586
  expect(result[1].path).toBe("20251024_002_f003_t004_apple-db.sql");
544
587
  expect(result[2].namespace).toBe("mango-db");
@@ -1,15 +1,17 @@
1
1
  import type { FragnoDatabase } from "../mod";
2
2
  import type { AnySchema } from "../schema/create";
3
3
  import type { PreparedMigration } from "./create";
4
- import {
5
- settingsSchema,
6
- SETTINGS_NAMESPACE,
7
- createSettingsManager,
8
- } from "../shared/settings-schema";
9
4
  import {
10
5
  fragnoDatabaseAdapterNameFakeSymbol,
11
6
  fragnoDatabaseAdapterVersionFakeSymbol,
12
7
  } from "../adapters/adapters";
8
+ import {
9
+ internalFragmentDef,
10
+ settingsSchema,
11
+ SETTINGS_TABLE_NAME,
12
+ SETTINGS_NAMESPACE,
13
+ } from "../fragments/internal-fragment";
14
+ import { instantiate } from "@fragno-dev/core";
13
15
 
14
16
  export interface GenerationEngineResult {
15
17
  schema: string;
@@ -59,12 +61,30 @@ export async function generateMigrationsOrSchema<
59
61
  );
60
62
  }
61
63
 
62
- const fragments = databases.map((db) => ({
63
- schema: db.schema,
64
- namespace: db.namespace,
65
- }));
64
+ // Collect all schemas, de-duplicating by namespace.
65
+ // The internal fragment (settings schema) is always included first since all database
66
+ // fragments automatically link to it via withDatabase().
67
+ const fragmentsMap = new Map<string, { schema: AnySchema; namespace: string }>();
68
+
69
+ // Include internal fragment first with empty namespace (settings table has no prefix)
70
+ fragmentsMap.set("", {
71
+ schema: settingsSchema,
72
+ namespace: "",
73
+ });
74
+
75
+ // Add user fragments, de-duplicating by namespace
76
+ // Each FragnoDatabase has a unique namespace, so this prevents duplicate schema generation
77
+ for (const db of databases) {
78
+ if (!fragmentsMap.has(db.namespace)) {
79
+ fragmentsMap.set(db.namespace, {
80
+ schema: db.schema,
81
+ namespace: db.namespace,
82
+ });
83
+ }
84
+ }
66
85
 
67
- const generator = adapter.createSchemaGenerator(fragments, {
86
+ const allFragments = Array.from(fragmentsMap.values());
87
+ const generator = adapter.createSchemaGenerator(allFragments, {
68
88
  path: options?.path,
69
89
  });
70
90
 
@@ -89,12 +109,22 @@ export async function generateMigrationsOrSchema<
89
109
  );
90
110
  }
91
111
 
92
- const settingsQueryEngine = adapter.createQueryEngine(settingsSchema, "");
93
- const settingsManager = createSettingsManager(settingsQueryEngine, SETTINGS_NAMESPACE);
112
+ // Use the internal fragment for settings management
113
+ const internalFragment = instantiate(internalFragmentDef)
114
+ .withConfig({})
115
+ .withOptions({ databaseAdapter: adapter })
116
+ .build();
94
117
 
95
118
  let settingsSourceVersion: number;
96
119
  try {
97
- const result = await settingsManager.get("version");
120
+ const result = await internalFragment.inContext(async function () {
121
+ return await this.uow(async ({ executeRetrieve }) => {
122
+ const v = internalFragment.services.settingsService.get("version");
123
+ await executeRetrieve();
124
+
125
+ return v;
126
+ });
127
+ });
98
128
 
99
129
  if (!result) {
100
130
  settingsSourceVersion = 0;
@@ -102,13 +132,14 @@ export async function generateMigrationsOrSchema<
102
132
  settingsSourceVersion = parseInt(result.value);
103
133
  }
104
134
  } catch {
105
- // We don't really have a way to verify this error happens because the key doesn't exist in the database
135
+ // Settings table doesn't exist yet (first migration)
106
136
  settingsSourceVersion = 0;
107
137
  }
108
138
 
109
139
  const generatedFiles: GenerationInternalResult[] = [];
110
140
 
111
- const settingsMigrator = adapter.createMigrationEngine(settingsSchema, SETTINGS_NAMESPACE);
141
+ // Use empty namespace for settings (SETTINGS_NAMESPACE is for prefixing keys, not the database namespace)
142
+ const settingsMigrator = adapter.createMigrationEngine(settingsSchema, "");
112
143
  const settingsTargetVersion = settingsSchema.version;
113
144
 
114
145
  // Generate settings table migration
@@ -128,7 +159,7 @@ export async function generateMigrationsOrSchema<
128
159
  generatedFiles.push({
129
160
  schema: settingsSql,
130
161
  path: "settings-migration.sql", // Placeholder, will be renamed in post-processing
131
- namespace: SETTINGS_NAMESPACE,
162
+ namespace: "", // Empty namespace for settings table
132
163
  fromVersion: settingsSourceVersion,
133
164
  toVersion: settingsTargetVersion,
134
165
  preparedMigration: settingsMigration,
@@ -237,18 +268,35 @@ export async function executeMigrations<const TDatabases extends FragnoDatabase<
237
268
  }> = [];
238
269
 
239
270
  // 1. Prepare settings table migration
240
- const settingsQueryEngine = adapter.createQueryEngine(settingsSchema, "");
241
- const settingsManager = createSettingsManager(settingsQueryEngine, SETTINGS_NAMESPACE);
271
+ // Use the internal fragment for settings management
272
+ const internalFragment = instantiate(internalFragmentDef)
273
+ .withConfig({})
274
+ .withOptions({ databaseAdapter: adapter })
275
+ .build();
242
276
 
243
277
  let settingsSourceVersion: number;
244
278
  try {
245
- const result = await settingsManager.get("version");
279
+ const result = await internalFragment.inContext(async function () {
280
+ return this.uow(async ({ forSchema, executeRetrieve }) => {
281
+ const uow = forSchema(settingsSchema);
282
+ const findOp = uow.find(SETTINGS_TABLE_NAME, (b) =>
283
+ b.whereIndex("unique_key", (eb) => eb("key", "=", `${SETTINGS_NAMESPACE}.version`)),
284
+ );
285
+
286
+ await executeRetrieve();
287
+
288
+ const [results] = await findOp.retrievalPhase;
289
+ return results?.[0];
290
+ });
291
+ });
246
292
  settingsSourceVersion = result ? parseInt(result.value) : 0;
247
293
  } catch {
294
+ // Settings table doesn't exist yet (first migration)
248
295
  settingsSourceVersion = 0;
249
296
  }
250
297
 
251
- const settingsMigrator = adapter.createMigrationEngine(settingsSchema, SETTINGS_NAMESPACE);
298
+ // Use empty namespace for settings (SETTINGS_NAMESPACE is for prefixing keys, not the database namespace)
299
+ const settingsMigrator = adapter.createMigrationEngine(settingsSchema, "");
252
300
  const settingsTargetVersion = settingsSchema.version;
253
301
 
254
302
  if (settingsSourceVersion < settingsTargetVersion) {
@@ -259,7 +307,7 @@ export async function executeMigrations<const TDatabases extends FragnoDatabase<
259
307
 
260
308
  if (settingsMigration.operations.length > 0) {
261
309
  migrationsToExecute.push({
262
- namespace: SETTINGS_NAMESPACE,
310
+ namespace: "", // Empty namespace for settings table
263
311
  fromVersion: settingsSourceVersion,
264
312
  toVersion: settingsTargetVersion,
265
313
  preparedMigration: settingsMigration,
@@ -334,12 +382,13 @@ export function postProcessMigrationFilenames(
334
382
  return [];
335
383
  }
336
384
 
337
- // Sort files: settings namespace first, then alphabetically by namespace
385
+ // Sort files: settings namespace first (empty string), then alphabetically by namespace
338
386
  const sortedFiles = [...files].sort((a, b) => {
339
- if (a.namespace === SETTINGS_NAMESPACE) {
387
+ // Settings table has empty namespace - sort it first
388
+ if (a.namespace === "") {
340
389
  return -1;
341
390
  }
342
- if (b.namespace === SETTINGS_NAMESPACE) {
391
+ if (b.namespace === "") {
343
392
  return 1;
344
393
  }
345
394
  return a.namespace.localeCompare(b.namespace);
@@ -357,7 +406,11 @@ export function postProcessMigrationFilenames(
357
406
  const orderNum = (index + 1).toString().padStart(3, "0");
358
407
  const fromPadded = fromVersion.toString().padStart(3, "0");
359
408
  const toPadded = toVersion.toString().padStart(3, "0");
360
- const safeName = file.namespace.replace(/[^a-z0-9-]/gi, "_");
409
+
410
+ // For settings table (empty namespace), use "fragno_db_settings" in the filename
411
+ // For other tables, use their namespace
412
+ const safeName =
413
+ file.namespace === "" ? "fragno_db_settings" : file.namespace.replace(/[^a-z0-9-]/gi, "_");
361
414
  const newPath = `${date}_${orderNum}_f${fromPadded}_t${toPadded}_${safeName}.sql`;
362
415
 
363
416
  return {
package/src/mod.ts CHANGED
@@ -30,40 +30,6 @@ export function isFragnoDatabase(value: unknown): value is FragnoDatabase<AnySch
30
30
  );
31
31
  }
32
32
 
33
- /**
34
- * Definition of a Fragno database schema and namespace.
35
- * Created by library authors using defineFragnoDatabase().
36
- * Apps instantiate it by calling .create(adapter).
37
- */
38
- export class FragnoDatabaseDefinition<const T extends AnySchema> {
39
- #namespace: string;
40
- #schema: T;
41
-
42
- constructor(options: CreateFragnoDatabaseDefinitionOptions<T>) {
43
- this.#namespace = options.namespace;
44
- this.#schema = options.schema;
45
- }
46
-
47
- get namespace() {
48
- return this.#namespace;
49
- }
50
-
51
- get schema() {
52
- return this.#schema;
53
- }
54
-
55
- /**
56
- * Creates a FragnoDatabase instance by binding an adapter to this definition.
57
- */
58
- create<TUOWConfig = void>(adapter: DatabaseAdapter<TUOWConfig>): FragnoDatabase<T, TUOWConfig> {
59
- return new FragnoDatabase({
60
- namespace: this.#namespace,
61
- schema: this.#schema,
62
- adapter,
63
- });
64
- }
65
- }
66
-
67
33
  /**
68
34
  * A Fragno database instance with a bound adapter.
69
35
  * Created from a FragnoDatabaseDefinition by calling .create(adapter).
@@ -120,17 +86,43 @@ export class FragnoDatabase<const T extends AnySchema, TUOWConfig = void> {
120
86
  }
121
87
  }
122
88
 
123
- export function defineFragnoDatabase<const TSchema extends AnySchema>(
124
- options: CreateFragnoDatabaseDefinitionOptions<TSchema>,
125
- ): FragnoDatabaseDefinition<TSchema> {
126
- return new FragnoDatabaseDefinition(options);
127
- }
128
-
129
89
  export {
130
- defineFragmentWithDatabase,
131
- DatabaseFragmentBuilder,
90
+ DatabaseFragmentDefinitionBuilder,
132
91
  type FragnoPublicConfigWithDatabase,
133
92
  type DatabaseFragmentContext,
134
- } from "./fragment";
93
+ type DatabaseHandlerContext as DatabaseRequestContext,
94
+ type ImplicitDatabaseDependencies,
95
+ } from "./db-fragment-definition-builder";
96
+
97
+ export { withDatabase } from "./with-database";
135
98
 
136
99
  export { decodeCursor, type CursorData } from "./query/cursor";
100
+
101
+ export {
102
+ createUnitOfWork,
103
+ UnitOfWork,
104
+ TypedUnitOfWork,
105
+ type IUnitOfWork,
106
+ type IUnitOfWorkRestricted,
107
+ type UOWCompiler,
108
+ type UOWExecutor,
109
+ type UOWDecoder,
110
+ } from "./query/unit-of-work";
111
+
112
+ export {
113
+ type RetryPolicy,
114
+ NoRetryPolicy,
115
+ ExponentialBackoffRetryPolicy,
116
+ LinearBackoffRetryPolicy,
117
+ } from "./query/retry-policy";
118
+
119
+ export {
120
+ executeUnitOfWork,
121
+ type ExecuteUnitOfWorkResult,
122
+ type ExecuteUnitOfWorkCallbacks,
123
+ type ExecuteUnitOfWorkOptions,
124
+ } from "./query/execute-unit-of-work";
125
+
126
+ export { type BoundServices } from "@fragno-dev/core";
127
+
128
+ export { internalFragmentDef } from "./fragments/internal-fragment";
@@ -314,6 +314,125 @@ describe("Cursor utilities", () => {
314
314
  expect(serialized["createdAt"]).toBe(1234567890);
315
315
  });
316
316
 
317
+ it("should handle Date objects in cursors for PostgreSQL", () => {
318
+ const testSchema = schema((s) =>
319
+ s.addTable("posts", (t) =>
320
+ t
321
+ .addColumn("id", idColumn())
322
+ .addColumn("createdAt", column("timestamp"))
323
+ .createIndex("created", ["createdAt"]),
324
+ ),
325
+ );
326
+
327
+ const table = testSchema.tables.posts;
328
+ const createdAtDate = new Date("2025-11-07T09:36:57.959Z");
329
+ const record = {
330
+ id: "post123",
331
+ createdAt: createdAtDate,
332
+ };
333
+
334
+ const index = table.indexes.created;
335
+ const indexColumns = index.columns;
336
+
337
+ // Create cursor from record (has Date object)
338
+ const cursor = createCursorFromRecord(record, indexColumns, {
339
+ indexName: "created",
340
+ orderDirection: "asc",
341
+ pageSize: 10,
342
+ });
343
+
344
+ // Encode and decode (Date becomes string in JSON)
345
+ const encoded = cursor.encode();
346
+ const decoded = decodeCursor(encoded);
347
+
348
+ // After decode, the value is a string (from JSON.parse)
349
+ expect(typeof decoded.indexValues["createdAt"]).toBe("string");
350
+ expect(decoded.indexValues["createdAt"]).toBe("2025-11-07T09:36:57.959Z");
351
+
352
+ // Serialize should convert it back to Date for the database
353
+ const serialized = serializeCursorValues(decoded, indexColumns, "postgresql");
354
+
355
+ // For PostgreSQL, Date objects should be passed through as-is
356
+ expect(serialized["createdAt"]).toBeInstanceOf(Date);
357
+ expect((serialized["createdAt"] as Date).toISOString()).toBe("2025-11-07T09:36:57.959Z");
358
+ });
359
+
360
+ it("should handle Date objects in cursors for SQLite", () => {
361
+ const testSchema = schema((s) =>
362
+ s.addTable("posts", (t) =>
363
+ t
364
+ .addColumn("id", idColumn())
365
+ .addColumn("createdAt", column("timestamp"))
366
+ .createIndex("created", ["createdAt"]),
367
+ ),
368
+ );
369
+
370
+ const table = testSchema.tables.posts;
371
+ const createdAtDate = new Date("2025-11-07T09:36:57.959Z");
372
+ const record = {
373
+ id: "post123",
374
+ createdAt: createdAtDate,
375
+ };
376
+
377
+ const index = table.indexes.created;
378
+ const indexColumns = index.columns;
379
+
380
+ // Create cursor from record
381
+ const cursor = createCursorFromRecord(record, indexColumns, {
382
+ indexName: "created",
383
+ orderDirection: "asc",
384
+ pageSize: 10,
385
+ });
386
+
387
+ // Encode and decode
388
+ const decoded = decodeCursor(cursor.encode());
389
+
390
+ // Serialize should convert string back to Date, then to timestamp number for SQLite
391
+ const serialized = serializeCursorValues(decoded, indexColumns, "sqlite");
392
+
393
+ // For SQLite, Date should be converted to timestamp number
394
+ expect(typeof serialized["createdAt"]).toBe("number");
395
+ expect(serialized["createdAt"]).toBe(createdAtDate.getTime());
396
+ });
397
+
398
+ it("should handle Date objects in cursors for MySQL", () => {
399
+ const testSchema = schema((s) =>
400
+ s.addTable("posts", (t) =>
401
+ t
402
+ .addColumn("id", idColumn())
403
+ .addColumn("createdAt", column("timestamp"))
404
+ .createIndex("created", ["createdAt"]),
405
+ ),
406
+ );
407
+
408
+ const table = testSchema.tables.posts;
409
+ const createdAtDate = new Date("2025-11-07T09:36:57.959Z");
410
+ const record = {
411
+ id: "post123",
412
+ createdAt: createdAtDate,
413
+ };
414
+
415
+ const index = table.indexes.created;
416
+ const indexColumns = index.columns;
417
+
418
+ // Create cursor from record
419
+ const cursor = createCursorFromRecord(record, indexColumns, {
420
+ indexName: "created",
421
+ orderDirection: "asc",
422
+ pageSize: 10,
423
+ });
424
+
425
+ // Encode and decode
426
+ const decoded = decodeCursor(cursor.encode());
427
+
428
+ // Serialize for MySQL
429
+ const serialized = serializeCursorValues(decoded, indexColumns, "mysql");
430
+
431
+ // For MySQL, Date objects should be passed through as-is
432
+ expect(serialized["createdAt"]).toBeInstanceOf(Date);
433
+ expect((serialized["createdAt"] as Date).toISOString()).toBe("2025-11-07T09:36:57.959Z");
434
+ });
435
+
317
436
  it("should handle different order directions correctly", () => {
318
437
  const cursorAsc = new Cursor({
319
438
  indexName: "_primary",
@@ -1,5 +1,5 @@
1
1
  import type { AnyColumn } from "../schema/create";
2
- import { serialize } from "../schema/serialize";
2
+ import { deserialize, serialize } from "../schema/serialize";
3
3
  import type { SQLProvider } from "../shared/providers";
4
4
 
5
5
  /**
@@ -78,6 +78,10 @@ export interface CursorResult<T> {
78
78
  * Cursor to fetch the next page (undefined if no more results)
79
79
  */
80
80
  cursor?: Cursor;
81
+ /**
82
+ * Whether there are more results available after this page
83
+ */
84
+ hasNextPage: boolean;
81
85
  }
82
86
 
83
87
  /**
@@ -203,8 +207,12 @@ export function createCursorFromRecord(
203
207
  /**
204
208
  * Serialize cursor values for database queries
205
209
  *
206
- * Converts cursor values (which are in application format) to database format
207
- * using the column serialization rules.
210
+ * Converts cursor values (which are in JSON-compatible format after decode)
211
+ * to database format using the column serialization rules.
212
+ *
213
+ * This function performs a two-step process:
214
+ * 1. Deserialize from JSON format to application format (e.g., ISO string → Date)
215
+ * 2. Serialize from application format to database format (e.g., Date → driver format)
208
216
  *
209
217
  * @param cursor - The cursor object
210
218
  * @param indexColumns - The columns that make up the index
@@ -230,7 +238,12 @@ export function serializeCursorValues(
230
238
  for (const col of indexColumns) {
231
239
  const value = cursor.indexValues[col.ormName];
232
240
  if (value !== undefined) {
233
- serialized[col.ormName] = serialize(value, col, provider);
241
+ // First deserialize from JSON format to application format
242
+ // (e.g., "2025-11-07T09:36:57.959Z" string → Date object)
243
+ const deserialized = deserialize(value, col, provider);
244
+ // Then serialize to database format
245
+ // (e.g., Date → database driver format)
246
+ serialized[col.ormName] = serialize(deserialized, col, provider);
234
247
  }
235
248
  }
236
249