Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: List products middleware #6769

Merged
merged 7 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading