Skip to content

Commit

Permalink
fix(medusa): Clean response data usage for admin and store fields/exp…
Browse files Browse the repository at this point in the history
…and (#3323)

* fix(medusa): Clean response data usage for admin and store fields/expand

* cleanup

* Create mighty-ads-fold.md

* fix integration

* fix integration

* refactor transform query and cleanup

* fix missing re naming

* Update packages/medusa/src/api/middlewares/transform-query.ts

---------

Co-authored-by: Oliver Windall Juhl <[email protected]>
  • Loading branch information
adrien2p and olivermrbl authored Feb 28, 2023
1 parent 0a02b70 commit cbbf3ca
Show file tree
Hide file tree
Showing 22 changed files with 286 additions and 95 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-ads-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

fix(medusa): Clean response data usage for admin and store fields/expand
10 changes: 10 additions & 0 deletions integration-tests/api/__tests__/admin/order/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,16 @@ describe("/admin/orders", () => {
id: "test-billing-address",
first_name: "lebron",
}),
shipping_total: expect.any(Number),
discount_total: expect.any(Number),
tax_total: expect.any(Number),
refunded_total: expect.any(Number),
total: expect.any(Number),
subtotal: expect.any(Number),
paid_total: expect.any(Number),
refundable_amount: expect.any(Number),
gift_card_total: expect.any(Number),
gift_card_tax_total: expect.any(Number),
},
])
)
Expand Down
33 changes: 33 additions & 0 deletions integration-tests/api/__tests__/store/orders.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,17 @@ describe("/store/carts", () => {
"customer",
"payments",
"region",
// default
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
])
})

Expand All @@ -197,6 +208,17 @@ describe("/store/carts", () => {
"customer",
"payments",
"region",
// default
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
])
})

Expand All @@ -212,6 +234,17 @@ describe("/store/carts", () => {
"status",
// selected relations
"billing_address",
// default
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
])
})

Expand Down
2 changes: 1 addition & 1 deletion packages/medusa/src/api/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job"
export { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount"
export { transformIncludesOptions } from "./transform-includes-options"
export { transformBody } from "./transform-body"
export { transformQuery } from "./transform-query"
export { transformQuery, transformStoreQuery } from "./transform-query"

export default {
authenticate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export function transformIncludesOptions(
expectedIncludesFields: string[] = []
) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!allowedIncludesFields.length || !req.query["fields"]) {
if (!allowedIncludesFields.length || !req.query.fields) {
return next()
}

const fields = (req.query["fields"] as string).split(",") ?? []
const fields = (req.query.fields as string).split(",") ?? []

for (const includesField of allowedIncludesFields) {
const fieldIndex = fields.indexOf(includesField as keyof Order) ?? -1
Expand All @@ -40,16 +40,16 @@ export function transformIncludesOptions(
)
}

req["includes"] = req["includes"] ?? {}
req["includes"][includesField] = true
req.includes = req.includes ?? {}
req.includes[includesField] = true
}
}

if (req.query["fields"]) {
if (req.query.fields) {
if (fields.length) {
req.query["fields"] = fields.join(",")
req.query.fields = fields.join(",")
} else {
delete req.query["fields"]
delete req.query.fields
}
}

Expand Down
187 changes: 147 additions & 40 deletions packages/medusa/src/api/middlewares/transform-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common"
import { omit } from "lodash"
import { removeUndefinedProperties } from "../../utils"

/**
* 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>,
queryConfig?: Omit<
QueryConfig<TEntity>,
"allowedRelations" | "allowedFields"
>,
config: ValidatorOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
Expand All @@ -29,51 +38,149 @@ export function transformQuery<
config
)
req.validatedQuery = validated
req.filterableFields = getFilterableFields(validated)
req.allowedProperties = getAllowedProperties(
validated,
req.includes ?? {},
queryConfig
)
attachListOrRetrieveConfig<TEntity>(req, queryConfig)

req.filterableFields = omit(validated, [
"limit",
"offset",
"expand",
"fields",
"order",
])
req.filterableFields = removeUndefinedProperties(req.filterableFields)

if (
(queryConfig?.defaultFields || validated.fields) &&
(queryConfig?.defaultRelations || validated.expand)
) {
req.allowedProperties = [
...(validated.fields
? validated.fields.split(",")
: queryConfig?.allowedFields || [])!,
...(validated.expand
? validated.expand.split(",")
: queryConfig?.allowedRelations || [])!,
] as unknown as string[]
}

const includesFields = Object.keys(req["includes"] ?? {})
if (includesFields.length) {
req.allowedProperties = req.allowedProperties ?? []
req.allowedProperties.push(...includesFields)
}
next()
} catch (e) {
next(e)
}
}
}

if (queryConfig?.isList) {
req.listConfig = prepareListQuery(
validated,
queryConfig
) as FindConfig<unknown>
} else {
req.retrieveConfig = prepareRetrieveQuery(
validated,
queryConfig
) as FindConfig<unknown>
}
/**
* Middleware that transform the query input for the store endpoints
* @param plainToClass
* @param queryConfig
* @param config
*/
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 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)
req.allowedProperties = getStoreAllowedProperties(
validated,
req.includes ?? {},
queryConfig
)
attachListOrRetrieveConfig<TEntity>(req, queryConfig)

next()
} catch (e) {
next(e)
}
}
}

/**
* 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",
"expand",
"fields",
"order",
]) as T
return removeUndefinedProperties(result)
}

/**
* build and attach the `retrieveConfig` or `listConfig`
* @param req
* @param queryConfig
*/
function attachListOrRetrieveConfig<TEntity extends BaseEntity>(
req: Request,
queryConfig?: QueryConfig<TEntity>
) {
const validated = req.validatedQuery
if (queryConfig?.isList) {
req.listConfig = prepareListQuery(
validated,
queryConfig
) as FindConfig<unknown>
} else {
req.retrieveConfig = prepareRetrieveQuery(
validated,
queryConfig
) as FindConfig<unknown>
}
}
/**
* Build the store allowed props based on the custom fields query params, the defaults and the includes options.
* This can be used later with `cleanResponseData` in order to clean up the returned objects to the client.
* @param queryConfig
* @param validated
* @param includesOptions
*/
function getStoreAllowedProperties<TEntity extends BaseEntity>(
validated: RequestQueryFields,
includesOptions: Record<string, boolean>,
queryConfig?: QueryConfig<TEntity>
): string[] {
const allowed: string[] = []
allowed.push(
...(validated.fields
? validated.fields.split(",")
: queryConfig?.allowedFields || []),
...(validated.expand
? validated.expand.split(",")
: queryConfig?.allowedRelations || [])
)

const includeKeys = Object.keys(includesOptions)
if (includeKeys) {
allowed.push(...includeKeys)
}

return allowed
}

/**
* Build the admin allowed props based on the custom fields query params, the defaults and the includes options.
* Since admin can access everything, it is only in order to return what the user asked for through fields and expand query params.
* This can be used later with `cleanResponseData` in order to clean up the returned objects to the client.
* @param queryConfig
* @param validated
* @param includesOptions
*/
function getAllowedProperties<TEntity extends BaseEntity>(
validated: RequestQueryFields,
includesOptions: Record<string, boolean>,
queryConfig?: QueryConfig<TEntity>
): string[] {
const allowed: string[] = []
allowed.push(
...(validated.fields?.split(",") ?? []),
...(validated.expand?.split(",") ?? [])
)

const includeKeys = Object.keys(includesOptions)
if (includeKeys) {
allowed.push(...includeKeys)
}

return allowed
}
14 changes: 11 additions & 3 deletions packages/medusa/src/api/routes/admin/orders/get-order.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { OrderService } from "../../../../services"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import { Order } from "../../../../models"

/**
* @oas [get] /admin/orders/{id}
Expand Down Expand Up @@ -60,9 +62,15 @@ export default async (req, res) => {

const orderService: OrderService = req.scope.resolve("orderService")

const order = await orderService.retrieveWithTotals(id, req.retrieveConfig, {
includes: req.includes,
})
let order: Partial<Order> = await orderService.retrieveWithTotals(
id,
req.retrieveConfig,
{
includes: req.includes,
}
)

order = cleanResponseData(order, req.allowedProperties)

res.json({ order: order })
}
Expand Down
12 changes: 3 additions & 9 deletions packages/medusa/src/api/routes/admin/orders/list-orders.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { IsNumber, IsOptional, IsString } from "class-validator"

import { AdminListOrdersSelector } from "../../../../types/orders"
import { Order } from "../../../../models"
import { OrderService } from "../../../../services"
import { Type } from "class-transformer"
import { pick } from "lodash"
import { cleanResponseData } from "../../../../utils/clean-response-data"

/**
* @oas [get] /admin/orders
Expand Down Expand Up @@ -200,19 +199,14 @@ import { pick } from "lodash"
export default async (req, res) => {
const orderService: OrderService = req.scope.resolve("orderService")

const { skip, take, select, relations } = req.listConfig
const { skip, take } = req.listConfig

const [orders, count] = await orderService.listAndCount(
req.filterableFields,
req.listConfig
)

let data: Partial<Order>[] = orders

const fields = [...select, ...relations]
if (fields.length) {
data = orders.map((o) => pick(o, fields))
}
const data = cleanResponseData(orders, req.allowedProperties)

res.json({ orders: data, count, offset: skip, limit: take })
}
Expand Down
Loading

0 comments on commit cbbf3ca

Please sign in to comment.