chore(framework): Move and improve routes loader (#8392)
* chore(framework): Move and improve routes loader * cleanup * fix(framework): import
This commit is contained in:
1 parent
4081b3359d
commit
f81652bf6e
102 files changed
+925
-802
No files matched your search
@@ -1,8 +1,7 @@
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { Express } from "express"
|
||||
import path from "path"
|
||||
import { join } from "path"
|
||||
import qs from "qs"
|
||||
import { RoutesLoader } from "./helpers/routing"
|
||||
import { RoutesLoader } from "@medusajs/framework"
|
||||
import { MedusaContainer, PluginDetails } from "@medusajs/types"
|
||||
|
||||
type Options = {
|
||||
@@ -24,9 +23,7 @@ export default async ({ app, container, plugins }: Options) => {
|
||||
next()
|
||||
})
|
||||
|
||||
const configModule = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
const sourcePaths: string[] = []
|
||||
|
||||
/**
|
||||
* Always load plugin routes before the Medusa core routes, since it
|
||||
@@ -40,31 +37,27 @@ export default async ({ app, container, plugins }: Options) => {
|
||||
* route will never be resolved, because it will be handled by the
|
||||
* "/products/:id" route.
|
||||
*/
|
||||
await Promise.all(
|
||||
plugins.map(async (pluginDetails) => {
|
||||
return new RoutesLoader({
|
||||
app: app,
|
||||
configModule,
|
||||
rootDir: path.join(pluginDetails.resolve, "api"),
|
||||
}).load()
|
||||
})
|
||||
sourcePaths.push(
|
||||
...plugins.map((pluginDetails) => {
|
||||
return join(pluginDetails.resolve, "api")
|
||||
}),
|
||||
/**
|
||||
* Register the Medusa CORE API routes using the file based routing.
|
||||
*/
|
||||
join(__dirname, "../api")
|
||||
)
|
||||
|
||||
// TODO: Figure out why this is causing issues with test when placed inside ./api.ts
|
||||
// Adding this here temporarily
|
||||
// Test: (packages/medusa/src/api/routes/admin/currencies/update-currency.ts)
|
||||
try {
|
||||
/**
|
||||
* Register the Medusa CORE API routes using the file based routing.
|
||||
*/
|
||||
await new RoutesLoader({
|
||||
app: app,
|
||||
rootDir: path.join(__dirname, "../api"),
|
||||
configModule,
|
||||
sourceDir: sourcePaths,
|
||||
}).load()
|
||||
} catch (err) {
|
||||
throw Error(
|
||||
"An error occurred while registering Medusa Core API Routes. See error in logs for more details.",
|
||||
"An error occurred while registering API Routes. See error in logs for more details.",
|
||||
{ cause: err }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
import { FlagSettings } from "@medusajs/framework"
|
||||
|
||||
const AnalyticsFeatureFlag: FlagSettings = {
|
||||
key: "analytics",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
import { FlagSettings } from "@medusajs/framework"
|
||||
|
||||
const OrderEditingFeatureFlag: FlagSettings = {
|
||||
key: "order_editing",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
import { FlagSettings } from "@medusajs/framework"
|
||||
|
||||
const ProductCategoryFeatureFlag: FlagSettings = {
|
||||
key: "product_categories",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
import { FlagSettings } from "@medusajs/framework"
|
||||
|
||||
const PublishableAPIKeysFeatureFlag: FlagSettings = {
|
||||
key: "publishable_api_keys",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
import { FlagSettings } from "@medusajs/framework"
|
||||
|
||||
const SalesChannelFeatureFlag: FlagSettings = {
|
||||
key: "sales_channels",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
import { FlagSettings } from "@medusajs/framework"
|
||||
|
||||
const TaxInclusivePricingFeatureFlag: FlagSettings = {
|
||||
key: "tax_inclusive_pricing",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ConfigModule } from "@medusajs/types"
|
||||
|
||||
export const customersGlobalMiddlewareMock = jest.fn()
|
||||
export const customersCreateMiddlewareMock = jest.fn()
|
||||
export const storeGlobalMiddlewareMock = jest.fn()
|
||||
|
||||
export const config: ConfigModule = {
|
||||
projectConfig: {
|
||||
databaseLogging: false,
|
||||
http: {
|
||||
authCors: "http://localhost:9000",
|
||||
storeCors: "http://localhost:8000",
|
||||
adminCors: "http://localhost:7001",
|
||||
jwtSecret: "supersecret",
|
||||
cookieSecret: "superSecret",
|
||||
},
|
||||
},
|
||||
featureFlags: {},
|
||||
plugins: [],
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const GET = (req: Request, res: Response) => {
|
||||
res.send("get customer order")
|
||||
}
|
||||
|
||||
export const POST = (req: Request, res: Response) => {
|
||||
res.send("update customer order")
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { defineMiddlewares } from "../../../../../utils/define-middlewares"
|
||||
|
||||
export default defineMiddlewares({
|
||||
errorHandler: (err, _req, res, _next) => {
|
||||
const { code, message } = err
|
||||
|
||||
switch (code) {
|
||||
case "NOT_ALLOWED":
|
||||
res.status(405).json({
|
||||
type: code.toLowerCase(),
|
||||
message,
|
||||
})
|
||||
break
|
||||
case "INVALID_DATA":
|
||||
res.status(400).json({
|
||||
type: code.toLowerCase(),
|
||||
message,
|
||||
})
|
||||
break
|
||||
case "CONFLICT":
|
||||
res.status(409).json({
|
||||
type: code.toLowerCase(),
|
||||
message,
|
||||
})
|
||||
break
|
||||
case "TEAPOT":
|
||||
res.status(418).json({
|
||||
type: code.toLowerCase(),
|
||||
message,
|
||||
})
|
||||
break
|
||||
default:
|
||||
res.status(500).json({
|
||||
type: "unknown_error",
|
||||
message: "An unknown error occurred.",
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
export const GET = async (req: Request, res: Response) => {
|
||||
throw {
|
||||
code: "NOT_ALLOWED",
|
||||
message: "Not allowed to perform this action",
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = async (req: Request, res: Response) => {
|
||||
throw {
|
||||
code: "INVALID_DATA",
|
||||
message: "Invalid data provided",
|
||||
}
|
||||
}
|
||||
|
||||
export const PUT = async (req: Request, res: Response) => {
|
||||
throw {
|
||||
code: "CONFLICT",
|
||||
message: "Conflict with another request",
|
||||
}
|
||||
}
|
||||
|
||||
export const DELETE = async (req: Request, res: Response) => {
|
||||
throw {
|
||||
code: "TEAPOT",
|
||||
message: "I'm a teapot",
|
||||
}
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const GET = (req: Request, res: Response) => {
|
||||
res.send(`GET /admin/protected`)
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
|
||||
export const GET = (req: Request, res: Response) => {
|
||||
res.send(`GET /admin/unprotected`)
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export const GET = async (req: Request, res: Response) => {
|
||||
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not allowed")
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export function GET(req: Request, res: Response) {
|
||||
res.send("list customers")
|
||||
}
|
||||
|
||||
export function POST(req: Request, res: Response) {
|
||||
res.send("create customer")
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { NextFunction, Request, Response, raw } from "express"
|
||||
import {
|
||||
customersCreateMiddlewareMock,
|
||||
customersGlobalMiddlewareMock,
|
||||
storeGlobalMiddlewareMock,
|
||||
} from "../mocks"
|
||||
import { defineMiddlewares } from "../../../../../utils/define-middlewares"
|
||||
|
||||
const customersGlobalMiddleware = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
customersGlobalMiddlewareMock()
|
||||
next()
|
||||
}
|
||||
|
||||
const customersCreateMiddleware = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
customersCreateMiddlewareMock()
|
||||
next()
|
||||
}
|
||||
|
||||
const storeGlobal = (req: Request, res: Response, next: NextFunction) => {
|
||||
storeGlobalMiddlewareMock()
|
||||
next()
|
||||
}
|
||||
|
||||
export default defineMiddlewares([
|
||||
{
|
||||
matcher: "/customers",
|
||||
middlewares: [customersGlobalMiddleware],
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
matcher: "/customers",
|
||||
middlewares: [customersCreateMiddleware],
|
||||
},
|
||||
{
|
||||
matcher: "/store/*",
|
||||
middlewares: [storeGlobal],
|
||||
},
|
||||
{
|
||||
matcher: "/webhooks/*",
|
||||
method: "POST",
|
||||
bodyParser: false,
|
||||
middlewares: [raw({ type: "application/json" })],
|
||||
},
|
||||
])
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const GET = async (req: Request, res: Response) => {
|
||||
res.send(`GET /store/protected`)
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
|
||||
export const GET = async (req: Request, res: Response) => {
|
||||
res.send(`GET /store/unprotected`)
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const POST = (req: Request, res: Response) => {
|
||||
res.send(`sync product ${req.params.id}`)
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const POST = (req: Request, res: Response) => {
|
||||
if (!(req.body instanceof Buffer)) {
|
||||
res.status(400).send("Invalid body")
|
||||
}
|
||||
|
||||
res.send("OK")
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export const GET = async (req: Request, res: Response): Promise<void> => {
|
||||
res.send(`GET private route`)
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export async function GET(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.send(`GET order ${req.params.id}`)
|
||||
} catch (err) {
|
||||
res.status(400).send(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.send(`POST order ${req.params.id}`)
|
||||
} catch (err) {
|
||||
res.status(400).send(err)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export function GET(req: Request, res: Response) {
|
||||
res.send("hello world")
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export async function GET(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export async function GET(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function POST(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function PUT(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function OPTIONS(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function HEAD(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export async function GET(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function POST(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export function GET(req: Request, res: Response) {
|
||||
/* const customerId = req.params.id;
|
||||
const orderId = req.params.id;*/
|
||||
res.send("list customers " + JSON.stringify(req.params))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
|
||||
export function GET(req: Request, res: Response) {
|
||||
res.send("list customers")
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import {
|
||||
moduleLoader,
|
||||
ModulesDefinition,
|
||||
registerMedusaModule,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
configManager,
|
||||
ConfigModule,
|
||||
container,
|
||||
featureFlagsLoader,
|
||||
logger,
|
||||
} from "@medusajs/framework"
|
||||
import { ContainerRegistrationKeys, generateJwtToken } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import express from "express"
|
||||
import querystring from "querystring"
|
||||
import supertest from "supertest"
|
||||
import apiLoader from "../../../../api"
|
||||
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
|
||||
|
||||
import RoutesLoader from "../.."
|
||||
import { config } from "../mocks"
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
|
||||
function asArray(resolvers) {
|
||||
return {
|
||||
resolve: (container) =>
|
||||
resolvers.map((resolver) => container.build(resolver)),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a test server that injects API Routes using the RoutesLoader
|
||||
*
|
||||
* @param {String} rootDir - The root directory of the project
|
||||
*/
|
||||
export const createServer = async (rootDir) => {
|
||||
const app = express()
|
||||
|
||||
const moduleResolutions = {}
|
||||
Object.entries(ModulesDefinition).forEach(([moduleKey, module]) => {
|
||||
moduleResolutions[moduleKey] = registerMedusaModule(
|
||||
moduleKey,
|
||||
module.defaultModuleDeclaration,
|
||||
undefined,
|
||||
module
|
||||
)[moduleKey]
|
||||
})
|
||||
|
||||
configManager.loadConfig({
|
||||
projectConfig: config as ConfigModule,
|
||||
baseDir: rootDir,
|
||||
})
|
||||
|
||||
container.registerAdd = function (this: MedusaContainer, name, registration) {
|
||||
const storeKey = name + "_STORE"
|
||||
|
||||
if (this.registrations[storeKey] === undefined) {
|
||||
this.register(storeKey, asValue([]))
|
||||
}
|
||||
const store = this.resolve(storeKey) as Array<any>
|
||||
|
||||
if (this.registrations[name] === undefined) {
|
||||
this.register(name, asArray(store))
|
||||
}
|
||||
store.unshift(registration)
|
||||
|
||||
return this
|
||||
}.bind(container)
|
||||
|
||||
container.register(ContainerRegistrationKeys.PG_CONNECTION, asValue({}))
|
||||
container.register("configModule", asValue(config))
|
||||
container.register({
|
||||
logger: asValue({
|
||||
error: () => {},
|
||||
}),
|
||||
manager: asValue({}),
|
||||
})
|
||||
|
||||
app.set("trust proxy", 1)
|
||||
app.use((req, _res, next) => {
|
||||
req["session"] = {}
|
||||
const data = req.get("Cookie")
|
||||
if (data) {
|
||||
req["session"] = {
|
||||
...req["session"],
|
||||
...JSON.parse(data),
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
const plugins = getResolvedPlugins(rootDir, config) || []
|
||||
|
||||
await featureFlagsLoader()
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.scope = container.createScope() as MedusaContainer
|
||||
next()
|
||||
})
|
||||
|
||||
// This where plugins normally load, but we simply load the routes
|
||||
await new RoutesLoader({
|
||||
app,
|
||||
rootDir,
|
||||
configModule: config,
|
||||
}).load()
|
||||
|
||||
// the apiLoader needs to be called after plugins otherwise the core middleware bleads into the plugins
|
||||
await apiLoader({
|
||||
container,
|
||||
app: app,
|
||||
plugins,
|
||||
})
|
||||
|
||||
const superRequest = supertest(app)
|
||||
|
||||
return {
|
||||
request: async (method, url, opts: any = {}) => {
|
||||
const { payload, query, headers = {} } = opts
|
||||
|
||||
const queryParams = query && querystring.stringify(query)
|
||||
const req = superRequest[method.toLowerCase()](
|
||||
`${url}${queryParams ? "?" + queryParams : ""}`
|
||||
)
|
||||
headers.Cookie = headers.Cookie || ""
|
||||
if (opts.adminSession) {
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: opts.adminSession.userId || opts.adminSession.jwt?.userId,
|
||||
actor_type: "user",
|
||||
app_metadata: {
|
||||
user_id:
|
||||
opts.adminSession.userId || opts.adminSession.jwt?.userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
secret: config.projectConfig.http.jwtSecret!,
|
||||
expiresIn: "1d",
|
||||
}
|
||||
)
|
||||
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
if (opts.clientSession) {
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id:
|
||||
opts.clientSession.customer_id ||
|
||||
opts.clientSession.jwt?.customer_id,
|
||||
actor_type: "customer",
|
||||
app_metadata: {
|
||||
customer_id:
|
||||
opts.clientSession.customer_id ||
|
||||
opts.clientSession.jwt?.customer_id,
|
||||
},
|
||||
},
|
||||
{ secret: config.projectConfig.http.jwtSecret!, expiresIn: "1d" }
|
||||
)
|
||||
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
for (const name in headers) {
|
||||
if ({}.hasOwnProperty.call(headers, name)) {
|
||||
req.set(name, headers[name])
|
||||
}
|
||||
}
|
||||
|
||||
if (payload && !req.get("content-type")) {
|
||||
req.set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
if (!req.get("accept")) {
|
||||
req.set("Accept", "application/json")
|
||||
}
|
||||
|
||||
req.set("Host", "localhost")
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await req.send(JSON.stringify(payload))
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
res = e.response
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import express from "express"
|
||||
import { resolve } from "path"
|
||||
import {
|
||||
config,
|
||||
customersCreateMiddlewareMock,
|
||||
customersGlobalMiddlewareMock,
|
||||
storeGlobalMiddlewareMock,
|
||||
} from "../__fixtures__/mocks"
|
||||
import { createServer } from "../__fixtures__/server"
|
||||
import { RoutesLoader } from "../index"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("RoutesLoader", function () {
|
||||
afterEach(function () {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("Routes", function () {
|
||||
let request
|
||||
|
||||
beforeAll(async function () {
|
||||
const rootDir = resolve(__dirname, "../__fixtures__/routers")
|
||||
|
||||
const { request: request_ } = await createServer(rootDir)
|
||||
|
||||
request = request_
|
||||
})
|
||||
|
||||
it("should return a status 200 on GET admin/order/:id", async function () {
|
||||
const res = await request("GET", "/admin/orders/1000", {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: "admin_user",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("GET order 1000")
|
||||
})
|
||||
|
||||
it("should return a status 200 on POST admin/order/:id", async function () {
|
||||
const res = await request("POST", "/admin/orders/1000", {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: "admin_user",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("POST order 1000")
|
||||
})
|
||||
|
||||
it("should call GET /customers/[customer_id]/orders/[order_id]", async function () {
|
||||
const res = await request("GET", "/customers/test-customer/orders/test")
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe(
|
||||
'list customers {"customer_id":"test-customer","order_id":"test"}'
|
||||
)
|
||||
})
|
||||
|
||||
it("should not be able to GET /_private as the folder is prefixed with an underscore", async function () {
|
||||
const res = await request("GET", "/_private")
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
expect(res.text).toContain("Cannot GET /_private")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Middlewares", function () {
|
||||
let request
|
||||
|
||||
beforeAll(async function () {
|
||||
const rootDir = resolve(__dirname, "../__fixtures__/routers-middleware")
|
||||
|
||||
const { request: request_ } = await createServer(rootDir)
|
||||
|
||||
request = request_
|
||||
})
|
||||
|
||||
it("should call middleware applied to `/customers`", async function () {
|
||||
const res = await request("GET", "/customers")
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("list customers")
|
||||
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not call middleware applied to POST `/customers` when GET `/customers`", async function () {
|
||||
const res = await request("GET", "/customers")
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("list customers")
|
||||
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
||||
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call middleware applied to POST `/customers` when POST `/customers`", async function () {
|
||||
const res = await request("POST", "/customers")
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("create customer")
|
||||
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
||||
expect(customersCreateMiddlewareMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call store global middleware on `/store/*` routes", async function () {
|
||||
const res = await request("POST", "/store/products/1000/sync")
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("sync product 1000")
|
||||
expect(storeGlobalMiddlewareMock).toHaveBeenCalled()
|
||||
|
||||
expect(customersGlobalMiddlewareMock).not.toHaveBeenCalled()
|
||||
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should apply raw middleware on POST `/webhooks/payment` route", async function () {
|
||||
const res = await request("POST", "/webhooks/payment", {
|
||||
payload: { test: "test" },
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("OK")
|
||||
})
|
||||
|
||||
it("should return 200 when admin is authenticated", async () => {
|
||||
const res = await request("GET", "/admin/protected", {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: "admin_user",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("GET /admin/protected")
|
||||
})
|
||||
|
||||
it.skip("should return 401 when admin is not authenticated", async () => {
|
||||
const res = await request("GET", "/admin/protected")
|
||||
|
||||
expect(res.status).toBe(401)
|
||||
expect(res.text).toBe("Unauthorized")
|
||||
})
|
||||
|
||||
it("should return 200 when admin route is opted out of authentication", async () => {
|
||||
const res = await request("GET", "/admin/unprotected")
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.text).toBe("GET /admin/unprotected")
|
||||
})
|
||||
|
||||
it("should return the error as JSON when an error is thrown with default error handling", async () => {
|
||||
const res = await request("GET", "/customers/error")
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body).toEqual({
|
||||
message: "Not allowed",
|
||||
type: "not_allowed",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Custom error handling", function () {
|
||||
let request
|
||||
|
||||
beforeAll(async function () {
|
||||
const rootDir = resolve(
|
||||
__dirname,
|
||||
"../__fixtures__/routers-error-handler"
|
||||
)
|
||||
|
||||
const { request: request_ } = await createServer(rootDir)
|
||||
|
||||
request = request_
|
||||
})
|
||||
|
||||
it("should return 405 when NOT_ALLOWED error is thrown", async () => {
|
||||
const res = await request("GET", "/store")
|
||||
|
||||
expect(res.status).toBe(405)
|
||||
expect(res.body).toEqual({
|
||||
message: "Not allowed to perform this action",
|
||||
type: "not_allowed",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return 400 when INVALID_DATA error is thrown", async () => {
|
||||
const res = await request("POST", "/store")
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body).toEqual({
|
||||
message: "Invalid data provided",
|
||||
type: "invalid_data",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return 409 when CONFLICT error is thrown", async () => {
|
||||
const res = await request("PUT", "/store")
|
||||
|
||||
expect(res.status).toBe(409)
|
||||
expect(res.body).toEqual({
|
||||
message: "Conflict with another request",
|
||||
type: "conflict",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return 418 when TEAPOT error is thrown", async () => {
|
||||
const res = await request("DELETE", "/store")
|
||||
|
||||
expect(res.status).toBe(418)
|
||||
expect(res.body).toEqual({
|
||||
message: "I'm a teapot",
|
||||
type: "teapot",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Duplicate parameters", function () {
|
||||
const app = express()
|
||||
|
||||
it("should throw if a route contains the same parameter multiple times", async function () {
|
||||
const rootDir = resolve(
|
||||
__dirname,
|
||||
"../__fixtures__/routers-duplicate-parameter"
|
||||
)
|
||||
const err = await new RoutesLoader({
|
||||
app,
|
||||
rootDir,
|
||||
configModule: config,
|
||||
})
|
||||
.load()
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toBeDefined()
|
||||
expect(err.message).toBe(
|
||||
"Duplicate parameters found in route /admin/customers/[id]/orders/[id] (id). Make sure that all parameters are unique."
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,829 +0,0 @@
|
||||
import { ConfigModule } from "@medusajs/types"
|
||||
import { parseCorsOrigins, promiseAll, wrapHandler } from "@medusajs/utils"
|
||||
import cors from "cors"
|
||||
import { type Express, json, Router, text, urlencoded } from "express"
|
||||
import { readdir } from "fs/promises"
|
||||
import { extname, join, sep } from "path"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||
import { authenticate, errorHandler } from "../../../utils/middlewares"
|
||||
import { logger } from "@medusajs/framework"
|
||||
import {
|
||||
AsyncRouteHandler,
|
||||
GlobalMiddlewareDescriptor,
|
||||
HTTP_METHODS,
|
||||
MiddlewareRoute,
|
||||
MiddlewaresConfig,
|
||||
MiddlewareVerb,
|
||||
ParserConfigArgs,
|
||||
RouteConfig,
|
||||
RouteDescriptor,
|
||||
RouteVerb,
|
||||
} from "./types"
|
||||
|
||||
const log = ({
|
||||
activityId,
|
||||
message,
|
||||
}: {
|
||||
activityId?: string
|
||||
message: string
|
||||
}) => {
|
||||
if (activityId) {
|
||||
logger.progress(activityId, message)
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* File name that is used to indicate that the file is a route file
|
||||
*/
|
||||
const ROUTE_NAME = "route"
|
||||
|
||||
/**
|
||||
* Flag that developers can export from their route files to indicate
|
||||
* whether or not the route should be authenticated or not.
|
||||
*/
|
||||
const AUTHTHENTICATE = "AUTHENTICATE"
|
||||
|
||||
/**
|
||||
* File name for the global middlewares file
|
||||
*/
|
||||
const MIDDLEWARES_NAME = "middlewares"
|
||||
|
||||
const pathSegmentReplacer = {
|
||||
"\\[\\.\\.\\.\\]": () => `*`,
|
||||
"\\[(\\w+)?": (param?: string) => `:${param}`,
|
||||
"\\]": () => ``,
|
||||
}
|
||||
|
||||
/**
|
||||
* @param routes - The routes to prioritize
|
||||
*
|
||||
* @return An array of sorted
|
||||
* routes based on their priority
|
||||
*/
|
||||
const prioritize = (routes: RouteDescriptor[]): RouteDescriptor[] => {
|
||||
return routes.sort((a, b) => {
|
||||
return a.priority - b.priority
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The smaller the number the higher the priority with zero indicating
|
||||
* highest priority
|
||||
*
|
||||
* @param path - The path to calculate the priority for
|
||||
*
|
||||
* @return An integer ranging from `0` to `Infinity`
|
||||
*/
|
||||
function calculatePriority(path: string): number {
|
||||
const depth = path.match(/\/.+?/g)?.length || 0
|
||||
const specifity = path.match(/\/:.+?/g)?.length || 0
|
||||
const catchall = (path.match(/\/\*/g)?.length || 0) > 0 ? Infinity : 0
|
||||
|
||||
return depth + specifity + catchall
|
||||
}
|
||||
|
||||
function matchMethod(
|
||||
method: RouteVerb,
|
||||
configMethod: MiddlewareRoute["method"]
|
||||
): boolean {
|
||||
if (!configMethod || configMethod === "USE" || configMethod === "ALL") {
|
||||
return true
|
||||
} else if (Array.isArray(configMethod)) {
|
||||
return (
|
||||
configMethod.includes(method) ||
|
||||
configMethod.includes("ALL") ||
|
||||
configMethod.includes("USE")
|
||||
)
|
||||
} else {
|
||||
return method === configMethod
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that looks though the global middlewares and returns the first
|
||||
* complete match for the given path and method.
|
||||
*
|
||||
* @param path - The path to match
|
||||
* @param method - The method to match
|
||||
* @param routes - The routes to match against
|
||||
* @returns The first complete match or undefined if no match is found
|
||||
*/
|
||||
function findMatch(
|
||||
path: string,
|
||||
method: RouteVerb,
|
||||
routes: MiddlewareRoute[]
|
||||
): MiddlewareRoute | undefined {
|
||||
for (const route of routes) {
|
||||
const { matcher, method: configMethod } = route
|
||||
|
||||
if (matchMethod(method, configMethod)) {
|
||||
let isMatch = false
|
||||
|
||||
if (typeof matcher === "string") {
|
||||
// Convert wildcard expressions to proper regex for matching entire path
|
||||
// The '.*' will match any character sequence including '/'
|
||||
const regex = new RegExp(`^${matcher.split("*").join(".*")}$`)
|
||||
isMatch = regex.test(path)
|
||||
} else if (matcher instanceof RegExp) {
|
||||
// Ensure that the regex matches the entire path
|
||||
const match = path.match(matcher)
|
||||
isMatch = match !== null && match[0] === path
|
||||
}
|
||||
|
||||
if (isMatch) {
|
||||
return route // Return the first complete match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined // Return undefined if no complete match is found
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of body parser middlewares that are applied on routes
|
||||
* out-of-the-box.
|
||||
*/
|
||||
function getBodyParserMiddleware(args?: ParserConfigArgs) {
|
||||
const sizeLimit = args?.sizeLimit
|
||||
const preserveRawBody = args?.preserveRawBody
|
||||
return [
|
||||
json({
|
||||
limit: sizeLimit,
|
||||
verify: preserveRawBody
|
||||
? (req: MedusaRequest, res: MedusaResponse, buf: Buffer) => {
|
||||
req.rawBody = buf
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
text({ limit: sizeLimit }),
|
||||
urlencoded({ limit: sizeLimit, extended: true }),
|
||||
]
|
||||
}
|
||||
|
||||
export class RoutesLoader {
|
||||
protected routesMap = new Map<string, RouteDescriptor>()
|
||||
protected globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
|
||||
|
||||
protected app: Express
|
||||
protected router: Router
|
||||
protected activityId?: string
|
||||
protected rootDir: string
|
||||
protected configModule: ConfigModule
|
||||
protected excludes: RegExp[] = [
|
||||
/\.DS_Store/,
|
||||
/(\.ts\.map|\.js\.map|\.d\.ts)/,
|
||||
/^_[^/\\]*(\.[^/\\]+)?$/,
|
||||
]
|
||||
|
||||
constructor({
|
||||
app,
|
||||
activityId,
|
||||
rootDir,
|
||||
configModule,
|
||||
excludes = [],
|
||||
}: {
|
||||
app: Express
|
||||
activityId?: string
|
||||
rootDir: string
|
||||
configModule: ConfigModule
|
||||
excludes?: RegExp[]
|
||||
}) {
|
||||
this.app = app
|
||||
this.router = Router()
|
||||
this.activityId = activityId
|
||||
this.rootDir = rootDir
|
||||
this.configModule = configModule
|
||||
this.excludes.push(...(excludes ?? []))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the route config and display a log info if
|
||||
* it should be ignored or skipped.
|
||||
*
|
||||
* @param {GlobalMiddlewareDescriptor} descriptor
|
||||
* @param {MiddlewaresConfig} config
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
protected validateMiddlewaresConfig({
|
||||
config,
|
||||
}: {
|
||||
config?: MiddlewaresConfig
|
||||
}): void {
|
||||
if (!config?.routes && !config?.errorHandler) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Empty middleware config. Skipping middleware application.`,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const route of config.routes ?? []) {
|
||||
if (!route.matcher) {
|
||||
throw new Error(
|
||||
`Route is missing a \`matcher\` field. The 'matcher' field is required when applying middleware to this route.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take care of replacing the special path segments
|
||||
* to an express specific path segment
|
||||
*
|
||||
* @param route - The route to parse
|
||||
*
|
||||
* @example
|
||||
* "/admin/orders/[id]/route.ts => "/admin/orders/:id/route.ts"
|
||||
*/
|
||||
protected parseRoute(route: string): string {
|
||||
let route_ = route
|
||||
|
||||
for (const config of Object.entries(pathSegmentReplacer)) {
|
||||
const [searchFor, replacedByFn] = config
|
||||
const replacer = new RegExp(searchFor, "g")
|
||||
|
||||
const matches = [...route_.matchAll(replacer)]
|
||||
|
||||
const parameters = new Set()
|
||||
|
||||
for (const match of matches) {
|
||||
if (match?.[1] && !Number.isInteger(match?.[1])) {
|
||||
if (parameters.has(match?.[1])) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Duplicate parameters found in route ${route} (${match?.[1]})`,
|
||||
})
|
||||
|
||||
throw new Error(
|
||||
`Duplicate parameters found in route ${route} (${match?.[1]}). Make sure that all parameters are unique.`
|
||||
)
|
||||
}
|
||||
|
||||
parameters.add(match?.[1])
|
||||
}
|
||||
|
||||
route_ = route_.replace(match[0], replacedByFn(match?.[1]))
|
||||
}
|
||||
|
||||
const extension = extname(route_)
|
||||
if (extension) {
|
||||
route_ = route_.replace(extension, "")
|
||||
}
|
||||
}
|
||||
|
||||
route = route_
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the file content from a descriptor and retrieve the verbs and handlers
|
||||
* to be assigned to the descriptor
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
protected async createRoutesConfig(): Promise<void> {
|
||||
await promiseAll(
|
||||
[...this.routesMap.values()].map(async (descriptor: RouteDescriptor) => {
|
||||
const absolutePath = descriptor.absolutePath
|
||||
const route = descriptor.route
|
||||
|
||||
return await import(absolutePath).then((import_) => {
|
||||
const map = this.routesMap
|
||||
|
||||
const config: RouteConfig = {
|
||||
routes: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* If the developer has not exported the
|
||||
* AUTHENTICATE flag we default to true.
|
||||
*/
|
||||
const shouldRequireAuth =
|
||||
import_[AUTHTHENTICATE] !== undefined
|
||||
? (import_[AUTHTHENTICATE] as boolean)
|
||||
: true
|
||||
|
||||
config.optedOutOfAuth = !shouldRequireAuth
|
||||
/**
|
||||
* If the developer has not exported the
|
||||
* CORS flag we default to true.
|
||||
*/
|
||||
const shouldAddCors =
|
||||
import_["CORS"] !== undefined ? (import_["CORS"] as boolean) : true
|
||||
|
||||
if (route.startsWith("/admin")) {
|
||||
config.routeType = "admin"
|
||||
if (shouldAddCors) {
|
||||
config.shouldAppendAdminCors = true
|
||||
}
|
||||
}
|
||||
|
||||
if (route.startsWith("/store")) {
|
||||
config.routeType = "store"
|
||||
if (shouldAddCors) {
|
||||
config.shouldAppendStoreCors = true
|
||||
}
|
||||
}
|
||||
|
||||
if (route.startsWith("/auth") && shouldAddCors) {
|
||||
config.routeType = "auth"
|
||||
if (shouldAddCors) {
|
||||
config.shouldAppendAuthCors = true
|
||||
}
|
||||
}
|
||||
|
||||
const handlers = Object.keys(import_).filter((key) => {
|
||||
/**
|
||||
* Filter out any export that is not a function
|
||||
*/
|
||||
return typeof import_[key] === "function"
|
||||
})
|
||||
|
||||
for (const handler of handlers) {
|
||||
if (HTTP_METHODS.includes(handler as RouteVerb)) {
|
||||
config.routes?.push({
|
||||
method: handler as RouteVerb,
|
||||
handler: import_[handler],
|
||||
})
|
||||
} else {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Skipping handler ${handler} in ${absolutePath}. Invalid HTTP method: ${handler}.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.routes?.length) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `No valid route handlers detected in ${absolutePath}. Skipping route configuration.`,
|
||||
})
|
||||
|
||||
map.delete(absolutePath)
|
||||
return
|
||||
}
|
||||
|
||||
descriptor.config = config
|
||||
map.set(absolutePath, descriptor)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
protected createRoutesDescriptor({
|
||||
childPath,
|
||||
parentPath,
|
||||
}: {
|
||||
childPath: string
|
||||
parentPath?: string
|
||||
}) {
|
||||
const descriptor: RouteDescriptor = {
|
||||
absolutePath: childPath,
|
||||
relativePath: "",
|
||||
route: "",
|
||||
priority: Infinity,
|
||||
}
|
||||
|
||||
if (parentPath) {
|
||||
childPath = childPath.replace(parentPath, "")
|
||||
}
|
||||
|
||||
descriptor.relativePath = childPath
|
||||
|
||||
let routeToParse = childPath
|
||||
|
||||
const pathSegments = childPath.split(sep)
|
||||
const lastSegment = pathSegments[pathSegments.length - 1]
|
||||
|
||||
if (lastSegment.startsWith("route")) {
|
||||
pathSegments.pop()
|
||||
routeToParse = pathSegments.join("/")
|
||||
}
|
||||
|
||||
descriptor.route = this.parseRoute(routeToParse)
|
||||
descriptor.priority = calculatePriority(descriptor.route)
|
||||
|
||||
this.routesMap.set(childPath, descriptor)
|
||||
}
|
||||
|
||||
protected async createMiddlewaresDescriptor({
|
||||
dirPath,
|
||||
}: {
|
||||
dirPath: string
|
||||
}) {
|
||||
const files = await readdir(dirPath)
|
||||
|
||||
const middlewareFilePath = files
|
||||
.filter((path) => {
|
||||
if (
|
||||
this.excludes.length &&
|
||||
this.excludes.some((exclude) => exclude.test(path))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.find((file) => {
|
||||
return file.replace(/\.[^/.]+$/, "") === MIDDLEWARES_NAME
|
||||
})
|
||||
|
||||
if (!middlewareFilePath) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `No middleware files found in ${dirPath}. Skipping middleware configuration.`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const absolutePath = join(dirPath, middlewareFilePath)
|
||||
|
||||
try {
|
||||
await import(absolutePath).then((import_) => {
|
||||
const middlewaresConfig = import_.default as
|
||||
| MiddlewaresConfig
|
||||
| undefined
|
||||
|
||||
if (!middlewaresConfig) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
middlewaresConfig.routes = middlewaresConfig.routes?.map((route) => {
|
||||
return {
|
||||
...route,
|
||||
method: route.method ?? "USE",
|
||||
}
|
||||
})
|
||||
|
||||
const descriptor: GlobalMiddlewareDescriptor = {
|
||||
config: middlewaresConfig,
|
||||
}
|
||||
|
||||
this.validateMiddlewaresConfig(descriptor)
|
||||
|
||||
this.globalMiddlewaresDescriptor = descriptor
|
||||
})
|
||||
} catch (error) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Failed to load middleware configuration in ${absolutePath}. Skipping middleware configuration.`,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
protected async createRoutesMap({
|
||||
dirPath,
|
||||
parentPath,
|
||||
}: {
|
||||
dirPath: string
|
||||
parentPath?: string
|
||||
}): Promise<void> {
|
||||
await promiseAll(
|
||||
await readdir(dirPath, { withFileTypes: true }).then((entries) => {
|
||||
return entries
|
||||
.filter((entry) => {
|
||||
if (
|
||||
this.excludes.length &&
|
||||
this.excludes.some((exclude) => exclude.test(entry.name))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
let name = entry.name
|
||||
|
||||
const extension = extname(name)
|
||||
|
||||
if (extension) {
|
||||
name = name.replace(extension, "")
|
||||
}
|
||||
|
||||
if (entry.isFile() && name !== ROUTE_NAME) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.map(async (entry) => {
|
||||
const childPath = join(dirPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return await this.createRoutesMap({
|
||||
dirPath: childPath,
|
||||
parentPath: parentPath ?? dirPath,
|
||||
})
|
||||
}
|
||||
|
||||
return this.createRoutesDescriptor({
|
||||
childPath,
|
||||
parentPath,
|
||||
})
|
||||
})
|
||||
.flat(Infinity)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the most specific body parser middleware to the router
|
||||
*/
|
||||
applyBodyParserMiddleware(path: string, method: RouteVerb): void {
|
||||
const middlewareDescriptor = this.globalMiddlewaresDescriptor
|
||||
|
||||
const mostSpecificConfig = findMatch(
|
||||
path,
|
||||
method,
|
||||
middlewareDescriptor?.config?.routes ?? []
|
||||
)
|
||||
|
||||
if (!mostSpecificConfig || mostSpecificConfig?.bodyParser === undefined) {
|
||||
this.router[method.toLowerCase()](path, ...getBodyParserMiddleware())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (mostSpecificConfig?.bodyParser) {
|
||||
this.router[method.toLowerCase()](
|
||||
path,
|
||||
...getBodyParserMiddleware(mostSpecificConfig?.bodyParser)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the route specific middlewares to the router,
|
||||
* this includes the cors, authentication and
|
||||
* body parsing. These are applied first to ensure
|
||||
* that they are applied before any other middleware.
|
||||
*/
|
||||
applyRouteSpecificMiddlewares(): void {
|
||||
const prioritizedRoutes = prioritize([...this.routesMap.values()])
|
||||
|
||||
for (const descriptor of prioritizedRoutes) {
|
||||
if (!descriptor.config?.routes?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const config = descriptor.config
|
||||
const routes = descriptor.config.routes
|
||||
|
||||
/**
|
||||
* Apply default store and admin middlewares if
|
||||
* not opted out of.
|
||||
*/
|
||||
|
||||
if (config.shouldAppendAdminCors) {
|
||||
/**
|
||||
* Apply the admin cors
|
||||
*/
|
||||
this.router.use(
|
||||
descriptor.route,
|
||||
cors({
|
||||
origin: parseCorsOrigins(
|
||||
this.configModule.projectConfig.http.adminCors
|
||||
),
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.shouldAppendAuthCors) {
|
||||
/**
|
||||
* Apply the auth cors
|
||||
*/
|
||||
this.router.use(
|
||||
descriptor.route,
|
||||
cors({
|
||||
origin: parseCorsOrigins(
|
||||
this.configModule.projectConfig.http.authCors
|
||||
),
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.shouldAppendStoreCors) {
|
||||
/**
|
||||
* Apply the store cors
|
||||
*/
|
||||
this.router.use(
|
||||
descriptor.route,
|
||||
cors({
|
||||
origin: parseCorsOrigins(
|
||||
this.configModule.projectConfig.http.storeCors
|
||||
),
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// We only apply the auth middleware to store routes to populate the auth context. For actual authentication, users can just reapply the middleware.
|
||||
if (!config.optedOutOfAuth && config.routeType === "store") {
|
||||
this.router.use(
|
||||
descriptor.route,
|
||||
authenticate("customer", ["bearer", "session"], {
|
||||
allowUnauthenticated: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (!config.optedOutOfAuth && config.routeType === "admin") {
|
||||
// We probably don't want to allow access to all endpoints using an api key, but it will do until we revamp our routing.
|
||||
this.router.use(
|
||||
descriptor.route,
|
||||
authenticate("user", ["bearer", "session", "api-key"])
|
||||
)
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
/**
|
||||
* Apply the body parser middleware if the route
|
||||
* has not opted out of it.
|
||||
*/
|
||||
this.applyBodyParserMiddleware(descriptor.route, route.method!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the error handler middleware to the router
|
||||
*/
|
||||
applyErrorHandlerMiddleware(): void {
|
||||
const middlewareDescriptor = this.globalMiddlewaresDescriptor
|
||||
const errorHandlerFn = middlewareDescriptor?.config?.errorHandler
|
||||
|
||||
/**
|
||||
* If the user has opted out of the error handler then return
|
||||
*/
|
||||
if (errorHandlerFn === false) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user has provided a custom error handler then use it
|
||||
*/
|
||||
if (errorHandlerFn) {
|
||||
this.router.use(errorHandlerFn)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user has not provided a custom error handler then use the
|
||||
* default one.
|
||||
*/
|
||||
this.router.use(errorHandler())
|
||||
}
|
||||
|
||||
protected async registerRoutes(): Promise<void> {
|
||||
const middlewareDescriptor = this.globalMiddlewaresDescriptor
|
||||
|
||||
const shouldWrapHandler = middlewareDescriptor?.config
|
||||
? middlewareDescriptor.config.errorHandler !== false
|
||||
: true
|
||||
|
||||
const prioritizedRoutes = prioritize([...this.routesMap.values()])
|
||||
|
||||
for (const descriptor of prioritizedRoutes) {
|
||||
if (!descriptor.config?.routes?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const routes = descriptor.config.routes
|
||||
|
||||
for (const route of routes) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Registering route [${route.method?.toUpperCase()}] - ${
|
||||
descriptor.route
|
||||
}`,
|
||||
})
|
||||
|
||||
/**
|
||||
* If the user hasn't opted out of error handling then
|
||||
* we wrap the handler in a try/catch block.
|
||||
*/
|
||||
const handler = shouldWrapHandler
|
||||
? wrapHandler(route.handler as AsyncRouteHandler)
|
||||
: route.handler
|
||||
|
||||
this.router[route.method!.toLowerCase()](descriptor.route, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async registerMiddlewares(): Promise<void> {
|
||||
const descriptor = this.globalMiddlewaresDescriptor
|
||||
|
||||
if (!descriptor) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!descriptor.config?.routes?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const routes = descriptor.config.routes
|
||||
|
||||
/**
|
||||
* We don't prioritize the middlewares to preserve the order
|
||||
* in which they are defined in the 'middlewares.ts'. This is to
|
||||
* maintain the same behavior as how middleware is applied
|
||||
* in Express.
|
||||
*/
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.middlewares || !route.middlewares.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const methods = (
|
||||
Array.isArray(route.method) ? route.method : [route.method]
|
||||
).filter(Boolean) as MiddlewareVerb[]
|
||||
|
||||
for (const method of methods) {
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Registering middleware [${method}] - ${route.matcher}`,
|
||||
})
|
||||
|
||||
this.router[method.toLowerCase()](route.matcher, ...route.middlewares)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
performance && performance.mark("file-base-routing-start" + this.rootDir)
|
||||
|
||||
let apiExists = true
|
||||
|
||||
/**
|
||||
* Since the file based routing does not require a index file
|
||||
* we can't check if it exists using require. Instead we try
|
||||
* to read the directory and if it fails we know that the
|
||||
* directory does not exist.
|
||||
*/
|
||||
try {
|
||||
await readdir(this.rootDir)
|
||||
} catch (_error) {
|
||||
apiExists = false
|
||||
}
|
||||
|
||||
if (apiExists) {
|
||||
await this.createMiddlewaresDescriptor({ dirPath: this.rootDir })
|
||||
|
||||
await this.createRoutesMap({ dirPath: this.rootDir })
|
||||
await this.createRoutesConfig()
|
||||
|
||||
this.applyRouteSpecificMiddlewares()
|
||||
|
||||
await this.registerMiddlewares()
|
||||
await this.registerRoutes()
|
||||
|
||||
this.applyErrorHandlerMiddleware()
|
||||
|
||||
/**
|
||||
* Apply the router to the app.
|
||||
*
|
||||
* This prevents middleware from a plugin from
|
||||
* bleeding into the global middleware stack.
|
||||
*/
|
||||
this.app.use("/", this.router)
|
||||
}
|
||||
|
||||
performance && performance.mark("file-base-routing-end" + this.rootDir)
|
||||
const timeSpent =
|
||||
performance &&
|
||||
performance
|
||||
.measure(
|
||||
"file-base-routing-measure" + this.rootDir,
|
||||
"file-base-routing-start" + this.rootDir,
|
||||
"file-base-routing-end" + this.rootDir
|
||||
)
|
||||
?.duration?.toFixed(2)
|
||||
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
message: `Routes loaded in ${timeSpent} ms`,
|
||||
})
|
||||
|
||||
this.routesMap.clear()
|
||||
this.globalMiddlewaresDescriptor = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default RoutesLoader
|
||||
@@ -1,87 +0,0 @@
|
||||
import {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaRequestHandler,
|
||||
MedusaResponse,
|
||||
} from "../../../types/routing"
|
||||
|
||||
/**
|
||||
* List of all the supported HTTP methods
|
||||
*/
|
||||
export const HTTP_METHODS = [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"OPTIONS",
|
||||
"HEAD",
|
||||
] as const
|
||||
|
||||
export type RouteVerb = (typeof HTTP_METHODS)[number]
|
||||
export type MiddlewareVerb = "USE" | "ALL" | RouteVerb
|
||||
|
||||
type SyncRouteHandler = (req: MedusaRequest, res: MedusaResponse) => void
|
||||
|
||||
export type AsyncRouteHandler = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => Promise<void>
|
||||
|
||||
type RouteHandler = SyncRouteHandler | AsyncRouteHandler
|
||||
|
||||
export type RouteImplementation = {
|
||||
method?: RouteVerb
|
||||
handler: RouteHandler
|
||||
}
|
||||
|
||||
export type RouteConfig = {
|
||||
optedOutOfAuth?: boolean
|
||||
routeType?: "admin" | "store" | "auth"
|
||||
shouldAppendAdminCors?: boolean
|
||||
shouldAppendStoreCors?: boolean
|
||||
shouldAppendAuthCors?: boolean
|
||||
routes?: RouteImplementation[]
|
||||
}
|
||||
|
||||
export type MiddlewareFunction =
|
||||
| MedusaRequestHandler
|
||||
| ((...args: any[]) => any)
|
||||
|
||||
export type MedusaErrorHandlerFunction = (
|
||||
error: any,
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => Promise<void> | void
|
||||
|
||||
export type ParserConfigArgs = {
|
||||
sizeLimit?: string | number | undefined
|
||||
preserveRawBody?: boolean
|
||||
}
|
||||
|
||||
export type ParserConfig = false | ParserConfigArgs
|
||||
|
||||
export type MiddlewareRoute = {
|
||||
method?: MiddlewareVerb | MiddlewareVerb[]
|
||||
matcher: string | RegExp
|
||||
bodyParser?: ParserConfig
|
||||
middlewares?: MiddlewareFunction[]
|
||||
}
|
||||
|
||||
export type MiddlewaresConfig = {
|
||||
errorHandler?: false | MedusaErrorHandlerFunction
|
||||
routes?: MiddlewareRoute[]
|
||||
}
|
||||
|
||||
export type RouteDescriptor = {
|
||||
absolutePath: string
|
||||
relativePath: string
|
||||
route: string
|
||||
priority: number
|
||||
config?: RouteConfig
|
||||
}
|
||||
|
||||
export type GlobalMiddlewareDescriptor = {
|
||||
config?: MiddlewaresConfig
|
||||
}
|
||||
Reference in new issue
Block a user