feat(medusa): support file based routing (#5365)

* feat(medusa): Implement file base routing mechanism

* cleanup

* cleanup and improvements

* improve tests

* update tests

* Create tame-trains-grin.md

* fix tests, move fixtures

* fix tests

* lint

* init work

* work on supported methods

* add config generator

* progress

* progress

* rework middleware registartion

* progress

* progress

* export MiddlewareConfig  type

* add type extensions

* work on improving logs

* work on loggers

* apply cors, json parsing, and admin authentication per default

* address feedback

* rm changeset

---------

Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Kasper Fabricius Kristensen
2023-10-18 17:26:27 +03:00
committed by GitHub
parent 49ed7de752
commit ddff919655
22 changed files with 1022 additions and 7 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): Introduces a new file based routing system as an alternative to the current approach. File based routing is optional, and the previous approach can still be used. The two approaches can also be used together allowing for incremental adoption.

View File

@@ -1,3 +1,5 @@
global.performance = require("perf_hooks").performance
global.afterEach(async () => { global.afterEach(async () => {
await new Promise((resolve) => setImmediate(resolve)) await new Promise((resolve) => setImmediate(resolve))
}) })

View File

@@ -5,6 +5,8 @@ export * from "./models"
export * from "./services" export * from "./services"
export * from "./types/batch-job" export * from "./types/batch-job"
export * from "./types/common" export * from "./types/common"
export * from "./types/middlewares"
export * from "./types/routing"
export * from "./types/global" export * from "./types/global"
export * from "./types/price-list" export * from "./types/price-list"
export * from "./utils" export * from "./utils"

View File

@@ -0,0 +1,3 @@
export const customersGlobalMiddlewareMock = jest.fn()
export const customersCreateMiddlewareMock = jest.fn()
export const storeCorsMiddlewareMock = jest.fn()

View File

@@ -0,0 +1,9 @@
import { Request, Response } from "express"
export const GET = (req: Request, res: Response) => {
res.send("get customer order")
}
export const POST = (req: Request, res: Response) => {
res.send("update customer order")
}

View File

@@ -0,0 +1,9 @@
import { Request, Response } from "express"
export function GET(req: Request, res: Response) {
res.send("list customers")
}
export function POST(req: Request, res: Response) {
res.send("create customer")
}

View File

@@ -0,0 +1,48 @@
import { NextFunction, Request, Response } from "express"
import { MiddlewaresConfig } from "../../types"
import {
customersCreateMiddlewareMock,
customersGlobalMiddlewareMock,
storeCorsMiddlewareMock,
} from "../mocks"
const customersGlobalMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
customersGlobalMiddlewareMock()
next()
}
const customersCreateMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
customersCreateMiddlewareMock()
next()
}
const storeCors = (req: Request, res: Response, next: NextFunction) => {
storeCorsMiddlewareMock()
next()
}
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/customers",
middlewares: [customersGlobalMiddleware],
},
{
method: "POST",
matcher: "/customers",
middlewares: [customersCreateMiddleware],
},
{
matcher: "/store/*",
middlewares: [storeCors],
},
],
}

View File

@@ -0,0 +1,5 @@
import { Request, Response } from "express"
export const POST = (req: Request, res: Response) => {
res.send(`sync product ${req.params.id}`)
}

View File

@@ -0,0 +1,5 @@
import { Request, Response } from "express"
export const GET = async (req: Request, res: Response): Promise<void> => {
res.send(`GET private route`)
}

View File

@@ -0,0 +1,9 @@
import { Request, Response } from "express"
export async function GET(req: Request, res: Response): Promise<void> {
res.send(`GET order ${req.params.id}`)
}
export async function POST(req: Request, res: Response): Promise<void> {
res.send(`POST order ${req.params.id}`)
}

View File

@@ -0,0 +1,5 @@
import { Request, Response } from "express"
export function GET(req: Request, res: Response) {
res.send("hello world")
}

View File

@@ -0,0 +1,5 @@
import { Request, Response } from "express"
export async function GET(req: Request, res: Response): Promise<void> {
console.log("hello world")
}

View File

@@ -0,0 +1,29 @@
import { Request, Response } from "express"
export async function GET(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
export async function POST(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
export async function PUT(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
export async function DELETE(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
export async function PATCH(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
export async function OPTIONS(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
export async function HEAD(req: Request, res: Response): Promise<void> {
console.log("hello world")
}

View File

@@ -0,0 +1,9 @@
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")
}

View File

@@ -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))
}

View File

@@ -0,0 +1,5 @@
import { NextFunction, Request, Response } from "express"
export function GET(req: Request, res: Response) {
res.send("list customers")
}

View File

@@ -0,0 +1,150 @@
import express from "express"
import http from "http"
import { resolve } from "path"
import request from "supertest"
import {
customersCreateMiddlewareMock,
customersGlobalMiddlewareMock,
storeCorsMiddlewareMock,
} from "../__fixtures__/mocks"
import { RoutesLoader } from "../index"
const mockConfigModule = {
projectConfig: {
store_cors: "http://localhost:8000",
admin_cors: "http://localhost:7001",
database_logging: false,
},
featureFlags: {},
plugins: [],
}
describe("RoutesLoader", function () {
afterEach(function () {
jest.clearAllMocks()
})
describe("Routes", function () {
const app = express()
const server = http.createServer(app)
beforeAll(async function () {
const rootDir = resolve(__dirname, "../__fixtures__/routers")
await new RoutesLoader({
app,
rootDir,
configModule: mockConfigModule,
}).load()
})
it("should return a status 200 on GET admin/order/:id", async function () {
await request(server)
.get("/admin/orders/1000")
.expect(200)
.expect("GET order 1000")
})
it("should return a status 200 on POST admin/order/:id", async function () {
await request(server)
.post("/admin/orders/1000")
.expect(200)
.expect("POST order 1000")
})
it("should call GET /customers/[customer_id]/orders/[order_id]", async function () {
await request(server)
.get("/customers/test-customer/orders/test-order")
.expect(200)
.expect(
'list customers {"customer_id":"test-customer","order_id":"test-order"}'
)
})
it("should not be able to GET /_private as the folder is prefixed with an underscore", async function () {
const res = await request(server).get("/_private")
expect(res.status).toBe(404)
expect(res.text).toContain("Cannot GET /_private")
})
})
describe("Middlewares", function () {
const app = express()
const server = http.createServer(app)
beforeAll(async function () {
const rootDir = resolve(__dirname, "../__fixtures__/routers-middleware")
await new RoutesLoader({
app,
rootDir,
configModule: mockConfigModule,
}).load()
})
it("should call middleware applied to `/customers`", async function () {
await request(server)
.get("/customers")
.expect(200)
.expect("list customers")
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
})
it("should not call middleware applied to POST `/customers` when GET `/customers`", async function () {
await request(server)
.get("/customers")
.expect(200)
.expect("list customers")
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
})
it("should call middleware applied to POST `/customers` when POST `/customers`", async function () {
await request(server)
.post("/customers")
.expect(200)
.expect("create customer")
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
expect(customersCreateMiddlewareMock).toHaveBeenCalled()
})
it("should call store cors middleware on `/store/*` routes", async function () {
await request(server)
.post("/store/products/1000/sync")
.expect(200)
.expect("sync product 1000")
expect(customersGlobalMiddlewareMock).not.toHaveBeenCalled()
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
expect(storeCorsMiddlewareMock).toHaveBeenCalled()
})
})
describe("Duplicate parameters", function () {
const app = express()
it("should throw if a route contains the same parameter multiple times", async function () {
const rootDir = resolve(
__dirname,
"../__fixtures__/routers-duplicate-parameter"
)
const err = await new RoutesLoader({
app,
rootDir,
configModule: mockConfigModule,
})
.load()
.catch((e) => e)
expect(err).toBeDefined()
expect(err.message).toBe(
"Duplicate parameters found in route /admin/customers/[id]/orders/[id] (id). Make sure that all parameters are unique."
)
})
})
})

View File

@@ -0,0 +1,585 @@
import cors from "cors"
import { Express, json, urlencoded } from "express"
import { readdir } from "fs/promises"
import { parseCorsOrigins } from "medusa-core-utils"
import { extname, join } from "path"
import {
authenticate,
authenticateCustomer,
requireCustomerAuthentication,
} from "../../../api/middlewares"
import { ConfigModule } from "../../../types/global"
import logger from "../../logger"
import {
GlobalMiddlewareDescriptor,
HTTP_METHODS,
MiddlewaresConfig,
RouteConfig,
RouteDescriptor,
RouteVerb,
} from "./types"
const log = ({
activityId,
message,
}: {
activityId?: string
message: string
}) => {
if (activityId) {
logger.progress(activityId, message)
return
}
logger.info(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
}
export class RoutesLoader {
protected routesMap = new Map<string, RouteDescriptor>()
protected globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
protected app: Express
protected activityId?: string
protected rootDir: string
protected configModule: ConfigModule
protected excludes: RegExp[] = [
/\.DS_Store/,
/(\.ts\.map|\.js\.map|\.d\.ts)/,
/^_/,
]
constructor({
app,
activityId,
rootDir,
configModule,
excludes = [],
}: {
app: Express
activityId?: string
rootDir: string
configModule: ConfigModule
excludes?: RegExp[]
}) {
this.app = app
this.activityId = activityId
this.rootDir = rootDir
this.configModule = configModule
this.excludes.push(...(excludes ?? []))
}
/**
* Validate the route config and display a log info if
* it should be ignored or skipped.
*
* @param {GlobalMiddlewareDescriptor} descriptor
* @param {MiddlewaresConfig} config
*
* @return {void}
*/
protected validateMiddlewaresConfig({
config,
}: {
config?: MiddlewaresConfig
}): void {
if (!config?.routes) {
log({
activityId: this.activityId,
message: `No middleware routes found. 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]/index.ts" => "/admin/orders/:id/index.ts"
*/
protected parseRoute(route: string): string {
let route_ = route
for (const config of Object.entries(pathSegmentReplacer)) {
const [searchFor, replacedByFn] = config
const replacer = new RegExp(searchFor, "g")
const matches = [...route_.matchAll(replacer)]
const parameters = new Set()
for (const match of matches) {
if (match?.[1] && !Number.isInteger(match?.[1])) {
if (parameters.has(match?.[1])) {
log({
activityId: this.activityId,
message: `Duplicate parameters found in route ${route} (${match?.[1]})`,
})
throw new Error(
`Duplicate parameters found in route ${route} (${match?.[1]}). Make sure that all parameters are unique.`
)
}
parameters.add(match?.[1])
}
route_ = route_.replace(match[0], replacedByFn(match?.[1]))
}
const extension = extname(route_)
if (extension) {
route_ = route_.replace(extension, "")
}
}
route = route_
return route
}
/**
* Load the file content from a descriptor and retrieve the verbs and handlers
* to be assigned to the descriptor
*
* @return {Promise<void>}
*/
protected async createRoutesConfig(): Promise<void> {
await Promise.all(
[...this.routesMap.values()].map(async (descriptor: RouteDescriptor) => {
const absolutePath = descriptor.absolutePath
return await import(absolutePath).then((import_) => {
const map = this.routesMap
const config: RouteConfig = {
routes: [],
shouldRequireAdminAuth: false,
shouldRequireCustomerAuth: false,
shouldAppendCustomer: false,
}
/**
* If the developer has not exported the authenticate flag
* we default to true.
*/
const shouldRequireAuth =
import_[AUTHTHENTICATE] !== undefined
? (import_[AUTHTHENTICATE] as boolean)
: true
if (
shouldRequireAuth &&
absolutePath.includes(join("api", "admin"))
) {
config.shouldRequireAdminAuth = shouldRequireAuth
}
if (
shouldRequireAuth &&
absolutePath.includes(join("api", "store", "me"))
) {
config.shouldRequireCustomerAuth = shouldRequireAuth
}
if (absolutePath.includes(join("api", "store"))) {
config.shouldAppendCustomer = true
}
const handlers = Object.keys(import_).filter((key) => {
/**
* Filter out any export that is not a function
*/
return typeof import_[key] === "function"
})
for (const handler of handlers) {
if (HTTP_METHODS.includes(handler as RouteVerb)) {
config.routes?.push({
method: handler as RouteVerb,
handler: import_[handler],
})
} else {
log({
activityId: this.activityId,
message: `Skipping handler ${handler} in ${absolutePath}. Invalid HTTP method: ${handler}.`,
})
}
}
if (!config.routes?.length) {
log({
activityId: this.activityId,
message: `No valid route handlers detected in ${absolutePath}. Skipping route configuration.`,
})
map.delete(absolutePath)
return
}
descriptor.config = config
map.set(absolutePath, descriptor)
})
})
)
}
protected createRoutesDescriptor({
childPath,
parentPath,
}: {
childPath: string
parentPath?: string
}) {
const descriptor: RouteDescriptor = {
absolutePath: childPath,
relativePath: "",
route: "",
priority: Infinity,
}
if (parentPath) {
childPath = childPath.replace(parentPath, "")
}
descriptor.relativePath = childPath
let routeToParse = childPath
const pathSegments = childPath.split("/")
const lastSegment = pathSegments[pathSegments.length - 1]
if (lastSegment.startsWith("route")) {
pathSegments.pop()
routeToParse = pathSegments.join("/")
}
descriptor.route = this.parseRoute(routeToParse)
descriptor.priority = calculatePriority(descriptor.route)
this.routesMap.set(childPath, descriptor)
}
protected async createMiddlewaresDescriptor({
dirPath,
}: {
dirPath: string
}) {
const files = await readdir(dirPath)
const middlewareFilePath = files
.filter((path) => {
if (
this.excludes.length &&
this.excludes.some((exclude) => exclude.test(path))
) {
return false
}
return true
})
.find((file) => {
return file.replace(/\.[^/.]+$/, "") === MIDDLEWARES_NAME
})
if (!middlewareFilePath) {
log({
activityId: this.activityId,
message: `No middleware files found in ${dirPath}. Skipping middleware configuration.`,
})
return
}
const absolutePath = join(dirPath, middlewareFilePath)
try {
await import(absolutePath).then((import_) => {
const middlewaresConfig = import_.config as
| MiddlewaresConfig
| undefined
if (!middlewaresConfig) {
log({
activityId: this.activityId,
message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`,
})
return
}
middlewaresConfig.routes = middlewaresConfig.routes?.map((route) => {
return {
...route,
method: route.method ?? "USE",
}
})
const descriptor: GlobalMiddlewareDescriptor = {
config: middlewaresConfig,
}
this.validateMiddlewaresConfig(descriptor)
this.globalMiddlewaresDescriptor = descriptor
})
} catch (error) {
log({
activityId: this.activityId,
message: `Failed to load middleware configuration in ${absolutePath}. Skipping middleware configuration.`,
})
return
}
}
protected async createRoutesMap({
dirPath,
parentPath,
}: {
dirPath: string
parentPath?: string
}): Promise<void> {
await Promise.all(
await readdir(dirPath, { withFileTypes: true }).then((entries) => {
return entries
.filter((entry) => {
const fullPath = join(dirPath, entry.name)
if (
this.excludes.length &&
this.excludes.some((exclude) => exclude.test(entry.name))
) {
return false
}
// Get entry name without extension
const name = entry.name.replace(/\.[^/.]+$/, "")
if (entry.isFile() && name !== ROUTE_NAME) {
return false
}
return true
})
.map(async (entry) => {
const childPath = join(dirPath, entry.name)
if (entry.isDirectory()) {
return this.createRoutesMap({
dirPath: childPath,
parentPath: parentPath ?? dirPath,
})
}
return this.createRoutesDescriptor({
childPath,
parentPath,
})
})
.flat(Infinity)
})
)
}
protected async registerRoutes(): Promise<void> {
const prioritizedRoutes = prioritize([...this.routesMap.values()])
for (const descriptor of prioritizedRoutes) {
if (!descriptor.config?.routes?.length) {
continue
}
const routes = descriptor.config.routes
if (descriptor.config.shouldAppendCustomer) {
this.app.use(descriptor.route, authenticateCustomer())
}
if (descriptor.config.shouldRequireAdminAuth) {
this.app.use(descriptor.route, authenticate())
}
if (descriptor.config.shouldRequireCustomerAuth) {
this.app.use(descriptor.route, requireCustomerAuthentication())
}
for (const route of routes) {
log({
activityId: this.activityId,
message: `Registering route [${route.method?.toUpperCase()}] - ${
descriptor.route
}`,
})
this.app[route.method!.toLowerCase()](descriptor.route, route.handler)
}
}
}
protected async registerMiddlewares(): Promise<void> {
const descriptor = this.globalMiddlewaresDescriptor
if (!descriptor) {
return
}
if (!descriptor.config?.routes?.length) {
return
}
const routes = descriptor.config.routes
for (const route of routes) {
if (Array.isArray(route.method)) {
for (const method of route.method) {
log({
activityId: this.activityId,
message: `Registering middleware [${method}] - ${route.matcher}`,
})
this.app[method.toLowerCase()](route.matcher, ...route.middlewares)
}
} else {
log({
activityId: this.activityId,
message: `Registering middleware [${route.method}] - ${route.matcher}`,
})
this.app[route.method!.toLowerCase()](
route.matcher,
...route.middlewares
)
}
}
}
applyGlobalMiddlewares() {
if (this.routesMap.size > 0) {
this.app.use(json(), urlencoded({ extended: true }))
const adminCors = this.configModule.projectConfig.admin_cors || ""
this.app.use(
"/admin",
cors({
origin: parseCorsOrigins(adminCors),
credentials: true,
})
)
const storeCors = this.configModule.projectConfig.store_cors || ""
this.app.use(
"/store",
cors({ origin: parseCorsOrigins(storeCors), credentials: true })
)
}
}
async load() {
performance.mark("file-base-routing-start" + this.rootDir)
let apiExists = true
/**
* Since the file based routing does not require a index file
* we can check if it exists using require. Instead we try
* to read the directory and if it fails we know that the
* directory does not exist.
*/
try {
await readdir(this.rootDir)
} catch (_error) {
apiExists = false
}
if (apiExists) {
await this.createMiddlewaresDescriptor({ dirPath: this.rootDir })
await this.createRoutesMap({ dirPath: this.rootDir })
await this.createRoutesConfig()
this.applyGlobalMiddlewares()
await this.registerMiddlewares()
await this.registerRoutes()
}
performance.mark("file-base-routing-end" + this.rootDir)
const timeSpent = performance
.measure(
"file-base-routing-measure" + this.rootDir,
"file-base-routing-start" + this.rootDir,
"file-base-routing-end" + this.rootDir
)
?.duration?.toFixed(2)
log({
activityId: this.activityId,
message: `Routes loaded in ${timeSpent} ms`,
})
this.routesMap.clear()
this.globalMiddlewaresDescriptor = undefined
}
}
export default RoutesLoader

View File

@@ -0,0 +1,64 @@
import {
MedusaRequest,
MedusaRequestHandler,
MedusaResponse,
} from "../../../types/routing"
/**
* List of all the supported HTTP methods
*/
export const HTTP_METHODS = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"OPTIONS",
"HEAD",
] as const
export type RouteVerb = (typeof HTTP_METHODS)[number]
type MiddlewareVerb = "USE" | "ALL" | RouteVerb
type RouteHandler = (
req: MedusaRequest,
res: MedusaResponse
) => Promise<void> | void
export type RouteImplementation = {
method?: RouteVerb
handler: RouteHandler
}
export type RouteConfig = {
shouldRequireAdminAuth?: boolean
shouldRequireCustomerAuth?: boolean
shouldAppendCustomer?: boolean
routes?: RouteImplementation[]
}
export type MiddlewareFunction =
| MedusaRequestHandler
| ((...args: any[]) => any)
export type MiddlewareRoute = {
method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp
middlewares: MiddlewareFunction[]
}
export type MiddlewaresConfig = {
routes?: MiddlewareRoute[]
}
export type RouteDescriptor = {
absolutePath: string
relativePath: string
route: string
priority: number
config?: RouteConfig
}
export type GlobalMiddlewareDescriptor = {
config?: MiddlewaresConfig
}

View File

@@ -39,6 +39,7 @@ import { EntitySchema } from "typeorm"
import { MiddlewareService } from "../services" import { MiddlewareService } from "../services"
import { getModelExtensionsMap } from "./helpers/get-model-extension-map" import { getModelExtensionsMap } from "./helpers/get-model-extension-map"
import logger from "./logger" import logger from "./logger"
import { RoutesLoader } from "./helpers/routing"
type Options = { type Options = {
rootDirectory: string rootDirectory: string
@@ -83,7 +84,14 @@ export default async ({
registerRepositories(pluginDetails, container) registerRepositories(pluginDetails, container)
await registerServices(pluginDetails, container) await registerServices(pluginDetails, container)
await registerMedusaApi(pluginDetails, container) await registerMedusaApi(pluginDetails, container)
registerApi(pluginDetails, app, rootDirectory, container, activityId) await registerApi(
pluginDetails,
app,
rootDirectory,
container,
configModule,
activityId
)
registerCoreRouters(pluginDetails, container) registerCoreRouters(pluginDetails, container)
registerSubscribers(pluginDetails, container) registerSubscribers(pluginDetails, container)
}) })
@@ -332,13 +340,14 @@ function registerCoreRouters(
/** /**
* Registers the plugin's api routes. * Registers the plugin's api routes.
*/ */
function registerApi( async function registerApi(
pluginDetails: PluginDetails, pluginDetails: PluginDetails,
app: Express, app: Express,
rootDirectory = "", rootDirectory = "",
container: MedusaContainer, container: MedusaContainer,
configmodule: ConfigModule,
activityId: string activityId: string
): Express { ): Promise<Express> {
const logger = container.resolve<Logger>("logger") const logger = container.resolve<Logger>("logger")
const projectName = const projectName =
pluginDetails.name === MEDUSA_PROJECT_NAME pluginDetails.name === MEDUSA_PROJECT_NAME
@@ -346,16 +355,42 @@ function registerApi(
: `${pluginDetails.name}` : `${pluginDetails.name}`
logger.progress(activityId, `Registering custom endpoints for ${projectName}`) logger.progress(activityId, `Registering custom endpoints for ${projectName}`)
try { try {
const routes = require(`${pluginDetails.resolve}/api`).default /**
if (routes) { * Register the plugin's api routes using the file based routing.
app.use("/", routes(rootDirectory, pluginDetails.options)) */
await new RoutesLoader({
app,
rootDir: `${pluginDetails.resolve}/api`,
activityId: activityId,
configModule: configmodule,
}).load()
/**
* For backwards compatibility we also support loading routes from
* `/api/index` if the file exists.
*/
let apiFolderExists = true
try {
require.resolve(`${pluginDetails.resolve}/api`)
} catch (e) {
apiFolderExists = false
} }
if (apiFolderExists) {
const routes = require(`${pluginDetails.resolve}/api`).default
if (routes) {
app.use("/", routes(rootDirectory, pluginDetails.options))
}
}
return app return app
} catch (err) { } catch (err) {
if (err.code !== "MODULE_NOT_FOUND") { if (err.code !== "MODULE_NOT_FOUND") {
logger.warn( logger.warn(
`An error occured while registering endpoints in ${projectName}` `An error occurred while registering endpoints in ${projectName}`
) )
if (err.stack) { if (err.stack) {

View File

@@ -0,0 +1,5 @@
export type {
MiddlewareFunction,
MiddlewareRoute,
MiddlewaresConfig,
} from "../loaders/helpers/routing/types"

View File

@@ -0,0 +1,19 @@
import type { NextFunction, Request, Response } from "express"
import type { Customer, User } from "../models"
import type { MedusaContainer } from "./global"
export interface MedusaRequest extends Request {
user?: (User | Customer) & { customer_id?: string; userId?: string }
scope: MedusaContainer
}
export type MedusaResponse = Response
export type MedusaNextFunction = NextFunction
export type MedusaRequestHandler = (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => Promise<void> | void