@buenojs/bueno 0.8.4 → 0.8.6

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 (234) hide show
  1. package/README.md +264 -17
  2. package/dist/cli/{index.js → bin.js} +413 -332
  3. package/dist/container/index.js +273 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/graphql/index.js +2156 -0
  8. package/dist/health/index.js +364 -0
  9. package/dist/i18n/index.js +345 -0
  10. package/dist/index.js +9694 -5047
  11. package/dist/jobs/index.js +819 -0
  12. package/dist/lock/index.js +367 -0
  13. package/dist/logger/index.js +281 -0
  14. package/dist/metrics/index.js +289 -0
  15. package/dist/middleware/index.js +77 -0
  16. package/dist/migrations/index.js +571 -0
  17. package/dist/modules/index.js +3411 -0
  18. package/dist/notification/index.js +484 -0
  19. package/dist/observability/index.js +331 -0
  20. package/dist/openapi/index.js +795 -0
  21. package/dist/orm/index.js +1356 -0
  22. package/dist/router/index.js +886 -0
  23. package/dist/rpc/index.js +691 -0
  24. package/dist/schema/index.js +400 -0
  25. package/dist/telemetry/index.js +595 -0
  26. package/dist/template/index.js +640 -0
  27. package/dist/templates/index.js +640 -0
  28. package/dist/testing/index.js +1111 -0
  29. package/dist/types/index.js +60 -0
  30. package/llms.txt +231 -0
  31. package/package.json +125 -27
  32. package/src/cache/index.ts +2 -1
  33. package/src/cli/ARCHITECTURE.md +3 -3
  34. package/src/cli/bin.ts +2 -2
  35. package/src/cli/commands/build.ts +183 -165
  36. package/src/cli/commands/dev.ts +96 -89
  37. package/src/cli/commands/generate.ts +142 -111
  38. package/src/cli/commands/help.ts +20 -16
  39. package/src/cli/commands/index.ts +3 -6
  40. package/src/cli/commands/migration.ts +124 -105
  41. package/src/cli/commands/new.ts +294 -232
  42. package/src/cli/commands/start.ts +81 -79
  43. package/src/cli/core/args.ts +68 -50
  44. package/src/cli/core/console.ts +89 -95
  45. package/src/cli/core/index.ts +4 -4
  46. package/src/cli/core/prompt.ts +65 -62
  47. package/src/cli/core/spinner.ts +23 -20
  48. package/src/cli/index.ts +46 -38
  49. package/src/cli/templates/database/index.ts +37 -18
  50. package/src/cli/templates/database/mysql.ts +3 -3
  51. package/src/cli/templates/database/none.ts +2 -2
  52. package/src/cli/templates/database/postgresql.ts +3 -3
  53. package/src/cli/templates/database/sqlite.ts +3 -3
  54. package/src/cli/templates/deploy.ts +29 -26
  55. package/src/cli/templates/docker.ts +41 -30
  56. package/src/cli/templates/frontend/index.ts +33 -15
  57. package/src/cli/templates/frontend/none.ts +2 -2
  58. package/src/cli/templates/frontend/react.ts +18 -18
  59. package/src/cli/templates/frontend/solid.ts +15 -15
  60. package/src/cli/templates/frontend/svelte.ts +17 -17
  61. package/src/cli/templates/frontend/vue.ts +15 -15
  62. package/src/cli/templates/generators/index.ts +29 -29
  63. package/src/cli/templates/generators/types.ts +21 -21
  64. package/src/cli/templates/index.ts +6 -6
  65. package/src/cli/templates/project/api.ts +37 -36
  66. package/src/cli/templates/project/default.ts +25 -25
  67. package/src/cli/templates/project/fullstack.ts +28 -26
  68. package/src/cli/templates/project/index.ts +55 -16
  69. package/src/cli/templates/project/minimal.ts +17 -12
  70. package/src/cli/templates/project/types.ts +10 -5
  71. package/src/cli/templates/project/website.ts +15 -15
  72. package/src/cli/utils/fs.ts +55 -41
  73. package/src/cli/utils/index.ts +3 -3
  74. package/src/cli/utils/strings.ts +47 -33
  75. package/src/cli/utils/version.ts +14 -8
  76. package/src/config/env-validation.ts +100 -0
  77. package/src/config/env.ts +169 -41
  78. package/src/config/index.ts +28 -20
  79. package/src/config/loader.ts +25 -16
  80. package/src/config/merge.ts +21 -10
  81. package/src/config/types.ts +566 -25
  82. package/src/config/validation.ts +215 -7
  83. package/src/container/forward-ref.ts +22 -22
  84. package/src/container/index.ts +34 -12
  85. package/src/context/index.ts +11 -1
  86. package/src/database/index.ts +7 -190
  87. package/src/database/orm/builder.ts +457 -0
  88. package/src/database/orm/casts/index.ts +130 -0
  89. package/src/database/orm/casts/types.ts +25 -0
  90. package/src/database/orm/compiler.ts +304 -0
  91. package/src/database/orm/hooks/index.ts +114 -0
  92. package/src/database/orm/index.ts +61 -0
  93. package/src/database/orm/model-registry.ts +59 -0
  94. package/src/database/orm/model.ts +821 -0
  95. package/src/database/orm/relationships/base.ts +146 -0
  96. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  97. package/src/database/orm/relationships/belongs-to.ts +56 -0
  98. package/src/database/orm/relationships/has-many.ts +45 -0
  99. package/src/database/orm/relationships/has-one.ts +41 -0
  100. package/src/database/orm/relationships/index.ts +11 -0
  101. package/src/database/orm/scopes/index.ts +55 -0
  102. package/src/events/__tests__/event-system.test.ts +235 -0
  103. package/src/events/config.ts +238 -0
  104. package/src/events/example-usage.ts +185 -0
  105. package/src/events/index.ts +278 -0
  106. package/src/events/manager.ts +385 -0
  107. package/src/events/registry.ts +182 -0
  108. package/src/events/types.ts +124 -0
  109. package/src/frontend/api-routes.ts +65 -23
  110. package/src/frontend/bundler.ts +76 -34
  111. package/src/frontend/console-client.ts +2 -2
  112. package/src/frontend/console-stream.ts +94 -38
  113. package/src/frontend/dev-server.ts +94 -46
  114. package/src/frontend/file-router.ts +61 -19
  115. package/src/frontend/frameworks/index.ts +37 -10
  116. package/src/frontend/frameworks/react.ts +10 -8
  117. package/src/frontend/frameworks/solid.ts +11 -9
  118. package/src/frontend/frameworks/svelte.ts +15 -9
  119. package/src/frontend/frameworks/vue.ts +13 -11
  120. package/src/frontend/hmr-client.ts +12 -10
  121. package/src/frontend/hmr.ts +146 -103
  122. package/src/frontend/index.ts +14 -5
  123. package/src/frontend/islands.ts +41 -22
  124. package/src/frontend/isr.ts +59 -37
  125. package/src/frontend/layout.ts +36 -21
  126. package/src/frontend/ssr/react.ts +74 -27
  127. package/src/frontend/ssr/solid.ts +54 -20
  128. package/src/frontend/ssr/svelte.ts +48 -14
  129. package/src/frontend/ssr/vue.ts +50 -18
  130. package/src/frontend/ssr.ts +83 -39
  131. package/src/frontend/types.ts +91 -56
  132. package/src/graphql/built-in-engine.ts +598 -0
  133. package/src/graphql/context-builder.ts +110 -0
  134. package/src/graphql/decorators.ts +358 -0
  135. package/src/graphql/execution-pipeline.ts +227 -0
  136. package/src/graphql/graphql-module.ts +563 -0
  137. package/src/graphql/index.ts +101 -0
  138. package/src/graphql/metadata.ts +237 -0
  139. package/src/graphql/schema-builder.ts +319 -0
  140. package/src/graphql/subscription-handler.ts +283 -0
  141. package/src/graphql/types.ts +324 -0
  142. package/src/health/index.ts +21 -9
  143. package/src/i18n/engine.ts +305 -0
  144. package/src/i18n/index.ts +38 -0
  145. package/src/i18n/loader.ts +218 -0
  146. package/src/i18n/middleware.ts +164 -0
  147. package/src/i18n/negotiator.ts +162 -0
  148. package/src/i18n/types.ts +158 -0
  149. package/src/index.ts +182 -27
  150. package/src/jobs/drivers/memory.ts +315 -0
  151. package/src/jobs/drivers/redis.ts +459 -0
  152. package/src/jobs/index.ts +30 -0
  153. package/src/jobs/queue.ts +281 -0
  154. package/src/jobs/types.ts +295 -0
  155. package/src/jobs/worker.ts +380 -0
  156. package/src/logger/index.ts +1 -3
  157. package/src/logger/transports/index.ts +62 -22
  158. package/src/metrics/index.ts +25 -16
  159. package/src/migrations/index.ts +9 -0
  160. package/src/modules/filters.ts +13 -17
  161. package/src/modules/guards.ts +49 -26
  162. package/src/modules/index.ts +457 -299
  163. package/src/modules/interceptors.ts +58 -20
  164. package/src/modules/lazy.ts +11 -19
  165. package/src/modules/lifecycle.ts +15 -7
  166. package/src/modules/metadata.ts +15 -5
  167. package/src/modules/pipes.ts +94 -72
  168. package/src/notification/channels/base.ts +68 -0
  169. package/src/notification/channels/email.ts +105 -0
  170. package/src/notification/channels/push.ts +104 -0
  171. package/src/notification/channels/sms.ts +105 -0
  172. package/src/notification/channels/whatsapp.ts +104 -0
  173. package/src/notification/index.ts +48 -0
  174. package/src/notification/service.ts +354 -0
  175. package/src/notification/types.ts +344 -0
  176. package/src/observability/__tests__/observability.test.ts +483 -0
  177. package/src/observability/breadcrumbs.ts +114 -0
  178. package/src/observability/index.ts +136 -0
  179. package/src/observability/interceptor.ts +85 -0
  180. package/src/observability/service.ts +303 -0
  181. package/src/observability/trace.ts +37 -0
  182. package/src/observability/types.ts +196 -0
  183. package/src/openapi/__tests__/decorators.test.ts +335 -0
  184. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  185. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  186. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  187. package/src/openapi/decorators.ts +328 -0
  188. package/src/openapi/document-builder.ts +274 -0
  189. package/src/openapi/index.ts +112 -0
  190. package/src/openapi/metadata.ts +112 -0
  191. package/src/openapi/route-scanner.ts +289 -0
  192. package/src/openapi/schema-generator.ts +256 -0
  193. package/src/openapi/swagger-module.ts +166 -0
  194. package/src/openapi/types.ts +398 -0
  195. package/src/orm/index.ts +10 -0
  196. package/src/rpc/index.ts +3 -1
  197. package/src/schema/index.ts +9 -0
  198. package/src/security/index.ts +15 -6
  199. package/src/ssg/index.ts +9 -8
  200. package/src/telemetry/index.ts +76 -22
  201. package/src/template/index.ts +7 -0
  202. package/src/templates/engine.ts +224 -0
  203. package/src/templates/index.ts +9 -0
  204. package/src/templates/loader.ts +331 -0
  205. package/src/templates/renderers/markdown.ts +212 -0
  206. package/src/templates/renderers/simple.ts +269 -0
  207. package/src/templates/types.ts +154 -0
  208. package/src/testing/index.ts +100 -27
  209. package/src/types/optional-deps.d.ts +347 -187
  210. package/src/validation/index.ts +92 -2
  211. package/src/validation/schemas.ts +536 -0
  212. package/tests/integration/cli.test.ts +19 -19
  213. package/tests/integration/fullstack.test.ts +4 -4
  214. package/tests/unit/cli.test.ts +1 -1
  215. package/tests/unit/database.test.ts +2 -72
  216. package/tests/unit/env-validation.test.ts +166 -0
  217. package/tests/unit/events.test.ts +910 -0
  218. package/tests/unit/graphql.test.ts +991 -0
  219. package/tests/unit/i18n.test.ts +455 -0
  220. package/tests/unit/jobs.test.ts +493 -0
  221. package/tests/unit/notification.test.ts +988 -0
  222. package/tests/unit/observability.test.ts +453 -0
  223. package/tests/unit/orm/builder.test.ts +323 -0
  224. package/tests/unit/orm/casts.test.ts +179 -0
  225. package/tests/unit/orm/compiler.test.ts +220 -0
  226. package/tests/unit/orm/eager-loading.test.ts +285 -0
  227. package/tests/unit/orm/hooks.test.ts +191 -0
  228. package/tests/unit/orm/model.test.ts +373 -0
  229. package/tests/unit/orm/relationships.test.ts +303 -0
  230. package/tests/unit/orm/scopes.test.ts +74 -0
  231. package/tests/unit/templates-simple.test.ts +53 -0
  232. package/tests/unit/templates.test.ts +454 -0
  233. package/tests/unit/validation.test.ts +18 -24
  234. package/tsconfig.json +11 -3
@@ -0,0 +1,323 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { Database } from "../../../src/database";
3
+ import { Model } from "../../../src/database/orm/model";
4
+ import { setDefaultDatabase, clearDefaultDatabase, clearModelDatabaseRegistry } from "../../../src/database/orm";
5
+
6
+ let db: Database;
7
+
8
+ class User extends Model {
9
+ static table = "users";
10
+ static timestamps = false;
11
+ fillable = ["name", "email", "age", "status"];
12
+ }
13
+
14
+ class Post extends Model {
15
+ static table = "posts";
16
+ static timestamps = false;
17
+ fillable = ["user_id", "title"];
18
+ }
19
+
20
+ beforeEach(async () => {
21
+ db = new Database({ url: ":memory:" });
22
+ await db.connect();
23
+ setDefaultDatabase(db);
24
+ clearModelDatabaseRegistry();
25
+ await db.raw(
26
+ "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, age INTEGER, status TEXT DEFAULT 'active')",
27
+ );
28
+ });
29
+
30
+ afterEach(async () => {
31
+ clearDefaultDatabase();
32
+ clearModelDatabaseRegistry();
33
+ await db.close();
34
+ });
35
+
36
+ describe("OrmQueryBuilder", () => {
37
+ describe("chainable methods", () => {
38
+ test("select() sets columns", async () => {
39
+ await User.create({ name: "John", email: "john@example.com" });
40
+ const builder = User.query().select("name", "email");
41
+ const results = await builder.get();
42
+ expect(results[0]).toHaveProperty("name");
43
+ expect(results[0]).toHaveProperty("email");
44
+ });
45
+
46
+ test("addSelect() adds to columns", async () => {
47
+ await User.create({ name: "John", email: "john@example.com" });
48
+ const builder = User.query().select("name").addSelect("email");
49
+ const results = await builder.get();
50
+ expect(results[0]).toHaveProperty("name");
51
+ expect(results[0]).toHaveProperty("email");
52
+ });
53
+
54
+ test("distinct() removes duplicates", async () => {
55
+ await User.create({ name: "John", email: "john@example.com" });
56
+ await User.create({ name: "John", email: "jane@example.com" });
57
+ const builder = User.query().select("name").distinct();
58
+ const results = await builder.get();
59
+ expect(results.length).toBe(1);
60
+ });
61
+
62
+ test("where(col, val) - equality shorthand", async () => {
63
+ await User.create({ name: "John", status: "active" });
64
+ await User.create({ name: "Jane", status: "inactive" });
65
+ const results = await User.query().where("status", "active").get();
66
+ expect(results.length).toBe(1);
67
+ expect(results[0].name).toBe("John");
68
+ });
69
+
70
+ test("where(col, op, val) - explicit operator", async () => {
71
+ await User.create({ name: "John", age: 25 });
72
+ await User.create({ name: "Jane", age: 30 });
73
+ const results = await User.query().where("age", ">", 28).get();
74
+ expect(results.length).toBe(1);
75
+ expect(results[0].age).toBe(30);
76
+ });
77
+
78
+ test("orWhere()", async () => {
79
+ await User.create({ name: "John", status: "admin" });
80
+ await User.create({ name: "Jane", status: "moderator" });
81
+ await User.create({ name: "Bob", status: "user" });
82
+ const results = await User.query()
83
+ .where("status", "admin")
84
+ .orWhere("status", "moderator")
85
+ .get();
86
+ expect(results.length).toBe(2);
87
+ });
88
+
89
+ test("whereRaw()", async () => {
90
+ await User.create({ name: "John", age: 25 });
91
+ await User.create({ name: "Jane", age: 30 });
92
+ const results = await User.query().whereRaw("age > 27").get();
93
+ expect(results.length).toBe(1);
94
+ });
95
+
96
+ test("whereIn()", async () => {
97
+ await User.create({ name: "John", status: "active" });
98
+ await User.create({ name: "Jane", status: "pending" });
99
+ await User.create({ name: "Bob", status: "inactive" });
100
+ const results = await User.query().whereIn("status", ["active", "pending"]).get();
101
+ expect(results.length).toBe(2);
102
+ });
103
+
104
+ test("whereNotIn()", async () => {
105
+ await User.create({ name: "John", status: "active" });
106
+ await User.create({ name: "Jane", status: "pending" });
107
+ await User.create({ name: "Bob", status: "inactive" });
108
+ const results = await User.query().whereNotIn("status", ["inactive"]).get();
109
+ expect(results.length).toBe(2);
110
+ });
111
+
112
+ test("whereNull()", async () => {
113
+ await User.create({ name: "John", email: "john@example.com" });
114
+ await User.create({ name: "Jane", email: null });
115
+ const results = await User.query().whereNull("email").get();
116
+ expect(results.length).toBe(1);
117
+ expect(results[0].name).toBe("Jane");
118
+ });
119
+
120
+ test("whereNotNull()", async () => {
121
+ await User.create({ name: "John", email: "john@example.com" });
122
+ await User.create({ name: "Jane", email: null });
123
+ const results = await User.query().whereNotNull("email").get();
124
+ expect(results.length).toBe(1);
125
+ expect(results[0].name).toBe("John");
126
+ });
127
+
128
+ test("whereBetween()", async () => {
129
+ await User.create({ name: "John", age: 25 });
130
+ await User.create({ name: "Jane", age: 30 });
131
+ await User.create({ name: "Bob", age: 35 });
132
+ const results = await User.query().whereBetween("age", [27, 32]).get();
133
+ expect(results.length).toBe(1);
134
+ expect(results[0].age).toBe(30);
135
+ });
136
+
137
+ test("orderBy() ASC", async () => {
138
+ await User.create({ name: "Charlie" });
139
+ await User.create({ name: "Alice" });
140
+ await User.create({ name: "Bob" });
141
+ const results = await User.query().orderBy("name", "asc").get();
142
+ expect(results[0].name).toBe("Alice");
143
+ expect(results[1].name).toBe("Bob");
144
+ expect(results[2].name).toBe("Charlie");
145
+ });
146
+
147
+ test("orderBy() DESC", async () => {
148
+ await User.create({ name: "Charlie" });
149
+ await User.create({ name: "Alice" });
150
+ await User.create({ name: "Bob" });
151
+ const results = await User.query().orderBy("name", "desc").get();
152
+ expect(results[0].name).toBe("Charlie");
153
+ expect(results[1].name).toBe("Bob");
154
+ expect(results[2].name).toBe("Alice");
155
+ });
156
+
157
+ test("limit()", async () => {
158
+ await User.create({ name: "John" });
159
+ await User.create({ name: "Jane" });
160
+ await User.create({ name: "Bob" });
161
+ const results = await User.query().limit(2).get();
162
+ expect(results.length).toBe(2);
163
+ });
164
+
165
+ test("offset()", async () => {
166
+ await User.create({ name: "A" });
167
+ await User.create({ name: "B" });
168
+ await User.create({ name: "C" });
169
+ const results = await User.query().orderBy("name").offset(1).limit(1).get();
170
+ expect(results.length).toBe(1);
171
+ expect(results[0].name).toBe("B");
172
+ });
173
+
174
+ test("clone() does not mutate original", async () => {
175
+ await User.create({ name: "Test", status: "active" });
176
+ const builder = User.query().where("status", "active");
177
+ const cloned = builder.clone().where("age", ">", 25);
178
+
179
+ const original = await builder.get();
180
+ expect(original.length >= 0).toBe(true);
181
+ });
182
+ });
183
+
184
+ describe("terminal methods", () => {
185
+ test("get() returns all rows", async () => {
186
+ await User.create({ name: "John" });
187
+ await User.create({ name: "Jane" });
188
+ const results = await User.query().get();
189
+ expect(results.length).toBe(2);
190
+ expect(results[0]).toHaveProperty("id");
191
+ });
192
+
193
+ test("first() returns first row or null", async () => {
194
+ await User.create({ name: "John" });
195
+ await User.create({ name: "Jane" });
196
+ const result = await User.query().first();
197
+ expect(result).not.toBeNull();
198
+ expect(result!.name).toBe("John");
199
+ });
200
+
201
+ test("first() returns null when empty", async () => {
202
+ const result = await User.query().where("name", "NonExistent").first();
203
+ expect(result).toBeNull();
204
+ });
205
+
206
+ test("count() returns number of rows", async () => {
207
+ await User.create({ name: "John" });
208
+ await User.create({ name: "Jane" });
209
+ const count = await User.query().count();
210
+ expect(count).toBe(2);
211
+ });
212
+
213
+ test("count() with WHERE clause", async () => {
214
+ await User.create({ name: "John", status: "active" });
215
+ await User.create({ name: "Jane", status: "inactive" });
216
+ const count = await User.query().where("status", "active").count();
217
+ expect(count).toBe(1);
218
+ });
219
+
220
+ test("exists() returns boolean", async () => {
221
+ await User.create({ name: "John" });
222
+ const exists = await User.query().where("name", "John").exists();
223
+ expect(exists).toBe(true);
224
+ });
225
+
226
+ test("exists() false when no match", async () => {
227
+ const exists = await User.query().where("name", "NonExistent").exists();
228
+ expect(exists).toBe(false);
229
+ });
230
+
231
+ test("pluck() returns array of values", async () => {
232
+ await User.create({ name: "John" });
233
+ await User.create({ name: "Jane" });
234
+ const names = await User.query().pluck("name");
235
+ expect(names).toEqual(["John", "Jane"]);
236
+ });
237
+
238
+ test("value() returns single value", async () => {
239
+ await User.create({ name: "John", email: "john@example.com" });
240
+ const email = await User.query().value("email");
241
+ expect(email).toBe("john@example.com");
242
+ });
243
+
244
+ test("value() returns null when empty", async () => {
245
+ const value = await User.query().where("name", "NonExistent").value("name");
246
+ expect(value).toBeNull();
247
+ });
248
+
249
+ test("paginate() returns paginated result", async () => {
250
+ for (let i = 1; i <= 25; i++) {
251
+ await User.create({ name: `User${i}` });
252
+ }
253
+ const result = await User.query().orderBy("id").paginate(2, 10);
254
+ expect(result.data.length).toBe(10);
255
+ expect(result.total).toBe(25);
256
+ expect(result.page).toBe(2);
257
+ expect(result.limit).toBe(10);
258
+ expect(result.totalPages).toBe(3);
259
+ });
260
+
261
+ test("update() modifies rows", async () => {
262
+ const user = await User.create({ name: "John", status: "active" });
263
+ await User.query().where("id", user.id).update({ status: "inactive" });
264
+ const updated = await User.find(user.id!);
265
+ expect(updated!.status).toBe("inactive");
266
+ });
267
+
268
+ test("delete() removes rows", async () => {
269
+ const user = await User.create({ name: "John" });
270
+ await User.query().where("id", user.id).delete();
271
+ const result = await User.find(user.id!);
272
+ expect(result).toBeNull();
273
+ });
274
+ });
275
+
276
+ describe("join operations", () => {
277
+ beforeEach(async () => {
278
+ await db.raw("CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, title TEXT)");
279
+ });
280
+
281
+ test("join() performs INNER JOIN", async () => {
282
+ const user = await User.create({ name: "John" });
283
+ await db.raw("INSERT INTO posts (user_id, title) VALUES (?, ?)", [user.id, "Post 1"]);
284
+
285
+ const results = await User.query()
286
+ .join("posts", "posts.user_id = users.id")
287
+ .select("posts.title", "users.name")
288
+ .get();
289
+ expect(results.length).toBe(1);
290
+ });
291
+
292
+ test("leftJoin() performs LEFT JOIN", async () => {
293
+ await User.create({ name: "John" });
294
+ const results = await User.query()
295
+ .leftJoin("posts", "posts.user_id = users.id")
296
+ .get();
297
+ expect(results.length).toBe(1);
298
+ });
299
+ });
300
+
301
+ describe("grouping operations", () => {
302
+ test("groupBy() groups results", async () => {
303
+ await User.create({ name: "John", status: "active" });
304
+ await User.create({ name: "Jane", status: "active" });
305
+ await User.create({ name: "Bob", status: "inactive" });
306
+ const results = await User.query().select("status").groupBy("status").get();
307
+ expect(results.length).toBe(2);
308
+ });
309
+
310
+ test("having() filters groups", async () => {
311
+ await User.create({ name: "John", status: "active" });
312
+ await User.create({ name: "Jane", status: "active" });
313
+ await User.create({ name: "Bob", status: "inactive" });
314
+ const results = await User.query()
315
+ .select("status")
316
+ .groupBy("status")
317
+ .having("COUNT(*) > 1")
318
+ .get();
319
+ expect(results.length).toBe(1);
320
+ expect(results[0].status).toBe("active");
321
+ });
322
+ });
323
+ });
@@ -0,0 +1,179 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { CastRegistry } from "../../../src/database/orm/casts";
3
+ import { Model } from "../../../src/database/orm/model";
4
+
5
+ describe("CastRegistry", () => {
6
+ describe("deserialize (read from DB)", () => {
7
+ test("json: string → object", () => {
8
+ const result = CastRegistry.deserialize("json", '{"key":"value"}');
9
+ expect(result).toEqual({ key: "value" });
10
+ });
11
+
12
+ test("json: already-object pass-through", () => {
13
+ const obj = { key: "value" };
14
+ const result = CastRegistry.deserialize("json", obj);
15
+ expect(result).toBe(obj);
16
+ });
17
+
18
+ test("boolean: 1 → true", () => {
19
+ expect(CastRegistry.deserialize("boolean", 1)).toBe(true);
20
+ });
21
+
22
+ test("boolean: 0 → false", () => {
23
+ expect(CastRegistry.deserialize("boolean", 0)).toBe(false);
24
+ });
25
+
26
+ test('boolean: "false" → false', () => {
27
+ expect(CastRegistry.deserialize("boolean", "false")).toBe(false);
28
+ });
29
+
30
+ test("boolean: true → true", () => {
31
+ expect(CastRegistry.deserialize("boolean", true)).toBe(true);
32
+ });
33
+
34
+ test("integer: '42' → 42", () => {
35
+ expect(CastRegistry.deserialize("integer", "42")).toBe(42);
36
+ });
37
+
38
+ test("integer: 42 → 42", () => {
39
+ expect(CastRegistry.deserialize("integer", 42)).toBe(42);
40
+ });
41
+
42
+ test("float: '3.14' → 3.14", () => {
43
+ expect(CastRegistry.deserialize("float", "3.14")).toBe(3.14);
44
+ });
45
+
46
+ test("float: 3.14 → 3.14", () => {
47
+ expect(CastRegistry.deserialize("float", 3.14)).toBe(3.14);
48
+ });
49
+
50
+ test("date: ISO string → Date", () => {
51
+ const date = CastRegistry.deserialize("date", "2024-02-27");
52
+ expect(date instanceof Date).toBe(true);
53
+ });
54
+
55
+ test("date: null → null", () => {
56
+ expect(CastRegistry.deserialize("date", null)).toBeNull();
57
+ });
58
+
59
+ test("datetime: ISO string → Date", () => {
60
+ const date = CastRegistry.deserialize("datetime", "2024-02-27T10:30:00Z");
61
+ expect(date instanceof Date).toBe(true);
62
+ });
63
+
64
+ test("timestamp: number → Date", () => {
65
+ const ms = 1709031000000;
66
+ const date = CastRegistry.deserialize("timestamp", ms);
67
+ expect(date instanceof Date).toBe(true);
68
+ expect((date as Date).getTime()).toBe(ms);
69
+ });
70
+
71
+ test("timestamp: string → Date", () => {
72
+ const ms = 1709031000000;
73
+ const date = CastRegistry.deserialize("timestamp", ms.toString());
74
+ expect(date instanceof Date).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe("serialize (write to DB)", () => {
79
+ test("json: object → JSON string", () => {
80
+ const result = CastRegistry.serialize("json", { key: "value" });
81
+ expect(result).toBe('{"key":"value"}');
82
+ });
83
+
84
+ test("boolean: true → 1", () => {
85
+ expect(CastRegistry.serialize("boolean", true)).toBe(1);
86
+ });
87
+
88
+ test("boolean: false → 0", () => {
89
+ expect(CastRegistry.serialize("boolean", false)).toBe(0);
90
+ });
91
+
92
+ test("date: Date → 'YYYY-MM-DD' string", () => {
93
+ const date = new Date("2024-02-27");
94
+ const result = CastRegistry.serialize("date", date);
95
+ expect(typeof result).toBe("string");
96
+ expect(result).toMatch(/\d{4}-\d{2}-\d{2}/);
97
+ });
98
+
99
+ test("datetime: Date → ISO string", () => {
100
+ const date = new Date("2024-02-27T10:30:00Z");
101
+ const result = CastRegistry.serialize("datetime", date);
102
+ expect(typeof result).toBe("string");
103
+ expect(result).toContain("T");
104
+ });
105
+
106
+ test("timestamp: Date → millisecond number", () => {
107
+ const date = new Date("2024-02-27T10:30:00Z");
108
+ const result = CastRegistry.serialize("timestamp", date);
109
+ expect(typeof result).toBe("number");
110
+ });
111
+
112
+ test("integer: number → number", () => {
113
+ expect(CastRegistry.serialize("integer", 42)).toBe(42);
114
+ });
115
+
116
+ test("float: number → number", () => {
117
+ expect(CastRegistry.serialize("float", 3.14)).toBe(3.14);
118
+ });
119
+ });
120
+
121
+ describe("custom cast object", () => {
122
+ test("custom get/set round-trips correctly", () => {
123
+ const customCast = {
124
+ get: (v: number) => v * 2,
125
+ set: (v: number) => v / 2,
126
+ };
127
+ const serialized = CastRegistry.serialize(customCast, 10);
128
+ expect(serialized).toBe(5);
129
+ const deserialized = CastRegistry.deserialize(customCast, serialized);
130
+ expect(deserialized).toBe(10);
131
+ });
132
+ });
133
+
134
+ describe("integration: cast applied during hydrate()", () => {
135
+ test("Model with casts applies during hydration", () => {
136
+ class User extends Model {
137
+ static table = "users";
138
+ static casts = {
139
+ is_admin: "boolean",
140
+ settings: "json",
141
+ age: "integer",
142
+ };
143
+ }
144
+
145
+ const users = User.hydrate([
146
+ {
147
+ id: 1,
148
+ name: "John",
149
+ is_admin: 1,
150
+ settings: '{"theme":"dark"}',
151
+ age: "30",
152
+ },
153
+ ]);
154
+
155
+ expect(users[0].is_admin).toBe(true);
156
+ expect(typeof users[0].settings).toBe("object");
157
+ expect(users[0].age).toBe(30);
158
+ });
159
+
160
+ test("Model with null values handles gracefully", () => {
161
+ class User extends Model {
162
+ static table = "users";
163
+ static casts = {
164
+ is_admin: "boolean",
165
+ };
166
+ }
167
+
168
+ const users = User.hydrate([
169
+ {
170
+ id: 1,
171
+ name: "John",
172
+ is_admin: null,
173
+ },
174
+ ]);
175
+
176
+ expect(users[0].is_admin).toBeNull();
177
+ });
178
+ });
179
+ });