Skip to content

Commit b0a1648

Browse files
authored
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
1 parent 090d15b commit b0a1648

File tree

6 files changed

+229
-2
lines changed

6 files changed

+229
-2
lines changed

.changeset/grumpy-pumas-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/framework": patch
3+
---
4+
5+
feat: add middleware-file-loader

packages/core/framework/src/http/__fixtures__/routers-middleware/middlewares.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ export default defineMiddlewares([
4343
matcher: "/store/*",
4444
middlewares: [storeGlobal],
4545
},
46+
{
47+
matcher: "/webhooks",
48+
bodyParser: {
49+
preserveRawBody: true,
50+
},
51+
},
4652
{
4753
matcher: "/webhooks/*",
4854
method: "POST",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { resolve } from "path"
2+
import { MiddlewareFileLoader } from "../middleware-file-loader"
3+
4+
describe("Middleware file loader", () => {
5+
it("should load routes from the filesystem", async () => {
6+
const BASE_DIR = resolve(__dirname, "../__fixtures__/routers-middleware")
7+
const loader = new MiddlewareFileLoader({})
8+
await loader.scanDir(BASE_DIR)
9+
10+
expect(loader.getBodyParserConfigRoutes()).toMatchInlineSnapshot(`
11+
[
12+
{
13+
"config": {
14+
"preserveRawBody": true,
15+
},
16+
"matcher": "/webhooks",
17+
"method": undefined,
18+
},
19+
{
20+
"config": false,
21+
"matcher": "/webhooks/*",
22+
"method": "POST",
23+
},
24+
]
25+
`)
26+
expect(loader.getMiddlewares()).toMatchInlineSnapshot(`
27+
[
28+
{
29+
"handler": [Function],
30+
"matcher": "/customers",
31+
"method": undefined,
32+
},
33+
{
34+
"handler": [Function],
35+
"matcher": "/customers",
36+
"method": "POST",
37+
},
38+
{
39+
"handler": [Function],
40+
"matcher": "/store/*",
41+
"method": undefined,
42+
},
43+
{
44+
"handler": [Function],
45+
"matcher": "/webhooks/*",
46+
"method": "POST",
47+
},
48+
]
49+
`)
50+
})
51+
})
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { join } from "path"
2+
import { dynamicImport, FileSystem } from "@medusajs/utils"
3+
4+
import { logger } from "../logger"
5+
import type {
6+
MiddlewaresConfig,
7+
BodyParserConfigRoute,
8+
ScannedMiddlewareDescriptor,
9+
} from "./types"
10+
11+
/**
12+
* File name that is used to indicate that the file is a middleware file
13+
*/
14+
const MIDDLEWARE_FILE_NAME = "middlewares"
15+
16+
const log = ({
17+
activityId,
18+
message,
19+
}: {
20+
activityId?: string
21+
message: string
22+
}) => {
23+
if (activityId) {
24+
logger.progress(activityId, message)
25+
return
26+
}
27+
28+
logger.debug(message)
29+
}
30+
31+
/**
32+
* Exposes the API to scan a directory and load the `middleware.ts` file. This file contains
33+
* the configuration for certain global middlewares and core routes validators. Also, it may
34+
* contain custom middlewares.
35+
*/
36+
export class MiddlewareFileLoader {
37+
/**
38+
* Middleware collected manually or by scanning directories
39+
*/
40+
#middleware: ScannedMiddlewareDescriptor[] = []
41+
#bodyParserConfigRoutes: BodyParserConfigRoute[] = []
42+
43+
/**
44+
* An eventual activity id for information tracking
45+
*/
46+
readonly #activityId?: string
47+
48+
constructor({ activityId }: { activityId?: string }) {
49+
this.#activityId = activityId
50+
}
51+
52+
/**
53+
* Processes the middleware file and returns the middleware and the
54+
* routes config exported by it.
55+
*/
56+
async #processMiddlewareFile(absolutePath: string): Promise<void> {
57+
const middlewareExports = await dynamicImport(absolutePath)
58+
59+
const middlewareConfig = middlewareExports.default
60+
if (!middlewareConfig) {
61+
log({
62+
activityId: this.#activityId,
63+
message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`,
64+
})
65+
return
66+
}
67+
68+
const routes = middlewareConfig.routes as MiddlewaresConfig["routes"]
69+
if (!routes || !Array.isArray(routes)) {
70+
log({
71+
activityId: this.#activityId,
72+
message: `Invalid default export found in ${absolutePath}. Make sure to use "defineMiddlewares" function and export its output.`,
73+
})
74+
return
75+
}
76+
77+
const result = routes.reduce<{
78+
bodyParserConfigRoutes: BodyParserConfigRoute[]
79+
middleware: ScannedMiddlewareDescriptor[]
80+
}>(
81+
(result, route) => {
82+
if (!route.matcher) {
83+
throw new Error(
84+
`Middleware is missing a \`matcher\` field. The 'matcher' field is required when applying middleware. ${JSON.stringify(
85+
route,
86+
null,
87+
2
88+
)}`
89+
)
90+
}
91+
92+
const matcher = String(route.matcher)
93+
94+
if ("bodyParser" in route && route.bodyParser !== undefined) {
95+
result.bodyParserConfigRoutes.push({
96+
matcher: matcher,
97+
method: route.method,
98+
config: route.bodyParser,
99+
})
100+
}
101+
102+
if (route.middlewares) {
103+
route.middlewares.forEach((middleware) => {
104+
result.middleware.push({
105+
handler: middleware,
106+
matcher: matcher,
107+
method: route.method,
108+
})
109+
})
110+
}
111+
return result
112+
},
113+
{
114+
bodyParserConfigRoutes: [],
115+
middleware: [],
116+
}
117+
)
118+
119+
this.#middleware = result.middleware
120+
this.#bodyParserConfigRoutes = result.bodyParserConfigRoutes
121+
}
122+
123+
/**
124+
* Scans a given directory for the "middleware.ts" or "middleware.js" files and
125+
* imports them for reading the registered middleware and configuration for
126+
* existing routes/middleware.
127+
*/
128+
async scanDir(sourceDir: string) {
129+
const fs = new FileSystem(sourceDir)
130+
if (await fs.exists(`${MIDDLEWARE_FILE_NAME}.ts`)) {
131+
await this.#processMiddlewareFile(
132+
join(sourceDir, `${MIDDLEWARE_FILE_NAME}.ts`)
133+
)
134+
} else if (await fs.exists(`${MIDDLEWARE_FILE_NAME}.js`)) {
135+
await this.#processMiddlewareFile(
136+
join(sourceDir, `${MIDDLEWARE_FILE_NAME}.ts`)
137+
)
138+
}
139+
}
140+
141+
/**
142+
* Returns a collection of registered middleware
143+
*/
144+
getMiddlewares() {
145+
return this.#middleware
146+
}
147+
148+
/**
149+
* Returns routes that have bodyparser config on them
150+
*/
151+
getBodyParserConfigRoutes() {
152+
return this.#bodyParserConfigRoutes
153+
}
154+
}

packages/core/framework/src/http/routes-sorter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type Route = {
2525
/**
2626
* The HTTP methods this route is supposed to handle.
2727
*/
28-
methods?: MiddlewareVerb[]
28+
methods?: MiddlewareVerb | MiddlewareVerb[]
2929
}
3030

3131
/**
@@ -108,7 +108,6 @@ export class RoutesSorter {
108108

109109
constructor(routes: Route[]) {
110110
this.#routesToProcess = routes
111-
console.log("Processing routes", this.#routesToProcess)
112111
}
113112

114113
/**

packages/core/framework/src/http/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ export type FileSystemRouteDescriptor = ScannedRouteDescriptor & {
111111
relativePath: string
112112
}
113113

114+
export type ScannedMiddlewareDescriptor = {
115+
matcher: string
116+
method?: MiddlewareVerb | MiddlewareVerb[]
117+
handler: MiddlewareFunction
118+
}
119+
120+
export type BodyParserConfigRoute = {
121+
matcher: string
122+
method?: MiddlewareVerb | MiddlewareVerb[]
123+
config?: ParserConfig
124+
}
125+
114126
export type GlobalMiddlewareDescriptor = {
115127
config?: MiddlewaresConfig
116128
}

0 commit comments

Comments
 (0)