@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.
- package/.turbo/turbo-build.log +179 -132
- package/CHANGELOG.md +30 -0
- package/dist/adapters/adapters.d.ts +27 -1
- package/dist/adapters/adapters.d.ts.map +1 -1
- package/dist/adapters/adapters.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +5 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +7 -5
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +76 -44
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
- package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
- package/dist/adapters/drizzle/generate.d.ts +4 -1
- package/dist/adapters/drizzle/generate.d.ts.map +1 -1
- package/dist/adapters/drizzle/generate.js +11 -18
- package/dist/adapters/drizzle/generate.js.map +1 -1
- package/dist/adapters/drizzle/shared.d.ts +14 -1
- package/dist/adapters/drizzle/shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js +14 -3
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query-builder.js +1 -1
- package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
- package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts +1 -0
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-query.js +28 -19
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
- package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-shared.js +16 -1
- package/dist/adapters/kysely/kysely-shared.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
- package/dist/adapters/kysely/migration/execute-base.js +1 -1
- package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
- package/dist/db-fragment-definition-builder.d.ts +152 -0
- package/dist/db-fragment-definition-builder.d.ts.map +1 -0
- package/dist/db-fragment-definition-builder.js +137 -0
- package/dist/db-fragment-definition-builder.js.map +1 -0
- package/dist/fragments/internal-fragment.d.ts +19 -0
- package/dist/fragments/internal-fragment.d.ts.map +1 -0
- package/dist/fragments/internal-fragment.js +39 -0
- package/dist/fragments/internal-fragment.js.map +1 -0
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js +35 -15
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/mod.d.ts +8 -18
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +7 -34
- package/dist/mod.js.map +1 -1
- package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
- package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
- package/dist/packages/fragno/dist/api/bind-services.js +20 -0
- package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
- package/dist/packages/fragno/dist/api/error.js +48 -0
- package/dist/packages/fragno/dist/api/error.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
- package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
- package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
- package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
- package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
- package/dist/packages/fragno/dist/api/internal/route.js +10 -0
- package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
- package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
- package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
- package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
- package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
- package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
- package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
- package/dist/packages/fragno/dist/api/route.js +17 -0
- package/dist/packages/fragno/dist/api/route.js.map +1 -0
- package/dist/packages/fragno/dist/internal/symbols.js +10 -0
- package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
- package/dist/query/cursor.d.ts +10 -2
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +11 -4
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/execute-unit-of-work.d.ts +123 -0
- package/dist/query/execute-unit-of-work.d.ts.map +1 -0
- package/dist/query/execute-unit-of-work.js +184 -0
- package/dist/query/execute-unit-of-work.js.map +1 -0
- package/dist/query/query.d.ts +3 -3
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/result-transform.js +4 -2
- package/dist/query/result-transform.js.map +1 -1
- package/dist/query/retry-policy.d.ts +88 -0
- package/dist/query/retry-policy.d.ts.map +1 -0
- package/dist/query/retry-policy.js +61 -0
- package/dist/query/retry-policy.js.map +1 -0
- package/dist/query/unit-of-work.d.ts +171 -32
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +530 -133
- package/dist/query/unit-of-work.js.map +1 -1
- package/dist/schema/serialize.js +12 -7
- package/dist/schema/serialize.js.map +1 -1
- package/dist/with-database.d.ts +28 -0
- package/dist/with-database.d.ts.map +1 -0
- package/dist/with-database.js +34 -0
- package/dist/with-database.js.map +1 -0
- package/package.json +10 -3
- package/src/adapters/adapters.ts +30 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
- package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
- package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
- package/src/adapters/drizzle/drizzle-query.ts +25 -15
- package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
- package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
- package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
- package/src/adapters/drizzle/generate.test.ts +102 -269
- package/src/adapters/drizzle/generate.ts +12 -30
- package/src/adapters/drizzle/test-utils.ts +36 -5
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
- package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
- package/src/adapters/kysely/kysely-adapter.ts +25 -2
- package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
- package/src/adapters/kysely/kysely-query.ts +57 -37
- package/src/adapters/kysely/kysely-shared.ts +34 -0
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
- package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
- package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
- package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
- package/src/adapters/kysely/migration/execute-base.ts +1 -1
- package/src/db-fragment-definition-builder.test.ts +887 -0
- package/src/db-fragment-definition-builder.ts +506 -0
- package/src/db-fragment-instantiator.test.ts +467 -0
- package/src/db-fragment-integration.test.ts +408 -0
- package/src/fragments/internal-fragment.test.ts +160 -0
- package/src/fragments/internal-fragment.ts +85 -0
- package/src/migration-engine/generation-engine.test.ts +58 -15
- package/src/migration-engine/generation-engine.ts +78 -25
- package/src/mod.ts +35 -43
- package/src/query/cursor.test.ts +119 -0
- package/src/query/cursor.ts +17 -4
- package/src/query/execute-unit-of-work.test.ts +1310 -0
- package/src/query/execute-unit-of-work.ts +463 -0
- package/src/query/query.ts +4 -4
- package/src/query/result-transform.test.ts +129 -0
- package/src/query/result-transform.ts +4 -1
- package/src/query/retry-policy.test.ts +217 -0
- package/src/query/retry-policy.ts +141 -0
- package/src/query/unit-of-work-coordinator.test.ts +833 -0
- package/src/query/unit-of-work-types.test.ts +15 -2
- package/src/query/unit-of-work.test.ts +878 -200
- package/src/query/unit-of-work.ts +963 -321
- package/src/schema/serialize.ts +22 -11
- package/src/with-database.ts +140 -0
- package/tsdown.config.ts +1 -0
- package/dist/fragment.d.ts +0 -54
- package/dist/fragment.d.ts.map +0 -1
- package/dist/fragment.js +0 -92
- package/dist/fragment.js.map +0 -1
- package/dist/shared/settings-schema.js +0 -36
- package/dist/shared/settings-schema.js.map +0 -1
- package/src/fragment.test.ts +0 -341
- package/src/fragment.ts +0 -198
- 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("
|
|
54
|
-
expect(results[0].path).toBe("
|
|
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("
|
|
105
|
-
expect(results[0].path).toBe("
|
|
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("
|
|
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("
|
|
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: "
|
|
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("
|
|
397
|
-
expect(result[0].path).toBe("
|
|
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: "
|
|
481
|
+
namespace: "", // Empty namespace for settings table
|
|
439
482
|
fromVersion: 0,
|
|
440
|
-
toVersion:
|
|
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("
|
|
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: "
|
|
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("
|
|
541
|
-
expect(result[0].path).toBe("
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
241
|
-
const
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
387
|
+
// Settings table has empty namespace - sort it first
|
|
388
|
+
if (a.namespace === "") {
|
|
340
389
|
return -1;
|
|
341
390
|
}
|
|
342
|
-
if (b.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
|
-
|
|
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
|
-
|
|
131
|
-
DatabaseFragmentBuilder,
|
|
90
|
+
DatabaseFragmentDefinitionBuilder,
|
|
132
91
|
type FragnoPublicConfigWithDatabase,
|
|
133
92
|
type DatabaseFragmentContext,
|
|
134
|
-
|
|
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";
|
package/src/query/cursor.test.ts
CHANGED
|
@@ -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",
|
package/src/query/cursor.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|