Skip to content

Commit

Permalink
Fix async middleware typings (#165)
Browse files Browse the repository at this point in the history
* Fix example app SwaggerUI link

* Fix async middleware typings

This fixes the TS error when using an async
middleware by fixing the return types of the
middleware function. This also exports the typings
of `TypedNextRerquest`, `TypedNextApiRequest` and
`TypedNextApiResponse` so that they can be used for
custom abstractions.
  • Loading branch information
blomqma authored May 4, 2024
1 parent 65d1e4c commit 6f9cb4b
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 58 deletions.
2 changes: 1 addition & 1 deletion apps/example/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function Page() {
</a>
</li>
<li>
<a className="link link-primary" href="/api/v2">
<a className="link link-primary" href="/api/v1">
SwaggerUI
</a>
</li>
Expand Down
59 changes: 25 additions & 34 deletions packages/next-rest-framework/src/app-router/route-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,36 @@ import { NextResponse, type NextRequest } from 'next/server';
import { type ZodSchema, type z } from 'zod';
import { type ValidMethod } from '../constants';
import { type I18NConfig } from 'next/dist/server/config-shared';
import { type ResponseCookies } from 'next/dist/server/web/spec-extension/cookies';
import { type NextURL } from 'next/dist/server/web/next-url';
import { type OpenAPIV3_1 } from 'openapi-types';
import { type ResponseCookies } from 'next/dist/compiled/@edge-runtime/cookies';

export type TypedNextRequest<
Method = keyof typeof ValidMethod,
interface TypedSearchParams<Query = BaseQuery> extends URLSearchParams {
get: <K extends keyof Query & string>(key: K) => string | null;
getAll: <K extends keyof Query & string>(key: K) => string[];
}

interface TypedNextURL<Query = BaseQuery> extends NextURL {
searchParams: TypedSearchParams<Query>;
}

export interface TypedNextRequest<
Method extends string = keyof typeof ValidMethod,
ContentType = BaseContentType,
Body = unknown,
Query = BaseQuery
> = Modify<
NextRequest,
{
method: Method;
/*! Prevent parsing JSON body for GET requests. Form requests return parsed form data as JSON when the form schema is defined. */
json: Method extends 'GET' ? never : () => Promise<Body>;
/*! Prevent parsing form data for GET and non-form requests. */
formData: Method extends 'GET'
? never
: ContentType extends FormDataContentType
? () => Promise<TypedFormData<Body>>
: never;
nextUrl: Modify<
NextURL,
{
searchParams: Modify<
URLSearchParams,
{
get: (key: keyof Query) => string | null;
getAll: (key: keyof Query) => string[];
}
>;
}
>;
}
>;
> extends NextRequest {
method: Method;
/*! Prevent parsing JSON body for GET requests. Form requests return parsed form data as JSON when the form schema is defined. */
json: Method extends 'GET' ? never : () => Promise<Body>;
/*! Prevent parsing form data for GET and non-form requests. */
formData: Method extends 'GET'
? never
: ContentType extends FormDataContentType
? () => Promise<TypedFormData<Body>>
: never;
nextUrl: TypedNextURL<Query>;
}

type TypedHeaders<ContentType extends BaseContentType> = Modify<
Record<string, string>,
Expand Down Expand Up @@ -165,11 +160,7 @@ type RouteMiddleware<
req: NextRequest,
context: { params: BaseParams },
options: InputOptions
) =>
| Promise<TypedResponse>
| TypedResponse
| Promise<OutputOptions>
| OutputOptions;
) => Promise<TypedResponse | OutputOptions> | TypedResponse | OutputOptions;

type TypedRouteHandler<
Method extends keyof typeof ValidMethod = keyof typeof ValidMethod,
Expand Down
5 changes: 4 additions & 1 deletion packages/next-rest-framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ export {
docsApiRoute,
apiRoute,
apiRouteOperation,
rpcApiRoute
rpcApiRoute,
type TypedNextApiRequest,
type TypedNextApiResponse
} from './pages-router';
export {
docsRoute,
route,
routeOperation,
rpcRoute,
type TypedNextRequest,
TypedNextResponse
} from './app-router';
export { rpcOperation } from './shared';
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type TypedNextApiRequest<
}
>;

type TypedNextApiResponse<Body, Status, ContentType> = Modify<
export type TypedNextApiResponse<Body, Status, ContentType> = Modify<
NextApiResponse<Body>,
{
status: (status: Status) => TypedNextApiResponse<Body, Status, ContentType>;
Expand Down
33 changes: 18 additions & 15 deletions packages/next-rest-framework/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,24 @@ export type FormDataContentType =
| 'application/x-www-form-urlencoded'
| 'multipart/form-data';

export type TypedFormData<T> = Modify<
FormData,
{
append: <K extends keyof T>(name: K, value: T[K] | Blob) => void;
delete: <K extends keyof T>(name: K) => void;
get: <K extends keyof T>(name: K) => T[K];
getAll: <K extends keyof T>(name: K) => Array<T[K]>;
has: <K extends keyof T>(name: K) => boolean;
set: <K extends keyof T>(name: K, value: T[K] | Blob) => void;
forEach: <K extends keyof T>(
callbackfn: (value: T[K], key: T, parent: TypedFormData<T>) => void,
thisArg?: any
) => void;
}
>;
export interface TypedFormData<T> extends FormData {
append: <K extends keyof T>(name: K, value: Blob | string) => void;
delete: <K extends keyof T & string>(name: K) => void;
get: <K extends keyof T & string>(name: K) => T[K] & FormDataEntryValue;
getAll: <K extends keyof T & string>(
name: K
) => Array<T[K]> & FormDataEntryValue[];
has: <K extends keyof T & string>(name: K) => boolean;
set: <K extends keyof T>(name: K, value: Blob | string) => void;
forEach: <K extends keyof T & string>(
callbackfn: (
value: T[K] & FormDataEntryValue,
key: K,
parent: TypedFormData<T>
) => void,
thisArg?: any
) => void;
}

interface FormDataLikeInput {
[Symbol.iterator]: () => IterableIterator<[string, FormDataEntryValue]>;
Expand Down
11 changes: 5 additions & 6 deletions packages/next-rest-framework/tests/app-router/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { z } from 'zod';
import { TypedNextResponse, route, routeOperation } from '../../src/app-router';
import { DEFAULT_ERRORS, ValidMethod } from '../../src/constants';
import { createMockRouteRequest } from '../utils';
import { NextResponse } from 'next/server';
import { validateSchema } from '../../src/shared';
import { zfd } from 'zod-form-data';

Expand All @@ -25,7 +24,7 @@ describe('route', () => {
body: z.array(z.string())
}
])
.handler(() => NextResponse.json(data));
.handler(() => TypedNextResponse.json(data));

const res = await route({
testGet: getOperation('GET'),
Expand Down Expand Up @@ -353,7 +352,7 @@ describe('route', () => {
])
.handler(async (req) => {
const { foo } = await req.json();
return NextResponse.json({ foo });
return TypedNextResponse.json({ foo });
})
}).POST(req, context);

Expand Down Expand Up @@ -542,7 +541,7 @@ describe('route', () => {
const res = await route({
test: routeOperation({ method: 'GET' })
.middleware(() => {
return NextResponse.json({ foo: 'bar' }, { status: 200 });
return TypedNextResponse.json({ foo: 'bar' }, { status: 200 });
})
.handler(() => {
console.log('foo');
Expand Down Expand Up @@ -591,7 +590,7 @@ describe('route', () => {
console.log({ options: true, ...options });
console.log({ 'x-foo': req.headers.get('x-foo') });
console.log({ 'x-bar': req.headers.get('x-bar') });
return NextResponse.json(options);
return TypedNextResponse.json(options);
})
}).POST(req, context);

Expand Down Expand Up @@ -656,7 +655,7 @@ describe('route', () => {
})
.handler((_req, _ctx, options) => {
console.log('handler');
return NextResponse.json(options);
return TypedNextResponse.json(options);
})
}).GET(req, context);

Expand Down

0 comments on commit 6f9cb4b

Please sign in to comment.