@fragno-dev/db 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +41 -39
- package/CHANGELOG.md +19 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +1 -1
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +42 -34
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +2 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +25 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/generate.js +1 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts +4 -3
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts +22 -0
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-query.js +101 -51
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js +2 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
- package/dist/adapters/kysely/migration/execute-base.js +1 -1
- package/dist/migration-engine/generation-engine.d.ts +1 -1
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/mod.d.ts +7 -6
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -1
- package/dist/mod.js.map +1 -1
- package/dist/query/cursor.d.ts +67 -32
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +84 -31
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/query.d.ts +29 -8
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/result-transform.js +17 -5
- package/dist/query/result-transform.js.map +1 -1
- package/dist/query/unit-of-work.d.ts +19 -8
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +54 -12
- package/dist/query/unit-of-work.js.map +1 -1
- package/dist/schema/serialize.js +2 -0
- package/dist/schema/serialize.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +242 -55
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +95 -39
- package/src/adapters/drizzle/drizzle-query.test.ts +54 -4
- package/src/adapters/drizzle/drizzle-query.ts +74 -60
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +82 -6
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +3 -2
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +40 -1
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +190 -4
- package/src/adapters/kysely/kysely-adapter.ts +6 -3
- package/src/adapters/kysely/kysely-query.test.ts +498 -0
- package/src/adapters/kysely/kysely-query.ts +187 -83
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +85 -3
- package/src/adapters/kysely/kysely-uow-compiler.ts +3 -2
- package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
- package/src/migration-engine/generation-engine.ts +2 -1
- package/src/mod.ts +12 -7
- package/src/query/cursor.test.ts +113 -68
- package/src/query/cursor.ts +127 -36
- package/src/query/query-type.test.ts +34 -14
- package/src/query/query.ts +94 -34
- package/src/query/result-transform.test.ts +5 -5
- package/src/query/result-transform.ts +29 -11
- package/src/query/unit-of-work.ts +141 -26
- package/src/schema/serialize.test.ts +223 -0
- package/src/schema/serialize.ts +16 -0
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type FragnoId,
|
|
11
11
|
type FragnoReference,
|
|
12
12
|
} from "../../schema/create";
|
|
13
|
-
import {
|
|
13
|
+
import { Cursor } from "../../query/cursor";
|
|
14
14
|
|
|
15
15
|
describe("KyselyAdapter PGLite", () => {
|
|
16
16
|
const testSchema = schema((s) => {
|
|
@@ -396,10 +396,12 @@ describe("KyselyAdapter PGLite", () => {
|
|
|
396
396
|
|
|
397
397
|
// Get cursor for pagination (using the last item from page 1)
|
|
398
398
|
const lastItem = page1Results[page1Results.length - 1]!;
|
|
399
|
-
const cursor =
|
|
399
|
+
const cursor = new Cursor({
|
|
400
|
+
indexName: "name_idx",
|
|
401
|
+
orderDirection: "asc",
|
|
402
|
+
pageSize: 2,
|
|
400
403
|
indexValues: { name: lastItem.name },
|
|
401
|
-
|
|
402
|
-
});
|
|
404
|
+
}).encode();
|
|
403
405
|
|
|
404
406
|
// Get page 2 using the cursor
|
|
405
407
|
const page2 = queryEngine
|
|
@@ -784,4 +786,188 @@ describe("KyselyAdapter PGLite", () => {
|
|
|
784
786
|
age: 60,
|
|
785
787
|
});
|
|
786
788
|
});
|
|
789
|
+
|
|
790
|
+
it("should handle timestamps and timezones correctly", async () => {
|
|
791
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
792
|
+
|
|
793
|
+
// Create a user
|
|
794
|
+
const userId = await queryEngine.create("users", {
|
|
795
|
+
name: "Timestamp Test User",
|
|
796
|
+
age: 28,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Create a post
|
|
800
|
+
const postId = await queryEngine.create("posts", {
|
|
801
|
+
user_id: userId,
|
|
802
|
+
title: "Timestamp Test Post",
|
|
803
|
+
content: "Testing timestamp handling",
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Retrieve the post
|
|
807
|
+
const post = await queryEngine.findFirst("posts", (b) =>
|
|
808
|
+
b.whereIndex("primary", (eb) => eb("id", "=", postId)),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
expect(post).toBeDefined();
|
|
812
|
+
|
|
813
|
+
// Test with a table that doesn't have timestamps
|
|
814
|
+
// Verify that Date handling works in general by checking basic Date operations
|
|
815
|
+
const now = new Date();
|
|
816
|
+
expect(now).toBeInstanceOf(Date);
|
|
817
|
+
expect(typeof now.getTime).toBe("function");
|
|
818
|
+
expect(typeof now.toISOString).toBe("function");
|
|
819
|
+
|
|
820
|
+
// Verify date serialization/deserialization works
|
|
821
|
+
const isoString = now.toISOString();
|
|
822
|
+
expect(typeof isoString).toBe("string");
|
|
823
|
+
expect(new Date(isoString).getTime()).toBe(now.getTime());
|
|
824
|
+
|
|
825
|
+
// Test timezone preservation
|
|
826
|
+
const specificDate = new Date("2024-06-15T14:30:00Z");
|
|
827
|
+
expect(specificDate.toISOString()).toBe("2024-06-15T14:30:00.000Z");
|
|
828
|
+
|
|
829
|
+
// Verify that dates from different timezones are handled correctly
|
|
830
|
+
const localDate = new Date("2024-06-15T14:30:00");
|
|
831
|
+
expect(localDate).toBeInstanceOf(Date);
|
|
832
|
+
expect(typeof localDate.getTimezoneOffset()).toBe("number");
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("should create user and post in same transaction using returned ID", async () => {
|
|
836
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
837
|
+
|
|
838
|
+
// Create UOW and create both user and post in same transaction
|
|
839
|
+
const uow = queryEngine.createUnitOfWork("create-user-and-post");
|
|
840
|
+
|
|
841
|
+
// Create user and capture the returned ID
|
|
842
|
+
const userId = uow.create("users", {
|
|
843
|
+
name: "UOW Test User",
|
|
844
|
+
age: 35,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// Use the returned FragnoId directly to create a post in the same transaction
|
|
848
|
+
// The compiler will extract externalId and generate a subquery to lookup the internal ID
|
|
849
|
+
const postId = uow.create("posts", {
|
|
850
|
+
user_id: userId,
|
|
851
|
+
title: "UOW Test Post",
|
|
852
|
+
content: "This post was created in the same transaction as the user",
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// Execute all mutations in a single transaction
|
|
856
|
+
const { success } = await uow.executeMutations();
|
|
857
|
+
expect(success).toBe(true);
|
|
858
|
+
|
|
859
|
+
// Verify both records were created
|
|
860
|
+
const user = await queryEngine.findFirst("users", (b) =>
|
|
861
|
+
b.whereIndex("primary", (eb) => eb("id", "=", userId)),
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
expect(user?.name).toBe("UOW Test User");
|
|
865
|
+
expect(user?.age).toBe(35);
|
|
866
|
+
|
|
867
|
+
const post = await queryEngine.findFirst("posts", (b) =>
|
|
868
|
+
b.whereIndex("primary", (eb) => eb("id", "=", postId.externalId)),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
expect(post?.title).toBe("UOW Test Post");
|
|
872
|
+
expect(post?.content).toBe("This post was created in the same transaction as the user");
|
|
873
|
+
|
|
874
|
+
// Verify the foreign key relationship is correct
|
|
875
|
+
expect(post?.user_id.internalId).toBe(user?.id.internalId);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it("should support cursor-based pagination with findWithCursor()", async () => {
|
|
879
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
880
|
+
|
|
881
|
+
// Create multiple users for pagination testing with unique prefix
|
|
882
|
+
const prefix = "CursorPagTest";
|
|
883
|
+
const userIds: FragnoId[] = [];
|
|
884
|
+
for (let i = 1; i <= 25; i++) {
|
|
885
|
+
const userId = await queryEngine.create("users", {
|
|
886
|
+
name: `${prefix} ${i.toString().padStart(2, "0")}`,
|
|
887
|
+
age: 20 + i,
|
|
888
|
+
});
|
|
889
|
+
userIds.push(userId);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Fetch first page with cursor (filter by prefix to avoid other test data)
|
|
893
|
+
const firstPage = await queryEngine.findWithCursor("users", (b) =>
|
|
894
|
+
b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(10),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
// Check structure
|
|
898
|
+
expect(firstPage).toHaveProperty("items");
|
|
899
|
+
expect(firstPage).toHaveProperty("cursor");
|
|
900
|
+
expect(Array.isArray(firstPage.items)).toBe(true);
|
|
901
|
+
expect(firstPage.items.length).toBeGreaterThan(0);
|
|
902
|
+
|
|
903
|
+
assert(firstPage.cursor instanceof Cursor);
|
|
904
|
+
|
|
905
|
+
// Fetch second page using cursor
|
|
906
|
+
const secondPage = await queryEngine.findWithCursor("users", (b) =>
|
|
907
|
+
b
|
|
908
|
+
.whereIndex("name_idx")
|
|
909
|
+
.after(firstPage.cursor!)
|
|
910
|
+
.orderByIndex("name_idx", "asc")
|
|
911
|
+
.pageSize(10),
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
expect(secondPage.items.length).toBeGreaterThan(0);
|
|
915
|
+
|
|
916
|
+
// Verify no overlap - all names in second page should be different from first page
|
|
917
|
+
const firstPageNames = new Set(firstPage.items.map((u) => u.name));
|
|
918
|
+
const secondPageNames = secondPage.items.map((u) => u.name);
|
|
919
|
+
|
|
920
|
+
for (const name of secondPageNames) {
|
|
921
|
+
expect(firstPageNames.has(name)).toBe(false);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Verify ordering - last item of first page should come before first item of second page
|
|
925
|
+
const firstPageLast = firstPage.items[firstPage.items.length - 1].name;
|
|
926
|
+
const secondPageFirst = secondPage.items[0].name;
|
|
927
|
+
expect(firstPageLast < secondPageFirst).toBe(true);
|
|
928
|
+
|
|
929
|
+
// Verify our test data is present
|
|
930
|
+
const testUsers = await queryEngine.find("users", (b) =>
|
|
931
|
+
b.whereIndex("name_idx").pageSize(100),
|
|
932
|
+
);
|
|
933
|
+
const testUserNames = testUsers.filter((u) => u.name.startsWith(prefix)).map((u) => u.name);
|
|
934
|
+
expect(testUserNames).toHaveLength(25);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("should support findWithCursor() in Unit of Work", async () => {
|
|
938
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
939
|
+
|
|
940
|
+
// Create test users if not already present
|
|
941
|
+
const existingUsers = await queryEngine.find("users", (b) =>
|
|
942
|
+
b.whereIndex("name_idx").pageSize(1),
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (existingUsers.length === 0) {
|
|
946
|
+
for (let i = 1; i <= 5; i++) {
|
|
947
|
+
await queryEngine.create("users", {
|
|
948
|
+
name: `UOW Cursor User ${i}`,
|
|
949
|
+
age: 30 + i,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Use findWithCursor in UOW
|
|
955
|
+
const uow = queryEngine
|
|
956
|
+
.createUnitOfWork("cursor-test")
|
|
957
|
+
.findWithCursor("users", (b) =>
|
|
958
|
+
b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(3),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
const [result] = await uow.executeRetrieve();
|
|
962
|
+
|
|
963
|
+
// Verify result structure
|
|
964
|
+
expect(result).toHaveProperty("items");
|
|
965
|
+
expect(result).toHaveProperty("cursor");
|
|
966
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
967
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
968
|
+
|
|
969
|
+
if (result.items.length === 3) {
|
|
970
|
+
expect(result.cursor).toBeInstanceOf(Cursor);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
787
973
|
});
|
|
@@ -10,7 +10,7 @@ import type { AnySchema } from "../../schema/create";
|
|
|
10
10
|
import type { CustomOperation, MigrationOperation } from "../../migration-engine/shared";
|
|
11
11
|
import { execute, preprocessOperations } from "./migration/execute";
|
|
12
12
|
import type { AbstractQuery } from "../../query/query";
|
|
13
|
-
import { fromKysely } from "./kysely-query";
|
|
13
|
+
import { fromKysely, type KyselyUOWConfig } from "./kysely-query";
|
|
14
14
|
import { createTableNameMapper } from "./kysely-shared";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import { SETTINGS_TABLE_NAME } from "../../shared/settings-schema";
|
|
@@ -25,7 +25,7 @@ export interface KyselyConfig {
|
|
|
25
25
|
provider: SQLProvider;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export class KyselyAdapter implements DatabaseAdapter {
|
|
28
|
+
export class KyselyAdapter implements DatabaseAdapter<KyselyUOWConfig> {
|
|
29
29
|
#connectionPool: ConnectionPool<KyselyAny>;
|
|
30
30
|
#provider: SQLProvider;
|
|
31
31
|
|
|
@@ -46,7 +46,10 @@ export class KyselyAdapter implements DatabaseAdapter {
|
|
|
46
46
|
await this.#connectionPool.close();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
createQueryEngine<T extends AnySchema>(
|
|
49
|
+
createQueryEngine<T extends AnySchema>(
|
|
50
|
+
schema: T,
|
|
51
|
+
namespace: string,
|
|
52
|
+
): AbstractQuery<T, KyselyUOWConfig> {
|
|
50
53
|
// Only create mapper if namespace is non-empty
|
|
51
54
|
const mapper = namespace ? createTableNameMapper(namespace) : undefined;
|
|
52
55
|
return fromKysely(schema, this.#connectionPool, this.#provider, mapper);
|