feat: add middleware-file-loader (#11638)

Fixes: FRMW-2920

The feature is called `middleware-file-loader`, because it does more than just collecting middlewares. It also collects the bodyParser config for routes
This commit is contained in:
Harminder Virk
2025-02-27 15:12:29 +05:30
committed by GitHub
parent 090d15b1e4
commit b0a16488e0
6 changed files with 229 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/framework": patch
---
feat: add middleware-file-loader

View File

@@ -43,6 +43,12 @@ export default defineMiddlewares([
matcher: "/store/*",
middlewares: [storeGlobal],
},
{
matcher: "/webhooks",
bodyParser: {
preserveRawBody: true,
},
},
{
matcher: "/webhooks/*",
method: "POST",

View File

@@ -0,0 +1,51 @@
import { resolve } from "path"
import { MiddlewareFileLoader } from "../middleware-file-loader"
describe("Middleware file loader", () => {
it("should load routes from the filesystem", async () => {
const BASE_DIR = resolve(__dirname, "../__fixtures__/routers-middleware")
const loader = new MiddlewareFileLoader({})
await loader.scanDir(BASE_DIR)
expect(loader.getBodyParserConfigRoutes()).toMatchInlineSnapshot(`
[
{
"config": {
"preserveRawBody": true,
},
"matcher": "/webhooks",
"method": undefined,
},
{
"config": false,
"matcher": "/webhooks/*",
"method": "POST",
},
]
`)
expect(loader.getMiddlewares()).toMatchInlineSnapshot(`
[
{
"handler": [Function],
"matcher": "/customers",
"method": undefined,
},
{
"handler": [Function],
"matcher": "/customers",
"method": "POST",
},
{
"handler": [Function],
"matcher": "/store/*",
"method": undefined,
},
{
"handler": [Function],
"matcher": "/webhooks/*",
"method": "POST",
},
]
`)
})
})

View File

@@ -0,0 +1,154 @@
import { join } from "path"
import { dynamicImport, FileSystem } from "@medusajs/utils"
import { logger } from "../logger"
import type {
MiddlewaresConfig,
BodyParserConfigRoute,
ScannedMiddlewareDescriptor,
} from "./types"
/**
* File name that is used to indicate that the file is a middleware file
*/
const MIDDLEWARE_FILE_NAME = "middlewares"
const log = ({
activityId,
message,
}: {
activityId?: string
message: string
}) => {
if (activityId) {
logger.progress(activityId, message)
return
}
logger.debug(message)
}
/**
* Exposes the API to scan a directory and load the `middleware.ts` file. This file contains
* the configuration for certain global middlewares and core routes validators. Also, it may
* contain custom middlewares.
*/
export class MiddlewareFileLoader {
/**
* Middleware collected manually or by scanning directories
*/
#middleware: ScannedMiddlewareDescriptor[] = []
#bodyParserConfigRoutes: BodyParserConfigRoute[] = []
/**
* An eventual activity id for information tracking
*/
readonly #activityId?: string
constructor({ activityId }: { activityId?: string }) {
this.#activityId = activityId
}
/**
* Processes the middleware file and returns the middleware and the
* routes config exported by it.
*/
async #processMiddlewareFile(absolutePath: string): Promise<void> {
const middlewareExports = await dynamicImport(absolutePath)
const middlewareConfig = middlewareExports.default
if (!middlewareConfig) {
log({
activityId: this.#activityId,
message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`,
})
return
}
const routes = middlewareConfig.routes as MiddlewaresConfig["routes"]
if (!routes || !Array.isArray(routes)) {
log({
activityId: this.#activityId,
message: `Invalid default export found in ${absolutePath}. Make sure to use "defineMiddlewares" function and export its output.`,
})
return
}
const result = routes.reduce<{
bodyParserConfigRoutes: BodyParserConfigRoute[]
middleware: ScannedMiddlewareDescriptor[]
}>(
(result, route) => {
if (!route.matcher) {
throw new Error(
`Middleware is missing a \`matcher\` field. The 'matcher' field is required when applying middleware. ${JSON.stringify(
route,
null,
2
)}`
)
}
const matcher = String(route.matcher)
if ("bodyParser" in route && route.bodyParser !== undefined) {
result.bodyParserConfigRoutes.push({
matcher: matcher,
method: route.method,
config: route.bodyParser,
})
}
if (route.middlewares) {
route.middlewares.forEach((middleware) => {
result.middleware.push({
handler: middleware,
matcher: matcher,
method: route.method,
})
})
}
return result
},
{
bodyParserConfigRoutes: [],
middleware: [],
}
)
this.#middleware = result.middleware
this.#bodyParserConfigRoutes = result.bodyParserConfigRoutes
}
/**
* Scans a given directory for the "middleware.ts" or "middleware.js" files and
* imports them for reading the registered middleware and configuration for
* existing routes/middleware.
*/
async scanDir(sourceDir: string) {
const fs = new FileSystem(sourceDir)
if (await fs.exists(`${MIDDLEWARE_FILE_NAME}.ts`)) {
await this.#processMiddlewareFile(
join(sourceDir, `${MIDDLEWARE_FILE_NAME}.ts`)
)
} else if (await fs.exists(`${MIDDLEWARE_FILE_NAME}.js`)) {
await this.#processMiddlewareFile(
join(sourceDir, `${MIDDLEWARE_FILE_NAME}.ts`)
)
}
}
/**
* Returns a collection of registered middleware
*/
getMiddlewares() {
return this.#middleware
}
/**
* Returns routes that have bodyparser config on them
*/
getBodyParserConfigRoutes() {
return this.#bodyParserConfigRoutes
}
}

View File

@@ -25,7 +25,7 @@ type Route = {
/**
* The HTTP methods this route is supposed to handle.
*/
methods?: MiddlewareVerb[]
methods?: MiddlewareVerb | MiddlewareVerb[]
}
/**
@@ -108,7 +108,6 @@ export class RoutesSorter {
constructor(routes: Route[]) {
this.#routesToProcess = routes
console.log("Processing routes", this.#routesToProcess)
}
/**

View File

@@ -111,6 +111,18 @@ export type FileSystemRouteDescriptor = ScannedRouteDescriptor & {
relativePath: string
}
export type ScannedMiddlewareDescriptor = {
matcher: string
method?: MiddlewareVerb | MiddlewareVerb[]
handler: MiddlewareFunction
}
export type BodyParserConfigRoute = {
matcher: string
method?: MiddlewareVerb | MiddlewareVerb[]
config?: ParserConfig
}
export type GlobalMiddlewareDescriptor = {
config?: MiddlewaresConfig
}