@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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +49 -45
  2. package/CHANGELOG.md +52 -0
  3. package/dist/api/api.d.ts +2 -2
  4. package/dist/api/fragment-builder.d.ts +3 -2
  5. package/dist/api/fragment-instantiation.d.ts +4 -3
  6. package/dist/api/fragment-instantiation.js +3 -3
  7. package/dist/api/route.d.ts +3 -0
  8. package/dist/api/route.js +3 -0
  9. package/dist/{api-B1-h7jPC.d.ts → api-BWN97TOr.d.ts} +17 -3
  10. package/dist/api-BWN97TOr.d.ts.map +1 -0
  11. package/dist/api-DngJDcmO.js.map +1 -1
  12. package/dist/client/client.d.ts +4 -3
  13. package/dist/client/client.js +3 -3
  14. package/dist/client/client.svelte.d.ts +3 -3
  15. package/dist/client/client.svelte.d.ts.map +1 -1
  16. package/dist/client/client.svelte.js +3 -3
  17. package/dist/client/react.d.ts +3 -3
  18. package/dist/client/react.d.ts.map +1 -1
  19. package/dist/client/react.js +3 -3
  20. package/dist/client/solid.d.ts +3 -3
  21. package/dist/client/solid.d.ts.map +1 -1
  22. package/dist/client/solid.js +3 -3
  23. package/dist/client/vanilla.d.ts +3 -3
  24. package/dist/client/vanilla.d.ts.map +1 -1
  25. package/dist/client/vanilla.js +3 -3
  26. package/dist/client/vue.d.ts +3 -3
  27. package/dist/client/vue.d.ts.map +1 -1
  28. package/dist/client/vue.js +7 -7
  29. package/dist/client/vue.js.map +1 -1
  30. package/dist/{client-YUZaNg5U.js → client-C5LsYHEI.js} +92 -11
  31. package/dist/client-C5LsYHEI.js.map +1 -0
  32. package/dist/{fragment-builder-DsqUOfJ5.d.ts → fragment-builder-MGr68GNb.d.ts} +80 -44
  33. package/dist/fragment-builder-MGr68GNb.d.ts.map +1 -0
  34. package/dist/{fragment-instantiation-Cp0K8zdS.js → fragment-instantiation-C4wvwl6V.js} +108 -3
  35. package/dist/fragment-instantiation-C4wvwl6V.js.map +1 -0
  36. package/dist/mod.d.ts +3 -2
  37. package/dist/mod.js +3 -3
  38. package/dist/{route-Dk1GyqHs.js → request-output-context-CdIjwmEN.js} +13 -24
  39. package/dist/request-output-context-CdIjwmEN.js.map +1 -0
  40. package/dist/route-Bl9Zr1Yv.d.ts +26 -0
  41. package/dist/route-Bl9Zr1Yv.d.ts.map +1 -0
  42. package/dist/route-C5Uryylh.js +21 -0
  43. package/dist/route-C5Uryylh.js.map +1 -0
  44. package/dist/test/test.d.ts +24 -70
  45. package/dist/test/test.d.ts.map +1 -1
  46. package/dist/test/test.js +27 -115
  47. package/dist/test/test.js.map +1 -1
  48. package/package.json +6 -1
  49. package/src/api/api.ts +1 -0
  50. package/src/api/fragment-instantiation.test.ts +460 -0
  51. package/src/api/fragment-instantiation.ts +121 -0
  52. package/src/api/fragno-response.ts +132 -0
  53. package/src/api/internal/path-type.test.ts +7 -7
  54. package/src/api/internal/path.ts +1 -1
  55. package/src/api/request-output-context.test.ts +10 -10
  56. package/src/api/request-output-context.ts +3 -3
  57. package/src/api/route-handler-input-options.ts +15 -0
  58. package/src/client/client-types.test.ts +4 -4
  59. package/src/client/client.test.ts +341 -0
  60. package/src/client/client.ts +96 -15
  61. package/src/client/internal/fetcher-merge.ts +59 -0
  62. package/src/test/test.test.ts +110 -165
  63. package/src/test/test.ts +56 -266
  64. package/tsdown.config.ts +1 -0
  65. package/dist/api-B1-h7jPC.d.ts.map +0 -1
  66. package/dist/client-YUZaNg5U.js.map +0 -1
  67. package/dist/fragment-builder-DsqUOfJ5.d.ts.map +0 -1
  68. package/dist/fragment-instantiation-Cp0K8zdS.js.map +0 -1
  69. package/dist/route-CTxjMtGZ.js +0 -10
  70. package/dist/route-CTxjMtGZ.js.map +0 -1
  71. 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