chore(medusa): cleanup medusa package (#7206)

This commit is contained in:
Adrien de Peretti
2024-05-07 12:48:49 +02:00
committed by GitHub
parent 8b61dccd0f
commit 71f4f46cb9
1452 changed files with 4737 additions and 234780 deletions
@@ -1,40 +0,0 @@
import { IsString } from "class-validator"
import { AdminPostProductsReq as MedusaAdminPostProductsReq } from "../../api/routes/admin/products/create-product"
import { registerOverriddenValidators, validator } from "../validator"
class AdminPostProductsReq extends MedusaAdminPostProductsReq {
@IsString()
custom_attribute: string
}
describe("Validator", function () {
it("should override the original validator", async function () {
let err = await validator(MedusaAdminPostProductsReq, {
title: "test",
})
.then(() => void 0)
.catch((err) => err)
expect(err).not.toBeDefined()
registerOverriddenValidators(AdminPostProductsReq)
err = await validator(MedusaAdminPostProductsReq, {
title: "test",
})
.then(() => void 0)
.catch((err) => err)
expect(err).toBeDefined()
expect(err.message).toEqual("custom_attribute must be a string")
err = await validator(MedusaAdminPostProductsReq, {
title: "test",
custom_attribute: "test",
})
.then(() => void 0)
.catch((err) => err)
expect(err).not.toBeDefined()
})
})
-2
View File
@@ -10,8 +10,6 @@ export * from "./is-date"
export * from "./is-object"
export * from "./is-string"
export * from "./omit-deep"
export * from "./product-category"
export * from "./queries"
export * from "./remote-query-fetch-data"
export * from "./remove-undefined-properties"
export * from "./set-metadata"
@@ -0,0 +1,719 @@
import { NextFunction, Request, Response } from "express"
import { transformQuery } from "../transform-query"
import { extendedFindParamsMixin } from "../../../types/common"
import { MedusaError } from "medusa-core-utils"
describe("transformQuery", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should transform the input query", async () => {
let mockRequest = {
query: {},
} as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
const expectations = ({
offset,
limit,
inputOrder,
transformedOrder,
}: {
offset: number
limit: number
inputOrder: string | undefined
transformedOrder: Record<string, "ASC" | "DESC">
relations?: string[]
}) => {
expect(mockRequest.validatedQuery).toEqual({
offset,
limit,
order: inputOrder,
})
expect(mockRequest.filterableFields).toEqual({})
expect(mockRequest.allowedProperties).toEqual([
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
])
expect(mockRequest.listConfig).toEqual({
take: limit,
skip: offset,
select: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
relations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
order: transformedOrder,
})
expect(mockRequest.remoteQueryConfig).toEqual({
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
pagination: {
order: transformedOrder,
skip: offset,
take: limit,
},
})
}
let queryConfig: any = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expectations({
limit: 20,
offset: 0,
inputOrder: undefined,
transformedOrder: {
created_at: "DESC",
},
})
//////////////////////////////
mockRequest = {
query: {
limit: "10",
offset: "5",
order: "created_at",
},
} as unknown as Request
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expectations({
limit: 10,
offset: 5,
inputOrder: "created_at",
transformedOrder: { created_at: "ASC" },
})
//////////////////////////////
mockRequest = {
query: {
limit: "10",
offset: "5",
order: "created_at",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expectations({
limit: 10,
offset: 5,
inputOrder: "created_at",
transformedOrder: { created_at: "ASC" },
})
})
it("should transform the input query taking into account the fields symbols (+,- or no symbol)", async () => {
let mockRequest = {
query: {
fields: "id",
},
} as unknown as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
let queryConfig: any = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: ["id", "created_at"],
})
)
//////////////////////////////
mockRequest = {
query: {
fields: "+test_prop,-prop-test-something",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"prop-test-something",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"test_prop",
],
})
)
//////////////////////////////
mockRequest = {
query: {
fields: "+test_prop,-updated_at",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"test_prop",
],
})
)
})
it(`should transform the input and manage the allowed fields and relations properly without error`, async () => {
let mockRequest = {
query: {
fields: "*product.variants,+product.id",
},
} as unknown as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
let queryConfig: any = {
defaults: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
"product.variants",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product.id",
],
relations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
"product",
"product.variants",
],
})
)
expect(mockRequest.remoteQueryConfig).toEqual(
expect.objectContaining({
fields: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product.id",
"product.variants.*",
],
})
)
//////////////////////////////
mockRequest = {
query: {
fields: "store.name",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
"product.variants",
"store.name",
],
allowedRelations: ["metadata", "product"],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: ["store.name", "created_at", "id"],
relations: ["store"],
})
)
expect(mockRequest.remoteQueryConfig).toEqual(
expect.objectContaining({
fields: ["store.name", "created_at", "id"],
})
)
})
it("should throw when attempting to transform the input if disallowed fields are requested", async () => {
let mockRequest = {
query: {
fields: "+test_prop",
},
} as unknown as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
let queryConfig: any = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [test_prop] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
expand: "product",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowedRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
expand: "store",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
"product.variants",
"store.name",
],
allowedRelations: ["metadata", "product"],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [store] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
fields: "*product",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
fields: "*product.variants",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product.variants] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
fields: "product",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product] are not valid`
)
)
})
})
@@ -0,0 +1,34 @@
import { ContainerRegistrationKeys, MedusaV2Flag } from "@medusajs/utils"
import { NextFunction, Request, RequestHandler, Response } from "express"
import passport from "passport"
// Optional customer authentication
// If authenticated, middleware attaches customer to request (as user) otherwise we pass through
// If you want to require authentication, use `requireCustomerAuthentication` in `packages/medusa/src/api/middlewares/require-customer-authentication.ts`
export default (): RequestHandler => {
return (req: Request, res: Response, next: NextFunction): void => {
const featureFlagRouter = req.scope.resolve(
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER
)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
return next()
}
passport.authenticate(
["store-session", "store-bearer"],
{ session: false },
(err, user) => {
if (err) {
return next(err)
}
if (user) {
req.user = user
}
return next()
}
)(req, res, next)
}
}
@@ -0,0 +1,19 @@
import { ContainerRegistrationKeys, MedusaV2Flag } from "@medusajs/utils"
import { NextFunction, Request, RequestHandler, Response } from "express"
import passport from "passport"
export default (): RequestHandler => {
return (req: Request, res: Response, next: NextFunction): void => {
const featureFlagRouter = req.scope.resolve(
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER
)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
return next()
}
passport.authenticate(
["admin-session", "admin-bearer", "admin-api-token"],
{ session: false }
)(req, res, next)
}
}
@@ -0,0 +1,39 @@
import { NextFunction, Request, RequestHandler, Response } from "express"
type handler = (req: Request, res: Response) => Promise<void>
/**
* @deprecated use `import { wrapHandler } from "@medusajs/utils"`
*/
export default (fn: handler): RequestHandler => {
return async (req: Request, res: Response, next: NextFunction) => {
if (req?.errors?.length) {
return res.status(400).json({
errors: req.errors,
message:
"Provided request body contains errors. Please check the data and retry the request",
})
}
try {
return await fn(req, res)
} catch (err) {
next(err)
}
}
}
/**
* @schema MultipleErrors
* title: "Multiple Errors"
* type: object
* properties:
* errors:
* type: array
* description: Array of errors
* items:
* $ref: "#/components/schemas/Error"
* message:
* type: string
* default: "Provided request body contains errors. Please check the data and retry the request"
*/
@@ -0,0 +1,15 @@
import { NextFunction, Request, Response } from "express"
export function checkRegisteredModules(services: {
[serviceName: string]: string
}): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
for (const service of Object.keys(services)) {
if (!req.scope.resolve(service, { allowUnregistered: true })) {
return next(new Error(services[service]))
}
}
next()
}
}
@@ -0,0 +1,99 @@
import { NextFunction, Request, Response } from "express"
import { MedusaError } from "medusa-core-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]
*/
@@ -0,0 +1,18 @@
import { FlagRouter } from "@medusajs/utils"
import { NextFunction, Request, Response } from "express"
export function isFeatureFlagEnabled(
flagKey: string
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
const featureFlagRouter = req.scope.resolve(
"featureFlagRouter"
) as FlagRouter
if (!featureFlagRouter.isFeatureEnabled(flagKey)) {
res.sendStatus(404)
} else {
next()
}
}
}
@@ -0,0 +1,10 @@
export { default as authenticate } from "./authenticate"
export { default as authenticateCustomer } from "./authenticate-customer"
export { default as wrapHandler } from "./await-middleware"
export { default as errorHandler } from "./error-handler"
export { isFeatureFlagEnabled } from "./feature-flag-enabled"
export { default as normalizeQuery } from "./normalized-query"
export { default as requireCustomerAuthentication } from "./require-customer-authentication"
export { transformBody } from "./transform-body"
export { transformIncludesOptions } from "./transform-includes-options"
export { transformQuery, transformStoreQuery } from "./transform-query"
@@ -0,0 +1,24 @@
import { NextFunction, Request, Response } from "express"
/**
* Normalize an input query, especially from array like query params to an array type
* e.g: /admin/orders/?fields[]=id,status,cart_id becomes { fields: ["id", "status", "cart_id"] }
*/
export default (): ((
req: Request,
res: Response,
next: NextFunction
) => void) => {
return (req: Request, res: Response, next: NextFunction): void => {
req.query = Object.entries(req.query).reduce((acc, [key, val]) => {
if (Array.isArray(val) && val.length === 1) {
acc[key] = (val as string[])[0].split(",")
} else {
acc[key] = val
}
return acc
}, {})
next()
}
}
@@ -0,0 +1,16 @@
import { NextFunction, Request, RequestHandler, Response } from "express"
import passport from "passport"
export default (): RequestHandler => {
return (req: Request, res: Response, next: NextFunction): void => {
if (req.user) {
return next()
}
passport.authenticate(["store-session", "store-bearer"], { session: false })(
req,
res,
next
)
}
}
@@ -0,0 +1,20 @@
import { ValidatorOptions } from "class-validator"
import { NextFunction, Request, Response } from "express"
import { ClassConstructor } from "../../types/global"
import { validator } from "../../utils/validator"
export function transformBody<T>(
plainToClass: ClassConstructor<T>,
config: ValidatorOptions = {
forbidUnknownValues: false,
}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
try {
req.validatedBody = await validator(plainToClass, req.body, config)
next()
} catch (e) {
next(e)
}
}
}
@@ -0,0 +1,57 @@
import { NextFunction, Request, Response } from "express"
import { MedusaError } from "medusa-core-utils"
/**
* Retrieve the includes options from the fields query param.
* If the include option is present then assigned it to includes on req
* @param allowedIncludes The list of fields that can be passed and assign to req.includes
* @param expectedIncludes The list of fields that the consumer can pass to the end point using this middleware. It is a subset of `allowedIncludes`
*/
export function transformIncludesOptions(
allowedIncludes: string[] = [],
expectedIncludes: string[] = []
) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!allowedIncludes.length || !req.query.expand) {
return next()
}
const expand = (req.query.expand as string).split(",") ?? []
for (const includes of allowedIncludes) {
const fieldIndex = expand.indexOf(includes) ?? -1
const isPresent = fieldIndex !== -1
if (isPresent) {
expand.splice(fieldIndex, 1)
if (!expectedIncludes.includes(includes)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The field "${includes}" is not supported by this end point. ${
expectedIncludes.length
? `The includes fields can be one of entity properties or in [${expectedIncludes.join(
", "
)}]`
: ""
}`
)
}
req.includes = req.includes ?? {}
req.includes[includes] = true
}
}
if (req.query.expand) {
if (expand.length) {
req.query.expand = expand.join(",")
} else {
delete req.query.expand
}
}
next()
}
}
@@ -0,0 +1,139 @@
import { ValidatorOptions } from "class-validator"
import { NextFunction, Request, Response } from "express"
import { omit } from "lodash"
import { BaseEntity } from "../../interfaces"
import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common"
import { ClassConstructor } from "../../types/global"
import { removeUndefinedProperties } from "../../utils"
import {
prepareListQuery,
prepareRetrieveQuery,
} from "../../utils/get-query-config"
import { validator } from "../../utils/validator"
import { default as normalizeQuery } from "./normalized-query"
/**
* Middleware that transform the query input for the admin end points
* @param plainToClass
* @param queryConfig
* @param config
*/
export function transformQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(
plainToClass: ClassConstructor<T>,
queryConfig: QueryConfig<TEntity> = {},
config: ValidatorOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
try {
normalizeQuery()(req, res, () => void 0)
const validated: T = await validator<T, Record<string, unknown>>(
plainToClass,
req.query,
config
)
req.validatedQuery = validated
req.filterableFields = getFilterableFields(validated)
attachListOrRetrieveConfig<TEntity>(req, {
...queryConfig,
allowed:
req.allowed ?? queryConfig.allowed ?? queryConfig.allowedFields ?? [],
})
/**
* TODO: the bellow allowedProperties should probably need to be reworked which would create breaking changes everywhere
* cleanResponseData is used. It is in fact, what is expected to be returned which IMO
* should correspond to the select/relations
*
* Kept it as it is to maintain backward compatibility
*/
const queryConfigRes = !queryConfig.isList
? req.retrieveConfig
: req.listConfig
const includesRelations = Object.keys(req.includes ?? {})
req.allowedProperties = Array.from(
new Set(
[
...(req.validatedQuery.fields
? queryConfigRes.select ?? []
: req.allowed ??
queryConfig.allowed ??
queryConfig.allowedFields ??
(queryConfig.defaults as string[]) ??
queryConfig.defaultFields ??
[]),
...(req.validatedQuery.expand || includesRelations.length
? [...(validated.expand?.split(",") || []), ...includesRelations] // For backward compatibility, the includes takes precedence over the relations for the returnable fields
: queryConfig.allowedRelations ?? queryConfigRes.relations ?? []), // For backward compatibility, the allowedRelations takes precedence over the relations for the returnable fields
].filter(Boolean)
)
)
next()
} catch (e) {
next(e)
}
}
}
/**
* Middleware that transform the query input for the store endpoints
* @param plainToClass
* @param queryConfig
* @param config
*
* @deprecated use `transformQuery` instead
*/
export function transformStoreQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(
plainToClass: ClassConstructor<T>,
queryConfig?: QueryConfig<TEntity>,
config: ValidatorOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return transformQuery(plainToClass, queryConfig, config)
}
/**
* Omit the non filterable config from the validated object
* @param obj
*/
function getFilterableFields<T extends RequestQueryFields>(obj: T): T {
const result = omit(obj, [
"limit",
"offset",
/**
* @deprecated
*/
"expand",
"fields",
"order",
]) as T
return removeUndefinedProperties(result)
}
/**
* build and attach the `retrieveConfig` or `listConfig` and remoteQueryConfig to the request object
* @param req
* @param queryConfig
*/
function attachListOrRetrieveConfig<TEntity extends BaseEntity>(
req: Request,
queryConfig: QueryConfig<TEntity> = {}
) {
const validated = req.validatedQuery
const config = queryConfig.isList
? prepareListQuery(validated, queryConfig)
: prepareRetrieveQuery(validated, queryConfig)
req.listConfig = ("listConfig" in config &&
config.listConfig) as FindConfig<any>
req.retrieveConfig = ("retrieveConfig" in config &&
config.retrieveConfig) as FindConfig<any>
req.remoteQueryConfig = config.remoteQueryConfig
}
@@ -1,17 +0,0 @@
// Since typeorm require us to use ES6 and that migrating require a lot of work
// one solution is to override directly the one from typeorm so that there is no complain about
// the output build
import { DefaultNamingStrategy } from "typeorm"
DefaultNamingStrategy.prototype.eagerJoinRelationAlias = function (
alias: string,
propertyPath: string
): string {
const path = propertyPath
.split(".")
.map((p) => p.substring(0, 2))
.join("_")
const out = alias + "_" + path
const match = out.match(/_/g) || []
return out + match.length
}
@@ -1,25 +0,0 @@
import { FindOptionsWhere } from "typeorm"
import { ProductCategory } from "../../models"
import { isDefined } from "medusa-core-utils"
export const categoryMatchesScope = (
category: ProductCategory,
query: FindOptionsWhere<ProductCategory>
): boolean => {
return Object.keys(query ?? {}).every(key => category[key] === query[key])
}
export const fetchCategoryDescendantsIds = (
productCategory: ProductCategory,
query: FindOptionsWhere<ProductCategory>
) => {
let result = [productCategory.id]
;(productCategory.category_children || []).forEach((child) => {
if (categoryMatchesScope(child, query)) {
result = result.concat(fetchCategoryDescendantsIds(child, query))
}
})
return result
}
@@ -1 +0,0 @@
export * from "./products"
@@ -1,33 +0,0 @@
import { MedusaContainer, ProductVariantDTO } from "@medusajs/types"
export async function getVariantsFromPriceList(
container: MedusaContainer,
priceListId: string
) {
const remoteQuery = container.resolve("remoteQuery")
const query = {
price_list: {
__args: { id: [priceListId] },
prices: {
price_set: {
variant_link: { variant: { fields: ["id", "product_id"] } },
},
},
},
}
const priceLists = await remoteQuery(query)
const variants: ProductVariantDTO[] = []
priceLists.forEach((priceList) => {
priceList.prices?.forEach((price) => {
const variant = price.price_set?.variant_link?.variant
if (variant) {
variants.push(variant)
}
})
})
return variants
}
@@ -1,3 +0,0 @@
export * from "./get-variants-from-price-list"
export * from "./list-products"
export * from "./retrieve-product"
@@ -1,235 +0,0 @@
import { MedusaContainer } from "@medusajs/types"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { PriceListService } from "../../../services"
import { getVariantsFromPriceList } from "./get-variants-from-price-list"
export async function listProducts(
container: MedusaContainer,
filterableFields,
listConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = container.resolve("remoteQuery")
const featureFlagRouter = container.resolve("featureFlagRouter")
const productIdsFilter: Set<string> = new Set()
const variantIdsFilter: Set<string> = new Set()
const promises: Promise<void>[] = []
// This is not the best way of handling cross filtering but for now I would say it is fine
const salesChannelIdFilter = filterableFields.sales_channel_id
delete filterableFields.sales_channel_id
const priceListId = filterableFields.price_list_id
delete filterableFields.price_list_id
if (priceListId) {
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const variants = await getVariantsFromPriceList(container, priceListId)
variants.forEach((pv) => variantIdsFilter.add(pv.id))
} else {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = container.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
}
}
const discountConditionId = filterableFields.discount_condition_id
delete filterableFields.discount_condition_id
if (discountConditionId) {
// TODO implement later
}
await promiseAll(promises)
if (productIdsFilter.size > 0) {
filterableFields.id = Array.from(productIdsFilter)
}
if (variantIdsFilter.size > 0) {
filterableFields.variants = { id: Array.from(variantIdsFilter) }
}
const variables = {
filters: filterableFields,
order: listConfig.order,
skip: listConfig.skip,
take: listConfig.take,
}
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
if (salesChannelIdFilter) {
query.product["sales_channels"]["__args"] = { id: salesChannelIdFilter }
}
const {
rows: products,
metadata: { count },
} = await remoteQuery(query)
products.forEach((product) => {
product.profile_id = product.profile?.id
})
return [products, count]
}
export const defaultAdminProductRemoteQueryObject = {
fields: [
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
"discountable",
"thumbnail",
"collection_id",
"type_id",
"weight",
"length",
"height",
"width",
"hs_code",
"origin_country",
"mid_code",
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
],
images: {
fields: ["id", "created_at", "updated_at", "deleted_at", "url", "metadata"],
},
tags: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
type: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
collection: {
fields: ["title", "handle", "id", "created_at", "updated_at", "deleted_at"],
},
categories: {
fields: [
"id",
"name",
"description",
"handle",
"is_active",
"is_internal",
"parent_category_id",
],
},
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"metadata",
],
values: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
variants: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"sku",
"barcode",
"ean",
"upc",
"variant_rank",
"inventory_quantity",
"allow_backorder",
"manage_inventory",
"hs_code",
"origin_country",
"mid_code",
"material",
"weight",
"length",
"height",
"width",
"metadata",
],
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
profile: {
fields: ["id", "created_at", "updated_at", "deleted_at", "name", "type"],
},
sales_channels: {
fields: [
"id",
"name",
"description",
"is_disabled",
"created_at",
"updated_at",
"deleted_at",
"metadata",
],
},
}
@@ -1,28 +0,0 @@
import { MedusaError } from "@medusajs/utils"
export async function retrieveProduct(container, id, remoteQueryObject = {}) {
// TODO: Add support for fields/expands
const remoteQuery = container.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...remoteQueryObject,
},
}
const [product] = await remoteQuery(query)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id: ${id} not found`
)
}
product.profile_id = product.profile?.id
return product
}