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/
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export async function GET(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export async function GET(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export async function POST(req: Request, res: Response): Promise<void> {
|
||||
console.log("hello world")
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
@@ -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))
|
||||
}
|
||||
384
packages/core/framework/src/http/__tests__/routes-loader.spec.ts
Normal file
384
packages/core/framework/src/http/__tests__/routes-loader.spec.ts
Normal file
@@ -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."
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
265
packages/core/framework/src/http/routes-loader.ts
Normal file
265
packages/core/framework/src/http/routes-loader.ts
Normal file
@@ -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<string, ScannedRouteDescriptor | FileSystemRouteDescriptor>
|
||||
> = {}
|
||||
|
||||
/**
|
||||
* 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<string, boolean> = {}
|
||||
|
||||
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<ScannedRouteDescriptor[]> {
|
||||
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
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user