diff --git a/package.json b/package.json index cd9f425..bb4d809 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "scripts": { "dev": "pnpm --filter example run dev", - "build": "pnpm --filter next-rest-framework run build", + "build": "pnpm --filter next-rest-framework-patch-hugo run build", "test": "pnpm --filter next-rest-framework run test", "test:watch": "pnpm --filter next-rest-framework run test:watch", "format": "prettier --write '**/*.{ts,json}' && eslint --fix --max-warnings=0 --ext=.ts .", diff --git a/packages/next-rest-framework/package.json b/packages/next-rest-framework/package.json index 2e36694..1d579a8 100644 --- a/packages/next-rest-framework/package.json +++ b/packages/next-rest-framework/package.json @@ -1,6 +1,6 @@ { "name": "next-rest-framework", - "version": "6.0.6", + "version": "6.0.7", "description": "Next REST Framework - Type-safe, self-documenting APIs for Next.js", "keywords": [ "nextjs", 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 5c4013f..29271e5 100644 --- a/packages/next-rest-framework/src/app-router/route-operation.ts +++ b/packages/next-rest-framework/src/app-router/route-operation.ts @@ -14,7 +14,8 @@ import { type BaseContentType, type ZodFormSchema, type FormDataContentType, - type ContentTypesThatSupportInputValidation + type ContentTypesThatSupportInputValidation, + type BaseHeaders } from '../types'; import { NextResponse, type NextRequest } from 'next/server'; import { type ZodSchema, type z } from 'zod'; @@ -168,6 +169,7 @@ type TypedRouteHandler< Body = unknown, Query extends BaseQuery = BaseQuery, Params extends BaseParams = BaseParams, + Headers extends BaseHeaders = BaseHeaders, Options extends BaseOptions = BaseOptions, ResponseBody = unknown, Status extends BaseStatus = BaseStatus, @@ -185,7 +187,7 @@ type TypedRouteHandler< | void > = ( req: TypedNextRequest, - context: { params: Params }, + context: { params: Params; headers: Headers }, options: Options ) => Promise | TypedResponse; @@ -193,7 +195,8 @@ interface InputObject< ContentType = BaseContentType, Body = unknown, Query = BaseQuery, - Params = BaseParams + Params = BaseParams, + Headers = BaseHeaders > { contentType?: ContentType; /*! Body schema is supported only for certain content types that support input validation. */ @@ -210,6 +213,7 @@ interface InputObject< params?: ZodSchema; /*! If defined, this will override the params schema for the OpenAPI spec. */ paramsSchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject; + headers?: ZodSchema; } export interface RouteOperationDefinition< @@ -246,7 +250,7 @@ export const routeOperation = ({ middleware1?: RouteMiddleware; middleware2?: RouteMiddleware; middleware3?: RouteMiddleware; - handler?: TypedRouteHandler; + handler?: TypedRouteHandler; }): RouteOperationDefinition => ({ openApiOperation, method, @@ -263,9 +267,10 @@ export const routeOperation = ({ ContentType extends BaseContentType, Body, Query extends BaseQuery, - Params extends BaseParams + Params extends BaseParams, + Headers extends BaseHeaders >( - input: InputObject + input: InputObject ) => ({ outputs: < ResponseBody, @@ -314,6 +319,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options3, ResponseBody, Status, @@ -337,6 +343,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options2, ResponseBody, Status, @@ -359,6 +366,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options1, ResponseBody, Status, @@ -374,6 +382,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, BaseOptions, ResponseBody, Status, @@ -408,6 +417,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options3, ResponseBody, Status, @@ -431,6 +441,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options2 > ) => createOperation({ input, middleware1, middleware2, handler }) @@ -452,6 +463,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options2, ResponseBody, Status, @@ -473,6 +485,7 @@ export const routeOperation = ({ ContentType, Body, Query, + Headers, Params, Options2 > @@ -495,6 +508,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options1, ResponseBody, Status, @@ -510,6 +524,7 @@ export const routeOperation = ({ Body, Query, Params, + Headers, Options1 > ) => createOperation({ input, middleware1, handler }) @@ -565,6 +580,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, Options3, ResponseBody, Status, @@ -587,6 +603,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, Options2, ResponseBody, Status, @@ -602,6 +619,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, Options1, ResponseBody, Status, @@ -617,6 +635,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, BaseOptions, ResponseBody, Status, @@ -641,6 +660,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, Options3 > ) => @@ -653,6 +673,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, Options2 > ) => createOperation({ middleware1, middleware2, handler }) @@ -664,6 +685,7 @@ export const routeOperation = ({ unknown, BaseQuery, BaseParams, + BaseHeaders, Options1 > ) => createOperation({ middleware1, handler }) diff --git a/packages/next-rest-framework/src/app-router/route.ts b/packages/next-rest-framework/src/app-router/route.ts index 8427aa3..47f1c4c 100644 --- a/packages/next-rest-framework/src/app-router/route.ts +++ b/packages/next-rest-framework/src/app-router/route.ts @@ -8,7 +8,8 @@ import { type FormDataContentType, type BaseOptions, type BaseParams, - type OpenApiPathItem + type OpenApiPathItem, + type BaseHeaders } from '../types'; import { type RouteOperationDefinition, @@ -28,7 +29,7 @@ export const route = >( ) => { const handler = async ( _req: NextRequest, - context: { params: Promise } + context: { params: Promise; headers: BaseHeaders } ) => { try { const operation = Object.entries(operations).find( @@ -48,6 +49,7 @@ export const route = >( } ); } + context.headers = Object.fromEntries(_req.headers.entries()); const { input, handler, middleware1, middleware2, middleware3 } = operation; @@ -65,7 +67,11 @@ export const route = >( let middlewareOptions: BaseOptions = {}; if (middleware1) { - const res = await middleware1(reqClone, {...context, params: await context.params}, middlewareOptions); + const res = await middleware1( + reqClone, + { ...context, params: await context.params }, + middlewareOptions + ); const isOptionsResponse = (res: unknown): res is BaseOptions => typeof res === 'object'; @@ -77,7 +83,11 @@ export const route = >( } if (middleware2) { - const res2 = await middleware2(reqClone, {...context, params: await context.params}, middlewareOptions); + const res2 = await middleware2( + reqClone, + { ...context, params: await context.params }, + middlewareOptions + ); if (res2 instanceof Response) { return res2; @@ -88,7 +98,7 @@ export const route = >( if (middleware3) { const res3 = await middleware3( reqClone, - {...context, params: await context.params}, + { ...context, params: await context.params }, middlewareOptions ); @@ -106,7 +116,8 @@ export const route = >( body: bodySchema, query: querySchema, contentType: contentTypeSchema, - params: paramsSchema + params: paramsSchema, + headers: headersSchema } = input; const contentType = reqClone.headers.get('content-type')?.split(';')[0]; @@ -270,11 +281,32 @@ export const route = >( context.params = data; } + + if (headersSchema) { + const { valid, errors, data } = validateSchema({ + schema: headersSchema, + obj: context.headers + }); + + if (!valid) { + return NextResponse.json( + { + message: DEFAULT_ERRORS.invalidHeaders, + errors + }, + { + status: 400 + } + ); + } + + context.headers = data; + } } const res = await handler?.( reqClone as TypedNextRequest, - {...context, params: await context.params}, + { ...context, params: await context.params }, middlewareOptions ); diff --git a/packages/next-rest-framework/src/cli/generate.ts b/packages/next-rest-framework/src/cli/generate.ts old mode 100644 new mode 100755 diff --git a/packages/next-rest-framework/src/cli/index.ts b/packages/next-rest-framework/src/cli/index.ts old mode 100644 new mode 100755 diff --git a/packages/next-rest-framework/src/cli/utils.ts b/packages/next-rest-framework/src/cli/utils.ts old mode 100644 new mode 100755 index db9ac08..3ffb583 --- a/packages/next-rest-framework/src/cli/utils.ts +++ b/packages/next-rest-framework/src/cli/utils.ts @@ -428,7 +428,11 @@ export const generateOpenApiSpec = async ({ }; const components = Object.keys(schemas).length - ? { components: { schemas: sortObjectByKeys(schemas) } } + ? { + components: { + schemas: sortObjectByKeys(schemas) + } + } : {}; const spec: OpenAPIV3_1.Document = merge( diff --git a/packages/next-rest-framework/src/cli/validate.ts b/packages/next-rest-framework/src/cli/validate.ts old mode 100644 new mode 100755 diff --git a/packages/next-rest-framework/src/constants.ts b/packages/next-rest-framework/src/constants.ts index 1a85230..f4fcec3 100644 --- a/packages/next-rest-framework/src/constants.ts +++ b/packages/next-rest-framework/src/constants.ts @@ -10,7 +10,8 @@ export const DEFAULT_ERRORS = { operationNotAllowed: 'Operation not allowed.', invalidRequestBody: 'Invalid request body.', invalidQueryParameters: 'Invalid query parameters.', - invalidPathParameters: 'Invalid path parameters.' + invalidPathParameters: 'Invalid path parameters.', + invalidHeaders: 'Invalid headers.' }; export enum ValidMethod { 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 5c403d9..e12e3b6 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 @@ -15,7 +15,8 @@ import { type ZodFormSchema, type ContentTypesThatSupportInputValidation, type FormDataContentType, - type BaseParams + type BaseParams, + type BaseHeaders } from '../types'; import { type NextApiRequest, type NextApiResponse } from 'next/types'; import { type ZodSchema, type z } from 'zod'; @@ -125,7 +126,8 @@ interface InputObject< ContentType = BaseContentType, Body = unknown, Query = BaseQuery, - Params = BaseParams + Params = BaseParams, + Headers = BaseHeaders > { contentType?: ContentType; /*! @@ -145,6 +147,8 @@ interface InputObject< params?: ZodSchema; /*! If defined, this will override the params schema for the OpenAPI spec. */ paramsSchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject; + + headers?: ZodSchema; } export interface ApiRouteOperationDefinition< diff --git a/packages/next-rest-framework/src/shared/logging.ts b/packages/next-rest-framework/src/shared/logging.ts index a9ebfa2..4537a00 100644 --- a/packages/next-rest-framework/src/shared/logging.ts +++ b/packages/next-rest-framework/src/shared/logging.ts @@ -31,7 +31,11 @@ export const logGenerateErrorForRoute = (path: string, error: unknown) => { Error while importing ${path}, skipping path...`) ); - console.error(chalk.red(error)); + if (error instanceof Error && error.stack) { + console.error(chalk.red(error.stack)); + } else { + console.error(chalk.red(error)); + } console.info( chalk.yellow( diff --git a/packages/next-rest-framework/src/shared/paths.ts b/packages/next-rest-framework/src/shared/paths.ts index 232f03f..faef5c1 100644 --- a/packages/next-rest-framework/src/shared/paths.ts +++ b/packages/next-rest-framework/src/shared/paths.ts @@ -282,6 +282,34 @@ export const getPathsFromRoute = ({ ]; } + if (input?.headers) { + const schema = + input.querySchema ?? + getJsonSchema({ + schema: input.headers, + operationId, + type: 'input-header' + }).properties ?? + {}; + + generatedOperationObject.parameters = [ + ...(generatedOperationObject.parameters ?? []), + ...Object.entries(schema).map(([name, schema]) => { + const _schema = (input.headers as ZodObject).shape[ + name + ] as ZodSchema; + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + name, + in: 'header', + required: !_schema.isOptional(), + schema + } as OpenAPIV3_1.ParameterObject; + }) + ]; + } + paths[route] = { ...paths[route], [method]: merge(generatedOperationObject, openApiOperation) diff --git a/packages/next-rest-framework/src/shared/schemas.ts b/packages/next-rest-framework/src/shared/schemas.ts index c521fc8..fec1f03 100644 --- a/packages/next-rest-framework/src/shared/schemas.ts +++ b/packages/next-rest-framework/src/shared/schemas.ts @@ -41,7 +41,12 @@ export const validateSchema = ({ throw Error('Invalid schema.'); }; -type SchemaType = 'input-params' | 'input-query' | 'input-body' | 'output-body'; +type SchemaType = + | 'input-params' + | 'input-query' + | 'input-body' + | 'input-header' + | 'output-body'; export const getJsonSchema = ({ schema, @@ -62,7 +67,9 @@ export const getJsonSchema = ({ const solutions: Record = { 'input-params': 'paramsSchema', 'input-query': 'querySchema', + 'input-header': 'headersSchema', 'input-body': 'bodySchema', + 'output-body': 'bodySchema' }; diff --git a/packages/next-rest-framework/src/types.ts b/packages/next-rest-framework/src/types.ts index f4801ad..6f69954 100644 --- a/packages/next-rest-framework/src/types.ts +++ b/packages/next-rest-framework/src/types.ts @@ -78,6 +78,7 @@ export interface NextRestFrameworkConfig { export type BaseStatus = number; export type BaseQuery = Record; export type BaseParams = Record; +export type BaseHeaders = Record; export type BaseOptions = Record; export interface OutputObject< diff --git a/route-operation.ts b/route-operation.ts new file mode 100644 index 0000000..8ccd94a --- /dev/null +++ b/route-operation.ts @@ -0,0 +1,27 @@ +const createOperation = < + TOptions extends BaseOptions = BaseOptions, + TContentType extends BaseContentType = AnyContentTypeWithAutocompleteForMostCommonOnes +>({ + input, + outputs, + middleware1, + middleware2, + middleware3, + handler +}: { + input?: InputObject; + outputs?: readonly OutputObject[]; + middleware1?: RouteMiddleware; + middleware2?: RouteMiddleware; + middleware3?: RouteMiddleware; + handler?: TypedRouteHandler>; +}): RouteOperationDefinition => ({ + openApiOperation, + method, + input, + outputs, + middleware1, + middleware2, + middleware3, + handler +}); \ No newline at end of file