From 1751340a3dc1caf516012e17160ada0fbe1d39d0 Mon Sep 17 00:00:00 2001 From: Markus Blomqvist Date: Sat, 4 May 2024 12:16:47 +0300 Subject: [PATCH] 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. --- .../src/app-router/route-operation.ts | 59 ++++++++----------- packages/next-rest-framework/src/index.ts | 5 +- .../src/pages-router/api-route-operation.ts | 2 +- packages/next-rest-framework/src/types.ts | 33 ++++++----- .../tests/app-router/route.test.ts | 11 ++-- 5 files changed, 53 insertions(+), 57 deletions(-) diff --git a/packages/next-rest-framework/src/app-router/route-operation.ts b/packages/next-rest-framework/src/app-router/route-operation.ts index 923b768..5c4013f 100644 --- a/packages/next-rest-framework/src/app-router/route-operation.ts +++ b/packages/next-rest-framework/src/app-router/route-operation.ts @@ -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 extends URLSearchParams { + get: (key: K) => string | null; + getAll: (key: K) => string[]; +} + +interface TypedNextURL extends NextURL { + searchParams: TypedSearchParams; +} + +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; - /*! Prevent parsing form data for GET and non-form requests. */ - formData: Method extends 'GET' - ? never - : ContentType extends FormDataContentType - ? () => Promise> - : 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; + /*! Prevent parsing form data for GET and non-form requests. */ + formData: Method extends 'GET' + ? never + : ContentType extends FormDataContentType + ? () => Promise> + : never; + nextUrl: TypedNextURL; +} type TypedHeaders = Modify< Record, @@ -165,11 +160,7 @@ type RouteMiddleware< req: NextRequest, context: { params: BaseParams }, options: InputOptions -) => - | Promise - | TypedResponse - | Promise - | OutputOptions; +) => Promise | TypedResponse | OutputOptions; type TypedRouteHandler< Method extends keyof typeof ValidMethod = keyof typeof ValidMethod, diff --git a/packages/next-rest-framework/src/index.ts b/packages/next-rest-framework/src/index.ts index d2131ba..7a1f7ac 100644 --- a/packages/next-rest-framework/src/index.ts +++ b/packages/next-rest-framework/src/index.ts @@ -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'; diff --git a/packages/next-rest-framework/src/pages-router/api-route-operation.ts b/packages/next-rest-framework/src/pages-router/api-route-operation.ts index 2e6bff3..deff583 100644 --- a/packages/next-rest-framework/src/pages-router/api-route-operation.ts +++ b/packages/next-rest-framework/src/pages-router/api-route-operation.ts @@ -44,7 +44,7 @@ export type TypedNextApiRequest< } >; -type TypedNextApiResponse = Modify< +export type TypedNextApiResponse = Modify< NextApiResponse, { status: (status: Status) => TypedNextApiResponse; diff --git a/packages/next-rest-framework/src/types.ts b/packages/next-rest-framework/src/types.ts index 21ccff8..f4801ad 100644 --- a/packages/next-rest-framework/src/types.ts +++ b/packages/next-rest-framework/src/types.ts @@ -179,21 +179,24 @@ export type FormDataContentType = | 'application/x-www-form-urlencoded' | 'multipart/form-data'; -export type TypedFormData = Modify< - FormData, - { - append: (name: K, value: T[K] | Blob) => void; - delete: (name: K) => void; - get: (name: K) => T[K]; - getAll: (name: K) => Array; - has: (name: K) => boolean; - set: (name: K, value: T[K] | Blob) => void; - forEach: ( - callbackfn: (value: T[K], key: T, parent: TypedFormData) => void, - thisArg?: any - ) => void; - } ->; +export interface TypedFormData extends FormData { + append: (name: K, value: Blob | string) => void; + delete: (name: K) => void; + get: (name: K) => T[K] & FormDataEntryValue; + getAll: ( + name: K + ) => Array & FormDataEntryValue[]; + has: (name: K) => boolean; + set: (name: K, value: Blob | string) => void; + forEach: ( + callbackfn: ( + value: T[K] & FormDataEntryValue, + key: K, + parent: TypedFormData + ) => void, + thisArg?: any + ) => void; +} interface FormDataLikeInput { [Symbol.iterator]: () => IterableIterator<[string, FormDataEntryValue]>; diff --git a/packages/next-rest-framework/tests/app-router/route.test.ts b/packages/next-rest-framework/tests/app-router/route.test.ts index 7af20dd..1ea4f59 100644 --- a/packages/next-rest-framework/tests/app-router/route.test.ts +++ b/packages/next-rest-framework/tests/app-router/route.test.ts @@ -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'; @@ -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'), @@ -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); @@ -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'); @@ -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); @@ -656,7 +655,7 @@ describe('route', () => { }) .handler((_req, _ctx, options) => { console.log('handler'); - return NextResponse.json(options); + return TypedNextResponse.json(options); }) }).GET(req, context);