From fc20f54afcec923348ed40efc322d179a77343a6 Mon Sep 17 00:00:00 2001 From: Itelo Filho Date: Wed, 12 Jul 2023 17:53:16 -0300 Subject: [PATCH 1/3] edge initial commit --- .../generate-openapi/index.ts | 0 .../generate-route-types/index.ts | 0 .../default-map-file-path-to-http-route.ts | 0 .../lib/parse-routes-in-package.ts | 0 packages/nextlove/package.json | 1 + packages/nextlove/src/index.ts | 6 +- packages/nextlove/src/types/edge.ts | 156 +++++++++ packages/nextlove/src/with-route-spec/edge.ts | 81 +++++ .../nextlove/src/with-route-spec/index.ts | 2 - .../middlewares/with-validation-edge.ts | 190 +++++++++++ .../middlewares/with-validation.ts | 99 +----- .../src/with-route-spec/middlewares/zod.ts | 101 ++++++ .../src/with-route-spec/response-edge.ts | 22 ++ .../src/with-route-spec/wrappers-edge.ts | 320 ++++++++++++++++++ packages/nextlove/tsconfig.json | 2 +- 15 files changed, 877 insertions(+), 103 deletions(-) rename packages/nextlove/{src => old-src}/generate-openapi/index.ts (100%) rename packages/nextlove/{src => old-src}/generate-route-types/index.ts (100%) rename packages/nextlove/{src => old-src}/lib/default-map-file-path-to-http-route.ts (100%) rename packages/nextlove/{src => old-src}/lib/parse-routes-in-package.ts (100%) create mode 100644 packages/nextlove/src/types/edge.ts create mode 100644 packages/nextlove/src/with-route-spec/edge.ts create mode 100644 packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts create mode 100644 packages/nextlove/src/with-route-spec/middlewares/zod.ts create mode 100644 packages/nextlove/src/with-route-spec/response-edge.ts create mode 100644 packages/nextlove/src/with-route-spec/wrappers-edge.ts diff --git a/packages/nextlove/src/generate-openapi/index.ts b/packages/nextlove/old-src/generate-openapi/index.ts similarity index 100% rename from packages/nextlove/src/generate-openapi/index.ts rename to packages/nextlove/old-src/generate-openapi/index.ts diff --git a/packages/nextlove/src/generate-route-types/index.ts b/packages/nextlove/old-src/generate-route-types/index.ts similarity index 100% rename from packages/nextlove/src/generate-route-types/index.ts rename to packages/nextlove/old-src/generate-route-types/index.ts diff --git a/packages/nextlove/src/lib/default-map-file-path-to-http-route.ts b/packages/nextlove/old-src/lib/default-map-file-path-to-http-route.ts similarity index 100% rename from packages/nextlove/src/lib/default-map-file-path-to-http-route.ts rename to packages/nextlove/old-src/lib/default-map-file-path-to-http-route.ts diff --git a/packages/nextlove/src/lib/parse-routes-in-package.ts b/packages/nextlove/old-src/lib/parse-routes-in-package.ts similarity index 100% rename from packages/nextlove/src/lib/parse-routes-in-package.ts rename to packages/nextlove/old-src/lib/parse-routes-in-package.ts diff --git a/packages/nextlove/package.json b/packages/nextlove/package.json index 10c75c07c..07074af44 100644 --- a/packages/nextlove/package.json +++ b/packages/nextlove/package.json @@ -18,6 +18,7 @@ "scripts": { "test": "npm run typecheck", "typecheck": "tsc --noEmit", + "dev": "tsup --dts --sourcemap inline src --watch", "build": "tsup --dts --sourcemap inline src", "yalc": "npm run build && yalc push" }, diff --git a/packages/nextlove/src/index.ts b/packages/nextlove/src/index.ts index 73ee6480a..d36d27626 100644 --- a/packages/nextlove/src/index.ts +++ b/packages/nextlove/src/index.ts @@ -1,5 +1,7 @@ export * from "nextjs-exception-middleware" export * from "./with-route-spec" -export { wrappers } from "nextjs-middleware-wrappers" +export * from "./with-route-spec/edge" +// export { wrappers } from "nextjs-middleware-wrappers" export * from "./types" -export * from "./generate-openapi" +export * from "./types/edge" +// export * from "./generate-openapi" diff --git a/packages/nextlove/src/types/edge.ts b/packages/nextlove/src/types/edge.ts new file mode 100644 index 000000000..06f18c8dc --- /dev/null +++ b/packages/nextlove/src/types/edge.ts @@ -0,0 +1,156 @@ +import { NextApiResponse } from "next" +import { MiddlewareEdge as WrapperMiddlewareEdge } from "../with-route-spec/wrappers-edge" +import { z } from "zod" +import { HTTPMethods } from "../with-route-spec/middlewares/with-methods" +import { SecuritySchemeObject, SecurityRequirementObject } from "openapi3-ts" +import { NextloveRequest } from "../with-route-spec/response-edge" +import { NextResponse } from "next/server" + +export type MiddlewareEdge = WrapperMiddlewareEdge & { + /** + * @deprecated moved to setupParams + */ + securitySchema?: SecuritySchemeObject + securityObjects?: SecurityRequirementObject[] +} + +type ParamDef = z.ZodTypeAny | z.ZodEffects + +export interface RouteSpecEdge< + Auth extends string = string, + Methods extends HTTPMethods[] = any, + JsonBody extends ParamDef = z.ZodObject, + QueryParams extends ParamDef = z.ZodObject, + CommonParams extends ParamDef = z.ZodObject, + Middlewares extends readonly MiddlewareEdge[] = any[], + JsonResponse extends ParamDef = z.ZodObject, + FormData extends ParamDef = z.ZodTypeAny +> { + methods: Methods + auth: Auth + jsonBody?: JsonBody + queryParams?: QueryParams + commonParams?: CommonParams + middlewares?: Middlewares + jsonResponse?: JsonResponse + formData?: FormData +} + +export type MiddlewareEdgeChainOutput< + MWChain extends readonly MiddlewareEdge[] +> = MWChain extends readonly [] + ? {} + : MWChain extends readonly [infer First, ...infer Rest] + ? First extends MiddlewareEdge + ? T & + (Rest extends readonly MiddlewareEdge[] + ? MiddlewareEdgeChainOutput + : never) + : never + : never + +export type AuthMiddlewaresEdge = { + [auth_type: string]: MiddlewareEdge +} + +export interface SetupParamsEdge< + AuthMW extends AuthMiddlewaresEdge = AuthMiddlewaresEdge, + GlobalMW extends MiddlewareEdge[] = any[] +> { + authMiddlewareMap: AuthMW + globalMiddlewares: GlobalMW + exceptionHandlingMiddleware?: ((next: Function) => Function) | null + + // These improve OpenAPI generation + apiName: string + productionServerUrl: string + + addOkStatus?: boolean + + shouldValidateResponses?: boolean + shouldValidateGetRequestBody?: boolean + securitySchemas?: Record +} + +const defaultMiddlewareMap = { + none: (next) => next, +} as const + +type Send = (body: T) => void +type NextApiResponseWithoutJsonAndStatusMethods = Omit< + NextApiResponse, + "json" | "status" +> + +type SuccessfulNextApiResponseMethods = { + status: ( + statusCode: 200 | 201 + ) => NextApiResponseWithoutJsonAndStatusMethods & { + json: Send + } + json: Send +} + +type ErrorNextApiResponseMethods = { + status: (statusCode: number) => NextApiResponseWithoutJsonAndStatusMethods & { + json: Send + } + json: Send +} + +export type RouteEdgeFunction< + SP extends SetupParamsEdge, + RS extends RouteSpecEdge +> = ( + req: (SP["authMiddlewareMap"] & + typeof defaultMiddlewareMap)[RS["auth"]] extends MiddlewareEdge< + infer AuthMWOut, + any + > + ? Omit & + AuthMWOut & + MiddlewareEdgeChainOutput< + RS["middlewares"] extends readonly MiddlewareEdge[] + ? [...SP["globalMiddlewares"], ...RS["middlewares"]] + : SP["globalMiddlewares"] + > & { + edgeBody: RS["formData"] extends z.ZodTypeAny + ? z.infer + : RS["jsonBody"] extends z.ZodTypeAny + ? z.infer + : {} + edgeQuery: RS["queryParams"] extends z.ZodTypeAny + ? z.infer + : {} + commonParams: RS["commonParams"] extends z.ZodTypeAny + ? z.infer + : {} + } + : `unknown auth type: ${RS["auth"]}. You should configure this auth type in your auth_middlewares w/ createWithRouteSpec, or maybe you need to add "as const" to your route spec definition.`, + // res: NextApiResponseWithoutJsonAndStatusMethods & + // SuccessfulNextApiResponseMethods< + // RS["jsonResponse"] extends z.ZodTypeAny + // ? z.infer + // : any + // > & + // ErrorNextApiResponseMethods +) => NextResponse | Promise + +export type CreateWithRouteSpecEdgeFunction = < + SP extends SetupParamsEdge +>( + setupParams: SP +) => < + RS extends RouteSpecEdge< + string, + any, + any, + any, + any, + any, + z.ZodObject, + any + > +>( + route_spec: RS +) => (next: RouteEdgeFunction) => any diff --git a/packages/nextlove/src/with-route-spec/edge.ts b/packages/nextlove/src/with-route-spec/edge.ts new file mode 100644 index 000000000..e33e7fbdd --- /dev/null +++ b/packages/nextlove/src/with-route-spec/edge.ts @@ -0,0 +1,81 @@ +import type { NextApiResponse, NextApiRequest } from "next" +import { NextRequest, NextResponse } from "next/server" +import { wrappersEdge } from "./wrappers-edge" +import { withValidationEdge } from "./middlewares/with-validation-edge" +import { NextloveRequest, getResponseEdge } from "./response-edge" +import { CreateWithRouteSpecEdgeFunction, RouteSpecEdge } from "../types/edge" +import { withExceptionHandling } from "nextjs-exception-middleware" + +export const createWithRouteSpecEdge: CreateWithRouteSpecEdgeFunction = (( + setupParams +) => { + const { + authMiddlewareMap = {}, + globalMiddlewares = [], + shouldValidateResponses, + shouldValidateGetRequestBody = true, + // exceptionHandlingMiddleware = withExceptionHandling({ + // addOkStatus: setupParams.addOkStatus, + // exceptionHandlingOptions: { + // getErrorContext: (req, error) => { + // if (process.env.NODE_ENV === "production") { + // return {} + // } + + // return error + // }, + // }, + // }) as any, + } = setupParams + + const withRouteSpec = (spec: RouteSpecEdge) => { + const createRouteExport = (userDefinedRouteFn: (req: NextloveRequest) => any) => { + const rootRequestHandler = async ( + req: NextloveRequest, + ) => { + req.responseEdge = getResponseEdge() + + authMiddlewareMap["none"] = (next) => next; + + const auth_middleware = authMiddlewareMap[spec.auth] + if (!auth_middleware) throw new Error(`Unknown auth type: ${spec.auth}`) + + // return userDefinedRouteFn(req) + return wrappersEdge( + // ...((exceptionHandlingMiddleware + // ? [exceptionHandlingMiddleware] + // : []) as [any]), + ...((globalMiddlewares || []) as []), + auth_middleware, + ...((spec.middlewares || []) as []), + // withMethods(spec.methods), + // @ts-ignore + withValidationEdge({ + jsonBody: spec.jsonBody, + queryParams: spec.queryParams, + commonParams: spec.commonParams, + formData: spec.formData, + jsonResponse: spec.jsonResponse, + shouldValidateResponses, + shouldValidateGetRequestBody, + }), + userDefinedRouteFn + )(req) + } + + rootRequestHandler._setupParams = setupParams + rootRequestHandler._routeSpec = spec + + return rootRequestHandler + } + + createRouteExport._setupParams = setupParams + createRouteExport._routeSpec = spec + + return createRouteExport + } + + withRouteSpec._setupParams = setupParams + + return withRouteSpec +}) as any diff --git a/packages/nextlove/src/with-route-spec/index.ts b/packages/nextlove/src/with-route-spec/index.ts index b9533bda5..d5ae25d78 100644 --- a/packages/nextlove/src/with-route-spec/index.ts +++ b/packages/nextlove/src/with-route-spec/index.ts @@ -49,7 +49,6 @@ export const createWithRouteSpec: CreateWithRouteSpecFunction = (( authMiddlewareMap = {}, globalMiddlewares = [], shouldValidateResponses, - shouldValidateGetRequestBody = true, exceptionHandlingMiddleware = withExceptionHandling({ addOkStatus: setupParams.addOkStatus, exceptionHandlingOptions: { @@ -90,7 +89,6 @@ export const createWithRouteSpec: CreateWithRouteSpecFunction = (( formData: spec.formData, jsonResponse: spec.jsonResponse, shouldValidateResponses, - shouldValidateGetRequestBody, }), userDefinedRouteFn )(req as any, res) diff --git a/packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts b/packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts new file mode 100644 index 000000000..2ee3e90e5 --- /dev/null +++ b/packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts @@ -0,0 +1,190 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import { z, ZodFirstPartyTypeKind } from "zod" +import { + BadRequestException, + InternalServerErrorException, +} from "nextjs-exception-middleware" +import { isEmpty } from "lodash" +import { NextloveRequest } from "../response-edge" +import { parseQueryParams, zodIssueToString } from "./zod" + +export interface RequestInput< + JsonBody extends z.ZodTypeAny, + QueryParams extends z.ZodTypeAny, + CommonParams extends z.ZodTypeAny, + FormData extends z.ZodTypeAny, + JsonResponse extends z.ZodTypeAny +> { + jsonBody?: JsonBody + queryParams?: QueryParams + commonParams?: CommonParams + formData?: FormData + jsonResponse?: JsonResponse + shouldValidateResponses?: boolean + shouldValidateGetRequestBody?: boolean +} + + +// NOTE: we should be able to use the same validation logic for both the nodejs and edge runtime +function validateJsonResponse( + jsonResponse: JsonResponse | undefined, + res: NextloveRequest['responseEdge'] +) { + const original_res_json = res.json + const override_res_json: NextloveRequest['responseEdge']['json'] = (body, params) => { + const is_success = res.statusCode >= 200 && res.statusCode < 300 + if (!is_success) { + return original_res_json(body, params) + } + + try { + jsonResponse?.parse(body) + } catch (err) { + throw new InternalServerErrorException({ + type: "invalid_response", + message: "the response does not match with jsonResponse", + zodError: err, + }) + } + + return res.json(body, params) + } + res.json = override_res_json +} + +export const withValidationEdge = + < + JsonBody extends z.ZodTypeAny, + QueryParams extends z.ZodTypeAny, + CommonParams extends z.ZodTypeAny, + FormData extends z.ZodTypeAny, + JsonResponse extends z.ZodTypeAny + >( + input: RequestInput< + JsonBody, + QueryParams, + CommonParams, + FormData, + JsonResponse + > + ) => + (next) => + async (req: NextloveRequest) => { + if ( + (input.formData && input.jsonBody) || + (input.formData && input.commonParams) + ) { + throw new Error("Cannot use formData with jsonBody or commonParams") + } + const { searchParams } = new URL(req.url) + const paramsArray = Array.from(searchParams.entries()); + let queryEdge = Object.fromEntries(paramsArray); + + const isBodyPresent = !!req.body + + + let bodyEdge: any + if (isBodyPresent) { + bodyEdge = await req.json() + } + + const contentType = req.headers.get("content-type") + const isContentTypeJson = contentType?.includes("application/json") + const isContentTypeFormUrlEncoded = contentType?.includes( + "application/x-www-form-urlencoded" + ) + + if ( + (req.method === "POST" || req.method === "PATCH") && + (input.jsonBody || input.commonParams) && + !isContentTypeJson && + !isEmpty(req.body) + ) { + throw new BadRequestException({ + type: "invalid_content_type", + message: `${req.method} requests must have Content-Type header with "application/json"`, + }) + } + + if ( + input.formData && + req.method !== "GET" && + !isContentTypeFormUrlEncoded + // TODO eventually we should support multipart/form-data + ) { + throw new BadRequestException({ + type: "invalid_content_type", + message: `Must have Content-Type header with "application/x-www-form-urlencoded"`, + }) + } + + try { + const original_combined_params = { ...queryEdge, ...bodyEdge } + + const willValidateRequestBody = input.shouldValidateGetRequestBody + ? true + : req.method !== "GET" + + const isFormData = Boolean(input.formData) + + if (isFormData && willValidateRequestBody) { + (req as any).edgeBody = input.formData?.parse(bodyEdge) + } + + if (!isFormData && willValidateRequestBody) { + (req as any).edgeBody = input.jsonBody?.parse(bodyEdge) + } + + if (input.queryParams) { + (req as any).edgeQuery = parseQueryParams(input.queryParams, queryEdge) + } + + if (input.commonParams) { + /** + * as commonParams includes query params, we can use the parseQueryParams function + */ + ;(req as any).commonParams = parseQueryParams( + input.commonParams, + original_combined_params + ) + } + } catch (error: any) { + if (error.name === "ZodError") { + let message + if (error.issues.length === 1) { + const issue = error.issues[0] + message = zodIssueToString(issue) + } else { + const message_components: string[] = [] + for (const issue of error.issues) { + message_components.push(zodIssueToString(issue)) + } + message = + `${error.issues.length} Input Errors: ` + + message_components.join(", ") + } + + throw new BadRequestException({ + type: "invalid_input", + message, + validation_errors: error.format(), + }) + } + + throw new BadRequestException({ + type: "invalid_input", + message: "Error while parsing input", + }) + } + + /** + * this will override the res.json method to validate the response + */ + if (input.shouldValidateResponses) { + validateJsonResponse(input.jsonResponse, req.responseEdge) + } + + return next(req) + } + + diff --git a/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts b/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts index 0ba56d291..48c9432ed 100644 --- a/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts +++ b/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts @@ -5,95 +5,8 @@ import { InternalServerErrorException, } from "nextjs-exception-middleware" import { isEmpty } from "lodash" +import { parseQueryParams, zodIssueToString } from "./zod" -const getZodObjectSchemaFromZodEffectSchema = ( - isZodEffect: boolean, - schema: z.ZodTypeAny -): z.ZodTypeAny | z.ZodObject => { - if (!isZodEffect) { - return schema as z.ZodObject - } - - let currentSchema = schema - - while (currentSchema instanceof z.ZodEffects) { - currentSchema = currentSchema._def.schema - } - - return currentSchema as z.ZodObject -} - -/** - * This function is used to get the correct schema from a ZodEffect | ZodDefault | ZodOptional schema. - * TODO: this function should handle all special cases of ZodSchema and not just ZodEffect | ZodDefault | ZodOptional - */ -const getZodDefFromZodSchemaHelpers = (schema: z.ZodTypeAny) => { - const special_zod_types = [ - ZodFirstPartyTypeKind.ZodOptional, - ZodFirstPartyTypeKind.ZodDefault, - ZodFirstPartyTypeKind.ZodEffects, - ] - - while (special_zod_types.includes(schema._def.typeName)) { - if ( - schema._def.typeName === ZodFirstPartyTypeKind.ZodOptional || - schema._def.typeName === ZodFirstPartyTypeKind.ZodDefault - ) { - schema = schema._def.innerType - continue - } - - if (schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects) { - schema = schema._def.schema - continue - } - } - return schema._def -} - -const parseQueryParams = ( - schema: z.ZodTypeAny, - input: Record -) => { - const parsed_input = Object.assign({}, input) - const isZodEffect = schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects - const safe_schema = getZodObjectSchemaFromZodEffectSchema(isZodEffect, schema) - const isZodObject = - safe_schema._def.typeName === ZodFirstPartyTypeKind.ZodObject - - if (isZodObject) { - const obj_schema = safe_schema as z.ZodObject - - for (const [key, value] of Object.entries(obj_schema.shape)) { - const def = getZodDefFromZodSchemaHelpers(value as z.ZodTypeAny) - const isArray = def.typeName === ZodFirstPartyTypeKind.ZodArray - if (isArray) { - const array_input = input[key] - - if (typeof array_input === "string") { - parsed_input[key] = array_input.split(",") - } - - if (Array.isArray(input[`${key}[]`])) { - parsed_input[key] = input[`${key}[]`] - } - - continue - } - - const isBoolean = def.typeName === ZodFirstPartyTypeKind.ZodBoolean - if (isBoolean) { - const boolean_input = input[key] - - if (typeof boolean_input === "string") { - parsed_input[key] = boolean_input === "true" - } - } - } - } - - return schema.parse(parsed_input) -} export interface RequestInput< JsonBody extends z.ZodTypeAny, @@ -111,16 +24,6 @@ export interface RequestInput< shouldValidateGetRequestBody?: boolean } -const zodIssueToString = (issue: z.ZodIssue) => { - if (issue.path.join(".") === "") { - return issue.message - } - if (issue.message === "Required") { - return `${issue.path.join(".")} is required` - } - return `${issue.message} for "${issue.path.join(".")}"` -} - function validateJsonResponse( jsonResponse: JsonResponse | undefined, res: NextApiResponse diff --git a/packages/nextlove/src/with-route-spec/middlewares/zod.ts b/packages/nextlove/src/with-route-spec/middlewares/zod.ts new file mode 100644 index 000000000..8418771c3 --- /dev/null +++ b/packages/nextlove/src/with-route-spec/middlewares/zod.ts @@ -0,0 +1,101 @@ +import { z, ZodFirstPartyTypeKind } from "zod" + +export const getZodObjectSchemaFromZodEffectSchema = ( + isZodEffect: boolean, + schema: z.ZodTypeAny +): z.ZodTypeAny | z.ZodObject => { + if (!isZodEffect) { + return schema as z.ZodObject + } + + let currentSchema = schema + + while (currentSchema instanceof z.ZodEffects) { + currentSchema = currentSchema._def.schema + } + + return currentSchema as z.ZodObject +} + +/** + * This function is used to get the correct schema from a ZodEffect | ZodDefault | ZodOptional schema. + * TODO: this function should handle all special cases of ZodSchema and not just ZodEffect | ZodDefault | ZodOptional + */ +export const getZodDefFromZodSchemaHelpers = (schema: z.ZodTypeAny) => { + const special_zod_types = [ + ZodFirstPartyTypeKind.ZodOptional, + ZodFirstPartyTypeKind.ZodDefault, + ZodFirstPartyTypeKind.ZodEffects, + ] + + while (special_zod_types.includes(schema._def.typeName)) { + if ( + schema._def.typeName === ZodFirstPartyTypeKind.ZodOptional || + schema._def.typeName === ZodFirstPartyTypeKind.ZodDefault + ) { + schema = schema._def.innerType + continue + } + + if (schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects) { + schema = schema._def.schema + continue + } + } + return schema._def +} + +export const parseQueryParams = ( + schema: z.ZodTypeAny, + input: Record +) => { + const parsed_input = Object.assign({}, input) + const isZodEffect = schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + const safe_schema = getZodObjectSchemaFromZodEffectSchema(isZodEffect, schema) + const isZodObject = + safe_schema._def.typeName === ZodFirstPartyTypeKind.ZodObject + + if (isZodObject) { + const obj_schema = safe_schema as z.ZodObject + + for (const [key, value] of Object.entries(obj_schema.shape)) { + const def = getZodDefFromZodSchemaHelpers(value as z.ZodTypeAny) + const isArray = def.typeName === ZodFirstPartyTypeKind.ZodArray + if (isArray) { + const array_input = input[key] + + if (typeof array_input === "string") { + parsed_input[key] = array_input.split(",") + } + + if (Array.isArray(input[`${key}[]`])) { + parsed_input[key] = input[`${key}[]`] + } + + continue + } + + const isBoolean = def.typeName === ZodFirstPartyTypeKind.ZodBoolean + if (isBoolean) { + const boolean_input = input[key] + + if (typeof boolean_input === "string") { + parsed_input[key] = boolean_input === "true" + } + } + } + } + + return schema.parse(parsed_input) +} + + +export const zodIssueToString = (issue: z.ZodIssue) => { + if (issue.path.join(".") === "") { + return issue.message + } + if (issue.message === "Required") { + return `${issue.path.join(".")} is required` + } + return `${issue.message} for "${issue.path.join(".")}"` +} diff --git a/packages/nextlove/src/with-route-spec/response-edge.ts b/packages/nextlove/src/with-route-spec/response-edge.ts new file mode 100644 index 000000000..ad6da3f70 --- /dev/null +++ b/packages/nextlove/src/with-route-spec/response-edge.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server" + +export type NextloveRequest = NextRequest & { + responseEdge: ReturnType +} + +export const getResponseEdge = () => { + const json = (body, params?: ResponseInit) => NextResponse.json(body, params) + const status = (s: number) => { + return { + statusCode: s, + json: (body, params?: ResponseInit) => json(body, { status: s, ...params }), + status, + } + } + + return { + status, + json, + statusCode: 200, + } +} \ No newline at end of file diff --git a/packages/nextlove/src/with-route-spec/wrappers-edge.ts b/packages/nextlove/src/with-route-spec/wrappers-edge.ts new file mode 100644 index 000000000..4a6ac9c22 --- /dev/null +++ b/packages/nextlove/src/with-route-spec/wrappers-edge.ts @@ -0,0 +1,320 @@ +import type { NextRequest as Req } from "next/server" +/* + +Wraps a function in layers of other functions, while preserving the input/output +type. The output of wrappers will always have the type of it's last parameter +(the wrapped function) + +This function turns this type of composition... + +logger.withContext("somecontext")( + async (a, b) => { + return a + } +) + +Into... + +wrappers( + logger.withContext("somecontext"), + async (a, b) => { + return a + } +) + +Having this as a utility method helps preserve types, which otherwise can get +messed up by the middlewares. It also can make the code cleaner where there are +multiple wrappers. + +## EXAMPLES + +In the context of request middleware you might write something like this... + +const withRequestLoggingMiddleware = (next) => async (req, res) => { + console.log(`GOT REQUEST ${req.method} ${req.path}`) + return next(req, res) +} + +Here's an example of a wrapper that takes some parameters... + +const withLoggedArguments = + (logPrefix: string) => + (next) => + async (...funcArgs) => { + console.log(logPrefix, ...funcArgs) + return next(...funcArgs) + } + +*/ + +export type MiddlewareEdge = ( + next: (req: Req & Dep & T) => any +) => (req: Req & Dep & T) => any + +// Safer Middleware requires the use of extendRequest to ensure that the +// new context (T) was actually added to the request. It's kind of annoying +// to use in practice, so we don't use it for our Wrappers (yet) +export type SaferMiddlewareEgde = ( + next: (req: Req & Dep & T) => any +) => (req: Req & Dep) => any + +export const extendRequest = >(req: T, merge: K): T & K => { + for (const [key, v] of Object.entries(merge)) { + ;(req as any)[key] = v + } + return req as any +} + +type WrappersEdge1 = ( + mw1: MiddlewareEdge, + endpoint: (req: Req & Mw1RequestContext) => any +) => (req: Req) => any + +type WrappersEdge2 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep +>( + mw1: MiddlewareEdge, + mw2: MiddlewareEdge, + endpoint: (req: Req & Mw1RequestContext & Mw2RequestContext) => any +) => (req: Req) => any + +// TODO figure out how to do a recursive definition, or one that simplifies +// these redundant WrappersEdge + +type WrappersEdge3 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep +>( + mw1: MiddlewareEdge, + mw2: MiddlewareEdge, + mw3: MiddlewareEdge< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + endpoint: ( + req: Req & Mw1RequestContext & Mw2RequestContext & Mw3RequestContext + ) => any +) => (req: Req) => any + +type WrappersEdge4 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep +>( + mw1: MiddlewareEdge, + mw2: MiddlewareEdge, + mw3: MiddlewareEdge< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: MiddlewareEdge< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext + ) => any +) => (req: Req) => any + +type WrappersEdge5 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep, + Mw5RequestContext, + Mw5Dep +>( + mw1: MiddlewareEdge, + mw2: MiddlewareEdge, + mw3: MiddlewareEdge< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: MiddlewareEdge< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + mw5: MiddlewareEdge< + Mw5RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext extends Mw5Dep + ? Mw5Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext + ) => any +) => (req: Req) => any + +type WrappersEdge6 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep, + Mw5RequestContext, + Mw5Dep, + Mw6RequestContext, + Mw6Dep +>( + mw1: MiddlewareEdge, + mw2: MiddlewareEdge, + mw3: MiddlewareEdge< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: MiddlewareEdge< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + mw5: MiddlewareEdge< + Mw5RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext extends Mw5Dep + ? Mw5Dep + : never + >, + mw6: MiddlewareEdge< + Mw6RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext extends Mw6Dep + ? Mw6Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext & + Mw6RequestContext + ) => any +) => (req: Req) => any + +type WrappersEdge7 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep, + Mw5RequestContext, + Mw5Dep, + Mw6RequestContext, + Mw6Dep, + Mw7RequestContext, + Mw7Dep +>( + mw1: MiddlewareEdge, + mw2: MiddlewareEdge, + mw3: MiddlewareEdge< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: MiddlewareEdge< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + mw5: MiddlewareEdge< + Mw5RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext extends Mw5Dep + ? Mw5Dep + : never + >, + mw6: MiddlewareEdge< + Mw6RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext extends Mw6Dep + ? Mw6Dep + : never + >, + mw7: MiddlewareEdge< + Mw7RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext & + Mw6RequestContext extends Mw7Dep + ? Mw7Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext & + Mw6RequestContext & + Mw7RequestContext + ) => any +) => (req: Req) => any + +type WrappersEdge = WrappersEdge1 & + WrappersEdge2 & + WrappersEdge3 & + WrappersEdge4 & + WrappersEdge5 & + WrappersEdge6 & + WrappersEdge7 + +export const wrappersEdge: WrappersEdge = (...wrappersArgs: any[]) => { + const wrappedFunction = wrappersArgs[wrappersArgs.length - 1] + const mws = wrappersArgs.slice(0, -1) + + let lastWrappedFunction = wrappedFunction + for (let i = mws.length - 1; i >= 0; i--) { + lastWrappedFunction = (mws[i] as any)(lastWrappedFunction) + } + + return lastWrappedFunction +} \ No newline at end of file diff --git a/packages/nextlove/tsconfig.json b/packages/nextlove/tsconfig.json index b06c1fa83..166ce4ae8 100755 --- a/packages/nextlove/tsconfig.json +++ b/packages/nextlove/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2020", + "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, From 83c31b0d19d5c797af466cdf00a11a8e6103e679 Mon Sep 17 00:00:00 2001 From: Itelo Filho Date: Wed, 12 Jul 2023 17:53:30 -0300 Subject: [PATCH 2/3] edge stuffs --- apps/example-todo-app/next-env.d.ts | 1 + apps/example-todo-app/next.config.js | 14 +- apps/example-todo-app/package.json | 6 +- apps/example-todo-app/src/app/main/route.ts | 30 ++++ .../{ => src}/lib/middlewares/index.ts | 0 .../lib/middlewares/with-auth-token-edge.ts | 30 ++++ .../lib/middlewares/with-auth-token.ts | 0 .../lib/middlewares/with-route-spec.ts | 12 +- .../{ => src}/pages/api/health.ts | 0 .../todo/add-ignore-invalid-json-response.ts | 2 +- .../api/todo/add-invalid-json-response.ts | 2 +- .../{ => src}/pages/api/todo/add.ts | 6 +- .../pages/api/todo/delete-common-params.ts | 2 +- .../{ => src}/pages/api/todo/delete.ts | 2 +- .../{ => src}/pages/api/todo/form-add.ts | 2 +- .../pages/api/todo/get-no-validate-body.ts | 2 +- .../{ => src}/pages/api/todo/get.ts | 2 +- .../{ => src}/pages/api/todo/index.ts | 2 +- .../api/todo/json-response-must-be-schema.ts | 2 +- .../pages/api/todo/list-optional-ids.ts | 2 +- .../pages/api/todo/list-with-refine.ts | 2 +- .../{ => src}/pages/api/todo/list.ts | 2 +- .../tests/api/todo/form-add.test.ts | 2 +- apps/example-todo-app/tsconfig.json | 29 +++- yarn.lock | 145 +++++++++++++++++- 25 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 apps/example-todo-app/src/app/main/route.ts rename apps/example-todo-app/{ => src}/lib/middlewares/index.ts (100%) create mode 100644 apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts rename apps/example-todo-app/{ => src}/lib/middlewares/with-auth-token.ts (100%) rename apps/example-todo-app/{ => src}/lib/middlewares/with-route-spec.ts (68%) rename apps/example-todo-app/{ => src}/pages/api/health.ts (100%) rename apps/example-todo-app/{ => src}/pages/api/todo/add-ignore-invalid-json-response.ts (95%) rename apps/example-todo-app/{ => src}/pages/api/todo/add-invalid-json-response.ts (90%) rename apps/example-todo-app/{ => src}/pages/api/todo/add.ts (80%) rename apps/example-todo-app/{ => src}/pages/api/todo/delete-common-params.ts (90%) rename apps/example-todo-app/{ => src}/pages/api/todo/delete.ts (90%) rename apps/example-todo-app/{ => src}/pages/api/todo/form-add.ts (90%) rename apps/example-todo-app/{ => src}/pages/api/todo/get-no-validate-body.ts (94%) rename apps/example-todo-app/{ => src}/pages/api/todo/get.ts (94%) rename apps/example-todo-app/{ => src}/pages/api/todo/index.ts (94%) rename apps/example-todo-app/{ => src}/pages/api/todo/json-response-must-be-schema.ts (89%) rename apps/example-todo-app/{ => src}/pages/api/todo/list-optional-ids.ts (89%) rename apps/example-todo-app/{ => src}/pages/api/todo/list-with-refine.ts (94%) rename apps/example-todo-app/{ => src}/pages/api/todo/list.ts (88%) diff --git a/apps/example-todo-app/next-env.d.ts b/apps/example-todo-app/next-env.d.ts index 4f11a03dc..fd36f9494 100755 --- a/apps/example-todo-app/next-env.d.ts +++ b/apps/example-todo-app/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/example-todo-app/next.config.js b/apps/example-todo-app/next.config.js index 6f0005eab..1e9ffaf20 100755 --- a/apps/example-todo-app/next.config.js +++ b/apps/example-todo-app/next.config.js @@ -1,15 +1,19 @@ // This file changes the routing to allow top-level prefixes -module.exports = { +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, async rewrites() { return { beforeFiles: [ // Nextjs by default requires a /api prefix, let's remove that - { - source: "/:path*", - destination: "/api/:path*", - }, + // { + // source: "/:path*", + // destination: "/api/:path*", + // }, ], } }, } + +module.exports = nextConfig diff --git a/apps/example-todo-app/package.json b/apps/example-todo-app/package.json index 14fb1af17..508d6c7cd 100644 --- a/apps/example-todo-app/package.json +++ b/apps/example-todo-app/package.json @@ -21,13 +21,13 @@ "glob-promise": "4.2.2", "micro": "9.3.4", "mkdirp": "1.0.4", - "next": "12.2.0", + "next": "^13.4.9", "nextjs-middleware-wrappers": "^1.3.0", "nextlove": "*", "path-to-regexp": "6.2.1", "raw-body": "2.3.2", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "uuid": "^8.3.2", "zod": "^3.17.3" }, diff --git a/apps/example-todo-app/src/app/main/route.ts b/apps/example-todo-app/src/app/main/route.ts new file mode 100644 index 000000000..7610d67bd --- /dev/null +++ b/apps/example-todo-app/src/app/main/route.ts @@ -0,0 +1,30 @@ +import { withRouteSpecEdge } from "@/lib/middlewares" +import {z } from "zod" +export const runtime = 'edge' + +const route_spec = { + methods: ["GET", "POST"], + queryParams: z.object({ + foo: z.array(z.string()) + }), + jsonBody: z.object({ + top: z.boolean() + }), + auth: "none" +} as const + +export const GET = withRouteSpecEdge(route_spec)((req) => { + console.log({ + query: req.edgeQuery, + body: req.edgeBody + }) + return req.responseEdge.status(203).json({ product: [2] }) +}) + +export const POST = withRouteSpecEdge(route_spec)((req) => { + console.log({ + query: req.edgeQuery, + body: req.edgeBody + }) + return req.responseEdge.status(203).json({ product: [2] }) +}) \ No newline at end of file diff --git a/apps/example-todo-app/lib/middlewares/index.ts b/apps/example-todo-app/src/lib/middlewares/index.ts similarity index 100% rename from apps/example-todo-app/lib/middlewares/index.ts rename to apps/example-todo-app/src/lib/middlewares/index.ts diff --git a/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts b/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts new file mode 100644 index 000000000..b3747ad52 --- /dev/null +++ b/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts @@ -0,0 +1,30 @@ +import { UnauthorizedException, MiddlewareEdge } from "nextlove" + +export const withAuthTokenEdge: MiddlewareEdge<{ + auth: { + authorized_by: "auth_token" + } +}> = (next) => async (req) => { + const authorization = req.headers.get("authorization") + + if (authorization?.split("Bearer ")?.[1] !== "auth_token") { + throw new UnauthorizedException({ + type: "unauthorized", + message: "Unauthorized", + }) + } + + req.auth = { + authorized_by: "auth_token", + } + + return next(req) +} + +withAuthTokenEdge.securitySchema = { + type: "http", + scheme: "bearer", + bearerFormat: "API Token", +} + +export default withAuthTokenEdge diff --git a/apps/example-todo-app/lib/middlewares/with-auth-token.ts b/apps/example-todo-app/src/lib/middlewares/with-auth-token.ts similarity index 100% rename from apps/example-todo-app/lib/middlewares/with-auth-token.ts rename to apps/example-todo-app/src/lib/middlewares/with-auth-token.ts diff --git a/apps/example-todo-app/lib/middlewares/with-route-spec.ts b/apps/example-todo-app/src/lib/middlewares/with-route-spec.ts similarity index 68% rename from apps/example-todo-app/lib/middlewares/with-route-spec.ts rename to apps/example-todo-app/src/lib/middlewares/with-route-spec.ts index 8e40883dc..9594ee070 100644 --- a/apps/example-todo-app/lib/middlewares/with-route-spec.ts +++ b/apps/example-todo-app/src/lib/middlewares/with-route-spec.ts @@ -1,5 +1,6 @@ -import { createWithRouteSpec } from "nextlove" +import { createWithRouteSpec, createWithRouteSpecEdge } from "nextlove" import { withAuthToken } from "./with-auth-token" +import withAuthTokenEdge from "./with-auth-token-edge" export { checkRouteSpec } from "nextlove" export const withRouteSpec = createWithRouteSpec({ @@ -10,6 +11,15 @@ export const withRouteSpec = createWithRouteSpec({ shouldValidateResponses: true, } as const) +export const withRouteSpecEdge = createWithRouteSpecEdge({ + authMiddlewareMap: { auth_token: withAuthTokenEdge }, + globalMiddlewares: [], + apiName: "TODO API", + productionServerUrl: "https://example.com", + shouldValidateResponses: true, +} as const) + + export const withRouteSpecWithoutValidateGetRequestBody = createWithRouteSpec({ authMiddlewareMap: { auth_token: withAuthToken }, globalMiddlewares: [], diff --git a/apps/example-todo-app/pages/api/health.ts b/apps/example-todo-app/src/pages/api/health.ts similarity index 100% rename from apps/example-todo-app/pages/api/health.ts rename to apps/example-todo-app/src/pages/api/health.ts diff --git a/apps/example-todo-app/pages/api/todo/add-ignore-invalid-json-response.ts b/apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts similarity index 95% rename from apps/example-todo-app/pages/api/todo/add-ignore-invalid-json-response.ts rename to apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts index 3735c79c2..2ee88af46 100644 --- a/apps/example-todo-app/pages/api/todo/add-ignore-invalid-json-response.ts +++ b/apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts @@ -1,7 +1,7 @@ import { withRouteSpecWithoutValidateResponse, checkRouteSpec, -} from "lib/middlewares" +} from "src/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" diff --git a/apps/example-todo-app/pages/api/todo/add-invalid-json-response.ts b/apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts similarity index 90% rename from apps/example-todo-app/pages/api/todo/add-invalid-json-response.ts rename to apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts index 18c5eb5a1..7c439d89d 100644 --- a/apps/example-todo-app/pages/api/todo/add-invalid-json-response.ts +++ b/apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts @@ -1,4 +1,4 @@ -import { withRouteSpec } from "lib/middlewares" +import { withRouteSpec } from "src/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" diff --git a/apps/example-todo-app/pages/api/todo/add.ts b/apps/example-todo-app/src/pages/api/todo/add.ts similarity index 80% rename from apps/example-todo-app/pages/api/todo/add.ts rename to apps/example-todo-app/src/pages/api/todo/add.ts index da44443d3..69c3d4900 100644 --- a/apps/example-todo-app/pages/api/todo/add.ts +++ b/apps/example-todo-app/src/pages/api/todo/add.ts @@ -1,4 +1,4 @@ -import { withRouteSpec, checkRouteSpec } from "lib/middlewares" +import { withRouteSpec } from "@/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" @@ -8,14 +8,14 @@ export const jsonBody = z.object({ completed: z.boolean().optional().default(false), }) -export const route_spec = checkRouteSpec({ +export const route_spec = { methods: ["POST"], auth: "auth_token", jsonBody, jsonResponse: z.object({ ok: z.boolean(), }), -}) +} as const export default withRouteSpec(route_spec)(async (req, res) => { return res.status(200).json({ ok: true }) diff --git a/apps/example-todo-app/pages/api/todo/delete-common-params.ts b/apps/example-todo-app/src/pages/api/todo/delete-common-params.ts similarity index 90% rename from apps/example-todo-app/pages/api/todo/delete-common-params.ts rename to apps/example-todo-app/src/pages/api/todo/delete-common-params.ts index 5ca5fe286..663510e4d 100644 --- a/apps/example-todo-app/pages/api/todo/delete-common-params.ts +++ b/apps/example-todo-app/src/pages/api/todo/delete-common-params.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/pages/api/todo/delete.ts b/apps/example-todo-app/src/pages/api/todo/delete.ts similarity index 90% rename from apps/example-todo-app/pages/api/todo/delete.ts rename to apps/example-todo-app/src/pages/api/todo/delete.ts index 85ee01b81..8725d49a6 100644 --- a/apps/example-todo-app/pages/api/todo/delete.ts +++ b/apps/example-todo-app/src/pages/api/todo/delete.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/pages/api/todo/form-add.ts b/apps/example-todo-app/src/pages/api/todo/form-add.ts similarity index 90% rename from apps/example-todo-app/pages/api/todo/form-add.ts rename to apps/example-todo-app/src/pages/api/todo/form-add.ts index 86f70bede..9ee4a47e3 100644 --- a/apps/example-todo-app/pages/api/todo/form-add.ts +++ b/apps/example-todo-app/src/pages/api/todo/form-add.ts @@ -1,4 +1,4 @@ -import { withRouteSpec, checkRouteSpec } from "lib/middlewares" +import { withRouteSpec, checkRouteSpec } from "src/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" import { HttpException } from "nextlove" diff --git a/apps/example-todo-app/pages/api/todo/get-no-validate-body.ts b/apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts similarity index 94% rename from apps/example-todo-app/pages/api/todo/get-no-validate-body.ts rename to apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts index 83a393caf..46e8bc882 100644 --- a/apps/example-todo-app/pages/api/todo/get-no-validate-body.ts +++ b/apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts @@ -1,7 +1,7 @@ import { checkRouteSpec, withRouteSpecWithoutValidateGetRequestBody, -} from "lib/middlewares" +} from "src/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/pages/api/todo/get.ts b/apps/example-todo-app/src/pages/api/todo/get.ts similarity index 94% rename from apps/example-todo-app/pages/api/todo/get.ts rename to apps/example-todo-app/src/pages/api/todo/get.ts index 24daf19a6..4a0ed4ded 100644 --- a/apps/example-todo-app/pages/api/todo/get.ts +++ b/apps/example-todo-app/src/pages/api/todo/get.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/pages/api/todo/index.ts b/apps/example-todo-app/src/pages/api/todo/index.ts similarity index 94% rename from apps/example-todo-app/pages/api/todo/index.ts rename to apps/example-todo-app/src/pages/api/todo/index.ts index 24daf19a6..4a0ed4ded 100644 --- a/apps/example-todo-app/pages/api/todo/index.ts +++ b/apps/example-todo-app/src/pages/api/todo/index.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/pages/api/todo/json-response-must-be-schema.ts b/apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts similarity index 89% rename from apps/example-todo-app/pages/api/todo/json-response-must-be-schema.ts rename to apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts index 5dbb80023..8ce4f0459 100644 --- a/apps/example-todo-app/pages/api/todo/json-response-must-be-schema.ts +++ b/apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts @@ -1,4 +1,4 @@ -import { withRouteSpec } from "lib/middlewares" +import { withRouteSpec } from "src/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" diff --git a/apps/example-todo-app/pages/api/todo/list-optional-ids.ts b/apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts similarity index 89% rename from apps/example-todo-app/pages/api/todo/list-optional-ids.ts rename to apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts index 1ce757027..923b1dd87 100644 --- a/apps/example-todo-app/pages/api/todo/list-optional-ids.ts +++ b/apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" import { z } from "zod" export const commonParams = z.object({ diff --git a/apps/example-todo-app/pages/api/todo/list-with-refine.ts b/apps/example-todo-app/src/pages/api/todo/list-with-refine.ts similarity index 94% rename from apps/example-todo-app/pages/api/todo/list-with-refine.ts rename to apps/example-todo-app/src/pages/api/todo/list-with-refine.ts index 30fb9aa37..b596ba1a7 100644 --- a/apps/example-todo-app/pages/api/todo/list-with-refine.ts +++ b/apps/example-todo-app/src/pages/api/todo/list-with-refine.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" import { z } from "zod" export const commonParams = z diff --git a/apps/example-todo-app/pages/api/todo/list.ts b/apps/example-todo-app/src/pages/api/todo/list.ts similarity index 88% rename from apps/example-todo-app/pages/api/todo/list.ts rename to apps/example-todo-app/src/pages/api/todo/list.ts index bf0126557..8dce94009 100644 --- a/apps/example-todo-app/pages/api/todo/list.ts +++ b/apps/example-todo-app/src/pages/api/todo/list.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" import { z } from "zod" export const commonParams = z.object({ diff --git a/apps/example-todo-app/tests/api/todo/form-add.test.ts b/apps/example-todo-app/tests/api/todo/form-add.test.ts index 7bc084e13..f92a4c0c5 100644 --- a/apps/example-todo-app/tests/api/todo/form-add.test.ts +++ b/apps/example-todo-app/tests/api/todo/form-add.test.ts @@ -2,7 +2,7 @@ import test from "ava" import { TODO_ID } from "tests/fixtures" import getTestServer from "tests/fixtures/get-test-server" import { v4 as uuidv4 } from "uuid" -import { formData } from "pages/api/todo/form-add" +import { formData } from "@/pages/api/todo/form-add" test("POST /todo/form-add", async (t) => { const { axios } = await getTestServer(t) diff --git a/apps/example-todo-app/tsconfig.json b/apps/example-todo-app/tsconfig.json index 23e023584..d4a4df54d 100755 --- a/apps/example-todo-app/tsconfig.json +++ b/apps/example-todo-app/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -14,8 +18,25 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "baseUrl": "." + "strictNullChecks": true, + "paths": { + "@/*": [ + "./src/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/yarn.lock b/yarn.lock index 164eacb0a..7a1138536 100644 --- a/yarn.lock +++ b/yarn.lock @@ -686,6 +686,11 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.0.tgz#17ce2d9f5532b677829840037e06f208b7eed66b" integrity sha512-/FCkDpL/8SodJEXvx/DYNlOD5ijTtkozf4PPulYPtkPOJaMPpBSOkzmsta4fnrnbdH6eZjbwbiXFdr6gSQCV4w== +"@next/env@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e" + integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw== + "@next/eslint-plugin-next@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.2.0.tgz#38b36d3be244cc9a98c0e7d203bdb062f87df4ac" @@ -715,11 +720,21 @@ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.0.tgz#3473889157ba70b30ccdd4f59c46232d841744e2" integrity sha512-x5U5gJd7ZvrEtTFnBld9O2bUlX8opu7mIQUqRzj7KeWzBwPhrIzTTsQXAiNqsaMuaRPvyHBVW/5d/6g6+89Y8g== +"@next/swc-darwin-arm64@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677" + integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ== + "@next/swc-darwin-x64@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.0.tgz#b25198c3ef4c906000af49e4787a757965f760bb" integrity sha512-iwMNFsrAPjfedjKDv9AXPAV16PWIomP3qw/FfPaxkDVRbUls7BNdofBLzkQmqxqWh93WrawLwaqyXpJuAaiwJA== +"@next/swc-darwin-x64@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8" + integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A== + "@next/swc-freebsd-x64@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.0.tgz#78e2213f8b703be0fef23a49507779b4a9842929" @@ -735,36 +750,71 @@ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.0.tgz#134a42ddea804d6bf04761607f774432c3126de6" integrity sha512-++WAB4ElXCSOKG9H8r4ENF8EaV+w0QkrpjehmryFkQXmt5juVXz+nKDVlCRMwJU7A1O0Mie82XyEoOrf6Np1pA== +"@next/swc-linux-arm64-gnu@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1" + integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg== + "@next/swc-linux-arm64-musl@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.0.tgz#c781ac642ad35e0578d8a8d19c638b0f31c1a334" integrity sha512-XrqkHi/VglEn5zs2CYK6ofJGQySrd+Lr4YdmfJ7IhsCnMKkQY1ma9Hv5THwhZVof3e+6oFHrQ9bWrw9K4WTjFA== +"@next/swc-linux-arm64-musl@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0" + integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw== + "@next/swc-linux-x64-gnu@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.0.tgz#0e2235a59429eadd40ac8880aec18acdbc172a31" integrity sha512-MyhHbAKVjpn065WzRbqpLu2krj4kHLi6RITQdD1ee+uxq9r2yg5Qe02l24NxKW+1/lkmpusl4Y5Lks7rBiJn4w== +"@next/swc-linux-x64-gnu@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a" + integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg== + "@next/swc-linux-x64-musl@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.0.tgz#b0a10db0d9e16f079429588a58f71fa3c3d46178" integrity sha512-Tz1tJZ5egE0S/UqCd5V6ZPJsdSzv/8aa7FkwFmIJ9neLS8/00za+OY5pq470iZQbPrkTwpKzmfTTIPRVD5iqDg== +"@next/swc-linux-x64-musl@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a" + integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A== + "@next/swc-win32-arm64-msvc@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.0.tgz#3063f850c9db7b774c69e9be74ad59986cf6fc34" integrity sha512-0iRO/CPMCdCYUzuH6wXLnsfJX1ykBX4emOOvH0qIgtiZM0nVYbF8lkEyY2ph4XcsurpinS+ziWuYCXVqrOSqiw== +"@next/swc-win32-arm64-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a" + integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA== + "@next/swc-win32-ia32-msvc@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.0.tgz#001bbadf3d2cf006c4991f728d1d23e4d5c0e7cc" integrity sha512-8A26RJVcJHwIKm8xo/qk2ePRquJ6WCI2keV2qOW/Qm+ZXrPXHMIWPYABae/nKN243YFBNyPiHytjX37VrcpUhg== +"@next/swc-win32-ia32-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190" + integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA== + "@next/swc-win32-x64-msvc@12.2.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.0.tgz#9f66664f9122ca555b96a5f2fc6e2af677bf801b" integrity sha512-OI14ozFLThEV3ey6jE47zrzSTV/6eIMsvbwozo+XfdWqOPwQ7X00YkRx4GVMKMC0rM44oGS2gmwMKYpe4EblnA== +"@next/swc-win32-x64-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b" + integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1271,6 +1321,13 @@ dependencies: tslib "^2.4.0" +"@swc/helpers@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" + integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== + dependencies: + tslib "^2.4.0" + "@swc/jest@^0.2.24": version "0.2.24" resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.24.tgz#35d9377ede049613cd5fdd6c24af2b8dcf622875" @@ -2165,6 +2222,13 @@ bundle-require@^3.0.2: dependencies: load-tsconfig "^0.2.0" +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -2259,6 +2323,11 @@ caniuse-lite@^1.0.30001332: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001464.tgz#888922718df48ce5e33dcfe1a2af7d42676c5eb7" integrity sha512-oww27MtUmusatpRpCGSOneQk2/l5czXANDSFvsc7VuOQ86s3ANhZetpwXNf1zY/zdfP63Xvjz325DAdAoES13g== +caniuse-lite@^1.0.30001406: + version "1.0.30001515" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz#418aefeed9d024cd3129bfae0ccc782d4cb8f12b" + integrity sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA== + caniuse-lite@^1.0.30001449: version "1.0.30001470" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001470.tgz#09c8e87c711f75ff5d39804db2613dd593feeb10" @@ -2410,6 +2479,11 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -4027,6 +4101,11 @@ glob-promise@4.2.2, glob-promise@^4.2.2: dependencies: "@types/glob" "^7.1.3" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -6022,6 +6101,11 @@ nanoid@^3.1.30: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.4: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6073,6 +6157,30 @@ next@12.2.0: "@next/swc-win32-ia32-msvc" "12.2.0" "@next/swc-win32-x64-msvc" "12.2.0" +next@^13.4.9: + version "13.4.9" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba" + integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA== + dependencies: + "@next/env" "13.4.9" + "@swc/helpers" "0.5.1" + busboy "1.6.0" + caniuse-lite "^1.0.30001406" + postcss "8.4.14" + styled-jsx "5.1.1" + watchpack "2.4.0" + zod "3.21.4" + optionalDependencies: + "@next/swc-darwin-arm64" "13.4.9" + "@next/swc-darwin-x64" "13.4.9" + "@next/swc-linux-arm64-gnu" "13.4.9" + "@next/swc-linux-arm64-musl" "13.4.9" + "@next/swc-linux-x64-gnu" "13.4.9" + "@next/swc-linux-x64-musl" "13.4.9" + "@next/swc-win32-arm64-msvc" "13.4.9" + "@next/swc-win32-ia32-msvc" "13.4.9" + "@next/swc-win32-x64-msvc" "13.4.9" + nextjs-exception-middleware@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/nextjs-exception-middleware/-/nextjs-exception-middleware-2.0.1.tgz#45b3254cdb7deee583337ff04672109083b41938" @@ -6857,6 +6965,15 @@ postcss-selector-parser@^6.0.10: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss@8.4.14: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + postcss@8.4.5: version "8.4.5" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" @@ -7051,7 +7168,7 @@ rc@1.2.8, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@18.2.0: +react-dom@18.2.0, react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -7069,7 +7186,7 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react@18.2.0: +react@18.2.0, react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -7598,7 +7715,7 @@ socks@^2.6.2: ip "^2.0.0" smart-buffer "^4.2.0" -source-map-js@^1.0.1: +source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -7722,6 +7839,11 @@ stream-combiner2@~1.1.1: duplexer2 "~0.1.0" readable-stream "^2.0.2" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -7836,6 +7958,13 @@ styled-jsx@5.0.2: resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729" integrity sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ== +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + sucrase@^3.20.3: version "3.29.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.29.0.tgz#3207c5bc1b980fdae1e539df3f8a8a518236da7d" @@ -8409,6 +8538,14 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -8625,7 +8762,7 @@ zod-to-ts@^1.1.1: resolved "https://registry.yarnpkg.com/zod-to-ts/-/zod-to-ts-1.1.2.tgz#d505e46f99edd50a2e53e6dcd156494d2ef82a40" integrity sha512-IZdM0Ga1l/vQnrNy63aV3qsLNchD0aUYZEtXcyt8A3xA7+erFO0zeImEcUH5qNLA6tXDTjRpcWp0paoVoH/D0A== -zod@^3.17.3: +zod@3.21.4, zod@^3.17.3: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From c658fa79f6298ff5d25134c4f51bb8eef62ab12e Mon Sep 17 00:00:00 2001 From: Itelo Filho Date: Thu, 13 Jul 2023 16:22:01 -0300 Subject: [PATCH 3/3] poc ready --- apps/example-todo-app/next.config.js | 1 + apps/example-todo-app/src/app/edge/route.ts | 15 + apps/example-todo-app/src/app/main/route.ts | 30 -- .../lib/middlewares/with-auth-token-edge.ts | 6 - .../src/lib/middlewares/with-auth-token.ts | 6 - .../src/lib/middlewares/with-route-spec.ts | 1 + .../todo/add-ignore-invalid-json-response.ts | 2 +- .../api/todo/add-invalid-json-response.ts | 2 +- .../pages/api/todo/delete-common-params.ts | 2 +- .../src/pages/api/todo/form-add.ts | 2 +- .../pages/api/todo/get-no-validate-body.ts | 2 +- .../src/pages/api/todo/get.ts | 4 +- .../src/pages/api/todo/index.ts | 4 +- .../api/todo/json-response-must-be-schema.ts | 2 +- .../src/pages/api/todo/list-optional-ids.ts | 2 +- .../src/pages/api/todo/list-with-refine.ts | 2 +- .../src/pages/api/todo/list.ts | 2 +- .../example-todo-app/tests/api/health.test.ts | 6 +- .../add-ignore-invalid-json-response.test.ts | 2 +- .../todo/add-invalid-json-response.test.ts | 2 +- .../tests/api/todo/add.test.ts | 10 +- .../api/todo/delete-common-params.test.ts | 12 +- .../tests/api/todo/delete.test.ts | 12 +- .../tests/api/todo/form-add.test.ts | 2 +- .../tests/api/todo/get-boolean.test.ts | 6 +- .../api/todo/get-no-validate-body.test.ts | 2 +- .../tests/api/todo/get.test.ts | 10 +- .../tests/api/todo/list-optional-ids.test.ts | 6 +- .../tests/api/todo/list-with-refine.test.ts | 12 +- .../tests/api/todo/list.test.ts | 4 +- apps/example-todo-app/tsconfig.json | 4 + .../generate-openapi/index.ts | 21 +- .../generate-route-types/index.ts | 1 - .../default-map-file-path-to-http-route.ts | 0 .../lib/parse-routes-in-package.ts | 2 +- packages/nextlove/src/edge-helpers.ts | 46 +++ .../src/exceptions-middleware-egde/index.ts | 20 ++ .../with-exception-handling.ts | 59 ++++ .../src/exceptions-middleware-nodejs/index.ts | 32 ++ .../with-exception-handling.ts | 58 ++++ .../with-ok-status.ts | 31 ++ packages/nextlove/src/http-exceptions.ts | 105 ++++++ packages/nextlove/src/index.ts | 15 +- .../{types/edge.ts => types-edge/index.ts} | 58 ++-- packages/nextlove/src/types/index.ts | 12 +- .../edge.ts => with-route-spec-edge/index.ts} | 51 +-- .../with-validation-edge.ts | 53 +-- .../nextlove/src/with-route-spec/index.ts | 4 +- .../middlewares/with-methods.ts | 2 +- .../middlewares/with-validation.ts | 6 +- .../src/with-route-spec/response-edge.ts | 22 -- .../{with-route-spec => }/wrappers-edge.ts | 2 +- packages/nextlove/src/wrappers-nodejs.ts | 327 ++++++++++++++++++ .../middlewares/zod.ts => zod-helpers.ts} | 0 packages/nextlove/tsconfig.json | 2 +- 55 files changed, 866 insertions(+), 238 deletions(-) create mode 100644 apps/example-todo-app/src/app/edge/route.ts delete mode 100644 apps/example-todo-app/src/app/main/route.ts rename packages/nextlove/{old-src => nextlove-generate}/generate-openapi/index.ts (91%) rename packages/nextlove/{old-src => nextlove-generate}/generate-route-types/index.ts (96%) rename packages/nextlove/{old-src => nextlove-generate}/lib/default-map-file-path-to-http-route.ts (100%) rename packages/nextlove/{old-src => nextlove-generate}/lib/parse-routes-in-package.ts (96%) create mode 100644 packages/nextlove/src/edge-helpers.ts create mode 100644 packages/nextlove/src/exceptions-middleware-egde/index.ts create mode 100644 packages/nextlove/src/exceptions-middleware-egde/with-exception-handling.ts create mode 100644 packages/nextlove/src/exceptions-middleware-nodejs/index.ts create mode 100644 packages/nextlove/src/exceptions-middleware-nodejs/with-exception-handling.ts create mode 100644 packages/nextlove/src/exceptions-middleware-nodejs/with-ok-status.ts create mode 100644 packages/nextlove/src/http-exceptions.ts rename packages/nextlove/src/{types/edge.ts => types-edge/index.ts} (73%) rename packages/nextlove/src/{with-route-spec/edge.ts => with-route-spec-edge/index.ts} (61%) rename packages/nextlove/src/{with-route-spec/middlewares => with-route-spec-edge}/with-validation-edge.ts (80%) delete mode 100644 packages/nextlove/src/with-route-spec/response-edge.ts rename packages/nextlove/src/{with-route-spec => }/wrappers-edge.ts (98%) create mode 100644 packages/nextlove/src/wrappers-nodejs.ts rename packages/nextlove/src/{with-route-spec/middlewares/zod.ts => zod-helpers.ts} (100%) diff --git a/apps/example-todo-app/next.config.js b/apps/example-todo-app/next.config.js index 1e9ffaf20..dfbbfa8d2 100755 --- a/apps/example-todo-app/next.config.js +++ b/apps/example-todo-app/next.config.js @@ -7,6 +7,7 @@ const nextConfig = { return { beforeFiles: [ // Nextjs by default requires a /api prefix, let's remove that + // REVIEW: I was not able to make the redirect work // { // source: "/:path*", // destination: "/api/:path*", diff --git a/apps/example-todo-app/src/app/edge/route.ts b/apps/example-todo-app/src/app/edge/route.ts new file mode 100644 index 000000000..fa91fade9 --- /dev/null +++ b/apps/example-todo-app/src/app/edge/route.ts @@ -0,0 +1,15 @@ +import { withRouteSpecEdge } from "@/lib/middlewares" +import { z } from "zod" + +export const runtime = "edge" + +const route_spec = { + jsonResponse: z.object({ + return: z.boolean(), + }), + auth: "none", +} as const + +export const GET = withRouteSpecEdge(route_spec)((req) => { + return req.responseEdge.status(200).json({ return: true }) +}) diff --git a/apps/example-todo-app/src/app/main/route.ts b/apps/example-todo-app/src/app/main/route.ts deleted file mode 100644 index 7610d67bd..000000000 --- a/apps/example-todo-app/src/app/main/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { withRouteSpecEdge } from "@/lib/middlewares" -import {z } from "zod" -export const runtime = 'edge' - -const route_spec = { - methods: ["GET", "POST"], - queryParams: z.object({ - foo: z.array(z.string()) - }), - jsonBody: z.object({ - top: z.boolean() - }), - auth: "none" -} as const - -export const GET = withRouteSpecEdge(route_spec)((req) => { - console.log({ - query: req.edgeQuery, - body: req.edgeBody - }) - return req.responseEdge.status(203).json({ product: [2] }) -}) - -export const POST = withRouteSpecEdge(route_spec)((req) => { - console.log({ - query: req.edgeQuery, - body: req.edgeBody - }) - return req.responseEdge.status(203).json({ product: [2] }) -}) \ No newline at end of file diff --git a/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts b/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts index b3747ad52..8e4674126 100644 --- a/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts +++ b/apps/example-todo-app/src/lib/middlewares/with-auth-token-edge.ts @@ -21,10 +21,4 @@ export const withAuthTokenEdge: MiddlewareEdge<{ return next(req) } -withAuthTokenEdge.securitySchema = { - type: "http", - scheme: "bearer", - bearerFormat: "API Token", -} - export default withAuthTokenEdge diff --git a/apps/example-todo-app/src/lib/middlewares/with-auth-token.ts b/apps/example-todo-app/src/lib/middlewares/with-auth-token.ts index 31e89cc09..e511782a5 100644 --- a/apps/example-todo-app/src/lib/middlewares/with-auth-token.ts +++ b/apps/example-todo-app/src/lib/middlewares/with-auth-token.ts @@ -19,10 +19,4 @@ export const withAuthToken: Middleware<{ return next(req, res) } -withAuthToken.securitySchema = { - type: "http", - scheme: "bearer", - bearerFormat: "API Token", -} - export default withAuthToken diff --git a/apps/example-todo-app/src/lib/middlewares/with-route-spec.ts b/apps/example-todo-app/src/lib/middlewares/with-route-spec.ts index 9594ee070..0e048086b 100644 --- a/apps/example-todo-app/src/lib/middlewares/with-route-spec.ts +++ b/apps/example-todo-app/src/lib/middlewares/with-route-spec.ts @@ -17,6 +17,7 @@ export const withRouteSpecEdge = createWithRouteSpecEdge({ apiName: "TODO API", productionServerUrl: "https://example.com", shouldValidateResponses: true, + addOkStatus: true, } as const) diff --git a/apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts b/apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts index 2ee88af46..b98d1f0d7 100644 --- a/apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts +++ b/apps/example-todo-app/src/pages/api/todo/add-ignore-invalid-json-response.ts @@ -1,7 +1,7 @@ import { withRouteSpecWithoutValidateResponse, checkRouteSpec, -} from "src/lib/middlewares" +} from "@/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" diff --git a/apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts b/apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts index 7c439d89d..a615ac1cf 100644 --- a/apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts +++ b/apps/example-todo-app/src/pages/api/todo/add-invalid-json-response.ts @@ -1,4 +1,4 @@ -import { withRouteSpec } from "src/lib/middlewares" +import { withRouteSpec } from "@/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" diff --git a/apps/example-todo-app/src/pages/api/todo/delete-common-params.ts b/apps/example-todo-app/src/pages/api/todo/delete-common-params.ts index 663510e4d..02f273350 100644 --- a/apps/example-todo-app/src/pages/api/todo/delete-common-params.ts +++ b/apps/example-todo-app/src/pages/api/todo/delete-common-params.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/src/pages/api/todo/form-add.ts b/apps/example-todo-app/src/pages/api/todo/form-add.ts index 9ee4a47e3..a3624f8c5 100644 --- a/apps/example-todo-app/src/pages/api/todo/form-add.ts +++ b/apps/example-todo-app/src/pages/api/todo/form-add.ts @@ -1,4 +1,4 @@ -import { withRouteSpec, checkRouteSpec } from "src/lib/middlewares" +import { withRouteSpec, checkRouteSpec } from "@/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" import { HttpException } from "nextlove" diff --git a/apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts b/apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts index 46e8bc882..48688ea7d 100644 --- a/apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts +++ b/apps/example-todo-app/src/pages/api/todo/get-no-validate-body.ts @@ -1,7 +1,7 @@ import { checkRouteSpec, withRouteSpecWithoutValidateGetRequestBody, -} from "src/lib/middlewares" +} from "@/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" diff --git a/apps/example-todo-app/src/pages/api/todo/get.ts b/apps/example-todo-app/src/pages/api/todo/get.ts index 4a0ed4ded..114dcf4a9 100644 --- a/apps/example-todo-app/src/pages/api/todo/get.ts +++ b/apps/example-todo-app/src/pages/api/todo/get.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" @@ -20,7 +20,7 @@ export const route_spec = checkRouteSpec({ ok: z.boolean(), todo: z.object({ id: z.string().uuid(), - }), + }).optional(), error: z .object({ type: z.string(), diff --git a/apps/example-todo-app/src/pages/api/todo/index.ts b/apps/example-todo-app/src/pages/api/todo/index.ts index 4a0ed4ded..114dcf4a9 100644 --- a/apps/example-todo-app/src/pages/api/todo/index.ts +++ b/apps/example-todo-app/src/pages/api/todo/index.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { NotFoundException } from "nextlove" import { TODO_ID } from "tests/fixtures" import { z } from "zod" @@ -20,7 +20,7 @@ export const route_spec = checkRouteSpec({ ok: z.boolean(), todo: z.object({ id: z.string().uuid(), - }), + }).optional(), error: z .object({ type: z.string(), diff --git a/apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts b/apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts index 8ce4f0459..cec70a6e9 100644 --- a/apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts +++ b/apps/example-todo-app/src/pages/api/todo/json-response-must-be-schema.ts @@ -1,4 +1,4 @@ -import { withRouteSpec } from "src/lib/middlewares" +import { withRouteSpec } from "@/lib/middlewares" import { z } from "zod" import { v4 as uuidv4 } from "uuid" diff --git a/apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts b/apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts index 923b1dd87..8df769953 100644 --- a/apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts +++ b/apps/example-todo-app/src/pages/api/todo/list-optional-ids.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { z } from "zod" export const commonParams = z.object({ diff --git a/apps/example-todo-app/src/pages/api/todo/list-with-refine.ts b/apps/example-todo-app/src/pages/api/todo/list-with-refine.ts index b596ba1a7..9334f9a2c 100644 --- a/apps/example-todo-app/src/pages/api/todo/list-with-refine.ts +++ b/apps/example-todo-app/src/pages/api/todo/list-with-refine.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { z } from "zod" export const commonParams = z diff --git a/apps/example-todo-app/src/pages/api/todo/list.ts b/apps/example-todo-app/src/pages/api/todo/list.ts index 8dce94009..9878aa511 100644 --- a/apps/example-todo-app/src/pages/api/todo/list.ts +++ b/apps/example-todo-app/src/pages/api/todo/list.ts @@ -1,4 +1,4 @@ -import { checkRouteSpec, withRouteSpec } from "src/lib/middlewares" +import { checkRouteSpec, withRouteSpec } from "@/lib/middlewares" import { z } from "zod" export const commonParams = z.object({ diff --git a/apps/example-todo-app/tests/api/health.test.ts b/apps/example-todo-app/tests/api/health.test.ts index 7e6d3647a..cb4532272 100644 --- a/apps/example-todo-app/tests/api/health.test.ts +++ b/apps/example-todo-app/tests/api/health.test.ts @@ -1,9 +1,9 @@ import test from "ava" -import getTestServer from "tests/fixtures/get-test-server" +import getTestServer from "../fixtures/get-test-server" -test("GET /health", async (t) => { +test("GET /api/health", async (t) => { const { axios } = await getTestServer(t) - const res = await axios.get("/health") + const res = await axios.get("/api/health") t.truthy(res.data.ok) }) diff --git a/apps/example-todo-app/tests/api/todo/add-ignore-invalid-json-response.test.ts b/apps/example-todo-app/tests/api/todo/add-ignore-invalid-json-response.test.ts index 68dc44699..d22d683a4 100644 --- a/apps/example-todo-app/tests/api/todo/add-ignore-invalid-json-response.test.ts +++ b/apps/example-todo-app/tests/api/todo/add-ignore-invalid-json-response.test.ts @@ -7,7 +7,7 @@ test("POST /todo/add-ignore-invalid-json-response", async (t) => { axios.defaults.headers.common.Authorization = `Bearer auth_token` const successfulRes = await axios - .post("/todo/add-ignore-invalid-json-response", { title: "Todo Title" }) + .post("/api/todo/add-ignore-invalid-json-response", { title: "Todo Title" }) .catch((err) => err) t.is(successfulRes.status, 200) diff --git a/apps/example-todo-app/tests/api/todo/add-invalid-json-response.test.ts b/apps/example-todo-app/tests/api/todo/add-invalid-json-response.test.ts index 6136e2246..ac8cebc06 100644 --- a/apps/example-todo-app/tests/api/todo/add-invalid-json-response.test.ts +++ b/apps/example-todo-app/tests/api/todo/add-invalid-json-response.test.ts @@ -7,7 +7,7 @@ test("POST /todo/add-invalid-json-response", async (t) => { axios.defaults.headers.common.Authorization = `Bearer auth_token` const successfulRes = await axios - .post("/todo/add-invalid-json-response", { title: "Todo Title" }) + .post("/api/todo/add-invalid-json-response", { title: "Todo Title" }) .catch((err) => err) t.is(successfulRes.status, 500) diff --git a/apps/example-todo-app/tests/api/todo/add.test.ts b/apps/example-todo-app/tests/api/todo/add.test.ts index beaa8ba5d..ca6104154 100644 --- a/apps/example-todo-app/tests/api/todo/add.test.ts +++ b/apps/example-todo-app/tests/api/todo/add.test.ts @@ -4,7 +4,7 @@ import getTestServer from "tests/fixtures/get-test-server" test("POST /todo/add", async (t) => { const { axios } = await getTestServer(t) - const noAuthRes = await axios.post("/todo/add").catch((err) => err) + const noAuthRes = await axios.post("/api/todo/add").catch((err) => err) t.is(noAuthRes.status, 401, "no auth") const hasErrorStack = Boolean(noAuthRes.response.error.stack) @@ -12,21 +12,21 @@ test("POST /todo/add", async (t) => { axios.defaults.headers.common.Authorization = `Bearer auth_token` - const invalidMethodRes = await axios.get("/todo/add").catch((err) => err) + const invalidMethodRes = await axios.get("/api/todo/add").catch((err) => err) t.is(invalidMethodRes.status, 405, "invalid method") const invalidBodyParamTypeRes = await axios - .post("/todo/add", { title: true }) + .post("/api/todo/add", { title: true }) .catch((err) => err) t.is(invalidBodyParamTypeRes.status, 400, "bad body") const nonExistentIdRes = await axios - .post("/todo/add", { invalidParam: "invalidParam" }) + .post("/api/todo/add", { invalidParam: "invalidParam" }) .catch((err) => err) t.is(nonExistentIdRes.status, 400, "invalid param") const successfulRes = await axios - .post("/todo/add", { title: "Todo Title" }) + .post("/api/todo/add", { title: "Todo Title" }) .catch((err) => err) t.is(successfulRes.status, 200) }) diff --git a/apps/example-todo-app/tests/api/todo/delete-common-params.test.ts b/apps/example-todo-app/tests/api/todo/delete-common-params.test.ts index 873fdf592..e4dab809e 100644 --- a/apps/example-todo-app/tests/api/todo/delete-common-params.test.ts +++ b/apps/example-todo-app/tests/api/todo/delete-common-params.test.ts @@ -7,34 +7,34 @@ test("DELETE /todo/delete-common-params", async (t) => { const { axios } = await getTestServer(t) const noAuthRes = await axios - .delete("/todo/delete-common-params") + .delete("/api/todo/delete-common-params") .catch((err) => err) t.is(noAuthRes.status, 401, "no auth") axios.defaults.headers.common.Authorization = `Bearer auth_token` const invalidMethodRes = await axios - .get("/todo/delete-common-params") + .get("/api/todo/delete-common-params") .catch((err) => err) t.is(invalidMethodRes.status, 405, "invalid method") const invalidIdFormatRes = await axios - .delete("/todo/delete-common-params", { data: { id: "someId" } }) + .delete("/api/todo/delete-common-params", { data: { id: "someId" } }) .catch((err) => err) t.is(invalidIdFormatRes.status, 400, "invalid id format") const invalidIdTypeRes = await axios - .delete("/todo/delete-common-params", { data: { id: 123 } }) + .delete("/api/todo/delete-common-params", { data: { id: 123 } }) .catch((err) => err) t.is(invalidIdTypeRes.status, 400, "invalid id type") const nonExistentIdRes = await axios - .delete("/todo/delete-common-params", { data: { id: uuidv4() } }) + .delete("/api/todo/delete-common-params", { data: { id: uuidv4() } }) .catch((err) => err) t.is(nonExistentIdRes.status, 404, "non-existent id") const successfulRes = await axios - .delete("/todo/delete-common-params", { data: { id: TODO_ID } }) + .delete("/api/todo/delete-common-params", { data: { id: TODO_ID } }) .catch((err) => err) t.is(successfulRes.status, 200) }) diff --git a/apps/example-todo-app/tests/api/todo/delete.test.ts b/apps/example-todo-app/tests/api/todo/delete.test.ts index 75c47977e..ee90cf34b 100644 --- a/apps/example-todo-app/tests/api/todo/delete.test.ts +++ b/apps/example-todo-app/tests/api/todo/delete.test.ts @@ -6,31 +6,31 @@ import { v4 as uuidv4 } from "uuid" test("DELETE /todo/delete", async (t) => { const { axios } = await getTestServer(t) - const noAuthRes = await axios.delete("/todo/delete").catch((err) => err) + const noAuthRes = await axios.delete("/api/todo/delete").catch((err) => err) t.is(noAuthRes.status, 401, "no auth") axios.defaults.headers.common.Authorization = `Bearer auth_token` - const invalidMethodRes = await axios.get("/todo/delete").catch((err) => err) + const invalidMethodRes = await axios.get("/api/todo/delete").catch((err) => err) t.is(invalidMethodRes.status, 405, "invalid method") const invalidIdFormatRes = await axios - .delete("/todo/delete", { data: { id: "someId" } }) + .delete("/api/todo/delete", { data: { id: "someId" } }) .catch((err) => err) t.is(invalidIdFormatRes.status, 400, "invalid id format") const invalidIdTypeRes = await axios - .delete("/todo/delete", { data: { id: 123 } }) + .delete("/api/todo/delete", { data: { id: 123 } }) .catch((err) => err) t.is(invalidIdTypeRes.status, 400, "invalid id type") const nonExistentIdRes = await axios - .delete("/todo/delete", { data: { id: uuidv4() } }) + .delete("/api/todo/delete", { data: { id: uuidv4() } }) .catch((err) => err) t.is(nonExistentIdRes.status, 404, "non-existent id") const successfulRes = await axios - .delete("/todo/delete", { data: { id: TODO_ID } }) + .delete("/api/todo/delete", { data: { id: TODO_ID } }) .catch((err) => err) t.is(successfulRes.status, 200) }) diff --git a/apps/example-todo-app/tests/api/todo/form-add.test.ts b/apps/example-todo-app/tests/api/todo/form-add.test.ts index f92a4c0c5..58be8c5f8 100644 --- a/apps/example-todo-app/tests/api/todo/form-add.test.ts +++ b/apps/example-todo-app/tests/api/todo/form-add.test.ts @@ -14,7 +14,7 @@ test("POST /todo/form-add", async (t) => { const successfulRes = await axios({ method: "POST", - url: "/todo/form-add", + url: "/api/todo/form-add", data: bodyFormData, headers: { "Content-Type": "application/x-www-form-urlencoded", diff --git a/apps/example-todo-app/tests/api/todo/get-boolean.test.ts b/apps/example-todo-app/tests/api/todo/get-boolean.test.ts index 977bfc8bd..912d89e1d 100644 --- a/apps/example-todo-app/tests/api/todo/get-boolean.test.ts +++ b/apps/example-todo-app/tests/api/todo/get-boolean.test.ts @@ -4,7 +4,7 @@ import axiosAssert from "tests/fixtures/axios-assert" import getTestServer from "tests/fixtures/get-test-server" import { v4 as uuidv4 } from "uuid" -test.failing("GET /todo/get", async (t) => { +test("GET /todo/get", async (t) => { const { axios } = await getTestServer(t) axios.defaults.headers.common.Authorization = `Bearer auth_token` @@ -12,7 +12,7 @@ test.failing("GET /todo/get", async (t) => { const id = uuidv4() const invalidIdFormatRes = await axios - .get(`/todo/get?id=${id}&throwError=false`) + .get(`/api/todo/get?id=${id}&throwError=false`) .catch((err) => err) t.is(invalidIdFormatRes.status, 200) @@ -23,7 +23,7 @@ test.failing("GET /todo/get", async (t) => { axiosAssert.throws( t, - () => axios.get(`/todo/get?id=${id}&throwErrorAlwaysTrue=false`), + () => axios.get(`/api/todo/get?id=${id}&throwErrorAlwaysTrue=false`), { error: { type: "invalid_input", diff --git a/apps/example-todo-app/tests/api/todo/get-no-validate-body.test.ts b/apps/example-todo-app/tests/api/todo/get-no-validate-body.test.ts index 3d2a6e86a..379c8c962 100644 --- a/apps/example-todo-app/tests/api/todo/get-no-validate-body.test.ts +++ b/apps/example-todo-app/tests/api/todo/get-no-validate-body.test.ts @@ -6,6 +6,6 @@ import { v4 as uuidv4 } from "uuid" test("GET /todo/get", async (t) => { const { axios } = await getTestServer(t) axios.defaults.headers.common.Authorization = `Bearer auth_token` - const successfulRes = await axios.get(`/todo/get-no-validate-body`) + const successfulRes = await axios.get(`/api/todo/get-no-validate-body`) t.is(successfulRes.status, 200) }) diff --git a/apps/example-todo-app/tests/api/todo/get.test.ts b/apps/example-todo-app/tests/api/todo/get.test.ts index faed13dfb..363330e29 100644 --- a/apps/example-todo-app/tests/api/todo/get.test.ts +++ b/apps/example-todo-app/tests/api/todo/get.test.ts @@ -6,27 +6,27 @@ import { v4 as uuidv4 } from "uuid" test("GET /todo/get", async (t) => { const { axios } = await getTestServer(t) - const noAuthRes = await axios.get("/todo/get").catch((err) => err) + const noAuthRes = await axios.get("/api/todo/get").catch((err) => err) t.is(noAuthRes.status, 401) axios.defaults.headers.common.Authorization = `Bearer auth_token` - const invalidMethodRes = await axios.post("/todo/get").catch((err) => err) + const invalidMethodRes = await axios.post("/api/todo/get").catch((err) => err) t.is(invalidMethodRes.status, 405) const invalidIdFormatRes = await axios - .get("/todo/get?id=someId") + .get("/api/todo/get?id=someId") .catch((err) => err) t.is(invalidIdFormatRes.status, 400) const nonExistentIdRes = await axios - .get(`/todo/get?id=${uuidv4()}`) + .get(`/api/todo/get?id=${uuidv4()}`) .catch((err) => err) t.is(nonExistentIdRes.status, 404) // Test 200 response const successfulRes = await axios - .get(`/todo/get?id=${TODO_ID}`) + .get(`/api/todo/get?id=${TODO_ID}`) .catch((err) => err) t.is(successfulRes.status, 200) }) diff --git a/apps/example-todo-app/tests/api/todo/list-optional-ids.test.ts b/apps/example-todo-app/tests/api/todo/list-optional-ids.test.ts index a498e6105..d85682381 100644 --- a/apps/example-todo-app/tests/api/todo/list-optional-ids.test.ts +++ b/apps/example-todo-app/tests/api/todo/list-optional-ids.test.ts @@ -9,7 +9,7 @@ test("GET /todo/list-optional-ids", async (t) => { const ids = [uuidv4(), uuidv4()] - const responseWithArray = await axios.get("/todo/list-optional-ids", { + const responseWithArray = await axios.get("/api/todo/list-optional-ids", { params: { ids, }, @@ -22,7 +22,7 @@ test("GET /todo/list-optional-ids", async (t) => { })), }) - const responseWithCommas = await axios.get("/todo/list-optional-ids", { + const responseWithCommas = await axios.get("/api/todo/list-optional-ids", { params: { ids: ids.join(","), }, @@ -35,7 +35,7 @@ test("GET /todo/list-optional-ids", async (t) => { })), }) - const responseWithOptionalIds = await axios.get("/todo/list-optional-ids") + const responseWithOptionalIds = await axios.get("/api/todo/list-optional-ids") t.deepEqual(responseWithOptionalIds.data, { ok: true, diff --git a/apps/example-todo-app/tests/api/todo/list-with-refine.test.ts b/apps/example-todo-app/tests/api/todo/list-with-refine.test.ts index c9099b0ae..331c61060 100644 --- a/apps/example-todo-app/tests/api/todo/list-with-refine.test.ts +++ b/apps/example-todo-app/tests/api/todo/list-with-refine.test.ts @@ -10,7 +10,7 @@ test("GET /todo/list-with-refine", async (t) => { const ids = [uuidv4(), uuidv4()] - const responseWithArray = await axios.get("/todo/list-with-refine", { + const responseWithArray = await axios.get("/api/todo/list-with-refine", { params: { ids, }, @@ -23,7 +23,7 @@ test("GET /todo/list-with-refine", async (t) => { })), }) - const responseWithCommas = await axios.get("/todo/list-with-refine", { + const responseWithCommas = await axios.get("/api/todo/list-with-refine", { params: { ids: ids.join(","), }, @@ -37,7 +37,7 @@ test("GET /todo/list-with-refine", async (t) => { }) const title = uuidv4() - const responseWithTitle = await axios.get("/todo/list-with-refine", { + const responseWithTitle = await axios.get("/api/todo/list-with-refine", { params: { title, }, @@ -52,7 +52,7 @@ test("GET /todo/list-with-refine", async (t) => { ], }) - await axiosAssert.throws(t, async () => axios.get("/todo/list-with-refine"), { + await axiosAssert.throws(t, async () => axios.get("/api/todo/list-with-refine"), { status: 400, error: { type: "invalid_input", @@ -63,7 +63,7 @@ test("GET /todo/list-with-refine", async (t) => { await axiosAssert.throws( t, async () => - axios.get("/todo/list-with-refine", { + axios.get("/api/todo/list-with-refine", { params: { title: "title", ids: ids.join(","), @@ -81,7 +81,7 @@ test("GET /todo/list-with-refine", async (t) => { await axiosAssert.throws( t, async () => - axios.get("/todo/list-with-refine", { + axios.get("/api/todo/list-with-refine", { params: { title: "A title big enough to test if nextlove is handling correct with nested .refine (from zod) with at least 101 characters long", diff --git a/apps/example-todo-app/tests/api/todo/list.test.ts b/apps/example-todo-app/tests/api/todo/list.test.ts index 2677a727e..6c0c9ad9c 100644 --- a/apps/example-todo-app/tests/api/todo/list.test.ts +++ b/apps/example-todo-app/tests/api/todo/list.test.ts @@ -9,7 +9,7 @@ test("GET /todo/list", async (t) => { const ids = [uuidv4(), uuidv4()] - const responseWithArray = await axios.get("/todo/list", { + const responseWithArray = await axios.get("/api/todo/list", { params: { ids, }, @@ -22,7 +22,7 @@ test("GET /todo/list", async (t) => { })), }) - const responseWithCommas = await axios.get("/todo/list", { + const responseWithCommas = await axios.get("/api/todo/list", { params: { ids: ids.join(","), }, diff --git a/apps/example-todo-app/tsconfig.json b/apps/example-todo-app/tsconfig.json index d4a4df54d..717b988ff 100755 --- a/apps/example-todo-app/tsconfig.json +++ b/apps/example-todo-app/tsconfig.json @@ -19,9 +19,13 @@ "isolatedModules": true, "jsx": "preserve", "strictNullChecks": true, + "baseUrl": ".", "paths": { "@/*": [ "./src/*" + ], + "tests": [ + "./tests/*" ] }, "plugins": [ diff --git a/packages/nextlove/old-src/generate-openapi/index.ts b/packages/nextlove/nextlove-generate/generate-openapi/index.ts similarity index 91% rename from packages/nextlove/old-src/generate-openapi/index.ts rename to packages/nextlove/nextlove-generate/generate-openapi/index.ts index 1345d4442..eb8d45f15 100644 --- a/packages/nextlove/old-src/generate-openapi/index.ts +++ b/packages/nextlove/nextlove-generate/generate-openapi/index.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises" import { generateSchema } from "@anatine/zod-openapi" import { OpenApiBuilder, OperationObject, ParameterObject } from "openapi3-ts" -import { SetupParams } from "../types" +import { SetupParams } from "../../src/types" import chalk from "chalk" import { z } from "zod" import { parseRoutesInPackage } from "../lib/parse-routes-in-package" @@ -85,15 +85,16 @@ export async function generateOpenAPI(opts: GenerateOpenAPIOpts) { const securityObjectsForAuthType = {} for (const authName of Object.keys(globalSetupParams.authMiddlewareMap)) { const mw = globalSetupParams.authMiddlewareMap[authName] - if (mw.securitySchema) { - securitySchemes[authName] = (mw as any).securitySchema - } else { - console.warn( - chalk.yellow( - `Authentication middleware "${authName}" has no securitySchema. You can define this on the function (e.g. after the export do... \n\nmyMiddleware.securitySchema = {\n type: "http"\n scheme: "bearer"\n bearerFormat: "JWT"\n // or API Token etc.\n}\n\nYou can also define "securityObjects" this way, if you want to make the endpoint support multiple modes of authentication.\n\n` - ) - ) - } + // TODO: remove this warning + // if (mw.securitySchema) { + // securitySchemes[authName] = (mw as any).securitySchema + // } else { + // console.warn( + // chalk.yellow( + // `Authentication middleware "${authName}" has no securitySchema. You can define this on the function (e.g. after the export do... \n\nmyMiddleware.securitySchema = {\n type: "http"\n scheme: "bearer"\n bearerFormat: "JWT"\n // or API Token etc.\n}\n\nYou can also define "securityObjects" this way, if you want to make the endpoint support multiple modes of authentication.\n\n` + // ) + // ) + // } securityObjectsForAuthType[authName] = (mw as any).securityObjects || [ { diff --git a/packages/nextlove/old-src/generate-route-types/index.ts b/packages/nextlove/nextlove-generate/generate-route-types/index.ts similarity index 96% rename from packages/nextlove/old-src/generate-route-types/index.ts rename to packages/nextlove/nextlove-generate/generate-route-types/index.ts index b4988c688..8aef8c270 100644 --- a/packages/nextlove/old-src/generate-route-types/index.ts +++ b/packages/nextlove/nextlove-generate/generate-route-types/index.ts @@ -1,5 +1,4 @@ import * as fs from "node:fs/promises" -import { defaultMapFilePathToHTTPRoute } from "../lib/default-map-file-path-to-http-route" import { parseRoutesInPackage } from "../lib/parse-routes-in-package" import { zodToTs, printNode } from "zod-to-ts" import prettier from "prettier" diff --git a/packages/nextlove/old-src/lib/default-map-file-path-to-http-route.ts b/packages/nextlove/nextlove-generate/lib/default-map-file-path-to-http-route.ts similarity index 100% rename from packages/nextlove/old-src/lib/default-map-file-path-to-http-route.ts rename to packages/nextlove/nextlove-generate/lib/default-map-file-path-to-http-route.ts diff --git a/packages/nextlove/old-src/lib/parse-routes-in-package.ts b/packages/nextlove/nextlove-generate/lib/parse-routes-in-package.ts similarity index 96% rename from packages/nextlove/old-src/lib/parse-routes-in-package.ts rename to packages/nextlove/nextlove-generate/lib/parse-routes-in-package.ts index efd45dce5..3af20f191 100644 --- a/packages/nextlove/old-src/lib/parse-routes-in-package.ts +++ b/packages/nextlove/nextlove-generate/lib/parse-routes-in-package.ts @@ -1,7 +1,7 @@ import chalk from "chalk" import path from "node:path" import globby from "globby" -import { RouteSpec, SetupParams } from "../types" +import { RouteSpec, SetupParams } from "../../src/types" import { defaultMapFilePathToHTTPRoute } from "./default-map-file-path-to-http-route" export interface RouteInfo { diff --git a/packages/nextlove/src/edge-helpers.ts b/packages/nextlove/src/edge-helpers.ts new file mode 100644 index 000000000..f98cff22d --- /dev/null +++ b/packages/nextlove/src/edge-helpers.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from "next/server" + +export type NextloveResponse = ReturnType +export type NextloveRequest = NextRequest & { + responseEdge: NextloveResponse +} + +const DEFAULT_STATUS = 200 + +/* + * This is the edge runtime version of the response object. + * It is a wrapper around NextResponse that adds a `status` method + */ +export const getResponse = (req: NextloveRequest, { + addIf, + addOkStatus +}: { + addIf?: (req: NextloveRequest) => boolean + addOkStatus?: boolean +}) => { + const json = (body, params?: ResponseInit) => { + const statusCode = params?.status ?? DEFAULT_STATUS; + const ok = statusCode >= 200 && statusCode < 300; + + const shouldIncludeStatus = addIf && addOkStatus ? addIf(req) : addOkStatus; + + const bodyWithPossibleOk = shouldIncludeStatus ? { ...body, ok } : body; + + return NextResponse.json(bodyWithPossibleOk, params) + } + + + const status = (s: number) => { + return { + statusCode: s, + json: (body, params?: ResponseInit) => json(body, { status: s, ...params }), + status, + } + } + + return { + status, + json, + statusCode: 200, + } +} \ No newline at end of file diff --git a/packages/nextlove/src/exceptions-middleware-egde/index.ts b/packages/nextlove/src/exceptions-middleware-egde/index.ts new file mode 100644 index 000000000..b3c508e62 --- /dev/null +++ b/packages/nextlove/src/exceptions-middleware-egde/index.ts @@ -0,0 +1,20 @@ +import { NextloveRequest } from "../edge-helpers"; +import unwrappedWithExceptionHandlingEdge, { + WithExceptionHandlingEdgeOptions, +} from "./with-exception-handling" + +export interface ExceptionHandlingEdgeOptions { + exceptionHandlingOptions?: WithExceptionHandlingEdgeOptions +} + +export const withExceptionHandlingEdge = + ({ + exceptionHandlingOptions, + }: ExceptionHandlingEdgeOptions = {}) => + (next: (req: NextloveRequest) => Promise) => + (req: NextloveRequest) => { + + return unwrappedWithExceptionHandlingEdge(exceptionHandlingOptions)(next)(req) + } + +export * from "../http-exceptions" diff --git a/packages/nextlove/src/exceptions-middleware-egde/with-exception-handling.ts b/packages/nextlove/src/exceptions-middleware-egde/with-exception-handling.ts new file mode 100644 index 000000000..f8b4be872 --- /dev/null +++ b/packages/nextlove/src/exceptions-middleware-egde/with-exception-handling.ts @@ -0,0 +1,59 @@ +import { NextloveRequest } from "../edge-helpers" +import { HttpException } from "../http-exceptions" + +export interface WithExceptionHandlingEdgeOptions { + getErrorContext?: ( + req: NextloveRequest, + error: Error + ) => Record +} + +const withExceptionHandlingEdge = + (options: WithExceptionHandlingEdgeOptions = {}) => + (next: (req: NextloveRequest) => Promise) => + async (req: NextloveRequest) => { + const res = req.responseEdge + try { + return await next(req) + } catch (error: unknown) { + let errorContext: any = {} + + if (error instanceof Error) { + errorContext.stack = error.stack + } + + errorContext = options.getErrorContext + ? options.getErrorContext(req, errorContext) + : errorContext + + if (error instanceof HttpException) { + if (error.options.json) { + return res.status(error.status).json({ + error: { + ...error.metadata, + ...errorContext, + }, + }) + } else { + // REVIEW: we don't have the .end() method in Edge + return res + .status(error.status) + .json({ error: { message: error.metadata.message } }) + } + } else { + const formattedError = new HttpException(500, { + type: "internal_server_error", + message: error instanceof Error ? error.message : "Unknown error", + }) + + return res.status(500).json({ + error: { + ...formattedError.metadata, + ...errorContext, + }, + }) + } + } + } + +export default withExceptionHandlingEdge diff --git a/packages/nextlove/src/exceptions-middleware-nodejs/index.ts b/packages/nextlove/src/exceptions-middleware-nodejs/index.ts new file mode 100644 index 000000000..07cd25585 --- /dev/null +++ b/packages/nextlove/src/exceptions-middleware-nodejs/index.ts @@ -0,0 +1,32 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import unwrappedWithExceptionHandling, { + WithExceptionHandlingOptions, +} from "./with-exception-handling"; +import withOkStatus, { WithOkStatusOptions } from "./with-ok-status"; + +export interface ExceptionHandlingOptions { + addOkStatus?: boolean; + okStatusOptions?: WithOkStatusOptions; + exceptionHandlingOptions?: WithExceptionHandlingOptions; +} + +export const withExceptionHandling = + ({ + addOkStatus = false, + okStatusOptions, + exceptionHandlingOptions, + }: ExceptionHandlingOptions = {}) => + (next: (req: NextApiRequest, res: NextApiResponse) => Promise) => + (req: NextApiRequest, res: NextApiResponse) => { + if (addOkStatus) { + return withOkStatus(okStatusOptions)( + unwrappedWithExceptionHandling(exceptionHandlingOptions)(next) + )(req, res); + } + + return unwrappedWithExceptionHandling(exceptionHandlingOptions)(next)( + req, + res + ); + }; + diff --git a/packages/nextlove/src/exceptions-middleware-nodejs/with-exception-handling.ts b/packages/nextlove/src/exceptions-middleware-nodejs/with-exception-handling.ts new file mode 100644 index 000000000..931d38a26 --- /dev/null +++ b/packages/nextlove/src/exceptions-middleware-nodejs/with-exception-handling.ts @@ -0,0 +1,58 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { HttpException } from "../http-exceptions"; + +export interface WithExceptionHandlingOptions { + getErrorContext?: ( + req: NextApiRequest, + error: Error + ) => Record; +} + +const withExceptionHandling = + (options: WithExceptionHandlingOptions = {}) => + (next: (req: NextApiRequest, res: NextApiResponse) => Promise) => + async (req: NextApiRequest, res: NextApiResponse) => { + try { + await next(req, res); + } catch (error: unknown) { + let errorContext: any = {}; + + if (error instanceof Error) { + errorContext.stack = error.stack; + } + + errorContext = options.getErrorContext + ? options.getErrorContext(req, errorContext) + : errorContext; + + if (error instanceof HttpException) { + if (error.options.json) { + res.status(error.status).json({ + error: { + ...error.metadata, + ...errorContext, + }, + }); + return; + } else { + res.status(error.status).end(error.metadata.message); + return; + } + } else { + const formattedError = new HttpException(500, { + type: "internal_server_error", + message: error instanceof Error ? error.message : "Unknown error", + }); + + res.status(500).json({ + error: { + ...formattedError.metadata, + ...errorContext, + }, + }); + return; + } + } + }; + +export default withExceptionHandling; diff --git a/packages/nextlove/src/exceptions-middleware-nodejs/with-ok-status.ts b/packages/nextlove/src/exceptions-middleware-nodejs/with-ok-status.ts new file mode 100644 index 000000000..26d450ea2 --- /dev/null +++ b/packages/nextlove/src/exceptions-middleware-nodejs/with-ok-status.ts @@ -0,0 +1,31 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +export interface WithOkStatusOptions { + addIf?: (req: NextApiRequest) => boolean; +} + +const withOkStatus = + (options: WithOkStatusOptions = {}) => + (next: (req: NextApiRequest, res: NextApiResponse) => Promise) => + async (req: NextApiRequest, res: NextApiResponse) => { + // Patch .json() + const originalJson = res.json; + + res.json = function (data) { + const ok = res.statusCode >= 200 && res.statusCode < 300; + const shouldIncludeStatus = options.addIf ? options.addIf(req) : true; + + if (shouldIncludeStatus) { + originalJson.call(this, { + ...data, + ok, + }); + } else { + originalJson.call(this, data); + } + }; + + await next(req, res); + }; + +export default withOkStatus; diff --git a/packages/nextlove/src/http-exceptions.ts b/packages/nextlove/src/http-exceptions.ts new file mode 100644 index 000000000..b14f1e14d --- /dev/null +++ b/packages/nextlove/src/http-exceptions.ts @@ -0,0 +1,105 @@ +export type HttpExceptionMetadata = { + type: string; + message: string; + data?: Record; +} & Record; + +export interface ThrowingOptions { + json?: boolean; +} + +/** + * Throw HttpExceptions inside API endpoints to generate nice error messages + * + * @example + * ``` + * if (bad_soups.includes(soup_param)) { + * throw new HttpException(400, { + * type: "cant_make_soup", + * message: "Soup was too difficult, please specify a different soup" + * }) + * } + * ``` + * + **/ +export class HttpException extends Error { + options: ThrowingOptions; + + constructor( + public status: number, + public metadata: HttpExceptionMetadata, + options?: ThrowingOptions + ) { + super(metadata.message); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.options = options ?? { json: true }; + } + + toString() { + return `HttpException: ${this.status}, ${this.metadata.message} (${this.metadata.type})`; + } +} + +/** + * Throw BadRequestException inside API endpoints that were provided incorrect + * parameters or body + * + * @example + * ``` + * if (bad_soups.includes(soup_param)) { + * throw new BadRequestException({ + * type: "cant_make_soup", + * message: "Soup was too difficult, please specify a different soup", + * }) + * } + * ``` + * + **/ +export class BadRequestException extends HttpException { + constructor( + public metadata: HttpExceptionMetadata, + options?: ThrowingOptions + ) { + super(400, metadata, options); + } +} + +export class UnauthorizedException extends HttpException { + constructor( + public metadata: HttpExceptionMetadata, + options?: ThrowingOptions + ) { + super(401, metadata, options); + } +} + +export class NotFoundException extends HttpException { + constructor( + public metadata: HttpExceptionMetadata, + options?: ThrowingOptions + ) { + super(404, metadata, options); + } +} + +export class MethodNotAllowedException extends HttpException { + constructor( + public metadata: HttpExceptionMetadata, + options?: ThrowingOptions + ) { + super(405, metadata, options); + } +} + +export class InternalServerErrorException extends HttpException { + constructor( + public metadata: HttpExceptionMetadata, + options?: ThrowingOptions + ) { + super(500, metadata, options); + } +} diff --git a/packages/nextlove/src/index.ts b/packages/nextlove/src/index.ts index d36d27626..17fffb11c 100644 --- a/packages/nextlove/src/index.ts +++ b/packages/nextlove/src/index.ts @@ -1,7 +1,10 @@ -export * from "nextjs-exception-middleware" -export * from "./with-route-spec" -export * from "./with-route-spec/edge" -// export { wrappers } from "nextjs-middleware-wrappers" +export * from "./exceptions-middleware-egde" +export * from "./exceptions-middleware-nodejs" export * from "./types" -export * from "./types/edge" -// export * from "./generate-openapi" +export * from "./with-route-spec" +export * from "./with-route-spec-edge" +export * from "./http-exceptions" +export * from "./types-edge" +export * from "./wrappers-edge" +export * from "./wrappers-nodejs" + diff --git a/packages/nextlove/src/types/edge.ts b/packages/nextlove/src/types-edge/index.ts similarity index 73% rename from packages/nextlove/src/types/edge.ts rename to packages/nextlove/src/types-edge/index.ts index 06f18c8dc..61da2f532 100644 --- a/packages/nextlove/src/types/edge.ts +++ b/packages/nextlove/src/types-edge/index.ts @@ -1,24 +1,15 @@ -import { NextApiResponse } from "next" -import { MiddlewareEdge as WrapperMiddlewareEdge } from "../with-route-spec/wrappers-edge" import { z } from "zod" -import { HTTPMethods } from "../with-route-spec/middlewares/with-methods" -import { SecuritySchemeObject, SecurityRequirementObject } from "openapi3-ts" -import { NextloveRequest } from "../with-route-spec/response-edge" +import { SecuritySchemeObject } from "openapi3-ts" +import { NextloveRequest, NextloveResponse } from "../edge-helpers" import { NextResponse } from "next/server" - -export type MiddlewareEdge = WrapperMiddlewareEdge & { - /** - * @deprecated moved to setupParams - */ - securitySchema?: SecuritySchemeObject - securityObjects?: SecurityRequirementObject[] -} +import { MiddlewareEdge } from "../wrappers-edge" type ParamDef = z.ZodTypeAny | z.ZodEffects export interface RouteSpecEdge< Auth extends string = string, - Methods extends HTTPMethods[] = any, + // we don't need the withMethods middleware in Edge + // Methods extends HTTPMethods[] = any, JsonBody extends ParamDef = z.ZodObject, QueryParams extends ParamDef = z.ZodObject, CommonParams extends ParamDef = z.ZodObject, @@ -26,7 +17,8 @@ export interface RouteSpecEdge< JsonResponse extends ParamDef = z.ZodObject, FormData extends ParamDef = z.ZodTypeAny > { - methods: Methods + // we don't need the withMethods middleware in Edge + // methods: Methods auth: Auth jsonBody?: JsonBody queryParams?: QueryParams @@ -66,6 +58,9 @@ export interface SetupParamsEdge< productionServerUrl: string addOkStatus?: boolean + okStatusOptions?: { + addIf?: (req: NextloveRequest) => boolean; + } shouldValidateResponses?: boolean shouldValidateGetRequestBody?: boolean @@ -76,23 +71,23 @@ const defaultMiddlewareMap = { none: (next) => next, } as const -type Send = (body: T) => void -type NextApiResponseWithoutJsonAndStatusMethods = Omit< - NextApiResponse, +type Send = (body: T, params?: ResponseInit) => NextResponse +type NextloveResponseWithoutJsonAndStatusMethods = Omit< + NextloveResponse, "json" | "status" > -type SuccessfulNextApiResponseMethods = { +type SuccessfulNextloveResponseMethods = { status: ( statusCode: 200 | 201 - ) => NextApiResponseWithoutJsonAndStatusMethods & { + ) => NextloveResponseWithoutJsonAndStatusMethods & { json: Send } json: Send } -type ErrorNextApiResponseMethods = { - status: (statusCode: number) => NextApiResponseWithoutJsonAndStatusMethods & { +type ErrorNextloveResponseMethods = { + status: (statusCode: number) => NextloveResponseWithoutJsonAndStatusMethods & { json: Send } json: Send @@ -107,7 +102,7 @@ export type RouteEdgeFunction< infer AuthMWOut, any > - ? Omit & + ? Omit & AuthMWOut & MiddlewareEdgeChainOutput< RS["middlewares"] extends readonly MiddlewareEdge[] @@ -125,15 +120,15 @@ export type RouteEdgeFunction< commonParams: RS["commonParams"] extends z.ZodTypeAny ? z.infer : {} + responseEdge: NextloveResponseWithoutJsonAndStatusMethods & + SuccessfulNextloveResponseMethods< + RS["jsonResponse"] extends z.ZodTypeAny + ? z.infer + : any + > & + ErrorNextloveResponseMethods } : `unknown auth type: ${RS["auth"]}. You should configure this auth type in your auth_middlewares w/ createWithRouteSpec, or maybe you need to add "as const" to your route spec definition.`, - // res: NextApiResponseWithoutJsonAndStatusMethods & - // SuccessfulNextApiResponseMethods< - // RS["jsonResponse"] extends z.ZodTypeAny - // ? z.infer - // : any - // > & - // ErrorNextApiResponseMethods ) => NextResponse | Promise export type CreateWithRouteSpecEdgeFunction = < @@ -143,7 +138,8 @@ export type CreateWithRouteSpecEdgeFunction = < ) => < RS extends RouteSpecEdge< string, - any, + // we don't need the withMethods middleware in Edge + // any, any, any, any, diff --git a/packages/nextlove/src/types/index.ts b/packages/nextlove/src/types/index.ts index 9781ce88b..320c3a6bf 100644 --- a/packages/nextlove/src/types/index.ts +++ b/packages/nextlove/src/types/index.ts @@ -1,16 +1,8 @@ import { NextApiResponse, NextApiRequest } from "next" -import { Middleware as WrapperMiddleware } from "nextjs-middleware-wrappers" import { z } from "zod" import { HTTPMethods } from "../with-route-spec/middlewares/with-methods" -import { SecuritySchemeObject, SecurityRequirementObject } from "openapi3-ts" - -export type Middleware = WrapperMiddleware & { - /** - * @deprecated moved to setupParams - */ - securitySchema?: SecuritySchemeObject - securityObjects?: SecurityRequirementObject[] -} +import { SecuritySchemeObject } from "openapi3-ts" +import { Middleware } from "../wrappers-nodejs" type ParamDef = z.ZodTypeAny | z.ZodEffects diff --git a/packages/nextlove/src/with-route-spec/edge.ts b/packages/nextlove/src/with-route-spec-edge/index.ts similarity index 61% rename from packages/nextlove/src/with-route-spec/edge.ts rename to packages/nextlove/src/with-route-spec-edge/index.ts index e33e7fbdd..21a7460bb 100644 --- a/packages/nextlove/src/with-route-spec/edge.ts +++ b/packages/nextlove/src/with-route-spec-edge/index.ts @@ -1,10 +1,8 @@ -import type { NextApiResponse, NextApiRequest } from "next" -import { NextRequest, NextResponse } from "next/server" -import { wrappersEdge } from "./wrappers-edge" -import { withValidationEdge } from "./middlewares/with-validation-edge" -import { NextloveRequest, getResponseEdge } from "./response-edge" -import { CreateWithRouteSpecEdgeFunction, RouteSpecEdge } from "../types/edge" -import { withExceptionHandling } from "nextjs-exception-middleware" +import { wrappersEdge } from "../wrappers-edge" +import { withValidationEdge } from "./with-validation-edge" +import { NextloveRequest, getResponse } from "../edge-helpers" +import { CreateWithRouteSpecEdgeFunction, RouteSpecEdge } from "../types-edge" +import { withExceptionHandlingEdge } from "../exceptions-middleware-egde" export const createWithRouteSpecEdge: CreateWithRouteSpecEdgeFunction = (( setupParams @@ -14,18 +12,17 @@ export const createWithRouteSpecEdge: CreateWithRouteSpecEdgeFunction = (( globalMiddlewares = [], shouldValidateResponses, shouldValidateGetRequestBody = true, - // exceptionHandlingMiddleware = withExceptionHandling({ - // addOkStatus: setupParams.addOkStatus, - // exceptionHandlingOptions: { - // getErrorContext: (req, error) => { - // if (process.env.NODE_ENV === "production") { - // return {} - // } + exceptionHandlingMiddleware = withExceptionHandlingEdge({ + exceptionHandlingOptions: { + getErrorContext: (req, error) => { + if (process.env.NODE_ENV === "production") { + return {} + } - // return error - // }, - // }, - // }) as any, + return error + }, + }, + }) as any, } = setupParams const withRouteSpec = (spec: RouteSpecEdge) => { @@ -33,23 +30,27 @@ export const createWithRouteSpecEdge: CreateWithRouteSpecEdgeFunction = (( const rootRequestHandler = async ( req: NextloveRequest, ) => { - req.responseEdge = getResponseEdge() + req.responseEdge = getResponse(req, { + addIf: setupParams.okStatusOptions?.addIf, + addOkStatus: setupParams.addOkStatus, + }) authMiddlewareMap["none"] = (next) => next; const auth_middleware = authMiddlewareMap[spec.auth] if (!auth_middleware) throw new Error(`Unknown auth type: ${spec.auth}`) - // return userDefinedRouteFn(req) - return wrappersEdge( - // ...((exceptionHandlingMiddleware - // ? [exceptionHandlingMiddleware] - // : []) as [any]), + return wrappersEdge( + ...((exceptionHandlingMiddleware + ? [exceptionHandlingMiddleware] + : []) as [any]), ...((globalMiddlewares || []) as []), auth_middleware, ...((spec.middlewares || []) as []), + // we don't need the withMethods middleware in Edge // withMethods(spec.methods), - // @ts-ignore withValidationEdge({ jsonBody: spec.jsonBody, queryParams: spec.queryParams, diff --git a/packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts b/packages/nextlove/src/with-route-spec-edge/with-validation-edge.ts similarity index 80% rename from packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts rename to packages/nextlove/src/with-route-spec-edge/with-validation-edge.ts index 2ee3e90e5..fc68549b5 100644 --- a/packages/nextlove/src/with-route-spec/middlewares/with-validation-edge.ts +++ b/packages/nextlove/src/with-route-spec-edge/with-validation-edge.ts @@ -1,12 +1,11 @@ -import type { NextApiRequest, NextApiResponse } from "next" -import { z, ZodFirstPartyTypeKind } from "zod" +import { z } from "zod" import { BadRequestException, InternalServerErrorException, -} from "nextjs-exception-middleware" +} from "../http-exceptions" import { isEmpty } from "lodash" -import { NextloveRequest } from "../response-edge" -import { parseQueryParams, zodIssueToString } from "./zod" +import { NextloveRequest } from "../edge-helpers" +import { parseQueryParams, zodIssueToString } from "../zod-helpers" export interface RequestInput< JsonBody extends z.ZodTypeAny, @@ -24,15 +23,18 @@ export interface RequestInput< shouldValidateGetRequestBody?: boolean } - // NOTE: we should be able to use the same validation logic for both the nodejs and edge runtime function validateJsonResponse( jsonResponse: JsonResponse | undefined, - res: NextloveRequest['responseEdge'] + req: NextloveRequest ) { - const original_res_json = res.json - const override_res_json: NextloveRequest['responseEdge']['json'] = (body, params) => { - const is_success = res.statusCode >= 200 && res.statusCode < 300 + const original_res_json = req.responseEdge.json + const override_res_json: NextloveRequest["responseEdge"]["json"] = ( + body, + params + ) => { + const is_success = + req.responseEdge.statusCode >= 200 && req.responseEdge.statusCode < 300 if (!is_success) { return original_res_json(body, params) } @@ -47,9 +49,10 @@ function validateJsonResponse( }) } - return res.json(body, params) + return original_res_json(body, params) } - res.json = override_res_json + + req.responseEdge.json = override_res_json } export const withValidationEdge = @@ -77,18 +80,18 @@ export const withValidationEdge = throw new Error("Cannot use formData with jsonBody or commonParams") } const { searchParams } = new URL(req.url) - const paramsArray = Array.from(searchParams.entries()); - let queryEdge = Object.fromEntries(paramsArray); - - const isBodyPresent = !!req.body + const paramsArray = Array.from(searchParams.entries()) + let edgeQuery = Object.fromEntries(paramsArray) + const isBodyPresent = !!req.body - let bodyEdge: any + let bodyEdge: any if (isBodyPresent) { bodyEdge = await req.json() } - + const contentType = req.headers.get("content-type") + const isContentTypeJson = contentType?.includes("application/json") const isContentTypeFormUrlEncoded = contentType?.includes( "application/x-www-form-urlencoded" @@ -98,7 +101,7 @@ export const withValidationEdge = (req.method === "POST" || req.method === "PATCH") && (input.jsonBody || input.commonParams) && !isContentTypeJson && - !isEmpty(req.body) + !isEmpty(bodyEdge) ) { throw new BadRequestException({ type: "invalid_content_type", @@ -119,7 +122,7 @@ export const withValidationEdge = } try { - const original_combined_params = { ...queryEdge, ...bodyEdge } + const original_combined_params = { ...edgeQuery, ...bodyEdge } const willValidateRequestBody = input.shouldValidateGetRequestBody ? true @@ -128,15 +131,15 @@ export const withValidationEdge = const isFormData = Boolean(input.formData) if (isFormData && willValidateRequestBody) { - (req as any).edgeBody = input.formData?.parse(bodyEdge) + ;(req as any).edgeBody = input.formData?.parse(bodyEdge) } if (!isFormData && willValidateRequestBody) { - (req as any).edgeBody = input.jsonBody?.parse(bodyEdge) + ;(req as any).edgeBody = input.jsonBody?.parse(bodyEdge) } if (input.queryParams) { - (req as any).edgeQuery = parseQueryParams(input.queryParams, queryEdge) + ;(req as any).edgeQuery = parseQueryParams(input.queryParams, edgeQuery) } if (input.commonParams) { @@ -181,10 +184,8 @@ export const withValidationEdge = * this will override the res.json method to validate the response */ if (input.shouldValidateResponses) { - validateJsonResponse(input.jsonResponse, req.responseEdge) + validateJsonResponse(input.jsonResponse, req) } return next(req) } - - diff --git a/packages/nextlove/src/with-route-spec/index.ts b/packages/nextlove/src/with-route-spec/index.ts index d5ae25d78..b5cd3bc4c 100644 --- a/packages/nextlove/src/with-route-spec/index.ts +++ b/packages/nextlove/src/with-route-spec/index.ts @@ -1,6 +1,6 @@ import { NextApiResponse, NextApiRequest } from "next" -import { withExceptionHandling } from "nextjs-exception-middleware" -import wrappers, { Middleware } from "nextjs-middleware-wrappers" +import { withExceptionHandling } from "../exceptions-middleware-nodejs" +import wrappers, { Middleware } from "../wrappers-nodejs" import { CreateWithRouteSpecFunction, RouteSpec } from "../types" import withMethods, { HTTPMethods } from "./middlewares/with-methods" import withValidation from "./middlewares/with-validation" diff --git a/packages/nextlove/src/with-route-spec/middlewares/with-methods.ts b/packages/nextlove/src/with-route-spec/middlewares/with-methods.ts index 87b26d556..86bf6c64f 100644 --- a/packages/nextlove/src/with-route-spec/middlewares/with-methods.ts +++ b/packages/nextlove/src/with-route-spec/middlewares/with-methods.ts @@ -1,4 +1,4 @@ -import { MethodNotAllowedException } from "nextjs-exception-middleware" +import { MethodNotAllowedException } from "../../http-exceptions" export type HTTPMethods = | "GET" diff --git a/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts b/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts index 48c9432ed..2b15acf66 100644 --- a/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts +++ b/packages/nextlove/src/with-route-spec/middlewares/with-validation.ts @@ -1,11 +1,11 @@ import type { NextApiRequest, NextApiResponse } from "next" -import { z, ZodFirstPartyTypeKind } from "zod" +import { z } from "zod" import { BadRequestException, InternalServerErrorException, -} from "nextjs-exception-middleware" +} from "../../http-exceptions" import { isEmpty } from "lodash" -import { parseQueryParams, zodIssueToString } from "./zod" +import { parseQueryParams, zodIssueToString } from "../../zod-helpers" export interface RequestInput< diff --git a/packages/nextlove/src/with-route-spec/response-edge.ts b/packages/nextlove/src/with-route-spec/response-edge.ts deleted file mode 100644 index ad6da3f70..000000000 --- a/packages/nextlove/src/with-route-spec/response-edge.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextRequest, NextResponse } from "next/server" - -export type NextloveRequest = NextRequest & { - responseEdge: ReturnType -} - -export const getResponseEdge = () => { - const json = (body, params?: ResponseInit) => NextResponse.json(body, params) - const status = (s: number) => { - return { - statusCode: s, - json: (body, params?: ResponseInit) => json(body, { status: s, ...params }), - status, - } - } - - return { - status, - json, - statusCode: 200, - } -} \ No newline at end of file diff --git a/packages/nextlove/src/with-route-spec/wrappers-edge.ts b/packages/nextlove/src/wrappers-edge.ts similarity index 98% rename from packages/nextlove/src/with-route-spec/wrappers-edge.ts rename to packages/nextlove/src/wrappers-edge.ts index 4a6ac9c22..50e57d9b9 100644 --- a/packages/nextlove/src/with-route-spec/wrappers-edge.ts +++ b/packages/nextlove/src/wrappers-edge.ts @@ -58,7 +58,7 @@ export type SaferMiddlewareEgde = ( next: (req: Req & Dep & T) => any ) => (req: Req & Dep) => any -export const extendRequest = >(req: T, merge: K): T & K => { +export const extendRequestEdge = >(req: T, merge: K): T & K => { for (const [key, v] of Object.entries(merge)) { ;(req as any)[key] = v } diff --git a/packages/nextlove/src/wrappers-nodejs.ts b/packages/nextlove/src/wrappers-nodejs.ts new file mode 100644 index 000000000..0f80496fd --- /dev/null +++ b/packages/nextlove/src/wrappers-nodejs.ts @@ -0,0 +1,327 @@ +import type { NextApiRequest as Req, NextApiResponse as Res } from "next" +/* + +Wraps a function in layers of other functions, while preserving the input/output +type. The output of wrappers will always have the type of it's last parameter +(the wrapped function) + +This function turns this type of composition... + +logger.withContext("somecontext")( + async (a, b) => { + return a + } +) + +Into... + +wrappers( + logger.withContext("somecontext"), + async (a, b) => { + return a + } +) + +Having this as a utility method helps preserve types, which otherwise can get +messed up by the middlewares. It also can make the code cleaner where there are +multiple wrappers. + +## EXAMPLES + +In the context of request middleware you might write something like this... + +const withRequestLoggingMiddleware = (next) => async (req, res) => { + console.log(`GOT REQUEST ${req.method} ${req.path}`) + return next(req, res) +} + +Here's an example of a wrapper that takes some parameters... + +const withLoggedArguments = + (logPrefix: string) => + (next) => + async (...funcArgs) => { + console.log(logPrefix, ...funcArgs) + return next(...funcArgs) + } + +*/ + +export type Middleware = ( + next: (req: Req & Dep & T, res: Res) => any +) => (req: Req & Dep & T, res: Res) => any + +// Safer Middleware requires the use of extendRequest to ensure that the +// new context (T) was actually added to the request. It's kind of annoying +// to use in practice, so we don't use it for our Wrappers (yet) +export type SaferMiddleware = ( + next: (req: Req & Dep & T, res: Res) => any +) => (req: Req & Dep, res: Res) => any + +export const extendRequest = >(req: T, merge: K): T & K => { + for (const [key, v] of Object.entries(merge)) { + ;(req as any)[key] = v + } + return req as any +} + +type Wrappers1 = ( + mw1: Middleware, + endpoint: (req: Req & Mw1RequestContext, res: Res) => any +) => (req: Req, res: Res) => any + +type Wrappers2 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep +>( + mw1: Middleware, + mw2: Middleware, + endpoint: (req: Req & Mw1RequestContext & Mw2RequestContext, res: Res) => any +) => (req: Req, res: Res) => any + +// TODO figure out how to do a recursive definition, or one that simplifies +// these redundant wrappers + +type Wrappers3 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep +>( + mw1: Middleware, + mw2: Middleware, + mw3: Middleware< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + endpoint: ( + req: Req & Mw1RequestContext & Mw2RequestContext & Mw3RequestContext, + res: Res + ) => any +) => (req: Req, res: Res) => any + +type Wrappers4 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep +>( + mw1: Middleware, + mw2: Middleware, + mw3: Middleware< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: Middleware< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext, + res: Res + ) => any +) => (req: Req, res: Res) => any + +type Wrappers5 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep, + Mw5RequestContext, + Mw5Dep +>( + mw1: Middleware, + mw2: Middleware, + mw3: Middleware< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: Middleware< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + mw5: Middleware< + Mw5RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext extends Mw5Dep + ? Mw5Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext, + res: Res + ) => any +) => (req: Req, res: Res) => any + +type Wrappers6 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep, + Mw5RequestContext, + Mw5Dep, + Mw6RequestContext, + Mw6Dep +>( + mw1: Middleware, + mw2: Middleware, + mw3: Middleware< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: Middleware< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + mw5: Middleware< + Mw5RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext extends Mw5Dep + ? Mw5Dep + : never + >, + mw6: Middleware< + Mw6RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext extends Mw6Dep + ? Mw6Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext & + Mw6RequestContext, + res: Res + ) => any +) => (req: Req, res: Res) => any + +type Wrappers7 = < + Mw1RequestContext extends Mw2Dep, + Mw1Dep, + Mw2RequestContext, + Mw2Dep, + Mw3RequestContext, + Mw3Dep, + Mw4RequestContext, + Mw4Dep, + Mw5RequestContext, + Mw5Dep, + Mw6RequestContext, + Mw6Dep, + Mw7RequestContext, + Mw7Dep +>( + mw1: Middleware, + mw2: Middleware, + mw3: Middleware< + Mw3RequestContext, + Mw1RequestContext & Mw2RequestContext extends Mw3Dep ? Mw3Dep : never + >, + mw4: Middleware< + Mw4RequestContext, + Mw1RequestContext & Mw2RequestContext & Mw3RequestContext extends Mw4Dep + ? Mw4Dep + : never + >, + mw5: Middleware< + Mw5RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext extends Mw5Dep + ? Mw5Dep + : never + >, + mw6: Middleware< + Mw6RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext extends Mw6Dep + ? Mw6Dep + : never + >, + mw7: Middleware< + Mw7RequestContext, + Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext & + Mw6RequestContext extends Mw7Dep + ? Mw7Dep + : never + >, + endpoint: ( + req: Req & Mw1RequestContext & + Mw2RequestContext & + Mw3RequestContext & + Mw4RequestContext & + Mw5RequestContext & + Mw6RequestContext & + Mw7RequestContext, + res: Res + ) => any +) => (req: Req, res: Res) => any + +type Wrappers = Wrappers1 & + Wrappers2 & + Wrappers3 & + Wrappers4 & + Wrappers5 & + Wrappers6 & + Wrappers7 + +export const wrappers: Wrappers = (...wrappersArgs: any[]) => { + const wrappedFunction = wrappersArgs[wrappersArgs.length - 1] + const mws = wrappersArgs.slice(0, -1) + + let lastWrappedFunction = wrappedFunction + for (let i = mws.length - 1; i >= 0; i--) { + lastWrappedFunction = (mws[i] as any)(lastWrappedFunction) + } + + return lastWrappedFunction +} + +export default wrappers \ No newline at end of file diff --git a/packages/nextlove/src/with-route-spec/middlewares/zod.ts b/packages/nextlove/src/zod-helpers.ts similarity index 100% rename from packages/nextlove/src/with-route-spec/middlewares/zod.ts rename to packages/nextlove/src/zod-helpers.ts diff --git a/packages/nextlove/tsconfig.json b/packages/nextlove/tsconfig.json index 166ce4ae8..3f2f72de3 100755 --- a/packages/nextlove/tsconfig.json +++ b/packages/nextlove/tsconfig.json @@ -17,5 +17,5 @@ "incremental": false }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "dist"] }