chore(framework): Move and improve routes loader (#8392)

* chore(framework): Move and improve routes loader

* cleanup

* fix(framework): import
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-08-01 16:18:42 +02:00
1 parent 4081b3359d
commit f81652bf6e
102 files changed
+921 -798

No files matched your search

+6 -1
View File
@@ -41,8 +41,10 @@
"devDependencies": { "devDependencies": {
"@medusajs/types": "^1.11.16", "@medusajs/types": "^1.11.16",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jsonwebtoken": "^8.5.9",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"supertest": "^4.0.2",
"tsc-alias": "^1.8.6", "tsc-alias": "^1.8.6",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^5.2.11" "vite": "^5.2.11"
@@ -55,11 +57,14 @@
"@medusajs/workflows-sdk": "^0.1.6", "@medusajs/workflows-sdk": "^0.1.6",
"awilix": "^8.0.0", "awilix": "^8.0.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"ioredis": "^5.2.5", "ioredis": "^5.2.5",
"ioredis-mock": "8.4.0", "ioredis-mock": "8.4.0",
"jsonwebtoken": "^9.0.2",
"medusa-telemetry": "^0.0.17", "medusa-telemetry": "^0.0.17",
"morgan": "^1.9.1" "morgan": "^1.9.1",
"zod": "3.22.4"
} }
} }
@@ -1,4 +1,4 @@
import { defineMiddlewares } from "../../../../../utils/define-middlewares" import { defineMiddlewares } from "../../utils/define-middlewares"
export default defineMiddlewares({ export default defineMiddlewares({
errorHandler: (err, _req, res, _next) => { errorHandler: (err, _req, res, _next) => {
@@ -1,10 +1,10 @@
import { NextFunction, Request, Response, raw } from "express" import { NextFunction, raw, Request, Response } from "express"
import { import {
customersCreateMiddlewareMock, customersCreateMiddlewareMock,
customersGlobalMiddlewareMock, customersGlobalMiddlewareMock,
storeGlobalMiddlewareMock, storeGlobalMiddlewareMock,
} from "../mocks" } from "../mocks"
import { defineMiddlewares } from "../../../../../utils/define-middlewares" import { defineMiddlewares } from "../../utils/define-middlewares"
const customersGlobalMiddleware = ( const customersGlobalMiddleware = (
req: Request, req: Request,
@@ -3,24 +3,20 @@ import {
ModulesDefinition, ModulesDefinition,
registerMedusaModule, registerMedusaModule,
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import {
configManager,
ConfigModule,
container,
featureFlagsLoader,
logger,
} from "@medusajs/framework"
import { ContainerRegistrationKeys, generateJwtToken } from "@medusajs/utils" import { ContainerRegistrationKeys, generateJwtToken } from "@medusajs/utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import express from "express" import express from "express"
import querystring from "querystring" import querystring from "querystring"
import supertest from "supertest" import supertest from "supertest"
import apiLoader from "../../../../api"
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
import RoutesLoader from "../.."
import { config } from "../mocks" import { config } from "../mocks"
import { MedusaContainer } from "@medusajs/types" import { MedusaContainer } from "@medusajs/types"
import { configManager, ConfigModule } from "../../../config"
import { container } from "../../../container"
import { featureFlagsLoader } from "../../../feature-flags"
import { logger } from "../../../logger"
import { MedusaRequest } from "../../types"
import { RoutesLoader } from "../../router"
function asArray(resolvers) { function asArray(resolvers) {
return { return {
@@ -90,30 +86,19 @@ export const createServer = async (rootDir) => {
next() next()
}) })
const plugins = getResolvedPlugins(rootDir, config) || []
await featureFlagsLoader() await featureFlagsLoader()
await moduleLoader({ container, moduleResolutions, logger }) await moduleLoader({ container, moduleResolutions, logger })
app.use((req, res, next) => { app.use((req, res, next) => {
req.scope = container.createScope() as MedusaContainer ;(req as MedusaRequest).scope = container.createScope() as MedusaContainer
next() next()
}) })
// This where plugins normally load, but we simply load the routes
await new RoutesLoader({ await new RoutesLoader({
app, app,
rootDir, sourceDir: rootDir,
configModule: config,
}).load() }).load()
// the apiLoader needs to be called after plugins otherwise the core middleware bleads into the plugins
await apiLoader({
container,
app: app,
plugins,
})
const superRequest = supertest(app) const superRequest = supertest(app)
return { return {
@@ -1,7 +1,6 @@
import express from "express" import express from "express"
import { resolve } from "path" import { resolve } from "path"
import { import {
config,
customersCreateMiddlewareMock, customersCreateMiddlewareMock,
customersGlobalMiddlewareMock, customersGlobalMiddlewareMock,
storeGlobalMiddlewareMock, storeGlobalMiddlewareMock,
@@ -230,8 +229,7 @@ describe("RoutesLoader", function () {
) )
const err = await new RoutesLoader({ const err = await new RoutesLoader({
app, app,
rootDir, sourceDir: rootDir,
configModule: config,
}) })
.load() .load()
.catch((e) => e) .catch((e) => e)
@@ -1 +1,5 @@
export * from "./express-loader" export * from "./express-loader"
export * from "./router"
export * from "./types"
export * from "./middlewares"
export * from "./utils/define-middlewares"
@@ -0,0 +1,208 @@
import { ApiKeyDTO, IApiKeyModuleService } from "@medusajs/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
} from "@medusajs/utils"
import { NextFunction, RequestHandler } from "express"
import { JwtPayload, verify } from "jsonwebtoken"
import {
AuthContext,
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
} from "../types"
import { ConfigModule } from "../../config"
const SESSION_AUTH = "session"
const BEARER_AUTH = "bearer"
const API_KEY_AUTH = "api-key"
// This is the only hard-coded actor type, as API keys have special handling for now. We could also generalize API keys to carry the actor type with them.
const ADMIN_ACTOR_TYPE = "user"
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
type MedusaSession = {
auth_context: AuthContext
}
export const authenticate = (
actorType: string | string[],
authType: AuthType | AuthType[],
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
): RequestHandler => {
const handler = async (
req: MedusaRequest,
res: MedusaResponse,
next: NextFunction
): Promise<void> => {
const authTypes = Array.isArray(authType) ? authType : [authType]
const actorTypes = Array.isArray(actorType) ? actorType : [actorType]
const req_ = req as AuthenticatedMedusaRequest
// We only allow authenticating using a secret API key on the admin
const isExclusivelyUser =
actorTypes.length === 1 && actorTypes[0] === ADMIN_ACTOR_TYPE
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyUser) {
const apiKey = await getApiKeyInfo(req)
if (apiKey) {
req_.auth_context = {
actor_id: apiKey.id,
actor_type: "api-key",
auth_identity_id: "",
app_metadata: {},
}
return next()
}
}
// We try to extract the auth context either from the session or from a JWT token
let authContext: AuthContext | null = getAuthContextFromSession(
req.session,
authTypes,
actorTypes
)
if (!authContext) {
const {
projectConfig: { http },
} = req.scope.resolve<ConfigModule>(
ContainerRegistrationKeys.CONFIG_MODULE
)
authContext = getAuthContextFromJwtToken(
req.headers.authorization,
http.jwtSecret!,
authTypes,
actorTypes
)
}
// If the entity is authenticated, and it is a registered actor we can continue
if (authContext?.actor_id) {
req_.auth_context = authContext
return next()
}
// If the entity is authenticated, but there is no registered actor yet, we can continue (eg. in the case of a user invite) if allow unregistered is set
if (authContext?.auth_identity_id && options.allowUnregistered) {
req_.auth_context = authContext
return next()
}
// If we allow unauthenticated requests (i.e public endpoints), just continue
if (options.allowUnauthenticated) {
return next()
}
res.status(401).json({ message: "Unauthorized" })
}
return handler as unknown as RequestHandler
}
const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
const authHeader = req.headers.authorization
if (!authHeader) {
return null
}
const [tokenType, token] = authHeader.split(" ")
if (tokenType.toLowerCase() !== "basic" || !token) {
return null
}
// The token could have been base64 encoded, we want to decode it first.
let normalizedToken = token
if (!token.startsWith("sk_")) {
normalizedToken = Buffer.from(token, "base64").toString("utf-8")
}
// Basic auth is defined as a username:password set, and since the token is set to the username we need to trim the colon
if (normalizedToken.endsWith(":")) {
normalizedToken = normalizedToken.slice(0, -1)
}
// Secret tokens start with 'sk_', and if it doesn't it could be a user JWT or a malformed token
if (!normalizedToken.startsWith("sk_")) {
return null
}
const apiKeyModule = req.scope.resolve(
ModuleRegistrationName.API_KEY
) as IApiKeyModuleService
try {
const apiKey = await apiKeyModule.authenticate(normalizedToken)
if (!apiKey) {
return null
}
return apiKey
} catch (error) {
console.error(error)
return null
}
}
const getAuthContextFromSession = (
session: Partial<MedusaSession> = {},
authTypes: AuthType[],
actorTypes: string[]
): AuthContext | null => {
if (!authTypes.includes(SESSION_AUTH)) {
return null
}
if (
session.auth_context &&
(actorTypes.includes("*") ||
actorTypes.includes(session.auth_context.actor_type))
) {
return session.auth_context
}
return null
}
const getAuthContextFromJwtToken = (
authHeader: string | undefined,
jwtSecret: string,
authTypes: AuthType[],
actorTypes: string[]
): AuthContext | null => {
if (!authTypes.includes(BEARER_AUTH)) {
return null
}
if (!authHeader) {
return null
}
const re = /(\S+)\s+(\S+)/
const matches = authHeader.match(re)
// TODO: figure out how to obtain token (and store correct data in token)
if (matches) {
const tokenType = matches[1]
const token = matches[2]
if (tokenType.toLowerCase() === BEARER_AUTH) {
// get config jwt secret
// verify token and set authUser
try {
const verified = verify(token, jwtSecret) as JwtPayload
if (
actorTypes.includes("*") ||
actorTypes.includes(verified.actor_type)
) {
return verified as AuthContext
}
} catch (err) {
return null
}
}
}
return null
}
@@ -0,0 +1,102 @@
import { NextFunction, Response } from "express"
import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"
import { formatException } from "./exception-formatter"
import { MedusaRequest } from "../types"
import { logger as logger_ } from "../../logger"
const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError"
const TRANSACTION_STARTED = "TransactionAlreadyStartedError"
const TRANSACTION_NOT_STARTED = "TransactionNotStartedError"
const API_ERROR = "api_error"
const INVALID_REQUEST_ERROR = "invalid_request_error"
const INVALID_STATE_ERROR = "invalid_state_error"
export function errorHandler() {
return (
err: MedusaError,
req: MedusaRequest,
res: Response,
next: NextFunction
) => {
const logger: typeof logger_ = req.scope.resolve(
ContainerRegistrationKeys.LOGGER
)
err = formatException(err)
logger.error(err)
const errorType = err.type || err.name
const errObj = {
code: err.code,
type: err.type,
message: err.message,
}
let statusCode = 500
switch (errorType) {
case QUERY_RUNNER_RELEASED:
case TRANSACTION_STARTED:
case TRANSACTION_NOT_STARTED:
case MedusaError.Types.CONFLICT:
statusCode = 409
errObj.code = INVALID_STATE_ERROR
errObj.message =
"The request conflicted with another request. You may retry the request with the provided Idempotency-Key."
break
case MedusaError.Types.UNAUTHORIZED:
statusCode = 401
break
case MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR:
statusCode = 422
break
case MedusaError.Types.DUPLICATE_ERROR:
statusCode = 422
errObj.code = INVALID_REQUEST_ERROR
break
case MedusaError.Types.NOT_ALLOWED:
case MedusaError.Types.INVALID_DATA:
statusCode = 400
break
case MedusaError.Types.NOT_FOUND:
statusCode = 404
break
case MedusaError.Types.DB_ERROR:
statusCode = 500
errObj.code = API_ERROR
break
case MedusaError.Types.UNEXPECTED_STATE:
case MedusaError.Types.INVALID_ARGUMENT:
break
default:
errObj.code = "unknown_error"
errObj.message = "An unknown error occurred."
errObj.type = "unknown_error"
break
}
res.status(statusCode).json(errObj)
}
}
/**
* @schema Error
* title: "Response Error"
* type: object
* properties:
* code:
* type: string
* description: A slug code to indicate the type of the error.
* enum: [invalid_state_error, invalid_request_error, api_error, unknown_error]
* message:
* type: string
* description: Description of the error that occurred.
* example: "first_name must be a string"
* type:
* type: string
* description: A slug indicating the type of the error.
* enum: [QueryRunnerAlreadyReleasedError, TransactionAlreadyStartedError, TransactionNotStartedError, conflict, unauthorized, payment_authorization_error, duplicate_error, not_allowed, invalid_data, not_found, database_error, unexpected_state, invalid_argument, unknown_error]
*/
@@ -0,0 +1,56 @@
import { MedusaError } from "@medusajs/utils"
export enum PostgresError {
DUPLICATE_ERROR = "23505",
FOREIGN_KEY_ERROR = "23503",
SERIALIZATION_FAILURE = "40001",
NULL_VIOLATION = "23502",
}
export const formatException = (err): MedusaError => {
switch (err.code) {
case PostgresError.DUPLICATE_ERROR:
return new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
`${err.table.charAt(0).toUpperCase()}${err.table.slice(
1
)} with ${err.detail.slice(4).replace(/[()=]/g, (s) => {
return s === "=" ? " " : ""
})}`
)
case PostgresError.FOREIGN_KEY_ERROR: {
const matches =
/Key \(([\w-\d]+)\)=\(([\w-\d]+)\) is not present in table "(\w+)"/g.exec(
err.detail
)
if (matches?.length !== 4) {
return new MedusaError(
MedusaError.Types.NOT_FOUND,
JSON.stringify(matches)
)
}
return new MedusaError(
MedusaError.Types.NOT_FOUND,
`${matches[3]?.charAt(0).toUpperCase()}${matches[3]?.slice(1)} with ${
matches[1]
} ${matches[2]} does not exist.`
)
}
case PostgresError.SERIALIZATION_FAILURE: {
return new MedusaError(
MedusaError.Types.CONFLICT,
err?.detail ?? err?.message
)
}
case PostgresError.NULL_VIOLATION: {
return new MedusaError(
MedusaError.Types.INVALID_DATA,
`Can't insert null value in field ${err?.column} on insert in table ${err?.table}`
)
}
default:
return err
}
}
@@ -0,0 +1,3 @@
export * from "./authenticate-middleware"
export * from "./error-handler"
export * from "./exception-formatter"
@@ -1,16 +1,13 @@
import { ConfigModule } from "@medusajs/types"
import { parseCorsOrigins, promiseAll, wrapHandler } from "@medusajs/utils" import { parseCorsOrigins, promiseAll, wrapHandler } from "@medusajs/utils"
import cors from "cors" import cors from "cors"
import { type Express, json, Router, text, urlencoded } from "express" import { type Express, json, Router, text, urlencoded } from "express"
import { readdir } from "fs/promises" import { readdir } from "fs/promises"
import { extname, join, sep } from "path" import { extname, join, parse, sep } from "path"
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
import { authenticate, errorHandler } from "../../../utils/middlewares"
import { logger } from "@medusajs/framework"
import { import {
AsyncRouteHandler,
GlobalMiddlewareDescriptor, GlobalMiddlewareDescriptor,
HTTP_METHODS, HTTP_METHODS,
MedusaRequest,
MedusaResponse,
MiddlewareRoute, MiddlewareRoute,
MiddlewaresConfig, MiddlewaresConfig,
MiddlewareVerb, MiddlewareVerb,
@@ -19,6 +16,9 @@ import {
RouteDescriptor, RouteDescriptor,
RouteVerb, RouteVerb,
} from "./types" } from "./types"
import { authenticate, errorHandler } from "./middlewares"
import { configManager } from "../config"
import { logger } from "../logger"
const log = ({ const log = ({
activityId, activityId,
@@ -163,40 +163,68 @@ function getBodyParserMiddleware(args?: ParserConfigArgs) {
] ]
} }
export class RoutesLoader { // TODO this router would need a proper rework, but it is out of scope right now
protected routesMap = new Map<string, RouteDescriptor>()
protected globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
protected app: Express class ApiRoutesLoader {
protected router: Router /**
protected activityId?: string * Map of router path and its descriptor
protected rootDir: string * @private
protected configModule: ConfigModule */
protected excludes: RegExp[] = [ #routesMap = new Map<string, RouteDescriptor>()
/**
* Global middleware descriptors
* @private
*/
#globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
/**
* An express instance
* @private
*/
readonly #app: Express
/**
* A router to assign the route to
* @private
*/
readonly #router: Router
/**
* An eventual activity id for information tracking
* @private
*/
readonly #activityId?: string
/**
* The list of file names to exclude from the routes scan
* @private
*/
#excludes: RegExp[] = [
/\.DS_Store/, /\.DS_Store/,
/(\.ts\.map|\.js\.map|\.d\.ts)/, /(\.ts\.map|\.js\.map|\.d\.ts|\.md)/,
/^_[^/\\]*(\.[^/\\]+)?$/, /^_[^/\\]*(\.[^/\\]+)?$/,
] ]
/**
* Path from where to load the routes from
* @private
*/
readonly #sourceDir: string
constructor({ constructor({
app, app,
activityId, activityId,
rootDir, sourceDir,
configModule,
excludes = [],
}: { }: {
app: Express app: Express
activityId?: string activityId?: string
rootDir: string sourceDir: string
configModule: ConfigModule
excludes?: RegExp[]
}) { }) {
this.app = app this.#app = app
this.router = Router() this.#router = Router()
this.activityId = activityId this.#activityId = activityId
this.rootDir = rootDir this.#sourceDir = sourceDir
this.configModule = configModule
this.excludes.push(...(excludes ?? []))
} }
/** /**
@@ -215,7 +243,7 @@ export class RoutesLoader {
}): void { }): void {
if (!config?.routes && !config?.errorHandler) { if (!config?.routes && !config?.errorHandler) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Empty middleware config. Skipping middleware application.`, message: `Empty middleware config. Skipping middleware application.`,
}) })
@@ -255,7 +283,7 @@ export class RoutesLoader {
if (match?.[1] && !Number.isInteger(match?.[1])) { if (match?.[1] && !Number.isInteger(match?.[1])) {
if (parameters.has(match?.[1])) { if (parameters.has(match?.[1])) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Duplicate parameters found in route ${route} (${match?.[1]})`, message: `Duplicate parameters found in route ${route} (${match?.[1]})`,
}) })
@@ -289,12 +317,12 @@ export class RoutesLoader {
*/ */
protected async createRoutesConfig(): Promise<void> { protected async createRoutesConfig(): Promise<void> {
await promiseAll( await promiseAll(
[...this.routesMap.values()].map(async (descriptor: RouteDescriptor) => { [...this.#routesMap.values()].map(async (descriptor: RouteDescriptor) => {
const absolutePath = descriptor.absolutePath const absolutePath = descriptor.absolutePath
const route = descriptor.route const route = descriptor.route
return await import(absolutePath).then((import_) => { return await import(absolutePath).then((import_) => {
const map = this.routesMap const map = this.#routesMap
const config: RouteConfig = { const config: RouteConfig = {
routes: [], routes: [],
@@ -353,7 +381,7 @@ export class RoutesLoader {
}) })
} else { } else {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Skipping handler ${handler} in ${absolutePath}. Invalid HTTP method: ${handler}.`, message: `Skipping handler ${handler} in ${absolutePath}. Invalid HTTP method: ${handler}.`,
}) })
} }
@@ -361,7 +389,7 @@ export class RoutesLoader {
if (!config.routes?.length) { if (!config.routes?.length) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `No valid route handlers detected in ${absolutePath}. Skipping route configuration.`, message: `No valid route handlers detected in ${absolutePath}. Skipping route configuration.`,
}) })
@@ -376,29 +404,20 @@ export class RoutesLoader {
) )
} }
protected createRoutesDescriptor({ protected createRoutesDescriptor(path: string) {
childPath,
parentPath,
}: {
childPath: string
parentPath?: string
}) {
const descriptor: RouteDescriptor = { const descriptor: RouteDescriptor = {
absolutePath: childPath, absolutePath: path,
relativePath: "", relativePath: path,
route: "", route: "",
priority: Infinity, priority: Infinity,
} }
if (parentPath) { const childPath = path.replace(this.#sourceDir, "")
childPath = childPath.replace(parentPath, "")
}
descriptor.relativePath = childPath descriptor.relativePath = childPath
let routeToParse = childPath let routeToParse = childPath
const pathSegments = childPath.split(sep) const pathSegments = routeToParse.split(sep)
const lastSegment = pathSegments[pathSegments.length - 1] const lastSegment = pathSegments[pathSegments.length - 1]
if (lastSegment.startsWith("route")) { if (lastSegment.startsWith("route")) {
@@ -409,40 +428,36 @@ export class RoutesLoader {
descriptor.route = this.parseRoute(routeToParse) descriptor.route = this.parseRoute(routeToParse)
descriptor.priority = calculatePriority(descriptor.route) descriptor.priority = calculatePriority(descriptor.route)
this.routesMap.set(childPath, descriptor) this.#routesMap.set(path, descriptor)
} }
protected async createMiddlewaresDescriptor({ protected async createMiddlewaresDescriptor() {
dirPath, const filePaths = await readdir(this.#sourceDir)
}: {
dirPath: string
}) {
const files = await readdir(dirPath)
const middlewareFilePath = files const filteredFilePaths = filePaths.filter((path) => {
.filter((path) => { const pathToCheck = path.replace(this.#sourceDir, "")
if ( return !pathToCheck
this.excludes.length && .split(sep)
this.excludes.some((exclude) => exclude.test(path)) .some((segment) =>
) { this.#excludes.some((exclude) => exclude.test(segment))
return false )
}
return true
}) })
.find((file) => {
const middlewareFilePath = filteredFilePaths.find((file) => {
return file.replace(/\.[^/.]+$/, "") === MIDDLEWARES_NAME return file.replace(/\.[^/.]+$/, "") === MIDDLEWARES_NAME
}) })
if (!middlewareFilePath) { if (!middlewareFilePath) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `No middleware files found in ${dirPath}. Skipping middleware configuration.`, message: `No middleware files found in ${
this.#sourceDir
}. Skipping middleware configuration.`,
}) })
return return
} }
const absolutePath = join(dirPath, middlewareFilePath) const absolutePath = join(this.#sourceDir, middlewareFilePath)
try { try {
await import(absolutePath).then((import_) => { await import(absolutePath).then((import_) => {
@@ -452,7 +467,7 @@ export class RoutesLoader {
if (!middlewaresConfig) { if (!middlewaresConfig) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`, message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`,
}) })
return return
@@ -471,11 +486,11 @@ export class RoutesLoader {
this.validateMiddlewaresConfig(descriptor) this.validateMiddlewaresConfig(descriptor)
this.globalMiddlewaresDescriptor = descriptor this.#globalMiddlewaresDescriptor = descriptor
}) })
} catch (error) { } catch (error) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Failed to load middleware configuration in ${absolutePath}. Skipping middleware configuration.`, message: `Failed to load middleware configuration in ${absolutePath}. Skipping middleware configuration.`,
}) })
@@ -483,54 +498,34 @@ export class RoutesLoader {
} }
} }
protected async createRoutesMap({ protected async createRoutesMap(): Promise<void> {
dirPath,
parentPath,
}: {
dirPath: string
parentPath?: string
}): Promise<void> {
await promiseAll( await promiseAll(
await readdir(dirPath, { withFileTypes: true }).then((entries) => { await readdir(this.#sourceDir, {
return entries recursive: true,
.filter((entry) => { withFileTypes: true,
if ( }).then((entries) => {
this.excludes.length && const fileEntries = entries.filter((entry) => {
this.excludes.some((exclude) => exclude.test(entry.name)) const fullPathFromSource = join(entry.path, entry.name).replace(
) { this.#sourceDir,
return false ""
} )
const isExcluded = fullPathFromSource
.split(sep)
.some((segment) =>
this.#excludes.some((exclude) => exclude.test(segment))
)
let name = entry.name return (
!entry.isDirectory() &&
const extension = extname(name) !isExcluded &&
parse(entry.name).name === ROUTE_NAME
if (extension) { )
name = name.replace(extension, "")
}
if (entry.isFile() && name !== ROUTE_NAME) {
return false
}
return true
}) })
.map(async (entry) => {
const childPath = join(dirPath, entry.name)
if (entry.isDirectory()) { return fileEntries.map(async (entry) => {
return await this.createRoutesMap({ const path = join(entry.path, entry.name)
dirPath: childPath, return this.createRoutesDescriptor(path)
parentPath: parentPath ?? dirPath,
}) })
}
return this.createRoutesDescriptor({
childPath,
parentPath,
})
})
.flat(Infinity)
}) })
) )
} }
@@ -539,7 +534,7 @@ export class RoutesLoader {
* Apply the most specific body parser middleware to the router * Apply the most specific body parser middleware to the router
*/ */
applyBodyParserMiddleware(path: string, method: RouteVerb): void { applyBodyParserMiddleware(path: string, method: RouteVerb): void {
const middlewareDescriptor = this.globalMiddlewaresDescriptor const middlewareDescriptor = this.#globalMiddlewaresDescriptor
const mostSpecificConfig = findMatch( const mostSpecificConfig = findMatch(
path, path,
@@ -548,13 +543,13 @@ export class RoutesLoader {
) )
if (!mostSpecificConfig || mostSpecificConfig?.bodyParser === undefined) { if (!mostSpecificConfig || mostSpecificConfig?.bodyParser === undefined) {
this.router[method.toLowerCase()](path, ...getBodyParserMiddleware()) this.#router[method.toLowerCase()](path, ...getBodyParserMiddleware())
return return
} }
if (mostSpecificConfig?.bodyParser) { if (mostSpecificConfig?.bodyParser) {
this.router[method.toLowerCase()]( this.#router[method.toLowerCase()](
path, path,
...getBodyParserMiddleware(mostSpecificConfig?.bodyParser) ...getBodyParserMiddleware(mostSpecificConfig?.bodyParser)
) )
@@ -572,7 +567,7 @@ export class RoutesLoader {
* that they are applied before any other middleware. * that they are applied before any other middleware.
*/ */
applyRouteSpecificMiddlewares(): void { applyRouteSpecificMiddlewares(): void {
const prioritizedRoutes = prioritize([...this.routesMap.values()]) const prioritizedRoutes = prioritize([...this.#routesMap.values()])
for (const descriptor of prioritizedRoutes) { for (const descriptor of prioritizedRoutes) {
if (!descriptor.config?.routes?.length) { if (!descriptor.config?.routes?.length) {
@@ -591,11 +586,11 @@ export class RoutesLoader {
/** /**
* Apply the admin cors * Apply the admin cors
*/ */
this.router.use( this.#router.use(
descriptor.route, descriptor.route,
cors({ cors({
origin: parseCorsOrigins( origin: parseCorsOrigins(
this.configModule.projectConfig.http.adminCors configManager.config.projectConfig.http.adminCors
), ),
credentials: true, credentials: true,
}) })
@@ -606,11 +601,11 @@ export class RoutesLoader {
/** /**
* Apply the auth cors * Apply the auth cors
*/ */
this.router.use( this.#router.use(
descriptor.route, descriptor.route,
cors({ cors({
origin: parseCorsOrigins( origin: parseCorsOrigins(
this.configModule.projectConfig.http.authCors configManager.config.projectConfig.http.authCors
), ),
credentials: true, credentials: true,
}) })
@@ -621,11 +616,11 @@ export class RoutesLoader {
/** /**
* Apply the store cors * Apply the store cors
*/ */
this.router.use( this.#router.use(
descriptor.route, descriptor.route,
cors({ cors({
origin: parseCorsOrigins( origin: parseCorsOrigins(
this.configModule.projectConfig.http.storeCors configManager.config.projectConfig.http.storeCors
), ),
credentials: true, credentials: true,
}) })
@@ -634,7 +629,7 @@ export class RoutesLoader {
// We only apply the auth middleware to store routes to populate the auth context. For actual authentication, users can just reapply the middleware. // We only apply the auth middleware to store routes to populate the auth context. For actual authentication, users can just reapply the middleware.
if (!config.optedOutOfAuth && config.routeType === "store") { if (!config.optedOutOfAuth && config.routeType === "store") {
this.router.use( this.#router.use(
descriptor.route, descriptor.route,
authenticate("customer", ["bearer", "session"], { authenticate("customer", ["bearer", "session"], {
allowUnauthenticated: true, allowUnauthenticated: true,
@@ -644,7 +639,7 @@ export class RoutesLoader {
if (!config.optedOutOfAuth && config.routeType === "admin") { if (!config.optedOutOfAuth && config.routeType === "admin") {
// We probably don't want to allow access to all endpoints using an api key, but it will do until we revamp our routing. // We probably don't want to allow access to all endpoints using an api key, but it will do until we revamp our routing.
this.router.use( this.#router.use(
descriptor.route, descriptor.route,
authenticate("user", ["bearer", "session", "api-key"]) authenticate("user", ["bearer", "session", "api-key"])
) )
@@ -664,7 +659,7 @@ export class RoutesLoader {
* Apply the error handler middleware to the router * Apply the error handler middleware to the router
*/ */
applyErrorHandlerMiddleware(): void { applyErrorHandlerMiddleware(): void {
const middlewareDescriptor = this.globalMiddlewaresDescriptor const middlewareDescriptor = this.#globalMiddlewaresDescriptor
const errorHandlerFn = middlewareDescriptor?.config?.errorHandler const errorHandlerFn = middlewareDescriptor?.config?.errorHandler
/** /**
@@ -678,7 +673,7 @@ export class RoutesLoader {
* If the user has provided a custom error handler then use it * If the user has provided a custom error handler then use it
*/ */
if (errorHandlerFn) { if (errorHandlerFn) {
this.router.use(errorHandlerFn) this.#router.use(errorHandlerFn as any)
return return
} }
@@ -686,17 +681,17 @@ export class RoutesLoader {
* If the user has not provided a custom error handler then use the * If the user has not provided a custom error handler then use the
* default one. * default one.
*/ */
this.router.use(errorHandler()) this.#router.use(errorHandler() as any)
} }
protected async registerRoutes(): Promise<void> { protected async registerRoutes(): Promise<void> {
const middlewareDescriptor = this.globalMiddlewaresDescriptor const middlewareDescriptor = this.#globalMiddlewaresDescriptor
const shouldWrapHandler = middlewareDescriptor?.config const shouldWrapHandler = middlewareDescriptor?.config
? middlewareDescriptor.config.errorHandler !== false ? middlewareDescriptor.config.errorHandler !== false
: true : true
const prioritizedRoutes = prioritize([...this.routesMap.values()]) const prioritizedRoutes = prioritize([...this.#routesMap.values()])
for (const descriptor of prioritizedRoutes) { for (const descriptor of prioritizedRoutes) {
if (!descriptor.config?.routes?.length) { if (!descriptor.config?.routes?.length) {
@@ -707,7 +702,7 @@ export class RoutesLoader {
for (const route of routes) { for (const route of routes) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Registering route [${route.method?.toUpperCase()}] - ${ message: `Registering route [${route.method?.toUpperCase()}] - ${
descriptor.route descriptor.route
}`, }`,
@@ -718,16 +713,16 @@ export class RoutesLoader {
* we wrap the handler in a try/catch block. * we wrap the handler in a try/catch block.
*/ */
const handler = shouldWrapHandler const handler = shouldWrapHandler
? wrapHandler(route.handler as AsyncRouteHandler) ? wrapHandler(route.handler as Parameters<typeof wrapHandler>[0])
: route.handler : route.handler
this.router[route.method!.toLowerCase()](descriptor.route, handler) this.#router[route.method!.toLowerCase()](descriptor.route, handler)
} }
} }
} }
protected async registerMiddlewares(): Promise<void> { protected async registerMiddlewares(): Promise<void> {
const descriptor = this.globalMiddlewaresDescriptor const descriptor = this.#globalMiddlewaresDescriptor
if (!descriptor) { if (!descriptor) {
return return
@@ -757,17 +752,17 @@ export class RoutesLoader {
for (const method of methods) { for (const method of methods) {
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Registering middleware [${method}] - ${route.matcher}`, message: `Registering middleware [${method}] - ${route.matcher}`,
}) })
this.router[method.toLowerCase()](route.matcher, ...route.middlewares) this.#router[method.toLowerCase()](route.matcher, ...route.middlewares)
} }
} }
} }
async load() { async load() {
performance && performance.mark("file-base-routing-start" + this.rootDir) performance && performance.mark("file-base-routing-start" + this.#sourceDir)
let apiExists = true let apiExists = true
@@ -778,15 +773,15 @@ export class RoutesLoader {
* directory does not exist. * directory does not exist.
*/ */
try { try {
await readdir(this.rootDir) await readdir(this.#sourceDir)
} catch (_error) { } catch (_error) {
apiExists = false apiExists = false
} }
if (apiExists) { if (apiExists) {
await this.createMiddlewaresDescriptor({ dirPath: this.rootDir }) await this.createMiddlewaresDescriptor()
await this.createRoutesMap({ dirPath: this.rootDir }) await this.createRoutesMap()
await this.createRoutesConfig() await this.createRoutesConfig()
this.applyRouteSpecificMiddlewares() this.applyRouteSpecificMiddlewares()
@@ -802,28 +797,78 @@ export class RoutesLoader {
* This prevents middleware from a plugin from * This prevents middleware from a plugin from
* bleeding into the global middleware stack. * bleeding into the global middleware stack.
*/ */
this.app.use("/", this.router) this.#app.use("/", this.#router)
} }
performance && performance.mark("file-base-routing-end" + this.rootDir) performance && performance.mark("file-base-routing-end" + this.#sourceDir)
const timeSpent = const timeSpent =
performance && performance &&
performance performance
.measure( .measure(
"file-base-routing-measure" + this.rootDir, "file-base-routing-measure" + this.#sourceDir,
"file-base-routing-start" + this.rootDir, "file-base-routing-start" + this.#sourceDir,
"file-base-routing-end" + this.rootDir "file-base-routing-end" + this.#sourceDir
) )
?.duration?.toFixed(2) ?.duration?.toFixed(2)
log({ log({
activityId: this.activityId, activityId: this.#activityId,
message: `Routes loaded in ${timeSpent} ms`, message: `Routes loaded in ${timeSpent} ms`,
}) })
this.routesMap.clear() this.#routesMap.clear()
this.globalMiddlewaresDescriptor = undefined this.#globalMiddlewaresDescriptor = undefined
} }
} }
export default RoutesLoader export class RoutesLoader {
/**
* An express instance
* @private
*/
readonly #app: Express
/**
* An eventual activity id for information tracking
* @private
*/
readonly #activityId?: string
/**
* Path from where to load the routes from
* @private
*/
readonly #sourceDir: string | string[]
constructor({
app,
activityId,
sourceDir,
}: {
app: Express
activityId?: string
sourceDir: string | string[]
}) {
this.#app = app
this.#activityId = activityId
this.#sourceDir = sourceDir
}
async load() {
const normalizedSourcePath = Array.isArray(this.#sourceDir)
? this.#sourceDir
: [this.#sourceDir]
const promises = normalizedSourcePath.map(async (sourcePath) => {
const apiRoutesLoader = new ApiRoutesLoader({
app: this.#app,
activityId: this.#activityId,
sourceDir: sourcePath,
})
await apiRoutesLoader.load()
})
await promiseAll(promises)
}
}
@@ -0,0 +1,180 @@
import { ZodObject } from "zod"
import type { NextFunction, Request, Response } from "express"
import { MedusaPricingContext, RequestQueryFields } from "@medusajs/types"
import * as core from "express-serve-static-core"
import { MedusaContainer } from "../container"
export interface FindConfig<Entity> {
select?: (keyof Entity)[]
skip?: number
take?: number
relations?: string[]
order?: { [K: string]: "ASC" | "DESC" }
}
/**
* 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]
export type MiddlewareVerb = "USE" | "ALL" | RouteVerb
type SyncRouteHandler = (req: MedusaRequest, res: MedusaResponse) => void
export type AsyncRouteHandler = (
req: MedusaRequest,
res: MedusaResponse
) => Promise<void>
type RouteHandler = SyncRouteHandler | AsyncRouteHandler
export type RouteImplementation = {
method?: RouteVerb
handler: RouteHandler
}
export type RouteConfig = {
optedOutOfAuth?: boolean
routeType?: "admin" | "store" | "auth"
shouldAppendAdminCors?: boolean
shouldAppendStoreCors?: boolean
shouldAppendAuthCors?: boolean
routes?: RouteImplementation[]
}
export type MiddlewareFunction =
| MedusaRequestHandler
| ((...args: any[]) => any)
export type MedusaErrorHandlerFunction = (
error: any,
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => Promise<void> | void
export type ParserConfigArgs = {
sizeLimit?: string | number | undefined
preserveRawBody?: boolean
}
export type ParserConfig = false | ParserConfigArgs
export type MiddlewareRoute = {
method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp
bodyParser?: ParserConfig
middlewares?: MiddlewareFunction[]
}
export type MiddlewaresConfig = {
errorHandler?: false | MedusaErrorHandlerFunction
routes?: MiddlewareRoute[]
}
export type RouteDescriptor = {
absolutePath: string
relativePath: string
route: string
priority: number
config?: RouteConfig
}
export type GlobalMiddlewareDescriptor = {
config?: MiddlewaresConfig
}
export interface MedusaRequest<Body = unknown>
extends Request<core.ParamsDictionary, any, Body> {
validatedBody: Body
validatedQuery: RequestQueryFields & Record<string, unknown>
/**
* TODO: shouldn't this correspond to returnable fields instead of allowed fields? also it is used by the cleanResponseData util
*/
allowedProperties: string[]
/**
* An object containing the select, relation, skip, take and order to be used with medusa internal services
*/
listConfig: FindConfig<unknown>
/**
* An object containing the select, relation to be used with medusa internal services
*/
retrieveConfig: FindConfig<unknown>
/**
* An object containing fields and variables to be used with the remoteQuery
*/
remoteQueryConfig: {
fields: string[]
pagination: { order?: Record<string, string>; skip?: number; take?: number }
}
/**
* An object containing the fields that are filterable e.g `{ id: Any<String> }`
*/
filterableFields: Record<string, unknown>
includes?: Record<string, boolean>
/**
* An array of fields and relations that are allowed to be queried, this can be set by the
* consumer as part of a middleware and it will take precedence over the defaultAllowedFields
* @deprecated use `allowed` instead
*/
allowedFields?: string[]
/**
* An array of fields and relations that are allowed to be queried, this can be set by the
* consumer as part of a middleware and it will take precedence over the defaultAllowedFields set
* by the api
*/
allowed?: string[]
errors: string[]
scope: MedusaContainer
session?: any
rawBody?: any
requestId?: string
/**
* An object that carries the context that is used to calculate prices for variants
*/
pricingContext?: MedusaPricingContext
/**
* A generic context object that can be used across the request lifecycle
*/
context?: Record<string, any>
/**
* Custom validators for the request body and query params that will be
* merged with the original validator of the route.
*/
extendedValidators?: {
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
}
export interface AuthContext {
actor_id: string
actor_type: string
auth_identity_id: string
app_metadata: Record<string, unknown>
}
export interface AuthenticatedMedusaRequest<Body = never>
extends MedusaRequest<Body> {
auth_context: AuthContext
}
export type MedusaResponse<Body = unknown> = Response<Body>
export type MedusaNextFunction = NextFunction
export type MedusaRequestHandler<Body = unknown, Res = unknown> = (
req: MedusaRequest<Body>,
res: MedusaResponse<Res>,
next: MedusaNextFunction
) => Promise<void> | void
@@ -0,0 +1,65 @@
import {
MedusaNextFunction,
MedusaRequest,
MedusaRequestHandler,
MedusaResponse,
MiddlewaresConfig,
MiddlewareVerb,
ParserConfig,
} from "../types"
import { ZodObject } from "zod"
/**
* A helper function to configure the routes by defining custom middleware,
* bodyparser config and validators to be merged with the pre-existing
* route validators.
*/
export function defineMiddlewares<
Route extends {
method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp
bodyParser?: ParserConfig
extendedValidators?: {
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
// eslint-disable-next-line space-before-function-paren
middlewares?: (<Req extends MedusaRequest>(
req: Req,
res: MedusaResponse,
next: MedusaNextFunction
) => any)[]
}
>(
config:
| Route[]
| { routes?: Route[]; errorHandler?: MiddlewaresConfig["errorHandler"] }
): MiddlewaresConfig {
const routes = Array.isArray(config) ? config : config.routes || []
const errorHandler = Array.isArray(config) ? undefined : config.errorHandler
return {
errorHandler,
routes: routes.map((route) => {
const { middlewares, extendedValidators, ...rest } = route
const customMiddleware: MedusaRequestHandler[] = []
/**
* Define a custom validator when "extendedValidators.body" or
* "extendedValidators.queryParams" validation schema is
* provided.
*/
if (extendedValidators?.body || extendedValidators?.queryParams) {
customMiddleware.push((req, _, next) => {
req.extendedValidators = extendedValidators
next()
})
}
return {
...rest,
middlewares: customMiddleware.concat(middlewares || []),
}
}),
}
}
@@ -1,6 +1,6 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateApiKey, AdminCreateApiKey,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { createLinkBody } from "../../utils/validators" import { createLinkBody } from "../../utils/validators"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateCollection, AdminCreateCollection,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { AdminGetCurrencyParams, AdminGetCurrenciesParams } from "./validators" import { AdminGetCurrencyParams, AdminGetCurrenciesParams } from "./validators"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { createLinkBody } from "../../utils/validators" import { createLinkBody } from "../../utils/validators"
@@ -10,7 +10,7 @@ import {
AdminUpdateCustomerAddress, AdminUpdateCustomerAddress,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter" import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateInventoryItem, AdminCreateInventoryItem,
@@ -8,7 +8,7 @@ import {
AdminInviteAccept, AdminInviteAccept,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware" import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { import {
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { unlessPath } from "../../utils/unless-path" import { unlessPath } from "../../utils/unless-path"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { createBatchBody, createLinkBody } from "../../utils/validators" import { createBatchBody, createLinkBody } from "../../utils/validators"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { createLinkBody } from "../../utils/validators" import { createLinkBody } from "../../utils/validators"
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateProductTag, AdminCreateProductTag,
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateProductType, AdminCreateProductType,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter" import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { unlessPath } from "../../utils/unless-path" import { unlessPath } from "../../utils/unless-path"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { unlessPath } from "../../utils/unless-path" import { unlessPath } from "../../utils/unless-path"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateRegion, AdminCreateRegion,
@@ -1,6 +1,6 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
AdminCreateReservation, AdminCreateReservation,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter" import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { import {
AdminCreateShippingOption, AdminCreateShippingOption,
AdminCreateShippingOptionRule, AdminCreateShippingOptionRule,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter" import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -9,7 +9,7 @@ import {
AdminUpdateTaxRate, AdminUpdateTaxRate,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
@@ -6,7 +6,7 @@ import {
AdminGetTaxRegionsParams, AdminGetTaxRegionsParams,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,5 +1,5 @@
import multer from "multer" import multer from "multer"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { retrieveUploadConfig } from "./query-config" import { retrieveUploadConfig } from "./query-config"
import { AdminGetUploadParams } from "./validators" import { AdminGetUploadParams } from "./validators"
@@ -7,7 +7,7 @@ import {
AdminUpdateUser, AdminUpdateUser,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware" import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
@@ -7,7 +7,7 @@ import {
AdminGetWorkflowExecutionsParams, AdminGetWorkflowExecutionsParams,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
+1 -1
View File
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../utils/middlewares/authenticate-middleware" import { authenticate } from "../../utils/middlewares/authenticate-middleware"
export const authRoutesMiddlewares: MiddlewareRoute[] = [ export const authRoutesMiddlewares: MiddlewareRoute[] = [
+1 -1
View File
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
export const hooksRoutesMiddlewares: MiddlewareRoute[] = [ export const hooksRoutesMiddlewares: MiddlewareRoute[] = [
{ {
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { ensurePublishableKeyAndSalesChannelMatch } from "../../utils/middlewares/common/ensure-pub-key-sales-channel-match" import { ensurePublishableKeyAndSalesChannelMatch } from "../../utils/middlewares/common/ensure-pub-key-sales-channel-match"
import { maybeAttachPublishableKeyScopes } from "../../utils/middlewares/common/maybe-attach-pub-key-scopes" import { maybeAttachPublishableKeyScopes } from "../../utils/middlewares/common/maybe-attach-pub-key-scopes"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { import {
StoreGetCollectionParams, StoreGetCollectionParams,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { StoreGetCurrenciesParams, StoreGetCurrencyParams } from "./validators" import { StoreGetCurrenciesParams, StoreGetCurrencyParams } from "./validators"
@@ -10,7 +10,7 @@ import {
StoreGetCustomerAddressParams, StoreGetCustomerAddressParams,
} from "./validators" } from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware" import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware" import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,11 +1,11 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as queryConfig from "./query-config" import * as queryConfig from "./query-config"
import { import {
StoreGetPaymentCollectionParams,
StoreCreatePaymentCollection, StoreCreatePaymentCollection,
StoreCreatePaymentSession, StoreCreatePaymentSession,
StoreGetPaymentCollectionParams,
} from "./validators" } from "./validators"
export const storePaymentCollectionsMiddlewares: MiddlewareRoute[] = [ export const storePaymentCollectionsMiddlewares: MiddlewareRoute[] = [
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as queryConfig from "./query-config" import * as queryConfig from "./query-config"
import { StoreGetPaymentProvidersParams } from "./validators" import { StoreGetPaymentProvidersParams } from "./validators"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { applyCategoryFilters } from "./helpers" import { applyCategoryFilters } from "./helpers"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,5 +1,5 @@
import { isPresent, ProductStatus } from "@medusajs/utils" import { isPresent, ProductStatus } from "@medusajs/utils"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter" import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { import {
applyDefaultFilters, applyDefaultFilters,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { StoreGetRegionParams, StoreGetRegionsParams } from "./validators" import { StoreGetRegionParams, StoreGetRegionsParams } from "./validators"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { StoreReturnReasonParams } from "./validators" import { StoreReturnReasonParams } from "./validators"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query" import { validateAndTransformQuery } from "../../utils/validate-query"
import { listTransformQueryConfig } from "./query-config" import { listTransformQueryConfig } from "./query-config"
import { StoreGetShippingOptions } from "./validators" import { StoreGetShippingOptions } from "./validators"
+1 -1
View File
@@ -1,6 +1,6 @@
import { NextFunction } from "express" import { NextFunction } from "express"
import { MedusaRequest, MedusaResponse } from "../../types/routing" import { MedusaRequest, MedusaResponse } from "../../types/routing"
import { MiddlewareFunction } from "../../types/middlewares" import { MiddlewareFunction } from "@medusajs/framework"
// Due to how our route loader works, where we load all middlewares before routes, ambiguous routes end up having all middlewares on different routes executed before the route handler is. // Due to how our route loader works, where we load all middlewares before routes, ambiguous routes end up having all middlewares on different routes executed before the route handler is.
// This function allows us to skip middlewares for particular routes, so we can temporarily solve this without completely breaking the route loader for everyone. // This function allows us to skip middlewares for particular routes, so we can temporarily solve this without completely breaking the route loader for everyone.
+13 -20
View File
@@ -1,8 +1,7 @@
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { Express } from "express" import { Express } from "express"
import path from "path" import { join } from "path"
import qs from "qs" import qs from "qs"
import { RoutesLoader } from "./helpers/routing" import { RoutesLoader } from "@medusajs/framework"
import { MedusaContainer, PluginDetails } from "@medusajs/types" import { MedusaContainer, PluginDetails } from "@medusajs/types"
type Options = { type Options = {
@@ -24,9 +23,7 @@ export default async ({ app, container, plugins }: Options) => {
next() next()
}) })
const configModule = container.resolve( const sourcePaths: string[] = []
ContainerRegistrationKeys.CONFIG_MODULE
)
/** /**
* Always load plugin routes before the Medusa core routes, since it * Always load plugin routes before the Medusa core routes, since it
@@ -40,31 +37,27 @@ export default async ({ app, container, plugins }: Options) => {
* route will never be resolved, because it will be handled by the * route will never be resolved, because it will be handled by the
* "/products/:id" route. * "/products/:id" route.
*/ */
await Promise.all( sourcePaths.push(
plugins.map(async (pluginDetails) => { ...plugins.map((pluginDetails) => {
return new RoutesLoader({ return join(pluginDetails.resolve, "api")
app: app, }),
configModule, /**
rootDir: path.join(pluginDetails.resolve, "api"), * Register the Medusa CORE API routes using the file based routing.
}).load() */
}) join(__dirname, "../api")
) )
// TODO: Figure out why this is causing issues with test when placed inside ./api.ts // TODO: Figure out why this is causing issues with test when placed inside ./api.ts
// Adding this here temporarily // Adding this here temporarily
// Test: (packages/medusa/src/api/routes/admin/currencies/update-currency.ts) // Test: (packages/medusa/src/api/routes/admin/currencies/update-currency.ts)
try { try {
/**
* Register the Medusa CORE API routes using the file based routing.
*/
await new RoutesLoader({ await new RoutesLoader({
app: app, app: app,
rootDir: path.join(__dirname, "../api"), sourceDir: sourcePaths,
configModule,
}).load() }).load()
} catch (err) { } catch (err) {
throw Error( throw Error(
"An error occurred while registering Medusa Core API Routes. See error in logs for more details.", "An error occurred while registering API Routes. See error in logs for more details.",
{ cause: err } { cause: err }
) )
} }
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags" import { FlagSettings } from "@medusajs/framework"
const AnalyticsFeatureFlag: FlagSettings = { const AnalyticsFeatureFlag: FlagSettings = {
key: "analytics", key: "analytics",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags" import { FlagSettings } from "@medusajs/framework"
const OrderEditingFeatureFlag: FlagSettings = { const OrderEditingFeatureFlag: FlagSettings = {
key: "order_editing", key: "order_editing",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags" import { FlagSettings } from "@medusajs/framework"
const ProductCategoryFeatureFlag: FlagSettings = { const ProductCategoryFeatureFlag: FlagSettings = {
key: "product_categories", key: "product_categories",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags" import { FlagSettings } from "@medusajs/framework"
const PublishableAPIKeysFeatureFlag: FlagSettings = { const PublishableAPIKeysFeatureFlag: FlagSettings = {
key: "publishable_api_keys", key: "publishable_api_keys",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags" import { FlagSettings } from "@medusajs/framework"
const SalesChannelFeatureFlag: FlagSettings = { const SalesChannelFeatureFlag: FlagSettings = {
key: "sales_channels", key: "sales_channels",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags" import { FlagSettings } from "@medusajs/framework"
const TaxInclusivePricingFeatureFlag: FlagSettings = { const TaxInclusivePricingFeatureFlag: FlagSettings = {
key: "tax_inclusive_pricing", key: "tax_inclusive_pricing",
@@ -1,87 +0,0 @@
import {
MedusaNextFunction,
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]
export type MiddlewareVerb = "USE" | "ALL" | RouteVerb
type SyncRouteHandler = (req: MedusaRequest, res: MedusaResponse) => void
export type AsyncRouteHandler = (
req: MedusaRequest,
res: MedusaResponse
) => Promise<void>
type RouteHandler = SyncRouteHandler | AsyncRouteHandler
export type RouteImplementation = {
method?: RouteVerb
handler: RouteHandler
}
export type RouteConfig = {
optedOutOfAuth?: boolean
routeType?: "admin" | "store" | "auth"
shouldAppendAdminCors?: boolean
shouldAppendStoreCors?: boolean
shouldAppendAuthCors?: boolean
routes?: RouteImplementation[]
}
export type MiddlewareFunction =
| MedusaRequestHandler
| ((...args: any[]) => any)
export type MedusaErrorHandlerFunction = (
error: any,
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => Promise<void> | void
export type ParserConfigArgs = {
sizeLimit?: string | number | undefined
preserveRawBody?: boolean
}
export type ParserConfig = false | ParserConfigArgs
export type MiddlewareRoute = {
method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp
bodyParser?: ParserConfig
middlewares?: MiddlewareFunction[]
}
export type MiddlewaresConfig = {
errorHandler?: false | MedusaErrorHandlerFunction
routes?: MiddlewareRoute[]
}
export type RouteDescriptor = {
absolutePath: string
relativePath: string
route: string
priority: number
config?: RouteConfig
}
export type GlobalMiddlewareDescriptor = {
config?: MiddlewaresConfig
}
@@ -1,32 +0,0 @@
export interface IFlagRouter {
isFeatureEnabled: (key: string) => boolean
listFlags: () => FeatureFlagsResponse
}
/**
* @schema FeatureFlagsResponse
* type: array
* items:
* type: object
* required:
* - key
* - value
* properties:
* key:
* description: The key of the feature flag.
* type: string
* value:
* description: The value of the feature flag.
* type: boolean
*/
export type FeatureFlagsResponse = {
key: string
value: boolean | Record<string, boolean>
}[]
export type FlagSettings = {
key: string
description: string
env_key: string
default_val: boolean
}
+1 -1
View File
@@ -2,4 +2,4 @@ export type {
MiddlewareFunction, MiddlewareFunction,
MiddlewareRoute, MiddlewareRoute,
MiddlewaresConfig, MiddlewaresConfig,
} from "../loaders/helpers/routing/types" } from "@medusajs/framework"
+1
View File
@@ -9,6 +9,7 @@ import {
import * as core from "express-serve-static-core" import * as core from "express-serve-static-core"
import { FindConfig } from "./common" import { FindConfig } from "./common"
// TODO this will be reqorked and move to the framework at a later point unless decided otherwise
export interface MedusaRequest<Body = unknown> export interface MedusaRequest<Body = unknown>
extends Request<core.ParamsDictionary, any, Body> { extends Request<core.ParamsDictionary, any, Body> {
validatedBody: Body validatedBody: Body
@@ -1,67 +1,8 @@
import { ZodObject } from "zod" import { defineMiddlewares as originalDefineMiddlewares } from "@medusajs/framework"
import {
MedusaRequest,
MedusaResponse,
MedusaNextFunction,
MedusaRequestHandler,
} from "../types/routing"
import {
ParserConfig,
MiddlewareVerb,
MiddlewaresConfig,
} from "../loaders/helpers/routing/types"
/** /**
* A helper function to configure the routes by defining custom middleware, * A helper function to configure the routes by defining custom middleware,
* bodyparser config and validators to be merged with the pre-existing * bodyparser config and validators to be merged with the pre-existing
* route validators. * route validators.
*/ */
export function defineMiddlewares< export const defineMiddlewares = originalDefineMiddlewares
Route extends {
method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp
bodyParser?: ParserConfig
extendedValidators?: {
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
// eslint-disable-next-line space-before-function-paren
middlewares?: (<Req extends MedusaRequest>(
req: Req,
res: MedusaResponse,
next: MedusaNextFunction
) => any)[]
}
>(
config:
| Route[]
| { routes?: Route[]; errorHandler?: MiddlewaresConfig["errorHandler"] }
): MiddlewaresConfig {
const routes = Array.isArray(config) ? config : config.routes || []
const errorHandler = Array.isArray(config) ? undefined : config.errorHandler
return {
errorHandler,
routes: routes.map((route) => {
const { middlewares, extendedValidators, ...rest } = route
const customMiddleware: MedusaRequestHandler[] = []
/**
* Define a custom validator when "extendedValidators.body" or
* "extendedValidators.queryParams" validation schema is
* provided.
*/
if (extendedValidators?.body || extendedValidators?.queryParams) {
customMiddleware.push((req, _, next) => {
req.extendedValidators = extendedValidators
next()
})
}
return {
...rest,
middlewares: customMiddleware.concat(middlewares || []),
}
}),
}
}
@@ -1,56 +1,3 @@
import { MedusaError } from "@medusajs/utils" import { formatException as originalFormatException } from "@medusajs/framework"
export enum PostgresError { export const formatException = originalFormatException
DUPLICATE_ERROR = "23505",
FOREIGN_KEY_ERROR = "23503",
SERIALIZATION_FAILURE = "40001",
NULL_VIOLATION = "23502",
}
export const formatException = (err): MedusaError => {
switch (err.code) {
case PostgresError.DUPLICATE_ERROR:
return new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
`${err.table.charAt(0).toUpperCase()}${err.table.slice(
1
)} with ${err.detail.slice(4).replace(/[()=]/g, (s) => {
return s === "=" ? " " : ""
})}`
)
case PostgresError.FOREIGN_KEY_ERROR: {
const matches =
/Key \(([\w-\d]+)\)=\(([\w-\d]+)\) is not present in table "(\w+)"/g.exec(
err.detail
)
if (matches?.length !== 4) {
return new MedusaError(
MedusaError.Types.NOT_FOUND,
JSON.stringify(matches)
)
}
return new MedusaError(
MedusaError.Types.NOT_FOUND,
`${matches[3]?.charAt(0).toUpperCase()}${matches[3]?.slice(1)} with ${
matches[1]
} ${matches[2]} does not exist.`
)
}
case PostgresError.SERIALIZATION_FAILURE: {
return new MedusaError(
MedusaError.Types.CONFLICT,
err?.detail ?? err?.message
)
}
case PostgresError.NULL_VIOLATION: {
return new MedusaError(
MedusaError.Types.INVALID_DATA,
`Can't insert null value in field ${err?.column} on insert in table ${err?.table}`
)
}
default:
return err
}
}
@@ -1,203 +1,3 @@
import { ApiKeyDTO, ConfigModule, IApiKeyModuleService } from "@medusajs/types" import { authenticate as originalAuthenticate } from "@medusajs/framework"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
} from "@medusajs/utils"
import { NextFunction, RequestHandler } from "express"
import jwt, { JwtPayload } from "jsonwebtoken"
import {
AuthContext,
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
} from "../../types/routing"
const SESSION_AUTH = "session" export const authenticate = originalAuthenticate
const BEARER_AUTH = "bearer"
const API_KEY_AUTH = "api-key"
// This is the only hard-coded actor type, as API keys have special handling for now. We could also generalize API keys to carry the actor type with them.
const ADMIN_ACTOR_TYPE = "user"
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
type MedusaSession = {
auth_context: AuthContext
}
export const authenticate = (
actorType: string | string[],
authType: AuthType | AuthType[],
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
): RequestHandler => {
return async (
req: MedusaRequest,
res: MedusaResponse,
next: NextFunction
): Promise<void> => {
const authTypes = Array.isArray(authType) ? authType : [authType]
const actorTypes = Array.isArray(actorType) ? actorType : [actorType]
const req_ = req as AuthenticatedMedusaRequest
// We only allow authenticating using a secret API key on the admin
const isExclusivelyUser =
actorTypes.length === 1 && actorTypes[0] === ADMIN_ACTOR_TYPE
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyUser) {
const apiKey = await getApiKeyInfo(req)
if (apiKey) {
req_.auth_context = {
actor_id: apiKey.id,
actor_type: "api-key",
auth_identity_id: "",
app_metadata: {},
}
return next()
}
}
// We try to extract the auth context either from the session or from a JWT token
let authContext: AuthContext | null = getAuthContextFromSession(
req.session,
authTypes,
actorTypes
)
if (!authContext) {
const { http } = req.scope.resolve<ConfigModule>(
ContainerRegistrationKeys.CONFIG_MODULE
).projectConfig
authContext = getAuthContextFromJwtToken(
req.headers.authorization,
http.jwtSecret!,
authTypes,
actorTypes
)
}
// If the entity is authenticated, and it is a registered actor we can continue
if (authContext?.actor_id) {
req_.auth_context = authContext
return next()
}
// If the entity is authenticated, but there is no registered actor yet, we can continue (eg. in the case of a user invite) if allow unregistered is set
if (authContext?.auth_identity_id && options.allowUnregistered) {
req_.auth_context = authContext
return next()
}
// If we allow unauthenticated requests (i.e public endpoints), just continue
if (options.allowUnauthenticated) {
return next()
}
res.status(401).json({ message: "Unauthorized" })
}
}
const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
const authHeader = req.headers.authorization
if (!authHeader) {
return null
}
const [tokenType, token] = authHeader.split(" ")
if (tokenType.toLowerCase() !== "basic" || !token) {
return null
}
// The token could have been base64 encoded, we want to decode it first.
let normalizedToken = token
if (!token.startsWith("sk_")) {
normalizedToken = Buffer.from(token, "base64").toString("utf-8")
}
// Basic auth is defined as a username:password set, and since the token is set to the username we need to trim the colon
if (normalizedToken.endsWith(":")) {
normalizedToken = normalizedToken.slice(0, -1)
}
// Secret tokens start with 'sk_', and if it doesn't it could be a user JWT or a malformed token
if (!normalizedToken.startsWith("sk_")) {
return null
}
const apiKeyModule = req.scope.resolve(
ModuleRegistrationName.API_KEY
) as IApiKeyModuleService
try {
const apiKey = await apiKeyModule.authenticate(normalizedToken)
if (!apiKey) {
return null
}
return apiKey
} catch (error) {
console.error(error)
return null
}
}
const getAuthContextFromSession = (
session: Partial<MedusaSession> = {},
authTypes: AuthType[],
actorTypes: string[]
): AuthContext | null => {
if (!authTypes.includes(SESSION_AUTH)) {
return null
}
if (
session.auth_context &&
(actorTypes.includes("*") ||
actorTypes.includes(session.auth_context.actor_type))
) {
return session.auth_context
}
return null
}
const getAuthContextFromJwtToken = (
authHeader: string | undefined,
jwtSecret: string,
authTypes: AuthType[],
actorTypes: string[]
): AuthContext | null => {
if (!authTypes.includes(BEARER_AUTH)) {
return null
}
if (!authHeader) {
return null
}
const re = /(\S+)\s+(\S+)/
const matches = authHeader.match(re)
// TODO: figure out how to obtain token (and store correct data in token)
if (matches) {
const tokenType = matches[1]
const token = matches[2]
if (tokenType.toLowerCase() === BEARER_AUTH) {
// get config jwt secret
// verify token and set authUser
try {
const verified = jwt.verify(token, jwtSecret) as JwtPayload
if (
actorTypes.includes("*") ||
actorTypes.includes(verified.actor_type)
) {
return verified as AuthContext
}
} catch (err) {
return null
}
}
}
return null
}
@@ -1,99 +1,3 @@
import { NextFunction, Request, Response } from "express" import { errorHandler as originalErrorHandler } from "@medusajs/framework"
import { MedusaError } from "@medusajs/utils" export const errorHandler = originalErrorHandler
import { Logger } from "../../types/global"
import { formatException } from "../../utils"
const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError"
const TRANSACTION_STARTED = "TransactionAlreadyStartedError"
const TRANSACTION_NOT_STARTED = "TransactionNotStartedError"
const API_ERROR = "api_error"
const INVALID_REQUEST_ERROR = "invalid_request_error"
const INVALID_STATE_ERROR = "invalid_state_error"
export default () => {
return (
err: MedusaError,
req: Request,
res: Response,
next: NextFunction
) => {
const logger: Logger = req.scope.resolve("logger")
err = formatException(err)
logger.error(err)
const errorType = err.type || err.name
const errObj = {
code: err.code,
type: err.type,
message: err.message,
}
let statusCode = 500
switch (errorType) {
case QUERY_RUNNER_RELEASED:
case TRANSACTION_STARTED:
case TRANSACTION_NOT_STARTED:
case MedusaError.Types.CONFLICT:
statusCode = 409
errObj.code = INVALID_STATE_ERROR
errObj.message =
"The request conflicted with another request. You may retry the request with the provided Idempotency-Key."
break
case MedusaError.Types.UNAUTHORIZED:
statusCode = 401
break
case MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR:
statusCode = 422
break
case MedusaError.Types.DUPLICATE_ERROR:
statusCode = 422
errObj.code = INVALID_REQUEST_ERROR
break
case MedusaError.Types.NOT_ALLOWED:
case MedusaError.Types.INVALID_DATA:
statusCode = 400
break
case MedusaError.Types.NOT_FOUND:
statusCode = 404
break
case MedusaError.Types.DB_ERROR:
statusCode = 500
errObj.code = API_ERROR
break
case MedusaError.Types.UNEXPECTED_STATE:
case MedusaError.Types.INVALID_ARGUMENT:
break
default:
errObj.code = "unknown_error"
errObj.message = "An unknown error occurred."
errObj.type = "unknown_error"
break
}
res.status(statusCode).json(errObj)
}
}
/**
* @schema Error
* title: "Response Error"
* type: object
* properties:
* code:
* type: string
* description: A slug code to indicate the type of the error.
* enum: [invalid_state_error, invalid_request_error, api_error, unknown_error]
* message:
* type: string
* description: Description of the error that occurred.
* example: "first_name must be a string"
* type:
* type: string
* description: A slug indicating the type of the error.
* enum: [QueryRunnerAlreadyReleasedError, TransactionAlreadyStartedError, TransactionNotStartedError, conflict, unauthorized, payment_authorization_error, duplicate_error, not_allowed, invalid_data, not_found, database_error, unexpected_state, invalid_argument, unknown_error]
*/
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more