fix(dashboard,medusa,types): improve performance for price list prices retrieval (#14138)

## Summary

**What** — What changes are introduced in this PR?

Price lists prices didn't have a dedicated method to query them and instead, relied on being returned as part of price lists. This, however, introduces optimization issues that for price lists with many prices, could cause crashes. The reason being that relations are not paginated and thus, all prices linked to the price list would be returned.

This PR aims to solve this by introducing a dedicated endpoint and avoiding returning the `prices` as part of price lists by default. The idea being that it is up to the user to explicitly express this, which, for small price lists no issues will arise, but for bigger ones, they will easily recognize the performance impact.

**Why** — Why are these changes relevant or necessary?  

Users with large enough price lists would have serious performance issues or even crashes when querying the `/admin/price-lists` endpoints. This is also true when navigating to the price list section of the Admin UI since it queries this same endpoints.

**How** — How have these changes been implemented?

- Removed the `prices` relation to be part of the default fields returned by the `/admin/price-lists/` endpoints. User may still request it by passing it in `fields` query param.
- Added new `/admin/price-lists/[id]/prices` GET endpoint to be able to retrieve a price list prices with pagination.

**Testing** — How have these changes been tested, or how can the reviewer test the feature?

Integration tests.

---

## Examples

Provide examples or code snippets that demonstrate how this feature works, or how it can be used in practice.  
This helps with documentation and ensures maintainers can quickly understand and verify the change.

```ts
// Example usage
```

---

## Checklist

Please ensure the following before requesting a review:

- [x] I have added a **changeset** for this PR
    - Every non-breaking change should be marked as a **patch**
    - To add a changeset, run `yarn changeset` and follow the prompts
- [x] The changes are covered by relevant **tests**
- [x] I have verified the code works as intended locally
- [x] I have linked the related issue(s) if applicable

---

## Additional Context

The current state of the PR fixes the issue on the price list list and detail component. It still doesn't solve the issue for the following screens: Edit Prices & Add Prices

All the prices are still retrieved from the `/admin/price-lists/` endpoint for these. I want first some feedback before changing it to the new endpoint, since the current DataGrid implementation doesn't support pagination and it seems we are passing a default limit for the products to show there, an arbitrarily large number 9999 and there is also a TODO comment of changing that.

This previous point, though, could be implemented in a later PR, so we can already fix the issue in the price list list and detail pages, so at least for large price lists these screens don't explode and smaller price lists can still have its product prices edited, while only large ones will explode when trying to perform this action. @adrien2p @fPolic thoughts?

closes ENTSUP-265, CORE-1239
This commit is contained in:
Nicolas Gorga authored and GitHub committed 2025-12-11 13:13:08 +00:00
1 parent 237b472e73
commit 70929ecac3
17 files changed
+353 -147

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/dashboard": minor
"@medusajs/types": minor
"@medusajs/medusa": minor
---
fix(dashboard,medusa,types): improve performance for price list prices retrieval
@@ -14,6 +14,7 @@ medusaIntegrationTestRunner({
env: {}, env: {},
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
let pricelist1 let pricelist1
let pricelist1Prices
let pricelist2 let pricelist2
let region1 let region1
let product1 let product1
@@ -93,6 +94,10 @@ medusaIntegrationTestRunner({
) )
).data.price_list ).data.price_list
pricelist1Prices = await api
.get(`/admin/price-lists/${pricelist1.id}/prices`, adminHeaders)
.then((res) => res.data.prices)
pricelist2 = ( pricelist2 = (
await api.post( await api.post(
"/admin/price-lists", "/admin/price-lists",
@@ -134,7 +139,7 @@ medusaIntegrationTestRunner({
}) })
const response = await api.post( const response = await api.post(
"/admin/price-lists", "/admin/price-lists?fields=prices.*,prices.price_set.*,prices.price_set.variant.*",
payload, payload,
adminHeaders adminHeaders
) )
@@ -189,34 +194,12 @@ medusaIntegrationTestRunner({
status: pricelist1.status, status: pricelist1.status,
starts_at: pricelist1.starts_at, starts_at: pricelist1.starts_at,
ends_at: pricelist1.ends_at, ends_at: pricelist1.ends_at,
prices: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
amount: 100,
currency_code: "usd",
// BREAKING: Min and max quantity are returned as string
min_quantity: 1,
max_quantity: 100,
variant_id: product1.variants[0].id,
created_at: expect.any(String),
updated_at: expect.any(String),
// BREAKING: `variant` and `variants` are not returned as part of the prices
}),
expect.objectContaining({
id: expect.any(String),
amount: 80,
currency_code: "usd",
min_quantity: 101,
max_quantity: 500,
variant_id: product1.variants[0].id,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
]),
created_at: expect.any(String), created_at: expect.any(String),
updated_at: expect.any(String), updated_at: expect.any(String),
}) })
) )
// BREAKING: Prices are not returned as part of the price list if not specified in fields
expect(response.data.price_list).not.toHaveProperty("prices")
}) })
it("returns a list of price lists", async () => { it("returns a list of price lists", async () => {
@@ -367,22 +350,6 @@ medusaIntegrationTestRunner({
status: "draft", status: "draft",
starts_at: "2022-09-01T00:00:00.000Z", starts_at: "2022-09-01T00:00:00.000Z",
ends_at: "2022-12-31T00:00:00.000Z", ends_at: "2022-12-31T00:00:00.000Z",
prices: expect.arrayContaining([
expect.objectContaining({
amount: 100,
currency_code: "usd",
id: expect.any(String),
max_quantity: 100,
min_quantity: 1,
}),
expect.objectContaining({
amount: 80,
currency_code: "usd",
id: expect.any(String),
max_quantity: 500,
min_quantity: 101,
}),
]),
rules: { rules: {
"customer.groups.id": [customerGroup1.id], "customer.groups.id": [customerGroup1.id],
}, },
@@ -393,11 +360,12 @@ medusaIntegrationTestRunner({
}) })
it("updates the amount and currency of a price in the price list", async () => { it("updates the amount and currency of a price in the price list", async () => {
const priceToUpdate = pricelist1Prices.find((p) => p.amount === 80)
const payload = { const payload = {
// BREAKING: Updates of prices happen through the batch endpoint, and doing it through the price list update endpoint is no longer supported // BREAKING: Updates of prices happen through the batch endpoint, and doing it through the price list update endpoint is no longer supported
update: [ update: [
{ {
id: pricelist1.prices.find((p) => p.amount === 80).id, id: priceToUpdate.id,
amount: 250, amount: 250,
currency_code: "eur", currency_code: "eur",
variant_id: product1.variants[0].id, variant_id: product1.variants[0].id,
@@ -411,27 +379,29 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
const response = await api.get( const response = await api.get(
`/admin/price-lists/${pricelist1.id}`, `/admin/price-lists/${pricelist1.id}/prices`,
adminHeaders adminHeaders
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.price_list.prices).toEqual([ expect(response.data.prices).toEqual(
expect.objectContaining({ expect.arrayContaining([
amount: 100, expect.objectContaining({
currency_code: "usd", amount: 100,
id: expect.any(String), currency_code: "usd",
max_quantity: 100, id: expect.any(String),
min_quantity: 1, max_quantity: 100,
}), min_quantity: 1,
expect.objectContaining({ }),
amount: 250, expect.objectContaining({
currency_code: "eur", amount: 250,
id: expect.any(String), currency_code: "eur",
max_quantity: 500, id: expect.any(String),
min_quantity: 101, max_quantity: 500,
}), min_quantity: 101,
]) }),
])
)
}) })
}) })
@@ -470,13 +440,13 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
const response = await api.get( const response = await api.get(
`/admin/price-lists/${pricelist1.id}`, `/admin/price-lists/${pricelist1.id}/prices`,
adminHeaders adminHeaders
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.price_list.prices.length).toEqual(5) expect(response.data.prices.length).toEqual(5)
expect(response.data.price_list.prices).toEqual( expect(response.data.prices).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
@@ -484,7 +454,12 @@ medusaIntegrationTestRunner({
currency_code: "usd", currency_code: "usd",
min_quantity: 1, min_quantity: 1,
max_quantity: 100, max_quantity: 100,
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
@@ -492,13 +467,23 @@ medusaIntegrationTestRunner({
currency_code: "usd", currency_code: "usd",
min_quantity: 101, min_quantity: 101,
max_quantity: 500, max_quantity: 500,
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
amount: 45, amount: 45,
currency_code: "usd", currency_code: "usd",
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
min_quantity: 1001, min_quantity: 1001,
max_quantity: 2000, max_quantity: 2000,
}), }),
@@ -506,7 +491,12 @@ medusaIntegrationTestRunner({
id: expect.any(String), id: expect.any(String),
amount: 35, amount: 35,
currency_code: "usd", currency_code: "usd",
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
min_quantity: 2001, min_quantity: 2001,
max_quantity: 3000, max_quantity: 3000,
}), }),
@@ -514,7 +504,12 @@ medusaIntegrationTestRunner({
id: expect.any(String), id: expect.any(String),
amount: 25, amount: 25,
currency_code: "usd", currency_code: "usd",
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
min_quantity: 3001, min_quantity: 3001,
max_quantity: 4000, max_quantity: 4000,
}), }),
@@ -551,13 +546,13 @@ medusaIntegrationTestRunner({
) )
const response = await api.get( const response = await api.get(
`/admin/price-lists/${pricelist1.id}`, `/admin/price-lists/${pricelist1.id}/prices`,
adminHeaders adminHeaders
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.price_list.prices.length).toEqual(2) expect(response.data.prices.length).toEqual(2)
expect(response.data.price_list.prices).toEqual( expect(response.data.prices).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
@@ -565,7 +560,12 @@ medusaIntegrationTestRunner({
currency_code: "usd", currency_code: "usd",
min_quantity: 1, min_quantity: 1,
max_quantity: 100, max_quantity: 100,
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
@@ -573,7 +573,12 @@ medusaIntegrationTestRunner({
currency_code: "usd", currency_code: "usd",
min_quantity: 101, min_quantity: 101,
max_quantity: 500, max_quantity: 500,
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
]) ])
) )
@@ -605,13 +610,13 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
const response = await api.get( const response = await api.get(
`/admin/price-lists/${pricelist1.id}`, `/admin/price-lists/${pricelist1.id}/prices`,
adminHeaders adminHeaders
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.price_list.prices.length).toEqual(4) expect(response.data.prices.length).toEqual(4)
expect(response.data.price_list.prices).toEqual( expect(response.data.prices).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
@@ -619,7 +624,12 @@ medusaIntegrationTestRunner({
currency_code: "usd", currency_code: "usd",
min_quantity: 1, min_quantity: 1,
max_quantity: 100, max_quantity: 100,
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
@@ -627,20 +637,40 @@ medusaIntegrationTestRunner({
currency_code: "usd", currency_code: "usd",
min_quantity: 101, min_quantity: 101,
max_quantity: 500, max_quantity: 500,
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
amount: 100, amount: 100,
currency_code: "eur", currency_code: "eur",
rules: { region_id: region1.id }, price_rules: expect.arrayContaining([
variant_id: product1.variants[0].id, expect.objectContaining({
attribute: "region_id",
value: region1.id,
}),
]),
price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
amount: 200, amount: 200,
currency_code: "eur", currency_code: "eur",
variant_id: product1.variants[0].id, price_set: expect.objectContaining({
id: expect.any(String),
variant: {
id: product1.variants[0].id,
},
}),
}), }),
]) ])
) )
@@ -680,41 +710,48 @@ medusaIntegrationTestRunner({
) )
const response = await api.get( const response = await api.get(
`/admin/price-lists/${pricelist1.id}`, `/admin/price-lists/${pricelist1.id}/prices`,
adminHeaders adminHeaders
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.price_list.prices.length).toEqual(0) expect(response.data.prices.length).toEqual(0)
}) })
}) })
describe("DELETE /admin/price-lists/:id/prices/batch", () => { describe("DELETE /admin/price-lists/:id/prices/batch", () => {
// BREAKING: The batch method signature changed // BREAKING: The batch method signature changed
it("Deletes several prices associated with a price list", async () => { it("Deletes several prices associated with a price list", async () => {
const priceToDelete = pricelist1Prices.find((p) => p.amount === 100)
await api.post( await api.post(
`/admin/price-lists/${pricelist1.id}/prices/batch`, `/admin/price-lists/${pricelist1.id}/prices/batch`,
{ {
delete: [pricelist1.prices[0].id], delete: [priceToDelete.id],
}, },
adminHeaders adminHeaders
) )
const response = await api.get( const response = await api.get(
`/admin/price-lists/${pricelist1.id}`, `/admin/price-lists/${pricelist1.id}/prices`,
adminHeaders adminHeaders
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.price_list.prices).toEqual([ expect(response.data.prices).toEqual(
expect.objectContaining({ expect.arrayContaining([
amount: 80, expect.objectContaining({
currency_code: "usd", amount: 80,
min_quantity: 101, currency_code: "usd",
max_quantity: 500, min_quantity: 101,
variant_id: product1.variants[0].id, max_quantity: 500,
}), price_set: expect.objectContaining({
]) variant: expect.objectContaining({
id: product1.variants[0].id,
}),
}),
}),
])
)
}) })
}) })
@@ -99,7 +99,10 @@ medusaIntegrationTestRunner({
}, },
]) ])
let response = await api.get(`/admin/price-lists`, adminHeaders) let response = await api.get(
`/admin/price-lists?fields=prices.*,prices.price_rules.*,prices.price_set.*,prices.price_set.variant.*`,
adminHeaders
)
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1) expect(response.data.count).toEqual(1)
@@ -130,9 +133,16 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String), updated_at: expect.any(String),
deleted_at: null, deleted_at: null,
price_set_id: expect.any(String), price_set_id: expect.any(String),
price_list_id: expect.any(String),
title: null,
rules: { rules: {
region_id: region.id, region_id: region.id,
}, },
rules_count: 1,
raw_amount: expect.objectContaining({
value: "5000",
precision: 20,
}),
}, },
], ],
}, },
@@ -258,7 +268,7 @@ medusaIntegrationTestRunner({
]) ])
let response = await api.get( let response = await api.get(
`/admin/price-lists/${priceList.id}`, `/admin/price-lists/${priceList.id}?fields=prices.*,prices.price_rules.*,prices.price_set.*,prices.price_set.variant.*`,
adminHeaders adminHeaders
) )
@@ -288,8 +298,15 @@ medusaIntegrationTestRunner({
variant_id: variant.id, variant_id: variant.id,
created_at: expect.any(String), created_at: expect.any(String),
updated_at: expect.any(String), updated_at: expect.any(String),
price_list_id: expect.any(String),
price_set_id: expect.any(String), price_set_id: expect.any(String),
deleted_at: null, deleted_at: null,
rules_count: 1,
raw_amount: expect.objectContaining({
value: "5000",
precision: 20,
}),
title: null,
rules: { rules: {
region_id: region.id, region_id: region.id,
}, },
@@ -355,7 +372,7 @@ medusaIntegrationTestRunner({
} }
const response = await api.post( const response = await api.post(
`admin/price-lists`, `admin/price-lists?fields=prices.*,prices.price_rules.*,prices.price_set.*,prices.price_set.variant.*`,
data, data,
adminHeaders adminHeaders
) )
@@ -388,6 +405,12 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String), updated_at: expect.any(String),
deleted_at: null, deleted_at: null,
price_set_id: expect.any(String), price_set_id: expect.any(String),
rules_count: 1,
raw_amount: expect.objectContaining({
value: "400",
precision: 20,
}),
title: null,
rules: { rules: {
region_id: region.id, region_id: region.id,
}, },
@@ -14,7 +14,12 @@ import { customerGroupsQueryKeys } from "./customer-groups"
import { productsQueryKeys } from "./products" import { productsQueryKeys } from "./products"
const PRICE_LISTS_QUERY_KEY = "price-lists" as const const PRICE_LISTS_QUERY_KEY = "price-lists" as const
const PRICE_LIST_PRICES_QUERY_KEY = "price-list-prices" as const
export const priceListsQueryKeys = queryKeysFactory(PRICE_LISTS_QUERY_KEY) export const priceListsQueryKeys = queryKeysFactory(PRICE_LISTS_QUERY_KEY)
export const priceListPricesQueryKeys = queryKeysFactory(
PRICE_LIST_PRICES_QUERY_KEY
)
export const usePriceList = ( export const usePriceList = (
id: string, id: string,
@@ -31,7 +36,7 @@ export const usePriceList = (
) => { ) => {
const { data, ...rest } = useQuery({ const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.priceList.retrieve(id, query), queryFn: () => sdk.admin.priceList.retrieve(id, query),
queryKey: priceListsQueryKeys.detail(id), queryKey: priceListsQueryKeys.detail(id, query),
...options, ...options,
}) })
@@ -124,6 +129,25 @@ export const useDeletePriceList = (
}) })
} }
export const usePriceListPrices = (
id: string,
query?: HttpTypes.AdminPriceListPriceListParams,
options?: UseQueryOptions<
HttpTypes.AdminPriceListPriceListResponse,
FetchError,
HttpTypes.AdminPriceListPriceListResponse,
QueryKey
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.priceList.prices(id, query),
queryKey: priceListPricesQueryKeys.detail(id, query),
...options,
})
return { ...data, ...rest }
}
export const useBatchPriceListPrices = ( export const useBatchPriceListPrices = (
id: string, id: string,
query?: HttpTypes.AdminPriceListParams, query?: HttpTypes.AdminPriceListParams,
@@ -141,6 +165,9 @@ export const useBatchPriceListPrices = (
queryKey: priceListsQueryKeys.detail(id), queryKey: priceListsQueryKeys.detail(id),
}) })
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() }) queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: priceListPricesQueryKeys.detail(id),
})
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeletePriceListAction } from "../../../common/hooks/use-delete-price-list-action" import { useDeletePriceListAction } from "../../../common/hooks/use-delete-price-list-action"
import { getPriceListStatus } from "../../../common/utils" import { getPriceListStatus } from "../../../common/utils"
import { usePriceListPrices } from "../../../../../hooks/api"
type PriceListGeneralSectionProps = { type PriceListGeneralSectionProps = {
priceList: HttpTypes.AdminPriceList priceList: HttpTypes.AdminPriceList
@@ -15,8 +16,13 @@ export const PriceListGeneralSection = ({
priceList, priceList,
}: PriceListGeneralSectionProps) => { }: PriceListGeneralSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const {
const overrideCount = priceList.prices?.length || 0 count: overrideCount,
isLoading,
error,
} = usePriceListPrices(priceList.id, {
limit: 1,
})
const { color, text } = getPriceListStatus(t, priceList) const { color, text } = getPriceListStatus(t, priceList)
@@ -77,9 +83,11 @@ export const PriceListGeneralSection = ({
<Text leading="compact" size="small" weight="plus"> <Text leading="compact" size="small" weight="plus">
{t("priceLists.fields.priceOverrides.label")} {t("priceLists.fields.priceOverrides.label")}
</Text> </Text>
<Text size="small" className="text-pretty"> {!isLoading && !error && (
{overrideCount || "-"} <Text size="small" className="text-pretty">
</Text> {overrideCount || "-"}
</Text>
)}
</div> </div>
</Container> </Container>
) )
@@ -0,0 +1,19 @@
import { usePriceListPrices } from "../../../../../hooks/api/price-lists"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
import { TextCell } from "../../../../../components/table/table-cells/common/text-cell"
type PriceCountCellProps = {
priceListId: string
}
export const PriceCountCell = ({ priceListId }: PriceCountCellProps) => {
const { count, isLoading } = usePriceListPrices(priceListId, {
limit: 1,
})
if (isLoading) {
return <PlaceholderCell />
}
return <TextCell text={count && count > 0 ? count.toString() : "-"} />
}
@@ -4,12 +4,10 @@ import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell" import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
import { import { TextHeader } from "../../../../../components/table/table-cells/common/text-cell"
TextCell,
TextHeader,
} from "../../../../../components/table/table-cells/common/text-cell"
import { getPriceListStatus } from "../../../common/utils" import { getPriceListStatus } from "../../../common/utils"
import { PriceListListTableActions } from "./price-list-list-table-actions" import { PriceListListTableActions } from "./price-list-list-table-actions"
import { PriceCountCell } from "./price-count-cell"
const columnHelper = createColumnHelper<HttpTypes.AdminPriceList>() const columnHelper = createColumnHelper<HttpTypes.AdminPriceList>()
@@ -30,9 +28,10 @@ export const usePricingTableColumns = () => {
return <StatusCell color={color}>{text}</StatusCell> return <StatusCell color={color}>{text}</StatusCell>
}, },
}), }),
columnHelper.accessor("prices", { columnHelper.display({
id: "price_overrides",
header: t("priceLists.fields.priceOverrides.header"), header: t("priceLists.fields.priceOverrides.header"),
cell: (info) => <TextCell text={`${info.getValue()?.length || "-"}`} />, cell: ({ row }) => <PriceCountCell priceListId={row.original.id} />,
}), }),
columnHelper.display({ columnHelper.display({
id: "actions", id: "actions",
@@ -7,7 +7,10 @@ import { PriceListPricesAddForm } from "./components/price-list-prices-add-form"
export const PriceListProductsAdd = () => { export const PriceListProductsAdd = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const { price_list, isPending, isError, error } = usePriceList(id!) const { price_list, isPending, isError, error } = usePriceList(id!, {
fields:
"*prices,prices.price_set.variant.id,prices.price_rules.attribute,prices.price_rules.value",
})
const { currencies, regions, pricePreferences, isReady } = const { currencies, regions, pricePreferences, isReady } =
usePriceListCurrencyData() usePriceListCurrencyData()
@@ -10,7 +10,9 @@ export const PriceListPricesEdit = () => {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const ids = searchParams.get("ids[]") const ids = searchParams.get("ids[]")
const { price_list, isLoading, isError, error } = usePriceList(id!) const { price_list, isLoading, isError, error } = usePriceList(id!, {
fields: "*prices,prices.price_set.variant.id,prices.price_rules.attribute,prices.price_rules.value",
})
const productIds = ids?.split(",") const productIds = ids?.split(",")
const { const {
+57 -27
View File
@@ -19,24 +19,24 @@ export class PriceList {
* This method retrieves a price list. It sends a request to the * This method retrieves a price list. It sends a request to the
* [Get Price List](https://docs.medusajs.com/v2/api/admin#price-lists_getpricelistsid) * [Get Price List](https://docs.medusajs.com/v2/api/admin#price-lists_getpricelistsid)
* API route. * API route.
* *
* @param id - The price list's ID. * @param id - The price list's ID.
* @param query - Configure the fields to retrieve in the price list. * @param query - Configure the fields to retrieve in the price list.
* @param headers - Headers to pass in the request * @param headers - Headers to pass in the request
* @returns The price list's details. * @returns The price list's details.
* *
* @example * @example
* To retrieve a price list by its ID: * To retrieve a price list by its ID:
* *
* ```ts * ```ts
* sdk.admin.priceList.retrieve("plist_123") * sdk.admin.priceList.retrieve("plist_123")
* .then(({ price_list }) => { * .then(({ price_list }) => {
* console.log(price_list) * console.log(price_list)
* }) * })
* ``` * ```
* *
* To specify the fields and relations to retrieve: * To specify the fields and relations to retrieve:
* *
* ```ts * ```ts
* sdk.admin.priceList.retrieve("plist_123", { * sdk.admin.priceList.retrieve("plist_123", {
* fields: "id,*prices" * fields: "id,*prices"
@@ -45,7 +45,7 @@ export class PriceList {
* console.log(price_list) * console.log(price_list)
* }) * })
* ``` * ```
* *
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/v2/api/store#select-fields-and-relations). * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/v2/api/store#select-fields-and-relations).
*/ */
async retrieve( async retrieve(
@@ -64,27 +64,27 @@ export class PriceList {
} }
/** /**
* This method retrieves a paginated list of price lists. It sends a request to the * This method retrieves a paginated list of price lists. It sends a request to the
* [List Price Lists](https://docs.medusajs.com/v2/api/admin#price-lists_getpricelists) API route. * [List Price Lists](https://docs.medusajs.com/v2/api/admin#price-lists_getpricelists) API route.
* *
* @param query - Filters and pagination configurations. * @param query - Filters and pagination configurations.
* @param headers - Headers to pass in the request. * @param headers - Headers to pass in the request.
* @returns The paginated list of price lists. * @returns The paginated list of price lists.
* *
* @example * @example
* To retrieve the list of price lists: * To retrieve the list of price lists:
* *
* ```ts * ```ts
* sdk.admin.priceList.list() * sdk.admin.priceList.list()
* .then(({ price_lists, count, limit, offset }) => { * .then(({ price_lists, count, limit, offset }) => {
* console.log(price_lists) * console.log(price_lists)
* }) * })
* ``` * ```
* *
* To configure the pagination, pass the `limit` and `offset` query parameters. * To configure the pagination, pass the `limit` and `offset` query parameters.
* *
* For example, to retrieve only 10 items and skip 10 items: * For example, to retrieve only 10 items and skip 10 items:
* *
* ```ts * ```ts
* sdk.admin.priceList.list({ * sdk.admin.priceList.list({
* limit: 10, * limit: 10,
@@ -94,10 +94,10 @@ export class PriceList {
* console.log(price_lists) * console.log(price_lists)
* }) * })
* ``` * ```
* *
* Using the `fields` query parameter, you can specify the fields and relations to retrieve * Using the `fields` query parameter, you can specify the fields and relations to retrieve
* in each price list: * in each price list:
* *
* ```ts * ```ts
* sdk.admin.priceList.list({ * sdk.admin.priceList.list({
* fields: "id,*prices" * fields: "id,*prices"
@@ -106,7 +106,7 @@ export class PriceList {
* console.log(price_lists) * console.log(price_lists)
* }) * })
* ``` * ```
* *
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/v2/api/store#select-fields-and-relations). * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/v2/api/store#select-fields-and-relations).
*/ */
async list( async list(
@@ -127,12 +127,12 @@ export class PriceList {
* This method creates a price list. It sends a request to the * This method creates a price list. It sends a request to the
* [Create Price List](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelists) * [Create Price List](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelists)
* API route. * API route.
* *
* @param body - The details of the price list to create. * @param body - The details of the price list to create.
* @param query - Configure the fields to retrieve in the price list. * @param query - Configure the fields to retrieve in the price list.
* @param headers - Headers to pass in the request * @param headers - Headers to pass in the request
* @returns The price list's details. * @returns The price list's details.
* *
* @example * @example
* sdk.admin.priceList.create({ * sdk.admin.priceList.create({
* title: "My Price List", * title: "My Price List",
@@ -170,16 +170,16 @@ export class PriceList {
} }
/** /**
* This method updates a price list. It sends a request to the * This method updates a price list. It sends a request to the
* [Update Price List](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelistsid) * [Update Price List](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelistsid)
* API route. * API route.
* *
* @param id - The price list's ID. * @param id - The price list's ID.
* @param body - The data to update in the price list. * @param body - The data to update in the price list.
* @param query - Configure the fields to retrieve in the price list. * @param query - Configure the fields to retrieve in the price list.
* @param headers - Headers to pass in the request * @param headers - Headers to pass in the request
* @returns The price list's details. * @returns The price list's details.
* *
* @example * @example
* sdk.admin.priceList.update("plist_123", { * sdk.admin.priceList.update("plist_123", {
* title: "My Price List", * title: "My Price List",
@@ -209,11 +209,11 @@ export class PriceList {
* This method deletes a price list. It sends a request to the * This method deletes a price list. It sends a request to the
* [Delete Price List](https://docs.medusajs.com/v2/api/admin#price-lists_deletepricelistsid) * [Delete Price List](https://docs.medusajs.com/v2/api/admin#price-lists_deletepricelistsid)
* API route. * API route.
* *
* @param id - The price list's ID. * @param id - The price list's ID.
* @param headers - Headers to pass in the request * @param headers - Headers to pass in the request
* @returns The deletion's details. * @returns The deletion's details.
* *
* @example * @example
* sdk.admin.priceList.delete("plist_123") * sdk.admin.priceList.delete("plist_123")
* .then(({ deleted }) => { * .then(({ deleted }) => {
@@ -234,13 +234,13 @@ export class PriceList {
* This method manages the prices of a price list to create, update, or delete them. * This method manages the prices of a price list to create, update, or delete them.
* It sends a request to the [Manage Prices](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelistsidpricesbatch) * It sends a request to the [Manage Prices](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelistsidpricesbatch)
* API route. * API route.
* *
* @param id - The price list's ID. * @param id - The price list's ID.
* @param body - The prices to create, update, or delete. * @param body - The prices to create, update, or delete.
* @param query - Configure the fields to retrieve in the price list. * @param query - Configure the fields to retrieve in the price list.
* @param headers - Headers to pass in the request * @param headers - Headers to pass in the request
* @returns The price list's details. * @returns The price list's details.
* *
* @example * @example
* sdk.admin.priceList.batchPrices("plist_123", { * sdk.admin.priceList.batchPrices("plist_123", {
* create: [{ * create: [{
@@ -279,17 +279,47 @@ export class PriceList {
) )
} }
/**
* This method retrieves the prices of a price list. It sends a request to the
* [Get Prices](https://docs.medusajs.com/v2/api/admin#price-lists_getpricelistsidprices)
* API route.
*
* @param id - The price list's ID.
* @param query - Configure the fields to retrieve in the price list.
* @param headers - Headers to pass in the request
* @returns The price list's prices.
*
* @example
* sdk.admin.priceList.prices("plist_123")
* .then(({ prices }) => {
* console.log(prices)
* })
*/
async prices(
id: string,
query?: HttpTypes.AdminPriceListPriceListParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminPriceListPriceListResponse>(
`/admin/price-lists/${id}/prices`,
{
method: "GET",
headers,
query,
}
)
}
/** /**
* This method removes products from a price list. It sends a request to the * This method removes products from a price list. It sends a request to the
* [Remove Product](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelistsidproducts) * [Remove Product](https://docs.medusajs.com/v2/api/admin#price-lists_postpricelistsidproducts)
* API route. * API route.
* *
* @param id - The price list's ID. * @param id - The price list's ID.
* @param body - The details of the products to remove. * @param body - The details of the products to remove.
* @param query - Configure the fields to retrieve in the price list. * @param query - Configure the fields to retrieve in the price list.
* @param headers - Headers to pass in the request * @param headers - Headers to pass in the request
* @returns The price list's details. * @returns The price list's details.
* *
* @example * @example
* sdk.admin.priceList.linkProducts("plist_123", { * sdk.admin.priceList.linkProducts("plist_123", {
* remove: ["prod_123"] * remove: ["prod_123"]
@@ -32,3 +32,7 @@ export interface AdminPriceListListParams
} }
export interface AdminPriceListParams extends SelectParams {} export interface AdminPriceListParams extends SelectParams {}
export interface AdminPriceListPriceListParams
extends FindParams,
BaseFilterable<AdminPriceListPriceListParams> {}
@@ -38,3 +38,8 @@ export interface AdminPriceListBatchResponse {
deleted: boolean deleted: boolean
} }
} }
export interface AdminPriceListPriceListResponse
extends PaginatedResponse<{
prices: AdminPrice[]
}> {}
@@ -4,7 +4,7 @@ import {
MedusaResponse, MedusaResponse,
} from "@medusajs/framework/http" } from "@medusajs/framework/http"
import { listPrices } from "../../../queries" import { listPrices } from "../../../queries"
import { adminPriceListPriceRemoteQueryFields } from "../../../query-config" import { adminPriceListPriceQueryFields } from "../../../query-config"
import { BatchMethodRequest, HttpTypes } from "@medusajs/framework/types" import { BatchMethodRequest, HttpTypes } from "@medusajs/framework/types"
import { import {
AdminCreatePriceListPriceType, AdminCreatePriceListPriceType,
@@ -44,12 +44,12 @@ export const POST = async (
listPrices( listPrices(
result.created.map((c) => c.id), result.created.map((c) => c.id),
req.scope, req.scope,
adminPriceListPriceRemoteQueryFields adminPriceListPriceQueryFields
), ),
listPrices( listPrices(
result.updated.map((c) => c.id), result.updated.map((c) => c.id),
req.scope, req.scope,
adminPriceListPriceRemoteQueryFields adminPriceListPriceQueryFields
), ),
]) ])
@@ -0,0 +1,26 @@
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { HttpTypes } from "@medusajs/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminPriceListParams>,
res: MedusaResponse<HttpTypes.AdminPriceListPriceListResponse>
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const result = await query.graph({
entity: "price",
fields: req.queryConfig.fields,
filters: {
...req.filterableFields,
price_list_id: req.params.id,
},
pagination: req.queryConfig.pagination,
})
res.status(200).json({
prices: result.data,
count: result.metadata?.count ?? 0,
offset: result.metadata?.skip ?? 0,
limit: result.metadata?.take ?? 0,
})
}
@@ -10,6 +10,7 @@ import {
AdminCreatePriceList, AdminCreatePriceList,
AdminCreatePriceListPrice, AdminCreatePriceListPrice,
AdminGetPriceListParams, AdminGetPriceListParams,
AdminGetPriceListPriceParams,
AdminGetPriceListPricesParams, AdminGetPriceListPricesParams,
AdminGetPriceListsParams, AdminGetPriceListsParams,
AdminRemoveProductsPriceList, AdminRemoveProductsPriceList,
@@ -44,7 +45,7 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [ middlewares: [
validateAndTransformBody(AdminCreatePriceList), validateAndTransformBody(AdminCreatePriceList),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetPriceListPricesParams, AdminGetPriceListsParams,
QueryConfig.retrivePriceListQueryConfig QueryConfig.retrivePriceListQueryConfig
), ),
], ],
@@ -55,7 +56,7 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [ middlewares: [
validateAndTransformBody(AdminUpdatePriceList), validateAndTransformBody(AdminUpdatePriceList),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetPriceListPricesParams, AdminGetPriceListParams,
QueryConfig.retrivePriceListQueryConfig QueryConfig.retrivePriceListQueryConfig
), ),
], ],
@@ -71,6 +72,16 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
), ),
], ],
}, },
{
method: ["GET"],
matcher: "/admin/price-lists/:id/prices",
middlewares: [
validateAndTransformQuery(
AdminGetPriceListPricesParams,
QueryConfig.listPriceListPriceQueryConfig
),
],
},
{ {
method: ["POST"], method: ["POST"],
matcher: "/admin/price-lists/:id/prices/batch", matcher: "/admin/price-lists/:id/prices/batch",
@@ -82,7 +93,7 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
createBatchBody(AdminCreatePriceListPrice, AdminUpdatePriceListPrice) createBatchBody(AdminCreatePriceListPrice, AdminUpdatePriceListPrice)
), ),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetPriceListPricesParams, AdminGetPriceListPriceParams,
QueryConfig.listPriceListPriceQueryConfig QueryConfig.listPriceListPriceQueryConfig
), ),
], ],
@@ -2,7 +2,8 @@ export enum PriceListRelations {
PRICES = "prices", PRICES = "prices",
} }
export const adminPriceListPriceRemoteQueryFields = [ // Note: renamed to avoid referencing remoteQuery which is legacy
export const adminPriceListPriceQueryFields = [
"id", "id",
"currency_code", "currency_code",
"amount", "amount",
@@ -29,11 +30,10 @@ export const adminPriceListRemoteQueryFields = [
"deleted_at", "deleted_at",
"price_list_rules.value", "price_list_rules.value",
"price_list_rules.attribute", "price_list_rules.attribute",
...adminPriceListPriceRemoteQueryFields.map((field) => `prices.${field}`),
] ]
export const retrivePriceListPriceQueryConfig = { export const retrivePriceListPriceQueryConfig = {
defaults: adminPriceListPriceRemoteQueryFields, defaults: adminPriceListPriceQueryFields,
isList: false, isList: false,
} }
@@ -7,7 +7,12 @@ import {
} from "../../utils/validators" } from "../../utils/validators"
import { applyAndAndOrOperators } from "../../utils/common-validators" import { applyAndAndOrOperators } from "../../utils/common-validators"
export const AdminGetPriceListPricesParams = createSelectParams() export const AdminGetPriceListPriceParams = createSelectParams()
export const AdminGetPriceListPricesParams = createFindParams({
offset: 0,
limit: 50,
})
export const AdminGetPriceListsParamsFields = z.object({ export const AdminGetPriceListsParamsFields = z.object({
q: z.string().optional(), q: z.string().optional(),