diff --git a/apps/example-todo-app/lib/middlewares/index.ts b/apps/example-todo-app/lib/middlewares/index.ts index 25023eb1f..24512beaf 100644 --- a/apps/example-todo-app/lib/middlewares/index.ts +++ b/apps/example-todo-app/lib/middlewares/index.ts @@ -1,2 +1,3 @@ export * from "./with-auth-token" export * from "./with-route-spec" +export * from "./with-global-middeware-after-auth" diff --git a/apps/example-todo-app/lib/middlewares/with-global-middeware-after-auth.ts b/apps/example-todo-app/lib/middlewares/with-global-middeware-after-auth.ts new file mode 100644 index 000000000..6d22e1be2 --- /dev/null +++ b/apps/example-todo-app/lib/middlewares/with-global-middeware-after-auth.ts @@ -0,0 +1,17 @@ +import { Middleware } from "nextlove" + +export const withGlobalMiddlewareAfterAuth: Middleware<{ + auth: { + authorized_by: "auth_token" + seam: "withGlobalMiddlewareAfterAuth" + } +}> = (next) => async (req, res) => { + req.auth = { + ...req.auth, + seam: "withGlobalMiddlewareAfterAuth", + } + + return next(req, res) +} + +export default withGlobalMiddlewareAfterAuth diff --git a/apps/example-todo-app/lib/middlewares/with-route-spec.ts b/apps/example-todo-app/lib/middlewares/with-route-spec.ts index dbe4e7dbd..6e7339783 100644 --- a/apps/example-todo-app/lib/middlewares/with-route-spec.ts +++ b/apps/example-todo-app/lib/middlewares/with-route-spec.ts @@ -1,7 +1,8 @@ -import { createWithRouteSpec, QueryArrayFormats } from "nextlove" +import { createWithRouteSpec, Middleware, QueryArrayFormats } from "nextlove" import { withAuthToken } from "./with-auth-token" export { checkRouteSpec } from "nextlove" import * as ZT from "lib/zod" +import withGlobalMiddlewareAfterAuth from "./with-global-middeware-after-auth" const defaultRouteSpec = { authMiddlewareMap: { auth_token: withAuthToken }, @@ -17,6 +18,11 @@ const defaultRouteSpec = { export const withRouteSpec = createWithRouteSpec(defaultRouteSpec) +export const withRouteSpecWithGlobalMiddlewareAfterAuth = createWithRouteSpec({ + globalMiddlewaresAfterAuth: [withGlobalMiddlewareAfterAuth], + ...defaultRouteSpec, +} as const) + export const withRouteSpecSupportedArrayFormats = ( supportedArrayFormats: QueryArrayFormats ) => diff --git a/apps/example-todo-app/pages/api/todo/add-with-global-middeware-after-auth.ts b/apps/example-todo-app/pages/api/todo/add-with-global-middeware-after-auth.ts new file mode 100644 index 000000000..7d1348176 --- /dev/null +++ b/apps/example-todo-app/pages/api/todo/add-with-global-middeware-after-auth.ts @@ -0,0 +1,31 @@ +import { + checkRouteSpec, + withRouteSpecWithGlobalMiddlewareAfterAuth, +} from "lib/middlewares" +import { z } from "zod" +import { v4 as uuidv4 } from "uuid" + +export const jsonBody = z.object({ + id: z.string().uuid().optional().default(uuidv4()), + title: z.string(), + completed: z.boolean().optional().default(false), +}) + +export const route_spec = checkRouteSpec({ + methods: ["POST"], + auth: "auth_token", + jsonBody, + jsonResponse: z.object({ + ok: z.boolean(), + auth: z.object({ + authorized_by: z.literal("auth_token"), + seam: z.literal("withGlobalMiddlewareAfterAuth"), + }), + }), +}) + +export default withRouteSpecWithGlobalMiddlewareAfterAuth(route_spec)( + async (req, res) => { + return res.status(200).json({ ok: true, auth: req.auth }) + } +) diff --git a/apps/example-todo-app/tests/api/todo/add-with-global-middeware-after-auth.test.ts b/apps/example-todo-app/tests/api/todo/add-with-global-middeware-after-auth.test.ts new file mode 100644 index 000000000..6f3d34296 --- /dev/null +++ b/apps/example-todo-app/tests/api/todo/add-with-global-middeware-after-auth.test.ts @@ -0,0 +1,16 @@ +import test from "ava" +import getTestServer from "tests/fixtures/get-test-server" + +test("POST /todo/add-with-global-middeware-after-auth", async (t) => { + const { axios } = await getTestServer(t) + + axios.defaults.headers.common.Authorization = `Bearer auth_token` + + const successfulRes = await axios + .post("/todo/add-with-global-middeware-after-auth", { title: "Todo Title" }) + .catch((err) => err) + t.is(successfulRes.status, 200) + t.is(successfulRes.data.ok, true) + t.is(successfulRes.data.auth.authorized_by, "auth_token") + t.is(successfulRes.data.auth.seam, "withGlobalMiddlewareAfterAuth") +}) diff --git a/packages/nextlove/src/types/index.ts b/packages/nextlove/src/types/index.ts index 2987c9ed4..e51fd5c69 100644 --- a/packages/nextlove/src/types/index.ts +++ b/packages/nextlove/src/types/index.ts @@ -64,10 +64,12 @@ export type QueryArrayFormats = readonly QueryArrayFormat[] export interface SetupParams< AuthMW extends AuthMiddlewares = AuthMiddlewares, - GlobalMW extends Middleware[] = any[] + GlobalMW extends Middleware[] = any[], + GlobalMWAfterAuth extends Middleware[] = any[] > { authMiddlewareMap: AuthMW globalMiddlewares: GlobalMW + globalMiddlewaresAfterAuth?: GlobalMWAfterAuth exceptionHandlingMiddleware?: ((next: Function) => Function) | null // These improve OpenAPI generation @@ -110,10 +112,7 @@ type ErrorNextApiResponseMethods = { json: Send } -export type RouteFunction< - SP extends SetupParams, - RS extends RouteSpec -> = ( +export type RouteFunction = ( req: (SP["authMiddlewareMap"] & typeof defaultMiddlewareMap)[RS["auth"]] extends Middleware< infer AuthMWOut, @@ -121,6 +120,14 @@ export type RouteFunction< > ? Omit & AuthMWOut & + MiddlewareChainOutput< + SP["globalMiddlewaresAfterAuth"] extends readonly Middleware< + any, + any + >[] + ? SP["globalMiddlewaresAfterAuth"] + : [] + > & MiddlewareChainOutput< RS["middlewares"] extends readonly Middleware[] ? [...SP["globalMiddlewares"], ...RS["middlewares"]] @@ -149,7 +156,7 @@ export type RouteFunction< ) => Promise export type CreateWithRouteSpecFunction = < - SP extends SetupParams + SP extends SetupParams >( setupParams: SP ) => >( diff --git a/packages/nextlove/src/with-route-spec/index.ts b/packages/nextlove/src/with-route-spec/index.ts index 76fc2b59e..2b31f24e0 100644 --- a/packages/nextlove/src/with-route-spec/index.ts +++ b/packages/nextlove/src/with-route-spec/index.ts @@ -58,6 +58,7 @@ export const createWithRouteSpec: CreateWithRouteSpecFunction = (( const { authMiddlewareMap = {}, globalMiddlewares = [], + globalMiddlewaresAfterAuth = [], shouldValidateResponses, shouldValidateGetRequestBody = true, exceptionHandlingMiddleware = withExceptionHandling({ @@ -97,6 +98,7 @@ export const createWithRouteSpec: CreateWithRouteSpecFunction = (( : []) as [any]), ...((globalMiddlewares || []) as []), auth_middleware, + ...((globalMiddlewaresAfterAuth || []) as []), ...((spec.middlewares || []) as []), withMethods(spec.methods), withValidation({ diff --git a/packages/nextlove/tests/route-spec-types.ts b/packages/nextlove/tests/route-spec-types.ts index de1516e72..f087a656b 100644 --- a/packages/nextlove/tests/route-spec-types.ts +++ b/packages/nextlove/tests/route-spec-types.ts @@ -1,10 +1,5 @@ import { MiddlewareChainOutput } from "../src/types" -import { - checkRouteSpec, - createWithRouteSpec, - Middleware, - RouteSpec, -} from "../src" +import { checkRouteSpec, createWithRouteSpec, Middleware } from "../src" import { expectTypeOf } from "expect-type" import { z } from "zod" @@ -227,3 +222,48 @@ export const myRoute9 = withRouteSpec(myRoute9Spec)(async (req, res) => { const errorApiResponse400 = res.status(400) expectTypeOf(errorApiResponse400.json).toMatchTypeOf<(body: any) => void>() }) + +export const myRoute10 = createWithRouteSpec({ + authMiddlewareMap: {}, + globalMiddlewaresAfterAuth: [ + null as any as Middleware<{ + good: true + }>, + ], + apiName: "", + globalMiddlewares: [], + productionServerUrl: "", +} as const)({ + auth: "none", + methods: ["GET"], +} as const)(async (req, res) => { + expectTypeOf(req.good).toMatchTypeOf() +}) + +export const myRoute11 = createWithRouteSpec({ + authMiddlewareMap: { + // seam: (next) => (req, res) => {}, + }, + globalMiddlewaresAfterAuth: [ + null as any as Middleware<{ + auth: { + authorized_by: "auth_token" + seam: "withGlobalMiddlewareAfterAuth" + } + }>, + ], + apiName: "", + globalMiddlewares: [ + ((next) => async (req, res) => { + next(req, res) + }) as any as Middleware<{ + gmw: true + }>, + ], + productionServerUrl: "", +} as const)({ + auth: "none", + methods: ["GET"], +} as const)(async (req, res) => { + expectTypeOf(req.auth.seam).toMatchTypeOf<"withGlobalMiddlewareAfterAuth">() +})