fix(medusa): fields params usage in the storefront endpoints (#2980)
This commit is contained in:
1 parent
4339d47e1f
commit
e22a383f47
15 files changed
+299
-55
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): `fields` param in store products/orders endpoints
|
||||||
@@ -155,6 +155,58 @@ describe("/store/carts", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("lookup order response contains only fields defined with `fields` param", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(
|
||||||
|
"/store/orders?display_id=111&email=test@email.com&fields=status,object"
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(Object.keys(response.data.order)).toEqual([
|
||||||
|
// fields
|
||||||
|
"status",
|
||||||
|
"object",
|
||||||
|
// relations
|
||||||
|
"shipping_address",
|
||||||
|
"fulfillments",
|
||||||
|
"items",
|
||||||
|
"shipping_methods",
|
||||||
|
"discounts",
|
||||||
|
"customer",
|
||||||
|
"payments",
|
||||||
|
"region",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("get order response contains only fields defined with `fields` param", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get("/store/orders/order_test?fields=status,object")
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(Object.keys(response.data.order)).toEqual([
|
||||||
|
// fields
|
||||||
|
"status",
|
||||||
|
"object",
|
||||||
|
// relations
|
||||||
|
"shipping_address",
|
||||||
|
"fulfillments",
|
||||||
|
"items",
|
||||||
|
"shipping_methods",
|
||||||
|
"discounts",
|
||||||
|
"customer",
|
||||||
|
"payments",
|
||||||
|
"region",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("looks up order", async () => {
|
it("looks up order", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { initDb, useDb } = require("../../../helpers/use-db")
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
simpleProductFactory,
|
simpleProductFactory,
|
||||||
simpleProductCategoryFactory
|
simpleProductCategoryFactory,
|
||||||
} = require("../../factories")
|
} = require("../../factories")
|
||||||
|
|
||||||
const productSeeder = require("../../helpers/store-product-seeder")
|
const productSeeder = require("../../helpers/store-product-seeder")
|
||||||
@@ -194,6 +194,26 @@ describe("/store/products", () => {
|
|||||||
expect(testProduct2Index).toBe(2) // 200
|
expect(testProduct2Index).toBe(2) // 200
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("products contain only fields defined with `fields` param", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.get("/store/products?fields=handle")
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
expect(Object.keys(response.data.products[0])).toEqual([
|
||||||
|
// fields
|
||||||
|
"handle",
|
||||||
|
// relations
|
||||||
|
"variants",
|
||||||
|
"options",
|
||||||
|
"images",
|
||||||
|
"tags",
|
||||||
|
"collection",
|
||||||
|
"type",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("returns a list of ordered products by id ASC and filtered with free text search", async () => {
|
it("returns a list of ordered products by id ASC and filtered with free text search", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
@@ -455,7 +475,10 @@ describe("/store/products", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("Product Category filtering", () => {
|
describe("Product Category filtering", () => {
|
||||||
let categoryWithProduct, categoryWithoutProduct, nestedCategoryWithProduct, nested2CategoryWithProduct
|
let categoryWithProduct,
|
||||||
|
categoryWithoutProduct,
|
||||||
|
nestedCategoryWithProduct,
|
||||||
|
nested2CategoryWithProduct
|
||||||
const nestedCategoryWithProductId = "nested-category-with-product-id"
|
const nestedCategoryWithProductId = "nested-category-with-product-id"
|
||||||
const nested2CategoryWithProductId = "nested2-category-with-product-id"
|
const nested2CategoryWithProductId = "nested2-category-with-product-id"
|
||||||
const categoryWithProductId = "category-with-product-id"
|
const categoryWithProductId = "category-with-product-id"
|
||||||
@@ -463,14 +486,11 @@ describe("/store/products", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const manager = dbConnection.manager
|
const manager = dbConnection.manager
|
||||||
categoryWithProduct = await simpleProductCategoryFactory(
|
categoryWithProduct = await simpleProductCategoryFactory(dbConnection, {
|
||||||
dbConnection,
|
|
||||||
{
|
|
||||||
id: categoryWithProductId,
|
id: categoryWithProductId,
|
||||||
name: "category with Product",
|
name: "category with Product",
|
||||||
products: [{ id: testProductId }],
|
products: [{ id: testProductId }],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
nestedCategoryWithProduct = await simpleProductCategoryFactory(
|
nestedCategoryWithProduct = await simpleProductCategoryFactory(
|
||||||
dbConnection,
|
dbConnection,
|
||||||
@@ -504,49 +524,36 @@ describe("/store/products", () => {
|
|||||||
it("returns a list of products in product category without category children", async () => {
|
it("returns a list of products in product category without category children", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const params = `category_id[]=${categoryWithProductId}`
|
const params = `category_id[]=${categoryWithProductId}`
|
||||||
const response = await api
|
const response = await api.get(`/store/products?${params}`)
|
||||||
.get(
|
|
||||||
`/store/products?${params}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(1)
|
expect(response.data.products).toHaveLength(1)
|
||||||
expect(response.data.products).toEqual(
|
expect(response.data.products).toEqual([
|
||||||
[
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: testProductId,
|
id: testProductId,
|
||||||
}),
|
}),
|
||||||
]
|
])
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns a list of products in product category without category children explicitly set to false", async () => {
|
it("returns a list of products in product category without category children explicitly set to false", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
|
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
|
||||||
const response = await api
|
const response = await api.get(`/store/products?${params}`)
|
||||||
.get(
|
|
||||||
`/store/products?${params}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(1)
|
expect(response.data.products).toHaveLength(1)
|
||||||
expect(response.data.products).toEqual(
|
expect(response.data.products).toEqual([
|
||||||
[
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: testProductId,
|
id: testProductId,
|
||||||
}),
|
}),
|
||||||
]
|
])
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns a list of products in product category with category children", async () => {
|
it("returns a list of products in product category with category children", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const params = `category_id[]=${categoryWithProductId}&include_category_children=true`
|
const params = `category_id[]=${categoryWithProductId}&include_category_children=true`
|
||||||
const response = await api
|
const response = await api.get(`/store/products?${params}`)
|
||||||
.get(
|
|
||||||
`/store/products?${params}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(3)
|
expect(response.data.products).toHaveLength(3)
|
||||||
@@ -560,7 +567,7 @@ describe("/store/products", () => {
|
|||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: testProductFilteringId1,
|
id: testProductFilteringId1,
|
||||||
})
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -569,10 +576,7 @@ describe("/store/products", () => {
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const params = `category_id[]=${categoryWithoutProductId}&include_category_children=true`
|
const params = `category_id[]=${categoryWithoutProductId}&include_category_children=true`
|
||||||
const response = await api
|
const response = await api.get(`/store/products?${params}`)
|
||||||
.get(
|
|
||||||
`/store/products?${params}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(0)
|
expect(response.data.products).toHaveLength(0)
|
||||||
@@ -1082,5 +1086,27 @@ describe("/store/products", () => {
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("response contains only fields defined with `fields` param", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
"/store/products/test-product?fields=handle"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
expect(Object.keys(response.data.product)).toEqual([
|
||||||
|
// fields
|
||||||
|
"handle",
|
||||||
|
// relations
|
||||||
|
"variants",
|
||||||
|
"options",
|
||||||
|
"images",
|
||||||
|
"tags",
|
||||||
|
"collection",
|
||||||
|
"type",
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -39,6 +39,20 @@ export function transformQuery<
|
|||||||
])
|
])
|
||||||
req.filterableFields = removeUndefinedProperties(req.filterableFields)
|
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[]
|
||||||
|
}
|
||||||
|
|
||||||
if (queryConfig?.isList) {
|
if (queryConfig?.isList) {
|
||||||
req.listConfig = prepareListQuery(
|
req.listConfig = prepareListQuery(
|
||||||
validated,
|
validated,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import GiftCardService from "../../../../services/gift-card"
|
|||||||
* @oas [get] /gift-cards/{code}
|
* @oas [get] /gift-cards/{code}
|
||||||
* operationId: "GetGiftCardsCode"
|
* operationId: "GetGiftCardsCode"
|
||||||
* summary: "Get Gift Card by Code"
|
* summary: "Get Gift Card by Code"
|
||||||
* description: "Retrieves a Gift Card by its associated unqiue code."
|
* description: "Retrieves a Gift Card by its associated unique code."
|
||||||
* parameters:
|
* parameters:
|
||||||
* - (path) code=* {string} The unique Gift Card code.
|
* - (path) code=* {string} The unique Gift Card code.
|
||||||
* x-codegen:
|
* x-codegen:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { defaultStoreOrdersFields, defaultStoreOrdersRelations } from "./index"
|
import { defaultStoreOrdersFields, defaultStoreOrdersRelations } from "./index"
|
||||||
|
|
||||||
import { OrderService } from "../../../../services"
|
import { OrderService } from "../../../../services"
|
||||||
|
import { FindParams } from "../../../../types/common"
|
||||||
|
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /orders/{id}
|
* @oas [get] /orders/{id}
|
||||||
@@ -9,6 +11,8 @@ import { OrderService } from "../../../../services"
|
|||||||
* description: "Retrieves an Order"
|
* description: "Retrieves an Order"
|
||||||
* parameters:
|
* parameters:
|
||||||
* - (path) id=* {string} The id of the Order.
|
* - (path) id=* {string} The id of the Order.
|
||||||
|
* - (query) fields {string} (Comma separated) Which fields should be included in the result.
|
||||||
|
* - (query) expand {string} (Comma separated) Which fields should be expanded in the result.
|
||||||
* x-codegen:
|
* x-codegen:
|
||||||
* method: retrieve
|
* method: retrieve
|
||||||
* x-codeSamples:
|
* x-codeSamples:
|
||||||
@@ -54,5 +58,9 @@ export default async (req, res) => {
|
|||||||
relations: defaultStoreOrdersRelations,
|
relations: defaultStoreOrdersRelations,
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json({ order })
|
res.json({
|
||||||
|
order: cleanResponseData(order, req.allowedProperties || []),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class StoreGetOrderParams extends FindParams {}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { Order } from "../../../.."
|
import { Order } from "../../../.."
|
||||||
import middlewares, { transformBody } from "../../../middlewares"
|
import middlewares, {
|
||||||
|
transformBody,
|
||||||
|
transformQuery,
|
||||||
|
} from "../../../middlewares"
|
||||||
import requireCustomerAuthentication from "../../../middlewares/require-customer-authentication"
|
import requireCustomerAuthentication from "../../../middlewares/require-customer-authentication"
|
||||||
import { StorePostCustomersCustomerOrderClaimReq } from "./request-order"
|
import { StorePostCustomersCustomerOrderClaimReq } from "./request-order"
|
||||||
import { StorePostCustomersCustomerAcceptClaimReq } from "./confirm-order-request"
|
import { StorePostCustomersCustomerAcceptClaimReq } from "./confirm-order-request"
|
||||||
|
import { StoreGetOrderParams } from "./get-order"
|
||||||
|
import { StoreGetOrdersParams } from "./lookup-order"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -14,12 +19,31 @@ export default (app) => {
|
|||||||
/**
|
/**
|
||||||
* Lookup
|
* Lookup
|
||||||
*/
|
*/
|
||||||
route.get("/", middlewares.wrap(require("./lookup-order").default))
|
route.get(
|
||||||
|
"/",
|
||||||
|
transformQuery(StoreGetOrdersParams, {
|
||||||
|
defaultFields: defaultStoreOrdersFields,
|
||||||
|
defaultRelations: defaultStoreOrdersRelations,
|
||||||
|
allowedFields: allowedStoreOrdersFields,
|
||||||
|
allowedRelations: allowedStoreOrdersRelations,
|
||||||
|
isList: true,
|
||||||
|
}),
|
||||||
|
middlewares.wrap(require("./lookup-order").default)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve Order
|
* Retrieve Order
|
||||||
*/
|
*/
|
||||||
route.get("/:id", middlewares.wrap(require("./get-order").default))
|
route.get(
|
||||||
|
"/:id",
|
||||||
|
transformQuery(StoreGetOrderParams, {
|
||||||
|
defaultFields: defaultStoreOrdersFields,
|
||||||
|
defaultRelations: defaultStoreOrdersRelations,
|
||||||
|
allowedFields: allowedStoreOrdersFields,
|
||||||
|
allowedRelations: allowedStoreOrdersRelations,
|
||||||
|
}),
|
||||||
|
middlewares.wrap(require("./get-order").default)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve by Cart Id
|
* Retrieve by Cart Id
|
||||||
@@ -60,6 +84,11 @@ export const defaultStoreOrdersRelations = [
|
|||||||
"region",
|
"region",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const allowedStoreOrdersRelations = [
|
||||||
|
...defaultStoreOrdersRelations,
|
||||||
|
"billing_address",
|
||||||
|
]
|
||||||
|
|
||||||
export const defaultStoreOrdersFields = [
|
export const defaultStoreOrdersFields = [
|
||||||
"id",
|
"id",
|
||||||
"status",
|
"status",
|
||||||
@@ -83,6 +112,21 @@ export const defaultStoreOrdersFields = [
|
|||||||
"total",
|
"total",
|
||||||
] as (keyof Order)[]
|
] as (keyof Order)[]
|
||||||
|
|
||||||
|
export const allowedStoreOrdersFields = [
|
||||||
|
...defaultStoreOrdersFields,
|
||||||
|
"object",
|
||||||
|
"shipping_total",
|
||||||
|
"discount_total",
|
||||||
|
"tax_total",
|
||||||
|
"refunded_total",
|
||||||
|
"total",
|
||||||
|
"subtotal",
|
||||||
|
"paid_total",
|
||||||
|
"refundable_amount",
|
||||||
|
"gift_card_total",
|
||||||
|
"gift_card_tax_total",
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @schema StoreOrdersRes
|
* @schema StoreOrdersRes
|
||||||
* type: object
|
* type: object
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { defaultStoreOrdersFields, defaultStoreOrdersRelations } from "."
|
import { Type } from "class-transformer"
|
||||||
|
|
||||||
import { OrderService } from "../../../../services"
|
import { OrderService } from "../../../../services"
|
||||||
import { Type } from "class-transformer"
|
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
|
import { defaultStoreOrdersFields, defaultStoreOrdersRelations } from "."
|
||||||
|
import { FindParams } from "../../../../types/common"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /orders
|
* @oas [get] /orders
|
||||||
@@ -18,6 +20,8 @@ import { validator } from "../../../../utils/validator"
|
|||||||
* description: "Look up an order using filters."
|
* description: "Look up an order using filters."
|
||||||
* parameters:
|
* parameters:
|
||||||
* - (query) display_id=* {number} The display id given to the Order.
|
* - (query) display_id=* {number} The display id given to the Order.
|
||||||
|
* - (query) fields {string} (Comma separated) Which fields should be included in the result.
|
||||||
|
* - (query) expand {string} (Comma separated) Which fields should be expanded in the result.
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: email
|
* name: email
|
||||||
* style: form
|
* style: form
|
||||||
@@ -79,7 +83,7 @@ import { validator } from "../../../../utils/validator"
|
|||||||
* $ref: "#/components/responses/500_error"
|
* $ref: "#/components/responses/500_error"
|
||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const validated = await validator(StoreGetOrdersParams, req.query)
|
const validated = req.validatedQuery as StoreGetOrdersParams
|
||||||
|
|
||||||
const orderService: OrderService = req.scope.resolve("orderService")
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const order = orders[0]
|
const order = orders[0]
|
||||||
|
|
||||||
res.json({ order })
|
res.json({ order: cleanResponseData(order, req.allowedProperties || []) })
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShippingAddressPayload {
|
export class ShippingAddressPayload {
|
||||||
@@ -110,7 +114,7 @@ export class ShippingAddressPayload {
|
|||||||
postal_code?: string
|
postal_code?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StoreGetOrdersParams {
|
export class StoreGetOrdersParams extends FindParams {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
display_id: number
|
display_id: number
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default (app, container) => {
|
|||||||
"/:id",
|
"/:id",
|
||||||
transformQuery(GetPaymentCollectionsParams, {
|
transformQuery(GetPaymentCollectionsParams, {
|
||||||
defaultFields: defaultPaymentCollectionFields,
|
defaultFields: defaultPaymentCollectionFields,
|
||||||
defaultRelations: defaulPaymentCollectionRelations,
|
defaultRelations: defaultPaymentCollectionRelations,
|
||||||
isList: false,
|
isList: false,
|
||||||
}),
|
}),
|
||||||
middlewares.wrap(require("./get-payment-collection").default)
|
middlewares.wrap(require("./get-payment-collection").default)
|
||||||
@@ -69,7 +69,7 @@ export const defaultPaymentCollectionFields = [
|
|||||||
"metadata",
|
"metadata",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaulPaymentCollectionRelations = ["region", "payment_sessions"]
|
export const defaultPaymentCollectionRelations = ["region", "payment_sessions"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @schema StorePaymentCollectionsRes
|
* @schema StorePaymentCollectionsRes
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { defaultStoreProductsRelations } from ".."
|
import { defaultStoreProductsFields, defaultStoreProductsRelations } from ".."
|
||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
|
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ describe("GET /store/products", () => {
|
|||||||
{ status: ["published"] },
|
{ status: ["published"] },
|
||||||
{
|
{
|
||||||
relations: defaultStoreProductsRelations,
|
relations: defaultStoreProductsRelations,
|
||||||
|
select: defaultStoreProductsFields,
|
||||||
skip: 0,
|
skip: 0,
|
||||||
take: 100,
|
take: 100,
|
||||||
select: undefined,
|
|
||||||
order: {
|
order: {
|
||||||
created_at: "DESC",
|
created_at: "DESC",
|
||||||
},
|
},
|
||||||
@@ -52,12 +52,12 @@ describe("GET /store/products", () => {
|
|||||||
{ is_giftcard: true, status: ["published"] },
|
{ is_giftcard: true, status: ["published"] },
|
||||||
{
|
{
|
||||||
relations: defaultStoreProductsRelations,
|
relations: defaultStoreProductsRelations,
|
||||||
|
select: defaultStoreProductsFields,
|
||||||
skip: 0,
|
skip: 0,
|
||||||
take: 100,
|
take: 100,
|
||||||
order: {
|
order: {
|
||||||
created_at: "DESC",
|
created_at: "DESC",
|
||||||
},
|
},
|
||||||
select: undefined,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "../../../../services"
|
} from "../../../../services"
|
||||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||||
import { FlagRouter } from "../../../../utils/flag-router"
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /products/{id}
|
* @oas [get] /products/{id}
|
||||||
@@ -22,6 +22,8 @@ import { validator } from "../../../../utils/validator"
|
|||||||
* - (query) sales_channel_id {string} The sales channel used when fetching the product.
|
* - (query) sales_channel_id {string} The sales channel used when fetching the product.
|
||||||
* - (query) cart_id {string} The ID of the customer's cart.
|
* - (query) cart_id {string} The ID of the customer's cart.
|
||||||
* - (query) region_id {string} The ID of the region the customer is using. This is helpful to ensure correct prices are retrieved for a region.
|
* - (query) region_id {string} The ID of the region the customer is using. This is helpful to ensure correct prices are retrieved for a region.
|
||||||
|
* - (query) fields {string} (Comma separated) Which fields should be included in the result.
|
||||||
|
* - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result.
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: currency_code
|
* name: currency_code
|
||||||
* style: form
|
* style: form
|
||||||
@@ -72,7 +74,7 @@ import { validator } from "../../../../utils/validator"
|
|||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
|
||||||
const validated = await validator(StoreGetProductsProductParams, req.query)
|
const validated = req.validatedQuery as StoreGetProductsProductParams
|
||||||
|
|
||||||
const customer_id = req.user?.customer_id
|
const customer_id = req.user?.customer_id
|
||||||
|
|
||||||
@@ -123,11 +125,21 @@ export default async (req, res) => {
|
|||||||
sales_channel_id
|
sales_channel_id
|
||||||
)
|
)
|
||||||
|
|
||||||
res.json({ product })
|
res.json({
|
||||||
|
product: cleanResponseData(product, req.allowedProperties || []),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StoreGetProductsProductParams extends PriceSelectionParams {
|
export class StoreGetProductsProductParams extends PriceSelectionParams {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
sales_channel_id?: string
|
sales_channel_id?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
fields?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
expand?: string
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/pub
|
|||||||
import { validateProductSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-product-sales-channel-association"
|
import { validateProductSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-product-sales-channel-association"
|
||||||
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
|
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
|
||||||
import { StoreGetProductsParams } from "./list-products"
|
import { StoreGetProductsParams } from "./list-products"
|
||||||
|
import { StoreGetProductsProductParams } from "./get-product"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -29,11 +30,25 @@ export default (app, featureFlagRouter: FlagRouter) => {
|
|||||||
"/",
|
"/",
|
||||||
transformQuery(StoreGetProductsParams, {
|
transformQuery(StoreGetProductsParams, {
|
||||||
defaultRelations: defaultStoreProductsRelations,
|
defaultRelations: defaultStoreProductsRelations,
|
||||||
|
defaultFields: defaultStoreProductsFields,
|
||||||
|
allowedFields: allowedStoreProductsFields,
|
||||||
|
allowedRelations: allowedStoreProductsRelations,
|
||||||
isList: true,
|
isList: true,
|
||||||
}),
|
}),
|
||||||
middlewares.wrap(require("./list-products").default)
|
middlewares.wrap(require("./list-products").default)
|
||||||
)
|
)
|
||||||
route.get("/:id", middlewares.wrap(require("./get-product").default))
|
|
||||||
|
route.get(
|
||||||
|
"/:id",
|
||||||
|
transformQuery(StoreGetProductsProductParams, {
|
||||||
|
defaultRelations: defaultStoreProductsRelations,
|
||||||
|
defaultFields: defaultStoreProductsFields,
|
||||||
|
allowedFields: allowedStoreProductsFields,
|
||||||
|
allowedRelations: allowedStoreProductsRelations,
|
||||||
|
}),
|
||||||
|
middlewares.wrap(require("./get-product").default)
|
||||||
|
)
|
||||||
|
|
||||||
route.post("/search", middlewares.wrap(require("./search").default))
|
route.post("/search", middlewares.wrap(require("./search").default))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
@@ -51,6 +66,47 @@ export const defaultStoreProductsRelations = [
|
|||||||
"type",
|
"type",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const defaultStoreProductsFields: (keyof Product)[] = [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"status",
|
||||||
|
"external_id",
|
||||||
|
"description",
|
||||||
|
"handle",
|
||||||
|
"is_giftcard",
|
||||||
|
"discountable",
|
||||||
|
"thumbnail",
|
||||||
|
"profile_id",
|
||||||
|
"collection_id",
|
||||||
|
"type_id",
|
||||||
|
"weight",
|
||||||
|
"length",
|
||||||
|
"height",
|
||||||
|
"width",
|
||||||
|
"hs_code",
|
||||||
|
"origin_country",
|
||||||
|
"mid_code",
|
||||||
|
"material",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"deleted_at",
|
||||||
|
"metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const allowedStoreProductsFields = [
|
||||||
|
...defaultStoreProductsFields,
|
||||||
|
// TODO: order prop validation
|
||||||
|
"variants.title",
|
||||||
|
"variants.prices.amount",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const allowedStoreProductsRelations = [
|
||||||
|
...defaultStoreProductsRelations,
|
||||||
|
"variants.title",
|
||||||
|
"variants.prices.amount",
|
||||||
|
]
|
||||||
|
|
||||||
export * from "./list-products"
|
export * from "./list-products"
|
||||||
export * from "./search"
|
export * from "./search"
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
|||||||
import { IsType } from "../../../../utils/validators/is-type"
|
import { IsType } from "../../../../utils/validators/is-type"
|
||||||
import { FlagRouter } from "../../../../utils/flag-router"
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
|
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
|
||||||
|
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /products
|
* @oas [get] /products
|
||||||
@@ -137,8 +138,8 @@ import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/pub
|
|||||||
* - (query) include_category_children {boolean} Include category children when filtering by category_id.
|
* - (query) include_category_children {boolean} Include category children when filtering by category_id.
|
||||||
* - (query) offset=0 {integer} How many products to skip in the result.
|
* - (query) offset=0 {integer} How many products to skip in the result.
|
||||||
* - (query) limit=100 {integer} Limit the number of products returned.
|
* - (query) limit=100 {integer} Limit the number of products returned.
|
||||||
* - (query) expand {string} (Comma separated) Which fields should be expanded in each order of the result.
|
* - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result.
|
||||||
* - (query) fields {string} (Comma separated) Which fields should be included in each order of the result.
|
* - (query) fields {string} (Comma separated) Which fields should be included in each product of the result.
|
||||||
* - (query) order {string} the field used to order the products.
|
* - (query) order {string} the field used to order the products.
|
||||||
* - (query) cart_id {string} The id of the Cart to set prices based on.
|
* - (query) cart_id {string} The id of the Cart to set prices based on.
|
||||||
* - (query) region_id {string} The id of the Region to set prices based on.
|
* - (query) region_id {string} The id of the Region to set prices based on.
|
||||||
@@ -241,7 +242,7 @@ export default async (req, res) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
products,
|
products: cleanResponseData(products, req.allowedProperties || []),
|
||||||
count,
|
count,
|
||||||
offset: validated.offset,
|
offset: validated.offset,
|
||||||
limit: validated.limit,
|
limit: validated.limit,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ declare global {
|
|||||||
listConfig: FindConfig<unknown>
|
listConfig: FindConfig<unknown>
|
||||||
retrieveConfig: FindConfig<unknown>
|
retrieveConfig: FindConfig<unknown>
|
||||||
filterableFields: Record<string, unknown>
|
filterableFields: Record<string, unknown>
|
||||||
|
allowedProperties: string[]
|
||||||
errors: string[]
|
errors: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { pick } from "lodash"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter response data to contain props specified in the fields array.
|
||||||
|
*
|
||||||
|
* @param data - record or an array of records in the response
|
||||||
|
* @param fields - record props allowed in the response
|
||||||
|
*/
|
||||||
|
function cleanResponseData<T>(data: T, fields: string[]) {
|
||||||
|
if (!fields.length) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map((record) => pick(record, fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pick(data, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { cleanResponseData }
|
||||||
Reference in new issue
Block a user