@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
@@ -1,85 +1,91 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import {
3
- encodeCursor,
4
- decodeCursor,
5
- createCursorFromRecord,
6
- serializeCursorValues,
7
- type CursorData,
8
- } from "./cursor";
2
+ import { decodeCursor, createCursorFromRecord, serializeCursorValues, Cursor } from "./cursor";
9
3
  import { column, idColumn, schema } from "../schema/create";
10
4
 
11
5
  describe("Cursor utilities", () => {
12
- describe("encodeCursor and decodeCursor", () => {
6
+ describe("Cursor class encode and decode", () => {
13
7
  it("should encode and decode a cursor with simple values", () => {
14
- const cursorData: CursorData = {
8
+ const cursor = new Cursor({
9
+ indexName: "idx_test",
10
+ orderDirection: "asc",
11
+ pageSize: 10,
15
12
  indexValues: { id: "user123" },
16
- direction: "forward",
17
- };
13
+ });
18
14
 
19
- const encoded = encodeCursor(cursorData);
15
+ const encoded = cursor.encode();
20
16
  expect(encoded).toBeTruthy();
21
17
  expect(typeof encoded).toBe("string");
22
18
 
23
19
  const decoded = decodeCursor(encoded);
24
- expect(decoded).toEqual(cursorData);
20
+ expect(decoded.indexName).toBe("idx_test");
21
+ expect(decoded.indexValues).toEqual({ id: "user123" });
25
22
  });
26
23
 
27
24
  it("should encode and decode a cursor with multiple index values", () => {
28
- const cursorData: CursorData = {
25
+ const cursor = new Cursor({
26
+ indexName: "idx_created",
27
+ orderDirection: "desc",
28
+ pageSize: 20,
29
29
  indexValues: {
30
30
  createdAt: 1234567890,
31
31
  id: "user123",
32
32
  name: "Alice",
33
33
  },
34
- direction: "backward",
35
- };
34
+ });
36
35
 
37
- const encoded = encodeCursor(cursorData);
36
+ const encoded = cursor.encode();
38
37
  const decoded = decodeCursor(encoded);
39
- expect(decoded).toEqual(cursorData);
38
+ expect(decoded.indexValues).toEqual(cursor.indexValues);
39
+ expect(decoded.pageSize).toBe(20);
40
40
  });
41
41
 
42
42
  it("should handle different data types in index values", () => {
43
- const cursorData: CursorData = {
43
+ const cursor = new Cursor({
44
+ indexName: "_primary",
45
+ orderDirection: "asc",
46
+ pageSize: 10,
44
47
  indexValues: {
45
48
  stringValue: "test",
46
49
  numberValue: 42,
47
50
  boolValue: true,
48
51
  nullValue: null,
49
52
  },
50
- direction: "forward",
51
- };
53
+ });
52
54
 
53
- const encoded = encodeCursor(cursorData);
55
+ const encoded = cursor.encode();
54
56
  const decoded = decodeCursor(encoded);
55
- expect(decoded).toEqual(cursorData);
57
+ expect(decoded.indexValues).toEqual(cursor.indexValues);
56
58
  });
57
59
 
58
60
  it("should produce base64-encoded strings", () => {
59
- const cursorData: CursorData = {
61
+ const cursor = new Cursor({
62
+ indexName: "_primary",
63
+ orderDirection: "asc",
64
+ pageSize: 10,
60
65
  indexValues: { id: "test" },
61
- direction: "forward",
62
- };
66
+ });
63
67
 
64
- const encoded = encodeCursor(cursorData);
68
+ const encoded = cursor.encode();
65
69
 
66
70
  // Base64 pattern - should only contain valid base64 characters
67
71
  expect(encoded).toMatch(/^[A-Za-z0-9+/]+=*$/);
68
72
 
69
73
  // Should be decodeable
70
74
  const decoded = decodeCursor(encoded);
71
- expect(decoded).toEqual(cursorData);
75
+ expect(decoded.indexValues).toEqual(cursor.indexValues);
72
76
  });
73
77
 
74
78
  it("should handle empty index values", () => {
75
- const cursorData: CursorData = {
79
+ const cursor = new Cursor({
80
+ indexName: "_primary",
81
+ orderDirection: "asc",
82
+ pageSize: 10,
76
83
  indexValues: {},
77
- direction: "forward",
78
- };
84
+ });
79
85
 
80
- const encoded = encodeCursor(cursorData);
86
+ const encoded = cursor.encode();
81
87
  const decoded = decodeCursor(encoded);
82
- expect(decoded).toEqual(cursorData);
88
+ expect(decoded.indexValues).toEqual({});
83
89
  });
84
90
  });
85
91
 
@@ -95,25 +101,42 @@ describe("Cursor utilities", () => {
95
101
  });
96
102
 
97
103
  it("should throw error for missing indexValues", () => {
98
- const invalidData = { direction: "forward" };
104
+ const invalidData = { v: 1, indexName: "test", orderDirection: "asc", pageSize: 10 };
99
105
  const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
100
106
  expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
101
107
  });
102
108
 
103
- it("should throw error for missing direction", () => {
104
- const invalidData = { indexValues: { id: "test" } };
109
+ it("should throw error for missing indexName", () => {
110
+ const invalidData = {
111
+ v: 1,
112
+ indexValues: { id: "test" },
113
+ orderDirection: "asc",
114
+ pageSize: 10,
115
+ };
105
116
  const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
106
117
  expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
107
118
  });
108
119
 
109
- it("should throw error for invalid direction value", () => {
110
- const invalidData = { indexValues: { id: "test" }, direction: "sideways" };
120
+ it("should throw error for invalid orderDirection value", () => {
121
+ const invalidData = {
122
+ v: 1,
123
+ indexValues: { id: "test" },
124
+ indexName: "test",
125
+ orderDirection: "sideways",
126
+ pageSize: 10,
127
+ };
111
128
  const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
112
129
  expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
113
130
  });
114
131
 
115
132
  it("should throw error for non-object indexValues", () => {
116
- const invalidData = { indexValues: "not an object", direction: "forward" };
133
+ const invalidData = {
134
+ v: 1,
135
+ indexValues: "not an object",
136
+ indexName: "test",
137
+ orderDirection: "asc",
138
+ pageSize: 10,
139
+ };
117
140
  const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
118
141
  expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
119
142
  });
@@ -131,13 +154,16 @@ describe("Cursor utilities", () => {
131
154
  const record = { id: "user123", name: "Alice" };
132
155
  const indexColumns = [table.columns.id];
133
156
 
134
- const cursor = createCursorFromRecord(record, indexColumns, "forward");
157
+ const cursor = createCursorFromRecord(record, indexColumns, {
158
+ indexName: "_primary",
159
+ orderDirection: "asc",
160
+ pageSize: 10,
161
+ });
135
162
 
136
- expect(typeof cursor).toBe("string");
163
+ expect(cursor).toBeInstanceOf(Cursor);
137
164
 
138
- const decoded = decodeCursor(cursor);
165
+ const decoded = decodeCursor(cursor.encode());
139
166
  expect(decoded.indexValues).toEqual({ id: "user123" });
140
- expect(decoded.direction).toBe("forward");
141
167
  });
142
168
 
143
169
  it("should create a cursor from a record with multi-column index", () => {
@@ -163,14 +189,17 @@ describe("Cursor utilities", () => {
163
189
  const index = table.indexes.created_user;
164
190
  const indexColumns = index.columns;
165
191
 
166
- const cursor = createCursorFromRecord(record, indexColumns, "backward");
192
+ const cursor = createCursorFromRecord(record, indexColumns, {
193
+ indexName: "created_user",
194
+ orderDirection: "desc",
195
+ pageSize: 10,
196
+ });
167
197
 
168
- const decoded = decodeCursor(cursor);
198
+ const decoded = decodeCursor(cursor.encode());
169
199
  expect(decoded.indexValues).toEqual({
170
200
  createdAt: 1234567890,
171
201
  userId: "user456",
172
202
  });
173
- expect(decoded.direction).toBe("backward");
174
203
  });
175
204
 
176
205
  it("should only include columns that are in the index", () => {
@@ -187,9 +216,13 @@ describe("Cursor utilities", () => {
187
216
  const record = { id: "user123", name: "Alice", email: "alice@example.com" };
188
217
  const indexColumns = [table.columns.id];
189
218
 
190
- const cursor = createCursorFromRecord(record, indexColumns, "forward");
219
+ const cursor = createCursorFromRecord(record, indexColumns, {
220
+ indexName: "_primary",
221
+ orderDirection: "asc",
222
+ pageSize: 10,
223
+ });
191
224
 
192
- const decoded = decodeCursor(cursor);
225
+ const decoded = decodeCursor(cursor.encode());
193
226
  expect(decoded.indexValues).toEqual({ id: "user123" });
194
227
  expect(Object.keys(decoded.indexValues)).toHaveLength(1);
195
228
  });
@@ -204,13 +237,15 @@ describe("Cursor utilities", () => {
204
237
  );
205
238
 
206
239
  const table = testSchema.tables.users;
207
- const cursorData: CursorData = {
240
+ const cursor = new Cursor({
241
+ indexName: "_primary",
242
+ orderDirection: "asc",
243
+ pageSize: 10,
208
244
  indexValues: { id: "user123", age: 25 },
209
- direction: "forward",
210
- };
245
+ });
211
246
 
212
247
  const indexColumns = [table.columns.id, table.columns.age];
213
- const serialized = serializeCursorValues(cursorData, indexColumns, "postgresql");
248
+ const serialized = serializeCursorValues(cursor, indexColumns, "postgresql");
214
249
 
215
250
  expect(serialized).toHaveProperty("id", "user123");
216
251
  expect(serialized).toHaveProperty("age", 25);
@@ -224,13 +259,15 @@ describe("Cursor utilities", () => {
224
259
  );
225
260
 
226
261
  const table = testSchema.tables.users;
227
- const cursorData: CursorData = {
262
+ const cursor = new Cursor({
263
+ indexName: "_primary",
264
+ orderDirection: "asc",
265
+ pageSize: 10,
228
266
  indexValues: { id: "user123" },
229
- direction: "forward",
230
- };
267
+ });
231
268
 
232
269
  const indexColumns = [table.columns.id, table.columns.name];
233
- const serialized = serializeCursorValues(cursorData, indexColumns, "postgresql");
270
+ const serialized = serializeCursorValues(cursor, indexColumns, "postgresql");
234
271
 
235
272
  expect(serialized).toHaveProperty("id", "user123");
236
273
  expect(serialized).not.toHaveProperty("name");
@@ -260,10 +297,14 @@ describe("Cursor utilities", () => {
260
297
  const indexColumns = index.columns;
261
298
 
262
299
  // Create cursor from record
263
- const cursor = createCursorFromRecord(record, indexColumns, "forward");
300
+ const cursor = createCursorFromRecord(record, indexColumns, {
301
+ indexName: "trending",
302
+ orderDirection: "asc",
303
+ pageSize: 10,
304
+ });
264
305
 
265
306
  // Decode it
266
- const decoded = decodeCursor(cursor);
307
+ const decoded = decodeCursor(cursor.encode());
267
308
 
268
309
  // Serialize the values
269
310
  const serialized = serializeCursorValues(decoded, indexColumns, "postgresql");
@@ -273,24 +314,28 @@ describe("Cursor utilities", () => {
273
314
  expect(serialized["createdAt"]).toBe(1234567890);
274
315
  });
275
316
 
276
- it("should handle different directions correctly", () => {
277
- const cursorForward = encodeCursor({
317
+ it("should handle different order directions correctly", () => {
318
+ const cursorAsc = new Cursor({
319
+ indexName: "_primary",
320
+ orderDirection: "asc",
321
+ pageSize: 10,
278
322
  indexValues: { id: "test" },
279
- direction: "forward",
280
323
  });
281
324
 
282
- const cursorBackward = encodeCursor({
325
+ const cursorDesc = new Cursor({
326
+ indexName: "_primary",
327
+ orderDirection: "desc",
328
+ pageSize: 10,
283
329
  indexValues: { id: "test" },
284
- direction: "backward",
285
330
  });
286
331
 
287
- expect(cursorForward).not.toBe(cursorBackward);
332
+ expect(cursorAsc.encode()).not.toBe(cursorDesc.encode());
288
333
 
289
- const decodedForward = decodeCursor(cursorForward);
290
- const decodedBackward = decodeCursor(cursorBackward);
334
+ const decodedAsc = decodeCursor(cursorAsc.encode());
335
+ const decodedDesc = decodeCursor(cursorDesc.encode());
291
336
 
292
- expect(decodedForward.direction).toBe("forward");
293
- expect(decodedBackward.direction).toBe("backward");
337
+ expect(decodedAsc.orderDirection).toBe("asc");
338
+ expect(decodedDesc.orderDirection).toBe("desc");
294
339
  });
295
340
  });
296
341
  });
@@ -3,34 +3,98 @@ import { serialize } from "../schema/serialize";
3
3
  import type { SQLProvider } from "../shared/providers";
4
4
 
5
5
  /**
6
- * Cursor data structure containing index values and pagination direction
6
+ * Cursor object containing all information needed for pagination
7
7
  */
8
- export interface CursorData {
8
+ export class Cursor {
9
+ readonly #indexName: string;
10
+ readonly #orderDirection: "asc" | "desc";
11
+ readonly #pageSize: number;
12
+ readonly #indexValues: Record<string, unknown>;
13
+
14
+ constructor(data: {
15
+ indexName: string;
16
+ orderDirection: "asc" | "desc";
17
+ pageSize: number;
18
+ indexValues: Record<string, unknown>;
19
+ }) {
20
+ this.#indexName = data.indexName;
21
+ this.#orderDirection = data.orderDirection;
22
+ this.#pageSize = data.pageSize;
23
+ this.#indexValues = data.indexValues;
24
+ }
25
+
9
26
  /**
10
- * Values for each column in the index, keyed by column ORM name
27
+ * Get the index name being used for pagination
11
28
  */
12
- indexValues: Record<string, unknown>;
29
+ get indexName(): string {
30
+ return this.#indexName;
31
+ }
32
+
33
+ /**
34
+ * Get the ordering direction
35
+ */
36
+ get orderDirection(): "asc" | "desc" {
37
+ return this.#orderDirection;
38
+ }
39
+
13
40
  /**
14
- * Direction of pagination
41
+ * Get the page size
15
42
  */
16
- direction: "forward" | "backward";
43
+ get pageSize(): number {
44
+ return this.#pageSize;
45
+ }
46
+
47
+ /**
48
+ * Get the cursor position values
49
+ */
50
+ get indexValues(): Record<string, unknown> {
51
+ return this.#indexValues;
52
+ }
53
+
54
+ /**
55
+ * Encode cursor to an opaque base64 string (safe to send to client)
56
+ */
57
+ encode(): string {
58
+ const data: CursorData = {
59
+ v: 1,
60
+ indexName: this.#indexName,
61
+ orderDirection: this.#orderDirection,
62
+ pageSize: this.#pageSize,
63
+ indexValues: this.#indexValues,
64
+ };
65
+ return encodeCursorData(data);
66
+ }
17
67
  }
18
68
 
19
69
  /**
20
- * Encode cursor data to a base64 string
21
- *
22
- * @param data - The cursor data to encode
23
- * @returns Base64-encoded cursor string
24
- *
25
- * @example
26
- * ```ts
27
- * const cursor = encodeCursor({
28
- * indexValues: { id: "abc123", createdAt: 1234567890 },
29
- * direction: "forward"
30
- * });
31
- * ```
70
+ * Result of a cursor-based query containing items and pagination cursor
71
+ */
72
+ export interface CursorResult<T> {
73
+ /**
74
+ * The query results
75
+ */
76
+ items: T[];
77
+ /**
78
+ * Cursor to fetch the next page (undefined if no more results)
79
+ */
80
+ cursor?: Cursor;
81
+ }
82
+
83
+ /**
84
+ * Cursor data structure for serialization
85
+ */
86
+ export interface CursorData {
87
+ v: number; // version
88
+ indexName: string;
89
+ orderDirection: "asc" | "desc";
90
+ pageSize: number;
91
+ indexValues: Record<string, unknown>;
92
+ }
93
+
94
+ /**
95
+ * Encode cursor data to a base64 string (internal)
32
96
  */
33
- export function encodeCursor(data: CursorData): string {
97
+ function encodeCursorData(data: CursorData): string {
34
98
  const json = JSON.stringify(data);
35
99
  // Use Buffer in Node.js or btoa in browsers
36
100
  if (typeof Buffer !== "undefined") {
@@ -40,18 +104,18 @@ export function encodeCursor(data: CursorData): string {
40
104
  }
41
105
 
42
106
  /**
43
- * Decode a base64 cursor string back to cursor data
107
+ * Decode a base64 cursor string back to a Cursor object
44
108
  *
45
109
  * @param cursor - The base64-encoded cursor string
46
- * @returns Decoded cursor data
110
+ * @returns Decoded Cursor object
47
111
  * @throws Error if cursor is invalid or malformed
48
112
  *
49
113
  * @example
50
114
  * ```ts
51
- * const data = decodeCursor("eyJpbmRleFZhbHVlcyI6e30sImRpcmVjdGlvbiI6ImZvcndhcmQifQ==");
115
+ * const cursor = decodeCursor("eyJpbmRleFZhbHVlcyI6e30sImRpcmVjdGlvbiI6ImZvcndhcmQifQ==");
52
116
  * ```
53
117
  */
54
- export function decodeCursor(cursor: string): CursorData {
118
+ export function decodeCursor(cursor: string): Cursor {
55
119
  try {
56
120
  let json: string;
57
121
  if (typeof Buffer !== "undefined") {
@@ -67,46 +131,73 @@ export function decodeCursor(cursor: string): CursorData {
67
131
  typeof data !== "object" ||
68
132
  !data.indexValues ||
69
133
  typeof data.indexValues !== "object" ||
70
- (data.direction !== "forward" && data.direction !== "backward")
134
+ typeof data.pageSize !== "number" ||
135
+ !data.indexName ||
136
+ !data.orderDirection ||
137
+ (data.orderDirection !== "asc" && data.orderDirection !== "desc")
71
138
  ) {
72
139
  throw new Error("Invalid cursor structure");
73
140
  }
74
141
 
75
- return data as CursorData;
142
+ // Only support v1
143
+ const version = typeof data.v === "number" ? data.v : 0;
144
+ if (version !== 1) {
145
+ throw new Error(`Unsupported cursor version: ${version}. Only v1 is supported.`);
146
+ }
147
+
148
+ return new Cursor({
149
+ indexName: data.indexName,
150
+ orderDirection: data.orderDirection,
151
+ pageSize: data.pageSize,
152
+ indexValues: data.indexValues,
153
+ });
76
154
  } catch (error) {
77
155
  throw new Error(`Invalid cursor: ${error instanceof Error ? error.message : "malformed data"}`);
78
156
  }
79
157
  }
80
158
 
81
159
  /**
82
- * Create a cursor from a record and index columns
160
+ * Create a cursor from a record and pagination metadata
83
161
  *
84
162
  * @param record - The database record
85
163
  * @param indexColumns - The columns that make up the index
86
- * @param direction - The pagination direction
87
- * @returns Encoded cursor string
164
+ * @param metadata - Pagination metadata (index name, order direction, page size)
165
+ * @returns Cursor object
88
166
  *
89
167
  * @example
90
168
  * ```ts
91
169
  * const cursor = createCursorFromRecord(
92
170
  * { id: "abc", name: "Alice", createdAt: 123 },
93
171
  * [table.columns.createdAt, table.columns.id],
94
- * "forward"
172
+ * {
173
+ * indexName: "idx_created",
174
+ * orderDirection: "asc",
175
+ * pageSize: 10
176
+ * }
95
177
  * );
96
178
  * ```
97
179
  */
98
180
  export function createCursorFromRecord(
99
181
  record: Record<string, unknown>,
100
182
  indexColumns: AnyColumn[],
101
- direction: "forward" | "backward",
102
- ): string {
183
+ metadata: {
184
+ indexName: string;
185
+ orderDirection: "asc" | "desc";
186
+ pageSize: number;
187
+ },
188
+ ): Cursor {
103
189
  const indexValues: Record<string, unknown> = {};
104
190
 
105
191
  for (const col of indexColumns) {
106
192
  indexValues[col.ormName] = record[col.ormName];
107
193
  }
108
194
 
109
- return encodeCursor({ indexValues, direction });
195
+ return new Cursor({
196
+ indexName: metadata.indexName,
197
+ orderDirection: metadata.orderDirection,
198
+ pageSize: metadata.pageSize,
199
+ indexValues,
200
+ });
110
201
  }
111
202
 
112
203
  /**
@@ -115,7 +206,7 @@ export function createCursorFromRecord(
115
206
  * Converts cursor values (which are in application format) to database format
116
207
  * using the column serialization rules.
117
208
  *
118
- * @param cursorData - The decoded cursor data
209
+ * @param cursor - The cursor object
119
210
  * @param indexColumns - The columns that make up the index
120
211
  * @param provider - The SQL provider
121
212
  * @returns Serialized values ready for database queries
@@ -123,21 +214,21 @@ export function createCursorFromRecord(
123
214
  * @example
124
215
  * ```ts
125
216
  * const serialized = serializeCursorValues(
126
- * cursorData,
217
+ * cursor,
127
218
  * [table.columns.createdAt],
128
219
  * "postgresql"
129
220
  * );
130
221
  * ```
131
222
  */
132
223
  export function serializeCursorValues(
133
- cursorData: CursorData,
224
+ cursor: Cursor,
134
225
  indexColumns: AnyColumn[],
135
226
  provider: SQLProvider,
136
227
  ): Record<string, unknown> {
137
228
  const serialized: Record<string, unknown> = {};
138
229
 
139
230
  for (const col of indexColumns) {
140
- const value = cursorData.indexValues[col.ormName];
231
+ const value = cursor.indexValues[col.ormName];
141
232
  if (value !== undefined) {
142
233
  serialized[col.ormName] = serialize(value, col, provider);
143
234
  }
@@ -111,7 +111,11 @@ describe("query type tests", () => {
111
111
  it("should return selected columns only", () => {
112
112
  const _query = {} as Query;
113
113
 
114
- type Result = Awaited<ReturnType<typeof _query.findFirst<"users", ["name", "email"]>>>;
114
+ // Test type inference through builder pattern
115
+ function selectNameAndEmailFirst(q: Query) {
116
+ return q.findFirst("users", (b) => b.select(["name", "email"]));
117
+ }
118
+ type Result = Awaited<ReturnType<typeof selectNameAndEmailFirst>>;
115
119
 
116
120
  expectTypeOf<Result>().toExtend<{
117
121
  name: string;
@@ -122,10 +126,14 @@ describe("query type tests", () => {
122
126
  it("should handle nullable columns correctly", () => {
123
127
  const _query = {} as Query;
124
128
 
125
- type Result = Awaited<ReturnType<typeof _query.findFirst<"users", ["age"]>>>;
129
+ // Test type inference through builder pattern
130
+ function selectAge(q: Query) {
131
+ return q.findFirst("users", (b) => b.select(["age"]));
132
+ }
133
+ type Result = Awaited<ReturnType<typeof selectAge>>;
126
134
  type NonNullResult = Exclude<Result, null>;
127
135
 
128
- expectTypeOf<NonNullResult>().toMatchTypeOf<{ age: number | null }>();
136
+ expectTypeOf<NonNullResult>().toMatchObjectType<{ age: number | null }>();
129
137
  });
130
138
  });
131
139
 
@@ -148,14 +156,25 @@ describe("query type tests", () => {
148
156
  it("should return array of selected columns only", () => {
149
157
  const _query = {} as Query;
150
158
 
151
- type Result = Awaited<ReturnType<typeof _query.find<"users", ["name", "email"], object>>>;
159
+ // Test type inference through builder pattern (mimics actual usage)
160
+ function selectNameAndEmail(q: Query) {
161
+ return q.find("users", (b) => b.select(["name", "email"]));
162
+ }
152
163
 
153
- expectTypeOf<Result>().toExtend<
154
- {
155
- name: string;
156
- email: string;
157
- }[]
158
- >();
164
+ type Result = Awaited<ReturnType<typeof selectNameAndEmail>>;
165
+ type ResultElement = Result[number];
166
+
167
+ // Verify the result array contains the selected columns
168
+ expectTypeOf<ResultElement>().toMatchObjectType<{
169
+ name: string;
170
+ email: string;
171
+ }>();
172
+
173
+ // Verify that only selected columns exist (not age or isActive)
174
+ // @ts-expect-error - age should not exist on the result type
175
+ type _AgeType = ResultElement["age"];
176
+ // @ts-expect-error - isActive should not exist on the result type
177
+ type _IsActiveType = ResultElement["isActive"];
159
178
  });
160
179
  });
161
180
 
@@ -302,10 +321,11 @@ describe("query type tests", () => {
302
321
  type PostResult = Awaited<ReturnType<typeof _query.create<"posts">>>;
303
322
  expectTypeOf<PostResult>().toEqualTypeOf<FragnoId>();
304
323
 
305
- // Find posts by user return type
306
- type UserPostsResult = Awaited<
307
- ReturnType<typeof _query.find<"posts", ["title", "viewCount"], object>>
308
- >;
324
+ // Find posts by user return type - type-only test
325
+ type UserPostsResult = {
326
+ title: string;
327
+ viewCount: number;
328
+ }[];
309
329
 
310
330
  expectTypeOf<UserPostsResult>().toExtend<
311
331
  {