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
+925 -802

No files matched your search

@@ -1,6 +1,6 @@
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 {
AdminCreateApiKey,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
@@ -1,5 +1,5 @@
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 {
AdminCreateCollection,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
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 { validateAndTransformQuery } from "../../utils/validate-query"
import { createLinkBody } from "../../utils/validators"
@@ -10,7 +10,7 @@ import {
AdminUpdateCustomerAddress,
} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
@@ -1,5 +1,5 @@
import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../types/middlewares"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
AdminCreateInventoryItem,
@@ -8,7 +8,7 @@ import {
AdminInviteAccept,
} from "./validators"
import { MiddlewareRoute } from "../../../types/middlewares"
import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query"
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 * as QueryConfig from "./query-config"
import {
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformBody } from "../../utils/validate-body"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
import { createLinkBody } from "../../utils/validators"
@@ -1,5 +1,5 @@
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 {
AdminCreateProductTag,
@@ -1,5 +1,5 @@
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 {
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 { unlessPath } from "../../utils/unless-path"
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 { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,5 +1,5 @@
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 {
AdminCreateRegion,
@@ -1,6 +1,6 @@
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 {
AdminCreateReservation,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import {
AdminCreateShippingOption,
AdminCreateShippingOptionRule,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../types/middlewares"
import { MiddlewareRoute } from "@medusajs/framework"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { validateAndTransformBody } from "../../utils/validate-body"
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 { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
@@ -9,7 +9,7 @@ import {
AdminUpdateTaxRate,
} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
@@ -6,7 +6,7 @@ import {
AdminGetTaxRegionsParams,
} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -1,5 +1,5 @@
import multer from "multer"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query"
import { retrieveUploadConfig } from "./query-config"
import { AdminGetUploadParams } from "./validators"
@@ -7,7 +7,7 @@ import {
AdminUpdateUser,
} from "./validators"
import { MiddlewareRoute } from "../../../types/middlewares"
import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query"
import { validateAndTransformBody } from "../../utils/validate-body"
@@ -7,7 +7,7 @@ import {
AdminGetWorkflowExecutionsParams,
} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query"
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"
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[] = [
{
@@ -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 { maybeAttachPublishableKeyScopes } from "../../utils/middlewares/common/maybe-attach-pub-key-scopes"
import { validateAndTransformBody } from "../../utils/validate-body"
@@ -1,5 +1,5 @@
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 {
StoreGetCollectionParams,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
import { StoreGetCurrenciesParams, StoreGetCurrencyParams } from "./validators"
@@ -10,7 +10,7 @@ import {
StoreGetCustomerAddressParams,
} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { validateAndTransformBody } from "../../utils/validate-body"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { validateAndTransformQuery } from "../../utils/validate-query"
import * as queryConfig from "./query-config"
import {
StoreGetPaymentCollectionParams,
StoreCreatePaymentCollection,
StoreCreatePaymentSession,
StoreGetPaymentCollectionParams,
} from "./validators"
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 * as queryConfig from "./query-config"
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 { applyCategoryFilters } from "./helpers"
import * as QueryConfig from "./query-config"
@@ -1,5 +1,5 @@
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 {
applyDefaultFilters,
@@ -1,4 +1,4 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { MiddlewareRoute } from "@medusajs/framework"
import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
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 * as QueryConfig from "./query-config"
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 { validateAndTransformQuery } from "../../utils/validate-query"
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 { listTransformQueryConfig } from "./query-config"
import { StoreGetShippingOptions } from "./validators"
+1 -1
View File
@@ -1,6 +1,6 @@
import { NextFunction } from "express"
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.
// 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 path from "path"
import { join } from "path"
import qs from "qs"
import { RoutesLoader } from "./helpers/routing"
import { RoutesLoader } from "@medusajs/framework"
import { MedusaContainer, PluginDetails } from "@medusajs/types"
type Options = {
@@ -24,9 +23,7 @@ export default async ({ app, container, plugins }: Options) => {
next()
})
const configModule = container.resolve(
ContainerRegistrationKeys.CONFIG_MODULE
)
const sourcePaths: string[] = []
/**
* 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
* "/products/:id" route.
*/
await Promise.all(
plugins.map(async (pluginDetails) => {
return new RoutesLoader({
app: app,
configModule,
rootDir: path.join(pluginDetails.resolve, "api"),
}).load()
})
sourcePaths.push(
...plugins.map((pluginDetails) => {
return join(pluginDetails.resolve, "api")
}),
/**
* Register the Medusa CORE API routes using the file based routing.
*/
join(__dirname, "../api")
)
// TODO: Figure out why this is causing issues with test when placed inside ./api.ts
// Adding this here temporarily
// Test: (packages/medusa/src/api/routes/admin/currencies/update-currency.ts)
try {
/**
* Register the Medusa CORE API routes using the file based routing.
*/
await new RoutesLoader({
app: app,
rootDir: path.join(__dirname, "../api"),
configModule,
sourceDir: sourcePaths,
}).load()
} catch (err) {
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 }
)
}
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags"
import { FlagSettings } from "@medusajs/framework"
const AnalyticsFeatureFlag: FlagSettings = {
key: "analytics",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags"
import { FlagSettings } from "@medusajs/framework"
const OrderEditingFeatureFlag: FlagSettings = {
key: "order_editing",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags"
import { FlagSettings } from "@medusajs/framework"
const ProductCategoryFeatureFlag: FlagSettings = {
key: "product_categories",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags"
import { FlagSettings } from "@medusajs/framework"
const PublishableAPIKeysFeatureFlag: FlagSettings = {
key: "publishable_api_keys",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags"
import { FlagSettings } from "@medusajs/framework"
const SalesChannelFeatureFlag: FlagSettings = {
key: "sales_channels",
@@ -1,4 +1,4 @@
import { FlagSettings } from "../../types/feature-flags"
import { FlagSettings } from "@medusajs/framework"
const TaxInclusivePricingFeatureFlag: FlagSettings = {
key: "tax_inclusive_pricing",
@@ -1,20 +0,0 @@
import { ConfigModule } from "@medusajs/types"
export const customersGlobalMiddlewareMock = jest.fn()
export const customersCreateMiddlewareMock = jest.fn()
export const storeGlobalMiddlewareMock = jest.fn()
export const config: ConfigModule = {
projectConfig: {
databaseLogging: false,
http: {
authCors: "http://localhost:9000",
storeCors: "http://localhost:8000",
adminCors: "http://localhost:7001",
jwtSecret: "supersecret",
cookieSecret: "superSecret",
},
},
featureFlags: {},
plugins: [],
}
@@ -1,9 +0,0 @@
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")
}
@@ -1,39 +0,0 @@
import { defineMiddlewares } from "../../../../../utils/define-middlewares"
export default defineMiddlewares({
errorHandler: (err, _req, res, _next) => {
const { code, message } = err
switch (code) {
case "NOT_ALLOWED":
res.status(405).json({
type: code.toLowerCase(),
message,
})
break
case "INVALID_DATA":
res.status(400).json({
type: code.toLowerCase(),
message,
})
break
case "CONFLICT":
res.status(409).json({
type: code.toLowerCase(),
message,
})
break
case "TEAPOT":
res.status(418).json({
type: code.toLowerCase(),
message,
})
break
default:
res.status(500).json({
type: "unknown_error",
message: "An unknown error occurred.",
})
}
},
})
@@ -1,27 +0,0 @@
export const GET = async (req: Request, res: Response) => {
throw {
code: "NOT_ALLOWED",
message: "Not allowed to perform this action",
}
}
export const POST = async (req: Request, res: Response) => {
throw {
code: "INVALID_DATA",
message: "Invalid data provided",
}
}
export const PUT = async (req: Request, res: Response) => {
throw {
code: "CONFLICT",
message: "Conflict with another request",
}
}
export const DELETE = async (req: Request, res: Response) => {
throw {
code: "TEAPOT",
message: "I'm a teapot",
}
}
@@ -1,5 +0,0 @@
import { Request, Response } from "express"
export const GET = (req: Request, res: Response) => {
res.send(`GET /admin/protected`)
}
@@ -1,7 +0,0 @@
import { Request, Response } from "express"
export const AUTHENTICATE = false
export const GET = (req: Request, res: Response) => {
res.send(`GET /admin/unprotected`)
}
@@ -1,6 +0,0 @@
import { Request, Response } from "express"
import { MedusaError } from "@medusajs/utils"
export const GET = async (req: Request, res: Response) => {
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not allowed")
}
@@ -1,9 +0,0 @@
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")
}
@@ -1,52 +0,0 @@
import { NextFunction, Request, Response, raw } from "express"
import {
customersCreateMiddlewareMock,
customersGlobalMiddlewareMock,
storeGlobalMiddlewareMock,
} from "../mocks"
import { defineMiddlewares } from "../../../../../utils/define-middlewares"
const customersGlobalMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
customersGlobalMiddlewareMock()
next()
}
const customersCreateMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
customersCreateMiddlewareMock()
next()
}
const storeGlobal = (req: Request, res: Response, next: NextFunction) => {
storeGlobalMiddlewareMock()
next()
}
export default defineMiddlewares([
{
matcher: "/customers",
middlewares: [customersGlobalMiddleware],
},
{
method: "POST",
matcher: "/customers",
middlewares: [customersCreateMiddleware],
},
{
matcher: "/store/*",
middlewares: [storeGlobal],
},
{
matcher: "/webhooks/*",
method: "POST",
bodyParser: false,
middlewares: [raw({ type: "application/json" })],
},
])
@@ -1,5 +0,0 @@
import { Request, Response } from "express"
export const GET = async (req: Request, res: Response) => {
res.send(`GET /store/protected`)
}
@@ -1,7 +0,0 @@
import { Request, Response } from "express"
export const AUTHENTICATE = false
export const GET = async (req: Request, res: Response) => {
res.send(`GET /store/unprotected`)
}
@@ -1,5 +0,0 @@
import { Request, Response } from "express"
export const POST = (req: Request, res: Response) => {
res.send(`sync product ${req.params.id}`)
}
@@ -1,9 +0,0 @@
import { Request, Response } from "express"
export const POST = (req: Request, res: Response) => {
if (!(req.body instanceof Buffer)) {
res.status(400).send("Invalid body")
}
res.send("OK")
}
@@ -1,5 +0,0 @@
import { Request, Response } from "express"
export const GET = async (req: Request, res: Response): Promise<void> => {
res.send(`GET private route`)
}
@@ -1,17 +0,0 @@
import { Request, Response } from "express"
export async function GET(req: Request, res: Response): Promise<void> {
try {
res.send(`GET order ${req.params.id}`)
} catch (err) {
res.status(400).send(err)
}
}
export async function POST(req: Request, res: Response): Promise<void> {
try {
res.send(`POST order ${req.params.id}`)
} catch (err) {
res.status(400).send(err)
}
}
@@ -1,5 +0,0 @@
import { Request, Response } from "express"
export function GET(req: Request, res: Response) {
res.send("hello world")
}
@@ -1,5 +0,0 @@
import { Request, Response } from "express"
export async function GET(req: Request, res: Response): Promise<void> {
console.log("hello world")
}
@@ -1,29 +0,0 @@
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")
}
@@ -1,9 +0,0 @@
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")
}
@@ -1,7 +0,0 @@
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))
}
@@ -1,5 +0,0 @@
import { NextFunction, Request, Response } from "express"
export function GET(req: Request, res: Response) {
res.send("list customers")
}
@@ -1,196 +0,0 @@
import {
moduleLoader,
ModulesDefinition,
registerMedusaModule,
} from "@medusajs/modules-sdk"
import {
configManager,
ConfigModule,
container,
featureFlagsLoader,
logger,
} from "@medusajs/framework"
import { ContainerRegistrationKeys, generateJwtToken } from "@medusajs/utils"
import { asValue } from "awilix"
import express from "express"
import querystring from "querystring"
import supertest from "supertest"
import apiLoader from "../../../../api"
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
import RoutesLoader from "../.."
import { config } from "../mocks"
import { MedusaContainer } from "@medusajs/types"
function asArray(resolvers) {
return {
resolve: (container) =>
resolvers.map((resolver) => container.build(resolver)),
}
}
/**
* Sets up a test server that injects API Routes using the RoutesLoader
*
* @param {String} rootDir - The root directory of the project
*/
export const createServer = async (rootDir) => {
const app = express()
const moduleResolutions = {}
Object.entries(ModulesDefinition).forEach(([moduleKey, module]) => {
moduleResolutions[moduleKey] = registerMedusaModule(
moduleKey,
module.defaultModuleDeclaration,
undefined,
module
)[moduleKey]
})
configManager.loadConfig({
projectConfig: config as ConfigModule,
baseDir: rootDir,
})
container.registerAdd = function (this: MedusaContainer, name, registration) {
const storeKey = name + "_STORE"
if (this.registrations[storeKey] === undefined) {
this.register(storeKey, asValue([]))
}
const store = this.resolve(storeKey) as Array<any>
if (this.registrations[name] === undefined) {
this.register(name, asArray(store))
}
store.unshift(registration)
return this
}.bind(container)
container.register(ContainerRegistrationKeys.PG_CONNECTION, asValue({}))
container.register("configModule", asValue(config))
container.register({
logger: asValue({
error: () => {},
}),
manager: asValue({}),
})
app.set("trust proxy", 1)
app.use((req, _res, next) => {
req["session"] = {}
const data = req.get("Cookie")
if (data) {
req["session"] = {
...req["session"],
...JSON.parse(data),
}
}
next()
})
const plugins = getResolvedPlugins(rootDir, config) || []
await featureFlagsLoader()
await moduleLoader({ container, moduleResolutions, logger })
app.use((req, res, next) => {
req.scope = container.createScope() as MedusaContainer
next()
})
// This where plugins normally load, but we simply load the routes
await new RoutesLoader({
app,
rootDir,
configModule: config,
}).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)
return {
request: async (method, url, opts: any = {}) => {
const { payload, query, headers = {} } = opts
const queryParams = query && querystring.stringify(query)
const req = superRequest[method.toLowerCase()](
`${url}${queryParams ? "?" + queryParams : ""}`
)
headers.Cookie = headers.Cookie || ""
if (opts.adminSession) {
const token = generateJwtToken(
{
actor_id: opts.adminSession.userId || opts.adminSession.jwt?.userId,
actor_type: "user",
app_metadata: {
user_id:
opts.adminSession.userId || opts.adminSession.jwt?.userId,
},
},
{
secret: config.projectConfig.http.jwtSecret!,
expiresIn: "1d",
}
)
headers.Authorization = `Bearer ${token}`
}
if (opts.clientSession) {
const token = generateJwtToken(
{
actor_id:
opts.clientSession.customer_id ||
opts.clientSession.jwt?.customer_id,
actor_type: "customer",
app_metadata: {
customer_id:
opts.clientSession.customer_id ||
opts.clientSession.jwt?.customer_id,
},
},
{ secret: config.projectConfig.http.jwtSecret!, expiresIn: "1d" }
)
headers.Authorization = `Bearer ${token}`
}
for (const name in headers) {
if ({}.hasOwnProperty.call(headers, name)) {
req.set(name, headers[name])
}
}
if (payload && !req.get("content-type")) {
req.set("Content-Type", "application/json")
}
if (!req.get("accept")) {
req.set("Accept", "application/json")
}
req.set("Host", "localhost")
let res
try {
res = await req.send(JSON.stringify(payload))
} catch (e) {
if (e.response) {
res = e.response
} else {
throw e
}
}
return res
},
}
}
@@ -1,245 +0,0 @@
import express from "express"
import { resolve } from "path"
import {
config,
customersCreateMiddlewareMock,
customersGlobalMiddlewareMock,
storeGlobalMiddlewareMock,
} from "../__fixtures__/mocks"
import { createServer } from "../__fixtures__/server"
import { RoutesLoader } from "../index"
jest.setTimeout(30000)
describe("RoutesLoader", function () {
afterEach(function () {
jest.clearAllMocks()
})
describe("Routes", function () {
let request
beforeAll(async function () {
const rootDir = resolve(__dirname, "../__fixtures__/routers")
const { request: request_ } = await createServer(rootDir)
request = request_
})
it("should return a status 200 on GET admin/order/:id", async function () {
const res = await request("GET", "/admin/orders/1000", {
adminSession: {
jwt: {
userId: "admin_user",
},
},
})
expect(res.status).toBe(200)
expect(res.text).toBe("GET order 1000")
})
it("should return a status 200 on POST admin/order/:id", async function () {
const res = await request("POST", "/admin/orders/1000", {
adminSession: {
jwt: {
userId: "admin_user",
},
},
})
expect(res.status).toBe(200)
expect(res.text).toBe("POST order 1000")
})
it("should call GET /customers/[customer_id]/orders/[order_id]", async function () {
const res = await request("GET", "/customers/test-customer/orders/test")
expect(res.status).toBe(200)
expect(res.text).toBe(
'list customers {"customer_id":"test-customer","order_id":"test"}'
)
})
it("should not be able to GET /_private as the folder is prefixed with an underscore", async function () {
const res = await request("GET", "/_private")
expect(res.status).toBe(404)
expect(res.text).toContain("Cannot GET /_private")
})
})
describe("Middlewares", function () {
let request
beforeAll(async function () {
const rootDir = resolve(__dirname, "../__fixtures__/routers-middleware")
const { request: request_ } = await createServer(rootDir)
request = request_
})
it("should call middleware applied to `/customers`", async function () {
const res = await request("GET", "/customers")
expect(res.status).toBe(200)
expect(res.text).toBe("list customers")
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
})
it("should not call middleware applied to POST `/customers` when GET `/customers`", async function () {
const res = await request("GET", "/customers")
expect(res.status).toBe(200)
expect(res.text).toBe("list customers")
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
})
it("should call middleware applied to POST `/customers` when POST `/customers`", async function () {
const res = await request("POST", "/customers")
expect(res.status).toBe(200)
expect(res.text).toBe("create customer")
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
expect(customersCreateMiddlewareMock).toHaveBeenCalled()
})
it("should call store global middleware on `/store/*` routes", async function () {
const res = await request("POST", "/store/products/1000/sync")
expect(res.status).toBe(200)
expect(res.text).toBe("sync product 1000")
expect(storeGlobalMiddlewareMock).toHaveBeenCalled()
expect(customersGlobalMiddlewareMock).not.toHaveBeenCalled()
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
})
it("should apply raw middleware on POST `/webhooks/payment` route", async function () {
const res = await request("POST", "/webhooks/payment", {
payload: { test: "test" },
})
expect(res.status).toBe(200)
expect(res.text).toBe("OK")
})
it("should return 200 when admin is authenticated", async () => {
const res = await request("GET", "/admin/protected", {
adminSession: {
jwt: {
userId: "admin_user",
},
},
})
expect(res.status).toBe(200)
expect(res.text).toBe("GET /admin/protected")
})
it.skip("should return 401 when admin is not authenticated", async () => {
const res = await request("GET", "/admin/protected")
expect(res.status).toBe(401)
expect(res.text).toBe("Unauthorized")
})
it("should return 200 when admin route is opted out of authentication", async () => {
const res = await request("GET", "/admin/unprotected")
expect(res.status).toBe(200)
expect(res.text).toBe("GET /admin/unprotected")
})
it("should return the error as JSON when an error is thrown with default error handling", async () => {
const res = await request("GET", "/customers/error")
expect(res.status).toBe(400)
expect(res.body).toEqual({
message: "Not allowed",
type: "not_allowed",
})
})
})
describe("Custom error handling", function () {
let request
beforeAll(async function () {
const rootDir = resolve(
__dirname,
"../__fixtures__/routers-error-handler"
)
const { request: request_ } = await createServer(rootDir)
request = request_
})
it("should return 405 when NOT_ALLOWED error is thrown", async () => {
const res = await request("GET", "/store")
expect(res.status).toBe(405)
expect(res.body).toEqual({
message: "Not allowed to perform this action",
type: "not_allowed",
})
})
it("should return 400 when INVALID_DATA error is thrown", async () => {
const res = await request("POST", "/store")
expect(res.status).toBe(400)
expect(res.body).toEqual({
message: "Invalid data provided",
type: "invalid_data",
})
})
it("should return 409 when CONFLICT error is thrown", async () => {
const res = await request("PUT", "/store")
expect(res.status).toBe(409)
expect(res.body).toEqual({
message: "Conflict with another request",
type: "conflict",
})
})
it("should return 418 when TEAPOT error is thrown", async () => {
const res = await request("DELETE", "/store")
expect(res.status).toBe(418)
expect(res.body).toEqual({
message: "I'm a teapot",
type: "teapot",
})
})
})
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: config,
})
.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."
)
})
})
})
@@ -1,829 +0,0 @@
import { ConfigModule } from "@medusajs/types"
import { parseCorsOrigins, promiseAll, wrapHandler } from "@medusajs/utils"
import cors from "cors"
import { type Express, json, Router, text, urlencoded } from "express"
import { readdir } from "fs/promises"
import { extname, join, sep } from "path"
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
import { authenticate, errorHandler } from "../../../utils/middlewares"
import { logger } from "@medusajs/framework"
import {
AsyncRouteHandler,
GlobalMiddlewareDescriptor,
HTTP_METHODS,
MiddlewareRoute,
MiddlewaresConfig,
MiddlewareVerb,
ParserConfigArgs,
RouteConfig,
RouteDescriptor,
RouteVerb,
} from "./types"
const log = ({
activityId,
message,
}: {
activityId?: string
message: string
}) => {
if (activityId) {
logger.progress(activityId, message)
return
}
logger.debug(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
}
function matchMethod(
method: RouteVerb,
configMethod: MiddlewareRoute["method"]
): boolean {
if (!configMethod || configMethod === "USE" || configMethod === "ALL") {
return true
} else if (Array.isArray(configMethod)) {
return (
configMethod.includes(method) ||
configMethod.includes("ALL") ||
configMethod.includes("USE")
)
} else {
return method === configMethod
}
}
/**
* Function that looks though the global middlewares and returns the first
* complete match for the given path and method.
*
* @param path - The path to match
* @param method - The method to match
* @param routes - The routes to match against
* @returns The first complete match or undefined if no match is found
*/
function findMatch(
path: string,
method: RouteVerb,
routes: MiddlewareRoute[]
): MiddlewareRoute | undefined {
for (const route of routes) {
const { matcher, method: configMethod } = route
if (matchMethod(method, configMethod)) {
let isMatch = false
if (typeof matcher === "string") {
// Convert wildcard expressions to proper regex for matching entire path
// The '.*' will match any character sequence including '/'
const regex = new RegExp(`^${matcher.split("*").join(".*")}$`)
isMatch = regex.test(path)
} else if (matcher instanceof RegExp) {
// Ensure that the regex matches the entire path
const match = path.match(matcher)
isMatch = match !== null && match[0] === path
}
if (isMatch) {
return route // Return the first complete match
}
}
}
return undefined // Return undefined if no complete match is found
}
/**
* Returns an array of body parser middlewares that are applied on routes
* out-of-the-box.
*/
function getBodyParserMiddleware(args?: ParserConfigArgs) {
const sizeLimit = args?.sizeLimit
const preserveRawBody = args?.preserveRawBody
return [
json({
limit: sizeLimit,
verify: preserveRawBody
? (req: MedusaRequest, res: MedusaResponse, buf: Buffer) => {
req.rawBody = buf
}
: undefined,
}),
text({ limit: sizeLimit }),
urlencoded({ limit: sizeLimit, extended: true }),
]
}
export class RoutesLoader {
protected routesMap = new Map<string, RouteDescriptor>()
protected globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
protected app: Express
protected router: Router
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.router = Router()
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 && !config?.errorHandler) {
log({
activityId: this.activityId,
message: `Empty middleware config. 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]/route.ts => "/admin/orders/:id/route.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 promiseAll(
[...this.routesMap.values()].map(async (descriptor: RouteDescriptor) => {
const absolutePath = descriptor.absolutePath
const route = descriptor.route
return await import(absolutePath).then((import_) => {
const map = this.routesMap
const config: RouteConfig = {
routes: [],
}
/**
* If the developer has not exported the
* AUTHENTICATE flag we default to true.
*/
const shouldRequireAuth =
import_[AUTHTHENTICATE] !== undefined
? (import_[AUTHTHENTICATE] as boolean)
: true
config.optedOutOfAuth = !shouldRequireAuth
/**
* If the developer has not exported the
* CORS flag we default to true.
*/
const shouldAddCors =
import_["CORS"] !== undefined ? (import_["CORS"] as boolean) : true
if (route.startsWith("/admin")) {
config.routeType = "admin"
if (shouldAddCors) {
config.shouldAppendAdminCors = true
}
}
if (route.startsWith("/store")) {
config.routeType = "store"
if (shouldAddCors) {
config.shouldAppendStoreCors = true
}
}
if (route.startsWith("/auth") && shouldAddCors) {
config.routeType = "auth"
if (shouldAddCors) {
config.shouldAppendAuthCors = 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(sep)
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_.default 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 promiseAll(
await readdir(dirPath, { withFileTypes: true }).then((entries) => {
return entries
.filter((entry) => {
if (
this.excludes.length &&
this.excludes.some((exclude) => exclude.test(entry.name))
) {
return false
}
let name = entry.name
const extension = extname(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 await this.createRoutesMap({
dirPath: childPath,
parentPath: parentPath ?? dirPath,
})
}
return this.createRoutesDescriptor({
childPath,
parentPath,
})
})
.flat(Infinity)
})
)
}
/**
* Apply the most specific body parser middleware to the router
*/
applyBodyParserMiddleware(path: string, method: RouteVerb): void {
const middlewareDescriptor = this.globalMiddlewaresDescriptor
const mostSpecificConfig = findMatch(
path,
method,
middlewareDescriptor?.config?.routes ?? []
)
if (!mostSpecificConfig || mostSpecificConfig?.bodyParser === undefined) {
this.router[method.toLowerCase()](path, ...getBodyParserMiddleware())
return
}
if (mostSpecificConfig?.bodyParser) {
this.router[method.toLowerCase()](
path,
...getBodyParserMiddleware(mostSpecificConfig?.bodyParser)
)
return
}
return
}
/**
* Apply the route specific middlewares to the router,
* this includes the cors, authentication and
* body parsing. These are applied first to ensure
* that they are applied before any other middleware.
*/
applyRouteSpecificMiddlewares(): void {
const prioritizedRoutes = prioritize([...this.routesMap.values()])
for (const descriptor of prioritizedRoutes) {
if (!descriptor.config?.routes?.length) {
continue
}
const config = descriptor.config
const routes = descriptor.config.routes
/**
* Apply default store and admin middlewares if
* not opted out of.
*/
if (config.shouldAppendAdminCors) {
/**
* Apply the admin cors
*/
this.router.use(
descriptor.route,
cors({
origin: parseCorsOrigins(
this.configModule.projectConfig.http.adminCors
),
credentials: true,
})
)
}
if (config.shouldAppendAuthCors) {
/**
* Apply the auth cors
*/
this.router.use(
descriptor.route,
cors({
origin: parseCorsOrigins(
this.configModule.projectConfig.http.authCors
),
credentials: true,
})
)
}
if (config.shouldAppendStoreCors) {
/**
* Apply the store cors
*/
this.router.use(
descriptor.route,
cors({
origin: parseCorsOrigins(
this.configModule.projectConfig.http.storeCors
),
credentials: true,
})
)
}
// 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") {
this.router.use(
descriptor.route,
authenticate("customer", ["bearer", "session"], {
allowUnauthenticated: true,
})
)
}
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.
this.router.use(
descriptor.route,
authenticate("user", ["bearer", "session", "api-key"])
)
}
for (const route of routes) {
/**
* Apply the body parser middleware if the route
* has not opted out of it.
*/
this.applyBodyParserMiddleware(descriptor.route, route.method!)
}
}
}
/**
* Apply the error handler middleware to the router
*/
applyErrorHandlerMiddleware(): void {
const middlewareDescriptor = this.globalMiddlewaresDescriptor
const errorHandlerFn = middlewareDescriptor?.config?.errorHandler
/**
* If the user has opted out of the error handler then return
*/
if (errorHandlerFn === false) {
return
}
/**
* If the user has provided a custom error handler then use it
*/
if (errorHandlerFn) {
this.router.use(errorHandlerFn)
return
}
/**
* If the user has not provided a custom error handler then use the
* default one.
*/
this.router.use(errorHandler())
}
protected async registerRoutes(): Promise<void> {
const middlewareDescriptor = this.globalMiddlewaresDescriptor
const shouldWrapHandler = middlewareDescriptor?.config
? middlewareDescriptor.config.errorHandler !== false
: true
const prioritizedRoutes = prioritize([...this.routesMap.values()])
for (const descriptor of prioritizedRoutes) {
if (!descriptor.config?.routes?.length) {
continue
}
const routes = descriptor.config.routes
for (const route of routes) {
log({
activityId: this.activityId,
message: `Registering route [${route.method?.toUpperCase()}] - ${
descriptor.route
}`,
})
/**
* If the user hasn't opted out of error handling then
* we wrap the handler in a try/catch block.
*/
const handler = shouldWrapHandler
? wrapHandler(route.handler as AsyncRouteHandler)
: route.handler
this.router[route.method!.toLowerCase()](descriptor.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
/**
* We don't prioritize the middlewares to preserve the order
* in which they are defined in the 'middlewares.ts'. This is to
* maintain the same behavior as how middleware is applied
* in Express.
*/
for (const route of routes) {
if (!route.middlewares || !route.middlewares.length) {
continue
}
const methods = (
Array.isArray(route.method) ? route.method : [route.method]
).filter(Boolean) as MiddlewareVerb[]
for (const method of methods) {
log({
activityId: this.activityId,
message: `Registering middleware [${method}] - ${route.matcher}`,
})
this.router[method.toLowerCase()](route.matcher, ...route.middlewares)
}
}
}
async load() {
performance && performance.mark("file-base-routing-start" + this.rootDir)
let apiExists = true
/**
* Since the file based routing does not require a index file
* we can't 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.applyRouteSpecificMiddlewares()
await this.registerMiddlewares()
await this.registerRoutes()
this.applyErrorHandlerMiddleware()
/**
* Apply the router to the app.
*
* This prevents middleware from a plugin from
* bleeding into the global middleware stack.
*/
this.app.use("/", this.router)
}
performance && performance.mark("file-base-routing-end" + this.rootDir)
const timeSpent =
performance &&
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
@@ -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,
MiddlewareRoute,
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 { 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>
extends Request<core.ParamsDictionary, any, Body> {
validatedBody: Body
@@ -1,67 +1,8 @@
import { ZodObject } from "zod"
import {
MedusaRequest,
MedusaResponse,
MedusaNextFunction,
MedusaRequestHandler,
} from "../types/routing"
import {
ParserConfig,
MiddlewareVerb,
MiddlewaresConfig,
} from "../loaders/helpers/routing/types"
import { defineMiddlewares as originalDefineMiddlewares } from "@medusajs/framework"
/**
* 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 || []),
}
}),
}
}
export const defineMiddlewares = originalDefineMiddlewares
@@ -1,56 +1,3 @@
import { MedusaError } from "@medusajs/utils"
import { formatException as originalFormatException } from "@medusajs/framework"
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
}
}
export const formatException = originalFormatException
@@ -1,203 +1,3 @@
import { ApiKeyDTO, ConfigModule, IApiKeyModuleService } from "@medusajs/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
} from "@medusajs/utils"
import { NextFunction, RequestHandler } from "express"
import jwt, { JwtPayload } from "jsonwebtoken"
import {
AuthContext,
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
} from "../../types/routing"
import { authenticate as originalAuthenticate } from "@medusajs/framework"
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 => {
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
}
export const authenticate = originalAuthenticate
@@ -1,99 +1,3 @@
import { NextFunction, Request, Response } from "express"
import { errorHandler as originalErrorHandler } from "@medusajs/framework"
import { MedusaError } from "@medusajs/utils"
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]
*/
export const errorHandler = originalErrorHandler
@@ -1,2 +1,2 @@
export { authenticate } from "./authenticate-middleware"
export { default as errorHandler } from "./error-handler"
export { errorHandler } from "./error-handler"