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:
@@ -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
|
||||||
@@ -71,6 +71,10 @@ medusaIntegrationTestRunner({
|
|||||||
amount: 3000,
|
amount: 3000,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
amount: 5000,
|
||||||
|
currency_code: "eur",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,6 +95,17 @@ medusaIntegrationTestRunner({
|
|||||||
price_set_link: {
|
price_set_link: {
|
||||||
fields: ["id", "price_set_id", "shipping_option_id"],
|
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,
|
price_set_id: priceSet.id,
|
||||||
shipping_option_id: shippingOption.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",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||||
import { addShippingMethodToCartStep } from "../steps"
|
import { addShippingMethodToCartStep } from "../steps"
|
||||||
import { getShippingOptionPriceSetsStep } from "../steps/get-shipping-option-price-sets"
|
|
||||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||||
|
|
||||||
@@ -28,32 +27,30 @@ export const addShippingMethodToWorkflow = createWorkflow(
|
|||||||
return (data.input.options ?? []).map((i) => i.id)
|
return (data.input.options ?? []).map((i) => i.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const priceSets = getShippingOptionPriceSetsStep({
|
|
||||||
optionIds: optionIds,
|
|
||||||
context: { currency_code: input.currency_code },
|
|
||||||
})
|
|
||||||
|
|
||||||
const shippingOptions = useRemoteQueryStep({
|
const shippingOptions = useRemoteQueryStep({
|
||||||
entry_point: "shipping_option",
|
entry_point: "shipping_option",
|
||||||
fields: ["id", "name"],
|
fields: ["id", "name", "calculated_price.calculated_amount"],
|
||||||
variables: {
|
variables: {
|
||||||
id: optionIds,
|
id: optionIds,
|
||||||
|
calculated_price: {
|
||||||
|
context: {
|
||||||
|
currency_code: input.currency_code,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const shippingMethodInput = transform(
|
const shippingMethodInput = transform(
|
||||||
{ priceSets, input, shippingOptions },
|
{ input, shippingOptions },
|
||||||
(data) => {
|
(data) => {
|
||||||
const options = (data.input.options ?? []).map((option) => {
|
const options = (data.input.options ?? []).map((option) => {
|
||||||
const shippingOption = data.shippingOptions.find(
|
const shippingOption = data.shippingOptions.find(
|
||||||
(so) => so.id === option.id
|
(so) => so.id === option.id
|
||||||
)!
|
)!
|
||||||
|
|
||||||
const price = data.priceSets[option.id].calculated_amount
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shipping_option_id: shippingOption.id,
|
shipping_option_id: shippingOption.id,
|
||||||
amount: price,
|
amount: shippingOption.calculated_price.calculated_amount,
|
||||||
data: option.data ?? {},
|
data: option.data ?? {},
|
||||||
name: shippingOption.name,
|
name: shippingOption.name,
|
||||||
cart_id: data.input.cart_id,
|
cart_id: data.input.cart_id,
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = {
|
|||||||
serviceName: Modules.PRODUCT,
|
serviceName: Modules.PRODUCT,
|
||||||
fieldAlias: {
|
fieldAlias: {
|
||||||
price_set: "price_set_link.price_set",
|
price_set: "price_set_link.price_set",
|
||||||
|
calculated_price: {
|
||||||
|
path: "price_set_link.price_set.calculated_price",
|
||||||
|
forwardArgumentsOnPath: ["price_set_link.price_set"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
serviceName: LINKS.ProductVariantPriceSet,
|
serviceName: LINKS.ProductVariantPriceSet,
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export const ShippingOptionPriceSet: ModuleJoinerConfig = {
|
|||||||
path: "price_set_link.price_set.prices",
|
path: "price_set_link.price_set.prices",
|
||||||
isList: true,
|
isList: true,
|
||||||
},
|
},
|
||||||
|
calculated_price: {
|
||||||
|
path: "price_set_link.price_set.calculated_price",
|
||||||
|
forwardArgumentsOnPath: ["price_set_link.price_set"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
serviceName: LINKS.ShippingOptionPriceSet,
|
serviceName: LINKS.ShippingOptionPriceSet,
|
||||||
|
|||||||
@@ -191,7 +191,9 @@ export class RemoteQuery {
|
|||||||
|
|
||||||
for (const arg of expand.args || []) {
|
for (const arg of expand.args || []) {
|
||||||
if (arg.name === "filters" && arg.value) {
|
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)) {
|
} else if (availableOptions.includes(arg.name)) {
|
||||||
const argName = availableOptionsAlias.has(arg.name)
|
const argName = availableOptionsAlias.has(arg.name)
|
||||||
? availableOptionsAlias.get(arg.name)!
|
? availableOptionsAlias.get(arg.name)!
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CreatePricesDTO,
|
CreatePricesDTO,
|
||||||
CreatePriceSetDTO,
|
CreatePriceSetDTO,
|
||||||
DAL,
|
DAL,
|
||||||
|
FindConfig,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
ModuleJoinerConfig,
|
ModuleJoinerConfig,
|
||||||
ModulesSdkTypes,
|
ModulesSdkTypes,
|
||||||
@@ -152,6 +153,95 @@ export default class PricingModuleService<
|
|||||||
return joinerConfig
|
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_")
|
@InjectManager("baseRepository_")
|
||||||
async calculatePrices(
|
async calculatePrices(
|
||||||
pricingFilters: PricingFilters,
|
pricingFilters: PricingFilters,
|
||||||
@@ -171,7 +261,9 @@ export default class PricingModuleService<
|
|||||||
(priceSetId: string): PricingTypes.CalculatedPriceSet => {
|
(priceSetId: string): PricingTypes.CalculatedPriceSet => {
|
||||||
// This is where we select prices, for now we just do a first match based on the database results
|
// 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
|
// 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 prices = pricesSetPricesMap.get(priceSetId) || []
|
||||||
const priceListPrice = prices.find((p) => p.price_list_id)
|
const priceListPrice = prices.find((p) => p.price_list_id)
|
||||||
|
|
||||||
@@ -749,7 +841,7 @@ export default class PricingModuleService<
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
title: "test", // TODO: accept title
|
title: "", // TODO: accept title
|
||||||
rules_count: numberOfRules,
|
rules_count: numberOfRules,
|
||||||
price_rules: Array.from(rulesDataMap.values()),
|
price_rules: Array.from(rulesDataMap.values()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ export interface PriceSetDTO {
|
|||||||
* The prices that belong to this price set.
|
* The prices that belong to this price set.
|
||||||
*/
|
*/
|
||||||
prices?: MoneyAmountDTO[]
|
prices?: MoneyAmountDTO[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The calculated price based on the context.
|
||||||
|
*/
|
||||||
|
calculated_price?: CalculatedPriceSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The rule types applied on this price set.
|
* The rule types applied on this price set.
|
||||||
*/
|
*/
|
||||||
@@ -321,7 +327,8 @@ export interface UpdatePriceSetDTO {
|
|||||||
* Filters to apply on price sets.
|
* Filters to apply on price sets.
|
||||||
*/
|
*/
|
||||||
export interface FilterablePriceSetProps
|
export interface FilterablePriceSetProps
|
||||||
extends BaseFilterable<FilterablePriceSetProps> {
|
extends BaseFilterable<FilterablePriceSetProps>,
|
||||||
|
PricingContext {
|
||||||
/**
|
/**
|
||||||
* IDs to filter price sets by.
|
* IDs to filter price sets by.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -33,13 +33,23 @@ describe("remoteQueryObjectFromString", function () {
|
|||||||
it("should return a remote query object", function () {
|
it("should return a remote query object", function () {
|
||||||
const output = remoteQueryObjectFromString({
|
const output = remoteQueryObjectFromString({
|
||||||
entryPoint: "product",
|
entryPoint: "product",
|
||||||
variables: {},
|
variables: {
|
||||||
|
q: "name",
|
||||||
|
options: {
|
||||||
|
name: "option_name",
|
||||||
|
},
|
||||||
|
"options.values": {
|
||||||
|
value: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
fields,
|
fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(output).toEqual({
|
expect(output).toEqual({
|
||||||
product: {
|
product: {
|
||||||
__args: {},
|
__args: {
|
||||||
|
q: "name",
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
@@ -54,6 +64,9 @@ describe("remoteQueryObjectFromString", function () {
|
|||||||
},
|
},
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
|
__args: {
|
||||||
|
name: "option_name",
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
@@ -64,6 +77,9 @@ describe("remoteQueryObjectFromString", function () {
|
|||||||
"metadata",
|
"metadata",
|
||||||
],
|
],
|
||||||
values: {
|
values: {
|
||||||
|
__args: {
|
||||||
|
value: 123,
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isObject } from "./is-object"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a string fields array to a remote query object
|
* Convert a string fields array to a remote query object
|
||||||
* @param config - The configuration object
|
* @param config - The configuration object
|
||||||
@@ -109,9 +111,7 @@ export function remoteQueryObjectFromString(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variables) {
|
const usedVariables = new Set()
|
||||||
remoteJoinerConfig[entryKey]["__args"] = variables
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (!field.includes(".")) {
|
if (!field.includes(".")) {
|
||||||
@@ -122,8 +122,19 @@ export function remoteQueryObjectFromString(
|
|||||||
const fieldSegments = field.split(".")
|
const fieldSegments = field.split(".")
|
||||||
const fieldProperty = fieldSegments.pop()
|
const fieldProperty = fieldSegments.pop()
|
||||||
|
|
||||||
|
let combinedPath = ""
|
||||||
|
|
||||||
const deepConfigRef = fieldSegments.reduce((acc, curr) => {
|
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]
|
return acc[curr]
|
||||||
}, remoteJoinerConfig[entryKey])
|
}, remoteJoinerConfig[entryKey])
|
||||||
|
|
||||||
@@ -131,5 +142,14 @@ export function remoteQueryObjectFromString(
|
|||||||
deepConfigRef["fields"].push(fieldProperty)
|
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
|
return remoteJoinerConfig
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user