Skip to content

Commit

Permalink
feat: List products middleware (#6769)
Browse files Browse the repository at this point in the history
  • Loading branch information
olivermrbl authored Mar 22, 2024
1 parent bb3cace commit 9e25e0c
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 101 deletions.
232 changes: 188 additions & 44 deletions integration-tests/api/__tests__/admin/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ const {
} = require("../../../helpers/create-admin-user")
const { breaking } = require("../../../helpers/breaking")
const { IdMap, medusaIntegrationTestRunner } = require("medusa-test-utils")
const { ModuleRegistrationName } = require("@medusajs/modules-sdk")
const { ModuleRegistrationName, Modules } = require("@medusajs/modules-sdk")
const {
createVariantPriceSet,
} = require("../../../modules/helpers/create-variant-price-set")
const { PriceListStatus, PriceListType } = require("@medusajs/types")
const { ContainerRegistrationKeys } = require("@medusajs/utils")

let productSeeder = undefined
let priceListSeeder = undefined
Expand All @@ -27,52 +32,17 @@ let {

jest.setTimeout(50000)

const productFixture = {
title: "Test fixture",
description: "test-product-description",
type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
tags: [{ value: "123" }, { value: "456" }],
options: breaking(
() => [{ title: "size" }, { title: "color" }],
() => [
{ title: "size", values: ["large"] },
{ title: "color", values: ["green"] },
]
),
variants: [
{
title: "Test variant",
inventory_quantity: 10,
prices: [
{
currency_code: "usd",
amount: 100,
},
{
currency_code: "eur",
amount: 45,
},
{
currency_code: "dkk",
amount: 30,
},
],
options: breaking(
() => [{ value: "large" }, { value: "green" }],
() => ({
size: "large",
color: "green",
})
),
},
],
}

medusaIntegrationTestRunner({
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
testSuite: ({ dbConnection, getContainer, api }) => {
let v2Product
let pricingService
let productService
let scService
let remoteLink
let container
let productFixture

beforeAll(() => {
// Note: We have to lazily load everything because there are weird ordering issues when doing `require` of `@medusajs/medusa`
productSeeder = require("../../../helpers/product-seeder")
Expand All @@ -96,9 +66,51 @@ medusaIntegrationTestRunner({
})

beforeEach(async () => {
const container = getContainer()
container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)

productFixture = {
title: "Test fixture",
description: "test-product-description",
type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
tags: [{ value: "123" }, { value: "456" }],
options: breaking(
() => [{ title: "size" }, { title: "color" }],
() => [
{ title: "size", values: ["large"] },
{ title: "color", values: ["green"] },
]
),
variants: [
{
title: "Test variant",
inventory_quantity: 10,
prices: [
{
currency_code: "usd",
amount: 100,
},
{
currency_code: "eur",
amount: 45,
},
{
currency_code: "dkk",
amount: 30,
},
],
options: breaking(
() => [{ value: "large" }, { value: "green" }],
() => ({
size: "large",
color: "green",
})
),
},
],
}

// We want to seed another product for v2 that has pricing correctly wired up for all pricing-related tests.
v2Product = (
await breaking(
Expand All @@ -107,6 +119,11 @@ medusaIntegrationTestRunner({
await api.post("/admin/products", productFixture, adminHeaders)
)
)?.data?.product

pricingService = container.resolve(ModuleRegistrationName.PRICING)
productService = container.resolve(ModuleRegistrationName.PRODUCT)
scService = container.resolve(ModuleRegistrationName.SALES_CHANNEL)
remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
})

describe("/admin/products", () => {
Expand Down Expand Up @@ -1104,6 +1121,133 @@ medusaIntegrationTestRunner({
])
)
})

it("should return products filtered by price_list_id", async () => {
const priceList = await breaking(
async () => {
return await simplePriceListFactory(dbConnection, {
prices: [
{
variant_id: "test-variant",
amount: 100,
currency_code: "usd",
},
],
})
},
async () => {
const variantId = v2Product.variants[0].id

await pricingService.createRuleTypes([
{
name: "Region ID",
rule_attribute: "region_id",
},
])

const priceSet = await createVariantPriceSet({
container,
variantId,
})

const [priceList] = await pricingService.createPriceLists([
{
title: "Test price list",
description: "Test",
status: PriceListStatus.ACTIVE,
type: PriceListType.OVERRIDE,
prices: [
{
amount: 5000,
currency_code: "usd",
price_set_id: priceSet.id,
rules: {
region_id: "test-region",
},
},
],
},
])

return priceList
}
)

const res = await api.get(
`/admin/products?price_list_id[]=${priceList.id}`,
adminHeaders
)

expect(res.status).toEqual(200)
expect(res.data.products.length).toEqual(1)
expect(res.data.products).toEqual([
expect.objectContaining({
id: breaking(
() => "test-product",
() => v2Product.id
),
status: "draft",
}),
])
})

it("should return products filtered by sales_channel_id", async () => {
const { salesChannel, product } = await breaking(
async () => {
const product = await simpleProductFactory(dbConnection, {
id: "product_1",
title: "test title",
})

await simpleProductFactory(dbConnection, {
id: "product_2",
title: "test title 2",
})

const salesChannel = await simpleSalesChannelFactory(
dbConnection,
{
name: "test name",
description: "test description",
products: [product],
}
)

return { salesChannel, product }
},
async () => {
const salesChannel = await scService.create({
name: "Test channel",
description: "Lorem Ipsum",
})

await remoteLink.create({
[Modules.PRODUCT]: {
product_id: v2Product.id,
},
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
})

return { salesChannel, product: v2Product }
}
)

const res = await api.get(
`/admin/products?sales_channel_id[]=${salesChannel.id}`,
adminHeaders
)

expect(res.status).toEqual(200)
expect(res.data.products.length).toEqual(1)
expect(res.data.products).toEqual([
expect.objectContaining({
id: product.id,
status: "draft",
}),
])
})
})

describe("GET /admin/products/:id", () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/medusa/src/api-v2/admin/products/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import * as QueryConfig from "./query-config"
import {
maybeApplyPriceListsFilter,
maybeApplySalesChannelsFilter,
} from "./utils"
import {
AdminGetProductsOptionsParams,
AdminGetProductsParams,
Expand Down Expand Up @@ -31,6 +35,8 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
AdminGetProductsParams,
QueryConfig.listProductQueryConfig
),
maybeApplySalesChannelsFilter(),
maybeApplyPriceListsFilter(),
],
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/medusa/src/api-v2/admin/products/query-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const defaultAdminProductFields = [
"*variants",
"*variants.prices",
"*variants.options",
"*sales_channels",
]

export const retrieveProductQueryConfig = {
Expand Down
50 changes: 1 addition & 49 deletions packages/medusa/src/api-v2/admin/products/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,26 @@ import { createProductsWorkflow } from "@medusajs/core-flows"
import { CreateProductDTO } from "@medusajs/types"
import {
ContainerRegistrationKeys,
isString,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { MedusaContainer } from "medusa-core-utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { listPriceLists } from "../price-lists/queries"
import { refetchProduct, remapKeysForProduct, remapProduct } from "./helpers"
import { AdminGetProductsParams } from "./validators"

const applyVariantFiltersForPriceList = async (
scope: MedusaContainer,
filterableFields: AdminGetProductsParams
) => {
const filterByPriceListIds = filterableFields.price_list_id
const priceListVariantIds: string[] = []

// When filtering by price_list_id, we need use the remote query to get
// the variant IDs through the price list price sets.
if (Array.isArray(filterByPriceListIds)) {
const [priceLists] = await listPriceLists({
container: scope,
remoteQueryFields: ["price_set_money_amounts.price_set.variant.id"],
apiFields: ["prices.variant_id"],
variables: { filters: { id: filterByPriceListIds }, skip: 0, take: null },
})

priceListVariantIds.push(
...((priceLists
.map((priceList) => priceList.prices?.map((price) => price.variant_id))
.flat(2)
.filter(isString) || []) as string[])
)

delete filterableFields.price_list_id
}

if (priceListVariantIds.length) {
const existingVariantFilters = filterableFields.variants || {}

filterableFields.variants = {
...existingVariantFilters,
id: priceListVariantIds,
}
}

return filterableFields
}

export const GET = async (
req: AuthenticatedMedusaRequest<AdminGetProductsParams>,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
let filterableFields: AdminGetProductsParams = { ...req.filterableFields }

filterableFields = await applyVariantFiltersForPriceList(
req.scope,
filterableFields
)

const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? [])
const queryObject = remoteQueryObjectFromString({
entryPoint: "product",
variables: {
filters: filterableFields,
filters: req.filterableFields,
...req.remoteQueryConfig.pagination,
},
fields: selectFields,
Expand Down
3 changes: 3 additions & 0 deletions packages/medusa/src/api-v2/admin/products/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./maybe-apply-price-lists-filter"
export * from "./maybe-apply-sales-channels-filter"

Loading

0 comments on commit 9e25e0c

Please sign in to comment.