@fragno-dev/core 0.1.4 → 0.1.6
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 +53 -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-BX90b4-D.d.ts → api-CoCkNi6h.d.ts} +20 -7
- package/dist/api-CoCkNi6h.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 +3 -3
- package/dist/{client-C6LChM0Y.js → client-DJfCJiHK.js} +81 -7
- package/dist/client-DJfCJiHK.js.map +1 -0
- package/dist/{fragment-builder-BZr2JkuW.d.ts → fragment-builder-8-tiECi5.d.ts} +75 -38
- package/dist/fragment-builder-8-tiECi5.d.ts.map +1 -0
- package/dist/{fragment-instantiation-D74OQjbn.js → fragment-instantiation-C4wvwl6V.js} +129 -6
- 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-D1MZR6JL.js → request-output-context-CdIjwmEN.js} +22 -33
- package/dist/request-output-context-CdIjwmEN.js.map +1 -0
- package/dist/route-C5Uryylh.js +21 -0
- package/dist/route-C5Uryylh.js.map +1 -0
- package/dist/route-mGLYSUvD.d.ts +26 -0
- package/dist/route-mGLYSUvD.d.ts.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 +157 -5
- package/src/api/fragno-response.ts +132 -0
- package/src/api/request-input-context.test.ts +37 -29
- package/src/api/request-input-context.ts +16 -14
- 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.test.ts +264 -0
- package/src/client/client.ts +65 -3
- 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-BX90b4-D.d.ts.map +0 -1
- package/dist/client-C6LChM0Y.js.map +0 -1
- package/dist/fragment-builder-BZr2JkuW.d.ts.map +0 -1
- package/dist/fragment-instantiation-D74OQjbn.js.map +0 -1
- package/dist/route-CTxjMtGZ.js +0 -10
- package/dist/route-CTxjMtGZ.js.map +0 -1
- package/dist/route-D1MZR6JL.js.map +0 -1
|
@@ -3,7 +3,7 @@ import { type FragnoRouteConfig, type HTTPMethod } from "./api";
|
|
|
3
3
|
import { FragnoApiError } from "./error";
|
|
4
4
|
import { getMountRoute } from "./internal/route";
|
|
5
5
|
import { addRoute, createRouter, findRoute } from "rou3";
|
|
6
|
-
import { RequestInputContext } from "./request-input-context";
|
|
6
|
+
import { RequestInputContext, type RequestBodyType } from "./request-input-context";
|
|
7
7
|
import type { ExtractPathParams } from "./internal/path";
|
|
8
8
|
import { RequestOutputContext } from "./request-output-context";
|
|
9
9
|
import {
|
|
@@ -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 = {
|
|
@@ -48,6 +57,8 @@ type SolidStartHandlers = {
|
|
|
48
57
|
OPTIONS: (args: { request: Request }) => Promise<Response>;
|
|
49
58
|
};
|
|
50
59
|
|
|
60
|
+
type TanStackStartHandlers = SolidStartHandlers;
|
|
61
|
+
|
|
51
62
|
type StandardHandlers = {
|
|
52
63
|
GET: (req: Request) => Promise<Response>;
|
|
53
64
|
POST: (req: Request) => Promise<Response>;
|
|
@@ -64,6 +75,7 @@ type HandlersByFramework = {
|
|
|
64
75
|
"next-js": StandardHandlers;
|
|
65
76
|
"svelte-kit": StandardHandlers;
|
|
66
77
|
"solid-start": SolidStartHandlers;
|
|
78
|
+
"tanstack-start": TanStackStartHandlers;
|
|
67
79
|
};
|
|
68
80
|
|
|
69
81
|
// Not actually a symbol, since we might be dealing with multiple instances of this code.
|
|
@@ -86,6 +98,26 @@ export interface FragnoInstantiatedFragment<
|
|
|
86
98
|
handlersFor: <T extends FullstackFrameworks>(framework: T) => HandlersByFramework[T];
|
|
87
99
|
handler: (req: Request) => Promise<Response>;
|
|
88
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>;
|
|
89
121
|
withMiddleware: (
|
|
90
122
|
handler: FragnoMiddlewareCallback<TRoutes, TDeps, TServices>,
|
|
91
123
|
) => FragnoInstantiatedFragment<TRoutes, TDeps, TServices, TAdditionalContext>;
|
|
@@ -130,6 +162,8 @@ export function createFragment<
|
|
|
130
162
|
TServices,
|
|
131
163
|
TAdditionalContext
|
|
132
164
|
> {
|
|
165
|
+
type TRoutes = FlattenRouteFactories<TRoutesOrFactories>;
|
|
166
|
+
|
|
133
167
|
const definition = fragmentBuilder.definition;
|
|
134
168
|
|
|
135
169
|
const dependencies = definition.dependencies?.(config, options) ?? ({} as TDeps);
|
|
@@ -190,6 +224,96 @@ export function createFragment<
|
|
|
190
224
|
|
|
191
225
|
return fragment;
|
|
192
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
|
+
},
|
|
193
317
|
handlersFor: <T extends FullstackFrameworks>(framework: T): HandlersByFramework[T] => {
|
|
194
318
|
const handler = fragment.handler;
|
|
195
319
|
|
|
@@ -233,6 +357,15 @@ export function createFragment<
|
|
|
233
357
|
HEAD: ({ request }: { request: Request }) => handler(request),
|
|
234
358
|
OPTIONS: ({ request }: { request: Request }) => handler(request),
|
|
235
359
|
},
|
|
360
|
+
"tanstack-start": {
|
|
361
|
+
GET: ({ request }: { request: Request }) => handler(request),
|
|
362
|
+
POST: ({ request }: { request: Request }) => handler(request),
|
|
363
|
+
PUT: ({ request }: { request: Request }) => handler(request),
|
|
364
|
+
DELETE: ({ request }: { request: Request }) => handler(request),
|
|
365
|
+
PATCH: ({ request }: { request: Request }) => handler(request),
|
|
366
|
+
HEAD: ({ request }: { request: Request }) => handler(request),
|
|
367
|
+
OPTIONS: ({ request }: { request: Request }) => handler(request),
|
|
368
|
+
},
|
|
236
369
|
} satisfies HandlersByFramework;
|
|
237
370
|
|
|
238
371
|
return allHandlers[framework];
|
|
@@ -269,10 +402,28 @@ export function createFragment<
|
|
|
269
402
|
const outputContext = new RequestOutputContext(outputSchema);
|
|
270
403
|
|
|
271
404
|
// Create mutable request state that can be modified by middleware
|
|
272
|
-
// Clone the request to
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
405
|
+
// Clone the request to read body as both text and JSON without consuming original stream
|
|
406
|
+
let requestBody: RequestBodyType = undefined;
|
|
407
|
+
let rawBody: string | undefined = undefined;
|
|
408
|
+
|
|
409
|
+
if (req.body instanceof ReadableStream) {
|
|
410
|
+
// Clone request to make sure we don't consume body stream
|
|
411
|
+
const clonedReq = req.clone();
|
|
412
|
+
|
|
413
|
+
// Get raw text
|
|
414
|
+
rawBody = await clonedReq.text();
|
|
415
|
+
|
|
416
|
+
// Parse JSON if body is not empty
|
|
417
|
+
if (rawBody) {
|
|
418
|
+
try {
|
|
419
|
+
requestBody = JSON.parse(rawBody);
|
|
420
|
+
} catch {
|
|
421
|
+
// If JSON parsing fails, keep body as undefined
|
|
422
|
+
// This handles cases where body is not JSON
|
|
423
|
+
requestBody = undefined;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
276
427
|
|
|
277
428
|
const requestState = new MutableRequestState({
|
|
278
429
|
pathParams: route.params ?? {},
|
|
@@ -323,6 +474,7 @@ export function createFragment<
|
|
|
323
474
|
pathParams: (route.params ?? {}) as ExtractPathParams<typeof path>,
|
|
324
475
|
inputSchema,
|
|
325
476
|
state: requestState,
|
|
477
|
+
rawBody,
|
|
326
478
|
});
|
|
327
479
|
|
|
328
480
|
try {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discriminated union representing all possible Fragno response types
|
|
3
|
+
*/
|
|
4
|
+
export type FragnoResponse<T> =
|
|
5
|
+
| {
|
|
6
|
+
type: "empty";
|
|
7
|
+
status: number;
|
|
8
|
+
headers: Headers;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
type: "error";
|
|
12
|
+
status: number;
|
|
13
|
+
headers: Headers;
|
|
14
|
+
error: { message: string; code: string };
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
type: "json";
|
|
18
|
+
status: number;
|
|
19
|
+
headers: Headers;
|
|
20
|
+
data: T;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
type: "jsonStream";
|
|
24
|
+
status: number;
|
|
25
|
+
headers: Headers;
|
|
26
|
+
stream: AsyncGenerator<T extends unknown[] ? T[number] : T>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse a Response object into a FragnoResponse discriminated union
|
|
31
|
+
*/
|
|
32
|
+
export async function parseFragnoResponse<T>(response: Response): Promise<FragnoResponse<T>> {
|
|
33
|
+
const status = response.status;
|
|
34
|
+
const headers = response.headers;
|
|
35
|
+
const contentType = headers.get("content-type") || "";
|
|
36
|
+
|
|
37
|
+
// Check for streaming response
|
|
38
|
+
if (contentType.includes("application/x-ndjson")) {
|
|
39
|
+
return {
|
|
40
|
+
type: "jsonStream",
|
|
41
|
+
status,
|
|
42
|
+
headers,
|
|
43
|
+
stream: parseNDJSONStream<T>(response),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse JSON body
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
|
|
50
|
+
// Empty response
|
|
51
|
+
if (!text || text === "null") {
|
|
52
|
+
return {
|
|
53
|
+
type: "empty",
|
|
54
|
+
status,
|
|
55
|
+
headers,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = JSON.parse(text);
|
|
60
|
+
|
|
61
|
+
// Error response (has message and code, or error and code)
|
|
62
|
+
if (data && typeof data === "object" && "code" in data) {
|
|
63
|
+
if ("message" in data) {
|
|
64
|
+
return {
|
|
65
|
+
type: "error",
|
|
66
|
+
status,
|
|
67
|
+
headers,
|
|
68
|
+
error: { message: data.message, code: data.code },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if ("error" in data) {
|
|
72
|
+
return {
|
|
73
|
+
type: "error",
|
|
74
|
+
status,
|
|
75
|
+
headers,
|
|
76
|
+
error: { message: data.error, code: data.code },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// JSON response
|
|
82
|
+
return {
|
|
83
|
+
type: "json",
|
|
84
|
+
status,
|
|
85
|
+
headers,
|
|
86
|
+
data: data as T,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse an NDJSON stream into an async generator
|
|
92
|
+
*/
|
|
93
|
+
async function* parseNDJSONStream<T>(
|
|
94
|
+
response: Response,
|
|
95
|
+
): AsyncGenerator<T extends unknown[] ? T[number] : T> {
|
|
96
|
+
if (!response.body) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reader = response.body.getReader();
|
|
101
|
+
const decoder = new TextDecoder();
|
|
102
|
+
let buffer = "";
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
while (true) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
|
|
108
|
+
if (done) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
buffer += decoder.decode(value, { stream: true });
|
|
113
|
+
const lines = buffer.split("\n");
|
|
114
|
+
|
|
115
|
+
// Keep the last incomplete line in the buffer
|
|
116
|
+
buffer = lines.pop() || "";
|
|
117
|
+
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
if (line.trim()) {
|
|
120
|
+
yield JSON.parse(line) as T extends unknown[] ? T[number] : T;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Process any remaining data in the buffer
|
|
126
|
+
if (buffer.trim()) {
|
|
127
|
+
yield JSON.parse(buffer) as T extends unknown[] ? T[number] : T;
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
reader.releaseLock();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -42,61 +42,67 @@ describe("RequestContext", () => {
|
|
|
42
42
|
pathParams: {},
|
|
43
43
|
searchParams: new URLSearchParams(),
|
|
44
44
|
headers: new Headers(),
|
|
45
|
-
|
|
45
|
+
parsedBody: undefined,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
const { path, pathParams, query: searchParams, input, rawBody
|
|
48
|
+
const { path, pathParams, query: searchParams, input, rawBody } = ctx;
|
|
49
49
|
|
|
50
50
|
expect(path).toBe("/");
|
|
51
51
|
expect(pathParams).toEqual({});
|
|
52
52
|
expect(searchParams).toBeInstanceOf(URLSearchParams);
|
|
53
53
|
expect(input).toBeUndefined();
|
|
54
|
-
expect(
|
|
54
|
+
expect(rawBody).toBeUndefined();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test("Should support body in constructor", () => {
|
|
58
58
|
const jsonBody = { test: "data" };
|
|
59
|
+
const rawBodyText = JSON.stringify(jsonBody);
|
|
59
60
|
const ctx = new RequestInputContext({
|
|
60
61
|
method: "POST",
|
|
61
62
|
path: "/api/test",
|
|
62
63
|
pathParams: {},
|
|
63
64
|
searchParams: new URLSearchParams(),
|
|
64
65
|
headers: new Headers(),
|
|
65
|
-
|
|
66
|
+
parsedBody: jsonBody,
|
|
67
|
+
rawBody: rawBodyText,
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
expect(ctx.rawBody).toEqual(
|
|
70
|
+
expect(ctx.rawBody).toEqual(rawBodyText);
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
test("Should support FormData body", () => {
|
|
72
74
|
const formData = new FormData();
|
|
73
75
|
formData.append("key", "value");
|
|
76
|
+
const rawBodyText = "form-data-as-text";
|
|
74
77
|
|
|
75
78
|
const ctx = new RequestInputContext({
|
|
76
79
|
path: "/api/form",
|
|
77
80
|
pathParams: {},
|
|
78
81
|
searchParams: new URLSearchParams(),
|
|
79
82
|
headers: new Headers(),
|
|
80
|
-
|
|
83
|
+
parsedBody: formData,
|
|
84
|
+
rawBody: rawBodyText,
|
|
81
85
|
method: "POST",
|
|
82
86
|
});
|
|
83
87
|
|
|
84
|
-
expect(ctx.rawBody).toBe(
|
|
88
|
+
expect(ctx.rawBody).toBe(rawBodyText);
|
|
85
89
|
});
|
|
86
90
|
|
|
87
91
|
test("Should support Blob body", () => {
|
|
88
92
|
const blob = new Blob(["test content"], { type: "text/plain" });
|
|
93
|
+
const rawBodyText = "test content";
|
|
89
94
|
|
|
90
95
|
const ctx = new RequestInputContext({
|
|
91
96
|
path: "/api/upload",
|
|
92
97
|
pathParams: {},
|
|
93
98
|
searchParams: new URLSearchParams(),
|
|
94
99
|
headers: new Headers(),
|
|
95
|
-
|
|
100
|
+
parsedBody: blob,
|
|
101
|
+
rawBody: rawBodyText,
|
|
96
102
|
method: "POST",
|
|
97
103
|
});
|
|
98
104
|
|
|
99
|
-
expect(ctx.rawBody).toBe(
|
|
105
|
+
expect(ctx.rawBody).toBe(rawBodyText);
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
test("Should create RequestContext with fromRequest static method", async () => {
|
|
@@ -105,10 +111,10 @@ describe("RequestContext", () => {
|
|
|
105
111
|
body: JSON.stringify({ test: "data" }),
|
|
106
112
|
});
|
|
107
113
|
|
|
108
|
-
const bodyData = { test: "data" };
|
|
109
114
|
const url = new URL(request.url);
|
|
110
115
|
const clonedReq = request.clone();
|
|
111
116
|
const body = clonedReq.body instanceof ReadableStream ? await clonedReq.json() : undefined;
|
|
117
|
+
const rawBodyText = JSON.stringify({ test: "data" });
|
|
112
118
|
|
|
113
119
|
const state = new MutableRequestState({
|
|
114
120
|
pathParams: {},
|
|
@@ -123,10 +129,11 @@ describe("RequestContext", () => {
|
|
|
123
129
|
path: "/api/test",
|
|
124
130
|
pathParams: {},
|
|
125
131
|
state,
|
|
132
|
+
rawBody: rawBodyText,
|
|
126
133
|
});
|
|
127
134
|
|
|
128
135
|
expect(ctx.path).toBe("/api/test");
|
|
129
|
-
expect(ctx.rawBody).toEqual(
|
|
136
|
+
expect(ctx.rawBody).toEqual(rawBodyText);
|
|
130
137
|
});
|
|
131
138
|
|
|
132
139
|
test("Should create RequestContext with fromSSRContext static method", () => {
|
|
@@ -139,7 +146,8 @@ describe("RequestContext", () => {
|
|
|
139
146
|
});
|
|
140
147
|
|
|
141
148
|
expect(ctx.path).toBe("/api/ssr");
|
|
142
|
-
|
|
149
|
+
// rawBody is not set in fromSSRContext
|
|
150
|
+
expect(ctx.rawBody).toBeUndefined();
|
|
143
151
|
});
|
|
144
152
|
|
|
145
153
|
describe("Input handling", () => {
|
|
@@ -149,7 +157,7 @@ describe("RequestContext", () => {
|
|
|
149
157
|
pathParams: {},
|
|
150
158
|
searchParams: new URLSearchParams(),
|
|
151
159
|
headers: new Headers(),
|
|
152
|
-
|
|
160
|
+
parsedBody: { test: "data" },
|
|
153
161
|
method: "POST",
|
|
154
162
|
});
|
|
155
163
|
|
|
@@ -162,7 +170,7 @@ describe("RequestContext", () => {
|
|
|
162
170
|
pathParams: {},
|
|
163
171
|
searchParams: new URLSearchParams(),
|
|
164
172
|
headers: new Headers(),
|
|
165
|
-
|
|
173
|
+
parsedBody: { test: "data" },
|
|
166
174
|
inputSchema: validStringSchema,
|
|
167
175
|
method: "POST",
|
|
168
176
|
});
|
|
@@ -178,7 +186,7 @@ describe("RequestContext", () => {
|
|
|
178
186
|
pathParams: {},
|
|
179
187
|
searchParams: new URLSearchParams(),
|
|
180
188
|
headers: new Headers(),
|
|
181
|
-
|
|
189
|
+
parsedBody: "test string",
|
|
182
190
|
inputSchema: validStringSchema,
|
|
183
191
|
method: "POST",
|
|
184
192
|
});
|
|
@@ -193,7 +201,7 @@ describe("RequestContext", () => {
|
|
|
193
201
|
pathParams: {},
|
|
194
202
|
searchParams: new URLSearchParams(),
|
|
195
203
|
headers: new Headers(),
|
|
196
|
-
|
|
204
|
+
parsedBody: 123, // Invalid for string schema
|
|
197
205
|
inputSchema: invalidSchema,
|
|
198
206
|
method: "POST",
|
|
199
207
|
});
|
|
@@ -207,7 +215,7 @@ describe("RequestContext", () => {
|
|
|
207
215
|
pathParams: {},
|
|
208
216
|
searchParams: new URLSearchParams(),
|
|
209
217
|
headers: new Headers(),
|
|
210
|
-
|
|
218
|
+
parsedBody: 123,
|
|
211
219
|
inputSchema: invalidSchema,
|
|
212
220
|
method: "POST",
|
|
213
221
|
});
|
|
@@ -236,13 +244,13 @@ describe("RequestContext", () => {
|
|
|
236
244
|
pathParams: {},
|
|
237
245
|
searchParams: new URLSearchParams(),
|
|
238
246
|
headers: new Headers(),
|
|
239
|
-
|
|
247
|
+
parsedBody: 123,
|
|
240
248
|
inputSchema: invalidSchema,
|
|
241
249
|
shouldValidateInput: false,
|
|
242
250
|
method: "POST",
|
|
243
251
|
});
|
|
244
252
|
|
|
245
|
-
// Should return the
|
|
253
|
+
// Should return the parsed body without validation when validation is disabled
|
|
246
254
|
const result = await ctx.input?.valid();
|
|
247
255
|
expect(result).toBe(123);
|
|
248
256
|
});
|
|
@@ -256,7 +264,7 @@ describe("RequestContext", () => {
|
|
|
256
264
|
pathParams: {},
|
|
257
265
|
searchParams: new URLSearchParams(),
|
|
258
266
|
headers: new Headers(),
|
|
259
|
-
|
|
267
|
+
parsedBody: formData,
|
|
260
268
|
inputSchema: validStringSchema,
|
|
261
269
|
method: "POST",
|
|
262
270
|
});
|
|
@@ -274,7 +282,7 @@ describe("RequestContext", () => {
|
|
|
274
282
|
pathParams: {},
|
|
275
283
|
searchParams: new URLSearchParams(),
|
|
276
284
|
headers: new Headers(),
|
|
277
|
-
|
|
285
|
+
parsedBody: blob,
|
|
278
286
|
inputSchema: validStringSchema,
|
|
279
287
|
method: "POST",
|
|
280
288
|
});
|
|
@@ -290,7 +298,7 @@ describe("RequestContext", () => {
|
|
|
290
298
|
pathParams: {},
|
|
291
299
|
searchParams: new URLSearchParams(),
|
|
292
300
|
headers: new Headers(),
|
|
293
|
-
|
|
301
|
+
parsedBody: null,
|
|
294
302
|
inputSchema: validStringSchema,
|
|
295
303
|
method: "POST",
|
|
296
304
|
});
|
|
@@ -305,7 +313,7 @@ describe("RequestContext", () => {
|
|
|
305
313
|
pathParams: {},
|
|
306
314
|
searchParams: new URLSearchParams(),
|
|
307
315
|
headers: new Headers(),
|
|
308
|
-
|
|
316
|
+
parsedBody: undefined,
|
|
309
317
|
inputSchema: validStringSchema,
|
|
310
318
|
method: "POST",
|
|
311
319
|
});
|
|
@@ -324,7 +332,7 @@ describe("RequestContext", () => {
|
|
|
324
332
|
headers: new Headers(),
|
|
325
333
|
inputSchema: validStringSchema,
|
|
326
334
|
method: "POST",
|
|
327
|
-
|
|
335
|
+
parsedBody: undefined,
|
|
328
336
|
});
|
|
329
337
|
|
|
330
338
|
// We can't directly access the private field, but we can test the behavior
|
|
@@ -340,7 +348,7 @@ describe("RequestContext", () => {
|
|
|
340
348
|
inputSchema: validStringSchema,
|
|
341
349
|
shouldValidateInput: true,
|
|
342
350
|
method: "POST",
|
|
343
|
-
|
|
351
|
+
parsedBody: undefined,
|
|
344
352
|
});
|
|
345
353
|
|
|
346
354
|
expect(ctx.input).toBeDefined();
|
|
@@ -355,7 +363,7 @@ describe("RequestContext", () => {
|
|
|
355
363
|
inputSchema: validStringSchema,
|
|
356
364
|
shouldValidateInput: false,
|
|
357
365
|
method: "POST",
|
|
358
|
-
|
|
366
|
+
parsedBody: undefined,
|
|
359
367
|
});
|
|
360
368
|
|
|
361
369
|
expect(ctx.input).toBeDefined();
|
|
@@ -406,7 +414,7 @@ describe("RequestContext", () => {
|
|
|
406
414
|
searchParams: new URLSearchParams(),
|
|
407
415
|
headers: new Headers(),
|
|
408
416
|
method: "POST",
|
|
409
|
-
|
|
417
|
+
parsedBody: undefined,
|
|
410
418
|
});
|
|
411
419
|
|
|
412
420
|
expect(ctx.pathParams).toEqual({});
|
|
@@ -419,7 +427,7 @@ describe("RequestContext", () => {
|
|
|
419
427
|
searchParams: new URLSearchParams(),
|
|
420
428
|
headers: new Headers(),
|
|
421
429
|
method: "POST",
|
|
422
|
-
|
|
430
|
+
parsedBody: undefined,
|
|
423
431
|
});
|
|
424
432
|
|
|
425
433
|
expect(ctx.query.toString()).toBe("");
|
|
@@ -433,7 +441,7 @@ describe("RequestContext", () => {
|
|
|
433
441
|
searchParams,
|
|
434
442
|
headers: new Headers(),
|
|
435
443
|
method: "POST",
|
|
436
|
-
|
|
444
|
+
parsedBody: undefined,
|
|
437
445
|
});
|
|
438
446
|
|
|
439
447
|
expect(ctx.query.get("key")).toBe("value");
|