@buenojs/bueno 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
beforeEach(async () => {
|
|
15
|
+
db = new Database({ url: ":memory:" });
|
|
16
|
+
await db.connect();
|
|
17
|
+
setDefaultDatabase(db);
|
|
18
|
+
clearModelDatabaseRegistry();
|
|
19
|
+
await db.raw("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, age INTEGER, status TEXT)");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
clearDefaultDatabase();
|
|
24
|
+
clearModelDatabaseRegistry();
|
|
25
|
+
await db.close();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("QueryCompiler (via OrmQueryBuilder)", () => {
|
|
29
|
+
describe("SELECT compilation", () => {
|
|
30
|
+
test("simple SELECT * FROM table", async () => {
|
|
31
|
+
await User.create({ name: "John" });
|
|
32
|
+
const results = await User.all();
|
|
33
|
+
expect(results.length).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("SELECT with columns", async () => {
|
|
37
|
+
await User.create({ name: "John", email: "john@example.com" });
|
|
38
|
+
const results = await User.query().select("name", "email").get();
|
|
39
|
+
expect(results[0]).toHaveProperty("name");
|
|
40
|
+
expect(results[0]).toHaveProperty("email");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("SELECT DISTINCT", async () => {
|
|
44
|
+
await User.create({ name: "John", email: "john@example.com" });
|
|
45
|
+
await User.create({ name: "John", email: "jane@example.com" });
|
|
46
|
+
const results = await User.query().select("name").distinct().get();
|
|
47
|
+
expect(results.length).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("WHERE with equality", async () => {
|
|
51
|
+
await User.create({ name: "John", status: "active" });
|
|
52
|
+
await User.create({ name: "Jane", status: "inactive" });
|
|
53
|
+
const results = await User.query().where("status", "active").get();
|
|
54
|
+
expect(results.length).toBe(1);
|
|
55
|
+
expect(results[0].name).toBe("John");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("WHERE with comparison operators", async () => {
|
|
59
|
+
await User.create({ name: "John", age: 25 });
|
|
60
|
+
await User.create({ name: "Jane", age: 30 });
|
|
61
|
+
const results = await User.query().where("age", ">", 28).get();
|
|
62
|
+
expect(results.length).toBe(1);
|
|
63
|
+
expect(results[0].age).toBe(30);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("WHERE IN with array", async () => {
|
|
67
|
+
await User.create({ name: "John", status: "active" });
|
|
68
|
+
await User.create({ name: "Jane", status: "pending" });
|
|
69
|
+
await User.create({ name: "Bob", status: "inactive" });
|
|
70
|
+
const results = await User.query().whereIn("status", ["active", "pending"]).get();
|
|
71
|
+
expect(results.length).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("WHERE IS NULL", async () => {
|
|
75
|
+
await User.create({ name: "John", email: null });
|
|
76
|
+
await User.create({ name: "Jane", email: "jane@example.com" });
|
|
77
|
+
const results = await User.query().whereNull("email").get();
|
|
78
|
+
expect(results.length).toBe(1);
|
|
79
|
+
expect(results[0].name).toBe("John");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("WHERE IS NOT NULL", async () => {
|
|
83
|
+
await User.create({ name: "John", email: null });
|
|
84
|
+
await User.create({ name: "Jane", email: "jane@example.com" });
|
|
85
|
+
const results = await User.query().whereNotNull("email").get();
|
|
86
|
+
expect(results.length).toBe(1);
|
|
87
|
+
expect(results[0].email).toBe("jane@example.com");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("WHERE BETWEEN", async () => {
|
|
91
|
+
await User.create({ name: "John", age: 25 });
|
|
92
|
+
await User.create({ name: "Jane", age: 30 });
|
|
93
|
+
await User.create({ name: "Bob", age: 35 });
|
|
94
|
+
const results = await User.query().whereBetween("age", [27, 32]).get();
|
|
95
|
+
expect(results.length).toBe(1);
|
|
96
|
+
expect(results[0].age).toBe(30);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("WHERE RAW", async () => {
|
|
100
|
+
await User.create({ name: "John", age: 25 });
|
|
101
|
+
await User.create({ name: "Jane", age: 30 });
|
|
102
|
+
const results = await User.query().whereRaw("age > 27").get();
|
|
103
|
+
expect(results.length).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("OR WHERE", async () => {
|
|
107
|
+
await User.create({ name: "John", status: "admin" });
|
|
108
|
+
await User.create({ name: "Jane", status: "moderator" });
|
|
109
|
+
await User.create({ name: "Bob", status: "user" });
|
|
110
|
+
const results = await User.query().where("status", "admin").orWhere("status", "moderator").get();
|
|
111
|
+
expect(results.length).toBe(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("ORDER BY ASC", async () => {
|
|
115
|
+
await User.create({ name: "Charlie" });
|
|
116
|
+
await User.create({ name: "Alice" });
|
|
117
|
+
await User.create({ name: "Bob" });
|
|
118
|
+
const results = await User.query().orderBy("name", "asc").get();
|
|
119
|
+
expect(results[0].name).toBe("Alice");
|
|
120
|
+
expect(results[2].name).toBe("Charlie");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("ORDER BY DESC", async () => {
|
|
124
|
+
await User.create({ name: "Charlie" });
|
|
125
|
+
await User.create({ name: "Alice" });
|
|
126
|
+
const results = await User.query().orderBy("name", "desc").get();
|
|
127
|
+
expect(results[0].name).toBe("Charlie");
|
|
128
|
+
expect(results[1].name).toBe("Alice");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("LIMIT", async () => {
|
|
132
|
+
await User.create({ name: "John" });
|
|
133
|
+
await User.create({ name: "Jane" });
|
|
134
|
+
await User.create({ name: "Bob" });
|
|
135
|
+
const results = await User.query().limit(2).get();
|
|
136
|
+
expect(results.length).toBe(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("LIMIT and OFFSET", async () => {
|
|
140
|
+
for (let i = 1; i <= 5; i++) {
|
|
141
|
+
await User.create({ name: `User${i}` });
|
|
142
|
+
}
|
|
143
|
+
const results = await User.query().orderBy("id").limit(2).offset(2).get();
|
|
144
|
+
expect(results.length).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("COUNT compilation", () => {
|
|
149
|
+
test("count all", async () => {
|
|
150
|
+
await User.create({ name: "John" });
|
|
151
|
+
await User.create({ name: "Jane" });
|
|
152
|
+
const count = await User.query().count();
|
|
153
|
+
expect(count).toBe(2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("count with WHERE clause", async () => {
|
|
157
|
+
await User.create({ name: "John", status: "active" });
|
|
158
|
+
await User.create({ name: "Jane", status: "inactive" });
|
|
159
|
+
const count = await User.query().where("status", "active").count();
|
|
160
|
+
expect(count).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("EXISTS compilation", () => {
|
|
165
|
+
test("exists returns true when record exists", async () => {
|
|
166
|
+
await User.create({ name: "John" });
|
|
167
|
+
const exists = await User.query().where("name", "John").exists();
|
|
168
|
+
expect(exists).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("exists returns false when no record matches", async () => {
|
|
172
|
+
const exists = await User.query().where("name", "NonExistent").exists();
|
|
173
|
+
expect(exists).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("INSERT compilation", () => {
|
|
178
|
+
test("single row insert", async () => {
|
|
179
|
+
const result = await User.create({ name: "John", email: "john@example.com" });
|
|
180
|
+
expect(result.id).toBeDefined();
|
|
181
|
+
expect(result.name).toBe("John");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("batch insert", async () => {
|
|
185
|
+
const user1 = await User.create({ name: "John", email: "john@example.com" });
|
|
186
|
+
const user2 = await User.create({ name: "Jane", email: "jane@example.com" });
|
|
187
|
+
expect(user1.id).toBeDefined();
|
|
188
|
+
expect(user2.id).toBeDefined();
|
|
189
|
+
expect(user1.name).toBe("John");
|
|
190
|
+
expect(user2.name).toBe("Jane");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("UPDATE compilation", () => {
|
|
195
|
+
test("update with WHERE clause", async () => {
|
|
196
|
+
const user = await User.create({ name: "John" });
|
|
197
|
+
user.name = "Jane";
|
|
198
|
+
await user.save();
|
|
199
|
+
const updated = await User.find(user.id!);
|
|
200
|
+
expect(updated!.name).toBe("Jane");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("DELETE compilation", () => {
|
|
205
|
+
test("delete with WHERE clause", async () => {
|
|
206
|
+
const user = await User.create({ name: "John" });
|
|
207
|
+
await user.delete();
|
|
208
|
+
const found = await User.find(user.id!);
|
|
209
|
+
expect(found).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("Placeholder dialect handling", () => {
|
|
214
|
+
test("SQLite uses ? placeholders", async () => {
|
|
215
|
+
await User.create({ name: "Test", status: "active" });
|
|
216
|
+
const results = await User.query().where("status", "active").get();
|
|
217
|
+
expect(results.length).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
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"];
|
|
12
|
+
|
|
13
|
+
override posts() {
|
|
14
|
+
return this.hasMany(Post, "user_id");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override profile() {
|
|
18
|
+
return this.hasOne(Profile, "user_id");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class Post extends Model {
|
|
23
|
+
static table = "posts";
|
|
24
|
+
static timestamps = false;
|
|
25
|
+
fillable = ["user_id", "title"];
|
|
26
|
+
|
|
27
|
+
override author() {
|
|
28
|
+
return this.belongsTo(User, "user_id");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override comments() {
|
|
32
|
+
return this.hasMany(Comment, "post_id");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class Comment extends Model {
|
|
37
|
+
static table = "comments";
|
|
38
|
+
static timestamps = false;
|
|
39
|
+
fillable = ["post_id", "body"];
|
|
40
|
+
|
|
41
|
+
override post() {
|
|
42
|
+
return this.belongsTo(Post, "post_id");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Profile extends Model {
|
|
47
|
+
static table = "profiles";
|
|
48
|
+
static timestamps = false;
|
|
49
|
+
fillable = ["user_id", "bio"];
|
|
50
|
+
|
|
51
|
+
override user() {
|
|
52
|
+
return this.belongsTo(User, "user_id");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
db = new Database({ url: ":memory:" });
|
|
58
|
+
await db.connect();
|
|
59
|
+
setDefaultDatabase(db);
|
|
60
|
+
clearModelDatabaseRegistry();
|
|
61
|
+
|
|
62
|
+
await db.raw(`
|
|
63
|
+
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)
|
|
64
|
+
`);
|
|
65
|
+
await db.raw(`
|
|
66
|
+
CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, title TEXT)
|
|
67
|
+
`);
|
|
68
|
+
await db.raw(`
|
|
69
|
+
CREATE TABLE comments (id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER, body TEXT)
|
|
70
|
+
`);
|
|
71
|
+
await db.raw(`
|
|
72
|
+
CREATE TABLE profiles (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, bio TEXT)
|
|
73
|
+
`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
clearDefaultDatabase();
|
|
78
|
+
clearModelDatabaseRegistry();
|
|
79
|
+
await db.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("Eager Loading", () => {
|
|
83
|
+
describe("simple eager load", () => {
|
|
84
|
+
test("with('posts').get() loads posts into users", async () => {
|
|
85
|
+
const user = await User.create({ name: "John" });
|
|
86
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
87
|
+
await Post.create({ user_id: user.id, title: "Post 2" });
|
|
88
|
+
|
|
89
|
+
const users = await User.query().with("posts").get();
|
|
90
|
+
expect(users.length).toBe(1);
|
|
91
|
+
expect((users[0] as any)._relations.has("posts")).toBe(true);
|
|
92
|
+
expect(users[0].posts).toEqual(expect.any(Array));
|
|
93
|
+
expect(users[0].posts.length).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("with('posts').first() loads posts into user", async () => {
|
|
97
|
+
const user = await User.create({ name: "John" });
|
|
98
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
99
|
+
|
|
100
|
+
const foundUser = await User.query().with("posts").first();
|
|
101
|
+
expect(foundUser).not.toBeNull();
|
|
102
|
+
expect(foundUser!.posts).toEqual(expect.any(Array));
|
|
103
|
+
expect(foundUser!.posts.length).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("user with no relations returns empty array", async () => {
|
|
107
|
+
const user = await User.create({ name: "John" });
|
|
108
|
+
const users = await User.query().with("posts").get();
|
|
109
|
+
expect(users[0].posts).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("BelongsTo eager load", () => {
|
|
114
|
+
test("with('author').get() loads author into posts", async () => {
|
|
115
|
+
const user = await User.create({ name: "John" });
|
|
116
|
+
const post = await Post.create({ user_id: user.id, title: "Post 1" });
|
|
117
|
+
|
|
118
|
+
const posts = await Post.query().with("author").get();
|
|
119
|
+
expect(posts.length).toBe(1);
|
|
120
|
+
expect(posts[0].author).not.toBeNull();
|
|
121
|
+
expect(posts[0].author!.name).toBe("John");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("post with null foreign key returns null", async () => {
|
|
125
|
+
await Post.create({ user_id: null, title: "Post 1" });
|
|
126
|
+
|
|
127
|
+
const posts = await Post.query().with("author").get();
|
|
128
|
+
expect(posts[0].author).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("nested eager load", () => {
|
|
133
|
+
test("with('posts.comments').get() loads nested relations", async () => {
|
|
134
|
+
const user = await User.create({ name: "John" });
|
|
135
|
+
const post = await Post.create({ user_id: user.id, title: "Post 1" });
|
|
136
|
+
await Comment.create({ post_id: post.id, body: "Comment 1" });
|
|
137
|
+
await Comment.create({ post_id: post.id, body: "Comment 2" });
|
|
138
|
+
|
|
139
|
+
const users = await User.query().with("posts.comments").get();
|
|
140
|
+
expect(users.length).toBe(1);
|
|
141
|
+
expect(users[0].posts).toEqual(expect.any(Array));
|
|
142
|
+
expect(users[0].posts.length).toBe(1);
|
|
143
|
+
expect(users[0].posts[0].comments).toEqual(expect.any(Array));
|
|
144
|
+
expect(users[0].posts[0].comments.length).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("nested eager load with multiple levels", async () => {
|
|
148
|
+
const user = await User.create({ name: "John" });
|
|
149
|
+
const post = await Post.create({ user_id: user.id, title: "Post 1" });
|
|
150
|
+
await Comment.create({ post_id: post.id, body: "Comment 1" });
|
|
151
|
+
|
|
152
|
+
const users = await User.query().with("posts.comments").get();
|
|
153
|
+
expect(users[0].posts[0].comments[0].body).toBe("Comment 1");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("nested eager load with empty relations", async () => {
|
|
157
|
+
const user = await User.create({ name: "John" });
|
|
158
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
159
|
+
|
|
160
|
+
const users = await User.query().with("posts.comments").get();
|
|
161
|
+
expect(users[0].posts[0].comments).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("constrained eager load", () => {
|
|
166
|
+
test("with('posts', callback) applies constraint", async () => {
|
|
167
|
+
const user = await User.create({ name: "John" });
|
|
168
|
+
await Post.create({ user_id: user.id, title: "Published" });
|
|
169
|
+
await Post.create({ user_id: user.id, title: "Draft" });
|
|
170
|
+
|
|
171
|
+
const users = await User.query()
|
|
172
|
+
.with("posts", (q) => q.where("title", "Published"))
|
|
173
|
+
.get();
|
|
174
|
+
expect(users[0].posts.length).toBe(1);
|
|
175
|
+
expect(users[0].posts[0].title).toBe("Published");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("constraint on nested relation", async () => {
|
|
179
|
+
const user = await User.create({ name: "John" });
|
|
180
|
+
const post = await Post.create({ user_id: user.id, title: "Post 1" });
|
|
181
|
+
await Comment.create({ post_id: post.id, body: "Keep this" });
|
|
182
|
+
await Comment.create({ post_id: post.id, body: "Filter this" });
|
|
183
|
+
|
|
184
|
+
const users = await User.query()
|
|
185
|
+
.with("posts.comments", (q) => q.where("body", "Keep this"))
|
|
186
|
+
.get();
|
|
187
|
+
expect(users[0].posts[0].comments.length).toBe(1);
|
|
188
|
+
expect(users[0].posts[0].comments[0].body).toBe("Keep this");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("multiple eager loads", () => {
|
|
193
|
+
test("with() called multiple times loads all relations", async () => {
|
|
194
|
+
const user = await User.create({ name: "John" });
|
|
195
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
196
|
+
await Profile.create({ user_id: user.id, bio: "Developer" });
|
|
197
|
+
|
|
198
|
+
const users = await User.query().with("posts").with("profile").get();
|
|
199
|
+
expect(users[0].posts).toEqual(expect.any(Array));
|
|
200
|
+
expect(users[0].profile).not.toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("multiple relations on same parent", async () => {
|
|
204
|
+
const user = await User.create({ name: "John" });
|
|
205
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
206
|
+
await Profile.create({ user_id: user.id, bio: "Developer" });
|
|
207
|
+
|
|
208
|
+
const users = await User.query().with("posts").with("profile").get();
|
|
209
|
+
expect((users[0] as any)._relations.has("posts")).toBe(true);
|
|
210
|
+
expect((users[0] as any)._relations.has("profile")).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("eager load with filtering", () => {
|
|
215
|
+
test("where() before with() filters parent", async () => {
|
|
216
|
+
await User.create({ name: "John" });
|
|
217
|
+
const user2 = await User.create({ name: "Jane" });
|
|
218
|
+
await Post.create({ user_id: user2.id, title: "Post 1" });
|
|
219
|
+
|
|
220
|
+
const users = await User.query().where("name", "Jane").with("posts").get();
|
|
221
|
+
expect(users.length).toBe(1);
|
|
222
|
+
expect(users[0].name).toBe("Jane");
|
|
223
|
+
expect(users[0].posts.length).toBe(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("constraint narrows child relations without filtering parent", async () => {
|
|
227
|
+
const user1 = await User.create({ name: "John" });
|
|
228
|
+
const user2 = await User.create({ name: "Jane" });
|
|
229
|
+
await Post.create({ user_id: user1.id, title: "Published" });
|
|
230
|
+
await Post.create({ user_id: user1.id, title: "Draft" });
|
|
231
|
+
await Post.create({ user_id: user2.id, title: "Published" });
|
|
232
|
+
|
|
233
|
+
const users = await User.query()
|
|
234
|
+
.with("posts", (q) => q.where("title", "Published"))
|
|
235
|
+
.get();
|
|
236
|
+
expect(users.length).toBe(2);
|
|
237
|
+
expect(users[0].posts.length).toBe(1);
|
|
238
|
+
expect(users[1].posts.length).toBe(1);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("eager load return value", () => {
|
|
243
|
+
test("get() with eager loading returns Model instances", async () => {
|
|
244
|
+
const user = await User.create({ name: "John" });
|
|
245
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
246
|
+
|
|
247
|
+
const users = await User.query().with("posts").get();
|
|
248
|
+
expect(users[0] instanceof User).toBe(true);
|
|
249
|
+
expect(users[0].posts[0] instanceof Post).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("first() with eager loading returns Model instance", async () => {
|
|
253
|
+
const user = await User.create({ name: "John" });
|
|
254
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
255
|
+
|
|
256
|
+
const foundUser = await User.query().with("posts").first();
|
|
257
|
+
expect(foundUser instanceof User).toBe(true);
|
|
258
|
+
expect(foundUser!.posts[0] instanceof Post).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("edge cases", () => {
|
|
263
|
+
test("eager load with no results", async () => {
|
|
264
|
+
const users = await User.query().with("posts").get();
|
|
265
|
+
expect(users.length).toBe(0);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("eager load same relation twice", async () => {
|
|
269
|
+
const user = await User.create({ name: "John" });
|
|
270
|
+
await Post.create({ user_id: user.id, title: "Post 1" });
|
|
271
|
+
|
|
272
|
+
const users = await User.query().with("posts").with("posts").get();
|
|
273
|
+
expect(users[0].posts.length).toBe(1);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("complex nested eager load chain", async () => {
|
|
277
|
+
const user = await User.create({ name: "John" });
|
|
278
|
+
const post = await Post.create({ user_id: user.id, title: "Post 1" });
|
|
279
|
+
const comment = await Comment.create({ post_id: post.id, body: "Comment 1" });
|
|
280
|
+
|
|
281
|
+
const users = await User.query().with("posts.comments").get();
|
|
282
|
+
expect(users[0].posts[0].comments[0].id).toBe(comment.id);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { HookRunner } from "../../../src/database/orm/hooks";
|
|
3
|
+
import { Model } from "../../../src/database/orm/model";
|
|
4
|
+
|
|
5
|
+
describe("HookRunner", () => {
|
|
6
|
+
describe("on() + run()", () => {
|
|
7
|
+
test("callback called with correct model argument", async () => {
|
|
8
|
+
let called = false;
|
|
9
|
+
let capturedModel: Model | null = null;
|
|
10
|
+
|
|
11
|
+
const runner = new HookRunner();
|
|
12
|
+
runner.on("saving", (model) => {
|
|
13
|
+
called = true;
|
|
14
|
+
capturedModel = model;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const model = new Model();
|
|
18
|
+
await runner.run("saving", model);
|
|
19
|
+
|
|
20
|
+
expect(called).toBe(true);
|
|
21
|
+
expect(capturedModel).toBe(model);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("multiple callbacks run in order", async () => {
|
|
25
|
+
const order: number[] = [];
|
|
26
|
+
|
|
27
|
+
const runner = new HookRunner();
|
|
28
|
+
runner.on("saving", () => order.push(1));
|
|
29
|
+
runner.on("saving", () => order.push(2));
|
|
30
|
+
runner.on("saving", () => order.push(3));
|
|
31
|
+
|
|
32
|
+
const model = new Model();
|
|
33
|
+
await runner.run("saving", model);
|
|
34
|
+
|
|
35
|
+
expect(order).toEqual([1, 2, 3]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("returning false aborts remaining callbacks", async () => {
|
|
39
|
+
const order: number[] = [];
|
|
40
|
+
|
|
41
|
+
const runner = new HookRunner();
|
|
42
|
+
runner.on("saving", () => {
|
|
43
|
+
order.push(1);
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
runner.on("saving", () => order.push(2));
|
|
47
|
+
runner.on("saving", () => order.push(3));
|
|
48
|
+
|
|
49
|
+
const model = new Model();
|
|
50
|
+
const result = await runner.run("saving", model);
|
|
51
|
+
|
|
52
|
+
expect(result).toBe(false);
|
|
53
|
+
expect(order).toEqual([1]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returning void/undefined continues, run returns true", async () => {
|
|
57
|
+
const runner = new HookRunner();
|
|
58
|
+
let callback1Called = false;
|
|
59
|
+
let callback2Called = false;
|
|
60
|
+
|
|
61
|
+
runner.on("saving", () => {
|
|
62
|
+
callback1Called = true;
|
|
63
|
+
});
|
|
64
|
+
runner.on("saving", () => {
|
|
65
|
+
callback2Called = true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const model = new Model();
|
|
69
|
+
const result = await runner.run("saving", model);
|
|
70
|
+
|
|
71
|
+
expect(result).toBe(true);
|
|
72
|
+
expect(callback1Called).toBe(true);
|
|
73
|
+
expect(callback2Called).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("async callbacks awaited correctly", async () => {
|
|
77
|
+
let order: number[] = [];
|
|
78
|
+
|
|
79
|
+
const runner = new HookRunner();
|
|
80
|
+
runner.on("saving", async () => {
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
82
|
+
order.push(1);
|
|
83
|
+
});
|
|
84
|
+
runner.on("saving", () => order.push(2));
|
|
85
|
+
|
|
86
|
+
const model = new Model();
|
|
87
|
+
await runner.run("saving", model);
|
|
88
|
+
|
|
89
|
+
expect(order).toEqual([1, 2]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("async callback returning false aborts", async () => {
|
|
93
|
+
const order: number[] = [];
|
|
94
|
+
|
|
95
|
+
const runner = new HookRunner();
|
|
96
|
+
runner.on("saving", async () => {
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
98
|
+
order.push(1);
|
|
99
|
+
return false;
|
|
100
|
+
});
|
|
101
|
+
runner.on("saving", () => order.push(2));
|
|
102
|
+
|
|
103
|
+
const model = new Model();
|
|
104
|
+
const result = await runner.run("saving", model);
|
|
105
|
+
|
|
106
|
+
expect(result).toBe(false);
|
|
107
|
+
expect(order).toEqual([1]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("hook name isolation - different hooks don't affect each other", async () => {
|
|
111
|
+
const savingCalled = { count: 0 };
|
|
112
|
+
const updatingCalled = { count: 0 };
|
|
113
|
+
|
|
114
|
+
const runner = new HookRunner();
|
|
115
|
+
runner.on("saving", () => savingCalled.count++);
|
|
116
|
+
runner.on("updating", () => updatingCalled.count++);
|
|
117
|
+
|
|
118
|
+
const model = new Model();
|
|
119
|
+
await runner.run("saving", model);
|
|
120
|
+
|
|
121
|
+
expect(savingCalled.count).toBe(1);
|
|
122
|
+
expect(updatingCalled.count).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("multiple hook types", async () => {
|
|
126
|
+
const calls: string[] = [];
|
|
127
|
+
|
|
128
|
+
const runner = new HookRunner();
|
|
129
|
+
runner.on("creating", () => calls.push("creating"));
|
|
130
|
+
runner.on("created", () => calls.push("created"));
|
|
131
|
+
runner.on("updating", () => calls.push("updating"));
|
|
132
|
+
runner.on("updated", () => calls.push("updated"));
|
|
133
|
+
|
|
134
|
+
const model = new Model();
|
|
135
|
+
await runner.run("creating", model);
|
|
136
|
+
await runner.run("created", model);
|
|
137
|
+
await runner.run("updating", model);
|
|
138
|
+
await runner.run("updated", model);
|
|
139
|
+
|
|
140
|
+
expect(calls).toEqual(["creating", "created", "updating", "updated"]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("Model.on() (static registration)", () => {
|
|
145
|
+
test("Model.on() registers callback", async () => {
|
|
146
|
+
class TestModel extends Model {
|
|
147
|
+
table = "test";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let called = false;
|
|
151
|
+
TestModel.on("saving", () => {
|
|
152
|
+
called = true;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const callbacks = TestModel.getHookCallbacks("saving");
|
|
156
|
+
expect(callbacks.length > 0).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("Model.getHookCallbacks() returns array", async () => {
|
|
160
|
+
class TestModel2 extends Model {
|
|
161
|
+
static table = "test";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
TestModel2.on("saving", () => {});
|
|
165
|
+
TestModel2.on("saving", () => {});
|
|
166
|
+
|
|
167
|
+
const callbacks = TestModel2.getHookCallbacks("saving");
|
|
168
|
+
expect(Array.isArray(callbacks)).toBe(true);
|
|
169
|
+
expect(callbacks.length).toBe(2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("static hooks isolated per model", async () => {
|
|
173
|
+
class ModelA extends Model {
|
|
174
|
+
table = "a";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class ModelB extends Model {
|
|
178
|
+
table = "b";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
ModelA.on("saving", () => {});
|
|
182
|
+
ModelB.on("saving", () => {});
|
|
183
|
+
|
|
184
|
+
const aCallbacks = ModelA.getHookCallbacks("saving");
|
|
185
|
+
const bCallbacks = ModelB.getHookCallbacks("saving");
|
|
186
|
+
|
|
187
|
+
expect(aCallbacks.length).toBe(1);
|
|
188
|
+
expect(bCallbacks.length).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|