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:
5
.changeset/grumpy-pumas-occur.md
Normal file
5
.changeset/grumpy-pumas-occur.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/framework": patch
|
||||
---
|
||||
|
||||
feat: add middleware-file-loader
|
||||
@@ -43,6 +43,12 @@ export default defineMiddlewares([
|
||||
matcher: "/store/*",
|
||||
middlewares: [storeGlobal],
|
||||
},
|
||||
{
|
||||
matcher: "/webhooks",
|
||||
bodyParser: {
|
||||
preserveRawBody: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
matcher: "/webhooks/*",
|
||||
method: "POST",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
})
|
||||
154
packages/core/framework/src/http/middleware-file-loader.ts
Normal file
154
packages/core/framework/src/http/middleware-file-loader.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user