@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { drizzle } from "drizzle-orm/pglite";
|
|
2
2
|
import { DrizzleAdapter } from "./drizzle-adapter";
|
|
3
|
-
import {
|
|
3
|
+
import { beforeAll, describe, expect, expectTypeOf, it } from "vitest";
|
|
4
4
|
import { column, idColumn, referenceColumn, schema } from "../../schema/create";
|
|
5
5
|
import type { DBType } from "./shared";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -126,7 +126,12 @@ describe("DrizzleAdapter PGLite", () => {
|
|
|
126
126
|
age: 25,
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
expectTypeOf
|
|
129
|
+
expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
|
|
130
|
+
Parameters<typeof createUow.find>[0]
|
|
131
|
+
>();
|
|
132
|
+
expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
|
|
133
|
+
"users" | "emails" | "posts" | "comments"
|
|
134
|
+
>();
|
|
130
135
|
|
|
131
136
|
const { success: createSuccess } = await createUow.executeMutations();
|
|
132
137
|
expect(createSuccess).toBe(true);
|
|
@@ -735,46 +740,61 @@ describe("DrizzleAdapter PGLite", () => {
|
|
|
735
740
|
it("should support cursor-based pagination with findWithCursor()", async () => {
|
|
736
741
|
const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
|
|
737
742
|
|
|
738
|
-
// Create
|
|
739
|
-
|
|
743
|
+
// Create exactly 15 users for precise pagination testing
|
|
744
|
+
const prefix = "CursorPagTest";
|
|
745
|
+
|
|
746
|
+
for (let i = 1; i <= 15; i++) {
|
|
740
747
|
await queryEngine.create("users", {
|
|
741
|
-
name:
|
|
748
|
+
name: `${prefix} ${i.toString().padStart(2, "0")}`,
|
|
742
749
|
age: 20 + i,
|
|
743
750
|
});
|
|
744
751
|
}
|
|
745
752
|
|
|
746
|
-
// Fetch first page with cursor
|
|
753
|
+
// Fetch first page with cursor (pageSize=10, total=15 items)
|
|
747
754
|
const firstPage = await queryEngine.findWithCursor("users", (b) =>
|
|
748
|
-
b
|
|
755
|
+
b
|
|
756
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
|
|
757
|
+
.orderByIndex("name_idx", "asc")
|
|
758
|
+
.pageSize(10),
|
|
749
759
|
);
|
|
750
760
|
|
|
751
|
-
// Check structure
|
|
761
|
+
// Check structure and hasNextPage
|
|
752
762
|
expect(firstPage).toHaveProperty("items");
|
|
753
763
|
expect(firstPage).toHaveProperty("cursor");
|
|
764
|
+
expect(firstPage).toHaveProperty("hasNextPage");
|
|
754
765
|
expect(Array.isArray(firstPage.items)).toBe(true);
|
|
755
|
-
expect(firstPage.items.length).toBeGreaterThan(0);
|
|
756
|
-
expect(firstPage.items.length).toBeLessThanOrEqual(10);
|
|
757
|
-
|
|
758
|
-
assert(firstPage.cursor instanceof Cursor);
|
|
759
766
|
expect(firstPage.items).toHaveLength(10);
|
|
767
|
+
expect(firstPage.hasNextPage).toBe(true);
|
|
760
768
|
expect(firstPage.cursor).toBeInstanceOf(Cursor);
|
|
761
769
|
|
|
762
|
-
// Fetch second page using cursor
|
|
770
|
+
// Fetch second page using cursor (last page with 5 remaining items)
|
|
763
771
|
const secondPage = await queryEngine.findWithCursor("users", (b) =>
|
|
764
772
|
b
|
|
765
|
-
.whereIndex("name_idx")
|
|
773
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
|
|
766
774
|
.after(firstPage.cursor!)
|
|
767
775
|
.orderByIndex("name_idx", "asc")
|
|
768
776
|
.pageSize(10),
|
|
769
777
|
);
|
|
770
778
|
|
|
771
|
-
expect(secondPage.items
|
|
772
|
-
expect(secondPage.
|
|
779
|
+
expect(secondPage.items).toHaveLength(5);
|
|
780
|
+
expect(secondPage.hasNextPage).toBe(false);
|
|
781
|
+
expect(secondPage.cursor).toBeUndefined();
|
|
773
782
|
|
|
774
783
|
// Verify no overlap - first item of second page should come after last item of first page
|
|
775
784
|
const firstPageLastName = firstPage.items[firstPage.items.length - 1].name;
|
|
776
785
|
const secondPageFirstName = secondPage.items[0].name;
|
|
777
786
|
expect(secondPageFirstName > firstPageLastName).toBe(true);
|
|
787
|
+
|
|
788
|
+
// Test empty results
|
|
789
|
+
const emptyPage = await queryEngine.findWithCursor("users", (b) =>
|
|
790
|
+
b
|
|
791
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", "NonExistentPrefix"))
|
|
792
|
+
.orderByIndex("name_idx", "asc")
|
|
793
|
+
.pageSize(10),
|
|
794
|
+
);
|
|
795
|
+
expect(emptyPage.items).toHaveLength(0);
|
|
796
|
+
expect(emptyPage.hasNextPage).toBe(false);
|
|
797
|
+
expect(emptyPage.cursor).toBeUndefined();
|
|
778
798
|
});
|
|
779
799
|
|
|
780
800
|
it("should support findWithCursor() in Unit of Work", async () => {
|
|
@@ -789,11 +809,60 @@ describe("DrizzleAdapter PGLite", () => {
|
|
|
789
809
|
|
|
790
810
|
const [result] = await uow.executeRetrieve();
|
|
791
811
|
|
|
792
|
-
// Verify result structure
|
|
812
|
+
// Verify result structure including hasNextPage
|
|
793
813
|
expect(result).toHaveProperty("items");
|
|
794
814
|
expect(result).toHaveProperty("cursor");
|
|
815
|
+
expect(result).toHaveProperty("hasNextPage");
|
|
795
816
|
expect(Array.isArray(result.items)).toBe(true);
|
|
796
817
|
expect(result.items).toHaveLength(5);
|
|
818
|
+
expect(typeof result.hasNextPage).toBe("boolean");
|
|
797
819
|
expect(result.cursor).toBeInstanceOf(Cursor);
|
|
798
820
|
});
|
|
821
|
+
|
|
822
|
+
it("should fail check() when version changes", async () => {
|
|
823
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
|
|
824
|
+
|
|
825
|
+
// Create a user
|
|
826
|
+
const createUserUow = queryEngine.createUnitOfWork("create-user-for-version-conflict");
|
|
827
|
+
createUserUow.create("users", {
|
|
828
|
+
name: "Version Conflict User",
|
|
829
|
+
age: 40,
|
|
830
|
+
});
|
|
831
|
+
await createUserUow.executeMutations();
|
|
832
|
+
|
|
833
|
+
// Get the user
|
|
834
|
+
const [[user]] = await queryEngine
|
|
835
|
+
.createUnitOfWork("get-user-for-version-conflict")
|
|
836
|
+
.find("users", (b) =>
|
|
837
|
+
b.whereIndex("name_idx", (eb) => eb("name", "=", "Version Conflict User")),
|
|
838
|
+
)
|
|
839
|
+
.executeRetrieve();
|
|
840
|
+
|
|
841
|
+
// Update the user to increment their version
|
|
842
|
+
const updateUow = queryEngine.createUnitOfWork("update-user-version");
|
|
843
|
+
updateUow.update("users", user.id, (b) => b.set({ age: 41 }));
|
|
844
|
+
await updateUow.executeMutations();
|
|
845
|
+
|
|
846
|
+
// Try to check with the old version (should fail)
|
|
847
|
+
const uow = queryEngine.createUnitOfWork("check-stale-version");
|
|
848
|
+
uow.check("users", user.id); // This has version 0, but the user now has version 1
|
|
849
|
+
uow.create("posts", {
|
|
850
|
+
user_id: user.id,
|
|
851
|
+
title: "Should Not Be Created",
|
|
852
|
+
content: "Content",
|
|
853
|
+
created_at: new Date(),
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const { success } = await uow.executeMutations();
|
|
857
|
+
expect(success).toBe(false);
|
|
858
|
+
|
|
859
|
+
// Verify the post was NOT created
|
|
860
|
+
const [posts] = await queryEngine
|
|
861
|
+
.createUnitOfWork("get-posts-for-version-conflict")
|
|
862
|
+
.find("posts", (b) => b.whereIndex("posts_user_idx", (eb) => eb("user_id", "=", user.id)))
|
|
863
|
+
.executeRetrieve();
|
|
864
|
+
|
|
865
|
+
const conflictPosts = posts.filter((p) => p.title === "Should Not Be Created");
|
|
866
|
+
expect(conflictPosts).toHaveLength(0);
|
|
867
|
+
});
|
|
799
868
|
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { drizzle } from "drizzle-orm/libsql";
|
|
2
2
|
import { createClient } from "@libsql/client";
|
|
3
3
|
import { DrizzleAdapter } from "./drizzle-adapter";
|
|
4
|
-
import { beforeAll, describe, expect, expectTypeOf, it } from "vitest";
|
|
5
|
-
import { column, idColumn, referenceColumn, schema } from "../../schema/create";
|
|
4
|
+
import { beforeAll, describe, expect, expectTypeOf, it, assert } from "vitest";
|
|
5
|
+
import { column, idColumn, referenceColumn, schema, type FragnoId } from "../../schema/create";
|
|
6
6
|
import type { DBType } from "./shared";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { writeAndLoadSchema } from "./test-utils";
|
|
9
9
|
import { Cursor } from "../../query/cursor";
|
|
10
|
+
import { executeUnitOfWork } from "../../query/execute-unit-of-work";
|
|
11
|
+
import { ExponentialBackoffRetryPolicy } from "../../query/retry-policy";
|
|
10
12
|
|
|
11
13
|
// Import drizzle-kit for migrations
|
|
12
14
|
const require = createRequire(import.meta.url);
|
|
@@ -71,6 +73,30 @@ describe("DrizzleAdapter SQLite", () => {
|
|
|
71
73
|
});
|
|
72
74
|
});
|
|
73
75
|
|
|
76
|
+
// Second schema for multi-schema testing
|
|
77
|
+
const schema2 = schema((s) => {
|
|
78
|
+
return s
|
|
79
|
+
.addTable("products", (t) => {
|
|
80
|
+
return t
|
|
81
|
+
.addColumn("id", idColumn())
|
|
82
|
+
.addColumn("name", column("string"))
|
|
83
|
+
.addColumn("price", column("integer"))
|
|
84
|
+
.createIndex("name_idx", ["name"]);
|
|
85
|
+
})
|
|
86
|
+
.addTable("orders", (t) => {
|
|
87
|
+
return t
|
|
88
|
+
.addColumn("id", idColumn())
|
|
89
|
+
.addColumn("product_id", referenceColumn())
|
|
90
|
+
.addColumn("quantity", column("integer"))
|
|
91
|
+
.createIndex("product_orders_idx", ["product_id"]);
|
|
92
|
+
})
|
|
93
|
+
.addReference("product", {
|
|
94
|
+
type: "one",
|
|
95
|
+
from: { table: "orders", column: "product_id" },
|
|
96
|
+
to: { table: "products", column: "id" },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
74
100
|
let adapter: DrizzleAdapter;
|
|
75
101
|
let db: DBType;
|
|
76
102
|
// let sqliteDb: Database.Database;
|
|
@@ -84,17 +110,28 @@ describe("DrizzleAdapter SQLite", () => {
|
|
|
84
110
|
"namespace",
|
|
85
111
|
);
|
|
86
112
|
|
|
113
|
+
// Write second schema to file and dynamically import it
|
|
114
|
+
const { schemaModule: schemaModule2, cleanup: cleanup2 } = await writeAndLoadSchema(
|
|
115
|
+
"drizzle-adapter-sqlite-schema2",
|
|
116
|
+
schema2,
|
|
117
|
+
"sqlite",
|
|
118
|
+
"namespace2",
|
|
119
|
+
);
|
|
120
|
+
|
|
87
121
|
const client = createClient({
|
|
88
122
|
url: "file::memory:?cache=shared",
|
|
89
123
|
});
|
|
90
124
|
|
|
125
|
+
// Merge both schema modules for the db
|
|
126
|
+
const mergedSchema = { ...schemaModule, ...schemaModule2 };
|
|
127
|
+
|
|
91
128
|
db = drizzle(client, {
|
|
92
|
-
schema:
|
|
129
|
+
schema: mergedSchema,
|
|
93
130
|
}) as unknown as DBType;
|
|
94
131
|
|
|
95
|
-
// Generate and run migrations
|
|
132
|
+
// Generate and run migrations for both schemas
|
|
96
133
|
const emptyJson = await generateSQLiteDrizzleJson({});
|
|
97
|
-
const targetJson = await generateSQLiteDrizzleJson(
|
|
134
|
+
const targetJson = await generateSQLiteDrizzleJson(mergedSchema);
|
|
98
135
|
|
|
99
136
|
const migrationStatements = await generateSQLiteMigration(emptyJson, targetJson);
|
|
100
137
|
|
|
@@ -110,6 +147,7 @@ describe("DrizzleAdapter SQLite", () => {
|
|
|
110
147
|
return async () => {
|
|
111
148
|
client.close();
|
|
112
149
|
await cleanup();
|
|
150
|
+
await cleanup2();
|
|
113
151
|
};
|
|
114
152
|
}, 12000);
|
|
115
153
|
|
|
@@ -128,7 +166,12 @@ describe("DrizzleAdapter SQLite", () => {
|
|
|
128
166
|
age: 30,
|
|
129
167
|
});
|
|
130
168
|
|
|
131
|
-
expectTypeOf
|
|
169
|
+
expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
|
|
170
|
+
Parameters<typeof createUow.find>[0]
|
|
171
|
+
>();
|
|
172
|
+
expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
|
|
173
|
+
"users" | "emails" | "posts" | "comments"
|
|
174
|
+
>();
|
|
132
175
|
|
|
133
176
|
const { success: createSuccess } = await createUow.executeMutations();
|
|
134
177
|
expect(createSuccess).toBe(true);
|
|
@@ -230,7 +273,6 @@ describe("DrizzleAdapter SQLite", () => {
|
|
|
230
273
|
it("should support count operations", async () => {
|
|
231
274
|
const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
|
|
232
275
|
|
|
233
|
-
// Create some users
|
|
234
276
|
const createUow = queryEngine.createUnitOfWork("create-users");
|
|
235
277
|
createUow.create("users", { name: "User1", age: 20 });
|
|
236
278
|
createUow.create("users", { name: "User2", age: 30 });
|
|
@@ -636,4 +678,246 @@ describe("DrizzleAdapter SQLite", () => {
|
|
|
636
678
|
expect(localDate).toBeInstanceOf(Date);
|
|
637
679
|
expect(typeof localDate.getTimezoneOffset()).toBe("number");
|
|
638
680
|
});
|
|
681
|
+
|
|
682
|
+
it("should support forSchema for multi-schema queries", async () => {
|
|
683
|
+
const queryEngine1 = adapter.createQueryEngine(testSchema, "namespace");
|
|
684
|
+
const queryEngine2 = adapter.createQueryEngine(schema2, "namespace2");
|
|
685
|
+
|
|
686
|
+
// Create test data in schema1 (users)
|
|
687
|
+
const createUsersUow = queryEngine1.createUnitOfWork("create-users-for-multi-schema");
|
|
688
|
+
createUsersUow.create("users", { name: "Multi Schema User 1", age: 25 });
|
|
689
|
+
createUsersUow.create("users", { name: "Multi Schema User 2", age: 30 });
|
|
690
|
+
const { success: success1 } = await createUsersUow.executeMutations();
|
|
691
|
+
expect(success1).toBe(true);
|
|
692
|
+
|
|
693
|
+
// Create test data in schema2 (products)
|
|
694
|
+
const createProductsUow = queryEngine2.createUnitOfWork("create-products-for-multi-schema");
|
|
695
|
+
createProductsUow.create("products", { name: "Product A", price: 100 });
|
|
696
|
+
createProductsUow.create("products", { name: "Product B", price: 200 });
|
|
697
|
+
const { success: success2 } = await createProductsUow.executeMutations();
|
|
698
|
+
expect(success2).toBe(true);
|
|
699
|
+
|
|
700
|
+
// Now use forSchema to query from both schemas
|
|
701
|
+
const uow = queryEngine1.createUnitOfWork("multi-schema-query");
|
|
702
|
+
|
|
703
|
+
const view1 = uow
|
|
704
|
+
.forSchema(testSchema)
|
|
705
|
+
.find("users", (b) =>
|
|
706
|
+
b
|
|
707
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", "Multi Schema User"))
|
|
708
|
+
.select(["id", "name"]),
|
|
709
|
+
)
|
|
710
|
+
.find("users", (b) =>
|
|
711
|
+
b
|
|
712
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", "Multi Schema User"))
|
|
713
|
+
.select(["name", "age"]),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
const view2 = uow
|
|
717
|
+
.forSchema(schema2)
|
|
718
|
+
.find("products", (b) => b.whereIndex("primary").select(["name", "price"]));
|
|
719
|
+
|
|
720
|
+
// Execute the retrieval phase once
|
|
721
|
+
await uow.executeRetrieve();
|
|
722
|
+
|
|
723
|
+
// Get results from view1
|
|
724
|
+
const [users1, users2] = await view1.retrievalPhase;
|
|
725
|
+
const [user1] = users1;
|
|
726
|
+
expectTypeOf(user1).toMatchObjectType<{ id: FragnoId; name: string }>();
|
|
727
|
+
|
|
728
|
+
const [user2] = users2;
|
|
729
|
+
expectTypeOf(user2).toMatchObjectType<{ name: string; age: number | null }>();
|
|
730
|
+
|
|
731
|
+
// Get results from view2
|
|
732
|
+
const [products] = await view2.retrievalPhase;
|
|
733
|
+
const [product1] = products;
|
|
734
|
+
expectTypeOf(product1).toMatchObjectType<{ name: string; price: number }>();
|
|
735
|
+
|
|
736
|
+
// Verify users from schema1
|
|
737
|
+
expect(users1).toHaveLength(2);
|
|
738
|
+
expect(users1[0]).toMatchObject({
|
|
739
|
+
id: expect.any(Object),
|
|
740
|
+
name: "Multi Schema User 1",
|
|
741
|
+
});
|
|
742
|
+
expect(users1[1]).toMatchObject({
|
|
743
|
+
id: expect.any(Object),
|
|
744
|
+
name: "Multi Schema User 2",
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
expect(users2).toHaveLength(2);
|
|
748
|
+
expect(users2[0]).toMatchObject({
|
|
749
|
+
name: "Multi Schema User 1",
|
|
750
|
+
age: 25,
|
|
751
|
+
});
|
|
752
|
+
expect(users2[1]).toMatchObject({
|
|
753
|
+
name: "Multi Schema User 2",
|
|
754
|
+
age: 30,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Verify products from schema2
|
|
758
|
+
expect(products).toHaveLength(2);
|
|
759
|
+
expect(products[0]).toMatchObject({
|
|
760
|
+
name: "Product A",
|
|
761
|
+
price: 100,
|
|
762
|
+
});
|
|
763
|
+
expect(products[1]).toMatchObject({
|
|
764
|
+
name: "Product B",
|
|
765
|
+
price: 200,
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("should verify hasNextPage in cursor pagination", async () => {
|
|
770
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
|
|
771
|
+
|
|
772
|
+
// Create exactly 15 users for precise pagination testing
|
|
773
|
+
const prefix = "HasNextPageTest";
|
|
774
|
+
|
|
775
|
+
for (let i = 1; i <= 15; i++) {
|
|
776
|
+
await queryEngine.create("users", {
|
|
777
|
+
name: `${prefix} ${i.toString().padStart(2, "0")}`,
|
|
778
|
+
age: 20 + i,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Test 1: First page with more results available (pageSize=10, total=15)
|
|
783
|
+
const firstPage = await queryEngine.findWithCursor("users", (b) =>
|
|
784
|
+
b
|
|
785
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
|
|
786
|
+
.orderByIndex("name_idx", "asc")
|
|
787
|
+
.pageSize(10),
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
expect(firstPage.items).toHaveLength(10);
|
|
791
|
+
expect(firstPage.hasNextPage).toBe(true);
|
|
792
|
+
expect(firstPage.cursor).toBeInstanceOf(Cursor);
|
|
793
|
+
|
|
794
|
+
// Test 2: Second page (last page, partial results: 5 items remaining)
|
|
795
|
+
const secondPage = await queryEngine.findWithCursor("users", (b) =>
|
|
796
|
+
b
|
|
797
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
|
|
798
|
+
.after(firstPage.cursor!)
|
|
799
|
+
.orderByIndex("name_idx", "asc")
|
|
800
|
+
.pageSize(10),
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
expect(secondPage.items).toHaveLength(5);
|
|
804
|
+
expect(secondPage.hasNextPage).toBe(false);
|
|
805
|
+
expect(secondPage.cursor).toBeUndefined();
|
|
806
|
+
|
|
807
|
+
// Test 3: Empty results
|
|
808
|
+
const emptyPage = await queryEngine.findWithCursor("users", (b) =>
|
|
809
|
+
b
|
|
810
|
+
.whereIndex("name_idx", (eb) => eb("name", "starts with", "NonExistentPrefix"))
|
|
811
|
+
.orderByIndex("name_idx", "asc")
|
|
812
|
+
.pageSize(10),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
expect(emptyPage.items).toHaveLength(0);
|
|
816
|
+
expect(emptyPage.hasNextPage).toBe(false);
|
|
817
|
+
expect(emptyPage.cursor).toBeUndefined();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("should support executeUnitOfWork with retry logic", async () => {
|
|
821
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
|
|
822
|
+
|
|
823
|
+
// Create a test user
|
|
824
|
+
const createUow = queryEngine.createUnitOfWork("create-user-for-execute-uow");
|
|
825
|
+
createUow.create("users", { name: "Execute UOW User", age: 42 });
|
|
826
|
+
await createUow.executeMutations();
|
|
827
|
+
|
|
828
|
+
// Fetch the user to get their ID
|
|
829
|
+
const [[user]] = await queryEngine
|
|
830
|
+
.createUnitOfWork("get-user-for-execute-uow")
|
|
831
|
+
.find("users", (b) => b.whereIndex("name_idx", (eb) => eb("name", "=", "Execute UOW User")))
|
|
832
|
+
.executeRetrieve();
|
|
833
|
+
|
|
834
|
+
// Use executeUnitOfWork to increment age with optimistic locking
|
|
835
|
+
const result = await executeUnitOfWork(
|
|
836
|
+
{
|
|
837
|
+
retrieve: (uow) =>
|
|
838
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", user.id))),
|
|
839
|
+
mutate: (uow, [users]) => {
|
|
840
|
+
const foundUser = users[0];
|
|
841
|
+
const newAge = foundUser.age! + 1;
|
|
842
|
+
uow.update("users", foundUser.id, (b) => b.set({ age: newAge }).check());
|
|
843
|
+
return { previousAge: foundUser.age, newAge };
|
|
844
|
+
},
|
|
845
|
+
onSuccess: ({ mutationResult }) => {
|
|
846
|
+
// Verify the age was incremented correctly
|
|
847
|
+
expect(mutationResult.newAge).toBe(mutationResult.previousAge! + 1);
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
createUnitOfWork: () => queryEngine.createUnitOfWork("execute-uow-update"),
|
|
852
|
+
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
|
|
853
|
+
},
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
// Verify the operation succeeded
|
|
857
|
+
assert(result.success);
|
|
858
|
+
expect(result.mutationResult).toEqual({
|
|
859
|
+
previousAge: 42,
|
|
860
|
+
newAge: 43,
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// Verify the user was actually updated in the database
|
|
864
|
+
const updatedUser = await queryEngine.findFirst("users", (b) =>
|
|
865
|
+
b.whereIndex("primary", (eb) => eb("id", "=", user.id)),
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
expect(updatedUser).toMatchObject({
|
|
869
|
+
id: expect.objectContaining({
|
|
870
|
+
externalId: user.id.externalId,
|
|
871
|
+
version: 1, // Version incremented due to check()
|
|
872
|
+
}),
|
|
873
|
+
name: "Execute UOW User",
|
|
874
|
+
age: 43,
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it("should fail check() when version changes", async () => {
|
|
879
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
|
|
880
|
+
|
|
881
|
+
// Create a user
|
|
882
|
+
const createUserUow = queryEngine.createUnitOfWork("create-user-for-version-conflict");
|
|
883
|
+
createUserUow.create("users", {
|
|
884
|
+
name: "Version Conflict User SQLite",
|
|
885
|
+
age: 40,
|
|
886
|
+
});
|
|
887
|
+
await createUserUow.executeMutations();
|
|
888
|
+
|
|
889
|
+
// Get the user
|
|
890
|
+
const [[user]] = await queryEngine
|
|
891
|
+
.createUnitOfWork("get-user-for-version-conflict")
|
|
892
|
+
.find("users", (b) =>
|
|
893
|
+
b.whereIndex("name_idx", (eb) => eb("name", "=", "Version Conflict User SQLite")),
|
|
894
|
+
)
|
|
895
|
+
.executeRetrieve();
|
|
896
|
+
|
|
897
|
+
// Update the user to increment their version
|
|
898
|
+
const updateUow = queryEngine.createUnitOfWork("update-user-version");
|
|
899
|
+
updateUow.update("users", user.id, (b) => b.set({ age: 41 }));
|
|
900
|
+
await updateUow.executeMutations();
|
|
901
|
+
|
|
902
|
+
// Try to check with the old version (should fail)
|
|
903
|
+
const uow = queryEngine.createUnitOfWork("check-stale-version");
|
|
904
|
+
uow.check("users", user.id); // This has version 0, but the user now has version 1
|
|
905
|
+
uow.create("posts", {
|
|
906
|
+
user_id: user.id,
|
|
907
|
+
title: "Should Not Be Created SQLite",
|
|
908
|
+
content: "Content",
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
const { success } = await uow.executeMutations();
|
|
912
|
+
expect(success).toBe(false);
|
|
913
|
+
|
|
914
|
+
// Verify the post was NOT created
|
|
915
|
+
const [posts] = await queryEngine
|
|
916
|
+
.createUnitOfWork("get-posts-for-version-conflict")
|
|
917
|
+
.find("posts", (b) => b.whereIndex("posts_user_idx", (eb) => eb("user_id", "=", user.id)))
|
|
918
|
+
.executeRetrieve();
|
|
919
|
+
|
|
920
|
+
const conflictPosts = posts.filter((p) => p.title === "Should Not Be Created SQLite");
|
|
921
|
+
expect(conflictPosts).toHaveLength(0);
|
|
922
|
+
});
|
|
639
923
|
});
|
|
@@ -20,25 +20,9 @@ describe("DrizzleAdapter", () => {
|
|
|
20
20
|
|
|
21
21
|
expect(result.path).toBe("schema.ts");
|
|
22
22
|
expect(result.schema).toMatchInlineSnapshot(`
|
|
23
|
-
"import { pgTable, varchar, text, bigserial, integer
|
|
23
|
+
"import { pgTable, varchar, text, bigserial, integer } from "drizzle-orm/pg-core"
|
|
24
24
|
import { createId } from "@fragno-dev/db/id"
|
|
25
25
|
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Settings Table (shared across all fragments)
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
|
-
export const fragno_db_settings = pgTable("fragno_db_settings", {
|
|
31
|
-
id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
|
|
32
|
-
key: text("key").notNull(),
|
|
33
|
-
value: text("value").notNull(),
|
|
34
|
-
_internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
|
|
35
|
-
_version: integer("_version").notNull().default(0)
|
|
36
|
-
}, (table) => [
|
|
37
|
-
uniqueIndex("unique_key").on(table.key)
|
|
38
|
-
])
|
|
39
|
-
|
|
40
|
-
export const fragnoDbSettingSchemaVersion = 1;
|
|
41
|
-
|
|
42
26
|
// ============================================================================
|
|
43
27
|
// Fragment: test
|
|
44
28
|
// ============================================================================
|
|
@@ -69,25 +53,9 @@ describe("DrizzleAdapter", () => {
|
|
|
69
53
|
|
|
70
54
|
expect(result.path).toBe("schema.ts");
|
|
71
55
|
expect(result.schema).toMatchInlineSnapshot(`
|
|
72
|
-
"import { sqliteTable, text, integer
|
|
56
|
+
"import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
|
73
57
|
import { createId } from "@fragno-dev/db/id"
|
|
74
58
|
|
|
75
|
-
// ============================================================================
|
|
76
|
-
// Settings Table (shared across all fragments)
|
|
77
|
-
// ============================================================================
|
|
78
|
-
|
|
79
|
-
export const fragno_db_settings = sqliteTable("fragno_db_settings", {
|
|
80
|
-
id: text("id").notNull().$defaultFn(() => createId()),
|
|
81
|
-
key: text("key").notNull(),
|
|
82
|
-
value: text("value").notNull(),
|
|
83
|
-
_internalId: integer("_internalId").primaryKey({ autoIncrement: true }).notNull(),
|
|
84
|
-
_version: integer("_version").notNull().default(0)
|
|
85
|
-
}, (table) => [
|
|
86
|
-
uniqueIndex("unique_key").on(table.key)
|
|
87
|
-
])
|
|
88
|
-
|
|
89
|
-
export const fragnoDbSettingSchemaVersion = 1;
|
|
90
|
-
|
|
91
59
|
// ============================================================================
|
|
92
60
|
// Fragment: test
|
|
93
61
|
// ============================================================================
|
|
@@ -130,25 +98,9 @@ describe("DrizzleAdapter", () => {
|
|
|
130
98
|
|
|
131
99
|
// Original table should still be there
|
|
132
100
|
expect(result.schema).toMatchInlineSnapshot(`
|
|
133
|
-
"import { pgTable, varchar, text, bigserial, integer
|
|
101
|
+
"import { pgTable, varchar, text, bigserial, integer } from "drizzle-orm/pg-core"
|
|
134
102
|
import { createId } from "@fragno-dev/db/id"
|
|
135
103
|
|
|
136
|
-
// ============================================================================
|
|
137
|
-
// Settings Table (shared across all fragments)
|
|
138
|
-
// ============================================================================
|
|
139
|
-
|
|
140
|
-
export const fragno_db_settings = pgTable("fragno_db_settings", {
|
|
141
|
-
id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
|
|
142
|
-
key: text("key").notNull(),
|
|
143
|
-
value: text("value").notNull(),
|
|
144
|
-
_internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
|
|
145
|
-
_version: integer("_version").notNull().default(0)
|
|
146
|
-
}, (table) => [
|
|
147
|
-
uniqueIndex("unique_key").on(table.key)
|
|
148
|
-
])
|
|
149
|
-
|
|
150
|
-
export const fragnoDbSettingSchemaVersion = 1;
|
|
151
|
-
|
|
152
104
|
// ============================================================================
|
|
153
105
|
// Fragment: test
|
|
154
106
|
// ============================================================================
|