@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
package/src/query/cursor.test.ts
CHANGED
|
@@ -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("
|
|
6
|
+
describe("Cursor class encode and decode", () => {
|
|
13
7
|
it("should encode and decode a cursor with simple values", () => {
|
|
14
|
-
const
|
|
8
|
+
const cursor = new Cursor({
|
|
9
|
+
indexName: "idx_test",
|
|
10
|
+
orderDirection: "asc",
|
|
11
|
+
pageSize: 10,
|
|
15
12
|
indexValues: { id: "user123" },
|
|
16
|
-
|
|
17
|
-
};
|
|
13
|
+
});
|
|
18
14
|
|
|
19
|
-
const encoded =
|
|
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).
|
|
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
|
|
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
|
-
|
|
35
|
-
};
|
|
34
|
+
});
|
|
36
35
|
|
|
37
|
-
const encoded =
|
|
36
|
+
const encoded = cursor.encode();
|
|
38
37
|
const decoded = decodeCursor(encoded);
|
|
39
|
-
expect(decoded).toEqual(
|
|
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
|
|
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
|
-
|
|
51
|
-
};
|
|
53
|
+
});
|
|
52
54
|
|
|
53
|
-
const encoded =
|
|
55
|
+
const encoded = cursor.encode();
|
|
54
56
|
const decoded = decodeCursor(encoded);
|
|
55
|
-
expect(decoded).toEqual(
|
|
57
|
+
expect(decoded.indexValues).toEqual(cursor.indexValues);
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
it("should produce base64-encoded strings", () => {
|
|
59
|
-
const
|
|
61
|
+
const cursor = new Cursor({
|
|
62
|
+
indexName: "_primary",
|
|
63
|
+
orderDirection: "asc",
|
|
64
|
+
pageSize: 10,
|
|
60
65
|
indexValues: { id: "test" },
|
|
61
|
-
|
|
62
|
-
};
|
|
66
|
+
});
|
|
63
67
|
|
|
64
|
-
const encoded =
|
|
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(
|
|
75
|
+
expect(decoded.indexValues).toEqual(cursor.indexValues);
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
it("should handle empty index values", () => {
|
|
75
|
-
const
|
|
79
|
+
const cursor = new Cursor({
|
|
80
|
+
indexName: "_primary",
|
|
81
|
+
orderDirection: "asc",
|
|
82
|
+
pageSize: 10,
|
|
76
83
|
indexValues: {},
|
|
77
|
-
|
|
78
|
-
};
|
|
84
|
+
});
|
|
79
85
|
|
|
80
|
-
const encoded =
|
|
86
|
+
const encoded = cursor.encode();
|
|
81
87
|
const decoded = decodeCursor(encoded);
|
|
82
|
-
expect(decoded).toEqual(
|
|
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 = {
|
|
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
|
|
104
|
-
const invalidData = {
|
|
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
|
|
110
|
-
const invalidData = {
|
|
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 = {
|
|
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,
|
|
157
|
+
const cursor = createCursorFromRecord(record, indexColumns, {
|
|
158
|
+
indexName: "_primary",
|
|
159
|
+
orderDirection: "asc",
|
|
160
|
+
pageSize: 10,
|
|
161
|
+
});
|
|
135
162
|
|
|
136
|
-
expect(
|
|
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,
|
|
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,
|
|
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
|
|
240
|
+
const cursor = new Cursor({
|
|
241
|
+
indexName: "_primary",
|
|
242
|
+
orderDirection: "asc",
|
|
243
|
+
pageSize: 10,
|
|
208
244
|
indexValues: { id: "user123", age: 25 },
|
|
209
|
-
|
|
210
|
-
};
|
|
245
|
+
});
|
|
211
246
|
|
|
212
247
|
const indexColumns = [table.columns.id, table.columns.age];
|
|
213
|
-
const serialized = serializeCursorValues(
|
|
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
|
|
262
|
+
const cursor = new Cursor({
|
|
263
|
+
indexName: "_primary",
|
|
264
|
+
orderDirection: "asc",
|
|
265
|
+
pageSize: 10,
|
|
228
266
|
indexValues: { id: "user123" },
|
|
229
|
-
|
|
230
|
-
};
|
|
267
|
+
});
|
|
231
268
|
|
|
232
269
|
const indexColumns = [table.columns.id, table.columns.name];
|
|
233
|
-
const serialized = serializeCursorValues(
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
332
|
+
expect(cursorAsc.encode()).not.toBe(cursorDesc.encode());
|
|
288
333
|
|
|
289
|
-
const
|
|
290
|
-
const
|
|
334
|
+
const decodedAsc = decodeCursor(cursorAsc.encode());
|
|
335
|
+
const decodedDesc = decodeCursor(cursorDesc.encode());
|
|
291
336
|
|
|
292
|
-
expect(
|
|
293
|
-
expect(
|
|
337
|
+
expect(decodedAsc.orderDirection).toBe("asc");
|
|
338
|
+
expect(decodedDesc.orderDirection).toBe("desc");
|
|
294
339
|
});
|
|
295
340
|
});
|
|
296
341
|
});
|
package/src/query/cursor.ts
CHANGED
|
@@ -3,34 +3,98 @@ import { serialize } from "../schema/serialize";
|
|
|
3
3
|
import type { SQLProvider } from "../shared/providers";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Cursor
|
|
6
|
+
* Cursor object containing all information needed for pagination
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
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
|
-
*
|
|
27
|
+
* Get the index name being used for pagination
|
|
11
28
|
*/
|
|
12
|
-
|
|
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
|
-
*
|
|
41
|
+
* Get the page size
|
|
15
42
|
*/
|
|
16
|
-
|
|
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
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
115
|
+
* const cursor = decodeCursor("eyJpbmRleFZhbHVlcyI6e30sImRpcmVjdGlvbiI6ImZvcndhcmQifQ==");
|
|
52
116
|
* ```
|
|
53
117
|
*/
|
|
54
|
-
export function decodeCursor(cursor: string):
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
87
|
-
* @returns
|
|
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
|
-
*
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
217
|
+
* cursor,
|
|
127
218
|
* [table.columns.createdAt],
|
|
128
219
|
* "postgresql"
|
|
129
220
|
* );
|
|
130
221
|
* ```
|
|
131
222
|
*/
|
|
132
223
|
export function serializeCursorValues(
|
|
133
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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>().
|
|
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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 =
|
|
307
|
-
|
|
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
|
{
|