feat(modules-sdk): remote query context filter (#7153)

What:

- Remote query now handles `context` keywork in the arguments to forward it as a filter to the method `list`
- Pricing module `list` method returning `calculated_price` if requested as a field
This commit is contained in:
Carlos R. L. Rodrigues
2024-04-26 07:20:42 -03:00
committed by GitHub
parent aef222278b
commit 4b57c5d286
10 changed files with 202 additions and 21 deletions

View File

@@ -0,0 +1,10 @@
---
"@medusajs/link-modules": patch
"@medusajs/modules-sdk": patch
"@medusajs/core-flows": patch
"@medusajs/pricing": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
Remote query supporting context

View File

@@ -71,6 +71,10 @@ medusaIntegrationTestRunner({
amount: 3000,
currency_code: "usd",
},
{
amount: 5000,
currency_code: "eur",
},
],
})
@@ -91,6 +95,17 @@ medusaIntegrationTestRunner({
price_set_link: {
fields: ["id", "price_set_id", "shipping_option_id"],
},
prices: {
fields: ["amount", "currency_code"],
},
calculated_price: {
fields: ["calculated_amount", "currency_code"],
__args: {
context: {
currency_code: "eur",
},
},
},
},
})
@@ -103,6 +118,20 @@ medusaIntegrationTestRunner({
price_set_id: priceSet.id,
shipping_option_id: shippingOption.id,
}),
prices: [
expect.objectContaining({
amount: 5000,
currency_code: "eur",
}),
expect.objectContaining({
amount: 3000,
currency_code: "usd",
}),
],
calculated_price: expect.objectContaining({
calculated_amount: 5000,
currency_code: "eur",
}),
}),
])
)

View File

@@ -5,7 +5,6 @@ import {
} from "@medusajs/workflows-sdk"
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
import { addShippingMethodToCartStep } from "../steps"
import { getShippingOptionPriceSetsStep } from "../steps/get-shipping-option-price-sets"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
import { updateTaxLinesStep } from "../steps/update-tax-lines"
@@ -28,32 +27,30 @@ export const addShippingMethodToWorkflow = createWorkflow(
return (data.input.options ?? []).map((i) => i.id)
})
const priceSets = getShippingOptionPriceSetsStep({
optionIds: optionIds,
context: { currency_code: input.currency_code },
})
const shippingOptions = useRemoteQueryStep({
entry_point: "shipping_option",
fields: ["id", "name"],
fields: ["id", "name", "calculated_price.calculated_amount"],
variables: {
id: optionIds,
calculated_price: {
context: {
currency_code: input.currency_code,
},
},
},
})
const shippingMethodInput = transform(
{ priceSets, input, shippingOptions },
{ input, shippingOptions },
(data) => {
const options = (data.input.options ?? []).map((option) => {
const shippingOption = data.shippingOptions.find(
(so) => so.id === option.id
)!
const price = data.priceSets[option.id].calculated_amount
return {
shipping_option_id: shippingOption.id,
amount: price,
amount: shippingOption.calculated_price.calculated_amount,
data: option.data ?? {},
name: shippingOption.name,
cart_id: data.input.cart_id,

View File

@@ -43,6 +43,10 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = {
serviceName: Modules.PRODUCT,
fieldAlias: {
price_set: "price_set_link.price_set",
calculated_price: {
path: "price_set_link.price_set.calculated_price",
forwardArgumentsOnPath: ["price_set_link.price_set"],
},
},
relationship: {
serviceName: LINKS.ProductVariantPriceSet,

View File

@@ -44,6 +44,10 @@ export const ShippingOptionPriceSet: ModuleJoinerConfig = {
path: "price_set_link.price_set.prices",
isList: true,
},
calculated_price: {
path: "price_set_link.price_set.calculated_price",
forwardArgumentsOnPath: ["price_set_link.price_set"],
},
},
relationship: {
serviceName: LINKS.ShippingOptionPriceSet,

View File

@@ -191,7 +191,9 @@ export class RemoteQuery {
for (const arg of expand.args || []) {
if (arg.name === "filters" && arg.value) {
filters = { ...arg.value }
filters = { ...filters, ...arg.value }
} else if (arg.name === "context" && arg.value) {
filters["context"] = arg.value
} else if (availableOptions.includes(arg.name)) {
const argName = availableOptionsAlias.has(arg.name)
? availableOptionsAlias.get(arg.name)!

View File

@@ -6,6 +6,7 @@ import {
CreatePricesDTO,
CreatePriceSetDTO,
DAL,
FindConfig,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
@@ -152,6 +153,95 @@ export default class PricingModuleService<
return joinerConfig
}
private setupCalculatedPriceConfig_(
filters,
config
): PricingContext["context"] | undefined {
const fieldIdx = config.relations?.indexOf("calculated_price")
const shouldCalculatePrice = fieldIdx > -1
if (!shouldCalculatePrice) {
return
}
let pricingContext = filters.context ?? {}
// cleanup virtual field "calculated_price"
config.relations?.splice(fieldIdx, 1)
delete filters.context
return pricingContext
}
@InjectManager("baseRepository_")
async list(
filters: PricingTypes.FilterablePriceSetProps = {},
config: FindConfig<PricingTypes.PriceSetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PriceSetDTO[]> {
const pricingContext = this.setupCalculatedPriceConfig_(filters, config)
const priceSets = await super.list(filters, config, sharedContext)
if (pricingContext && priceSets.length) {
const priceSetIds: string[] = []
const priceSetMap = new Map()
for (const priceSet of priceSets) {
priceSetIds.push(priceSet.id)
priceSetMap.set(priceSet.id, priceSet)
}
const calculatedPrices = await this.calculatePrices(
{ id: priceSets.map((p) => p.id) },
{ context: pricingContext },
sharedContext
)
for (const calculatedPrice of calculatedPrices) {
const priceSet = priceSetMap.get(calculatedPrice.id)
priceSet.calculated_price = calculatedPrice
}
}
return priceSets
}
@InjectManager("baseRepository_")
async listAndCount(
filters: PricingTypes.FilterablePriceSetProps = {},
config: FindConfig<PricingTypes.PriceSetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[PriceSetDTO[], number]> {
const pricingContext = this.setupCalculatedPriceConfig_(filters, config)
const [priceSets, count] = await super.listAndCount(
filters,
config,
sharedContext
)
if (pricingContext && priceSets.length) {
const priceSetIds: string[] = []
const priceSetMap = new Map()
for (const priceSet of priceSets) {
priceSetIds.push(priceSet.id)
priceSetMap.set(priceSet.id, priceSet)
}
const calculatedPrices = await this.calculatePrices(
{ id: priceSets.map((p) => p.id) },
{ context: pricingContext },
sharedContext
)
for (const calculatedPrice of calculatedPrices) {
const priceSet = priceSetMap.get(calculatedPrice.id)
priceSet.calculated_price = calculatedPrice
}
}
return [priceSets, count]
}
@InjectManager("baseRepository_")
async calculatePrices(
pricingFilters: PricingFilters,
@@ -171,7 +261,9 @@ export default class PricingModuleService<
(priceSetId: string): PricingTypes.CalculatedPriceSet => {
// This is where we select prices, for now we just do a first match based on the database results
// which is prioritized by rules_count first for exact match and then deafult_priority of the rule_type
// inject custom price selection here
// TODO: inject custom price selection here
const prices = pricesSetPricesMap.get(priceSetId) || []
const priceListPrice = prices.find((p) => p.price_list_id)
@@ -749,7 +841,7 @@ export default class PricingModuleService<
return {
...rest,
title: "test", // TODO: accept title
title: "", // TODO: accept title
rules_count: numberOfRules,
price_rules: Array.from(rulesDataMap.values()),
}

View File

@@ -58,6 +58,12 @@ export interface PriceSetDTO {
* The prices that belong to this price set.
*/
prices?: MoneyAmountDTO[]
/**
* The calculated price based on the context.
*/
calculated_price?: CalculatedPriceSet
/**
* The rule types applied on this price set.
*/
@@ -321,7 +327,8 @@ export interface UpdatePriceSetDTO {
* Filters to apply on price sets.
*/
export interface FilterablePriceSetProps
extends BaseFilterable<FilterablePriceSetProps> {
extends BaseFilterable<FilterablePriceSetProps>,
PricingContext {
/**
* IDs to filter price sets by.
*/

View File

@@ -33,13 +33,23 @@ describe("remoteQueryObjectFromString", function () {
it("should return a remote query object", function () {
const output = remoteQueryObjectFromString({
entryPoint: "product",
variables: {},
variables: {
q: "name",
options: {
name: "option_name",
},
"options.values": {
value: 123,
},
},
fields,
})
expect(output).toEqual({
product: {
__args: {},
__args: {
q: "name",
},
fields: [
"id",
"created_at",
@@ -54,6 +64,9 @@ describe("remoteQueryObjectFromString", function () {
},
options: {
__args: {
name: "option_name",
},
fields: [
"id",
"created_at",
@@ -64,6 +77,9 @@ describe("remoteQueryObjectFromString", function () {
"metadata",
],
values: {
__args: {
value: 123,
},
fields: [
"id",
"created_at",

View File

@@ -1,3 +1,5 @@
import { isObject } from "./is-object"
/**
* Convert a string fields array to a remote query object
* @param config - The configuration object
@@ -109,9 +111,7 @@ export function remoteQueryObjectFromString(
},
}
if (variables) {
remoteJoinerConfig[entryKey]["__args"] = variables
}
const usedVariables = new Set()
for (const field of fields) {
if (!field.includes(".")) {
@@ -122,8 +122,19 @@ export function remoteQueryObjectFromString(
const fieldSegments = field.split(".")
const fieldProperty = fieldSegments.pop()
let combinedPath = ""
const deepConfigRef = fieldSegments.reduce((acc, curr) => {
acc[curr] ??= {}
combinedPath = combinedPath ? combinedPath + "." + curr : curr
if (isObject(variables) && combinedPath in variables) {
acc[curr] ??= {}
acc[curr]["__args"] = variables[combinedPath]
usedVariables.add(combinedPath)
} else {
acc[curr] ??= {}
}
return acc[curr]
}, remoteJoinerConfig[entryKey])
@@ -131,5 +142,14 @@ export function remoteQueryObjectFromString(
deepConfigRef["fields"].push(fieldProperty)
}
const topLevelArgs = {}
for (const key of Object.keys(variables ?? {})) {
if (!usedVariables.has(key)) {
topLevelArgs[key] = variables[key]
}
}
remoteJoinerConfig[entryKey]["__args"] = topLevelArgs ?? {}
return remoteJoinerConfig
}