@fragno-dev/test 0.1.11 → 0.1.13

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.
@@ -0,0 +1,352 @@
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import { column, idColumn, schema } from "@fragno-dev/db/schema";
3
+ import { withDatabase } from "@fragno-dev/db";
4
+ import { defineFragment, instantiate } from "@fragno-dev/core";
5
+ import { defineRoute } from "@fragno-dev/core/route";
6
+ import { z } from "zod";
7
+ import { buildDatabaseFragmentsTest } from "./db-test";
8
+
9
+ // Test schema with users table
10
+ const userSchema = schema((s) => {
11
+ return s.addTable("users", (t) => {
12
+ return t
13
+ .addColumn("id", idColumn())
14
+ .addColumn("name", column("string"))
15
+ .addColumn("email", column("string"))
16
+ .createIndex("idx_users_all", ["id"]);
17
+ });
18
+ });
19
+
20
+ // Test schema with posts table
21
+ const postSchema = schema((s) => {
22
+ return s.addTable("posts", (t) => {
23
+ return t
24
+ .addColumn("id", idColumn())
25
+ .addColumn("title", column("string"))
26
+ .addColumn("userId", column("string"))
27
+ .createIndex("idx_posts_all", ["id"]);
28
+ });
29
+ });
30
+
31
+ describe("buildDatabaseFragmentsTest", () => {
32
+ it("should create multiple fragments with shared adapter", async () => {
33
+ // Define fragments using new API
34
+ const userFragmentDef = defineFragment<{}>("user-fragment")
35
+ .extend(withDatabase(userSchema))
36
+ .providesBaseService(({ deps }) => ({
37
+ createUser: async (data: { name: string; email: string }) => {
38
+ const id = await deps.db.create("users", data);
39
+ return { ...data, id: id.valueOf() };
40
+ },
41
+ getUsers: async () => {
42
+ const users = await deps.db.find("users", (b) =>
43
+ b.whereIndex("idx_users_all", (eb) => eb("id", "!=", "")),
44
+ );
45
+ return users.map((u) => ({ ...u, id: u.id.valueOf() }));
46
+ },
47
+ }))
48
+ .build();
49
+
50
+ const postFragmentDef = defineFragment<{}>("post-fragment")
51
+ .extend(withDatabase(postSchema))
52
+ .providesBaseService(({ deps }) => ({
53
+ createPost: async (data: { title: string; userId: string }) => {
54
+ const id = await deps.db.create("posts", data);
55
+ return { ...data, id: id.valueOf() };
56
+ },
57
+ getPosts: async () => {
58
+ const posts = await deps.db.find("posts", (b) =>
59
+ b.whereIndex("idx_posts_all", (eb) => eb("id", "!=", "")),
60
+ );
61
+ return posts.map((p) => ({ ...p, id: p.id.valueOf() }));
62
+ },
63
+ }))
64
+ .build();
65
+
66
+ // Build test setup with new builder API
67
+ const { fragments, test } = await buildDatabaseFragmentsTest()
68
+ .withTestAdapter({ type: "kysely-sqlite" })
69
+ .withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
70
+ .withFragment("post", instantiate(postFragmentDef).withConfig({}).withRoutes([]))
71
+ .build();
72
+
73
+ // Test user fragment
74
+ const user = await fragments.user.services.createUser({
75
+ name: "Test User",
76
+ email: "test@example.com",
77
+ });
78
+
79
+ expect(user).toMatchObject({
80
+ id: expect.any(String),
81
+ name: "Test User",
82
+ email: "test@example.com",
83
+ });
84
+
85
+ // Test post fragment
86
+ const post = await fragments.post.services.createPost({
87
+ title: "Test Post",
88
+ userId: user.id,
89
+ });
90
+
91
+ expect(post).toMatchObject({
92
+ id: expect.any(String),
93
+ title: "Test Post",
94
+ userId: user.id,
95
+ });
96
+
97
+ // Verify data exists
98
+ const users = await fragments.user.services.getUsers();
99
+ expect(users).toHaveLength(1);
100
+
101
+ const posts = await fragments.post.services.getPosts();
102
+ expect(posts).toHaveLength(1);
103
+ expect(posts[0]!.userId).toBe(user.id);
104
+
105
+ // Cleanup
106
+ await test.cleanup();
107
+ });
108
+
109
+ it("should reset database and recreate fragments", async () => {
110
+ const userFragmentDef = defineFragment<{}>("user-fragment")
111
+ .extend(withDatabase(userSchema))
112
+ .providesBaseService(({ deps }) => ({
113
+ createUser: async (data: { name: string; email: string }) => {
114
+ const id = await deps.db.create("users", data);
115
+ return { ...data, id: id.valueOf() };
116
+ },
117
+ getUsers: async () => {
118
+ const users = await deps.db.find("users", (b) =>
119
+ b.whereIndex("idx_users_all", (eb) => eb("id", "!=", "")),
120
+ );
121
+ return users.map((u) => ({ ...u, id: u.id.valueOf() }));
122
+ },
123
+ }))
124
+ .build();
125
+
126
+ const { fragments, test } = await buildDatabaseFragmentsTest()
127
+ .withTestAdapter({ type: "kysely-sqlite" })
128
+ .withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
129
+ .build();
130
+
131
+ // Create a user
132
+ await fragments.user.services.createUser({
133
+ name: "User 1",
134
+ email: "user1@example.com",
135
+ });
136
+
137
+ // Verify user exists
138
+ let users = await fragments.user.services.getUsers();
139
+ expect(users).toHaveLength(1);
140
+
141
+ // Reset database
142
+ await test.resetDatabase();
143
+
144
+ // Verify database is empty
145
+ users = await fragments.user.services.getUsers();
146
+ expect(users).toHaveLength(0);
147
+
148
+ // Cleanup
149
+ await test.cleanup();
150
+ });
151
+
152
+ it("should expose db for direct queries", async () => {
153
+ const userFragmentDef = defineFragment<{}>("user-fragment")
154
+ .extend(withDatabase(userSchema))
155
+ .providesBaseService(() => ({}))
156
+ .build();
157
+
158
+ const { fragments, test } = await buildDatabaseFragmentsTest()
159
+ .withTestAdapter({ type: "kysely-sqlite" })
160
+ .withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
161
+ .build();
162
+
163
+ // Use db directly
164
+ const userId = await fragments.user.db.create("users", {
165
+ name: "Direct DB User",
166
+ email: "direct@example.com",
167
+ });
168
+
169
+ expect(userId).toBeDefined();
170
+ expect(typeof userId.valueOf()).toBe("string");
171
+
172
+ // Find using db
173
+ const users = await fragments.user.db.find("users", (b) =>
174
+ b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
175
+ );
176
+
177
+ expect(users).toHaveLength(1);
178
+ expect(users[0]).toMatchObject({
179
+ id: userId,
180
+ name: "Direct DB User",
181
+ email: "direct@example.com",
182
+ });
183
+
184
+ await test.cleanup();
185
+ });
186
+
187
+ it("should expose deps and adapter", async () => {
188
+ const userFragmentDef = defineFragment<{}>("user-fragment")
189
+ .extend(withDatabase(userSchema))
190
+ .withDependencies(() => ({
191
+ testValue: "test-dependency",
192
+ }))
193
+ .providesBaseService(({ deps }) => ({
194
+ getTestValue: () => deps.testValue,
195
+ }))
196
+ .build();
197
+
198
+ const { fragments, test } = await buildDatabaseFragmentsTest()
199
+ .withTestAdapter({ type: "kysely-sqlite" })
200
+ .withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
201
+ .build();
202
+
203
+ // Test that deps are accessible
204
+ expect(fragments.user.deps).toBeDefined();
205
+ expect(fragments.user.deps.testValue).toBe("test-dependency");
206
+ expect(fragments.user.deps.db).toBeDefined();
207
+ expect(fragments.user.deps.schema).toBeDefined();
208
+
209
+ // Test that adapter is accessible
210
+ expect(test.adapter).toBeDefined();
211
+ expect(test.adapter.createQueryEngine).toBeDefined();
212
+
213
+ await test.cleanup();
214
+ });
215
+
216
+ it("should support callRoute with database operations", async () => {
217
+ // This is a simpler test that verifies callRoute exists and can be called.
218
+ // For now, we just verify that the method exists and is callable.
219
+ const userFragmentDef = defineFragment<{}>("user-fragment")
220
+ .extend(withDatabase(userSchema))
221
+ .providesBaseService(() => ({}))
222
+ .build();
223
+
224
+ const createUserRoute = defineRoute({
225
+ method: "POST",
226
+ path: "/users",
227
+ inputSchema: z.object({
228
+ name: z.string(),
229
+ email: z.string(),
230
+ }),
231
+ outputSchema: z.object({
232
+ id: z.string(),
233
+ name: z.string(),
234
+ email: z.string(),
235
+ }),
236
+ handler: async ({ input }, { json }) => {
237
+ const body = await input.valid();
238
+ return json({ ...body, id: "123" });
239
+ },
240
+ });
241
+
242
+ const { fragments, test } = await buildDatabaseFragmentsTest()
243
+ .withTestAdapter({ type: "kysely-sqlite" })
244
+ .withFragment(
245
+ "user",
246
+ instantiate(userFragmentDef).withConfig({}).withRoutes([createUserRoute]),
247
+ )
248
+ .build();
249
+
250
+ const response = await fragments.user.callRoute("POST", "/users", {
251
+ body: { name: "Test User", email: "test@example.com" },
252
+ });
253
+
254
+ assert(response.type === "json");
255
+ expect(response.data).toMatchObject({
256
+ id: "123",
257
+ name: "Test User",
258
+ email: "test@example.com",
259
+ });
260
+
261
+ await test.cleanup();
262
+ });
263
+
264
+ it("should use actual config during schema extraction", async () => {
265
+ // Test that the builder uses the actual config provided via .withConfig()
266
+ // This is important for fragments like Stripe that need API keys to initialize dependencies
267
+ interface RequiredConfigFragmentConfig {
268
+ apiKey: string;
269
+ apiSecret: string;
270
+ }
271
+
272
+ const requiredConfigFragmentDef = defineFragment<RequiredConfigFragmentConfig>(
273
+ "required-config-fragment",
274
+ )
275
+ .extend(withDatabase(userSchema))
276
+ .withDependencies(({ config }) => {
277
+ // This should receive the actual config, not an empty mock
278
+ return {
279
+ client: { key: config.apiKey, secret: config.apiSecret },
280
+ apiKey: config.apiKey,
281
+ };
282
+ })
283
+ .providesBaseService(({ deps }) => ({
284
+ getApiKey: () => deps.apiKey,
285
+ createUser: async (data: { name: string; email: string }) => {
286
+ const id = await deps.db.create("users", data);
287
+ return { ...data, id: id.valueOf() };
288
+ },
289
+ }))
290
+ .build();
291
+
292
+ const { fragments, test } = await buildDatabaseFragmentsTest()
293
+ .withTestAdapter({ type: "kysely-sqlite" })
294
+ .withFragment(
295
+ "requiredConfig",
296
+ instantiate(requiredConfigFragmentDef)
297
+ .withConfig({
298
+ apiKey: "test-key",
299
+ apiSecret: "test-secret",
300
+ })
301
+ .withRoutes([]),
302
+ )
303
+ .build();
304
+
305
+ // Verify the fragment was created with actual config
306
+ expect(fragments.requiredConfig.deps.apiKey).toBe("test-key");
307
+ expect(fragments.requiredConfig.deps.client).toEqual({
308
+ key: "test-key",
309
+ secret: "test-secret",
310
+ });
311
+
312
+ // Verify database operations work
313
+ const user = await fragments.requiredConfig.services.createUser({
314
+ name: "Config Test User",
315
+ email: "config@example.com",
316
+ });
317
+
318
+ expect(user).toMatchObject({
319
+ id: expect.any(String),
320
+ name: "Config Test User",
321
+ email: "config@example.com",
322
+ });
323
+
324
+ await test.cleanup();
325
+ });
326
+
327
+ it("should provide helpful error when config is missing", async () => {
328
+ // Test that we get a helpful error when required config is not provided
329
+ const badFragmentDef = defineFragment<{ apiKey: string }>("bad-fragment")
330
+ .extend(withDatabase(userSchema))
331
+ .withDependencies(({ config }) => {
332
+ // This will throw if apiKey is undefined
333
+ if (!config.apiKey) {
334
+ throw new Error("API key is required");
335
+ }
336
+ return {
337
+ apiKey: config.apiKey,
338
+ };
339
+ })
340
+ .providesBaseService(() => ({}))
341
+ .build();
342
+
343
+ // Intentionally omit the required config to test error handling
344
+ const buildPromise = buildDatabaseFragmentsTest()
345
+ .withTestAdapter({ type: "kysely-sqlite" })
346
+ .withFragment("bad", instantiate(badFragmentDef).withRoutes([]))
347
+ .build();
348
+
349
+ await expect(buildPromise).rejects.toThrow(/Failed to extract schema from fragment/);
350
+ await expect(buildPromise).rejects.toThrow(/API key is required/);
351
+ });
352
+ });