1 parent
38a57b1ddc
commit
feec0243ab
3 files changed
+557
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/framework": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add middleware and routes sorter
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import { RoutesSorter } from "../routes-sorter"
|
||||||
|
|
||||||
|
describe("Routes sorter", () => {
|
||||||
|
test("sort the given routes", () => {
|
||||||
|
const sorter = new RoutesSorter([
|
||||||
|
{
|
||||||
|
matcher: "/v1",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products",
|
||||||
|
methods: ["GET"],
|
||||||
|
isAppRoute: true,
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products/export",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products",
|
||||||
|
isAppRoute: true,
|
||||||
|
methods: ["POST"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products/:id",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products/batch",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products/*",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/(products|collections)",
|
||||||
|
methods: ["POST"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/*",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/:id/export",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/*/export",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(sorter.sort()).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/v1",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/*",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/*/export",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/(products|collections)",
|
||||||
|
"methods": [
|
||||||
|
"POST",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/products",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"isAppRoute": true,
|
||||||
|
"matcher": "/admin/products",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"isAppRoute": true,
|
||||||
|
"matcher": "/admin/products",
|
||||||
|
"methods": [
|
||||||
|
"POST",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/products/*",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/products/export",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/products/batch",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/products/:id",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/:id/export",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handle all regex based routes", () => {
|
||||||
|
const sorter = new RoutesSorter([
|
||||||
|
{
|
||||||
|
matcher: "/admin/:id/export",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/*/export",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/products",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/pr*ts",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/product(collection)?s",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(sorter.sort()).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/*/export",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/pr*ts",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/product(collection)?s",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/products",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/:id/export",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handle routes with multiple params", () => {
|
||||||
|
const sorter = new RoutesSorter([
|
||||||
|
{
|
||||||
|
matcher: "/admin/customers/:id/addresses",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/customers/:id",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/customers/:id/addresses/:addressId/switch",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/admin/customers/:id/addresses/:addressId",
|
||||||
|
methods: ["GET"],
|
||||||
|
handler: () => {},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(sorter.sort()).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/customers/:id",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/customers/:id/addresses",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/customers/:id/addresses/:addressId",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": [Function],
|
||||||
|
"matcher": "/admin/customers/:id/addresses/:addressId/switch",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import { MiddlewareVerb } 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".
|
||||||
|
*/
|
||||||
|
type Route = {
|
||||||
|
/**
|
||||||
|
* The route path.
|
||||||
|
*/
|
||||||
|
matcher: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to handle the request
|
||||||
|
*/
|
||||||
|
handler?: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be true when the route is discovered via the fileystem
|
||||||
|
* scanning.
|
||||||
|
*/
|
||||||
|
isAppRoute?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP methods this route is supposed to handle.
|
||||||
|
*/
|
||||||
|
methods?: MiddlewareVerb[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoutesBranch represents a branch in a tree of routes. All routes are
|
||||||
|
* part of a node in a branch. Following is the list of nodes a branch
|
||||||
|
* can have.
|
||||||
|
*
|
||||||
|
* - global: Global routes for the current branch
|
||||||
|
* - regex: Routes using Regex patterns for the current branch
|
||||||
|
* - wildcard: Routes using the wildcard character for the current branch
|
||||||
|
* - params: Routes using params for the current branch
|
||||||
|
* - static: Routes with no dynamic segments for the current branch
|
||||||
|
*
|
||||||
|
* We separate routes under these nodes, so that we can register them with
|
||||||
|
* their priority. Priorities are as follows:
|
||||||
|
*
|
||||||
|
* - global
|
||||||
|
* - regex
|
||||||
|
* - wildcard
|
||||||
|
* - static
|
||||||
|
* - params
|
||||||
|
*/
|
||||||
|
type RoutesBranch = {
|
||||||
|
global: {
|
||||||
|
routes: Route[]
|
||||||
|
children?: {
|
||||||
|
[segment: string]: RoutesBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regex: {
|
||||||
|
routes: Route[]
|
||||||
|
children?: {
|
||||||
|
[segment: string]: RoutesBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wildcard: {
|
||||||
|
routes: Route[]
|
||||||
|
children?: {
|
||||||
|
[segment: string]: RoutesBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params: {
|
||||||
|
routes: Route[]
|
||||||
|
children?: {
|
||||||
|
[segment: string]: RoutesBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static: {
|
||||||
|
routes: Route[]
|
||||||
|
children?: {
|
||||||
|
[segment: string]: RoutesBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoutesSorter exposes the API to sort middleware and inApp route handlers
|
||||||
|
* by their priorities. The class converts an array of matchers to a tree
|
||||||
|
* like structure and then sort them back to a flat array based upon the
|
||||||
|
* priorities of different types of nodes.
|
||||||
|
*/
|
||||||
|
export class RoutesSorter {
|
||||||
|
/**
|
||||||
|
* Input routes
|
||||||
|
*/
|
||||||
|
#routesToProcess: Route[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intermediate tree representation for sorting routes
|
||||||
|
*/
|
||||||
|
#routesTree: {
|
||||||
|
[segment: string]: RoutesBranch
|
||||||
|
} = {
|
||||||
|
root: this.#createBranch(),
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(routes: Route[]) {
|
||||||
|
this.#routesToProcess = routes
|
||||||
|
console.log("Processing routes", this.#routesToProcess)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty branch with known nodes
|
||||||
|
*/
|
||||||
|
#createBranch(): RoutesBranch {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
regex: {
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
wildcard: {
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
static: {
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the route by splitting it with a "/" and then placing
|
||||||
|
* each segment into a specific node of a specific branch.
|
||||||
|
*
|
||||||
|
* The tree structure for a "/admin/products/:id" will look as follows
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* root: {
|
||||||
|
* static: {
|
||||||
|
* routes: [],
|
||||||
|
* children: {
|
||||||
|
* admin: {
|
||||||
|
* static: {
|
||||||
|
* routes: [],
|
||||||
|
* children: {
|
||||||
|
* products: {
|
||||||
|
* routes: [],
|
||||||
|
* params: {
|
||||||
|
* routes: [{ matcher: '/admin/products/:id', ... }]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
#processRoute(route: Route) {
|
||||||
|
const segments = route.matcher.split("/").filter((s) => s.length)
|
||||||
|
let parent = this.#routesTree["root"]
|
||||||
|
|
||||||
|
segments.forEach((segment, index) => {
|
||||||
|
let bucket: keyof RoutesBranch = "static"
|
||||||
|
|
||||||
|
if (!route.methods) {
|
||||||
|
bucket = "global"
|
||||||
|
} else if (segment.startsWith("*")) {
|
||||||
|
bucket = "wildcard"
|
||||||
|
} else if (segment.startsWith(":")) {
|
||||||
|
bucket = "params"
|
||||||
|
} else if (/[\(\+\*\[\]\)\!]/.test(segment)) {
|
||||||
|
bucket = "regex"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index + 1 === segments.length) {
|
||||||
|
parent[bucket].routes.push(route)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent[bucket].children = parent[bucket].children ?? {}
|
||||||
|
parent[bucket].children![segment] =
|
||||||
|
parent[bucket].children![segment] ?? this.#createBranch()
|
||||||
|
parent = parent[bucket].children![segment]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of sorted routes for a given branch.
|
||||||
|
*/
|
||||||
|
#sortBranch(routeBranch: { [segment: string]: RoutesBranch }) {
|
||||||
|
const branchRoutes = Object.keys(routeBranch).reduce<{
|
||||||
|
global: Route[]
|
||||||
|
wildcard: Route[]
|
||||||
|
regex: Route[]
|
||||||
|
params: Route[]
|
||||||
|
static: Route[]
|
||||||
|
}>(
|
||||||
|
(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.wildcard.push(...node.wildcard.routes)
|
||||||
|
if (node.wildcard.children) {
|
||||||
|
result.wildcard.push(...this.#sortBranch(node.wildcard.children))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.regex.push(...node.regex.routes)
|
||||||
|
if (node.regex.children) {
|
||||||
|
result.regex.push(...this.#sortBranch(node.regex.children))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.static.push(...node.static.routes)
|
||||||
|
if (node.static.children) {
|
||||||
|
result.static.push(...this.#sortBranch(node.static.children))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.params.push(...node.params.routes)
|
||||||
|
if (node.params.children) {
|
||||||
|
result.params.push(...this.#sortBranch(node.params.children))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
{
|
||||||
|
global: [],
|
||||||
|
wildcard: [],
|
||||||
|
regex: [],
|
||||||
|
params: [],
|
||||||
|
static: [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort the input routes
|
||||||
|
*/
|
||||||
|
sort() {
|
||||||
|
this.#routesToProcess.map((route) => this.#processRoute(route))
|
||||||
|
return this.#sortBranch(this.#routesTree)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user