Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
blomqma committed Mar 22, 2024
1 parent 228e13a commit d2860f2
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 67 deletions.
20 changes: 12 additions & 8 deletions apps/example/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { z } from 'zod';
export const getTodos = rpcOperation()
.outputs([
{
body: z.array(todoSchema)
body: z.array(todoSchema),
contentType: 'application/json'
}
])
.handler(() => {
Expand All @@ -30,10 +31,12 @@ export const getTodoById = rpcOperation()
{
body: z.object({
error: z.string()
})
}),
contentType: 'application/json'
},
{
body: todoSchema
body: todoSchema,
contentType: 'application/json'
}
])
.handler((id) => {
Expand All @@ -53,7 +56,7 @@ export const createTodo = rpcOperation()
name: z.string()
})
})
.outputs([{ body: todoSchema }])
.outputs([{ body: todoSchema, contentType: 'application/json' }])
.handler(async ({ name }) => {
const todo = { id: 4, name, completed: false };
return todo;
Expand All @@ -65,8 +68,8 @@ export const deleteTodo = rpcOperation()
body: z.string()
})
.outputs([
{ body: z.object({ error: z.string() }) },
{ body: z.object({ message: z.string() }) }
{ body: z.object({ error: z.string() }), contentType: 'application/json' },
{ body: z.object({ message: z.string() }), contentType: 'application/json' }
])
.handler((id) => {
const todo = MOCK_TODOS.find((t) => t.id === Number(id));
Expand All @@ -85,7 +88,7 @@ export const formDataUrlEncoded = rpcOperation()
contentType: 'application/x-www-form-urlencoded',
body: formSchema // A zod-form-data schema is required.
})
.outputs([{ body: formSchema }])
.outputs([{ body: formSchema, contentType: 'application/json' }])
.handler((formData) => {
return {
text: formData.get('text')
Expand All @@ -104,7 +107,8 @@ export const formDataMultipart = rpcOperation()
bodySchema: {
type: 'string',
format: 'binary'
}
},
contentType: 'application/json'
}
])
.handler((formData) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/example/src/app/api/v2/form-data/multipart/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { multipartFormSchema } from '@/utils';
import { z } from 'zod';

export const runtime = 'edge';

export const { POST } = route({
multipartFormData: routeOperation({
method: 'POST'
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const formSchema = zfd.formData({

export const multipartFormSchema = zfd.formData({
text: zfd.text(),
file: zfd.file()
file: zfd.file() // In development with Edge runtime this won't work: https://github.com/vercel/next.js/issues/38184
});

export type Todo = z.infer<typeof todoSchema>;
Expand Down
5 changes: 3 additions & 2 deletions packages/next-rest-framework/src/app-router/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
const { input, handler, middleware1, middleware2, middleware3 } =
operation;

let reqClone = new NextRequest(req.clone());
let reqClone = req.clone() as NextRequest;

let middlewareOptions: BaseOptions = {};

if (middleware1) {
Expand Down Expand Up @@ -149,7 +150,7 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
FORM_DATA_CONTENT_TYPES.includes(contentType as FormDataContentType)
) {
try {
const formData = await reqClone.clone().formData();
const formData = await req.clone().formData();

const { valid, errors, data } = validateSchema({
schema: bodySchema,
Expand Down
21 changes: 20 additions & 1 deletion packages/next-rest-framework/src/app-router/rpc-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,26 @@ export const rpcRoute = <
);
}

return NextResponse.json(res, { status: 200 });
const parseRes = (res: unknown): BodyInit => {
if (
res instanceof ReadableStream ||
res instanceof ArrayBuffer ||
res instanceof Blob ||
res instanceof FormData ||
res instanceof URLSearchParams
) {
return res;
}

return JSON.stringify(res);
};

return new NextResponse(parseRes(res), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
logNextRestFrameworkError(error);

Expand Down
10 changes: 7 additions & 3 deletions packages/next-rest-framework/src/pages-router/api-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
import {
validateSchema,
logNextRestFrameworkError,
logPagesEdgeRuntimeErrorForRoute,
parseMultiPartFormData
logPagesEdgeRuntimeErrorForRoute
} from '../shared';
import { type NextApiRequest, type NextApiResponse } from 'next/types';
import {
Expand Down Expand Up @@ -139,8 +138,13 @@ export const apiRoute = <T extends Record<string, ApiRouteOperationDefinition>>(
) {
if (
contentType === 'multipart/form-data' &&
!(req.body instanceof FormData)
!(req.body instanceof FormData) &&
typeof EdgeRuntime !== 'string'
) {
const { parseMultiPartFormData } = await import(
'../shared/form-data'
);

// Parse multipart/form-data into a FormData object.
try {
req.body = await parseMultiPartFormData(req);
Expand Down
10 changes: 7 additions & 3 deletions packages/next-rest-framework/src/pages-router/rpc-api-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
validateSchema,
logNextRestFrameworkError,
type RpcOperationDefinition,
logPagesEdgeRuntimeErrorForRoute,
parseMultiPartFormData
logPagesEdgeRuntimeErrorForRoute
} from '../shared';
import { type NextApiRequest, type NextApiResponse } from 'next/types';
import {
Expand Down Expand Up @@ -108,8 +107,13 @@ export const rpcApiRoute = <
) {
if (
contentType === 'multipart/form-data' &&
!(req.body instanceof FormData)
!(req.body instanceof FormData) &&
typeof EdgeRuntime !== 'string'
) {
const { parseMultiPartFormData } = await import(
'../shared/form-data'
);

// Parse multipart/form-data into a FormData object.
try {
req.body = await parseMultiPartFormData(req);
Expand Down
39 changes: 39 additions & 0 deletions packages/next-rest-framework/src/shared/form-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type NextApiRequest } from 'next/types';
import { Formidable } from 'formidable';
import { readFileSync } from 'fs';

export const parseMultiPartFormData = async (req: NextApiRequest) =>
await new Promise<FormData>((resolve, reject) => {
const form = new Formidable();

form.parse(req, (err, fields, files) => {
if (err) {
reject(err);
return;
}

const formData = new FormData();

Object.entries(fields).forEach(([key, value]) => {
if (value?.[0]) {
formData.append(key, value[0]);
}
});

Object.entries(files).forEach(([key, fileArray]) => {
if (fileArray && fileArray.length > 0) {
fileArray.forEach((file) => {
const fileContent = readFileSync(file.filepath);

const blob = new Blob([fileContent], {
type: file.mimetype ?? ''
});

formData.append(key, blob, file.originalFilename ?? '');
});
}
});

resolve(formData);
});
});
4 changes: 2 additions & 2 deletions packages/next-rest-framework/src/shared/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export const getPathsFromRpcRoute = ({
}

generatedOperationObject.responses = outputs?.reduce(
(obj, { body, bodySchema, name }, i) => {
(obj, { body, bodySchema, contentType, name }, i) => {
const key =
name ??
`${capitalizeFirstLetter(operationId)}ResponseBody${
Expand Down Expand Up @@ -366,7 +366,7 @@ export const getPathsFromRpcRoute = ({
200: {
description: key,
content: {
'application/json': {
[contentType]: {
schema: {
$ref: ref
}
Expand Down
1 change: 1 addition & 0 deletions packages/next-rest-framework/src/shared/rpc-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface OutputObject {
body: ZodSchema;
/*! If defined, this will override the body schema for the OpenAPI spec. */
bodySchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
contentType: BaseContentType;
name?: string /*! A custom name for the response, used for the generated component name in the OpenAPI spec. */;
}

Expand Down
39 changes: 0 additions & 39 deletions packages/next-rest-framework/src/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,7 @@
import { type NextApiRequest } from 'next/types';
import { ValidMethod } from '../constants';
import { Formidable } from 'formidable';
import { readFileSync } from 'fs';

export const isValidMethod = (x: unknown): x is ValidMethod =>
Object.values(ValidMethod).includes(x as ValidMethod);

export const capitalizeFirstLetter = (str: string) =>
str[0]?.toUpperCase() + str.slice(1);

export const parseMultiPartFormData = async (req: NextApiRequest) =>
await new Promise<FormData>((resolve, reject) => {
const form = new Formidable();

form.parse(req, (err, fields, files) => {
if (err) {
reject(err);
return;
}

const formData = new FormData();

Object.entries(fields).forEach(([key, value]) => {
if (value?.[0]) {
formData.append(key, value[0]);
}
});

Object.entries(files).forEach(([key, fileArray]) => {
if (fileArray && fileArray.length > 0) {
fileArray.forEach((file) => {
const fileContent = readFileSync(file.filepath);

const blob = new Blob([fileContent], {
type: file.mimetype ?? ''
});

formData.append(key, blob, file.originalFilename ?? '');
});
}
});

resolve(formData);
});
});
13 changes: 9 additions & 4 deletions packages/next-rest-framework/tests/app-router/rpc-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe('rpcRoute', () => {
const data = ['All good!'];

const operation = rpcOperation()
.outputs([{ body: z.array(z.string()) }])
.outputs([
{ body: z.array(z.string()), contentType: 'application/json' }
])
.handler(() => data);

const res = await rpcRoute({
Expand Down Expand Up @@ -167,7 +169,8 @@ describe('rpcRoute', () => {
{
body: z.object({
foo: z.string()
})
}),
contentType: 'application/json'
}
])
.handler(({ foo }) => ({ foo }))
Expand Down Expand Up @@ -204,7 +207,8 @@ describe('rpcRoute', () => {
})
.outputs([
{
body: schema
body: schema,
contentType: 'application/json'
}
])
.handler((formData) => {
Expand Down Expand Up @@ -250,7 +254,8 @@ describe('rpcRoute', () => {
})
.outputs([
{
body: schema
body: schema,
contentType: 'application/json'
}
])
.handler(async (formData) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe('rpcApiRoute', () => {
const data = ['All good!'];

const operation = rpcOperation()
.outputs([{ body: z.array(z.string()) }])
.outputs([
{ body: z.array(z.string()), contentType: 'application/json' }
])
.handler(() => data);

await rpcApiRoute({
Expand Down Expand Up @@ -166,7 +168,8 @@ describe('rpcApiRoute', () => {
{
body: z.object({
foo: z.string()
})
}),
contentType: 'application/json'
}
])
.handler(({ foo }) => ({ foo }))
Expand Down Expand Up @@ -203,7 +206,8 @@ describe('rpcApiRoute', () => {
})
.outputs([
{
body: schema
body: schema,
contentType: 'application/json'
}
])
.handler((formData) => {
Expand Down Expand Up @@ -252,7 +256,8 @@ describe('rpcApiRoute', () => {
})
.outputs([
{
body: schema
body: schema,
contentType: 'application/json'
}
])
.handler(async (formData) => ({
Expand Down

0 comments on commit d2860f2

Please sign in to comment.