@fragno-dev/db 0.0.1 → 0.1.0
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 +137 -13
- package/.turbo/turbo-test.log +36 -0
- package/CHANGELOG.md +7 -0
- package/dist/adapters/adapters.d.ts +18 -0
- package/dist/adapters/adapters.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-query.js +139 -0
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
- package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
- package/dist/adapters/drizzle/generate.js +273 -0
- package/dist/adapters/drizzle/generate.js.map +1 -0
- package/dist/adapters/drizzle/join-column-utils.js +28 -0
- package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
- package/dist/adapters/drizzle/shared.js +11 -0
- package/dist/adapters/drizzle/shared.js.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.js +119 -0
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
- package/dist/adapters/kysely/kysely-query-builder.js +306 -0
- package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
- package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
- package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
- package/dist/adapters/kysely/kysely-query.js +158 -0
- package/dist/adapters/kysely/kysely-query.js.map +1 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
- package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
- package/dist/adapters/kysely/migration/execute.js +176 -0
- package/dist/adapters/kysely/migration/execute.js.map +1 -0
- package/dist/fragment.d.ts +54 -0
- package/dist/fragment.d.ts.map +1 -0
- package/dist/fragment.js +92 -0
- package/dist/fragment.js.map +1 -0
- package/dist/id.d.ts +2 -0
- package/dist/migration-engine/auto-from-schema.js +116 -0
- package/dist/migration-engine/auto-from-schema.js.map +1 -0
- package/dist/migration-engine/create.d.ts +41 -0
- package/dist/migration-engine/create.d.ts.map +1 -0
- package/dist/migration-engine/create.js +58 -0
- package/dist/migration-engine/create.js.map +1 -0
- package/dist/migration-engine/shared.d.ts +90 -0
- package/dist/migration-engine/shared.d.ts.map +1 -0
- package/dist/migration-engine/shared.js +8 -0
- package/dist/migration-engine/shared.js.map +1 -0
- package/dist/mod.d.ts +55 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +111 -2
- package/dist/mod.js.map +1 -1
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
- package/dist/query/condition-builder.d.ts +41 -0
- package/dist/query/condition-builder.d.ts.map +1 -0
- package/dist/query/condition-builder.js +93 -0
- package/dist/query/condition-builder.js.map +1 -0
- package/dist/query/cursor.d.ts +88 -0
- package/dist/query/cursor.d.ts.map +1 -0
- package/dist/query/cursor.js +103 -0
- package/dist/query/cursor.js.map +1 -0
- package/dist/query/orm/orm.d.ts +18 -0
- package/dist/query/orm/orm.d.ts.map +1 -0
- package/dist/query/orm/orm.js +48 -0
- package/dist/query/orm/orm.js.map +1 -0
- package/dist/query/query.d.ts +79 -0
- package/dist/query/query.d.ts.map +1 -0
- package/dist/query/query.js +1 -0
- package/dist/query/result-transform.js +155 -0
- package/dist/query/result-transform.js.map +1 -0
- package/dist/query/unit-of-work.d.ts +435 -0
- package/dist/query/unit-of-work.d.ts.map +1 -0
- package/dist/query/unit-of-work.js +549 -0
- package/dist/query/unit-of-work.js.map +1 -0
- package/dist/schema/create.d.ts +273 -116
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +410 -222
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/serialize.js +101 -0
- package/dist/schema/serialize.js.map +1 -0
- package/dist/schema-generator/schema-generator.d.ts +15 -0
- package/dist/schema-generator/schema-generator.d.ts.map +1 -0
- package/dist/shared/providers.d.ts +6 -0
- package/dist/shared/providers.d.ts.map +1 -0
- package/dist/util/import-generator.js +26 -0
- package/dist/util/import-generator.js.map +1 -0
- package/dist/util/parse.js +15 -0
- package/dist/util/parse.js.map +1 -0
- package/dist/util/types.d.ts +8 -0
- package/dist/util/types.d.ts.map +1 -0
- package/package.json +63 -2
- package/src/adapters/adapters.ts +22 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
- package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
- package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
- package/src/adapters/drizzle/drizzle-query.ts +234 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
- package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
- package/src/adapters/drizzle/generate.test.ts +643 -0
- package/src/adapters/drizzle/generate.ts +481 -0
- package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
- package/src/adapters/drizzle/join-column-utils.ts +39 -0
- package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
- package/src/adapters/drizzle/shared.ts +22 -0
- package/src/adapters/drizzle/test-utils.ts +56 -0
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
- package/src/adapters/kysely/kysely-adapter.ts +196 -0
- package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
- package/src/adapters/kysely/kysely-query-builder.ts +611 -0
- package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
- package/src/adapters/kysely/kysely-query.ts +254 -0
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
- package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
- package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
- package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
- package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
- package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
- package/src/adapters/kysely/migration/execute.ts +382 -0
- package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
- package/src/fragment.test.ts +287 -0
- package/src/fragment.ts +198 -0
- package/src/migration-engine/auto-from-schema.test.ts +118 -58
- package/src/migration-engine/auto-from-schema.ts +103 -32
- package/src/migration-engine/create.test.ts +34 -46
- package/src/migration-engine/create.ts +41 -26
- package/src/migration-engine/shared.ts +26 -6
- package/src/mod.ts +197 -1
- package/src/query/condition-builder.test.ts +379 -0
- package/src/query/condition-builder.ts +294 -0
- package/src/query/cursor.test.ts +296 -0
- package/src/query/cursor.ts +147 -0
- package/src/query/orm/orm.ts +92 -0
- package/src/query/query-type.test.ts +429 -0
- package/src/query/query.ts +200 -0
- package/src/query/result-transform.test.ts +795 -0
- package/src/query/result-transform.ts +247 -0
- package/src/query/unit-of-work-types.test.ts +192 -0
- package/src/query/unit-of-work.test.ts +947 -0
- package/src/query/unit-of-work.ts +1199 -0
- package/src/schema/create.test.ts +653 -110
- package/src/schema/create.ts +708 -337
- package/src/schema/serialize.test.ts +559 -0
- package/src/schema/serialize.ts +359 -0
- package/src/schema-generator/schema-generator.ts +12 -0
- package/src/shared/config.ts +0 -8
- package/src/util/import-generator.ts +28 -0
- package/src/util/parse.ts +16 -0
- package/src/util/types.ts +4 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +11 -1
- package/vitest.config.ts +3 -0
- /package/dist/{cuid.js → id.js} +0 -0
- /package/src/{cuid.ts → id.ts} +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
encodeCursor,
|
|
4
|
+
decodeCursor,
|
|
5
|
+
createCursorFromRecord,
|
|
6
|
+
serializeCursorValues,
|
|
7
|
+
type CursorData,
|
|
8
|
+
} from "./cursor";
|
|
9
|
+
import { column, idColumn, schema } from "../schema/create";
|
|
10
|
+
|
|
11
|
+
describe("Cursor utilities", () => {
|
|
12
|
+
describe("encodeCursor and decodeCursor", () => {
|
|
13
|
+
it("should encode and decode a cursor with simple values", () => {
|
|
14
|
+
const cursorData: CursorData = {
|
|
15
|
+
indexValues: { id: "user123" },
|
|
16
|
+
direction: "forward",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const encoded = encodeCursor(cursorData);
|
|
20
|
+
expect(encoded).toBeTruthy();
|
|
21
|
+
expect(typeof encoded).toBe("string");
|
|
22
|
+
|
|
23
|
+
const decoded = decodeCursor(encoded);
|
|
24
|
+
expect(decoded).toEqual(cursorData);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should encode and decode a cursor with multiple index values", () => {
|
|
28
|
+
const cursorData: CursorData = {
|
|
29
|
+
indexValues: {
|
|
30
|
+
createdAt: 1234567890,
|
|
31
|
+
id: "user123",
|
|
32
|
+
name: "Alice",
|
|
33
|
+
},
|
|
34
|
+
direction: "backward",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const encoded = encodeCursor(cursorData);
|
|
38
|
+
const decoded = decodeCursor(encoded);
|
|
39
|
+
expect(decoded).toEqual(cursorData);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle different data types in index values", () => {
|
|
43
|
+
const cursorData: CursorData = {
|
|
44
|
+
indexValues: {
|
|
45
|
+
stringValue: "test",
|
|
46
|
+
numberValue: 42,
|
|
47
|
+
boolValue: true,
|
|
48
|
+
nullValue: null,
|
|
49
|
+
},
|
|
50
|
+
direction: "forward",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const encoded = encodeCursor(cursorData);
|
|
54
|
+
const decoded = decodeCursor(encoded);
|
|
55
|
+
expect(decoded).toEqual(cursorData);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should produce base64-encoded strings", () => {
|
|
59
|
+
const cursorData: CursorData = {
|
|
60
|
+
indexValues: { id: "test" },
|
|
61
|
+
direction: "forward",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const encoded = encodeCursor(cursorData);
|
|
65
|
+
|
|
66
|
+
// Base64 pattern - should only contain valid base64 characters
|
|
67
|
+
expect(encoded).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
|
68
|
+
|
|
69
|
+
// Should be decodeable
|
|
70
|
+
const decoded = decodeCursor(encoded);
|
|
71
|
+
expect(decoded).toEqual(cursorData);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should handle empty index values", () => {
|
|
75
|
+
const cursorData: CursorData = {
|
|
76
|
+
indexValues: {},
|
|
77
|
+
direction: "forward",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const encoded = encodeCursor(cursorData);
|
|
81
|
+
const decoded = decodeCursor(encoded);
|
|
82
|
+
expect(decoded).toEqual(cursorData);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("decodeCursor error handling", () => {
|
|
87
|
+
it("should throw error for invalid base64", () => {
|
|
88
|
+
expect(() => decodeCursor("not-valid-base64!!!")).toThrow(/invalid cursor/i);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should throw error for invalid JSON", () => {
|
|
92
|
+
// Encode invalid JSON as base64
|
|
93
|
+
const invalidJson = Buffer.from("{not valid json}", "utf-8").toString("base64");
|
|
94
|
+
expect(() => decodeCursor(invalidJson)).toThrow(/invalid cursor/i);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should throw error for missing indexValues", () => {
|
|
98
|
+
const invalidData = { direction: "forward" };
|
|
99
|
+
const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
|
|
100
|
+
expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should throw error for missing direction", () => {
|
|
104
|
+
const invalidData = { indexValues: { id: "test" } };
|
|
105
|
+
const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
|
|
106
|
+
expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should throw error for invalid direction value", () => {
|
|
110
|
+
const invalidData = { indexValues: { id: "test" }, direction: "sideways" };
|
|
111
|
+
const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
|
|
112
|
+
expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should throw error for non-object indexValues", () => {
|
|
116
|
+
const invalidData = { indexValues: "not an object", direction: "forward" };
|
|
117
|
+
const encoded = Buffer.from(JSON.stringify(invalidData), "utf-8").toString("base64");
|
|
118
|
+
expect(() => decodeCursor(encoded)).toThrow(/invalid cursor/i);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("createCursorFromRecord", () => {
|
|
123
|
+
it("should create a cursor from a record with single column index", () => {
|
|
124
|
+
const testSchema = schema((s) =>
|
|
125
|
+
s.addTable("users", (t) =>
|
|
126
|
+
t.addColumn("id", idColumn()).addColumn("name", column("string")),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const table = testSchema.tables.users;
|
|
131
|
+
const record = { id: "user123", name: "Alice" };
|
|
132
|
+
const indexColumns = [table.columns.id];
|
|
133
|
+
|
|
134
|
+
const cursor = createCursorFromRecord(record, indexColumns, "forward");
|
|
135
|
+
|
|
136
|
+
expect(typeof cursor).toBe("string");
|
|
137
|
+
|
|
138
|
+
const decoded = decodeCursor(cursor);
|
|
139
|
+
expect(decoded.indexValues).toEqual({ id: "user123" });
|
|
140
|
+
expect(decoded.direction).toBe("forward");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should create a cursor from a record with multi-column index", () => {
|
|
144
|
+
const testSchema = schema((s) =>
|
|
145
|
+
s.addTable("posts", (t) =>
|
|
146
|
+
t
|
|
147
|
+
.addColumn("id", idColumn())
|
|
148
|
+
.addColumn("createdAt", column("integer"))
|
|
149
|
+
.addColumn("userId", column("string"))
|
|
150
|
+
.addColumn("title", column("string"))
|
|
151
|
+
.createIndex("created_user", ["createdAt", "userId"]),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const table = testSchema.tables.posts;
|
|
156
|
+
const record = {
|
|
157
|
+
id: "post123",
|
|
158
|
+
createdAt: 1234567890,
|
|
159
|
+
userId: "user456",
|
|
160
|
+
title: "Test Post",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const index = table.indexes.created_user;
|
|
164
|
+
const indexColumns = index.columns;
|
|
165
|
+
|
|
166
|
+
const cursor = createCursorFromRecord(record, indexColumns, "backward");
|
|
167
|
+
|
|
168
|
+
const decoded = decodeCursor(cursor);
|
|
169
|
+
expect(decoded.indexValues).toEqual({
|
|
170
|
+
createdAt: 1234567890,
|
|
171
|
+
userId: "user456",
|
|
172
|
+
});
|
|
173
|
+
expect(decoded.direction).toBe("backward");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should only include columns that are in the index", () => {
|
|
177
|
+
const testSchema = schema((s) =>
|
|
178
|
+
s.addTable("users", (t) =>
|
|
179
|
+
t
|
|
180
|
+
.addColumn("id", idColumn())
|
|
181
|
+
.addColumn("name", column("string"))
|
|
182
|
+
.addColumn("email", column("string")),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const table = testSchema.tables.users;
|
|
187
|
+
const record = { id: "user123", name: "Alice", email: "alice@example.com" };
|
|
188
|
+
const indexColumns = [table.columns.id];
|
|
189
|
+
|
|
190
|
+
const cursor = createCursorFromRecord(record, indexColumns, "forward");
|
|
191
|
+
|
|
192
|
+
const decoded = decodeCursor(cursor);
|
|
193
|
+
expect(decoded.indexValues).toEqual({ id: "user123" });
|
|
194
|
+
expect(Object.keys(decoded.indexValues)).toHaveLength(1);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("serializeCursorValues", () => {
|
|
199
|
+
it("should serialize cursor values for database queries", () => {
|
|
200
|
+
const testSchema = schema((s) =>
|
|
201
|
+
s.addTable("users", (t) =>
|
|
202
|
+
t.addColumn("id", idColumn()).addColumn("age", column("integer")),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const table = testSchema.tables.users;
|
|
207
|
+
const cursorData: CursorData = {
|
|
208
|
+
indexValues: { id: "user123", age: 25 },
|
|
209
|
+
direction: "forward",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const indexColumns = [table.columns.id, table.columns.age];
|
|
213
|
+
const serialized = serializeCursorValues(cursorData, indexColumns, "postgresql");
|
|
214
|
+
|
|
215
|
+
expect(serialized).toHaveProperty("id", "user123");
|
|
216
|
+
expect(serialized).toHaveProperty("age", 25);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should handle missing values in cursor data", () => {
|
|
220
|
+
const testSchema = schema((s) =>
|
|
221
|
+
s.addTable("users", (t) =>
|
|
222
|
+
t.addColumn("id", idColumn()).addColumn("name", column("string")),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const table = testSchema.tables.users;
|
|
227
|
+
const cursorData: CursorData = {
|
|
228
|
+
indexValues: { id: "user123" },
|
|
229
|
+
direction: "forward",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const indexColumns = [table.columns.id, table.columns.name];
|
|
233
|
+
const serialized = serializeCursorValues(cursorData, indexColumns, "postgresql");
|
|
234
|
+
|
|
235
|
+
expect(serialized).toHaveProperty("id", "user123");
|
|
236
|
+
expect(serialized).not.toHaveProperty("name");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("round-trip integration", () => {
|
|
241
|
+
it("should successfully round-trip cursor data through encode/decode", () => {
|
|
242
|
+
const testSchema = schema((s) =>
|
|
243
|
+
s.addTable("posts", (t) =>
|
|
244
|
+
t
|
|
245
|
+
.addColumn("id", idColumn())
|
|
246
|
+
.addColumn("createdAt", column("integer"))
|
|
247
|
+
.addColumn("score", column("integer"))
|
|
248
|
+
.createIndex("trending", ["score", "createdAt"]),
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const table = testSchema.tables.posts;
|
|
253
|
+
const record = {
|
|
254
|
+
id: "post123",
|
|
255
|
+
createdAt: 1234567890,
|
|
256
|
+
score: 42,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const index = table.indexes.trending;
|
|
260
|
+
const indexColumns = index.columns;
|
|
261
|
+
|
|
262
|
+
// Create cursor from record
|
|
263
|
+
const cursor = createCursorFromRecord(record, indexColumns, "forward");
|
|
264
|
+
|
|
265
|
+
// Decode it
|
|
266
|
+
const decoded = decodeCursor(cursor);
|
|
267
|
+
|
|
268
|
+
// Serialize the values
|
|
269
|
+
const serialized = serializeCursorValues(decoded, indexColumns, "postgresql");
|
|
270
|
+
|
|
271
|
+
// Should preserve the values
|
|
272
|
+
expect(serialized["score"]).toBe(42);
|
|
273
|
+
expect(serialized["createdAt"]).toBe(1234567890);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should handle different directions correctly", () => {
|
|
277
|
+
const cursorForward = encodeCursor({
|
|
278
|
+
indexValues: { id: "test" },
|
|
279
|
+
direction: "forward",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const cursorBackward = encodeCursor({
|
|
283
|
+
indexValues: { id: "test" },
|
|
284
|
+
direction: "backward",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(cursorForward).not.toBe(cursorBackward);
|
|
288
|
+
|
|
289
|
+
const decodedForward = decodeCursor(cursorForward);
|
|
290
|
+
const decodedBackward = decodeCursor(cursorBackward);
|
|
291
|
+
|
|
292
|
+
expect(decodedForward.direction).toBe("forward");
|
|
293
|
+
expect(decodedBackward.direction).toBe("backward");
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { AnyColumn } from "../schema/create";
|
|
2
|
+
import { serialize } from "../schema/serialize";
|
|
3
|
+
import type { SQLProvider } from "../shared/providers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cursor data structure containing index values and pagination direction
|
|
7
|
+
*/
|
|
8
|
+
export interface CursorData {
|
|
9
|
+
/**
|
|
10
|
+
* Values for each column in the index, keyed by column ORM name
|
|
11
|
+
*/
|
|
12
|
+
indexValues: Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Direction of pagination
|
|
15
|
+
*/
|
|
16
|
+
direction: "forward" | "backward";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
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
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function encodeCursor(data: CursorData): string {
|
|
34
|
+
const json = JSON.stringify(data);
|
|
35
|
+
// Use Buffer in Node.js or btoa in browsers
|
|
36
|
+
if (typeof Buffer !== "undefined") {
|
|
37
|
+
return Buffer.from(json, "utf-8").toString("base64");
|
|
38
|
+
}
|
|
39
|
+
return btoa(json);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decode a base64 cursor string back to cursor data
|
|
44
|
+
*
|
|
45
|
+
* @param cursor - The base64-encoded cursor string
|
|
46
|
+
* @returns Decoded cursor data
|
|
47
|
+
* @throws Error if cursor is invalid or malformed
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const data = decodeCursor("eyJpbmRleFZhbHVlcyI6e30sImRpcmVjdGlvbiI6ImZvcndhcmQifQ==");
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function decodeCursor(cursor: string): CursorData {
|
|
55
|
+
try {
|
|
56
|
+
let json: string;
|
|
57
|
+
if (typeof Buffer !== "undefined") {
|
|
58
|
+
json = Buffer.from(cursor, "base64").toString("utf-8");
|
|
59
|
+
} else {
|
|
60
|
+
json = atob(cursor);
|
|
61
|
+
}
|
|
62
|
+
const data = JSON.parse(json);
|
|
63
|
+
|
|
64
|
+
// Validate structure
|
|
65
|
+
if (
|
|
66
|
+
!data ||
|
|
67
|
+
typeof data !== "object" ||
|
|
68
|
+
!data.indexValues ||
|
|
69
|
+
typeof data.indexValues !== "object" ||
|
|
70
|
+
(data.direction !== "forward" && data.direction !== "backward")
|
|
71
|
+
) {
|
|
72
|
+
throw new Error("Invalid cursor structure");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return data as CursorData;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`Invalid cursor: ${error instanceof Error ? error.message : "malformed data"}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a cursor from a record and index columns
|
|
83
|
+
*
|
|
84
|
+
* @param record - The database record
|
|
85
|
+
* @param indexColumns - The columns that make up the index
|
|
86
|
+
* @param direction - The pagination direction
|
|
87
|
+
* @returns Encoded cursor string
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const cursor = createCursorFromRecord(
|
|
92
|
+
* { id: "abc", name: "Alice", createdAt: 123 },
|
|
93
|
+
* [table.columns.createdAt, table.columns.id],
|
|
94
|
+
* "forward"
|
|
95
|
+
* );
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function createCursorFromRecord(
|
|
99
|
+
record: Record<string, unknown>,
|
|
100
|
+
indexColumns: AnyColumn[],
|
|
101
|
+
direction: "forward" | "backward",
|
|
102
|
+
): string {
|
|
103
|
+
const indexValues: Record<string, unknown> = {};
|
|
104
|
+
|
|
105
|
+
for (const col of indexColumns) {
|
|
106
|
+
indexValues[col.ormName] = record[col.ormName];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return encodeCursor({ indexValues, direction });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Serialize cursor values for database queries
|
|
114
|
+
*
|
|
115
|
+
* Converts cursor values (which are in application format) to database format
|
|
116
|
+
* using the column serialization rules.
|
|
117
|
+
*
|
|
118
|
+
* @param cursorData - The decoded cursor data
|
|
119
|
+
* @param indexColumns - The columns that make up the index
|
|
120
|
+
* @param provider - The SQL provider
|
|
121
|
+
* @returns Serialized values ready for database queries
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const serialized = serializeCursorValues(
|
|
126
|
+
* cursorData,
|
|
127
|
+
* [table.columns.createdAt],
|
|
128
|
+
* "postgresql"
|
|
129
|
+
* );
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function serializeCursorValues(
|
|
133
|
+
cursorData: CursorData,
|
|
134
|
+
indexColumns: AnyColumn[],
|
|
135
|
+
provider: SQLProvider,
|
|
136
|
+
): Record<string, unknown> {
|
|
137
|
+
const serialized: Record<string, unknown> = {};
|
|
138
|
+
|
|
139
|
+
for (const col of indexColumns) {
|
|
140
|
+
const value = cursorData.indexValues[col.ormName];
|
|
141
|
+
if (value !== undefined) {
|
|
142
|
+
serialized[col.ormName] = serialize(value, col, provider);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return serialized;
|
|
147
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnySelectClause,
|
|
3
|
+
FindFirstOptions,
|
|
4
|
+
FindManyOptions,
|
|
5
|
+
JoinBuilder,
|
|
6
|
+
OrderBy,
|
|
7
|
+
} from "../query";
|
|
8
|
+
import { buildCondition, type Condition } from "../condition-builder";
|
|
9
|
+
import type { AnyColumn, AnyRelation, AnyTable } from "../../schema/create";
|
|
10
|
+
|
|
11
|
+
export interface CompiledJoin {
|
|
12
|
+
relation: AnyRelation;
|
|
13
|
+
options: SimplifyFindOptions<FindManyOptions> | false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isOrderByArray(v: OrderBy | OrderBy[]): v is OrderBy[] {
|
|
17
|
+
return Array.isArray(v) && Array.isArray(v[0]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function simplifyOrderBy(
|
|
21
|
+
columns: Record<string, AnyColumn>,
|
|
22
|
+
orderBy: OrderBy | OrderBy[] | undefined,
|
|
23
|
+
): OrderBy<AnyColumn>[] | undefined {
|
|
24
|
+
if (!orderBy || orderBy.length === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isOrderByArray(orderBy)) {
|
|
29
|
+
orderBy = [orderBy];
|
|
30
|
+
}
|
|
31
|
+
return orderBy.map(([name, value]) => {
|
|
32
|
+
const col = columns[name];
|
|
33
|
+
if (!col) {
|
|
34
|
+
throw new Error(`unknown column name ${name}.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [col, value];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildFindOptions(
|
|
42
|
+
table: AnyTable,
|
|
43
|
+
{ select = true, where, orderBy, join, ...options }: FindManyOptions,
|
|
44
|
+
): SimplifyFindOptions<FindManyOptions> | false {
|
|
45
|
+
let conditions = where ? buildCondition(table.columns, where) : undefined;
|
|
46
|
+
if (conditions === true) {
|
|
47
|
+
conditions = undefined;
|
|
48
|
+
}
|
|
49
|
+
if (conditions === false) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
select,
|
|
55
|
+
where: conditions,
|
|
56
|
+
orderBy: simplifyOrderBy(table.columns, orderBy),
|
|
57
|
+
join: join ? buildJoin(table, join) : undefined,
|
|
58
|
+
...options,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildJoin<TTable extends AnyTable>(
|
|
63
|
+
table: AnyTable,
|
|
64
|
+
fn: (builder: JoinBuilder<TTable>) => void,
|
|
65
|
+
): CompiledJoin[] {
|
|
66
|
+
const compiled: CompiledJoin[] = [];
|
|
67
|
+
const builder: Record<string, unknown> = {};
|
|
68
|
+
|
|
69
|
+
for (const name in table.relations) {
|
|
70
|
+
const relation = table.relations[name]!;
|
|
71
|
+
|
|
72
|
+
builder[name] = (options: FindFirstOptions | FindManyOptions = {}) => {
|
|
73
|
+
compiled.push({
|
|
74
|
+
relation,
|
|
75
|
+
options: buildFindOptions(relation.table, options),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
delete builder[name];
|
|
79
|
+
return builder;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn(builder as JoinBuilder<TTable>);
|
|
84
|
+
return compiled;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type SimplifyFindOptions<O> = Omit<O, "where" | "orderBy" | "select" | "join"> & {
|
|
88
|
+
select: AnySelectClause;
|
|
89
|
+
where?: Condition | undefined;
|
|
90
|
+
orderBy?: OrderBy<AnyColumn>[];
|
|
91
|
+
join?: CompiledJoin[];
|
|
92
|
+
};
|