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:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/framework": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add middleware-file-loader
|
||||||
@@ -43,6 +43,12 @@ export default defineMiddlewares([
|
|||||||
matcher: "/store/*",
|
matcher: "/store/*",
|
||||||
middlewares: [storeGlobal],
|
middlewares: [storeGlobal],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
matcher: "/webhooks",
|
||||||
|
bodyParser: {
|
||||||
|
preserveRawBody: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
matcher: "/webhooks/*",
|
matcher: "/webhooks/*",
|
||||||
method: "POST",
|
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",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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.
|
* The HTTP methods this route is supposed to handle.
|
||||||
*/
|
*/
|
||||||
methods?: MiddlewareVerb[]
|
methods?: MiddlewareVerb | MiddlewareVerb[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,7 +108,6 @@ export class RoutesSorter {
|
|||||||
|
|
||||||
constructor(routes: Route[]) {
|
constructor(routes: Route[]) {
|
||||||
this.#routesToProcess = routes
|
this.#routesToProcess = routes
|
||||||
console.log("Processing routes", this.#routesToProcess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ export type FileSystemRouteDescriptor = ScannedRouteDescriptor & {
|
|||||||
relativePath: string
|
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 = {
|
export type GlobalMiddlewareDescriptor = {
|
||||||
config?: MiddlewaresConfig
|
config?: MiddlewaresConfig
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user