Skip to content

Commit ffb642a

Browse files
committed
add FormData support
1 parent 56a9929 commit ffb642a

File tree

7 files changed

+52
-22
lines changed

7 files changed

+52
-22
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ The `createRoute` function is used to define route handlers in a type-safe and s
4646
| pathParams | [ZodType](https://zod.dev) | `(Optional)` Zod schema for validating path parameters. |
4747
| queryParams | [ZodType](https://zod.dev) | `(Optional)` Zod schema for validating query parameters. |
4848
| requestBody | [ZodType](https://zod.dev) | Zod schema for the request body (required for `POST`, `PUT`, `PATCH`). |
49+
| hasFormData | `boolean` | Is the request body a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) |
4950
| action | (source: [ActionSource](#action-source)) => Promise<[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)> | Function handling the request, receiving pathParams, queryParams, and requestBody. |
5051
| responses | Record<`number`, [ResponseDefinition](#response-definition)> | Object defining possible responses, each with a description and optional content schema. |
5152

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@omer-x/next-openapi-route-handler",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "a Next.js plugin to generate OpenAPI documentation from route handlers",
55
"keywords": [
66
"next.js",
@@ -44,8 +44,8 @@
4444
},
4545
"devDependencies": {
4646
"@omer-x/eslint-config": "^1.0.7",
47-
"@omer-x/openapi-types": "^0.1.0",
48-
"@types/node": "^20.14.1",
47+
"@omer-x/openapi-types": "^0.1.1",
48+
"@types/node": "^20.14.2",
4949
"eslint": "^8.57.0",
5050
"ts-unused-exports": "^10.1.0",
5151
"tsup": "^8.1.0",

src/core/body.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@ import { resolveContent } from "./content";
44
import type { RequestBodyObject } from "@omer-x/openapi-types/request-body";
55
import type { ZodType } from "zod";
66

7-
export function resolveRequestBody(source?: ZodType<unknown> | string) {
7+
export function resolveRequestBody(source?: ZodType<unknown> | string, isFormData: boolean = false) {
88
if (!source) return undefined;
99
return {
1010
// description: "", // how to fill this?
1111
required: true,
12-
content: resolveContent(source),
12+
content: resolveContent(source, false, isFormData),
1313
} as RequestBodyObject;
1414
}
1515

16-
export async function parseRequestBody<B>(request: FixedRequest<B>, method: HttpMethod, schema?: ZodType<B> | string) {
16+
export async function parseRequestBody<B>(
17+
request: FixedRequest<B>,
18+
method: HttpMethod,
19+
schema?: ZodType<B> | string,
20+
isFormData: boolean = false
21+
) {
1722
if (!schema || typeof schema === "string") return null;
1823
if (method === "GET") throw new Error("GET routes can't have request body");
24+
if (isFormData) {
25+
const formData = await request.formData();
26+
const body = Array.from(formData.keys()).reduce((collection, key) => ({
27+
...collection, [key]: formData.get(key),
28+
}), {});
29+
return schema.parse(body);
30+
}
1931
return schema.parse(await request.json());
2032
}

src/core/content.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import zodToJsonSchema from "zod-to-json-schema";
22
import type { RequestBodyObject } from "@omer-x/openapi-types/request-body";
33
import type { SchemaObject } from "@omer-x/openapi-types/schema";
4-
import type { ZodType } from "zod";
4+
import type { ZodObject, ZodType } from "zod";
55

66
function resolveSchema(source: ZodType<unknown> | string, isArray: boolean = false): SchemaObject {
77
if (typeof source === "string") {
@@ -11,13 +11,27 @@ function resolveSchema(source: ZodType<unknown> | string, isArray: boolean = fal
1111
}
1212
return refObject;
1313
}
14-
return zodToJsonSchema(isArray ? source.array() : source, { target: "openApi3" }) as SchemaObject;
14+
const schema = zodToJsonSchema(isArray ? source.array() : source, { target: "openApi3" }) as SchemaObject;
15+
if (schema.type === "object" && source.constructor.name === "ZodObject") {
16+
// eslint-disable-next-line @typescript-eslint/ban-types
17+
for (const [propName, prop] of Object.entries((source as ZodObject<{}>).shape)) {
18+
const result = (prop as ZodType).safeParse(new File([], "nothing.txt"));
19+
if (result.success) {
20+
schema.properties[propName] = {
21+
type: "string",
22+
format: "binary",
23+
// contentEncoding: "base64", // swagger-ui-react doesn't support this
24+
};
25+
}
26+
}
27+
}
28+
return schema;
1529
}
1630

17-
export function resolveContent(source?: ZodType<unknown> | string, isArray: boolean = false) {
31+
export function resolveContent(source?: ZodType<unknown> | string, isArray: boolean = false, isFormData: boolean = false) {
1832
if (!source) return undefined;
1933
return {
20-
"application/json": {
34+
[isFormData ? "multipart/form-data" : "application/json"]: {
2135
schema: resolveSchema(source, isArray),
2236
},
2337
} as RequestBodyObject["content"];

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ type ActionSource<PathParams, QueryParams, RequestBody> = {
1717
type RouteWithoutBody = {
1818
method: Extract<HttpMethod, "GET" | "DELETE" | "HEAD">,
1919
requestBody?: null,
20+
hasFormData?: boolean,
2021
};
2122

2223
type RouteWithBody<Body> = {
2324
method: Exclude<HttpMethod, "GET" | "DELETE" | "HEAD">,
2425
requestBody?: ZodType<Body> | string,
26+
hasFormData?: boolean,
2527
};
2628

2729
type RouteOptions<Method, PathParams, QueryParams, RequestBody> = {
@@ -42,7 +44,7 @@ export default function createRoute<M extends HttpMethod, PP, QP, RB>(input: Rou
4244
const { searchParams } = new URL(request.url);
4345
const pathParams = parsePathParams(props.params, input.pathParams) as PP;
4446
const queryParams = parseSearchParams(searchParams, input.queryParams) as QP;
45-
const body = await parseRequestBody(request, input.method, input.requestBody ?? undefined) as RB;
47+
const body = await parseRequestBody(request, input.method, input.requestBody ?? undefined, input.hasFormData) as RB;
4648
return await input.action({ pathParams, queryParams, body });
4749
} catch (error) {
4850
if (error instanceof Error && error.constructor.name === "ZodError") {
@@ -70,7 +72,7 @@ export default function createRoute<M extends HttpMethod, PP, QP, RB>(input: Rou
7072
description: input.description,
7173
tags: input.tags,
7274
parameters: parameters.length ? parameters : undefined,
73-
requestBody: resolveRequestBody(input.requestBody ?? undefined),
75+
requestBody: resolveRequestBody(input.requestBody ?? undefined, input.hasFormData),
7476
responses: responses,
7577
};
7678

src/types/request.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface FixedRequest<RequestBody> {
22
url: string,
33
json: () => Promise<RequestBody>,
4+
formData: () => Promise<FormData>,
45
}

0 commit comments

Comments
 (0)