diff --git a/.changeset/warm-spiders-smell.md b/.changeset/warm-spiders-smell.md new file mode 100644 index 0000000000..91a118f960 --- /dev/null +++ b/.changeset/warm-spiders-smell.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/framework": patch +--- + +feat: Replace existing router with the new implementation diff --git a/packages/core/framework/package.json b/packages/core/framework/package.json index ccbc5d91be..2357ccee94 100644 --- a/packages/core/framework/package.json +++ b/packages/core/framework/package.json @@ -93,6 +93,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "4.17.21", "morgan": "^1.9.1", + "path-to-regexp": "^0.1.10", "tsconfig-paths": "^4.2.0", "zod": "3.22.4" }, diff --git a/packages/core/framework/src/http/__fixtures__/server/index.ts b/packages/core/framework/src/http/__fixtures__/server/index.ts index 278c014fa4..dba58cb86c 100644 --- a/packages/core/framework/src/http/__fixtures__/server/index.ts +++ b/packages/core/framework/src/http/__fixtures__/server/index.ts @@ -16,7 +16,7 @@ import { container } from "../../../container" import { featureFlagsLoader } from "../../../feature-flags" import { logger } from "../../../logger" import { MedusaRequest } from "../../types" -import { RoutesLoader } from "../../router" +import { ApiLoader } from "../../router" function asArray(resolvers) { return { @@ -94,7 +94,7 @@ export const createServer = async (rootDir) => { next() }) - await new RoutesLoader({ + await new ApiLoader({ app, sourceDir: rootDir, }).load() diff --git a/packages/core/framework/src/http/__tests__/index.spec.ts b/packages/core/framework/src/http/__tests__/index.spec.ts index d81068a5e6..f31be74b87 100644 --- a/packages/core/framework/src/http/__tests__/index.spec.ts +++ b/packages/core/framework/src/http/__tests__/index.spec.ts @@ -6,7 +6,7 @@ import { storeGlobalMiddlewareMock, } from "../__fixtures__/mocks" import { createServer } from "../__fixtures__/server" -import { MedusaNextFunction, RoutesLoader } from "../index" +import { MedusaNextFunction, ApiLoader } from "../index" jest.setTimeout(30000) @@ -237,7 +237,7 @@ describe("RoutesLoader", function () { __dirname, "../__fixtures__/routers-duplicate-parameter" ) - const err = await new RoutesLoader({ + const err = await new ApiLoader({ app, sourceDir: rootDir, }) @@ -246,7 +246,7 @@ describe("RoutesLoader", function () { 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." + "Duplicate parameters found in route /admin/customers/[id]/orders/[id]/route.ts (id). Make sure that all parameters are unique." ) }) }) diff --git a/packages/core/framework/src/http/__tests__/middleware-file-loader.spec.ts b/packages/core/framework/src/http/__tests__/middleware-file-loader.spec.ts index 3fd2aad88a..d0d480e02b 100644 --- a/packages/core/framework/src/http/__tests__/middleware-file-loader.spec.ts +++ b/packages/core/framework/src/http/__tests__/middleware-file-loader.spec.ts @@ -4,7 +4,7 @@ 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({}) + const loader = new MiddlewareFileLoader() await loader.scanDir(BASE_DIR) expect(loader.getBodyParserConfigRoutes()).toMatchInlineSnapshot(` @@ -14,12 +14,22 @@ describe("Middleware file loader", () => { "preserveRawBody": true, }, "matcher": "/webhooks", - "method": undefined, + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS", + "HEAD", + ], }, { "config": false, "matcher": "/webhooks/*", - "method": "POST", + "methods": [ + "POST", + ], }, ] `) @@ -28,22 +38,26 @@ describe("Middleware file loader", () => { { "handler": [Function], "matcher": "/customers", - "method": undefined, + "methods": undefined, }, { "handler": [Function], "matcher": "/customers", - "method": "POST", + "methods": [ + "POST", + ], }, { "handler": [Function], "matcher": "/store/*", - "method": undefined, + "methods": undefined, }, { "handler": [Function], "matcher": "/webhooks/*", - "method": "POST", + "methods": [ + "POST", + ], }, ] `) diff --git a/packages/core/framework/src/http/__tests__/routes-loader.spec.ts b/packages/core/framework/src/http/__tests__/routes-loader.spec.ts index 2e1b583349..6e1c7d272c 100644 --- a/packages/core/framework/src/http/__tests__/routes-loader.spec.ts +++ b/packages/core/framework/src/http/__tests__/routes-loader.spec.ts @@ -4,7 +4,7 @@ import { RoutesLoader } from "../routes-loader" describe("Routes loader", () => { it("should load routes from the filesystem", async () => { const BASE_DIR = resolve(__dirname, "../__fixtures__/routers") - const loader = new RoutesLoader({}) + const loader = new RoutesLoader() await loader.scanDir(BASE_DIR) expect(loader.getRoutes()).toMatchInlineSnapshot(` @@ -12,10 +12,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/orders/:id", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/orders/[id]/route.ts", - "route": "/admin/orders/:id", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -23,10 +24,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/orders/:id", "method": "POST", "optedOutOfAuth": false, "relativePath": "/admin/orders/[id]/route.ts", - "route": "/admin/orders/:id", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -34,10 +36,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/orders/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/orders", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/orders/route.ts", - "route": "/admin/orders", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -45,10 +48,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/[id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products/:id", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/products/[id]/route.ts", - "route": "/admin/products/:id", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -56,10 +60,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "DELETE", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -67,10 +72,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -78,10 +84,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "HEAD", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -89,10 +96,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "OPTIONS", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -100,10 +108,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "PATCH", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -111,10 +120,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "POST", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -122,10 +132,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "PUT", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -133,10 +144,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/route.ts", - "route": "/admin", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -144,10 +156,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin", "method": "POST", "optedOutOfAuth": false, "relativePath": "/admin/route.ts", - "route": "/admin", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -155,10 +168,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/customers/[customer_id]/orders/[order_id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/customers/:customer_id/orders/:order_id", "method": "GET", "optedOutOfAuth": false, "relativePath": "/customers/[customer_id]/orders/[order_id]/route.ts", - "route": "/customers/:customer_id/orders/:order_id", "shouldAppendAdminCors": false, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -166,10 +180,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/customers/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/customers", "method": "GET", "optedOutOfAuth": false, "relativePath": "/customers/route.ts", - "route": "/customers", "shouldAppendAdminCors": false, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -184,7 +199,7 @@ describe("Routes loader", () => { __dirname, "../__fixtures__/routers-with-duplicates" ) - const loader = new RoutesLoader({}) + const loader = new RoutesLoader() await loader.scanDir(BASE_DIR) await loader.scanDir(BASE_DIR_2) @@ -193,10 +208,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/orders/:id", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/orders/[id]/route.ts", - "route": "/admin/orders/:id", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -204,10 +220,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/orders/:id", "method": "POST", "optedOutOfAuth": false, "relativePath": "/admin/orders/[id]/route.ts", - "route": "/admin/orders/:id", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -215,10 +232,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/orders/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/orders", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/orders/route.ts", - "route": "/admin/orders", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -226,10 +244,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR_2}/admin/products/[id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products/:id", "method": "GET", "optedOutOfAuth": true, "relativePath": "/admin/products/[id]/route.ts", - "route": "/admin/products/:id", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -237,10 +256,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "DELETE", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -248,10 +268,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR_2}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "GET", "optedOutOfAuth": true, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -259,10 +280,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "HEAD", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -270,10 +292,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "OPTIONS", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -281,10 +304,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "PATCH", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -292,10 +316,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR_2}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "POST", "optedOutOfAuth": true, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -303,10 +328,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/products/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin/products", "method": "PUT", "optedOutOfAuth": false, "relativePath": "/admin/products/route.ts", - "route": "/admin/products", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -314,10 +340,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin", "method": "GET", "optedOutOfAuth": false, "relativePath": "/admin/route.ts", - "route": "/admin", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -325,10 +352,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/admin/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/admin", "method": "POST", "optedOutOfAuth": false, "relativePath": "/admin/route.ts", - "route": "/admin", "shouldAppendAdminCors": true, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -336,10 +364,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/customers/[customer_id]/orders/[order_id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/customers/:customer_id/orders/:order_id", "method": "GET", "optedOutOfAuth": false, "relativePath": "/customers/[customer_id]/orders/[order_id]/route.ts", - "route": "/customers/:customer_id/orders/:order_id", "shouldAppendAdminCors": false, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -347,10 +376,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR}/customers/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/customers", "method": "GET", "optedOutOfAuth": false, "relativePath": "/customers/route.ts", - "route": "/customers", "shouldAppendAdminCors": false, "shouldAppendAuthCors": false, "shouldAppendStoreCors": false, @@ -358,10 +388,11 @@ describe("Routes loader", () => { { "absolutePath": "${BASE_DIR_2}/store/[customer_id]/orders/[order_id]/route.ts", "handler": [Function], + "isRoute": true, + "matcher": "/store/:customer_id/orders/:order_id", "method": "GET", "optedOutOfAuth": false, "relativePath": "/store/[customer_id]/orders/[order_id]/route.ts", - "route": "/store/:customer_id/orders/:order_id", "shouldAppendAdminCors": false, "shouldAppendAuthCors": false, "shouldAppendStoreCors": true, @@ -376,7 +407,7 @@ describe("Routes loader", () => { "../__fixtures__/routers-duplicate-parameter" ) - const loader = new RoutesLoader({}) + const loader = new RoutesLoader() await expect(() => loader.scanDir(BASE_DIR)).rejects.toThrow( "Duplicate parameters found in route /admin/customers/[id]/orders/[id]/route.ts (id). Make sure that all parameters are unique." ) diff --git a/packages/core/framework/src/http/__tests__/routes-sorter.spec.ts b/packages/core/framework/src/http/__tests__/routes-sorter.spec.ts index e3c41a211c..3d4a7225a1 100644 --- a/packages/core/framework/src/http/__tests__/routes-sorter.spec.ts +++ b/packages/core/framework/src/http/__tests__/routes-sorter.spec.ts @@ -19,6 +19,14 @@ describe("Routes sorter", () => { isAppRoute: true, handler: () => {}, }, + { + matcher: "/admin", + handler: () => {}, + }, + { + matcher: "/store", + handler: () => {}, + }, { matcher: "/admin/products/export", methods: ["GET"], @@ -32,12 +40,20 @@ describe("Routes sorter", () => { }, { matcher: "/admin/products/:id", + isAppRoute: true, methods: ["GET"], handler: () => {}, }, + { + matcher: "/admin/products/:id", + isAppRoute: true, + methods: ["POST"], + handler: () => {}, + }, { matcher: "/admin/products/batch", - methods: ["GET"], + isAppRoute: true, + methods: ["POST"], handler: () => {}, }, { @@ -57,6 +73,7 @@ describe("Routes sorter", () => { }, { matcher: "/admin/:id/export", + isAppRoute: true, methods: ["GET"], handler: () => {}, }, @@ -69,6 +86,14 @@ describe("Routes sorter", () => { expect(sorter.sort()).toMatchInlineSnapshot(` [ + { + "handler": [Function], + "matcher": "/admin", + }, + { + "handler": [Function], + "matcher": "/store", + }, { "handler": [Function], "matcher": "/v1", @@ -136,13 +161,15 @@ describe("Routes sorter", () => { }, { "handler": [Function], + "isAppRoute": true, "matcher": "/admin/products/batch", "methods": [ - "GET", + "POST", ], }, { "handler": [Function], + "isAppRoute": true, "matcher": "/admin/products/:id", "methods": [ "GET", @@ -150,6 +177,15 @@ describe("Routes sorter", () => { }, { "handler": [Function], + "isAppRoute": true, + "matcher": "/admin/products/:id", + "methods": [ + "POST", + ], + }, + { + "handler": [Function], + "isAppRoute": true, "matcher": "/admin/:id/export", "methods": [ "GET", @@ -286,4 +322,77 @@ describe("Routes sorter", () => { ] `) }) + + it("should sort routes with multiple params and static values", () => { + const sorter = new RoutesSorter([ + { + matcher: "/admin/promotions/:id/:rule_type", + methods: ["GET"], + handler: () => {}, + }, + { + matcher: + "/admin/promotions/rule-value-options/:rule_type/:rule_attribute_id", + methods: ["GET"], + handler: () => {}, + }, + { + matcher: "/admin/promotions/rule-attribute-options/:rule_type", + methods: ["GET"], + handler: () => {}, + }, + { + matcher: "/admin/promotions/:id/:rule_type", + methods: ["GET"], + handler: () => {}, + }, + { + matcher: "/admin/promotions/rule-attribute-options/:rule_type", + methods: ["GET"], + isAppRoute: true, + handler: () => {}, + }, + ]) + + expect(sorter.sort()).toMatchInlineSnapshot(` + [ + { + "handler": [Function], + "matcher": "/admin/promotions/rule-value-options/:rule_type/:rule_attribute_id", + "methods": [ + "GET", + ], + }, + { + "handler": [Function], + "matcher": "/admin/promotions/rule-attribute-options/:rule_type", + "methods": [ + "GET", + ], + }, + { + "handler": [Function], + "isAppRoute": true, + "matcher": "/admin/promotions/rule-attribute-options/:rule_type", + "methods": [ + "GET", + ], + }, + { + "handler": [Function], + "matcher": "/admin/promotions/:id/:rule_type", + "methods": [ + "GET", + ], + }, + { + "handler": [Function], + "matcher": "/admin/promotions/:id/:rule_type", + "methods": [ + "GET", + ], + }, + ] + `) + }) }) diff --git a/packages/core/framework/src/http/middleware-file-loader.ts b/packages/core/framework/src/http/middleware-file-loader.ts index 74cd6fee91..8f139f4b87 100644 --- a/packages/core/framework/src/http/middleware-file-loader.ts +++ b/packages/core/framework/src/http/middleware-file-loader.ts @@ -2,10 +2,12 @@ import { join } from "path" import { dynamicImport, FileSystem } from "@medusajs/utils" import { logger } from "../logger" -import type { - MiddlewaresConfig, - BodyParserConfigRoute, - ScannedMiddlewareDescriptor, +import { + type MiddlewaresConfig, + type BodyParserConfigRoute, + type MiddlewareDescriptor, + type MedusaErrorHandlerFunction, + HTTP_METHODS, } from "./types" /** @@ -13,21 +15,6 @@ import type { */ 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 @@ -35,19 +22,19 @@ const log = ({ */ export class MiddlewareFileLoader { /** - * Middleware collected manually or by scanning directories + * Global error handler exported from the middleware file loader */ - #middleware: ScannedMiddlewareDescriptor[] = [] - #bodyParserConfigRoutes: BodyParserConfigRoute[] = [] + #errorHandler?: MedusaErrorHandlerFunction /** - * An eventual activity id for information tracking + * Middleware collected manually or by scanning directories */ - readonly #activityId?: string + #middleware: MiddlewareDescriptor[] = [] - constructor({ activityId }: { activityId?: string }) { - this.#activityId = activityId - } + /** + * Route matchers on which a custom body parser config is used + */ + #bodyParserConfigRoutes: BodyParserConfigRoute[] = [] /** * Processes the middleware file and returns the middleware and the @@ -58,25 +45,23 @@ export class MiddlewareFileLoader { const middlewareConfig = middlewareExports.default if (!middlewareConfig) { - log({ - activityId: this.#activityId, - message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`, - }) + logger.warn( + `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.`, - }) + logger.warn( + `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[] + middleware: MiddlewareDescriptor[] }>( (result, route) => { if (!route.matcher) { @@ -92,9 +77,15 @@ export class MiddlewareFileLoader { const matcher = String(route.matcher) if ("bodyParser" in route && route.bodyParser !== undefined) { + const methods = route.methods || [...HTTP_METHODS] + + logger.debug( + `using custom bodyparser config on matcher ${methods}:${route.matcher}` + ) + result.bodyParserConfigRoutes.push({ matcher: matcher, - method: route.method, + methods, config: route.bodyParser, }) } @@ -104,7 +95,7 @@ export class MiddlewareFileLoader { result.middleware.push({ handler: middleware, matcher: matcher, - method: route.method, + methods: route.methods, }) }) } @@ -116,8 +107,16 @@ export class MiddlewareFileLoader { } ) - this.#middleware = result.middleware - this.#bodyParserConfigRoutes = result.bodyParserConfigRoutes + const errorHandler = + middlewareConfig.errorHandler as MiddlewaresConfig["errorHandler"] + + if (errorHandler) { + this.#errorHandler = errorHandler + } + this.#middleware = this.#middleware.concat(result.middleware) + this.#bodyParserConfigRoutes = this.#bodyParserConfigRoutes.concat( + result.bodyParserConfigRoutes + ) } /** @@ -133,11 +132,18 @@ export class MiddlewareFileLoader { ) } else if (await fs.exists(`${MIDDLEWARE_FILE_NAME}.js`)) { await this.#processMiddlewareFile( - join(sourceDir, `${MIDDLEWARE_FILE_NAME}.ts`) + join(sourceDir, `${MIDDLEWARE_FILE_NAME}.js`) ) } } + /** + * Returns the globally registered error handler (if any) + */ + getErrorHandler() { + return this.#errorHandler + } + /** * Returns a collection of registered middleware */ diff --git a/packages/core/framework/src/http/middlewares/apply-default-filters.ts b/packages/core/framework/src/http/middlewares/apply-default-filters.ts index 6cea3b632c..828d25a98f 100644 --- a/packages/core/framework/src/http/middlewares/apply-default-filters.ts +++ b/packages/core/framework/src/http/middlewares/apply-default-filters.ts @@ -1,10 +1,18 @@ import { isObject, isPresent } from "@medusajs/utils" -import { MedusaNextFunction, MedusaRequest } from "../types" +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "../types" export function applyDefaultFilters( filtersToApply: TFilter ) { - return async (req: MedusaRequest, _, next: MedusaNextFunction) => { + return async function defaultFiltersMiddleware( + req: MedusaRequest, + _: MedusaResponse, + next: MedusaNextFunction + ) { for (const [filter, filterValue] of Object.entries(filtersToApply)) { let valueToApply = filterValue diff --git a/packages/core/framework/src/http/middlewares/apply-params-as-filters.ts b/packages/core/framework/src/http/middlewares/apply-params-as-filters.ts index 5dfd21f8b8..602a7086ea 100644 --- a/packages/core/framework/src/http/middlewares/apply-params-as-filters.ts +++ b/packages/core/framework/src/http/middlewares/apply-params-as-filters.ts @@ -1,7 +1,15 @@ -import { MedusaNextFunction, MedusaRequest } from "../types" +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "../types" export function applyParamsAsFilters(mappings: { [param: string]: string }) { - return async (req: MedusaRequest, _, next: MedusaNextFunction) => { + return async function paramsAsFiltersMiddleware( + req: MedusaRequest, + _: MedusaResponse, + next: MedusaNextFunction + ) { for (const [param, paramValue] of Object.entries(req.params)) { if (mappings[param]) { req.filterableFields[mappings[param]] = paramValue diff --git a/packages/core/framework/src/http/middlewares/bodyparser.ts b/packages/core/framework/src/http/middlewares/bodyparser.ts new file mode 100644 index 0000000000..93dca4c1e7 --- /dev/null +++ b/packages/core/framework/src/http/middlewares/bodyparser.ts @@ -0,0 +1,96 @@ +import { memoize } from "lodash" +import logger from "@medusajs/cli/dist/reporter" +import { json, NextFunction, RequestHandler, text, urlencoded } from "express" + +import type { + MedusaRequest, + MedusaResponse, + MiddlewareVerb, + ParserConfigArgs, + MiddlewareFunction, + BodyParserConfigRoute, +} from "../types" +import type { RoutesFinder } from "../routes-finder" + +/** + * Parsers to use for parsing the HTTP request body + */ +const parsers = { + json: memoize(function jsonParserMiddleware(options?: ParserConfigArgs) { + return json({ + limit: options?.sizeLimit, + verify: options?.preserveRawBody + ? (req: MedusaRequest, res: MedusaResponse, buf: Buffer) => { + req.rawBody = buf + } + : undefined, + }) + }), + text: memoize(function textParser(options?: ParserConfigArgs) { + return text({ + limit: options?.sizeLimit, + }) + }), + urlencoded: memoize(function urlencodedParserMiddleware( + options?: ParserConfigArgs + ) { + return urlencoded({ + limit: options?.sizeLimit, + extended: true, + }) + }), +} + +/** + * Creates the bodyparser middlewares stack that creates custom bodyparsers + * during an HTTP request based upon the defined config. The bodyparser + * instances are cached for re-use. + */ +export function createBodyParserMiddlewaresStack( + route: string, + routesFinder: RoutesFinder, + tracer?: ( + handler: RequestHandler | MiddlewareFunction, + route: { route: string; method?: string } + ) => RequestHandler | MiddlewareFunction +) { + return (["json", "text", "urlencoded"] as (keyof typeof parsers)[]).map( + (parser) => { + function bodyParser( + req: MedusaRequest, + res: MedusaResponse, + next: NextFunction + ) { + const matchingRoute = routesFinder.find( + req.path, + req.method as MiddlewareVerb + ) + const parserMiddleware = parsers[parser] + + if (!matchingRoute) { + return parserMiddleware()(req, res, next) + } + + if (matchingRoute.config === false) { + logger.debug( + `skipping ${parser} bodyparser middleware ${req.method} ${req.path}` + ) + return next() + } + + logger.debug( + `using custom ${parser} bodyparser config ${req.method} ${req.path}` + ) + return parserMiddleware(matchingRoute.config)(req, res, next) + } + + Object.defineProperty(bodyParser, "name", { + value: `${parser}BodyParser`, + }) + + return ( + tracer ? tracer(bodyParser, { route }) : bodyParser + ) as RequestHandler + } + ) +} diff --git a/packages/core/framework/src/http/middlewares/clear-filters-by-key.ts b/packages/core/framework/src/http/middlewares/clear-filters-by-key.ts index 858ae13e6e..53ded51976 100644 --- a/packages/core/framework/src/http/middlewares/clear-filters-by-key.ts +++ b/packages/core/framework/src/http/middlewares/clear-filters-by-key.ts @@ -1,7 +1,15 @@ -import { MedusaNextFunction, MedusaRequest } from "../types" +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "../types" export function clearFiltersByKey(keys: string[]) { - return async (req: MedusaRequest, _, next: MedusaNextFunction) => { + return async function clearFiltersByKeyMiddleware( + req: MedusaRequest, + _: MedusaResponse, + next: MedusaNextFunction + ) { keys.forEach((key) => { delete req.filterableFields[key] }) diff --git a/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts b/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts index 9447ce3176..e4ab7ebcb5 100644 --- a/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts +++ b/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts @@ -5,7 +5,7 @@ import { MedusaError, PUBLISHABLE_KEY_HEADER, } from "@medusajs/utils" -import { +import type { MedusaNextFunction, MedusaResponse, MedusaStoreRequest, @@ -13,7 +13,7 @@ import { export async function ensurePublishableApiKeyMiddleware( req: MedusaStoreRequest, - _res: MedusaResponse, + _: MedusaResponse, next: MedusaNextFunction ) { const publishableApiKey = req.get(PUBLISHABLE_KEY_HEADER) diff --git a/packages/core/framework/src/http/middlewares/error-handler.ts b/packages/core/framework/src/http/middlewares/error-handler.ts index 41bf9f70e6..24cb12656f 100644 --- a/packages/core/framework/src/http/middlewares/error-handler.ts +++ b/packages/core/framework/src/http/middlewares/error-handler.ts @@ -1,4 +1,4 @@ -import { NextFunction, Response } from "express" +import { NextFunction, ErrorRequestHandler, Response } from "express" import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils" import { formatException } from "./exception-formatter" @@ -13,12 +13,12 @@ const INVALID_REQUEST_ERROR = "invalid_request_error" const INVALID_STATE_ERROR = "invalid_state_error" export function errorHandler() { - return ( + return function coreErrorHandler( err: MedusaError, req: MedusaRequest, res: Response, - next: NextFunction - ) => { + _: NextFunction + ) { const logger = req.scope.resolve(ContainerRegistrationKeys.LOGGER) err = formatException(err) @@ -76,7 +76,7 @@ export function errorHandler() { } res.status(statusCode).json(errObj) - } + } as unknown as ErrorRequestHandler } /** diff --git a/packages/core/framework/src/http/router.ts b/packages/core/framework/src/http/router.ts index ed375548e9..310011c8a5 100644 --- a/packages/core/framework/src/http/router.ts +++ b/packages/core/framework/src/http/router.ts @@ -1,275 +1,32 @@ -import { - dynamicImport, - parseCorsOrigins, - promiseAll, - readDirRecursive, - resolveExports, - wrapHandler, -} from "@medusajs/utils" -import cors from "cors" -import { - type Express, - json, - RequestHandler, - Router, - text, - urlencoded, -} from "express" -import { Dirent } from "fs" -import { readdir } from "fs/promises" -import { extname, join, parse, sep } from "path" -import { configManager } from "../config" -import { logger } from "../logger" -import { authenticate, AuthType, errorHandler } from "./middlewares" -import { ensurePublishableApiKeyMiddleware } from "./middlewares/ensure-publishable-api-key" -import { - GlobalMiddlewareDescriptor, - HTTP_METHODS, - MedusaNextFunction, +import logger from "@medusajs/cli/dist/reporter" +import cors, { CorsOptions } from "cors" +import { parseCorsOrigins } from "@medusajs/utils" +import type { Express, RequestHandler, ErrorRequestHandler } from "express" +import type { MedusaRequest, MedusaResponse, - MiddlewareFunction, - MiddlewareRoute, - MiddlewaresConfig, MiddlewareVerb, - ParserConfigArgs, - RouteConfig, RouteDescriptor, + MiddlewareFunction, + MedusaNextFunction, + MiddlewareDescriptor, + BodyParserConfigRoute, RouteHandler, - RouteVerb, } from "./types" + +import { RoutesLoader } from "./routes-loader" +import { RoutesFinder } from "./routes-finder" +import { RoutesSorter } from "./routes-sorter" +import { wrapHandler } from "./utils/wrap-handler" +import { authenticate, AuthType } from "./middlewares" +import { errorHandler } from "./middlewares/error-handler" import { RestrictedFields } from "./utils/restricted-fields" +import { MiddlewareFileLoader } from "./middleware-file-loader" +import { createBodyParserMiddlewaresStack } from "./middlewares/bodyparser" +import { ensurePublishableApiKeyMiddleware } from "./middlewares/ensure-publishable-api-key" +import { configManager } from "../config" -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 }), - ] -} - -function createCorsOptions(origin: string): cors.CorsOptions { - return { - origin: parseCorsOrigins(origin), - credentials: true, - } -} - -function applyCors( - router: Router, - route: string | RegExp, - corsConfig: cors.CorsOptions -) { - router.use(route, cors(corsConfig)) -} - -function getRouteContext( - path: string | RegExp -): "admin" | "store" | "auth" | null { - /** - * We cannot reliably guess the route context from a regex, so we skip it. - */ - if (path instanceof RegExp) { - return null - } - - if (path.startsWith("/admin")) { - return "admin" - } - if (path.startsWith("/store")) { - return "store" - } - if (path.startsWith("/auth")) { - return "auth" - } - - return null -} - -// TODO this router would need a proper rework, but it is out of scope right now - -export class ApiRoutesLoader { - /** - * Map of router path and its descriptor - * @private - */ - #routesMap = new Map() - - /** - * Global middleware descriptors - * @private - */ - #globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined - - /** - * An express instance - * @private - */ - readonly #app: Express - - /** - * A router to assign the route to - * @private - */ - readonly #router: Router - - /** - * An eventual activity id for information tracking - * @private - */ - readonly #activityId?: string - - /** - * The list of file names to exclude from the routes scan - * @private - */ - #excludes: RegExp[] = [ - /\.DS_Store/, - /(\.ts\.map|\.js\.map|\.d\.ts|\.md)/, - /^_[^/\\]*(\.[^/\\]+)?$/, - ] - - /** - * Path from where to load the routes from - * @private - */ - readonly #sourceDir: string - +export class ApiLoader { /** * Wrap the original route handler implementation for * instrumentation. @@ -288,737 +45,106 @@ export class ApiRoutesLoader { route: { route: string; method?: string } ) => RequestHandler | MiddlewareFunction - constructor({ - app, - activityId, - sourceDir, - }: { - app: Express - activityId?: string - sourceDir: string - }) { - this.#app = app - this.#router = Router() - this.#activityId = activityId - this.#sourceDir = sourceDir - } - - /** - * 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} - */ - protected async createRoutesConfig(): Promise { - await promiseAll( - [...this.#routesMap.values()].map(async (descriptor: RouteDescriptor) => { - const absolutePath = descriptor.absolutePath - const route = descriptor.route - - return await dynamicImport(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(path: string) { - const descriptor: RouteDescriptor = { - absolutePath: path, - relativePath: path, - route: "", - priority: Infinity, - } - - const childPath = path.replace(this.#sourceDir, "") - descriptor.relativePath = childPath - - let routeToParse = childPath - - const pathSegments = routeToParse.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(path, descriptor) - } - - protected async createMiddlewaresDescriptor() { - const filePaths = await readdir(this.#sourceDir) - - const filteredFilePaths = filePaths.filter((path) => { - const pathToCheck = path.replace(this.#sourceDir, "") - return !pathToCheck - .split(sep) - .some((segment) => - this.#excludes.some((exclude) => exclude.test(segment)) - ) - }) - - const middlewareFilePath = filteredFilePaths.find((file) => { - return file.replace(/\.[^/.]+$/, "") === MIDDLEWARES_NAME - }) - - if (!middlewareFilePath) { - log({ - activityId: this.#activityId, - message: `No middleware files found in ${ - this.#sourceDir - }. Skipping middleware configuration.`, - }) - return - } - - const absolutePath = join(this.#sourceDir, middlewareFilePath) - - await dynamicImport(absolutePath).then((import_) => { - const middlewaresConfig = resolveExports(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 - }) - } - - protected async createRoutesMap(): Promise { - await promiseAll( - await readDirRecursive(this.#sourceDir).then((entries) => { - const fileEntries = entries.filter((entry: Dirent) => { - const fullPathFromSource = join(entry.path, entry.name).replace( - this.#sourceDir, - "" - ) - const isExcluded = fullPathFromSource - .split(sep) - .some((segment) => - this.#excludes.some((exclude) => exclude.test(segment)) - ) - - return ( - !entry.isDirectory() && - !isExcluded && - parse(entry.name).name === ROUTE_NAME - ) - }) - - return fileEntries.map(async (entry: Dirent) => { - const path = join(entry.path, entry.name) - return this.createRoutesDescriptor(path) - }) - }) - ) - } - - /** - * 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 - } - - /** - * Applies middleware that checks if a valid publishable key is set on store request - */ - applyStorePublishableKeyMiddleware(route: string | RegExp) { - let middleware = ensurePublishableApiKeyMiddleware as unknown as - | RequestHandler - | MiddlewareFunction - - if (ApiRoutesLoader.traceMiddleware) { - middleware = ApiRoutesLoader.traceMiddleware(middleware, { - route: String(route), - }) - } - - this.#router.use(route, middleware as RequestHandler) - } - - /** - * Applies the route middleware on a route. Encapsulates the logic - * needed to pass the middleware via the trace calls - */ - applyAuthMiddleware( - route: string | RegExp, - actorType: string | string[], - authType: AuthType | AuthType[], - options?: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } - ) { - let authenticateMiddleware: RequestHandler | MiddlewareFunction = - authenticate(actorType, authType, options) - - if (ApiRoutesLoader.traceMiddleware) { - authenticateMiddleware = ApiRoutesLoader.traceMiddleware( - authenticateMiddleware, - { route: String(route) } - ) - } - - this.#router.use(route, authenticateMiddleware as RequestHandler) - } - - /** - * 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()]) - const handledPaths = new Set() - const middlewarePaths = new Set() - - const globalRoutes = this.#globalMiddlewaresDescriptor?.config?.routes ?? [] - - for (const route of globalRoutes) { - middlewarePaths.add(route.matcher) - } - - for (const descriptor of prioritizedRoutes) { - if (!descriptor.config?.routes?.length) { - continue - } - - const config = descriptor.config - handledPaths.add(descriptor.route) - - if (config.shouldAppendAdminCors) { - applyCors( - this.#router, - descriptor.route, - createCorsOptions(configManager.config.projectConfig.http.adminCors) - ) - } - - if (config.shouldAppendAuthCors) { - applyCors( - this.#router, - descriptor.route, - createCorsOptions(configManager.config.projectConfig.http.authCors) - ) - } - - if (config.shouldAppendStoreCors) { - applyCors( - this.#router, - descriptor.route, - createCorsOptions(configManager.config.projectConfig.http.storeCors) - ) - } - - // Apply other middlewares - if (config.routeType === "store") { - this.applyStorePublishableKeyMiddleware(descriptor.route) - } - - // 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.applyAuthMiddleware( - descriptor.route, - "customer", - ["bearer", "session"], - { - allowUnauthenticated: true, - } - ) - } - - if (!config.optedOutOfAuth && config.routeType === "admin") { - this.applyAuthMiddleware(descriptor.route, "user", [ - "bearer", - "session", - "api-key", - ]) - } - - for (const route of descriptor.config.routes) { - /** - * Apply the body parser middleware if the route - * has not opted out of it. - */ - this.applyBodyParserMiddleware(descriptor.route, route.method!) - } - } - - /** - * Apply CORS and auth middleware for paths defined in global middleware but not already handled by routes. - */ - for (const path of middlewarePaths) { - if (typeof path === "string" && handledPaths.has(path)) { - continue - } - - const context = getRouteContext(path) - - if (!context) { - continue - } - - switch (context) { - case "admin": - applyCors( - this.#router, - path, - createCorsOptions(configManager.config.projectConfig.http.adminCors) - ) - this.applyAuthMiddleware(path, "user", [ - "bearer", - "session", - "api-key", - ]) - break - case "store": - applyCors( - this.#router, - path, - createCorsOptions(configManager.config.projectConfig.http.storeCors) - ) - this.applyStorePublishableKeyMiddleware(path) - break - case "auth": - applyCors( - this.#router, - path, - createCorsOptions(configManager.config.projectConfig.http.authCors) - ) - break - } - } - } - - /** - * 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 as any) - return - } - - /** - * If the user has not provided a custom error handler then use the - * default one. - */ - this.#router.use(errorHandler() as any) - } - - protected async registerRoutes(): Promise { - 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 - }`, - }) - - let handler: RequestHandler | RouteHandler = route.handler - - /** - * Give handler to the trace route handler for instrumentation - * from outside-in. - */ - if (ApiRoutesLoader.traceRoute) { - handler = ApiRoutesLoader.traceRoute(handler, { - method: route.method!, - route: descriptor.route, - }) - } - - /** - * If the user hasn't opted out of error handling then - * we wrap the handler in a try/catch block. - */ - if (shouldWrapHandler) { - handler = wrapHandler(handler as Parameters[0]) - } - - this.#router[route.method!.toLowerCase()](descriptor.route, handler) - } - } - } - - protected async registerMiddlewares(): Promise { - 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}`, - }) - - let middlewares = route.middlewares - if (ApiRoutesLoader.traceMiddleware) { - middlewares = middlewares.map((middleware) => - ApiRoutesLoader.traceMiddleware!(middleware, { - route: String(route.matcher), - method, - }) - ) - } - - this.#router[method.toLowerCase()](route.matcher, ...middlewares) - } - } - } - - async load() { - performance && performance.mark("file-base-routing-start" + this.#sourceDir) - - 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.#sourceDir) - } catch (_error) { - apiExists = false - } - - if (apiExists) { - await this.createMiddlewaresDescriptor() - - await this.createRoutesMap() - 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.#sourceDir) - const timeSpent = - performance && - performance - .measure( - "file-base-routing-measure" + this.#sourceDir, - "file-base-routing-start" + this.#sourceDir, - "file-base-routing-end" + this.#sourceDir - ) - ?.duration?.toFixed(2) - - log({ - activityId: this.#activityId, - message: `Routes loaded in ${timeSpent} ms`, - }) - - this.#routesMap.clear() - this.#globalMiddlewaresDescriptor = undefined - } -} - -export class RoutesLoader { /** * An express instance * @private */ readonly #app: Express - /** - * An eventual activity id for information tracking - * @private - */ - readonly #activityId?: string - /** * Path from where to load the routes from * @private */ - readonly #sourceDir: string | string[] + readonly #sourceDirs: string[] constructor({ app, - activityId, sourceDir, baseRestrictedFields = [], }: { app: Express - activityId?: string sourceDir: string | string[] baseRestrictedFields?: string[] }) { this.#app = app - this.#activityId = activityId - this.#sourceDir = sourceDir - - this.#assignRestrictedFields(baseRestrictedFields) + this.#sourceDirs = Array.isArray(sourceDir) ? sourceDir : [sourceDir] + this.#assignRestrictedFields(baseRestrictedFields ?? []) } + /** + * Loads routes, middleware, bodyParserConfig routes, routes that have + * opted out for Auth and CORS and the error handler. + */ + async #loadHttpResources() { + const routesLoader = new RoutesLoader() + const middlewareLoader = new MiddlewareFileLoader() + + for (let dir of this.#sourceDirs) { + await routesLoader.scanDir(dir) + await middlewareLoader.scanDir(dir) + } + + return { + routes: routesLoader.getRoutes(), + routesFinder: new RoutesFinder(), + middlewares: middlewareLoader.getMiddlewares(), + errorHandler: middlewareLoader.getErrorHandler() as + | ErrorRequestHandler + | undefined, + bodyParserConfigRoutes: middlewareLoader.getBodyParserConfigRoutes(), + } + } + + /** + * Registers a middleware or a route handler with Express + */ + #registerExpressHandler( + route: MiddlewareDescriptor | RouteDescriptor | RouteDescriptor + ) { + if ("isRoute" in route) { + logger.debug(`registering route ${route.method} ${route.matcher}`) + const handler = ApiLoader.traceRoute + ? ApiLoader.traceRoute(wrapHandler(route.handler), { + route: route.matcher, + method: route.method, + }) + : wrapHandler(route.handler) + + this.#app[route.method.toLowerCase()](route.matcher, handler) + return + } + + if (!route.methods) { + logger.debug(`registering global middleware for ${route.matcher}`) + const handler = ApiLoader.traceMiddleware + ? (ApiLoader.traceMiddleware(wrapHandler(route.handler), { + route: route.matcher, + }) as RequestHandler) + : (wrapHandler(route.handler) as RequestHandler) + + this.#app.use(route.matcher, handler) + return + } + + const methods = Array.isArray(route.methods) + ? route.methods + : [route.methods] + methods.forEach((method) => { + logger.debug(`registering route middleware ${method} ${route.matcher}`) + const handler = ApiLoader.traceMiddleware + ? (ApiLoader.traceMiddleware(wrapHandler(route.handler), { + route: route.matcher, + method: method, + }) as RequestHandler) + : wrapHandler(route.handler) + + this.#app[method.toLowerCase()](route.matcher, handler) + }) + } + + /** + * Registers the middleware for restricted fields + */ #assignRestrictedFields(baseRestrictedFields: string[]) { this.#app.use("/store", (( req: MedusaRequest, @@ -1040,21 +166,219 @@ export class RoutesLoader { }) as unknown as RequestHandler) } + /** + * Creates the options for the Cors middleware + */ + #createCorsOptions(origin: string): CorsOptions { + return { + origin: parseCorsOrigins(origin), + credentials: true, + } + } + + /** + * Assigns global cors middleware for a given prefix + */ + #applyCorsMiddleware( + routesFinder: RoutesFinder, + namespace: string, + toggleKey: + | "shouldAppendAdminCors" + | "shouldAppendAuthCors" + | "shouldAppendStoreCors", + corsOptions: CorsOptions + ) { + const corsFn = cors(corsOptions) + const corsMiddleware: RequestHandler = function corsMiddleware( + req, + res, + next + ) { + const path = `${namespace}${req.path}` + const matchingRoute = routesFinder.find( + path, + req.method as MiddlewareVerb + ) + if (matchingRoute && matchingRoute[toggleKey] === true) { + return corsFn(req, res, next) + } + + logger.debug(`Skipping CORS middleware ${req.method} ${path}`) + return next() + } + + this.#app.use( + namespace, + ApiLoader.traceMiddleware + ? (ApiLoader.traceMiddleware(corsMiddleware, { + route: namespace, + }) as RequestHandler) + : cors(corsOptions) + ) + } + + /** + * Applies the route middleware on a route. Encapsulates the logic + * needed to pass the middleware via the trace calls + */ + #applyAuthMiddleware( + routesFinder: RoutesFinder, + namespace: string, + actorType: string | string[], + authType: AuthType | AuthType[], + options?: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } + ) { + logger.debug(`Registering auth middleware for prefix ${namespace}`) + + const originalFn = authenticate(actorType, authType, options) + const authMiddleware: RequestHandler = function authMiddleware( + req, + res, + next + ) { + const path = `${namespace}${req.path}` + const matchingRoute = routesFinder.find( + path, + req.method as MiddlewareVerb + ) + if (matchingRoute && matchingRoute.optedOutOfAuth) { + logger.debug(`Skipping auth ${req.method} ${path}`) + return next() + } + + logger.debug(`Authenticating route ${req.method} ${path}`) + return originalFn(req, res, next) + } + + this.#app.use( + namespace, + ApiLoader.traceMiddleware + ? (ApiLoader.traceMiddleware(authMiddleware, { + route: namespace, + }) as RequestHandler) + : authMiddleware + ) + } + + /** + * Apply the most specific body parser middleware to the router + */ + #applyBodyParserMiddleware( + namespace: string, + routesFinder: RoutesFinder + ): void { + logger.debug(`Registering bodyparser middleware for prefix ${namespace}`) + this.#app.use( + namespace, + createBodyParserMiddlewaresStack( + namespace, + routesFinder, + ApiLoader.traceMiddleware + ) + ) + } + + /** + * Applies the middleware to authenticate the headers to contain + * a `x-publishable-key` header + */ + #applyStorePublishableKeyMiddleware(namespace: string) { + logger.debug( + `Registering publishable key middleware for namespace ${namespace}` + ) + let middleware = ApiLoader.traceMiddleware + ? ApiLoader.traceMiddleware(ensurePublishableApiKeyMiddleware, { + route: namespace, + }) + : ensurePublishableApiKeyMiddleware + + this.#app.use(namespace, middleware as RequestHandler) + } + async load() { - const normalizedSourcePath = Array.isArray(this.#sourceDir) - ? this.#sourceDir - : [this.#sourceDir] + const { + errorHandler: sourceErrorHandler, + middlewares, + routes, + routesFinder, + bodyParserConfigRoutes, + } = await this.#loadHttpResources() - const promises = normalizedSourcePath.map(async (sourcePath) => { - const apiRoutesLoader = new ApiRoutesLoader({ - app: this.#app, - activityId: this.#activityId, - sourceDir: sourcePath, - }) + /** + * Parse request body on all the requests and use the routes finder + * to pick the best matching config for the given route. + */ + const bodyParserRoutesFinder = new RoutesFinder( + new RoutesSorter(bodyParserConfigRoutes).sort([ + "static", + "params", + "regex", + "wildcard", + "global", + ]) + ) + this.#applyBodyParserMiddleware("/", bodyParserRoutesFinder) - await apiRoutesLoader.load() + /** + * CORS and Auth setup for admin routes + */ + this.#applyCorsMiddleware( + routesFinder, + "/admin", + "shouldAppendAdminCors", + this.#createCorsOptions(configManager.config.projectConfig.http.adminCors) + ) + this.#applyAuthMiddleware(routesFinder, "/admin", "user", [ + "bearer", + "session", + "api-key", + ]) + + /** + * Publishable key check, CORS and auth setup for store routes. + */ + this.#applyStorePublishableKeyMiddleware("/store") + this.#applyCorsMiddleware( + routesFinder, + "/store", + "shouldAppendStoreCors", + this.#createCorsOptions(configManager.config.projectConfig.http.storeCors) + ) + this.#applyAuthMiddleware( + routesFinder, + "/store", + "customer", + ["bearer", "session"], + { + allowUnauthenticated: true, + } + ) + + /** + * Apply CORS middleware for "/auth" routes + */ + this.#applyCorsMiddleware( + routesFinder, + "/auth", + "shouldAppendAuthCors", + this.#createCorsOptions(configManager.config.projectConfig.http.authCors) + ) + + const collectionToSort = ([] as (MiddlewareDescriptor | RouteDescriptor)[]) + .concat(middlewares) + .concat(routes) + + const sortedRoutes = new RoutesSorter(collectionToSort).sort() + sortedRoutes.forEach((route) => { + if ("isRoute" in route) { + routesFinder.add(route) + } + this.#registerExpressHandler(route) }) - await promiseAll(promises) + /** + * Registering error handler as the final handler + */ + this.#app.use(sourceErrorHandler ?? errorHandler()) } } diff --git a/packages/core/framework/src/http/routes-finder.ts b/packages/core/framework/src/http/routes-finder.ts new file mode 100644 index 0000000000..5d8edd267e --- /dev/null +++ b/packages/core/framework/src/http/routes-finder.ts @@ -0,0 +1,64 @@ +import pathToRegexp from "path-to-regexp" +import type { MiddlewareVerb, RouteVerb } from "./types" + +export class RoutesFinder< + T extends + | { matcher: string; methods: MiddlewareVerb | MiddlewareVerb[] } + | { matcher: string; method: RouteVerb } +> { + /** + * Cache of existing matches to avoid regex tests on every + * single HTTP request + */ + #existingMatches: Map< + string, + | (T & { + matchRegex: RegExp + }) + | null + > = new Map() + + /** + * Collection of registered routes + */ + #routes: (T & { + matchRegex: RegExp + })[] = [] + + constructor(routes?: T[]) { + if (routes) { + routes.forEach((route) => this.add(route)) + } + } + + /** + * Register route for lookup + */ + add(route: T) { + this.#routes.push({ + ...route, + matchRegex: pathToRegexp(route.matcher), + }) + } + + /** + * Get the matching route for a given HTTP method and URL + */ + find(url: string, method: MiddlewareVerb) { + const key = `${method}:${url}` + if (this.#existingMatches.has(key)) { + return this.#existingMatches.get(key) + } + + const result = + this.#routes.find((route) => { + if ("methods" in route) { + return route.methods.includes(method) && route.matchRegex.test(url) + } + return route.method === method && route.matchRegex.test(url) + }) ?? null + + this.#existingMatches.set(key, result) + return result + } +} diff --git a/packages/core/framework/src/http/routes-loader.ts b/packages/core/framework/src/http/routes-loader.ts index 4edf59c0ab..bcf4fc58bf 100644 --- a/packages/core/framework/src/http/routes-loader.ts +++ b/packages/core/framework/src/http/routes-loader.ts @@ -1,12 +1,7 @@ import { join, parse, sep } from "path" import { dynamicImport, readDirRecursive } from "@medusajs/utils" import { logger } from "../logger" -import { - type RouteVerb, - HTTP_METHODS, - type ScannedRouteDescriptor, - type FileSystemRouteDescriptor, -} from "./types" +import { type RouteVerb, HTTP_METHODS, type RouteDescriptor } from "./types" /** * File name that is used to indicate that the file is a route file @@ -43,21 +38,6 @@ const ADMIN_ROUTE_MATCH = /(\/admin$|\/admin\/)/ const STORE_ROUTE_MATCH = /(\/store$|\/store\/)/ const AUTH_ROUTE_MATCH = /(\/auth$|\/auth\/)/ -const log = ({ - activityId, - message, -}: { - activityId?: string - message: string -}) => { - if (activityId) { - logger.progress(activityId, message) - return - } - - logger.debug(message) -} - /** * Exposes to API to register routes manually or by scanning the filesystem from a * source directory. @@ -69,19 +49,7 @@ export class RoutesLoader { /** * Routes collected manually or by scanning directories */ - #routes: Record< - string, - Record - > = {} - - /** - * An eventual activity id for information tracking - */ - readonly #activityId?: string - - constructor({ activityId }: { activityId?: string }) { - this.#activityId = activityId - } + #routes: Record> = {} /** * Creates the route path from its relative file path. @@ -96,10 +64,9 @@ export class RoutesLoader { if (segment.startsWith("[")) { segment = segment.replace(PARAM_SEGMENT_MATCHER, (_, group) => { if (params[group]) { - log({ - activityId: this.#activityId, - message: `Duplicate parameters found in route ${relativePath} (${group})`, - }) + logger.debug( + `Duplicate parameters found in route ${relativePath} (${group})` + ) throw new Error( `Duplicate parameters found in route ${relativePath} (${group}). Make sure that all parameters are unique.` @@ -122,7 +89,7 @@ export class RoutesLoader { async #getRoutesForFile( routePath: string, absolutePath: string - ): Promise { + ): Promise { const routeExports = await dynamicImport(absolutePath) /** @@ -161,10 +128,9 @@ export class RoutesLoader { } if (!HTTP_METHODS.includes(key as RouteVerb)) { - log({ - activityId: this.#activityId, - message: `Skipping handler ${key} in ${absolutePath}. Invalid HTTP method: ${key}.`, - }) + logger.debug( + `Skipping handler ${key} in ${absolutePath}. Invalid HTTP method: ${key}.` + ) return false } @@ -172,14 +138,15 @@ export class RoutesLoader { }) .map((key) => { return { - route: routePath, + isRoute: true, + matcher: routePath, method: key as RouteVerb, handler: routeExports[key], optedOutOfAuth: !shouldAuthenticate, shouldAppendAdminCors: shouldApplyCors && routeType === "admin", shouldAppendAuthCors: shouldApplyCors && routeType === "auth", shouldAppendStoreCors: shouldApplyCors && routeType === "store", - } satisfies ScannedRouteDescriptor + } satisfies RouteDescriptor }) } @@ -232,18 +199,16 @@ export class RoutesLoader { /** * Register a route */ - registerRoute(route: ScannedRouteDescriptor | FileSystemRouteDescriptor) { - this.#routes[route.route] = this.#routes[route.route] ?? {} - const trackedRoute = this.#routes[route.route] + registerRoute(route: RouteDescriptor) { + this.#routes[route.matcher] = this.#routes[route.matcher] ?? {} + const trackedRoute = this.#routes[route.matcher] trackedRoute[route.method] = route } /** * Register one or more routes */ - registerRoutes( - routes: (ScannedRouteDescriptor | FileSystemRouteDescriptor)[] - ) { + registerRoutes(routes: RouteDescriptor[]) { routes.forEach((route) => this.registerRoute(route)) } @@ -252,14 +217,16 @@ export class RoutesLoader { * manually. */ getRoutes() { - return Object.keys(this.#routes).reduce< - (ScannedRouteDescriptor | FileSystemRouteDescriptor)[] - >((result, routePattern) => { - const methodsRoutes = this.#routes[routePattern] - Object.keys(methodsRoutes).forEach((method) => { - result.push(methodsRoutes[method]) - }) - return result - }, []) + return Object.keys(this.#routes).reduce( + (result, routePattern) => { + const methodsRoutes = this.#routes[routePattern] + Object.keys(methodsRoutes).forEach((method) => { + const route = methodsRoutes[method] + result.push(route) + }) + return result + }, + [] + ) } } diff --git a/packages/core/framework/src/http/routes-sorter.ts b/packages/core/framework/src/http/routes-sorter.ts index fa453539b8..a318608216 100644 --- a/packages/core/framework/src/http/routes-sorter.ts +++ b/packages/core/framework/src/http/routes-sorter.ts @@ -1,9 +1,9 @@ -import { MiddlewareVerb } from "./types" +import { MiddlewareVerb, RouteVerb } from "./types" /** * Route represents both the middleware/routes defined via the * "defineMiddlewares" method and the routes scanned from - * the filesystem. The later one's must be marked with "isAppRoute = true". + * the filesystem. */ type Route = { /** @@ -17,10 +17,9 @@ type Route = { handler?: any /** - * Must be true when the route is discovered via the fileystem - * scanning. + * The HTTP method specified as a single value */ - isAppRoute?: boolean + method?: RouteVerb /** * The HTTP methods this route is supposed to handle. @@ -48,39 +47,39 @@ type Route = { * - static * - params */ -type RoutesBranch = { +type RoutesBranch = { global: { - routes: Route[] + routes: T[] children?: { - [segment: string]: RoutesBranch + [segment: string]: RoutesBranch } } regex: { - routes: Route[] + routes: T[] children?: { - [segment: string]: RoutesBranch + [segment: string]: RoutesBranch } } wildcard: { - routes: Route[] + routes: T[] children?: { - [segment: string]: RoutesBranch + [segment: string]: RoutesBranch } } params: { - routes: Route[] + routes: T[] children?: { - [segment: string]: RoutesBranch + [segment: string]: RoutesBranch } } static: { - routes: Route[] + routes: T[] children?: { - [segment: string]: RoutesBranch + [segment: string]: RoutesBranch } } } @@ -91,29 +90,42 @@ type RoutesBranch = { * like structure and then sort them back to a flat array based upon the * priorities of different types of nodes. */ -export class RoutesSorter { +export class RoutesSorter { + /** + * The order in which the routes will be sorted. This + * can be overridden at the time of call the sort + * method. + */ + #orderBy: [ + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch + ] = ["global", "wildcard", "regex", "static", "params"] + /** * Input routes */ - #routesToProcess: Route[] + #routesToProcess: T[] /** * Intermediate tree representation for sorting routes */ #routesTree: { - [segment: string]: RoutesBranch + [segment: string]: RoutesBranch } = { root: this.#createBranch(), } - constructor(routes: Route[]) { + constructor(routes: T[]) { this.#routesToProcess = routes } /** * Creates an empty branch with known nodes */ - #createBranch(): RoutesBranch { + #createBranch(): RoutesBranch { return { global: { routes: [], @@ -162,14 +174,14 @@ export class RoutesSorter { * } * ``` */ - #processRoute(route: Route) { + #processRoute(route: T) { const segments = route.matcher.split("/").filter((s) => s.length) let parent = this.#routesTree["root"] segments.forEach((segment, index) => { - let bucket: keyof RoutesBranch = "static" + let bucket: keyof RoutesBranch = "static" - if (!route.methods) { + if (!route.methods && !route.method) { bucket = "global" } else if (segment.startsWith("*")) { bucket = "wildcard" @@ -194,40 +206,51 @@ export class RoutesSorter { /** * Returns an array of sorted routes for a given branch. */ - #sortBranch(routeBranch: { [segment: string]: RoutesBranch }) { + #sortBranch( + routeBranch: { [segment: string]: RoutesBranch }, + orderBy: [ + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch + ] + ) { const branchRoutes = Object.keys(routeBranch).reduce<{ - global: Route[] - wildcard: Route[] - regex: Route[] - params: Route[] - static: Route[] + global: T[] + wildcard: T[] + regex: T[] + params: T[] + static: T[] }>( (result, branchKey) => { const node = routeBranch[branchKey] result.global.push(...node.global.routes) if (node.global.children) { - result.global.push(...this.#sortBranch(node.global.children)) + result.global.push(...this.#sortBranch(node.global.children, orderBy)) } result.wildcard.push(...node.wildcard.routes) if (node.wildcard.children) { - result.wildcard.push(...this.#sortBranch(node.wildcard.children)) + result.wildcard.push( + ...this.#sortBranch(node.wildcard.children, orderBy) + ) } result.regex.push(...node.regex.routes) if (node.regex.children) { - result.regex.push(...this.#sortBranch(node.regex.children)) + result.regex.push(...this.#sortBranch(node.regex.children, orderBy)) } result.static.push(...node.static.routes) if (node.static.children) { - result.static.push(...this.#sortBranch(node.static.children)) + result.static.push(...this.#sortBranch(node.static.children, orderBy)) } result.params.push(...node.params.routes) if (node.params.children) { - result.params.push(...this.#sortBranch(node.params.children)) + result.params.push(...this.#sortBranch(node.params.children, orderBy)) } return result @@ -244,19 +267,33 @@ export class RoutesSorter { /** * Concatenating routes as per their priority. */ - const routes: Route[] = branchRoutes.global - .concat(branchRoutes.wildcard) - .concat(branchRoutes.regex) - .concat(branchRoutes.static) - .concat(branchRoutes.params) - return routes + return orderBy.reduce((result, branch) => { + result = result.concat(branchRoutes[branch]) + return result + }, []) } /** - * Sort the input routes + * Returns the intermediate representation of routes as a tree. */ - sort() { + getTree() { + return this.#routesTree + } + + /** + * Sort the input routes. You can optionally specify a custom + * orderBy array. Defaults to: ["global", "wildcard", "regex", "static", "params"] + */ + sort( + orderBy?: [ + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch, + keyof RoutesBranch + ] + ) { this.#routesToProcess.map((route) => this.#processRoute(route)) - return this.#sortBranch(this.#routesTree) + return this.#sortBranch(this.#routesTree, orderBy ?? this.#orderBy) } } diff --git a/packages/core/framework/src/http/types.ts b/packages/core/framework/src/http/types.ts index 1a062a1bd4..9480322aeb 100644 --- a/packages/core/framework/src/http/types.ts +++ b/packages/core/framework/src/http/types.ts @@ -34,20 +34,6 @@ export type AsyncRouteHandler = ( export 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) @@ -67,7 +53,11 @@ export type ParserConfigArgs = { export type ParserConfig = false | ParserConfigArgs export type MiddlewareRoute = { + /** + * @deprecated. Instead use {@link MiddlewareRoute.methods} + */ method?: MiddlewareVerb | MiddlewareVerb[] + methods?: MiddlewareVerb[] matcher: string | RegExp bodyParser?: ParserConfig middlewares?: MiddlewareFunction[] @@ -78,49 +68,38 @@ export type MiddlewaresConfig = { routes?: MiddlewareRoute[] } -export type RouteDescriptor = { - absolutePath: string - relativePath: string - route: string - priority: number - config?: RouteConfig -} - /** * Route descriptor refers represents a route either scanned * from the filesystem or registered manually. It does not * represent a middleware */ -export type ScannedRouteDescriptor = { - route: string +export type RouteDescriptor = { + matcher: string method: RouteVerb handler: RouteHandler optedOutOfAuth: boolean + isRoute: true routeType?: "admin" | "store" | "auth" + absolutePath?: string + relativePath?: string shouldAppendAdminCors: boolean shouldAppendStoreCors: boolean shouldAppendAuthCors: boolean } /** - * FileSystem route description represents a route scanned from - * the filesystem + * Represents a middleware */ -export type FileSystemRouteDescriptor = ScannedRouteDescriptor & { - absolutePath: string - relativePath: string -} - -export type ScannedMiddlewareDescriptor = { +export type MiddlewareDescriptor = { matcher: string - method?: MiddlewareVerb | MiddlewareVerb[] + methods?: MiddlewareVerb | MiddlewareVerb[] handler: MiddlewareFunction } export type BodyParserConfigRoute = { matcher: string - method?: MiddlewareVerb | MiddlewareVerb[] - config?: ParserConfig + methods: MiddlewareVerb | MiddlewareVerb[] + config: ParserConfig } export type GlobalMiddlewareDescriptor = { diff --git a/packages/core/framework/src/http/utils/define-middlewares.ts b/packages/core/framework/src/http/utils/define-middlewares.ts index eba76d36b8..ed2a2c230d 100644 --- a/packages/core/framework/src/http/utils/define-middlewares.ts +++ b/packages/core/framework/src/http/utils/define-middlewares.ts @@ -16,7 +16,11 @@ import zod, { ZodRawShape } from "zod" */ export function defineMiddlewares< Route extends { + /** + * @deprecated. Instead use {@link MiddlewareRoute.methods} + */ method?: MiddlewareVerb | MiddlewareVerb[] + methods?: MiddlewareVerb[] matcher: string | RegExp bodyParser?: ParserConfig additionalDataValidator?: ZodRawShape @@ -38,7 +42,8 @@ export function defineMiddlewares< return { errorHandler, routes: routes.map((route) => { - const { middlewares, additionalDataValidator, ...rest } = route + let { middlewares, method, methods, additionalDataValidator, ...rest } = + route const customMiddleware: MedusaRequestHandler[] = [] /** @@ -54,8 +59,13 @@ export function defineMiddlewares< }) } + if (!methods) { + methods = Array.isArray(method) ? method : method ? [method] : method + } + return { ...rest, + methods, middlewares: customMiddleware.concat(middlewares || []), } }), diff --git a/packages/core/framework/src/http/utils/maybe-apply-link-filter.ts b/packages/core/framework/src/http/utils/maybe-apply-link-filter.ts index c7f9f73e88..9f772f4466 100644 --- a/packages/core/framework/src/http/utils/maybe-apply-link-filter.ts +++ b/packages/core/framework/src/http/utils/maybe-apply-link-filter.ts @@ -3,7 +3,11 @@ import { ContainerRegistrationKeys, remoteQueryObjectFromString, } from "@medusajs/utils" -import { MedusaNextFunction, MedusaRequest } from "../types" +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "../types" export function maybeApplyLinkFilter({ entryPoint, @@ -13,7 +17,7 @@ export function maybeApplyLinkFilter({ }) { return async function linkFilter( req: MedusaRequest, - _, + _: MedusaResponse, next: MedusaNextFunction ) { const filterableFields = req.filterableFields diff --git a/packages/core/framework/src/http/utils/wrap-handler.ts b/packages/core/framework/src/http/utils/wrap-handler.ts new file mode 100644 index 0000000000..420bd3bd0e --- /dev/null +++ b/packages/core/framework/src/http/utils/wrap-handler.ts @@ -0,0 +1,38 @@ +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + MiddlewareFunction, + RouteHandler, +} from "../types" + +export const wrapHandler = ( + fn: T +) => { + async function wrappedHandler( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) { + const req_ = req as MedusaRequest & { errors?: Error[] } + if (req_?.errors?.length) { + return res.status(400).json({ + errors: req_.errors, + message: + "Provided request body contains errors. Please check the data and retry the request", + }) + } + + try { + return await fn(req, res, next) + } catch (err) { + console.log(err) + next(err) + } + } + + if (fn.name) { + Object.defineProperty(wrappedHandler, "name", { value: fn.name }) + } + return wrappedHandler as T +} diff --git a/packages/medusa/src/api/admin/inventory-items/middlewares.ts b/packages/medusa/src/api/admin/inventory-items/middlewares.ts index 89d77ddd84..4abb38f475 100644 --- a/packages/medusa/src/api/admin/inventory-items/middlewares.ts +++ b/packages/medusa/src/api/admin/inventory-items/middlewares.ts @@ -2,7 +2,7 @@ import { validateAndTransformBody, validateAndTransformQuery, } from "@medusajs/framework" -import { MiddlewareRoute, unlessPath } from "@medusajs/framework/http" +import { MiddlewareRoute } from "@medusajs/framework/http" import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares" import * as QueryConfig from "./query-config" import { @@ -116,12 +116,9 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [ method: ["DELETE"], matcher: "/admin/inventory-items/:id/location-levels/:location_id", middlewares: [ - unlessPath( - /.*\/location-levels\/batch/, - validateAndTransformQuery( - AdminGetInventoryItemParams, - QueryConfig.retrieveTransformQueryConfig - ) + validateAndTransformQuery( + AdminGetInventoryItemParams, + QueryConfig.retrieveTransformQueryConfig ), ], }, @@ -129,16 +126,10 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [ method: ["POST"], matcher: "/admin/inventory-items/:id/location-levels/:location_id", middlewares: [ - unlessPath( - /.*\/location-levels\/batch/, - validateAndTransformBody(AdminUpdateInventoryLocationLevel) - ), - unlessPath( - /.*\/location-levels\/batch/, - validateAndTransformQuery( - AdminGetInventoryItemParams, - QueryConfig.retrieveTransformQueryConfig - ) + validateAndTransformBody(AdminUpdateInventoryLocationLevel), + validateAndTransformQuery( + AdminGetInventoryItemParams, + QueryConfig.retrieveTransformQueryConfig ), ], }, diff --git a/packages/medusa/src/api/admin/payments/middlewares.ts b/packages/medusa/src/api/admin/payments/middlewares.ts index c3bf4484ce..8d24f3efe8 100644 --- a/packages/medusa/src/api/admin/payments/middlewares.ts +++ b/packages/medusa/src/api/admin/payments/middlewares.ts @@ -1,4 +1,4 @@ -import { MiddlewareRoute, unlessPath } from "@medusajs/framework/http" +import { MiddlewareRoute } from "@medusajs/framework/http" import { validateAndTransformBody, validateAndTransformQuery, @@ -37,12 +37,9 @@ export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/admin/payments/:id", middlewares: [ - unlessPath( - /.*\/payments\/payment-providers/, - validateAndTransformQuery( - AdminGetPaymentParams, - queryConfig.retrieveTransformQueryConfig - ) + validateAndTransformQuery( + AdminGetPaymentParams, + queryConfig.retrieveTransformQueryConfig ), ], }, diff --git a/packages/medusa/src/api/admin/products/middlewares.ts b/packages/medusa/src/api/admin/products/middlewares.ts index f71b14d62a..6759d02150 100644 --- a/packages/medusa/src/api/admin/products/middlewares.ts +++ b/packages/medusa/src/api/admin/products/middlewares.ts @@ -3,11 +3,7 @@ import { validateAndTransformBody, validateAndTransformQuery, } from "@medusajs/framework" -import { - maybeApplyLinkFilter, - MiddlewareRoute, - unlessPath, -} from "@medusajs/framework/http" +import { maybeApplyLinkFilter, MiddlewareRoute } from "@medusajs/framework/http" import multer from "multer" import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares" import { createBatchBody } from "../../utils/validators" @@ -117,12 +113,9 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/admin/products/:id", middlewares: [ - unlessPath( - /.*\/products\/(batch|export|import)/, - validateAndTransformQuery( - AdminGetProductParams, - QueryConfig.retrieveProductQueryConfig - ) + validateAndTransformQuery( + AdminGetProductParams, + QueryConfig.retrieveProductQueryConfig ), ], }, @@ -130,16 +123,10 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["POST"], matcher: "/admin/products/:id", middlewares: [ - unlessPath( - /.*\/products\/(batch|export|import)/, - validateAndTransformBody(AdminUpdateProduct) - ), - unlessPath( - /.*\/products\/(batch|export|import)/, - validateAndTransformQuery( - AdminGetProductParams, - QueryConfig.retrieveProductQueryConfig - ) + validateAndTransformBody(AdminUpdateProduct), + validateAndTransformQuery( + AdminGetProductParams, + QueryConfig.retrieveProductQueryConfig ), ], }, @@ -147,12 +134,9 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["DELETE"], matcher: "/admin/products/:id", middlewares: [ - unlessPath( - /.*\/products\/(batch|export|import)/, - validateAndTransformQuery( - AdminGetProductParams, - QueryConfig.retrieveProductQueryConfig - ) + validateAndTransformQuery( + AdminGetProductParams, + QueryConfig.retrieveProductQueryConfig ), ], }, @@ -198,12 +182,9 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/admin/products/:id/variants/:variant_id", middlewares: [ - unlessPath( - /.*\/variants\/batch/, - validateAndTransformQuery( - AdminGetProductVariantParams, - QueryConfig.retrieveVariantConfig - ) + validateAndTransformQuery( + AdminGetProductVariantParams, + QueryConfig.retrieveVariantConfig ), ], }, @@ -211,16 +192,10 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["POST"], matcher: "/admin/products/:id/variants/:variant_id", middlewares: [ - unlessPath( - /.*\/variants\/batch/, - validateAndTransformBody(AdminUpdateProductVariant) - ), - unlessPath( - /.*\/variants\/batch/, - validateAndTransformQuery( - AdminGetProductParams, - QueryConfig.retrieveProductQueryConfig - ) + validateAndTransformBody(AdminUpdateProductVariant), + validateAndTransformQuery( + AdminGetProductParams, + QueryConfig.retrieveProductQueryConfig ), ], }, @@ -228,12 +203,9 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["DELETE"], matcher: "/admin/products/:id/variants/:variant_id", middlewares: [ - unlessPath( - /.*\/variants\/batch/, - validateAndTransformQuery( - AdminGetProductParams, - QueryConfig.retrieveProductQueryConfig - ) + validateAndTransformQuery( + AdminGetProductParams, + QueryConfig.retrieveProductQueryConfig ), ], }, diff --git a/packages/medusa/src/api/admin/promotions/middlewares.ts b/packages/medusa/src/api/admin/promotions/middlewares.ts index de016c5eff..ba49dd28e2 100644 --- a/packages/medusa/src/api/admin/promotions/middlewares.ts +++ b/packages/medusa/src/api/admin/promotions/middlewares.ts @@ -2,7 +2,7 @@ import { validateAndTransformBody, validateAndTransformQuery, } from "@medusajs/framework" -import { MiddlewareRoute, unlessPath } from "@medusajs/framework/http" +import { MiddlewareRoute } from "@medusajs/framework/http" import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares" import { createBatchBody } from "../../utils/validators" import * as QueryConfig from "./query-config" @@ -65,12 +65,9 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/admin/promotions/:id/:rule_type", middlewares: [ - unlessPath( - /.*\/promotions\/rule-attribute-options/, - validateAndTransformQuery( - AdminGetPromotionRuleTypeParams, - QueryConfig.retrieveTransformQueryConfig - ) + validateAndTransformQuery( + AdminGetPromotionRuleTypeParams, + QueryConfig.retrieveTransformQueryConfig ), ], }, diff --git a/packages/medusa/src/api/admin/workflows-executions/middlewares.ts b/packages/medusa/src/api/admin/workflows-executions/middlewares.ts index ee1e70b3d2..fdcad2b274 100644 --- a/packages/medusa/src/api/admin/workflows-executions/middlewares.ts +++ b/packages/medusa/src/api/admin/workflows-executions/middlewares.ts @@ -44,18 +44,18 @@ export const adminWorkflowsExecutionsMiddlewares: MiddlewareRoute[] = [ }, { method: ["POST"], - matcher: "/admin/workflows-executions/:id/run", + matcher: "/admin/workflows-executions/:workflow_id/run", middlewares: [validateAndTransformBody(AdminCreateWorkflowsRun)], }, { method: ["POST"], - matcher: "/admin/workflows-executions/:id/steps/success", + matcher: "/admin/workflows-executions/:workflow_id/steps/success", middlewares: [validateAndTransformBody(AdminCreateWorkflowsAsyncResponse)], }, { method: ["POST"], - matcher: "/admin/workflows-executions/:id/steps/failure", + matcher: "/admin/workflows-executions/:workflow_id/steps/failure", middlewares: [validateAndTransformBody(AdminCreateWorkflowsAsyncResponse)], }, ] diff --git a/packages/medusa/src/instrumentation/index.ts b/packages/medusa/src/instrumentation/index.ts index 0a018cdf94..46cb40f967 100644 --- a/packages/medusa/src/instrumentation/index.ts +++ b/packages/medusa/src/instrumentation/index.ts @@ -5,7 +5,7 @@ import { MedusaResponse, Query, } from "@medusajs/framework" -import { ApiRoutesLoader } from "@medusajs/framework/http" +import { ApiLoader } from "@medusajs/framework/http" import { Tracer } from "@medusajs/framework/telemetry" import type { SpanExporter } from "@opentelemetry/sdk-trace-node" import type { NodeSDKConfiguration } from "@opentelemetry/sdk-node" @@ -66,7 +66,7 @@ export function instrumentHttpLayer() { * Instrumenting the route handler to report traces to * OpenTelemetry */ - ApiRoutesLoader.traceRoute = (handler) => { + ApiLoader.traceRoute = (handler) => { return async (req, res) => { if (shouldExcludeResource(req.originalUrl)) { return await handler(req, res) @@ -95,7 +95,7 @@ export function instrumentHttpLayer() { * Instrumenting the middleware handler to report traces to * OpenTelemetry */ - ApiRoutesLoader.traceMiddleware = (handler) => { + ApiLoader.traceMiddleware = (handler) => { return async ( req: MedusaRequest, res: MedusaResponse, diff --git a/packages/medusa/src/loaders/api.ts b/packages/medusa/src/loaders/api.ts index baa5bb807a..159304d6c7 100644 --- a/packages/medusa/src/loaders/api.ts +++ b/packages/medusa/src/loaders/api.ts @@ -1,7 +1,7 @@ import { Express } from "express" import { join } from "path" import qs from "qs" -import { RoutesLoader } from "@medusajs/framework/http" +import { ApiLoader } from "@medusajs/framework/http" import { MedusaContainer, PluginDetails } from "@medusajs/framework/types" import { ConfigModule } from "@medusajs/framework/config" @@ -39,13 +39,10 @@ export default async ({ app, container, plugins }: Options) => { * "/products/:id" route. */ sourcePaths.push( + join(__dirname, "../api"), ...plugins.map((pluginDetails) => { return join(pluginDetails.resolve, "api") - }), - /** - * Register the Medusa CORE API routes using the file based routing. - */ - join(__dirname, "../api") + }) ) const { @@ -58,7 +55,7 @@ export default async ({ app, container, plugins }: Options) => { // Adding this here temporarily // Test: (packages/medusa/src/api/routes/admin/currencies/update-currency.ts) try { - await new RoutesLoader({ + await new ApiLoader({ app: app, sourceDir: sourcePaths, baseRestrictedFields: restrictedFields?.store, diff --git a/yarn.lock b/yarn.lock index 0cb03cfeab..e57d0b2646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5792,6 +5792,7 @@ __metadata: jsonwebtoken: ^9.0.2 lodash: 4.17.21 morgan: ^1.9.1 + path-to-regexp: ^0.1.10 pg: ^8.13.0 rimraf: ^3.0.2 supertest: ^4.0.2 @@ -26955,6 +26956,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^0.1.10": + version: 0.1.12 + resolution: "path-to-regexp@npm:0.1.12" + checksum: 1c6ff10ca169b773f3bba943bbc6a07182e332464704572962d277b900aeee81ac6aa5d060ff9e01149636c30b1f63af6e69dd7786ba6e0ddb39d4dee1f0645b + languageName: node + linkType: hard + "path-to-regexp@npm:^6.2.0": version: 6.2.2 resolution: "path-to-regexp@npm:6.2.2"