@fragno-dev/db 0.1.12 → 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 (49) hide show
  1. package/.turbo/turbo-build.log +27 -27
  2. package/CHANGELOG.md +6 -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 +4 -0
  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 +1 -1
  14. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  15. package/dist/adapters/kysely/kysely-query.js +29 -1
  16. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  17. package/dist/adapters/kysely/kysely-uow-compiler.js +2 -1
  18. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  19. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  20. package/dist/mod.d.ts +2 -1
  21. package/dist/mod.d.ts.map +1 -1
  22. package/dist/mod.js +2 -1
  23. package/dist/mod.js.map +1 -1
  24. package/dist/query/cursor.d.ts +67 -32
  25. package/dist/query/cursor.d.ts.map +1 -1
  26. package/dist/query/cursor.js +84 -31
  27. package/dist/query/cursor.js.map +1 -1
  28. package/dist/query/query.d.ts +5 -0
  29. package/dist/query/query.d.ts.map +1 -1
  30. package/dist/query/unit-of-work.d.ts +14 -4
  31. package/dist/query/unit-of-work.d.ts.map +1 -1
  32. package/dist/query/unit-of-work.js +52 -9
  33. package/dist/query/unit-of-work.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +72 -5
  36. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +6 -4
  37. package/src/adapters/drizzle/drizzle-query.ts +9 -0
  38. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +19 -3
  39. package/src/adapters/drizzle/drizzle-uow-compiler.ts +3 -2
  40. package/src/adapters/drizzle/drizzle-uow-decoder.ts +40 -1
  41. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +102 -4
  42. package/src/adapters/kysely/kysely-query.ts +50 -1
  43. package/src/adapters/kysely/kysely-uow-compiler.test.ts +19 -3
  44. package/src/adapters/kysely/kysely-uow-compiler.ts +3 -2
  45. package/src/mod.ts +6 -1
  46. package/src/query/cursor.test.ts +113 -68
  47. package/src/query/cursor.ts +127 -36
  48. package/src/query/query.ts +19 -0
  49. package/src/query/unit-of-work.ts +133 -15
@@ -1,10 +1,10 @@
1
1
  import { drizzle } from "drizzle-orm/pglite";
2
2
  import { DrizzleAdapter } from "./drizzle-adapter";
3
- import { beforeAll, describe, expect, expectTypeOf, it } from "vitest";
3
+ import { assert, 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";
7
- import { encodeCursor } from "../../query/cursor";
7
+ import { Cursor } from "../../query/cursor";
8
8
  import type { DrizzleCompiledQuery } from "./drizzle-uow-compiler";
9
9
  import { writeAndLoadSchema } from "./test-utils";
10
10
 
@@ -245,10 +245,12 @@ describe("DrizzleAdapter PGLite", () => {
245
245
 
246
246
  // Create cursor from last item of first page
247
247
  const lastItem = firstPage[firstPage.length - 1]!;
248
- const cursor = encodeCursor({
248
+ const cursor = new Cursor({
249
+ indexName: "name_idx",
250
+ orderDirection: "asc",
251
+ pageSize: 2,
249
252
  indexValues: { name: lastItem.name },
250
- direction: "forward",
251
- });
253
+ }).encode();
252
254
 
253
255
  // Fetch next page using cursor
254
256
  const [secondPage] = await queryEngine
@@ -729,4 +731,69 @@ describe("DrizzleAdapter PGLite", () => {
729
731
  "This post was created in the same transaction as the user",
730
732
  ]);
731
733
  });
734
+
735
+ it("should support cursor-based pagination with findWithCursor()", async () => {
736
+ const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
737
+
738
+ // Create multiple users for pagination testing
739
+ for (let i = 1; i <= 25; i++) {
740
+ await queryEngine.create("users", {
741
+ name: `Cursor User ${i.toString().padStart(2, "0")}`,
742
+ age: 20 + i,
743
+ });
744
+ }
745
+
746
+ // Fetch first page with cursor
747
+ const firstPage = await queryEngine.findWithCursor("users", (b) =>
748
+ b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(10),
749
+ );
750
+
751
+ // Check structure
752
+ expect(firstPage).toHaveProperty("items");
753
+ expect(firstPage).toHaveProperty("cursor");
754
+ 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
+ expect(firstPage.items).toHaveLength(10);
760
+ expect(firstPage.cursor).toBeInstanceOf(Cursor);
761
+
762
+ // Fetch second page using cursor
763
+ const secondPage = await queryEngine.findWithCursor("users", (b) =>
764
+ b
765
+ .whereIndex("name_idx")
766
+ .after(firstPage.cursor!)
767
+ .orderByIndex("name_idx", "asc")
768
+ .pageSize(10),
769
+ );
770
+
771
+ expect(secondPage.items.length).toBeGreaterThan(0);
772
+ expect(secondPage.items.length).toBeLessThanOrEqual(10);
773
+
774
+ // Verify no overlap - first item of second page should come after last item of first page
775
+ const firstPageLastName = firstPage.items[firstPage.items.length - 1].name;
776
+ const secondPageFirstName = secondPage.items[0].name;
777
+ expect(secondPageFirstName > firstPageLastName).toBe(true);
778
+ });
779
+
780
+ it("should support findWithCursor() in Unit of Work", async () => {
781
+ const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
782
+
783
+ // Use findWithCursor in UOW
784
+ const uow = queryEngine
785
+ .createUnitOfWork("cursor-test")
786
+ .findWithCursor("users", (b) =>
787
+ b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(5),
788
+ );
789
+
790
+ const [result] = await uow.executeRetrieve();
791
+
792
+ // Verify result structure
793
+ expect(result).toHaveProperty("items");
794
+ expect(result).toHaveProperty("cursor");
795
+ expect(Array.isArray(result.items)).toBe(true);
796
+ expect(result.items).toHaveLength(5);
797
+ expect(result.cursor).toBeInstanceOf(Cursor);
798
+ });
732
799
  });
@@ -6,7 +6,7 @@ import { column, idColumn, referenceColumn, schema } 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
- import { encodeCursor } from "../../query/cursor";
9
+ import { Cursor } from "../../query/cursor";
10
10
 
11
11
  // Import drizzle-kit for migrations
12
12
  const require = createRequire(import.meta.url);
@@ -272,10 +272,12 @@ describe("DrizzleAdapter SQLite", () => {
272
272
 
273
273
  // Create cursor from last item of first page
274
274
  const lastItem = firstPage[firstPage.length - 1]!;
275
- const cursor = encodeCursor({
275
+ const cursor = new Cursor({
276
+ indexName: "name_idx",
277
+ orderDirection: "asc",
278
+ pageSize: 2,
276
279
  indexValues: { name: lastItem.name },
277
- direction: "forward",
278
- });
280
+ }).encode();
279
281
 
280
282
  // Fetch next page using cursor
281
283
  const [secondPage] = await queryEngine
@@ -8,6 +8,7 @@ import { parseDrizzle, type DrizzleResult, type TableNameMapper, type DBType } f
8
8
  import { createDrizzleUOWDecoder } from "./drizzle-uow-decoder";
9
9
  import type { ConnectionPool } from "../../shared/connection-pool";
10
10
  import type { TableToUpdateValues } from "../../query/query";
11
+ import type { CursorResult } from "../../query/cursor";
11
12
 
12
13
  /**
13
14
  * Configuration options for creating a Drizzle Unit of Work
@@ -153,6 +154,14 @@ export function fromDrizzle<T extends AnySchema>(
153
154
  return result;
154
155
  },
155
156
 
157
+ async findWithCursor(tableName, builderFn) {
158
+ // Safe: builderFn returns a FindBuilder, which matches UnitOfWork signature
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ const uow = createUOW({ config: uowConfig }).findWithCursor(tableName, builderFn as any);
161
+ const [result] = await uow.executeRetrieve();
162
+ return result as CursorResult<unknown>;
163
+ },
164
+
156
165
  async findFirst(tableName, builderFn) {
157
166
  const uow = createUOW({ config: uowConfig });
158
167
  if (builderFn) {
@@ -14,6 +14,7 @@ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
14
14
  import { writeAndLoadSchema } from "./test-utils";
15
15
  import type { ConnectionPool } from "../../shared/connection-pool";
16
16
  import { createDrizzleConnectionPool } from "./drizzle-connection-pool";
17
+ import { Cursor } from "../../query/cursor";
17
18
 
18
19
  /**
19
20
  * Integration tests for Drizzle UOW compiler and executor.
@@ -233,7 +234,12 @@ describe("drizzle-uow-compiler", () => {
233
234
 
234
235
  it("should compile find operation with cursor pagination using after", () => {
235
236
  const uow = createTestUOW();
236
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9"; // {"indexValues":{"name":"Alice"},"direction":"forward"}
237
+ const cursor = new Cursor({
238
+ indexName: "idx_name",
239
+ orderDirection: "asc",
240
+ pageSize: 10,
241
+ indexValues: { name: "Alice" },
242
+ });
237
243
  uow.find("users", (b) =>
238
244
  b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
239
245
  );
@@ -250,7 +256,12 @@ describe("drizzle-uow-compiler", () => {
250
256
 
251
257
  it("should compile find operation with cursor pagination using before", () => {
252
258
  const uow = createTestUOW();
253
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQm9iIn0sImRpcmVjdGlvbiI6ImJhY2t3YXJkIn0="; // {"indexValues":{"name":"Bob"},"direction":"backward"}
259
+ const cursor = new Cursor({
260
+ indexName: "idx_name",
261
+ orderDirection: "desc",
262
+ pageSize: 10,
263
+ indexValues: { name: "Bob" },
264
+ });
254
265
  uow.find("users", (b) =>
255
266
  b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
256
267
  );
@@ -267,7 +278,12 @@ describe("drizzle-uow-compiler", () => {
267
278
 
268
279
  it("should compile find operation with cursor pagination and additional where conditions", () => {
269
280
  const uow = createTestUOW();
270
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9";
281
+ const cursor = new Cursor({
282
+ indexName: "idx_name",
283
+ orderDirection: "asc",
284
+ pageSize: 5,
285
+ indexValues: { name: "Alice" },
286
+ });
271
287
  uow.find("users", (b) =>
272
288
  b
273
289
  .whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
@@ -423,8 +423,9 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
423
423
  // Add cursor-based pagination conditions
424
424
  if ((after || before) && indexColumns.length > 0) {
425
425
  const cursor = after || before;
426
- const cursorData = decodeCursor(cursor!);
427
- const serializedValues = serializeCursorValues(cursorData, indexColumns, provider);
426
+ // Decode cursor if it's a string, otherwise use it as-is
427
+ const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
428
+ const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
428
429
 
429
430
  // Build tuple comparison for cursor pagination
430
431
  // For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
@@ -4,6 +4,7 @@ import type { RetrievalOperation, UOWDecoder } from "../../query/unit-of-work";
4
4
  import { decodeResult } from "../../query/result-transform";
5
5
  import { getOrderedJoinColumns } from "./join-column-utils";
6
6
  import type { DrizzleResult } from "./shared";
7
+ import { createCursorFromRecord, Cursor, type CursorResult } from "../../query/cursor";
7
8
 
8
9
  /**
9
10
  * Join information with nested join support
@@ -173,10 +174,48 @@ export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
173
174
  }
174
175
 
175
176
  // Handle find operations - decode each row
176
- return result.rows.map((row) => {
177
+ const decodedRows = result.rows.map((row) => {
177
178
  const transformedRow = transformJoinArraysToObjects(row, op, provider);
178
179
  return decodeResult(transformedRow, op.table, provider);
179
180
  });
181
+
182
+ // If cursor generation is requested, wrap in CursorResult
183
+ if (op.withCursor) {
184
+ let cursor: Cursor | undefined;
185
+
186
+ // Generate cursor from last item if results exist
187
+ if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
188
+ const lastItem = decodedRows[decodedRows.length - 1];
189
+ const indexName = op.options.orderByIndex.indexName;
190
+
191
+ // Get index columns
192
+ let indexColumns;
193
+ if (indexName === "_primary") {
194
+ indexColumns = [op.table.getIdColumn()];
195
+ } else {
196
+ const index = op.table.indexes[indexName];
197
+ if (index) {
198
+ indexColumns = index.columns;
199
+ }
200
+ }
201
+
202
+ if (indexColumns && lastItem) {
203
+ cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
204
+ indexName: op.options.orderByIndex.indexName,
205
+ orderDirection: op.options.orderByIndex.direction,
206
+ pageSize: op.options.pageSize,
207
+ });
208
+ }
209
+ }
210
+
211
+ const cursorResult: CursorResult<unknown> = {
212
+ items: decodedRows,
213
+ cursor,
214
+ };
215
+ return cursorResult;
216
+ }
217
+
218
+ return decodedRows;
180
219
  });
181
220
  };
182
221
  }
@@ -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
@@ -872,4 +874,100 @@ describe("KyselyAdapter PGLite", () => {
872
874
  // Verify the foreign key relationship is correct
873
875
  expect(post?.user_id.internalId).toBe(user?.id.internalId);
874
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
+ });
875
973
  });
@@ -14,6 +14,7 @@ import type { CompiledQuery, Kysely } from "kysely";
14
14
  import type { TableNameMapper } from "./kysely-shared";
15
15
  import type { ConnectionPool } from "../../shared/connection-pool";
16
16
  import type { SQLProvider } from "../../shared/providers";
17
+ import { createCursorFromRecord, Cursor, type CursorResult } from "../../query/cursor";
17
18
 
18
19
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
20
  type KyselyAny = Kysely<any>;
@@ -159,7 +160,45 @@ export function fromKysely<T extends AnySchema>(
159
160
 
160
161
  // Each result is an array of rows - decode each row
161
162
  const rowArray = rows as Record<string, unknown>[];
162
- return rowArray.map((row) => decodeResult(row, op.table, provider));
163
+ const decodedRows = rowArray.map((row) => decodeResult(row, op.table, provider));
164
+
165
+ // If cursor generation is requested, wrap in CursorResult
166
+ if (op.withCursor) {
167
+ let cursor: Cursor | undefined;
168
+
169
+ // Generate cursor from last item if results exist
170
+ if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
171
+ const lastItem = decodedRows[decodedRows.length - 1];
172
+ const indexName = op.options.orderByIndex.indexName;
173
+
174
+ // Get index columns
175
+ let indexColumns;
176
+ if (indexName === "_primary") {
177
+ indexColumns = [op.table.getIdColumn()];
178
+ } else {
179
+ const index = op.table.indexes[indexName];
180
+ if (index) {
181
+ indexColumns = index.columns;
182
+ }
183
+ }
184
+
185
+ if (indexColumns && lastItem) {
186
+ cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
187
+ indexName: op.options.orderByIndex.indexName,
188
+ orderDirection: op.options.orderByIndex.direction,
189
+ pageSize: op.options.pageSize,
190
+ });
191
+ }
192
+ }
193
+
194
+ const result: CursorResult<unknown> = {
195
+ items: decodedRows,
196
+ cursor,
197
+ };
198
+ return result;
199
+ }
200
+
201
+ return decodedRows;
163
202
  });
164
203
  };
165
204
 
@@ -193,6 +232,16 @@ export function fromKysely<T extends AnySchema>(
193
232
  return result ?? [];
194
233
  },
195
234
 
235
+ async findWithCursor(tableName, builderFn) {
236
+ // Safe: builderFn returns a FindBuilder, which matches UnitOfWork signature
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ const uow = createUOW({ config: uowConfig }).findWithCursor(tableName, builderFn as any);
239
+ // executeRetrieve returns an array of results (one per find operation)
240
+ // Since we only have one findWithCursor, unwrap the first result
241
+ const [result] = await uow.executeRetrieve();
242
+ return result as CursorResult<unknown>;
243
+ },
244
+
196
245
  async findFirst(tableName, builderFn) {
197
246
  const uow = createUOW({ config: uowConfig });
198
247
  if (builderFn) {
@@ -5,6 +5,7 @@ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
5
5
  import { createKyselyUOWCompiler } from "./kysely-uow-compiler";
6
6
  import type { ConnectionPool } from "../../shared/connection-pool";
7
7
  import { createKyselyConnectionPool } from "./kysely-connection-pool";
8
+ import { Cursor } from "../../query/cursor";
8
9
 
9
10
  describe("kysely-uow-compiler", () => {
10
11
  const testSchema = schema((s) => {
@@ -252,7 +253,12 @@ describe("kysely-uow-compiler", () => {
252
253
 
253
254
  it("should compile find operation with cursor pagination using after", () => {
254
255
  const uow = createTestUOW();
255
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9"; // {"indexValues":{"name":"Alice"},"direction":"forward"}
256
+ const cursor = new Cursor({
257
+ indexName: "idx_name",
258
+ orderDirection: "asc",
259
+ pageSize: 10,
260
+ indexValues: { name: "Alice" },
261
+ });
256
262
  uow.find("users", (b) =>
257
263
  b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
258
264
  );
@@ -269,7 +275,12 @@ describe("kysely-uow-compiler", () => {
269
275
 
270
276
  it("should compile find operation with cursor pagination using before", () => {
271
277
  const uow = createTestUOW();
272
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQm9iIn0sImRpcmVjdGlvbiI6ImJhY2t3YXJkIn0="; // {"indexValues":{"name":"Bob"},"direction":"backward"}
278
+ const cursor = new Cursor({
279
+ indexName: "idx_name",
280
+ orderDirection: "desc",
281
+ pageSize: 10,
282
+ indexValues: { name: "Bob" },
283
+ });
273
284
  uow.find("users", (b) =>
274
285
  b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
275
286
  );
@@ -286,7 +297,12 @@ describe("kysely-uow-compiler", () => {
286
297
 
287
298
  it("should compile find operation with cursor pagination and additional where conditions", () => {
288
299
  const uow = createTestUOW();
289
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9";
300
+ const cursor = new Cursor({
301
+ indexName: "idx_name",
302
+ orderDirection: "asc",
303
+ pageSize: 5,
304
+ indexValues: { name: "Alice" },
305
+ });
290
306
  uow.find("users", (b) =>
291
307
  b
292
308
  .whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
@@ -105,8 +105,9 @@ export function createKyselyUOWCompiler<TSchema extends AnySchema>(
105
105
 
106
106
  if ((after || before) && indexColumns.length > 0) {
107
107
  const cursor = after || before;
108
- const cursorData = decodeCursor(cursor!);
109
- const serializedValues = serializeCursorValues(cursorData, indexColumns, provider);
108
+ // Decode cursor if it's a string, otherwise use it as-is
109
+ const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
110
+ const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
110
111
 
111
112
  // Build tuple comparison for cursor pagination
112
113
  // For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
package/src/mod.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import type { DatabaseAdapter } from "./adapters/adapters";
2
2
  import type { AnySchema } from "./schema/create";
3
3
  import type { AbstractQuery } from "./query/query";
4
+ import type { CursorResult } from "./query/cursor";
5
+ import { Cursor } from "./query/cursor";
4
6
 
5
- export type { DatabaseAdapter };
7
+ export type { DatabaseAdapter, CursorResult };
8
+ export { Cursor };
6
9
 
7
10
  export const fragnoDatabaseFakeSymbol = "$fragno-database" as const;
8
11
  export const fragnoDatabaseLibraryVersion = "0.1" as const;
@@ -129,3 +132,5 @@ export {
129
132
  type FragnoPublicConfigWithDatabase,
130
133
  type DatabaseFragmentContext,
131
134
  } from "./fragment";
135
+
136
+ export { decodeCursor, type CursorData } from "./query/cursor";