@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.
- package/.turbo/turbo-build.log +15 -11
- package/CHANGELOG.md +35 -0
- package/dist/adapters.d.ts +3 -11
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +65 -50
- package/dist/adapters.js.map +1 -1
- package/dist/db-test.d.ts +129 -0
- package/dist/db-test.d.ts.map +1 -0
- package/dist/db-test.js +214 -0
- package/dist/db-test.js.map +1 -0
- package/dist/index.d.ts +23 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -91
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/adapters.ts +137 -118
- package/src/db-test.test.ts +352 -0
- package/src/db-test.ts +574 -0
- package/src/index.test.ts +287 -546
- package/src/index.ts +39 -241
|
@@ -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
|
+
});
|