Feat(): distributed caching (#13435)
RESOLVES CORE-1153 **What** - This pr mainly lay the foundation the caching layer. It comes with a modules (built in memory cache) and a redis provider. - Apply caching to few touch point to test Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Carlos R. L. Rodrigues
parent
5b135a41fe
commit
b9d6f73320
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
IRegionModuleService,
|
||||
IStoreModuleService,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/framework/types"
|
||||
import { MedusaError, Modules } from "@medusajs/framework/utils"
|
||||
import { MedusaError, Modules, useCache } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,48 @@ export type FindOneOrAnyRegionStepInput = {
|
||||
regionId?: string
|
||||
}
|
||||
|
||||
async function fetchRegionById(regionId: string, container: MedusaContainer) {
|
||||
const service = container.resolve<IRegionModuleService>(Modules.REGION)
|
||||
|
||||
const args = [
|
||||
regionId,
|
||||
{
|
||||
relations: ["countries"],
|
||||
},
|
||||
] as Parameters<IRegionModuleService["retrieveRegion"]>
|
||||
|
||||
return await useCache(async () => service.retrieveRegion(...args), {
|
||||
container,
|
||||
key: args,
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchDefaultStore(container: MedusaContainer) {
|
||||
const storeModule = container.resolve<IStoreModuleService>(Modules.STORE)
|
||||
|
||||
return await useCache(async () => storeModule.listStores(), {
|
||||
container,
|
||||
key: "find-one-or-any-region-default-store",
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchDefaultRegion(
|
||||
defaultRegionId: string,
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const service = container.resolve<IRegionModuleService>(Modules.REGION)
|
||||
|
||||
const args = [
|
||||
{ id: defaultRegionId },
|
||||
{ relations: ["countries"] },
|
||||
] as Parameters<IRegionModuleService["listRegions"]>
|
||||
|
||||
return await useCache(async () => service.listRegions(...args), {
|
||||
container,
|
||||
key: args,
|
||||
})
|
||||
}
|
||||
|
||||
export const findOneOrAnyRegionStepId = "find-one-or-any-region"
|
||||
/**
|
||||
* This step retrieves a region either by the provided ID or the first region in the first store.
|
||||
@@ -22,32 +65,24 @@ export const findOneOrAnyRegionStepId = "find-one-or-any-region"
|
||||
export const findOneOrAnyRegionStep = createStep(
|
||||
findOneOrAnyRegionStepId,
|
||||
async (data: FindOneOrAnyRegionStepInput, { container }) => {
|
||||
const service = container.resolve<IRegionModuleService>(Modules.REGION)
|
||||
|
||||
const storeModule = container.resolve<IStoreModuleService>(Modules.STORE)
|
||||
|
||||
if (data.regionId) {
|
||||
try {
|
||||
const region = await service.retrieveRegion(data.regionId, {
|
||||
relations: ["countries"],
|
||||
})
|
||||
const region = await fetchRegionById(data.regionId, container)
|
||||
return new StepResponse(region)
|
||||
} catch (error) {
|
||||
return new StepResponse(null)
|
||||
}
|
||||
}
|
||||
|
||||
const [store] = await storeModule.listStores()
|
||||
const [store] = await fetchDefaultStore(container)
|
||||
|
||||
if (!store) {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Store not found")
|
||||
}
|
||||
|
||||
const [region] = await service.listRegions(
|
||||
{
|
||||
id: store.default_region_id,
|
||||
},
|
||||
{ relations: ["countries"] }
|
||||
const [region] = await fetchDefaultRegion(
|
||||
store.default_region_id!,
|
||||
container
|
||||
)
|
||||
|
||||
if (!region) {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import type {
|
||||
CustomerDTO,
|
||||
ICustomerModuleService,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/framework/types"
|
||||
import { isDefined, Modules, validateEmail } from "@medusajs/framework/utils"
|
||||
import {
|
||||
isDefined,
|
||||
Modules,
|
||||
useCache,
|
||||
validateEmail,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
@@ -39,6 +45,40 @@ interface StepCompensateInput {
|
||||
customerWasCreated: boolean
|
||||
}
|
||||
|
||||
async function fetchCustomerById(
|
||||
customerId: string,
|
||||
container: MedusaContainer
|
||||
): Promise<CustomerDTO> {
|
||||
const service = container.resolve<ICustomerModuleService>(Modules.CUSTOMER)
|
||||
|
||||
return await useCache<CustomerDTO>(
|
||||
async () => service.retrieveCustomer(customerId),
|
||||
{
|
||||
container,
|
||||
key: ["find-or-create-customer-by-id", customerId],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchCustomersByEmail(
|
||||
email: string,
|
||||
container: MedusaContainer,
|
||||
hasAccount?: boolean
|
||||
): Promise<CustomerDTO[]> {
|
||||
const service = container.resolve<ICustomerModuleService>(Modules.CUSTOMER)
|
||||
|
||||
const filters =
|
||||
hasAccount !== undefined ? { email, has_account: hasAccount } : { email }
|
||||
|
||||
return await useCache<CustomerDTO[]>(
|
||||
async () => service.listCustomers(filters),
|
||||
{
|
||||
container,
|
||||
key: ["find-or-create-customer-by-email", filters],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const findOrCreateCustomerStepId = "find-or-create-customer"
|
||||
/**
|
||||
* This step finds or creates a customer based on the provided ID or email. It prioritizes finding the customer by ID, then by email.
|
||||
@@ -75,7 +115,7 @@ export const findOrCreateCustomerStep = createStep(
|
||||
let customerWasCreated = false
|
||||
|
||||
if (data.customerId) {
|
||||
originalCustomer = await service.retrieveCustomer(data.customerId)
|
||||
originalCustomer = await fetchCustomerById(data.customerId, container)
|
||||
customerData.customer = originalCustomer
|
||||
customerData.email = originalCustomer.email
|
||||
}
|
||||
@@ -85,9 +125,7 @@ export const findOrCreateCustomerStep = createStep(
|
||||
|
||||
let [customer] = originalCustomer
|
||||
? [originalCustomer]
|
||||
: await service.listCustomers({
|
||||
email: validatedEmail,
|
||||
})
|
||||
: await fetchCustomersByEmail(validatedEmail, container)
|
||||
|
||||
// if NOT a guest customer, return it
|
||||
if (customer?.has_account) {
|
||||
@@ -100,10 +138,11 @@ export const findOrCreateCustomerStep = createStep(
|
||||
}
|
||||
|
||||
if (customer && customer.email !== validatedEmail) {
|
||||
;[customer] = await service.listCustomers({
|
||||
email: validatedEmail,
|
||||
has_account: false,
|
||||
})
|
||||
;[customer] = await fetchCustomersByEmail(
|
||||
validatedEmail,
|
||||
container,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
if (!customer) {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import {
|
||||
ISalesChannelModuleService,
|
||||
IStoreModuleService,
|
||||
MedusaContainer,
|
||||
SalesChannelDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import { MedusaError, Modules, isDefined } from "@medusajs/framework/utils"
|
||||
import {
|
||||
MedusaError,
|
||||
Modules,
|
||||
isDefined,
|
||||
useCache,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
@@ -16,6 +22,34 @@ export interface FindSalesChannelStepInput {
|
||||
salesChannelId?: string | null
|
||||
}
|
||||
|
||||
async function fetchSalesChannel(
|
||||
salesChannelId: string,
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const salesChannelService = container.resolve<ISalesChannelModuleService>(
|
||||
Modules.SALES_CHANNEL
|
||||
)
|
||||
|
||||
return await useCache<
|
||||
Awaited<ReturnType<typeof salesChannelService.retrieveSalesChannel>>
|
||||
>(async () => salesChannelService.retrieveSalesChannel(salesChannelId), {
|
||||
container,
|
||||
key: ["find-sales-channel", salesChannelId],
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchStore(container: MedusaContainer) {
|
||||
const storeModule = container.resolve<IStoreModuleService>(Modules.STORE)
|
||||
return await useCache<Awaited<ReturnType<typeof storeModule.listStores>>>(
|
||||
async () =>
|
||||
storeModule.listStores(
|
||||
{},
|
||||
{ select: ["id", "default_sales_channel_id"] }
|
||||
),
|
||||
{ key: "find-sales-channel-default-store", container }
|
||||
)
|
||||
}
|
||||
|
||||
export const findSalesChannelStepId = "find-sales-channel"
|
||||
/**
|
||||
* This step either retrieves a sales channel either using the ID provided as an input, or, if no ID
|
||||
@@ -24,26 +58,17 @@ export const findSalesChannelStepId = "find-sales-channel"
|
||||
export const findSalesChannelStep = createStep(
|
||||
findSalesChannelStepId,
|
||||
async (data: FindSalesChannelStepInput, { container }) => {
|
||||
const salesChannelService = container.resolve<ISalesChannelModuleService>(
|
||||
Modules.SALES_CHANNEL
|
||||
)
|
||||
const storeModule = container.resolve<IStoreModuleService>(Modules.STORE)
|
||||
|
||||
let salesChannel: SalesChannelDTO | undefined
|
||||
|
||||
if (data.salesChannelId) {
|
||||
salesChannel = await salesChannelService.retrieveSalesChannel(
|
||||
data.salesChannelId
|
||||
)
|
||||
salesChannel = await fetchSalesChannel(data.salesChannelId, container)
|
||||
} else if (!isDefined(data.salesChannelId)) {
|
||||
const [store] = await storeModule.listStores(
|
||||
{},
|
||||
{ select: ["default_sales_channel_id"] }
|
||||
)
|
||||
const [store] = await fetchStore(container)
|
||||
|
||||
if (store?.default_sales_channel_id) {
|
||||
salesChannel = await salesChannelService.retrieveSalesChannel(
|
||||
store.default_sales_channel_id
|
||||
salesChannel = await fetchSalesChannel(
|
||||
store.default_sales_channel_id,
|
||||
container
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { IPromotionModuleService } from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
Modules,
|
||||
PromotionActions,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
@@ -72,9 +71,6 @@ export const getPromotionCodesToApply = createStep(
|
||||
async (data: GetPromotionCodesToApplyStepInput, { container }) => {
|
||||
const { promo_codes = [], cart, action = PromotionActions.ADD } = data
|
||||
const { items = [], shipping_methods = [] } = cart
|
||||
const promotionService = container.resolve<IPromotionModuleService>(
|
||||
Modules.PROMOTION
|
||||
)
|
||||
|
||||
const adjustmentCodes: string[] = []
|
||||
items.concat(shipping_methods).forEach((object) => {
|
||||
@@ -99,14 +95,23 @@ export const getPromotionCodesToApply = createStep(
|
||||
action === PromotionActions.ADD ||
|
||||
action === PromotionActions.REPLACE
|
||||
) {
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const validPromoCodes: Set<string> = new Set(
|
||||
promo_codes.length
|
||||
? (
|
||||
await promotionService.listPromotions(
|
||||
{ code: promo_codes },
|
||||
{ select: ["code"] }
|
||||
await query.graph(
|
||||
{
|
||||
entity: "promotion",
|
||||
fields: ["id", "code"],
|
||||
filters: { code: promo_codes },
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
).map((p) => p.code!)
|
||||
).data.map((p) => p.code!)
|
||||
: []
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Query } from "@medusajs/framework"
|
||||
import { MedusaContainer, Query } from "@medusajs/framework"
|
||||
import {
|
||||
CalculatedPriceSet,
|
||||
IPricingModuleService,
|
||||
@@ -75,11 +75,18 @@ async function fetchVariantPriceSets(
|
||||
variantIds: string[]
|
||||
): Promise<VariantPriceSetData[]> {
|
||||
return (
|
||||
await query.graph({
|
||||
entity: "variant",
|
||||
fields: ["id", "price_set.id"],
|
||||
filters: { id: variantIds },
|
||||
})
|
||||
await query.graph(
|
||||
{
|
||||
entity: "variant",
|
||||
fields: ["id", "price_set.id"],
|
||||
filters: { id: variantIds },
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
).data
|
||||
}
|
||||
|
||||
@@ -108,7 +115,8 @@ function validateVariantPriceSets(
|
||||
*/
|
||||
async function processVariantPriceSets(
|
||||
pricingService: IPricingModuleService,
|
||||
items: PriceCalculationItem[]
|
||||
items: PriceCalculationItem[],
|
||||
container: MedusaContainer
|
||||
): Promise<GetVariantPriceSetsStepOutput> {
|
||||
const result: GetVariantPriceSetsStepOutput = {}
|
||||
|
||||
@@ -298,7 +306,8 @@ export const getVariantPriceSetsStep = createStep(
|
||||
// Use unified processing logic for both input types
|
||||
const result = await processVariantPriceSets(
|
||||
pricingModuleService,
|
||||
calculationItems
|
||||
calculationItems,
|
||||
container
|
||||
)
|
||||
|
||||
return new StepResponse(result)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { IPromotionModuleService } from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
Modules,
|
||||
@@ -40,9 +39,6 @@ export const updateCartPromotionsStep = createStep(
|
||||
const remoteQuery = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
const promotionService = container.resolve<IPromotionModuleService>(
|
||||
Modules.PROMOTION
|
||||
)
|
||||
|
||||
const existingCartPromotionLinks = await remoteQuery({
|
||||
entryPoint: "cart_promotion",
|
||||
@@ -60,9 +56,18 @@ export const updateCartPromotionsStep = createStep(
|
||||
const linksToDismiss: any[] = []
|
||||
|
||||
if (promo_codes?.length) {
|
||||
const promotions = await promotionService.listPromotions(
|
||||
{ code: promo_codes },
|
||||
{ select: ["id"] }
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const { data: promotions } = await query.graph(
|
||||
{
|
||||
entity: "promotion",
|
||||
fields: ["id", "code"],
|
||||
filters: { code: promo_codes },
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for (const promotion of promotions) {
|
||||
|
||||
@@ -205,6 +205,11 @@ export const addToCartWorkflow = createWorkflow(
|
||||
filters: {
|
||||
id: variantIds,
|
||||
},
|
||||
options: {
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
},
|
||||
}).config({ name: "fetch-variants" })
|
||||
})
|
||||
|
||||
|
||||
@@ -142,6 +142,11 @@ export const getVariantsAndItemsWithPrices = createWorkflow(
|
||||
filters: {
|
||||
id: variantIds,
|
||||
},
|
||||
options: {
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
},
|
||||
}).config({ name: "fetch-variants" })
|
||||
|
||||
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||
|
||||
+30
-13
@@ -75,26 +75,26 @@ export const listShippingOptionsForCartWithPricingWorkflowId =
|
||||
* @summary
|
||||
*
|
||||
* List a cart's shipping options with prices.
|
||||
*
|
||||
*
|
||||
* @property hooks.setShippingOptionsContext - This hook is executed after the cart is retrieved and before the shipping options are queried. You can consume this hook to return any custom context useful for the shipping options retrieval.
|
||||
*
|
||||
* For example, you can consume the hook to add the customer Id to the context:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import { listShippingOptionsForCartWithPricingWorkflow } from "@medusajs/medusa/core-flows"
|
||||
* import { StepResponse } from "@medusajs/workflows-sdk"
|
||||
*
|
||||
*
|
||||
* listShippingOptionsForCartWithPricingWorkflow.hooks.setShippingOptionsContext(
|
||||
* async ({ cart }, { container }) => {
|
||||
*
|
||||
*
|
||||
* if (cart.customer_id) {
|
||||
* return new StepResponse({
|
||||
* customer_id: cart.customer_id,
|
||||
* })
|
||||
* }
|
||||
*
|
||||
*
|
||||
* const query = container.resolve("query")
|
||||
*
|
||||
*
|
||||
* const { data: carts } = await query.graph({
|
||||
* entity: "cart",
|
||||
* filters: {
|
||||
@@ -102,16 +102,16 @@ export const listShippingOptionsForCartWithPricingWorkflowId =
|
||||
* },
|
||||
* fields: ["customer_id"],
|
||||
* })
|
||||
*
|
||||
*
|
||||
* return new StepResponse({
|
||||
* customer_id: carts[0].customer_id,
|
||||
* })
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* The `customer_id` property will be added to the context along with other properties such as `is_return` and `enabled_in_store`.
|
||||
*
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
* You should also consume the `setShippingOptionsContext` hook in the {@link listShippingOptionsForCartWorkflow} workflow to ensure that the context is consistent when listing shipping options across workflows.
|
||||
@@ -120,7 +120,11 @@ export const listShippingOptionsForCartWithPricingWorkflowId =
|
||||
*/
|
||||
export const listShippingOptionsForCartWithPricingWorkflow = createWorkflow(
|
||||
listShippingOptionsForCartWithPricingWorkflowId,
|
||||
(input: WorkflowData<ListShippingOptionsForCartWithPricingWorkflowInput & AdditionalData>) => {
|
||||
(
|
||||
input: WorkflowData<
|
||||
ListShippingOptionsForCartWithPricingWorkflowInput & AdditionalData
|
||||
>
|
||||
) => {
|
||||
const optionIds = transform({ input }, ({ input }) =>
|
||||
(input.options ?? []).map(({ id }) => id)
|
||||
)
|
||||
@@ -155,6 +159,11 @@ export const listShippingOptionsForCartWithPricingWorkflow = createWorkflow(
|
||||
"stock_locations.address.*",
|
||||
"stock_locations.fulfillment_sets.id",
|
||||
],
|
||||
options: {
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
},
|
||||
}).config({ name: "sales_channels-fulfillment-query" })
|
||||
|
||||
const scFulfillmentSets = transform(
|
||||
@@ -193,13 +202,21 @@ export const listShippingOptionsForCartWithPricingWorkflow = createWorkflow(
|
||||
resultValidator: shippingOptionsContextResult,
|
||||
}
|
||||
)
|
||||
const setShippingOptionsContextResult = setShippingOptionsContext.getResult()
|
||||
const setShippingOptionsContextResult =
|
||||
setShippingOptionsContext.getResult()
|
||||
|
||||
const commonOptions = transform(
|
||||
{ input, cart, fulfillmentSetIds, setShippingOptionsContextResult },
|
||||
({ input, cart, fulfillmentSetIds, setShippingOptionsContextResult }) => ({
|
||||
({
|
||||
input,
|
||||
cart,
|
||||
fulfillmentSetIds,
|
||||
setShippingOptionsContextResult,
|
||||
}) => ({
|
||||
context: {
|
||||
...(setShippingOptionsContextResult ? setShippingOptionsContextResult : {}),
|
||||
...(setShippingOptionsContextResult
|
||||
? setShippingOptionsContextResult
|
||||
: {}),
|
||||
is_return: input.is_return ? "true" : "false",
|
||||
enabled_in_store: !isDefined(input.enabled_in_store)
|
||||
? "true"
|
||||
|
||||
@@ -168,6 +168,11 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
"stock_locations.name",
|
||||
"stock_locations.address.*",
|
||||
],
|
||||
options: {
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
},
|
||||
}).config({ name: "sales_channels-fulfillment-query" })
|
||||
|
||||
const scFulfillmentSets = transform(
|
||||
|
||||
@@ -148,6 +148,9 @@ export const updateCartWorkflow = createWorkflow(
|
||||
options: {
|
||||
throwIfKeyNotFound: true,
|
||||
isList: false,
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
},
|
||||
}).config({ name: "get-region" })
|
||||
|
||||
|
||||
@@ -247,6 +247,11 @@ export const createOrderWorkflow = createWorkflow(
|
||||
filters: {
|
||||
id: variantIdsWithoutCalculatedPrice,
|
||||
},
|
||||
options: {
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
},
|
||||
}).config({ name: "query-variants-without-calculated-price" })
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user