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 1 commit
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
142 changes: 140 additions & 2 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 Down Expand Up @@ -73,6 +78,12 @@ medusaIntegrationTestRunner({
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
testSuite: ({ dbConnection, getContainer, api }) => {
let v2Product
let pricingService
let productService
let scService
let remoteLink
let container

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,7 +107,7 @@ medusaIntegrationTestRunner({
})

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

// We want to seed another product for v2 that has pricing correctly wired up for all pricing-related tests.
Expand All @@ -107,6 +118,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 +1120,128 @@ 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",
})

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"

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { NextFunction } from "express"
import { MedusaRequest } from "../../../../types/routing"
import { AdminGetProductsParams } from "../validators"

export function maybeApplyPriceListsFilter() {
return async (req: MedusaRequest, _, next: NextFunction) => {
const filterableFields: AdminGetProductsParams = req.filterableFields

if (!filterableFields.price_list_id) {
return next()
}

const priceListIds = filterableFields.price_list_id
delete filterableFields.price_list_id

const queryObject = remoteQueryObjectFromString({
entryPoint: "price_list",
fields: ["price_set_money_amounts.price_set.variant.id"],
variables: {
id: priceListIds,
},
})

const remoteQuery = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)

const variantIds: string[] = []
const priceLists = await remoteQuery(queryObject)

priceLists.forEach((priceList) => {
priceList.price_set_money_amounts?.forEach((psma) => {
const variantId = psma.price_set?.variant?.id
if (variantId) {
variantIds.push(variantId)
}
})
})

filterableFields.variants = {
...(filterableFields.variants ?? {}),
id: variantIds,
}

return next()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { NextFunction } from "express"
import { MedusaRequest } from "../../../../types/routing"
import { AdminGetProductsParams } from "../validators"

export function maybeApplySalesChannelsFilter() {
return async (req: MedusaRequest, _, next: NextFunction) => {
const filterableFields: AdminGetProductsParams = req.filterableFields

if (!filterableFields.sales_channel_id) {
return next()
}

const salesChannelIds = filterableFields.sales_channel_id
delete filterableFields.sales_channel_id

const remoteQuery = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)

const queryObject = remoteQueryObjectFromString({
entryPoint: "product_sales_channel",
fields: ["product_id"],
variables: { sales_channel_id: salesChannelIds },
})

const productsInSalesChannels = await remoteQuery(queryObject)

filterableFields.id = productsInSalesChannels.map((p) => p.product_id)

return next()
}
}
Loading
Loading