@fragno-dev/core 0.1.5 → 0.1.7
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 +49 -45
- package/CHANGELOG.md +52 -0
- package/dist/api/api.d.ts +2 -2
- package/dist/api/fragment-builder.d.ts +3 -2
- package/dist/api/fragment-instantiation.d.ts +4 -3
- package/dist/api/fragment-instantiation.js +3 -3
- package/dist/api/route.d.ts +3 -0
- package/dist/api/route.js +3 -0
- package/dist/{api-B1-h7jPC.d.ts → api-BWN97TOr.d.ts} +17 -3
- package/dist/api-BWN97TOr.d.ts.map +1 -0
- package/dist/api-DngJDcmO.js.map +1 -1
- package/dist/client/client.d.ts +4 -3
- package/dist/client/client.js +3 -3
- package/dist/client/client.svelte.d.ts +3 -3
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +3 -3
- package/dist/client/react.d.ts +3 -3
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +3 -3
- package/dist/client/solid.d.ts +3 -3
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +3 -3
- package/dist/client/vanilla.d.ts +3 -3
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +3 -3
- package/dist/client/vue.d.ts +3 -3
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +7 -7
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-YUZaNg5U.js → client-C5LsYHEI.js} +92 -11
- package/dist/client-C5LsYHEI.js.map +1 -0
- package/dist/{fragment-builder-DsqUOfJ5.d.ts → fragment-builder-MGr68GNb.d.ts} +80 -44
- package/dist/fragment-builder-MGr68GNb.d.ts.map +1 -0
- package/dist/{fragment-instantiation-Cp0K8zdS.js → fragment-instantiation-C4wvwl6V.js} +108 -3
- package/dist/fragment-instantiation-C4wvwl6V.js.map +1 -0
- package/dist/mod.d.ts +3 -2
- package/dist/mod.js +3 -3
- package/dist/{route-Dk1GyqHs.js → request-output-context-CdIjwmEN.js} +13 -24
- package/dist/request-output-context-CdIjwmEN.js.map +1 -0
- package/dist/route-Bl9Zr1Yv.d.ts +26 -0
- package/dist/route-Bl9Zr1Yv.d.ts.map +1 -0
- package/dist/route-C5Uryylh.js +21 -0
- package/dist/route-C5Uryylh.js.map +1 -0
- package/dist/test/test.d.ts +24 -70
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js +27 -115
- package/dist/test/test.js.map +1 -1
- package/package.json +6 -1
- package/src/api/api.ts +1 -0
- package/src/api/fragment-instantiation.test.ts +460 -0
- package/src/api/fragment-instantiation.ts +121 -0
- package/src/api/fragno-response.ts +132 -0
- package/src/api/internal/path-type.test.ts +7 -7
- package/src/api/internal/path.ts +1 -1
- package/src/api/request-output-context.test.ts +10 -10
- package/src/api/request-output-context.ts +3 -3
- package/src/api/route-handler-input-options.ts +15 -0
- package/src/client/client-types.test.ts +4 -4
- package/src/client/client.test.ts +341 -0
- package/src/client/client.ts +96 -15
- package/src/client/internal/fetcher-merge.ts +59 -0
- package/src/test/test.test.ts +110 -165
- package/src/test/test.ts +56 -266
- package/tsdown.config.ts +1 -0
- package/dist/api-B1-h7jPC.d.ts.map +0 -1
- package/dist/client-YUZaNg5U.js.map +0 -1
- package/dist/fragment-builder-DsqUOfJ5.d.ts.map +0 -1
- package/dist/fragment-instantiation-Cp0K8zdS.js.map +0 -1
- package/dist/route-CTxjMtGZ.js +0 -10
- package/dist/route-CTxjMtGZ.js.map +0 -1
- package/dist/route-Dk1GyqHs.js.map +0 -1
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { defineFragment } from "./fragment-builder";
|
|
3
|
+
import { createFragment } from "./fragment-instantiation";
|
|
4
|
+
import { defineRoute, defineRoutes } from "./route";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
describe("callRoute", () => {
|
|
8
|
+
test("calls route handler with body", async () => {
|
|
9
|
+
const config = { greeting: "Hello" };
|
|
10
|
+
|
|
11
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
12
|
+
|
|
13
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
14
|
+
return [
|
|
15
|
+
defineRoute({
|
|
16
|
+
method: "POST",
|
|
17
|
+
path: "/greet",
|
|
18
|
+
inputSchema: z.object({ name: z.string() }),
|
|
19
|
+
outputSchema: z.object({ message: z.string() }),
|
|
20
|
+
handler: async ({ input }, { json }) => {
|
|
21
|
+
const { name } = await input.valid();
|
|
22
|
+
return json({ message: `Hello, ${name}!` });
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
29
|
+
|
|
30
|
+
const response = await instance.callRoute("POST", "/greet", {
|
|
31
|
+
body: { name: "World" },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(response.type).toBe("json");
|
|
35
|
+
if (response.type === "json") {
|
|
36
|
+
expect(response.status).toBe(200);
|
|
37
|
+
expect(response.data).toEqual({ message: "Hello, World!" });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("calls route handler with path params", async () => {
|
|
42
|
+
const config = {};
|
|
43
|
+
|
|
44
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
45
|
+
|
|
46
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
47
|
+
return [
|
|
48
|
+
defineRoute({
|
|
49
|
+
method: "GET",
|
|
50
|
+
path: "/users/:id",
|
|
51
|
+
outputSchema: z.object({ userId: z.string() }),
|
|
52
|
+
handler: async ({ pathParams }, { json }) => {
|
|
53
|
+
return json({ userId: pathParams.id });
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
60
|
+
|
|
61
|
+
const response = await instance.callRoute("GET", "/users/:id", {
|
|
62
|
+
pathParams: { id: "123" },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(response.type).toBe("json");
|
|
66
|
+
if (response.type === "json") {
|
|
67
|
+
expect(response.status).toBe(200);
|
|
68
|
+
expect(response.data).toEqual({ userId: "123" });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("calls route handler with query parameters", async () => {
|
|
73
|
+
const config = {};
|
|
74
|
+
|
|
75
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
76
|
+
|
|
77
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
78
|
+
return [
|
|
79
|
+
defineRoute({
|
|
80
|
+
method: "GET",
|
|
81
|
+
path: "/search",
|
|
82
|
+
queryParameters: ["q", "limit"],
|
|
83
|
+
outputSchema: z.object({ query: z.string(), limit: z.string().nullable() }),
|
|
84
|
+
handler: async ({ query }, { json }) => {
|
|
85
|
+
return json({
|
|
86
|
+
query: query.get("q") || "",
|
|
87
|
+
limit: query.get("limit"),
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
95
|
+
|
|
96
|
+
const response = await instance.callRoute("GET", "/search", {
|
|
97
|
+
query: { q: "test", limit: "10" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(response.type).toBe("json");
|
|
101
|
+
if (response.type === "json") {
|
|
102
|
+
expect(response.status).toBe(200);
|
|
103
|
+
expect(response.data).toEqual({ query: "test", limit: "10" });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("calls route handler with URLSearchParams query", async () => {
|
|
108
|
+
const config = {};
|
|
109
|
+
|
|
110
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
111
|
+
|
|
112
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
113
|
+
return [
|
|
114
|
+
defineRoute({
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: "/search",
|
|
117
|
+
queryParameters: ["q"],
|
|
118
|
+
outputSchema: z.object({ query: z.string() }),
|
|
119
|
+
handler: async ({ query }, { json }) => {
|
|
120
|
+
return json({ query: query.get("q") || "" });
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
127
|
+
|
|
128
|
+
const searchParams = new URLSearchParams({ q: "test-query" });
|
|
129
|
+
const response = await instance.callRoute("GET", "/search", {
|
|
130
|
+
query: searchParams,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(response.type).toBe("json");
|
|
134
|
+
if (response.type === "json") {
|
|
135
|
+
expect(response.status).toBe(200);
|
|
136
|
+
expect(response.data).toEqual({ query: "test-query" });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("calls route handler with headers", async () => {
|
|
141
|
+
const config = {};
|
|
142
|
+
|
|
143
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
144
|
+
|
|
145
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
146
|
+
return [
|
|
147
|
+
defineRoute({
|
|
148
|
+
method: "GET",
|
|
149
|
+
path: "/headers",
|
|
150
|
+
outputSchema: z.object({ auth: z.string().nullable() }),
|
|
151
|
+
handler: async ({ headers }, { json }) => {
|
|
152
|
+
return json({ auth: headers.get("authorization") });
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
159
|
+
|
|
160
|
+
const response = await instance.callRoute("GET", "/headers", {
|
|
161
|
+
headers: { authorization: "Bearer token123" },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(response.type).toBe("json");
|
|
165
|
+
if (response.type === "json") {
|
|
166
|
+
expect(response.status).toBe(200);
|
|
167
|
+
expect(response.data).toEqual({ auth: "Bearer token123" });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("calls route handler with Headers object", async () => {
|
|
172
|
+
const config = {};
|
|
173
|
+
|
|
174
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
175
|
+
|
|
176
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
177
|
+
return [
|
|
178
|
+
defineRoute({
|
|
179
|
+
method: "GET",
|
|
180
|
+
path: "/headers",
|
|
181
|
+
outputSchema: z.object({ auth: z.string().nullable() }),
|
|
182
|
+
handler: async ({ headers }, { json }) => {
|
|
183
|
+
return json({ auth: headers.get("authorization") });
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
190
|
+
|
|
191
|
+
const requestHeaders = new Headers({ authorization: "Bearer token456" });
|
|
192
|
+
const response = await instance.callRoute("GET", "/headers", {
|
|
193
|
+
headers: requestHeaders,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(response.type).toBe("json");
|
|
197
|
+
if (response.type === "json") {
|
|
198
|
+
expect(response.status).toBe(200);
|
|
199
|
+
expect(response.data).toEqual({ auth: "Bearer token456" });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("preserves response headers including Set-Cookie", async () => {
|
|
204
|
+
const config = {};
|
|
205
|
+
|
|
206
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
207
|
+
|
|
208
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
209
|
+
return [
|
|
210
|
+
defineRoute({
|
|
211
|
+
method: "POST",
|
|
212
|
+
path: "/login",
|
|
213
|
+
inputSchema: z.object({ username: z.string() }),
|
|
214
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
215
|
+
handler: async ({ input }, { json }) => {
|
|
216
|
+
const { username } = await input.valid();
|
|
217
|
+
const response = json({ success: true });
|
|
218
|
+
response.headers.set("Set-Cookie", `session=${username}; HttpOnly; Path=/`);
|
|
219
|
+
response.headers.set("X-Custom-Header", "custom-value");
|
|
220
|
+
return response;
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
];
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
227
|
+
|
|
228
|
+
const response = await instance.callRoute("POST", "/login", {
|
|
229
|
+
body: { username: "testuser" },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(response.type).toBe("json");
|
|
233
|
+
if (response.type === "json") {
|
|
234
|
+
expect(response.status).toBe(200);
|
|
235
|
+
expect(response.headers.get("Set-Cookie")).toBe("session=testuser; HttpOnly; Path=/");
|
|
236
|
+
expect(response.headers.get("X-Custom-Header")).toBe("custom-value");
|
|
237
|
+
expect(response.data).toEqual({ success: true });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("validates input and returns error for invalid data", async () => {
|
|
242
|
+
const config = {};
|
|
243
|
+
|
|
244
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
245
|
+
|
|
246
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
247
|
+
return [
|
|
248
|
+
defineRoute({
|
|
249
|
+
method: "POST",
|
|
250
|
+
path: "/validate",
|
|
251
|
+
inputSchema: z.object({ age: z.number().min(18) }),
|
|
252
|
+
outputSchema: z.object({ valid: z.boolean() }),
|
|
253
|
+
handler: async ({ input }, { json }) => {
|
|
254
|
+
const { age } = await input.valid();
|
|
255
|
+
return json({ valid: age >= 18 });
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
];
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
262
|
+
|
|
263
|
+
const response = await instance.callRoute("POST", "/validate", {
|
|
264
|
+
body: { age: 15 },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(response.type).toBe("error");
|
|
268
|
+
if (response.type === "error") {
|
|
269
|
+
expect(response.status).toBe(400);
|
|
270
|
+
expect(response.error.code).toBe("FRAGNO_VALIDATION_ERROR");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("handles errors thrown in route handler", async () => {
|
|
275
|
+
const config = {};
|
|
276
|
+
|
|
277
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
278
|
+
|
|
279
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
280
|
+
return [
|
|
281
|
+
defineRoute({
|
|
282
|
+
method: "GET",
|
|
283
|
+
path: "/error",
|
|
284
|
+
outputSchema: z.object({ result: z.string() }),
|
|
285
|
+
handler: async () => {
|
|
286
|
+
throw new Error("Unexpected error");
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
];
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
293
|
+
|
|
294
|
+
const response = await instance.callRoute("GET", "/error");
|
|
295
|
+
|
|
296
|
+
expect(response.type).toBe("error");
|
|
297
|
+
if (response.type === "error") {
|
|
298
|
+
expect(response.status).toBe(500);
|
|
299
|
+
expect(response.error).toEqual({
|
|
300
|
+
message: "Internal server error",
|
|
301
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("calls route handler with all parameters combined", async () => {
|
|
307
|
+
const config = {};
|
|
308
|
+
|
|
309
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
310
|
+
|
|
311
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
312
|
+
return [
|
|
313
|
+
defineRoute({
|
|
314
|
+
method: "POST",
|
|
315
|
+
path: "/users/:id/update",
|
|
316
|
+
inputSchema: z.object({ name: z.string() }),
|
|
317
|
+
queryParameters: ["reason"],
|
|
318
|
+
outputSchema: z.object({
|
|
319
|
+
id: z.string(),
|
|
320
|
+
name: z.string(),
|
|
321
|
+
reason: z.string().nullable(),
|
|
322
|
+
auth: z.string().nullable(),
|
|
323
|
+
}),
|
|
324
|
+
handler: async ({ pathParams, input, query, headers }, { json }) => {
|
|
325
|
+
const { name } = await input.valid();
|
|
326
|
+
return json({
|
|
327
|
+
id: pathParams.id,
|
|
328
|
+
name,
|
|
329
|
+
reason: query.get("reason"),
|
|
330
|
+
auth: headers.get("authorization"),
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
];
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
338
|
+
|
|
339
|
+
const response = await instance.callRoute("POST", "/users/:id/update", {
|
|
340
|
+
pathParams: { id: "user123" },
|
|
341
|
+
body: { name: "John Doe" },
|
|
342
|
+
query: { reason: "profile-update" },
|
|
343
|
+
headers: { authorization: "Bearer xyz" },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(response.type).toBe("json");
|
|
347
|
+
if (response.type === "json") {
|
|
348
|
+
expect(response.status).toBe(200);
|
|
349
|
+
expect(response.data).toEqual({
|
|
350
|
+
id: "user123",
|
|
351
|
+
name: "John Doe",
|
|
352
|
+
reason: "profile-update",
|
|
353
|
+
auth: "Bearer xyz",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("calls route handler with no input options", async () => {
|
|
359
|
+
const config = {};
|
|
360
|
+
|
|
361
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
362
|
+
|
|
363
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
364
|
+
return [
|
|
365
|
+
defineRoute({
|
|
366
|
+
method: "GET",
|
|
367
|
+
path: "/ping",
|
|
368
|
+
outputSchema: z.object({ status: z.string() }),
|
|
369
|
+
handler: async (_, { json }) => {
|
|
370
|
+
return json({ status: "ok" });
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
];
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
377
|
+
|
|
378
|
+
const response = await instance.callRoute("GET", "/ping");
|
|
379
|
+
|
|
380
|
+
expect(response.type).toBe("json");
|
|
381
|
+
if (response.type === "json") {
|
|
382
|
+
expect(response.status).toBe(200);
|
|
383
|
+
expect(response.data).toEqual({ status: "ok" });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("uses services in route handler called via callRoute", async () => {
|
|
388
|
+
const config = {};
|
|
389
|
+
|
|
390
|
+
type Services = {
|
|
391
|
+
getUserName: () => string;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const fragment = defineFragment<typeof config>("test-fragment").withServices(() => {
|
|
395
|
+
return {
|
|
396
|
+
getUserName: () => "Test User",
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const routesFactory = defineRoutes<typeof config, {}, Services>().create(({ services }) => {
|
|
401
|
+
return [
|
|
402
|
+
defineRoute({
|
|
403
|
+
method: "GET",
|
|
404
|
+
path: "/me",
|
|
405
|
+
outputSchema: z.object({ name: z.string() }),
|
|
406
|
+
handler: async (_, { json }) => {
|
|
407
|
+
return json({ name: services.getUserName() });
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
];
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
414
|
+
|
|
415
|
+
const response = await instance.callRoute("GET", "/me");
|
|
416
|
+
|
|
417
|
+
expect(response.type).toBe("json");
|
|
418
|
+
if (response.type === "json") {
|
|
419
|
+
expect(response.status).toBe(200);
|
|
420
|
+
expect(response.data).toEqual({ name: "Test User" });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("uses deps in route handler called via callRoute", async () => {
|
|
425
|
+
const config = {};
|
|
426
|
+
|
|
427
|
+
type Deps = {
|
|
428
|
+
database: { query: () => string };
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const fragment = defineFragment<typeof config>("test-fragment").withDependencies(() => {
|
|
432
|
+
return {
|
|
433
|
+
database: { query: () => "database-result" },
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const routesFactory = defineRoutes<typeof config, Deps>().create(({ deps }) => {
|
|
438
|
+
return [
|
|
439
|
+
defineRoute({
|
|
440
|
+
method: "GET",
|
|
441
|
+
path: "/data",
|
|
442
|
+
outputSchema: z.object({ result: z.string() }),
|
|
443
|
+
handler: async (_, { json }) => {
|
|
444
|
+
return json({ result: deps.database.query() });
|
|
445
|
+
},
|
|
446
|
+
}),
|
|
447
|
+
];
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
451
|
+
|
|
452
|
+
const response = await instance.callRoute("GET", "/data");
|
|
453
|
+
|
|
454
|
+
expect(response.type).toBe("json");
|
|
455
|
+
if (response.type === "json") {
|
|
456
|
+
expect(response.status).toBe(200);
|
|
457
|
+
expect(response.data).toEqual({ result: "database-result" });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
@@ -19,14 +19,23 @@ import {
|
|
|
19
19
|
} from "./request-middleware";
|
|
20
20
|
import type { FragmentDefinition } from "./fragment-builder";
|
|
21
21
|
import { MutableRequestState } from "./mutable-request-state";
|
|
22
|
+
import type { RouteHandlerInputOptions } from "./route-handler-input-options";
|
|
23
|
+
import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
|
|
24
|
+
import { type FragnoResponse, parseFragnoResponse } from "./fragno-response";
|
|
25
|
+
import type { InferOrUnknown } from "../util/types-util";
|
|
22
26
|
|
|
23
27
|
export interface FragnoPublicConfig {
|
|
24
28
|
mountRoute?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export type FetcherConfig =
|
|
32
|
+
| { type: "options"; options: RequestInit }
|
|
33
|
+
| { type: "function"; fetcher: typeof fetch };
|
|
34
|
+
|
|
27
35
|
export interface FragnoPublicClientConfig {
|
|
28
36
|
mountRoute?: string;
|
|
29
37
|
baseUrl?: string;
|
|
38
|
+
fetcherConfig?: FetcherConfig;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
type AstroHandlers = {
|
|
@@ -89,6 +98,26 @@ export interface FragnoInstantiatedFragment<
|
|
|
89
98
|
handlersFor: <T extends FullstackFrameworks>(framework: T) => HandlersByFramework[T];
|
|
90
99
|
handler: (req: Request) => Promise<Response>;
|
|
91
100
|
mountRoute: string;
|
|
101
|
+
callRoute: <TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
|
|
102
|
+
method: TMethod,
|
|
103
|
+
path: TPath,
|
|
104
|
+
inputOptions?: RouteHandlerInputOptions<
|
|
105
|
+
TPath,
|
|
106
|
+
ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
|
|
107
|
+
>,
|
|
108
|
+
) => Promise<
|
|
109
|
+
FragnoResponse<
|
|
110
|
+
InferOrUnknown<NonNullable<ExtractRouteByPath<TRoutes, TPath, TMethod>["outputSchema"]>>
|
|
111
|
+
>
|
|
112
|
+
>;
|
|
113
|
+
callRouteRaw: <TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
|
|
114
|
+
method: TMethod,
|
|
115
|
+
path: TPath,
|
|
116
|
+
inputOptions?: RouteHandlerInputOptions<
|
|
117
|
+
TPath,
|
|
118
|
+
ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
|
|
119
|
+
>,
|
|
120
|
+
) => Promise<Response>;
|
|
92
121
|
withMiddleware: (
|
|
93
122
|
handler: FragnoMiddlewareCallback<TRoutes, TDeps, TServices>,
|
|
94
123
|
) => FragnoInstantiatedFragment<TRoutes, TDeps, TServices, TAdditionalContext>;
|
|
@@ -133,6 +162,8 @@ export function createFragment<
|
|
|
133
162
|
TServices,
|
|
134
163
|
TAdditionalContext
|
|
135
164
|
> {
|
|
165
|
+
type TRoutes = FlattenRouteFactories<TRoutesOrFactories>;
|
|
166
|
+
|
|
136
167
|
const definition = fragmentBuilder.definition;
|
|
137
168
|
|
|
138
169
|
const dependencies = definition.dependencies?.(config, options) ?? ({} as TDeps);
|
|
@@ -193,6 +224,96 @@ export function createFragment<
|
|
|
193
224
|
|
|
194
225
|
return fragment;
|
|
195
226
|
},
|
|
227
|
+
callRoute: async <TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
|
|
228
|
+
method: TMethod,
|
|
229
|
+
path: TPath,
|
|
230
|
+
inputOptions?: RouteHandlerInputOptions<
|
|
231
|
+
TPath,
|
|
232
|
+
ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
|
|
233
|
+
>,
|
|
234
|
+
): Promise<
|
|
235
|
+
FragnoResponse<
|
|
236
|
+
InferOrUnknown<NonNullable<ExtractRouteByPath<TRoutes, TPath, TMethod>["outputSchema"]>>
|
|
237
|
+
>
|
|
238
|
+
> => {
|
|
239
|
+
const response = await fragment.callRouteRaw(method, path, inputOptions);
|
|
240
|
+
return parseFragnoResponse(response);
|
|
241
|
+
},
|
|
242
|
+
callRouteRaw: async <
|
|
243
|
+
TMethod extends HTTPMethod,
|
|
244
|
+
TPath extends ExtractRoutePath<TRoutes, TMethod>,
|
|
245
|
+
>(
|
|
246
|
+
method: TMethod,
|
|
247
|
+
path: TPath,
|
|
248
|
+
inputOptions?: RouteHandlerInputOptions<
|
|
249
|
+
TPath,
|
|
250
|
+
ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
|
|
251
|
+
>,
|
|
252
|
+
): Promise<Response> => {
|
|
253
|
+
// Find the route configuration
|
|
254
|
+
const route = routes.find((r) => r.method === method && r.path === path);
|
|
255
|
+
|
|
256
|
+
if (!route) {
|
|
257
|
+
return Response.json(
|
|
258
|
+
{
|
|
259
|
+
error: `Route ${method} ${path} not found`,
|
|
260
|
+
code: "ROUTE_NOT_FOUND",
|
|
261
|
+
},
|
|
262
|
+
{ status: 404 },
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const {
|
|
267
|
+
pathParams = {} as ExtractPathParams<TPath>,
|
|
268
|
+
body,
|
|
269
|
+
query,
|
|
270
|
+
headers,
|
|
271
|
+
} = inputOptions || {};
|
|
272
|
+
|
|
273
|
+
// Convert query to URLSearchParams if needed
|
|
274
|
+
const searchParams =
|
|
275
|
+
query instanceof URLSearchParams
|
|
276
|
+
? query
|
|
277
|
+
: query
|
|
278
|
+
? new URLSearchParams(query)
|
|
279
|
+
: new URLSearchParams();
|
|
280
|
+
|
|
281
|
+
// Convert headers to Headers if needed
|
|
282
|
+
const requestHeaders =
|
|
283
|
+
headers instanceof Headers ? headers : headers ? new Headers(headers) : new Headers();
|
|
284
|
+
|
|
285
|
+
// Construct RequestInputContext
|
|
286
|
+
const inputContext = new RequestInputContext({
|
|
287
|
+
path: route.path,
|
|
288
|
+
method: route.method,
|
|
289
|
+
pathParams,
|
|
290
|
+
searchParams,
|
|
291
|
+
headers: requestHeaders,
|
|
292
|
+
parsedBody: body,
|
|
293
|
+
inputSchema: route.inputSchema,
|
|
294
|
+
shouldValidateInput: true, // Enable validation for production use
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Construct RequestOutputContext
|
|
298
|
+
const outputContext = new RequestOutputContext(route.outputSchema);
|
|
299
|
+
|
|
300
|
+
// Call the route handler
|
|
301
|
+
try {
|
|
302
|
+
const response = await route.handler(inputContext, outputContext);
|
|
303
|
+
return response;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error("Error in callRoute handler", error);
|
|
306
|
+
|
|
307
|
+
if (error instanceof FragnoApiError) {
|
|
308
|
+
return error.toResponse();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return Response.json(
|
|
312
|
+
{ error: "Internal server error", code: "INTERNAL_SERVER_ERROR" },
|
|
313
|
+
{ status: 500 },
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
196
317
|
handlersFor: <T extends FullstackFrameworks>(framework: T): HandlersByFramework[T] => {
|
|
197
318
|
const handler = fragment.handler;
|
|
198
319
|
|