@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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +41 -39
  2. package/CHANGELOG.md +19 -0
  3. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  4. package/dist/adapters/drizzle/drizzle-adapter.js +1 -1
  5. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-query.js +42 -34
  7. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-uow-compiler.js +2 -1
  9. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-uow-decoder.js +25 -1
  11. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  12. package/dist/adapters/drizzle/generate.js +1 -1
  13. package/dist/adapters/kysely/kysely-adapter.d.ts +4 -3
  14. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  15. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  16. package/dist/adapters/kysely/kysely-query.d.ts +22 -0
  17. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
  18. package/dist/adapters/kysely/kysely-query.js +101 -51
  19. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  20. package/dist/adapters/kysely/kysely-uow-compiler.js +2 -1
  21. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  22. package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
  23. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  24. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  25. package/dist/migration-engine/generation-engine.d.ts +1 -1
  26. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  27. package/dist/migration-engine/generation-engine.js.map +1 -1
  28. package/dist/mod.d.ts +7 -6
  29. package/dist/mod.d.ts.map +1 -1
  30. package/dist/mod.js +2 -1
  31. package/dist/mod.js.map +1 -1
  32. package/dist/query/cursor.d.ts +67 -32
  33. package/dist/query/cursor.d.ts.map +1 -1
  34. package/dist/query/cursor.js +84 -31
  35. package/dist/query/cursor.js.map +1 -1
  36. package/dist/query/query.d.ts +29 -8
  37. package/dist/query/query.d.ts.map +1 -1
  38. package/dist/query/result-transform.js +17 -5
  39. package/dist/query/result-transform.js.map +1 -1
  40. package/dist/query/unit-of-work.d.ts +19 -8
  41. package/dist/query/unit-of-work.d.ts.map +1 -1
  42. package/dist/query/unit-of-work.js +54 -12
  43. package/dist/query/unit-of-work.js.map +1 -1
  44. package/dist/schema/serialize.js +2 -0
  45. package/dist/schema/serialize.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +242 -55
  48. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +95 -39
  49. package/src/adapters/drizzle/drizzle-query.test.ts +54 -4
  50. package/src/adapters/drizzle/drizzle-query.ts +74 -60
  51. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +82 -6
  52. package/src/adapters/drizzle/drizzle-uow-compiler.ts +3 -2
  53. package/src/adapters/drizzle/drizzle-uow-decoder.ts +40 -1
  54. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +190 -4
  55. package/src/adapters/kysely/kysely-adapter.ts +6 -3
  56. package/src/adapters/kysely/kysely-query.test.ts +498 -0
  57. package/src/adapters/kysely/kysely-query.ts +187 -83
  58. package/src/adapters/kysely/kysely-uow-compiler.test.ts +85 -3
  59. package/src/adapters/kysely/kysely-uow-compiler.ts +3 -2
  60. package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
  61. package/src/migration-engine/generation-engine.ts +2 -1
  62. package/src/mod.ts +12 -7
  63. package/src/query/cursor.test.ts +113 -68
  64. package/src/query/cursor.ts +127 -36
  65. package/src/query/query-type.test.ts +34 -14
  66. package/src/query/query.ts +94 -34
  67. package/src/query/result-transform.test.ts +5 -5
  68. package/src/query/result-transform.ts +29 -11
  69. package/src/query/unit-of-work.ts +141 -26
  70. package/src/schema/serialize.test.ts +223 -0
  71. 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 { encodeCursor } from "../../query/cursor";
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 = encodeCursor({
399
+ const cursor = new Cursor({
400
+ indexName: "name_idx",
401
+ orderDirection: "asc",
402
+ pageSize: 2,
400
403
  indexValues: { name: lastItem.name },
401
- direction: "forward",
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>(schema: T, namespace: string): AbstractQuery<T> {
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);