@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.
Files changed (200) hide show
  1. package/.turbo/turbo-build.log +137 -13
  2. package/.turbo/turbo-test.log +36 -0
  3. package/CHANGELOG.md +7 -0
  4. package/dist/adapters/adapters.d.ts +18 -0
  5. package/dist/adapters/adapters.d.ts.map +1 -0
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
  8. package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
  10. package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
  11. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
  12. package/dist/adapters/drizzle/drizzle-query.js +139 -0
  13. package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
  16. package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
  17. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
  18. package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
  19. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
  20. package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
  21. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
  22. package/dist/adapters/drizzle/generate.js +273 -0
  23. package/dist/adapters/drizzle/generate.js.map +1 -0
  24. package/dist/adapters/drizzle/join-column-utils.js +28 -0
  25. package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
  26. package/dist/adapters/drizzle/shared.js +11 -0
  27. package/dist/adapters/drizzle/shared.js.map +1 -0
  28. package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
  29. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
  30. package/dist/adapters/kysely/kysely-adapter.js +119 -0
  31. package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
  32. package/dist/adapters/kysely/kysely-query-builder.js +306 -0
  33. package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
  34. package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
  35. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
  36. package/dist/adapters/kysely/kysely-query.js +158 -0
  37. package/dist/adapters/kysely/kysely-query.js.map +1 -0
  38. package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
  39. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
  40. package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
  41. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
  42. package/dist/adapters/kysely/migration/execute.js +176 -0
  43. package/dist/adapters/kysely/migration/execute.js.map +1 -0
  44. package/dist/fragment.d.ts +54 -0
  45. package/dist/fragment.d.ts.map +1 -0
  46. package/dist/fragment.js +92 -0
  47. package/dist/fragment.js.map +1 -0
  48. package/dist/id.d.ts +2 -0
  49. package/dist/migration-engine/auto-from-schema.js +116 -0
  50. package/dist/migration-engine/auto-from-schema.js.map +1 -0
  51. package/dist/migration-engine/create.d.ts +41 -0
  52. package/dist/migration-engine/create.d.ts.map +1 -0
  53. package/dist/migration-engine/create.js +58 -0
  54. package/dist/migration-engine/create.js.map +1 -0
  55. package/dist/migration-engine/shared.d.ts +90 -0
  56. package/dist/migration-engine/shared.d.ts.map +1 -0
  57. package/dist/migration-engine/shared.js +8 -0
  58. package/dist/migration-engine/shared.js.map +1 -0
  59. package/dist/mod.d.ts +55 -2
  60. package/dist/mod.d.ts.map +1 -1
  61. package/dist/mod.js +111 -2
  62. package/dist/mod.js.map +1 -1
  63. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
  64. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
  65. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
  66. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
  67. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
  68. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
  69. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
  70. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
  71. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
  72. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
  73. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
  74. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
  75. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
  76. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
  77. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
  78. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
  79. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
  80. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
  81. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
  82. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
  83. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
  84. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
  85. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
  86. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
  87. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
  88. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
  89. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
  90. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
  91. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
  92. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
  93. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
  94. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
  95. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
  96. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
  97. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
  98. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
  99. package/dist/query/condition-builder.d.ts +41 -0
  100. package/dist/query/condition-builder.d.ts.map +1 -0
  101. package/dist/query/condition-builder.js +93 -0
  102. package/dist/query/condition-builder.js.map +1 -0
  103. package/dist/query/cursor.d.ts +88 -0
  104. package/dist/query/cursor.d.ts.map +1 -0
  105. package/dist/query/cursor.js +103 -0
  106. package/dist/query/cursor.js.map +1 -0
  107. package/dist/query/orm/orm.d.ts +18 -0
  108. package/dist/query/orm/orm.d.ts.map +1 -0
  109. package/dist/query/orm/orm.js +48 -0
  110. package/dist/query/orm/orm.js.map +1 -0
  111. package/dist/query/query.d.ts +79 -0
  112. package/dist/query/query.d.ts.map +1 -0
  113. package/dist/query/query.js +1 -0
  114. package/dist/query/result-transform.js +155 -0
  115. package/dist/query/result-transform.js.map +1 -0
  116. package/dist/query/unit-of-work.d.ts +435 -0
  117. package/dist/query/unit-of-work.d.ts.map +1 -0
  118. package/dist/query/unit-of-work.js +549 -0
  119. package/dist/query/unit-of-work.js.map +1 -0
  120. package/dist/schema/create.d.ts +273 -116
  121. package/dist/schema/create.d.ts.map +1 -1
  122. package/dist/schema/create.js +410 -222
  123. package/dist/schema/create.js.map +1 -1
  124. package/dist/schema/serialize.js +101 -0
  125. package/dist/schema/serialize.js.map +1 -0
  126. package/dist/schema-generator/schema-generator.d.ts +15 -0
  127. package/dist/schema-generator/schema-generator.d.ts.map +1 -0
  128. package/dist/shared/providers.d.ts +6 -0
  129. package/dist/shared/providers.d.ts.map +1 -0
  130. package/dist/util/import-generator.js +26 -0
  131. package/dist/util/import-generator.js.map +1 -0
  132. package/dist/util/parse.js +15 -0
  133. package/dist/util/parse.js.map +1 -0
  134. package/dist/util/types.d.ts +8 -0
  135. package/dist/util/types.d.ts.map +1 -0
  136. package/package.json +63 -2
  137. package/src/adapters/adapters.ts +22 -0
  138. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
  139. package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
  140. package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
  141. package/src/adapters/drizzle/drizzle-query.ts +234 -0
  142. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
  143. package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
  144. package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
  145. package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
  146. package/src/adapters/drizzle/generate.test.ts +643 -0
  147. package/src/adapters/drizzle/generate.ts +481 -0
  148. package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
  149. package/src/adapters/drizzle/join-column-utils.ts +39 -0
  150. package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
  151. package/src/adapters/drizzle/shared.ts +22 -0
  152. package/src/adapters/drizzle/test-utils.ts +56 -0
  153. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
  154. package/src/adapters/kysely/kysely-adapter.ts +196 -0
  155. package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
  156. package/src/adapters/kysely/kysely-query-builder.ts +611 -0
  157. package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
  158. package/src/adapters/kysely/kysely-query.ts +254 -0
  159. package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
  160. package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
  161. package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
  162. package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
  163. package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
  164. package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
  165. package/src/adapters/kysely/migration/execute.ts +382 -0
  166. package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
  167. package/src/fragment.test.ts +287 -0
  168. package/src/fragment.ts +198 -0
  169. package/src/migration-engine/auto-from-schema.test.ts +118 -58
  170. package/src/migration-engine/auto-from-schema.ts +103 -32
  171. package/src/migration-engine/create.test.ts +34 -46
  172. package/src/migration-engine/create.ts +41 -26
  173. package/src/migration-engine/shared.ts +26 -6
  174. package/src/mod.ts +197 -1
  175. package/src/query/condition-builder.test.ts +379 -0
  176. package/src/query/condition-builder.ts +294 -0
  177. package/src/query/cursor.test.ts +296 -0
  178. package/src/query/cursor.ts +147 -0
  179. package/src/query/orm/orm.ts +92 -0
  180. package/src/query/query-type.test.ts +429 -0
  181. package/src/query/query.ts +200 -0
  182. package/src/query/result-transform.test.ts +795 -0
  183. package/src/query/result-transform.ts +247 -0
  184. package/src/query/unit-of-work-types.test.ts +192 -0
  185. package/src/query/unit-of-work.test.ts +947 -0
  186. package/src/query/unit-of-work.ts +1199 -0
  187. package/src/schema/create.test.ts +653 -110
  188. package/src/schema/create.ts +708 -337
  189. package/src/schema/serialize.test.ts +559 -0
  190. package/src/schema/serialize.ts +359 -0
  191. package/src/schema-generator/schema-generator.ts +12 -0
  192. package/src/shared/config.ts +0 -8
  193. package/src/util/import-generator.ts +28 -0
  194. package/src/util/parse.ts +16 -0
  195. package/src/util/types.ts +4 -0
  196. package/tsconfig.json +1 -1
  197. package/tsdown.config.ts +11 -1
  198. package/vitest.config.ts +3 -0
  199. /package/dist/{cuid.js → id.js} +0 -0
  200. /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
+ };