@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,795 @@
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import { column, idColumn, referenceColumn, schema, FragnoId } from "../schema/create";
3
+ import { decodeResult, encodeValues, ReferenceSubquery } from "./result-transform";
4
+
5
+ describe("encodeValues", () => {
6
+ const testSchema = schema((s) => {
7
+ return s
8
+ .addTable("users", (t) => {
9
+ return t
10
+ .addColumn("id", idColumn())
11
+ .addColumn("name", column("string"))
12
+ .addColumn("email", column("string"))
13
+ .addColumn("age", column("integer").nullable())
14
+ .addColumn("isActive", column("bool"))
15
+ .addColumn("createdAt", column("timestamp"));
16
+ })
17
+ .addTable("posts", (t) => {
18
+ return t
19
+ .addColumn("id", idColumn())
20
+ .addColumn("title", column("string"))
21
+ .addColumn("userId", referenceColumn())
22
+ .addColumn("viewCount", column("integer").defaultTo(0))
23
+ .addColumn("publishedAt", column("timestamp").nullable());
24
+ })
25
+ .addReference("author", {
26
+ type: "one",
27
+ from: { table: "posts", column: "userId" },
28
+ to: { table: "users", column: "id" },
29
+ });
30
+ });
31
+
32
+ const usersTable = testSchema.tables.users;
33
+ const postsTable = testSchema.tables.posts;
34
+
35
+ describe("basic encoding", () => {
36
+ it("should encode string values", () => {
37
+ const result = encodeValues(
38
+ { id: "user1", name: "John", email: "john@example.com" },
39
+ usersTable,
40
+ false,
41
+ "sqlite",
42
+ );
43
+
44
+ expect(result).toEqual({
45
+ id: "user1",
46
+ name: "John",
47
+ email: "john@example.com",
48
+ });
49
+ });
50
+
51
+ it("should encode integer values", () => {
52
+ const result = encodeValues({ age: 25 }, usersTable, false, "sqlite");
53
+
54
+ expect(result).toEqual({ age: 25 });
55
+ });
56
+
57
+ it("should encode boolean values for sqlite", () => {
58
+ const result = encodeValues({ isActive: true }, usersTable, false, "sqlite");
59
+
60
+ expect(result).toEqual({ isActive: 1 });
61
+ });
62
+
63
+ it("should encode boolean values for postgresql", () => {
64
+ const result = encodeValues({ isActive: true }, usersTable, false, "postgresql");
65
+
66
+ expect(result).toEqual({ isActive: true });
67
+ });
68
+ });
69
+
70
+ describe("date encoding", () => {
71
+ it("should encode Date to number for sqlite", () => {
72
+ const date = new Date("2024-01-15T10:30:00Z");
73
+ const result = encodeValues({ createdAt: date }, usersTable, false, "sqlite");
74
+
75
+ expect(result).toEqual({ createdAt: date.getTime() });
76
+ });
77
+
78
+ it("should keep Date as Date for postgresql", () => {
79
+ const date = new Date("2024-01-15T10:30:00Z");
80
+ const result = encodeValues({ createdAt: date }, usersTable, false, "postgresql");
81
+
82
+ expect(result).toEqual({ createdAt: date });
83
+ });
84
+
85
+ it("should keep Date as Date for mysql", () => {
86
+ const date = new Date("2024-01-15T10:30:00Z");
87
+ const result = encodeValues({ createdAt: date }, usersTable, false, "mysql");
88
+
89
+ expect(result).toEqual({ createdAt: date });
90
+ });
91
+ });
92
+
93
+ describe("nullable columns", () => {
94
+ it("should handle null values", () => {
95
+ const result = encodeValues({ age: null }, usersTable, false, "sqlite");
96
+
97
+ expect(result).toEqual({ age: null });
98
+ });
99
+
100
+ it("should omit undefined values", () => {
101
+ const result = encodeValues({ age: undefined }, usersTable, false, "sqlite");
102
+
103
+ expect(result).toEqual({});
104
+ });
105
+
106
+ it("should handle nullable timestamp", () => {
107
+ const result = encodeValues({ publishedAt: null }, postsTable, false, "sqlite");
108
+
109
+ expect(result).toEqual({ publishedAt: null });
110
+ });
111
+ });
112
+
113
+ describe("default value generation", () => {
114
+ it("should generate runtime defaults when generateDefault is true", () => {
115
+ const result = encodeValues(
116
+ { title: "Test Post", userId: "user1" },
117
+ postsTable,
118
+ true,
119
+ "sqlite",
120
+ );
121
+
122
+ expect(result["title"]).toBe("Test Post");
123
+ expect(result["userId"]).instanceOf(ReferenceSubquery);
124
+ // viewCount has static default (defaultTo), so it's omitted to let DB handle it
125
+ expect(result["viewCount"]).toBeUndefined();
126
+ expect(result["id"]).toBeDefined();
127
+ expect(typeof result["id"]).toBe("string");
128
+ });
129
+
130
+ it("should not generate default values when generateDefault is false", () => {
131
+ const result = encodeValues(
132
+ { title: "Test Post", userId: "user1" },
133
+ postsTable,
134
+ false,
135
+ "sqlite",
136
+ );
137
+
138
+ expect(result).toEqual({
139
+ title: "Test Post",
140
+ userId: expect.any(ReferenceSubquery),
141
+ });
142
+ });
143
+
144
+ it("should not override explicitly provided values", () => {
145
+ const result = encodeValues(
146
+ { title: "Test Post", userId: "user1", viewCount: 100 },
147
+ postsTable,
148
+ true,
149
+ "sqlite",
150
+ );
151
+
152
+ expect(result["viewCount"]).toBe(100);
153
+ });
154
+ });
155
+
156
+ describe("complete record encoding", () => {
157
+ it("should encode all fields correctly", () => {
158
+ const date = new Date("2024-01-15T10:30:00Z");
159
+ const result = encodeValues(
160
+ {
161
+ id: "user1",
162
+ name: "Alice",
163
+ email: "alice@example.com",
164
+ age: 30,
165
+ isActive: false,
166
+ createdAt: date,
167
+ },
168
+ usersTable,
169
+ false,
170
+ "sqlite",
171
+ );
172
+
173
+ expect(result).toEqual({
174
+ id: "user1",
175
+ name: "Alice",
176
+ email: "alice@example.com",
177
+ age: 30,
178
+ isActive: 0,
179
+ createdAt: date.getTime(),
180
+ });
181
+ });
182
+ });
183
+
184
+ describe("FragnoId encoding", () => {
185
+ it("should encode FragnoId with external ID only", () => {
186
+ const fragnoId = FragnoId.fromExternal("user123", 1);
187
+ const result = encodeValues({ id: fragnoId, name: "John" }, usersTable, false, "postgresql");
188
+
189
+ expect(result).toEqual({
190
+ id: "user123",
191
+ name: "John",
192
+ });
193
+ });
194
+
195
+ it("should encode FragnoId with both IDs", () => {
196
+ const fragnoId = new FragnoId({
197
+ externalId: "user123",
198
+ internalId: BigInt(456),
199
+ version: 1,
200
+ });
201
+ const result = encodeValues({ id: fragnoId, name: "John" }, usersTable, false, "postgresql");
202
+
203
+ expect(result).toEqual({
204
+ id: "user123",
205
+ name: "John",
206
+ });
207
+ });
208
+
209
+ it("should encode FragnoId in reference columns", () => {
210
+ const fragnoId = new FragnoId({
211
+ externalId: "user123",
212
+ internalId: BigInt(456),
213
+ version: 1,
214
+ });
215
+ const result = encodeValues(
216
+ { title: "Test Post", userId: fragnoId },
217
+ postsTable,
218
+ false,
219
+ "postgresql",
220
+ );
221
+
222
+ // Reference columns should use the internal ID (bigint)
223
+ expect(result).toEqual({
224
+ title: "Test Post",
225
+ userId: BigInt(456),
226
+ });
227
+ });
228
+
229
+ it("should fallback to external ID for reference when internal ID unavailable", () => {
230
+ const fragnoId = new FragnoId({
231
+ externalId: "user123",
232
+ version: 1,
233
+ });
234
+ const result = encodeValues(
235
+ { title: "Test Post", userId: fragnoId },
236
+ postsTable,
237
+ false,
238
+ "postgresql",
239
+ );
240
+
241
+ expect(result).toEqual({
242
+ title: "Test Post",
243
+ userId: "user123",
244
+ });
245
+ });
246
+
247
+ it("should handle FragnoId across different providers", () => {
248
+ const fragnoId = new FragnoId({
249
+ externalId: "user123",
250
+ internalId: BigInt(456),
251
+ version: 1,
252
+ });
253
+ const testData = { id: fragnoId, name: "John" };
254
+
255
+ // Test across providers
256
+ const sqliteResult = encodeValues(testData, usersTable, false, "sqlite");
257
+ const postgresqlResult = encodeValues(testData, usersTable, false, "postgresql");
258
+ const mysqlResult = encodeValues(testData, usersTable, false, "mysql");
259
+
260
+ expect(sqliteResult).toEqual({ id: "user123", name: "John" });
261
+ expect(postgresqlResult).toEqual({ id: "user123", name: "John" });
262
+ expect(mysqlResult).toEqual({ id: "user123", name: "John" });
263
+ });
264
+ });
265
+ });
266
+
267
+ describe("decodeResult", () => {
268
+ const testSchema = schema((s) => {
269
+ return s
270
+ .addTable("users", (t) => {
271
+ return t
272
+ .addColumn("id", idColumn())
273
+ .addColumn("name", column("string"))
274
+ .addColumn("email", column("string"))
275
+ .addColumn("age", column("integer").nullable())
276
+ .addColumn("isActive", column("bool"))
277
+ .addColumn("createdAt", column("timestamp"));
278
+ })
279
+ .addTable("posts", (t) => {
280
+ return t
281
+ .addColumn("id", idColumn())
282
+ .addColumn("title", column("string"))
283
+ .addColumn("content", column("string"))
284
+ .addColumn("userId", referenceColumn())
285
+ .addColumn("viewCount", column("integer"))
286
+ .addColumn("publishedAt", column("timestamp").nullable());
287
+ })
288
+ .addReference("author", {
289
+ type: "one",
290
+ from: { table: "posts", column: "userId" },
291
+ to: { table: "users", column: "id" },
292
+ });
293
+ });
294
+
295
+ const usersTable = testSchema.tables.users;
296
+ const postsTable = testSchema.tables.posts;
297
+
298
+ describe("basic decoding", () => {
299
+ it("should decode string values", () => {
300
+ const result = decodeResult(
301
+ { id: "user1", name: "John", email: "john@example.com" },
302
+ usersTable,
303
+ "sqlite",
304
+ );
305
+
306
+ expect(result).toEqual({
307
+ id: "user1",
308
+ name: "John",
309
+ email: "john@example.com",
310
+ });
311
+ });
312
+
313
+ it("should decode integer values", () => {
314
+ const result = decodeResult({ age: 25 }, usersTable, "sqlite");
315
+
316
+ expect(result).toEqual({ age: 25 });
317
+ });
318
+
319
+ it("should decode boolean values from sqlite", () => {
320
+ const result = decodeResult({ isActive: 1 }, usersTable, "sqlite");
321
+
322
+ expect(result).toEqual({ isActive: true });
323
+ });
324
+
325
+ it("should decode boolean false from sqlite", () => {
326
+ const result = decodeResult({ isActive: 0 }, usersTable, "sqlite");
327
+
328
+ expect(result).toEqual({ isActive: false });
329
+ });
330
+
331
+ it("should decode boolean values from postgresql", () => {
332
+ const result = decodeResult({ isActive: true }, usersTable, "postgresql");
333
+
334
+ expect(result).toEqual({ isActive: true });
335
+ });
336
+ });
337
+
338
+ describe("date decoding", () => {
339
+ it("should decode number to Date for sqlite timestamp", () => {
340
+ const timestamp = 1705317000000;
341
+ const result = decodeResult({ createdAt: timestamp }, usersTable, "sqlite");
342
+
343
+ expect(result["createdAt"]).toBeInstanceOf(Date);
344
+ expect((result["createdAt"] as Date).getTime()).toBe(timestamp);
345
+ });
346
+
347
+ it("should decode ISO string to Date for sqlite timestamp", () => {
348
+ const isoString = "2024-01-15T10:30:00.000Z";
349
+ const result = decodeResult({ createdAt: isoString }, usersTable, "sqlite");
350
+
351
+ expect(result["createdAt"]).toBeInstanceOf(Date);
352
+ expect((result["createdAt"] as Date).toISOString()).toBe(isoString);
353
+ });
354
+
355
+ it("should keep Date as Date for postgresql", () => {
356
+ const date = new Date("2024-01-15T10:30:00Z");
357
+ const result = decodeResult({ createdAt: date }, usersTable, "postgresql");
358
+
359
+ expect(result["createdAt"]).toBe(date);
360
+ });
361
+ });
362
+
363
+ describe("nullable columns", () => {
364
+ it("should handle null values", () => {
365
+ const result = decodeResult({ age: null }, usersTable, "sqlite");
366
+
367
+ expect(result).toEqual({ age: null });
368
+ });
369
+
370
+ it("should handle nullable timestamp", () => {
371
+ const result = decodeResult({ publishedAt: null }, postsTable, "sqlite");
372
+
373
+ expect(result).toEqual({ publishedAt: null });
374
+ });
375
+ });
376
+
377
+ describe("relation decoding", () => {
378
+ it("should decode relation data with colon pattern", () => {
379
+ const result = decodeResult(
380
+ {
381
+ id: "post1",
382
+ title: "My Post",
383
+ "author:id": "user1",
384
+ "author:name": "Alice",
385
+ "author:email": "alice@example.com",
386
+ },
387
+ postsTable,
388
+ "sqlite",
389
+ );
390
+
391
+ expect(result).toEqual({
392
+ id: "post1",
393
+ title: "My Post",
394
+ author: {
395
+ id: "user1",
396
+ name: "Alice",
397
+ email: "alice@example.com",
398
+ },
399
+ });
400
+ });
401
+
402
+ it("should decode relation data with type conversions", () => {
403
+ const timestamp = 1705317000000;
404
+ const result = decodeResult(
405
+ {
406
+ id: "post1",
407
+ title: "My Post",
408
+ "author:id": "user1",
409
+ "author:isActive": 1,
410
+ "author:createdAt": timestamp,
411
+ },
412
+ postsTable,
413
+ "sqlite",
414
+ );
415
+
416
+ expect(result["author"]).toEqual({
417
+ id: "user1",
418
+ isActive: true,
419
+ createdAt: new Date(timestamp),
420
+ });
421
+ });
422
+
423
+ it("should skip unknown relations", () => {
424
+ const result = decodeResult(
425
+ {
426
+ id: "post1",
427
+ title: "My Post",
428
+ "unknownRelation:field": "value",
429
+ },
430
+ postsTable,
431
+ "sqlite",
432
+ );
433
+
434
+ expect(result).toEqual({
435
+ id: "post1",
436
+ title: "My Post",
437
+ });
438
+ });
439
+
440
+ it("should skip unknown columns in relations", () => {
441
+ const result = decodeResult(
442
+ {
443
+ id: "post1",
444
+ title: "My Post",
445
+ "author:id": "user1",
446
+ "author:unknownField": "value",
447
+ },
448
+ postsTable,
449
+ "sqlite",
450
+ );
451
+
452
+ expect(result).toEqual({
453
+ id: "post1",
454
+ title: "My Post",
455
+ author: {
456
+ id: "user1",
457
+ },
458
+ });
459
+ });
460
+
461
+ it("should handle multiple relations in same result", () => {
462
+ const schemaWithMultipleRelations = schema((s) => {
463
+ return s
464
+ .addTable("users", (t) => {
465
+ return t.addColumn("id", idColumn()).addColumn("name", column("string"));
466
+ })
467
+ .addTable("categories", (t) => {
468
+ return t.addColumn("id", idColumn()).addColumn("name", column("string"));
469
+ })
470
+ .addTable("posts", (t) => {
471
+ return t
472
+ .addColumn("id", idColumn())
473
+ .addColumn("title", column("string"))
474
+ .addColumn("userId", referenceColumn())
475
+ .addColumn("categoryId", referenceColumn());
476
+ })
477
+ .addReference("author", {
478
+ type: "one",
479
+ from: { table: "posts", column: "userId" },
480
+ to: { table: "users", column: "id" },
481
+ })
482
+ .addReference("category", {
483
+ type: "one",
484
+ from: { table: "posts", column: "categoryId" },
485
+ to: { table: "categories", column: "id" },
486
+ });
487
+ });
488
+
489
+ const result = decodeResult(
490
+ {
491
+ id: "post1",
492
+ title: "My Post",
493
+ "author:id": "user1",
494
+ "author:name": "Alice",
495
+ "category:id": "cat1",
496
+ "category:name": "Technology",
497
+ },
498
+ schemaWithMultipleRelations.tables.posts,
499
+ "sqlite",
500
+ );
501
+
502
+ expect(result).toEqual({
503
+ id: "post1",
504
+ title: "My Post",
505
+ author: {
506
+ id: "user1",
507
+ name: "Alice",
508
+ },
509
+ category: {
510
+ id: "cat1",
511
+ name: "Technology",
512
+ },
513
+ });
514
+ });
515
+
516
+ it("should transform id columns in relations to FragnoId when _internalId and _version are present", () => {
517
+ const result = decodeResult(
518
+ {
519
+ id: "post1",
520
+ _internalId: BigInt(100),
521
+ _version: 0,
522
+ title: "My Post",
523
+ "author:id": "user1",
524
+ "author:_internalId": BigInt(200),
525
+ "author:_version": 0,
526
+ "author:name": "Alice",
527
+ },
528
+ postsTable,
529
+ "sqlite",
530
+ );
531
+
532
+ // Main table id should be FragnoId
533
+ assert(result["id"] instanceof FragnoId);
534
+ expect(result["id"].externalId).toBe("post1");
535
+ expect(result["id"].internalId).toBe(BigInt(100));
536
+
537
+ // Relation id should also be FragnoId (THIS IS THE BUG WE'RE FIXING)
538
+ expect(result["author"]).toBeDefined();
539
+ const author = result["author"] as Record<string, unknown>;
540
+ assert(author["id"] instanceof FragnoId);
541
+ expect(author["id"].externalId).toBe("user1");
542
+ expect(author["id"].internalId).toBe(BigInt(200));
543
+ expect(author["name"]).toBe("Alice");
544
+ });
545
+ });
546
+
547
+ describe("complete record decoding", () => {
548
+ it("should decode all fields correctly", () => {
549
+ const timestamp = 1705317000000;
550
+ const result = decodeResult(
551
+ {
552
+ id: "user1",
553
+ name: "Alice",
554
+ email: "alice@example.com",
555
+ age: 30,
556
+ isActive: 0,
557
+ createdAt: timestamp,
558
+ },
559
+ usersTable,
560
+ "sqlite",
561
+ );
562
+
563
+ expect(result).toEqual({
564
+ id: "user1",
565
+ name: "Alice",
566
+ email: "alice@example.com",
567
+ age: 30,
568
+ isActive: false,
569
+ createdAt: new Date(timestamp),
570
+ });
571
+ });
572
+ });
573
+
574
+ describe("edge cases", () => {
575
+ it("should handle empty result", () => {
576
+ const result = decodeResult({}, usersTable, "sqlite");
577
+
578
+ expect(result).toEqual({});
579
+ });
580
+
581
+ it("should handle result with only relation data", () => {
582
+ const result = decodeResult(
583
+ {
584
+ "author:id": "user1",
585
+ "author:name": "Alice",
586
+ },
587
+ postsTable,
588
+ "sqlite",
589
+ );
590
+
591
+ expect(result).toEqual({
592
+ author: {
593
+ id: "user1",
594
+ name: "Alice",
595
+ },
596
+ });
597
+ });
598
+
599
+ it("should handle mixed regular and relation data", () => {
600
+ const timestamp = 1705317000000;
601
+ const result = decodeResult(
602
+ {
603
+ id: "post1",
604
+ title: "My Post",
605
+ viewCount: 100,
606
+ publishedAt: timestamp,
607
+ "author:id": "user1",
608
+ "author:name": "Alice",
609
+ },
610
+ postsTable,
611
+ "sqlite",
612
+ );
613
+
614
+ expect(result).toEqual({
615
+ id: "post1",
616
+ title: "My Post",
617
+ viewCount: 100,
618
+ publishedAt: new Date(timestamp),
619
+ author: {
620
+ id: "user1",
621
+ name: "Alice",
622
+ },
623
+ });
624
+ });
625
+ });
626
+
627
+ describe("FragnoId decoding", () => {
628
+ it("should create FragnoId when both external and internal IDs are present", () => {
629
+ const result = decodeResult(
630
+ {
631
+ id: "user123",
632
+ _internalId: 456,
633
+ name: "John",
634
+ },
635
+ usersTable,
636
+ "postgresql",
637
+ );
638
+
639
+ const fragnoId = result["id"];
640
+ assert(fragnoId instanceof FragnoId);
641
+
642
+ expect(fragnoId.externalId).toBe("user123");
643
+ expect(fragnoId.internalId).toBe(456);
644
+ expect(result["name"]).toBe("John");
645
+ });
646
+
647
+ it("should create FragnoId from string internal ID", () => {
648
+ const result = decodeResult(
649
+ {
650
+ id: "user123",
651
+ _internalId: "456",
652
+ name: "John",
653
+ },
654
+ usersTable,
655
+ "postgresql",
656
+ );
657
+
658
+ const fragnoId = result["id"];
659
+ assert(fragnoId instanceof FragnoId);
660
+
661
+ expect(fragnoId.externalId).toBe("user123");
662
+ expect(fragnoId.internalId).toBe(BigInt(456));
663
+ expect(result["name"]).toBe("John");
664
+ });
665
+
666
+ it("should return regular string when internal ID is missing", () => {
667
+ const result = decodeResult(
668
+ {
669
+ id: "user123",
670
+ name: "John",
671
+ },
672
+ usersTable,
673
+ "postgresql",
674
+ );
675
+
676
+ expect(result).toEqual({
677
+ id: "user123",
678
+ name: "John",
679
+ });
680
+ expect(result["id"]).not.toBeInstanceOf(FragnoId);
681
+ });
682
+
683
+ it("should handle FragnoId creation across different providers", () => {
684
+ const testData = {
685
+ id: "user123",
686
+ _internalId: 456,
687
+ name: "John",
688
+ };
689
+
690
+ // Test across providers
691
+ const sqliteResult = decodeResult(testData, usersTable, "sqlite");
692
+ const postgresqlResult = decodeResult(testData, usersTable, "postgresql");
693
+ const mysqlResult = decodeResult(testData, usersTable, "mysql");
694
+
695
+ // All should create FragnoId objects
696
+ expect(sqliteResult["id"]).toBeInstanceOf(FragnoId);
697
+ expect(postgresqlResult["id"]).toBeInstanceOf(FragnoId);
698
+ expect(mysqlResult["id"]).toBeInstanceOf(FragnoId);
699
+
700
+ expect((sqliteResult["id"] as FragnoId).externalId).toBe("user123");
701
+ expect((sqliteResult["id"] as FragnoId).internalId).toBe(456);
702
+ });
703
+
704
+ it("should create FragnoId in relation data when both IDs present", () => {
705
+ const result = decodeResult(
706
+ {
707
+ id: "post1",
708
+ title: "My Post",
709
+ "author:id": "user123",
710
+ "author:_internalId": 456,
711
+ "author:_version": 0,
712
+ "author:name": "Alice",
713
+ },
714
+ postsTable,
715
+ "postgresql",
716
+ );
717
+
718
+ expect(result["id"]).toBe("post1");
719
+ expect(result["title"]).toBe("My Post");
720
+ // Relations now correctly create FragnoId objects when both IDs are present (thanks to recursive decoding)
721
+ const author: Record<string, unknown> = result["author"] as Record<string, unknown>;
722
+ assert(author["id"] instanceof FragnoId);
723
+ expect(author["id"].externalId).toBe("user123");
724
+ expect(author["id"].internalId).toBe(456);
725
+ expect(author["name"]).toBe("Alice");
726
+ });
727
+
728
+ it("should return regular string in relation data when internal ID missing", () => {
729
+ const result = decodeResult(
730
+ {
731
+ id: "post1",
732
+ title: "My Post",
733
+ "author:id": "user123",
734
+ "author:name": "Alice",
735
+ },
736
+ postsTable,
737
+ "postgresql",
738
+ );
739
+
740
+ expect(result).toEqual({
741
+ id: "post1",
742
+ title: "My Post",
743
+ author: {
744
+ id: "user123",
745
+ name: "Alice",
746
+ },
747
+ });
748
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
749
+ expect((result["author"] as any)["id"]).not.toBeInstanceOf(FragnoId);
750
+ });
751
+
752
+ it("should handle complete record with FragnoId creation", () => {
753
+ const timestamp = 1705317000000;
754
+ const result = decodeResult(
755
+ {
756
+ id: "user123",
757
+ _internalId: 456,
758
+ name: "Alice",
759
+ email: "alice@example.com",
760
+ age: 30,
761
+ isActive: true,
762
+ createdAt: timestamp,
763
+ },
764
+ usersTable,
765
+ "sqlite",
766
+ );
767
+
768
+ expect(result["id"]).toBeInstanceOf(FragnoId);
769
+ expect((result["id"] as FragnoId).externalId).toBe("user123");
770
+ expect((result["id"] as FragnoId).internalId).toBe(456);
771
+ expect(result["name"]).toBe("Alice");
772
+ expect(result["email"]).toBe("alice@example.com");
773
+ expect(result["age"]).toBe(30);
774
+ expect(result["isActive"]).toBe(true);
775
+ expect(result["createdAt"]).toEqual(new Date(timestamp));
776
+ });
777
+
778
+ it("should handle FragnoId with numeric internal ID from database", () => {
779
+ const result = decodeResult(
780
+ {
781
+ id: "user123",
782
+ _internalId: 789, // Numeric from database
783
+ name: "John",
784
+ },
785
+ usersTable,
786
+ "postgresql",
787
+ );
788
+
789
+ expect(result["id"]).toBeInstanceOf(FragnoId);
790
+ expect((result["id"] as FragnoId).externalId).toBe("user123");
791
+ expect((result["id"] as FragnoId).internalId).toBe(789);
792
+ expect(result["name"]).toBe("John");
793
+ });
794
+ });
795
+ });