From 9e2af4801daeb72d0e5a4d5ae674bdf6d415cf58 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Feb 2025 12:54:10 +0530 Subject: [PATCH] feat: add routes loader (#11592) Fixes: FRMW-2919 This PR adds a new routes loader with a single responsibility of scanning the filesystem and collecting routes. Sorting of routes, merging middleware and registering them with express are going to separate implementations. The new `RoutesLoader` class allows overriding routes as-well (not recommended though) and this is how routes are de-duplicated. - When two routes for the exact route pattern/matcher are discovered, the routes loader will only keep the last one. - Routes files can also override handlers for specific HTTP methods. For example, the original route file exported handlers for `GET` and `POST`, but the overriding one only defines `GET`. In that case, we will continue using the original implementation for the `POST` handler. - If an overriding route file exports additional configuration like `export const AUTHENTICATION=false`, then this will only impact the handlers exported from this file and not the original handlers. Routes sorting has been already been implemented in a separate PR and you can visualize it using this URL. https://routes-visualizer.fly.dev/ --- .changeset/strange-wasps-warn.md | 5 + .../admin/products/[id]/route.ts | 7 + .../admin/products/route.ts | 11 + .../[customer_id]/orders/[order_id]/route.ts | 7 + .../routers/admin/additional-file.ts | 0 .../src/http/__tests__/routes-loader.spec.ts | 384 ++++++++++++++++++ .../src/http/__tests__/routes-sorter.spec.ts | 6 +- .../core/framework/src/http/routes-loader.ts | 265 ++++++++++++ packages/core/framework/src/http/types.ts | 27 +- 9 files changed, 708 insertions(+), 4 deletions(-) create mode 100644 .changeset/strange-wasps-warn.md create mode 100644 packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/[id]/route.ts create mode 100644 packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/route.ts create mode 100644 packages/core/framework/src/http/__fixtures__/routers-with-duplicates/store/[customer_id]/orders/[order_id]/route.ts create mode 100644 packages/core/framework/src/http/__fixtures__/routers/admin/additional-file.ts create mode 100644 packages/core/framework/src/http/__tests__/routes-loader.spec.ts create mode 100644 packages/core/framework/src/http/routes-loader.ts diff --git a/.changeset/strange-wasps-warn.md b/.changeset/strange-wasps-warn.md new file mode 100644 index 0000000000..bd8f2e62d1 --- /dev/null +++ b/.changeset/strange-wasps-warn.md @@ -0,0 +1,5 @@ +--- +"@medusajs/framework": patch +--- + +feat: add routes loader diff --git a/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/[id]/route.ts b/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/[id]/route.ts new file mode 100644 index 0000000000..0581fed42a --- /dev/null +++ b/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/[id]/route.ts @@ -0,0 +1,7 @@ +import { Request, Response } from "express" + +export async function GET(req: Request, res: Response): Promise { + console.log("hello world") +} + +export const AUTHENTICATE = false diff --git a/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/route.ts b/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/route.ts new file mode 100644 index 0000000000..2ce2b110c3 --- /dev/null +++ b/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/admin/products/route.ts @@ -0,0 +1,11 @@ +import { Request, Response } from "express" + +export async function GET(req: Request, res: Response): Promise { + console.log("hello world") +} + +export async function POST(req: Request, res: Response): Promise { + console.log("hello world") +} + +export const AUTHENTICATE = false diff --git a/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/store/[customer_id]/orders/[order_id]/route.ts b/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/store/[customer_id]/orders/[order_id]/route.ts new file mode 100644 index 0000000000..855de749a7 --- /dev/null +++ b/packages/core/framework/src/http/__fixtures__/routers-with-duplicates/store/[customer_id]/orders/[order_id]/route.ts @@ -0,0 +1,7 @@ +import { Request, Response } from "express" + +export function GET(req: Request, res: Response) { + /* const customerId = req.params.id; + const orderId = req.params.id;*/ + res.send("list customers " + JSON.stringify(req.params)) +} diff --git a/packages/core/framework/src/http/__fixtures__/routers/admin/additional-file.ts b/packages/core/framework/src/http/__fixtures__/routers/admin/additional-file.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/framework/src/http/__tests__/routes-loader.spec.ts b/packages/core/framework/src/http/__tests__/routes-loader.spec.ts new file mode 100644 index 0000000000..2e1b583349 --- /dev/null +++ b/packages/core/framework/src/http/__tests__/routes-loader.spec.ts @@ -0,0 +1,384 @@ +import { resolve } from "path" +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({}) + await loader.scanDir(BASE_DIR) + + expect(loader.getRoutes()).toMatchInlineSnapshot(` + [ + { + "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/orders/[id]/route.ts", + "route": "/admin/orders/:id", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", + "handler": [Function], + "method": "POST", + "optedOutOfAuth": false, + "relativePath": "/admin/orders/[id]/route.ts", + "route": "/admin/orders/:id", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/orders/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/orders/route.ts", + "route": "/admin/orders", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/[id]/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/products/[id]/route.ts", + "route": "/admin/products/:id", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "DELETE", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "HEAD", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "OPTIONS", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "PATCH", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "POST", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "PUT", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/route.ts", + "route": "/admin", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/route.ts", + "handler": [Function], + "method": "POST", + "optedOutOfAuth": false, + "relativePath": "/admin/route.ts", + "route": "/admin", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/customers/[customer_id]/orders/[order_id]/route.ts", + "handler": [Function], + "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, + }, + { + "absolutePath": "${BASE_DIR}/customers/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/customers/route.ts", + "route": "/customers", + "shouldAppendAdminCors": false, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + ] + `) + }) + + it("should override existing routes when duplicates are found while scanning additional directories", async () => { + const BASE_DIR = resolve(__dirname, "../__fixtures__/routers") + const BASE_DIR_2 = resolve( + __dirname, + "../__fixtures__/routers-with-duplicates" + ) + const loader = new RoutesLoader({}) + await loader.scanDir(BASE_DIR) + await loader.scanDir(BASE_DIR_2) + + expect(loader.getRoutes()).toMatchInlineSnapshot(` + [ + { + "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/orders/[id]/route.ts", + "route": "/admin/orders/:id", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/orders/[id]/route.ts", + "handler": [Function], + "method": "POST", + "optedOutOfAuth": false, + "relativePath": "/admin/orders/[id]/route.ts", + "route": "/admin/orders/:id", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/orders/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/orders/route.ts", + "route": "/admin/orders", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR_2}/admin/products/[id]/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": true, + "relativePath": "/admin/products/[id]/route.ts", + "route": "/admin/products/:id", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "DELETE", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR_2}/admin/products/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": true, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "HEAD", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "OPTIONS", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "PATCH", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR_2}/admin/products/route.ts", + "handler": [Function], + "method": "POST", + "optedOutOfAuth": true, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/products/route.ts", + "handler": [Function], + "method": "PUT", + "optedOutOfAuth": false, + "relativePath": "/admin/products/route.ts", + "route": "/admin/products", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/admin/route.ts", + "route": "/admin", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/admin/route.ts", + "handler": [Function], + "method": "POST", + "optedOutOfAuth": false, + "relativePath": "/admin/route.ts", + "route": "/admin", + "shouldAppendAdminCors": true, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR}/customers/[customer_id]/orders/[order_id]/route.ts", + "handler": [Function], + "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, + }, + { + "absolutePath": "${BASE_DIR}/customers/route.ts", + "handler": [Function], + "method": "GET", + "optedOutOfAuth": false, + "relativePath": "/customers/route.ts", + "route": "/customers", + "shouldAppendAdminCors": false, + "shouldAppendAuthCors": false, + "shouldAppendStoreCors": false, + }, + { + "absolutePath": "${BASE_DIR_2}/store/[customer_id]/orders/[order_id]/route.ts", + "handler": [Function], + "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, + }, + ] + `) + }) + + it("should throw error for duplicate params", async () => { + const BASE_DIR = resolve( + __dirname, + "../__fixtures__/routers-duplicate-parameter" + ) + + 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 cec010fb57..e3c41a211c 100644 --- a/packages/core/framework/src/http/__tests__/routes-sorter.spec.ts +++ b/packages/core/framework/src/http/__tests__/routes-sorter.spec.ts @@ -1,7 +1,7 @@ import { RoutesSorter } from "../routes-sorter" describe("Routes sorter", () => { - test("sort the given routes", () => { + it("should sort the given routes", () => { const sorter = new RoutesSorter([ { matcher: "/v1", @@ -159,7 +159,7 @@ describe("Routes sorter", () => { `) }) - test("handle all regex based routes", () => { + it("should handle all regex based routes", () => { const sorter = new RoutesSorter([ { matcher: "/admin/:id/export", @@ -229,7 +229,7 @@ describe("Routes sorter", () => { `) }) - test("handle routes with multiple params", () => { + it("should handle routes with multiple params", () => { const sorter = new RoutesSorter([ { matcher: "/admin/customers/:id/addresses", diff --git a/packages/core/framework/src/http/routes-loader.ts b/packages/core/framework/src/http/routes-loader.ts new file mode 100644 index 0000000000..4edf59c0ab --- /dev/null +++ b/packages/core/framework/src/http/routes-loader.ts @@ -0,0 +1,265 @@ +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" + +/** + * 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 routes from this file should be authenticated. + */ +const AUTHTHENTICATION_FLAG = "AUTHENTICATE" + +/** + * Flag that developers can export from their route files to indicate + * whether or not the routes from this file should implement CORS + * policy. + */ +const CORS_FLAG = "CORS" + +/** + * The matcher to use to convert the dynamic params from the filesystem + * identifier to the express identifier. + * + * We capture all words under opening and closing brackets `[]` and mark + * it as a param via `:`. + */ +const PARAM_SEGMENT_MATCHER = /\[(\w+)\]/ + +/** + * Regexes to use to identify if a route is prefixed + * with "/admin", "/store", or "/auth". + */ +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. + * + * In case of duplicates routes, the route registered afterwards will override the + * one registered first. + */ +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 + } + + /** + * Creates the route path from its relative file path. + */ + #createRoutePath(relativePath: string): string { + const segments = relativePath.replace(/route(\.js|\.ts)$/, "").split(sep) + const params: Record = {} + + return `/${segments + .filter((segment) => !!segment) + .map((segment) => { + 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})`, + }) + + throw new Error( + `Duplicate parameters found in route ${relativePath} (${group}). Make sure that all parameters are unique.` + ) + } + + params[group] = true + return `:${group}` + }) + } + return segment + }) + .join("/")}` + } + + /** + * Returns the route config by exporting the route file and parsing + * its exports + */ + async #getRoutesForFile( + routePath: string, + absolutePath: string + ): Promise { + const routeExports = await dynamicImport(absolutePath) + + /** + * Find the route type based upon its prefix. + */ + const routeType = ADMIN_ROUTE_MATCH.test(routePath) + ? "admin" + : STORE_ROUTE_MATCH.test(routePath) + ? "store" + : AUTH_ROUTE_MATCH.test(routePath) + ? "auth" + : undefined + + /** + * Check if the route file has decided to opt-out of authentication + */ + const shouldAuthenticate = + AUTHTHENTICATION_FLAG in routeExports + ? !!routeExports[AUTHTHENTICATION_FLAG] + : true + + /** + * Check if the route file has decided to opt-out of CORS + */ + const shouldApplyCors = + CORS_FLAG in routeExports ? !!routeExports[CORS_FLAG] : true + + /** + * Loop over all the exports and collect functions that are exported + * with names after HTTP methods. + */ + return Object.keys(routeExports) + .filter((key) => { + if (typeof routeExports[key] !== "function") { + return false + } + + if (!HTTP_METHODS.includes(key as RouteVerb)) { + log({ + activityId: this.#activityId, + message: `Skipping handler ${key} in ${absolutePath}. Invalid HTTP method: ${key}.`, + }) + return false + } + + return true + }) + .map((key) => { + return { + route: routePath, + method: key as RouteVerb, + handler: routeExports[key], + optedOutOfAuth: !shouldAuthenticate, + shouldAppendAdminCors: shouldApplyCors && routeType === "admin", + shouldAppendAuthCors: shouldApplyCors && routeType === "auth", + shouldAppendStoreCors: shouldApplyCors && routeType === "store", + } satisfies ScannedRouteDescriptor + }) + } + + /** + * Scans a given directory and loads all routes from it. You can access the loaded + * routes via "getRoutes" method + */ + async scanDir(sourceDir: string) { + const entries = await readDirRecursive(sourceDir, { + ignoreMissing: true, + }) + + await Promise.all( + entries + .filter((entry) => { + if (entry.isDirectory()) { + return false + } + + const { name, ext } = parse(entry.name) + if (name === ROUTE_NAME && [".js", ".ts"].includes(ext)) { + const routeFilePathSegment = join(entry.path, entry.name) + .replace(sourceDir, "") + .split(sep) + + return !routeFilePathSegment.some((segment) => + segment.startsWith("_") + ) + } + + return false + }) + .map(async (entry) => { + const absolutePath = join(entry.path, entry.name) + const relativePath = absolutePath.replace(sourceDir, "") + const route = this.#createRoutePath(relativePath) + const routes = await this.#getRoutesForFile(route, absolutePath) + + routes.forEach((routeConfig) => { + this.registerRoute({ + absolutePath, + relativePath, + ...routeConfig, + }) + }) + }) + ) + } + + /** + * Register a route + */ + registerRoute(route: ScannedRouteDescriptor | FileSystemRouteDescriptor) { + this.#routes[route.route] = this.#routes[route.route] ?? {} + const trackedRoute = this.#routes[route.route] + trackedRoute[route.method] = route + } + + /** + * Register one or more routes + */ + registerRoutes( + routes: (ScannedRouteDescriptor | FileSystemRouteDescriptor)[] + ) { + routes.forEach((route) => this.registerRoute(route)) + } + + /** + * Returns an array of routes scanned by the routes loader or registered + * 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 + }, []) + } +} diff --git a/packages/core/framework/src/http/types.ts b/packages/core/framework/src/http/types.ts index 3e4fdb3dd2..717c628d6b 100644 --- a/packages/core/framework/src/http/types.ts +++ b/packages/core/framework/src/http/types.ts @@ -86,6 +86,31 @@ export type RouteDescriptor = { 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 + method: RouteVerb + handler: RouteHandler + optedOutOfAuth: boolean + routeType?: "admin" | "store" | "auth" + shouldAppendAdminCors: boolean + shouldAppendStoreCors: boolean + shouldAppendAuthCors: boolean +} + +/** + * FileSystem route description represents a route scanned from + * the filesystem + */ +export type FileSystemRouteDescriptor = ScannedRouteDescriptor & { + absolutePath: string + relativePath: string +} + export type GlobalMiddlewareDescriptor = { config?: MiddlewaresConfig } @@ -111,7 +136,7 @@ export interface MedusaRequest< /** * An object containing fields and variables to be used with the remoteQuery - * + * * @version 2.2.0 */ queryConfig: {