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:
1 parent
5b135a41fe
commit
b9d6f73320
117 files changed
+5741
-530
No files matched your search
@@ -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" })
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./config": "./dist/config/index.js",
|
||||
"./caching": "./dist/caching/index.js",
|
||||
"./logger": "./dist/logger/index.js",
|
||||
"./database": "./dist/database/index.js",
|
||||
"./subscribers": "./dist/subscribers/index.js",
|
||||
|
||||
@@ -30,20 +30,38 @@ export async function ensurePublishableApiKeyMiddleware(
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
try {
|
||||
const { data } = await query.graph({
|
||||
entity: "api_key",
|
||||
fields: ["id", "token", "sales_channels_link.sales_channel_id"],
|
||||
filters: {
|
||||
token: publishableApiKey,
|
||||
type: ApiKeyType.PUBLISHABLE,
|
||||
$or: [
|
||||
{ revoked_at: { $eq: null } },
|
||||
{ revoked_at: { $gt: new Date() } },
|
||||
// Cache API key data and check revocation in memory
|
||||
const { data } = await query.graph(
|
||||
{
|
||||
entity: "api_key",
|
||||
fields: [
|
||||
"id",
|
||||
"token",
|
||||
"revoked_at",
|
||||
"sales_channels_link.sales_channel_id",
|
||||
],
|
||||
filters: {
|
||||
token: publishableApiKey,
|
||||
type: ApiKeyType.PUBLISHABLE,
|
||||
},
|
||||
},
|
||||
})
|
||||
{
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
apiKey = data[0]
|
||||
if (data.length) {
|
||||
const now = new Date()
|
||||
const cachedApiKey = data[0]
|
||||
const isRevoked =
|
||||
!!cachedApiKey.revoked_at && new Date(cachedApiKey.revoked_at) <= now
|
||||
|
||||
if (!isRevoked) {
|
||||
apiKey = cachedApiKey
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return next(e)
|
||||
}
|
||||
|
||||
@@ -1,49 +1,88 @@
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
isString,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest } from "../types"
|
||||
import type {
|
||||
GraphResultSet,
|
||||
MedusaContainer,
|
||||
RemoteJoinerOptions,
|
||||
RemoteQueryEntryPoints,
|
||||
RemoteQueryFunctionReturnPagination,
|
||||
} from "../../types"
|
||||
import { ContainerRegistrationKeys, isString } from "../../utils"
|
||||
import type { MedusaRequest } from "../types"
|
||||
|
||||
export const refetchEntities = async (
|
||||
entryPoint: string,
|
||||
idOrFilter: string | object,
|
||||
scope: MedusaContainer,
|
||||
fields: string[],
|
||||
pagination?: MedusaRequest["queryConfig"]["pagination"],
|
||||
export const refetchEntities = async <TEntry extends string>({
|
||||
entity,
|
||||
idOrFilter,
|
||||
scope,
|
||||
fields,
|
||||
pagination,
|
||||
withDeleted,
|
||||
options,
|
||||
}: {
|
||||
entity: TEntry
|
||||
idOrFilter?: string | object
|
||||
scope: MedusaContainer
|
||||
fields?: string[]
|
||||
pagination?: MedusaRequest["queryConfig"]["pagination"]
|
||||
withDeleted?: boolean
|
||||
) => {
|
||||
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const filters = isString(idOrFilter) ? { id: idOrFilter } : idOrFilter
|
||||
let context: object = {}
|
||||
options?: RemoteJoinerOptions
|
||||
}): Promise<
|
||||
Omit<GraphResultSet<TEntry>, "metadata"> & {
|
||||
metadata: RemoteQueryFunctionReturnPagination
|
||||
}
|
||||
> => {
|
||||
const query = scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
let filters = isString(idOrFilter) ? { id: idOrFilter } : idOrFilter
|
||||
let context!: Record<string, unknown>
|
||||
|
||||
if ("context" in filters) {
|
||||
if (filters.context) {
|
||||
context = filters.context!
|
||||
if (filters && "context" in filters) {
|
||||
const { context: context_, ...rest } = filters
|
||||
if (context_) {
|
||||
context = context_! as Record<string, unknown>
|
||||
}
|
||||
|
||||
delete filters.context
|
||||
filters = rest
|
||||
}
|
||||
|
||||
const variables = { filters, ...context, ...pagination, withDeleted }
|
||||
const graphOptions: Parameters<typeof query.graph>[0] = {
|
||||
entity,
|
||||
fields: fields ?? [],
|
||||
filters,
|
||||
pagination,
|
||||
withDeleted,
|
||||
context: context,
|
||||
}
|
||||
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint,
|
||||
variables,
|
||||
const result = await query.graph(graphOptions, options)
|
||||
return {
|
||||
data: result.data as TEntry extends keyof RemoteQueryEntryPoints
|
||||
? RemoteQueryEntryPoints[TEntry][]
|
||||
: any[],
|
||||
metadata: result.metadata ?? ({} as RemoteQueryFunctionReturnPagination),
|
||||
}
|
||||
}
|
||||
|
||||
export const refetchEntity = async <TEntry extends string>({
|
||||
entity,
|
||||
idOrFilter,
|
||||
scope,
|
||||
fields,
|
||||
options,
|
||||
}: {
|
||||
entity: TEntry & string
|
||||
idOrFilter: string | object
|
||||
scope: MedusaContainer
|
||||
fields: string[]
|
||||
options?: RemoteJoinerOptions
|
||||
}): Promise<
|
||||
TEntry extends keyof RemoteQueryEntryPoints
|
||||
? RemoteQueryEntryPoints[TEntry]
|
||||
: any
|
||||
> => {
|
||||
const { data } = await refetchEntities<TEntry>({
|
||||
entity,
|
||||
idOrFilter,
|
||||
scope,
|
||||
fields,
|
||||
options,
|
||||
})
|
||||
|
||||
return await remoteQuery(queryObject)
|
||||
}
|
||||
|
||||
export const refetchEntity = async (
|
||||
entryPoint: string,
|
||||
idOrFilter: string | object,
|
||||
scope: MedusaContainer,
|
||||
fields: string[]
|
||||
) => {
|
||||
const [entity] = await refetchEntities(entryPoint, idOrFilter, scope, fields)
|
||||
|
||||
return entity
|
||||
return Array.isArray(data) ? data[0] : data
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IApiKeyModuleService,
|
||||
IAuthModuleService,
|
||||
ICacheService,
|
||||
ICachingModuleService,
|
||||
ICartModuleService,
|
||||
ICurrencyModuleService,
|
||||
ICustomerModuleService,
|
||||
@@ -76,6 +77,7 @@ declare module "@medusajs/types" {
|
||||
[Modules.NOTIFICATION]: INotificationModuleService
|
||||
[Modules.LOCKING]: ILockingModule
|
||||
[Modules.SETTINGS]: ISettingsModuleService
|
||||
[Modules.CACHING]: ICachingModuleService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ describe("load internal", () => {
|
||||
expect(generatedJoinerConfig).toEqual({
|
||||
serviceName: "module-without-joiner-config",
|
||||
primaryKeys: ["id"],
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {
|
||||
entity2_id: "Entity2",
|
||||
entity_model_id: "EntityModel",
|
||||
@@ -322,6 +323,7 @@ describe("load internal", () => {
|
||||
expect(generatedJoinerConfig).toEqual({
|
||||
serviceName: "module-service",
|
||||
primaryKeys: ["id"],
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {},
|
||||
schema: "",
|
||||
alias: [
|
||||
|
||||
@@ -231,7 +231,8 @@ export async function loadInternalModule(args: {
|
||||
ContainerRegistrationKeys.CONFIG_MODULE,
|
||||
ContainerRegistrationKeys.LOGGER,
|
||||
ContainerRegistrationKeys.PG_CONNECTION,
|
||||
Modules.EVENT_BUS
|
||||
Modules.EVENT_BUS,
|
||||
Modules.CACHING
|
||||
)
|
||||
|
||||
for (const dependency of dependencies) {
|
||||
|
||||
@@ -609,6 +609,7 @@ async function MedusaApp_({
|
||||
query: createQuery({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
container: sharedContainer_,
|
||||
}) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
|
||||
entitiesMap,
|
||||
gqlSchema: schema,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
GraphResultSet,
|
||||
IIndexService,
|
||||
MedusaContainer,
|
||||
RemoteJoinerOptions,
|
||||
RemoteJoinerQuery,
|
||||
RemoteQueryFilters,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
RemoteQueryObjectFromStringResult,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
Cached,
|
||||
MedusaError,
|
||||
isObject,
|
||||
remoteQueryObjectFromString,
|
||||
@@ -19,12 +21,62 @@ import {
|
||||
import { RemoteQuery } from "./remote-query"
|
||||
import { toRemoteQuery } from "./to-remote-query"
|
||||
|
||||
function extractCacheOptions(option: string) {
|
||||
return function extractKey(args: any[]) {
|
||||
return args[1]?.cache?.[option]
|
||||
}
|
||||
}
|
||||
|
||||
function isCacheEnabled(args: any[]) {
|
||||
const isEnabled = extractCacheOptions("enable")(args)
|
||||
if (isEnabled === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
isEnabled === true ||
|
||||
extractCacheOptions("key")(args) ||
|
||||
extractCacheOptions("ttl")(args) ||
|
||||
extractCacheOptions("tags")(args) ||
|
||||
extractCacheOptions("autoInvalidate")(args) ||
|
||||
extractCacheOptions("providers")(args)
|
||||
)
|
||||
}
|
||||
|
||||
const cacheDecoratorOptions = {
|
||||
enable: isCacheEnabled,
|
||||
key: async (args, cachingModule) => {
|
||||
const key = extractCacheOptions("key")(args)
|
||||
if (key) {
|
||||
return key
|
||||
}
|
||||
|
||||
const queryOptions = args[0]
|
||||
const remoteJoinerOptions = args[1] ?? {}
|
||||
const { initialData, cache, ...restOptions } = remoteJoinerOptions
|
||||
|
||||
const keyInput = {
|
||||
queryOptions,
|
||||
options: restOptions,
|
||||
}
|
||||
return await cachingModule.computeKey(keyInput)
|
||||
},
|
||||
ttl: extractCacheOptions("ttl"),
|
||||
tags: extractCacheOptions("tags"),
|
||||
autoInvalidate: extractCacheOptions("autoInvalidate"),
|
||||
providers: extractCacheOptions("providers"),
|
||||
container: function (this: Query) {
|
||||
return this.container
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* API wrapper around the remoteQuery
|
||||
*/
|
||||
export class Query {
|
||||
#remoteQuery: RemoteQuery
|
||||
#indexModule: IIndexService
|
||||
protected container: MedusaContainer
|
||||
|
||||
/**
|
||||
* Method to wrap execution of the graph query for instrumentation
|
||||
@@ -61,12 +113,15 @@ export class Query {
|
||||
constructor({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
container,
|
||||
}: {
|
||||
remoteQuery: RemoteQuery
|
||||
indexModule: IIndexService
|
||||
container: MedusaContainer
|
||||
}) {
|
||||
this.#remoteQuery = remoteQuery
|
||||
this.#indexModule = indexModule
|
||||
this.container = container
|
||||
}
|
||||
|
||||
#unwrapQueryConfig(
|
||||
@@ -151,6 +206,7 @@ export class Query {
|
||||
* Graph function uses the remoteQuery under the hood and
|
||||
* returns a result set
|
||||
*/
|
||||
@Cached(cacheDecoratorOptions)
|
||||
async graph<const TEntry extends string>(
|
||||
queryOptions: RemoteQueryInput<TEntry>,
|
||||
options?: RemoteJoinerOptions
|
||||
@@ -189,6 +245,7 @@ export class Query {
|
||||
* Index function uses the Index module to query and hydrates the data with query.graph
|
||||
* returns a result set
|
||||
*/
|
||||
@Cached(cacheDecoratorOptions)
|
||||
async index<const TEntry extends string>(
|
||||
queryOptions: RemoteQueryInput<TEntry> & {
|
||||
joinFilters?: RemoteQueryFilters<TEntry>
|
||||
@@ -266,13 +323,16 @@ export class Query {
|
||||
export function createQuery({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
container,
|
||||
}: {
|
||||
remoteQuery: RemoteQuery
|
||||
indexModule: IIndexService
|
||||
container: MedusaContainer
|
||||
}) {
|
||||
const query = new Query({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
container,
|
||||
})
|
||||
|
||||
function backwardCompatibleQuery(...args: any[]) {
|
||||
|
||||
@@ -69,8 +69,7 @@ export function toRemoteQuery<const TEntity extends string>(
|
||||
}
|
||||
|
||||
if (QueryContext.isQueryContext(src)) {
|
||||
const normalizedFilters = { ...src } as any
|
||||
delete normalizedFilters.__type
|
||||
const { __type, ...normalizedFilters } = src as any
|
||||
|
||||
const prop = "context"
|
||||
|
||||
@@ -100,7 +99,7 @@ export function toRemoteQuery<const TEntity extends string>(
|
||||
}
|
||||
|
||||
// Process filters and context recursively
|
||||
processNestedObjects(joinerQuery[entity], context)
|
||||
processNestedObjects(joinerQuery[entity], context, true)
|
||||
|
||||
for (const field of fields) {
|
||||
const fieldAsString = field as string
|
||||
|
||||
@@ -3,6 +3,7 @@ export * as AnalyticsTypes from "./analytics"
|
||||
export * as ApiKeyTypes from "./api-key"
|
||||
export * as AuthTypes from "./auth"
|
||||
export * as CacheTypes from "./cache"
|
||||
export * as CachingTypes from "./caching"
|
||||
export * as CartTypes from "./cart"
|
||||
export * as CommonTypes from "./common"
|
||||
export * as CurrencyTypes from "./currency"
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { IModuleService, ModuleJoinerConfig } from "../modules-sdk"
|
||||
|
||||
type Providers = string[] | { id: string; ttl?: number }[]
|
||||
|
||||
export interface ICachingModuleService extends IModuleService {
|
||||
// Static trace methods
|
||||
// traceGet: (
|
||||
// cacheGetFn: () => Promise<any>,
|
||||
// key: string,
|
||||
// tags: string[]
|
||||
// ) => Promise<any>
|
||||
|
||||
// traceSet?: (
|
||||
// cacheSetFn: () => Promise<any>,
|
||||
// key: string,
|
||||
// tags: string[],
|
||||
// options: { autoInvalidate?: boolean }
|
||||
// ) => Promise<any>
|
||||
|
||||
// traceClear?: (
|
||||
// cacheClearFn: () => Promise<any>,
|
||||
// key: string,
|
||||
// tags: string[],
|
||||
// options: { autoInvalidate?: boolean }
|
||||
// ) => Promise<any>
|
||||
|
||||
/**
|
||||
* This method retrieves data from the cache.
|
||||
*
|
||||
* @param key - The key of the item to retrieve.
|
||||
* @param tags - The tags of the items to retrieve.
|
||||
* @param providers - Array of providers to check in order of priority. If not provided,
|
||||
* only the default provider will be used.
|
||||
*
|
||||
* @returns The item(s) that was stored in the cache. If the item(s) was not found, null will
|
||||
* be returned.
|
||||
*
|
||||
*/
|
||||
get<T>({
|
||||
key,
|
||||
tags,
|
||||
providers,
|
||||
}: {
|
||||
key?: string
|
||||
tags?: string[]
|
||||
providers?: string[]
|
||||
}): Promise<T | null>
|
||||
|
||||
/**
|
||||
* This method stores data in the cache.
|
||||
*
|
||||
* @param key - The key of the item to store.
|
||||
* @param data - The data to store in the cache.
|
||||
* @param ttl - The time-to-live (TTL in seconds) value in seconds. If not provided, the default TTL value
|
||||
* is used. The default value is based on the used Cache Module.
|
||||
* @param tags - The tags of the items to store. can be used for cross invalidation.
|
||||
* @param options - if specified, will be stored with the item(s).
|
||||
* @param providers - The providers from which to store the item(s).
|
||||
*
|
||||
*/
|
||||
set({
|
||||
key,
|
||||
data,
|
||||
ttl,
|
||||
tags,
|
||||
options,
|
||||
providers,
|
||||
}: {
|
||||
key: string
|
||||
data: object
|
||||
ttl?: number
|
||||
tags?: string[]
|
||||
options?: {
|
||||
autoInvalidate?: boolean
|
||||
}
|
||||
providers?: Providers
|
||||
}): Promise<void>
|
||||
|
||||
/**
|
||||
* This method clears data from the cache.
|
||||
*
|
||||
* @param key - The key of the item to clear.
|
||||
* @param tags - The tags of the items to clear.
|
||||
* @param options - if specified, invalidate the item(s) that has the value of the given
|
||||
* options stored. e.g you can invalidate the tags X if their options.autoInvalidate is false or not present.
|
||||
* @param providers - The providers from which to clear the item(s).
|
||||
*
|
||||
*/
|
||||
clear({
|
||||
key,
|
||||
tags,
|
||||
options,
|
||||
providers,
|
||||
}: {
|
||||
key?: string
|
||||
tags?: string[]
|
||||
options?: {
|
||||
autoInvalidate?: boolean
|
||||
}
|
||||
providers?: string[]
|
||||
}): Promise<void>
|
||||
|
||||
computeKey(input: object): Promise<string>
|
||||
|
||||
computeTags(input: object, options?: Record<string, any>): Promise<string[]>
|
||||
}
|
||||
|
||||
export interface ICachingProviderService {
|
||||
get({ key, tags }: { key?: string; tags?: string[] }): Promise<any>
|
||||
set({
|
||||
key,
|
||||
data,
|
||||
ttl,
|
||||
tags,
|
||||
options,
|
||||
}: {
|
||||
key: string
|
||||
data: object
|
||||
ttl?: number
|
||||
tags?: string[]
|
||||
options?: { autoInvalidate?: boolean }
|
||||
}): Promise<void>
|
||||
clear({
|
||||
key,
|
||||
tags,
|
||||
options,
|
||||
}: {
|
||||
key?: string
|
||||
tags?: string[]
|
||||
options?: { autoInvalidate?: boolean }
|
||||
}): Promise<void>
|
||||
}
|
||||
|
||||
export interface EntityReference {
|
||||
type: string
|
||||
id: string | number
|
||||
field?: string
|
||||
}
|
||||
|
||||
export interface ICachingStrategy {
|
||||
/**
|
||||
* This method is called when the application starts. It can be useful to set up some auto
|
||||
* invalidation logic that reacts to something.
|
||||
*
|
||||
* @param container MedusaContainer
|
||||
* @param schema GraphQLSchema
|
||||
* @param cacheModule ICachingModuleService
|
||||
*/
|
||||
onApplicationStart?(
|
||||
schema: any,
|
||||
joinerConfigs: ModuleJoinerConfig[]
|
||||
): Promise<void>
|
||||
|
||||
onApplicationPrepareShutdown?(): Promise<void>
|
||||
|
||||
onApplicationShutdown?(): Promise<void>
|
||||
|
||||
computeKey(input: object): Promise<string>
|
||||
|
||||
computeTags(input: object, options?: Record<string, any>): Promise<string[]>
|
||||
}
|
||||
@@ -58,3 +58,8 @@ export type RawMessageFormat<TData = any> = {
|
||||
context?: Pick<Context, "eventGroupId">
|
||||
options?: Record<string, any>
|
||||
}
|
||||
|
||||
export type InterceptorSubscriber<T = unknown> = (
|
||||
message: Message<T>,
|
||||
context?: { isGrouped?: boolean; eventGroupId?: string }
|
||||
) => Promise<void> | void
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Message, Subscriber, SubscriberContext } from "./common"
|
||||
import {
|
||||
InterceptorSubscriber,
|
||||
Message,
|
||||
Subscriber,
|
||||
SubscriberContext,
|
||||
} from "./common"
|
||||
|
||||
export interface IEventBusModuleService {
|
||||
/**
|
||||
@@ -86,4 +91,31 @@ export interface IEventBusModuleService {
|
||||
eventNames?: string[]
|
||||
}
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* This method adds an interceptor to the event bus. This means that the interceptor will be
|
||||
* called before the event is emitted.
|
||||
*
|
||||
* @param interceptor - The interceptor to add.
|
||||
* @returns The instance of the Event Module
|
||||
*
|
||||
* @example
|
||||
* eventModuleService.addInterceptor((message, context) => {
|
||||
* console.log("Interceptor", message, context)
|
||||
* })
|
||||
*/
|
||||
addInterceptor?(interceptor: InterceptorSubscriber): this
|
||||
|
||||
/**
|
||||
* This method removes an interceptor from the event bus.
|
||||
*
|
||||
* @param interceptor - The interceptor to remove.
|
||||
* @returns The instance of the Event Module
|
||||
*
|
||||
* @example
|
||||
* eventModuleService.removeInterceptor((message, context) => {
|
||||
* console.log("Interceptor", message, context)
|
||||
* })
|
||||
*/
|
||||
removeInterceptor?(interceptor: InterceptorSubscriber): this
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export * from "./api-key"
|
||||
export * from "./auth"
|
||||
export * from "./bundles"
|
||||
export * from "./cache"
|
||||
export * from "./caching"
|
||||
export * from "./cart"
|
||||
export * from "./common"
|
||||
export * from "./currency"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ICachingModuleService } from "../caching"
|
||||
|
||||
export type JoinerRelationship = {
|
||||
alias: string
|
||||
foreignKey: string
|
||||
@@ -92,6 +94,40 @@ export interface RemoteJoinerOptions {
|
||||
throwIfRelationNotFound?: boolean | string[]
|
||||
initialData?: object | object[]
|
||||
initialDataOnly?: boolean
|
||||
cache?: {
|
||||
/**
|
||||
* Whether to enable the cache. This is only useful if you want to enable without providing any
|
||||
* other options or if you want to enable/disable the cache based on the arguments.
|
||||
*/
|
||||
enable?: boolean | ((args: any[]) => boolean | undefined)
|
||||
/**
|
||||
* The key to use for the cache.
|
||||
* If a function is provided, it will be called with the arguments as the first argument and the
|
||||
* container as the second argument.
|
||||
*/
|
||||
key?:
|
||||
| string
|
||||
| ((
|
||||
args: any[],
|
||||
cachingModule: ICachingModuleService
|
||||
) => string | Promise<string>)
|
||||
/**
|
||||
* The tags to use for the cache.
|
||||
*/
|
||||
tags?: string[] | ((args: any[]) => string[] | undefined)
|
||||
/**
|
||||
* The time-to-live (TTL) value in seconds.
|
||||
*/
|
||||
ttl?: number | ((args: any[]) => number | undefined)
|
||||
/**
|
||||
* Whether to auto invalidate the cache whenever it is possible.
|
||||
*/
|
||||
autoInvalidate?: boolean | ((args: any[]) => boolean | undefined)
|
||||
/**
|
||||
* The providers to use for the cache.
|
||||
*/
|
||||
providers?: string[] | ((args: any[]) => string[] | undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export interface RemoteNestedExpands {
|
||||
|
||||
@@ -196,6 +196,7 @@ export type ModuleJoinerConfig = Omit<
|
||||
* GraphQL schema for the all module's available entities and fields
|
||||
*/
|
||||
schema?: string
|
||||
idPrefixToEntityName?: Record<string, string>
|
||||
relationships?: ModuleJoinerRelationship[]
|
||||
extends?: {
|
||||
serviceName: string
|
||||
|
||||
@@ -48,9 +48,10 @@ export type QueryGraphFunction = {
|
||||
* a normalized/consistent output.
|
||||
*/
|
||||
export type QueryIndexFunction = {
|
||||
<const TEntry extends string>(queryOptions: IndexQueryInput<TEntry>): Promise<
|
||||
Prettify<QueryResultSet<TEntry>>
|
||||
>
|
||||
<const TEntry extends string>(
|
||||
queryOptions: IndexQueryInput<TEntry>,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<Prettify<QueryResultSet<TEntry>>>
|
||||
}
|
||||
|
||||
/*export type RemoteQueryReturnedData<TEntry extends string> =
|
||||
|
||||
@@ -132,7 +132,6 @@ export interface FilterablePriceRuleProps
|
||||
* The IDs to filter the price rule's associated price set.
|
||||
*/
|
||||
price_set_id?: string | string[] | OperatorMap<string | string[]>
|
||||
|
||||
/**
|
||||
* The IDs to filter the price rule's associated price.
|
||||
*/
|
||||
|
||||
@@ -18,3 +18,4 @@ export * as PromotionUtils from "./promotion"
|
||||
export * as SearchUtils from "./search"
|
||||
export * as ShippingProfileUtils from "./shipping"
|
||||
export * as UserUtils from "./user"
|
||||
export * as CachingUtils from "./caching"
|
||||
@@ -0,0 +1,249 @@
|
||||
import { ICachingModuleService, Logger, MedusaContainer } from "@medusajs/types"
|
||||
import { MedusaContextType, Modules } from "../modules-sdk"
|
||||
import { FeatureFlag } from "../feature-flags"
|
||||
import { ContainerRegistrationKeys, isObject } from "../common"
|
||||
|
||||
/**
|
||||
* This function is used to cache the result of a function call.
|
||||
*
|
||||
* @param cb - The callback to execute.
|
||||
* @param options - The options for the cache.
|
||||
* @returns The result of the callback.
|
||||
*/
|
||||
export async function useCache<T>(
|
||||
cb: (...args: any[]) => Promise<T>,
|
||||
options: {
|
||||
enable?: boolean
|
||||
key: string | any[]
|
||||
tags?: string[]
|
||||
ttl?: number
|
||||
/**
|
||||
* Whethere the default strategy should auto invalidate the cache whenever it is possible.
|
||||
*/
|
||||
autoInvalidate?: boolean
|
||||
providers?: string[]
|
||||
container: MedusaContainer
|
||||
}
|
||||
): Promise<T> {
|
||||
const cachingModule = options.container.resolve<ICachingModuleService>(
|
||||
Modules.CACHING,
|
||||
{
|
||||
allowUnregistered: true,
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
!options.enable ||
|
||||
!FeatureFlag.isFeatureEnabled("caching") ||
|
||||
!cachingModule
|
||||
) {
|
||||
return await cb()
|
||||
}
|
||||
|
||||
let key: string
|
||||
if (typeof options.key === "string") {
|
||||
key = options.key
|
||||
} else {
|
||||
key = await cachingModule.computeKey(options.key)
|
||||
}
|
||||
|
||||
const data = await cachingModule.get({
|
||||
key,
|
||||
tags: options.tags,
|
||||
providers: options.providers,
|
||||
})
|
||||
|
||||
if (data) {
|
||||
return data as T
|
||||
}
|
||||
|
||||
const result = await cb()
|
||||
|
||||
void cachingModule
|
||||
.set({
|
||||
key,
|
||||
tags: options.tags,
|
||||
ttl: options.ttl,
|
||||
data: result as object,
|
||||
options: { autoInvalidate: options.autoInvalidate },
|
||||
providers: options.providers,
|
||||
})
|
||||
.catch((e) => {
|
||||
const logger =
|
||||
options.container.resolve<Logger>(ContainerRegistrationKeys.LOGGER, {
|
||||
allowUnregistered: true,
|
||||
}) ?? (console as unknown as Logger)
|
||||
logger.error(
|
||||
`An error occured while setting cache for key: ${key}\n${e.message}\n${e.stack}`
|
||||
)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type TargetMethodArgs<Target, PropertyKey> = Target[PropertyKey &
|
||||
keyof Target] extends (...args: any[]) => any
|
||||
? Parameters<Target[PropertyKey & keyof Target]>
|
||||
: never
|
||||
|
||||
/**
|
||||
* This function is used to cache the result of a method call.
|
||||
*
|
||||
* @param options - The options for the cache.
|
||||
* @returns The original method with the cache applied.
|
||||
*/
|
||||
export function Cached<
|
||||
const Target extends object,
|
||||
const PropertyKey extends keyof Target
|
||||
>(options: {
|
||||
/**
|
||||
* The key to use for the cache.
|
||||
* If a function is provided, it will be called with the arguments as the first argument and the
|
||||
* container as the second argument.
|
||||
*/
|
||||
key?:
|
||||
| string
|
||||
| ((
|
||||
args: TargetMethodArgs<Target, PropertyKey>,
|
||||
cachingModule: ICachingModuleService
|
||||
) => string | Promise<string> | Promise<any[]> | any[])
|
||||
/**
|
||||
* Whether to enable the cache. This is only useful if you want to enable without providing any
|
||||
* other options.
|
||||
*/
|
||||
enable?:
|
||||
| boolean
|
||||
| ((args: TargetMethodArgs<Target, PropertyKey>) => boolean | undefined)
|
||||
/**
|
||||
* The tags to use for the cache.
|
||||
*/
|
||||
tags?:
|
||||
| string[]
|
||||
| ((args: TargetMethodArgs<Target, PropertyKey>) => string[] | undefined)
|
||||
/**
|
||||
* The time-to-live (TTL) value in seconds.
|
||||
*/
|
||||
ttl?:
|
||||
| number
|
||||
| ((args: TargetMethodArgs<Target, PropertyKey>) => number | undefined)
|
||||
/**
|
||||
* Whether to auto invalidate the cache whenever it is possible.
|
||||
*/
|
||||
autoInvalidate?:
|
||||
| boolean
|
||||
| ((args: TargetMethodArgs<Target, PropertyKey>) => boolean | undefined)
|
||||
/**
|
||||
* The providers to use for the cache.
|
||||
*/
|
||||
providers?:
|
||||
| string[]
|
||||
| ((args: TargetMethodArgs<Target, PropertyKey>) => string[] | undefined)
|
||||
|
||||
container: MedusaContainer | ((this: Target) => MedusaContainer)
|
||||
}) {
|
||||
return function (
|
||||
target: Target,
|
||||
propertyKey: PropertyKey,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
if (typeof originalMethod !== "function") {
|
||||
throw new Error("@cached can only be applied to methods")
|
||||
}
|
||||
|
||||
descriptor.value = async function (
|
||||
...args: Target[PropertyKey & keyof Target] extends (
|
||||
...args: any[]
|
||||
) => any
|
||||
? Parameters<Target[PropertyKey & keyof Target]>
|
||||
: never
|
||||
) {
|
||||
const container: MedusaContainer =
|
||||
typeof options.container === "function"
|
||||
? options.container.call(this)
|
||||
: options.container
|
||||
|
||||
const cachingModule = container.resolve<ICachingModuleService>(
|
||||
Modules.CACHING,
|
||||
{
|
||||
allowUnregistered: true,
|
||||
}
|
||||
)
|
||||
|
||||
if (!FeatureFlag.isFeatureEnabled("caching") || !cachingModule) {
|
||||
return await originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
if (!options.key) {
|
||||
options.key = await cachingModule.computeKey(
|
||||
args
|
||||
.map((arg) => {
|
||||
if (isObject(arg)) {
|
||||
// Prevent any container, manager, transactionManager, etc from being included in the key
|
||||
const {
|
||||
container,
|
||||
manager,
|
||||
transactionManager,
|
||||
__type,
|
||||
...rest
|
||||
} = arg as any
|
||||
if (__type === MedusaContextType) {
|
||||
return
|
||||
}
|
||||
return rest
|
||||
}
|
||||
return arg
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
}
|
||||
|
||||
const resolvableKeys = [
|
||||
"enable",
|
||||
"key",
|
||||
"tags",
|
||||
"ttl",
|
||||
"autoInvalidate",
|
||||
"providers",
|
||||
]
|
||||
|
||||
const cacheOptions = {} as Parameters<typeof useCache>[1]
|
||||
|
||||
const promises: Promise<any>[] = []
|
||||
for (const key of resolvableKeys) {
|
||||
if (typeof options[key] === "function") {
|
||||
const res = options[key](args, cachingModule)
|
||||
if (res instanceof Promise) {
|
||||
promises.push(
|
||||
res.then((value) => {
|
||||
cacheOptions[key] = value
|
||||
})
|
||||
)
|
||||
} else {
|
||||
cacheOptions[key] = res
|
||||
}
|
||||
} else {
|
||||
cacheOptions[key] = options[key]
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
if (!cacheOptions.enable) {
|
||||
return await originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
Object.assign(cacheOptions, {
|
||||
container,
|
||||
})
|
||||
|
||||
return await useCache(
|
||||
() => originalMethod.apply(this, args),
|
||||
cacheOptions as Parameters<typeof useCache>[1]
|
||||
)
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,22 @@ import { ContainerLike } from "@medusajs/types"
|
||||
|
||||
export function createContainerLike(obj): ContainerLike {
|
||||
return {
|
||||
resolve(key: string) {
|
||||
resolve(
|
||||
key: string,
|
||||
{
|
||||
allowUnregistered = false,
|
||||
}: {
|
||||
allowUnregistered?: boolean
|
||||
} = {}
|
||||
) {
|
||||
if (allowUnregistered) {
|
||||
try {
|
||||
return obj[key]
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return obj[key]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class IdProperty extends BaseProperty<string> {
|
||||
return !!value?.[IsIdProperty] || value?.dataType?.name === "id"
|
||||
}
|
||||
|
||||
protected dataType: {
|
||||
dataType: {
|
||||
name: "id"
|
||||
options: {
|
||||
prefix?: string
|
||||
|
||||
@@ -24,6 +24,10 @@ export class PrimaryKeyModifier<T, Schema extends PropertyType<T>>
|
||||
*/
|
||||
#schema: Schema
|
||||
|
||||
get schema() {
|
||||
return this.#schema
|
||||
}
|
||||
|
||||
constructor(schema: Schema) {
|
||||
this.#schema = schema
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { EventBusTypes, InternalModuleDeclaration } from "@medusajs/types"
|
||||
import {
|
||||
EventBusTypes,
|
||||
InterceptorSubscriber,
|
||||
InternalModuleDeclaration,
|
||||
} from "@medusajs/types"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
export abstract class AbstractEventBusModuleService
|
||||
@@ -11,6 +15,8 @@ export abstract class AbstractEventBusModuleService
|
||||
EventBusTypes.SubscriberDescriptor[]
|
||||
> = new Map()
|
||||
|
||||
protected interceptorSubscribers_: Set<InterceptorSubscriber> = new Set()
|
||||
|
||||
public get eventToSubscribersMap(): Map<
|
||||
string | symbol,
|
||||
EventBusTypes.SubscriberDescriptor[]
|
||||
@@ -134,6 +140,49 @@ export abstract class AbstractEventBusModuleService
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an interceptor subscriber that receives all messages before they are emitted
|
||||
*
|
||||
* @param interceptor - Function that receives messages before emission
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public addInterceptor(interceptor: InterceptorSubscriber): this {
|
||||
this.interceptorSubscribers_.add(interceptor)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an interceptor subscriber
|
||||
*
|
||||
* @param interceptor - Function to remove from interceptors
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public removeInterceptor(interceptor: InterceptorSubscriber): this {
|
||||
this.interceptorSubscribers_.delete(interceptor)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Call all interceptor subscribers with the message before emission
|
||||
* This should be called by implementations before emitting events
|
||||
*
|
||||
* @param message - The message to be intercepted
|
||||
* @param context - Optional context about the emission
|
||||
*/
|
||||
protected async callInterceptors<T = unknown>(
|
||||
message: EventBusTypes.Message<T>,
|
||||
context?: { isGrouped?: boolean; eventGroupId?: string }
|
||||
): Promise<void> {
|
||||
Array.from(this.interceptorSubscribers_).map(async (interceptor) => {
|
||||
try {
|
||||
await interceptor(message, context)
|
||||
} catch (error) {
|
||||
// Log error but don't stop other interceptors or the emission
|
||||
console.error("Error in event bus interceptor:", error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export * from "./build-event-messages"
|
||||
|
||||
@@ -29,6 +29,7 @@ export * from "./shipping"
|
||||
export * from "./totals"
|
||||
export * from "./totals/big-number"
|
||||
export * from "./user"
|
||||
export * from "./caching"
|
||||
|
||||
export const MedusaModuleType = Symbol.for("MedusaModule")
|
||||
export const MedusaModuleProviderType = Symbol.for("MedusaModuleProvider")
|
||||
@@ -48,6 +48,7 @@ describe("joiner-config-builder", () => {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKeys: ["id"],
|
||||
schema: "",
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {
|
||||
fulfillment_set_id: FulfillmentSet.name,
|
||||
shipping_option_id: ShippingOption.name,
|
||||
@@ -136,6 +137,7 @@ describe("joiner-config-builder", () => {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKeys: ["id"],
|
||||
schema: "",
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {},
|
||||
alias: [
|
||||
{
|
||||
@@ -176,6 +178,7 @@ describe("joiner-config-builder", () => {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKeys: ["id"],
|
||||
schema: "",
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {
|
||||
fulfillment_set_id: FulfillmentSet.name,
|
||||
shipping_option_id: ShippingOption.name,
|
||||
@@ -269,6 +272,7 @@ describe("joiner-config-builder", () => {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKeys: ["id"],
|
||||
schema: "",
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {},
|
||||
alias: [
|
||||
{
|
||||
@@ -300,6 +304,7 @@ describe("joiner-config-builder", () => {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKeys: ["id"],
|
||||
schema: "",
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {
|
||||
fulfillment_set_id: FulfillmentSet.name,
|
||||
},
|
||||
@@ -335,6 +340,7 @@ describe("joiner-config-builder", () => {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKeys: ["id"],
|
||||
schema: expect.any(String),
|
||||
idPrefixToEntityName: {},
|
||||
linkableKeys: {
|
||||
fulfillment_set_id: FulfillmentSet.name,
|
||||
shipping_option_id: ShippingOption.name,
|
||||
|
||||
@@ -27,6 +27,7 @@ export const Modules = {
|
||||
INDEX: "index",
|
||||
LOCKING: "locking",
|
||||
SETTINGS: "settings",
|
||||
CACHING: "caching",
|
||||
} as const
|
||||
|
||||
export const MODULE_PACKAGE_NAMES = {
|
||||
@@ -58,6 +59,7 @@ export const MODULE_PACKAGE_NAMES = {
|
||||
[Modules.INDEX]: "@medusajs/medusa/index-module",
|
||||
[Modules.LOCKING]: "@medusajs/medusa/locking",
|
||||
[Modules.SETTINGS]: "@medusajs/medusa/settings",
|
||||
[Modules.CACHING]: "@medusajs/caching",
|
||||
}
|
||||
|
||||
export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries(
|
||||
|
||||
@@ -46,8 +46,10 @@ export function defineJoinerConfig(
|
||||
models,
|
||||
linkableKeys,
|
||||
primaryKeys,
|
||||
idPrefixToEntityName,
|
||||
}: {
|
||||
alias?: JoinerServiceConfigAlias[]
|
||||
idPrefixToEntityName?: Record<string, string>
|
||||
schema?: string
|
||||
models?: DmlEntity<any, any>[] | { name: string }[]
|
||||
linkableKeys?: ModuleJoinerConfig["linkableKeys"]
|
||||
@@ -150,6 +152,12 @@ export function defineJoinerConfig(
|
||||
schema = toGraphQLSchema([...modelDefinitions.values()])
|
||||
}
|
||||
|
||||
if (!idPrefixToEntityName) {
|
||||
idPrefixToEntityName = buildIdPrefixToEntityNameFromDmlObjects([
|
||||
...modelDefinitions.values(),
|
||||
])
|
||||
}
|
||||
|
||||
const linkableKeysFromDml = buildLinkableKeysFromDmlObjects([
|
||||
...modelDefinitions.values(),
|
||||
])
|
||||
@@ -199,6 +207,7 @@ export function defineJoinerConfig(
|
||||
serviceName,
|
||||
primaryKeys,
|
||||
schema,
|
||||
idPrefixToEntityName,
|
||||
linkableKeys: linkableKeys,
|
||||
alias: [
|
||||
...[...(alias ?? ([] as any))].map((alias) => ({
|
||||
@@ -230,6 +239,31 @@ export function defineJoinerConfig(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the id prefix to entity name map from the DML objects
|
||||
* @param models
|
||||
*/
|
||||
export function buildIdPrefixToEntityNameFromDmlObjects(
|
||||
models: DmlEntity<any, any>[]
|
||||
): Record<string, string> {
|
||||
return models.reduce((acc, model) => {
|
||||
const id = model.parse().schema.id as
|
||||
| IdProperty
|
||||
| PrimaryKeyModifier<any, any>
|
||||
|
||||
if (
|
||||
PrimaryKeyModifier.isPrimaryKeyModifier(id) &&
|
||||
id.schema.dataType.options.prefix
|
||||
) {
|
||||
acc[id.schema.dataType.options.prefix] = model.name
|
||||
} else if (IdProperty.isIdProperty(id) && id.dataType.options.prefix) {
|
||||
acc[id.dataType.options.prefix] = model.name
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* From a set of DML objects, build the linkable keys
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Constructor, IDmlEntity, ModuleExports } from "@medusajs/types"
|
||||
import { DmlEntity } from "../dml"
|
||||
import {
|
||||
buildIdPrefixToEntityNameFromDmlObjects,
|
||||
buildLinkConfigFromLinkableKeys,
|
||||
buildLinkConfigFromModelObjects,
|
||||
defineJoinerConfig,
|
||||
@@ -53,6 +54,10 @@ export function Module<
|
||||
// TODO: Add support for non linkable modifier DML object to be skipped from the linkable generation
|
||||
|
||||
const linkableKeys = service.prototype.__joinerConfig().linkableKeys
|
||||
service.prototype.__joinerConfig().idPrefixToEntityName =
|
||||
buildIdPrefixToEntityNameFromDmlObjects(
|
||||
dmlObjects.map(([, model]) => model) as DmlEntity<any, any>[]
|
||||
)
|
||||
|
||||
if (dmlObjects.length) {
|
||||
linkable = buildLinkConfigFromModelObjects<ServiceName, ModelObjects>(
|
||||
|
||||
@@ -155,18 +155,25 @@ const getDataForComputation = async (
|
||||
query: Omit<RemoteQueryFunction, symbol>,
|
||||
data: { variant_ids: string[]; sales_channel_id?: string }
|
||||
) => {
|
||||
const { data: variantInventoryItems } = await query.graph({
|
||||
entity: "product_variant_inventory_items",
|
||||
fields: [
|
||||
"variant_id",
|
||||
"required_quantity",
|
||||
"variant.manage_inventory",
|
||||
"variant.allow_backorder",
|
||||
"inventory.*",
|
||||
"inventory.location_levels.*",
|
||||
],
|
||||
filters: { variant_id: data.variant_ids },
|
||||
})
|
||||
const { data: variantInventoryItems } = await query.graph(
|
||||
{
|
||||
entity: "product_variant_inventory_items",
|
||||
fields: [
|
||||
"variant_id",
|
||||
"required_quantity",
|
||||
"variant.manage_inventory",
|
||||
"variant.allow_backorder",
|
||||
"inventory.*",
|
||||
"inventory.location_levels.*",
|
||||
],
|
||||
filters: { variant_id: data.variant_ids },
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const variantInventoriesMap = new Map()
|
||||
variantInventoryItems.forEach((link) => {
|
||||
@@ -177,11 +184,21 @@ const getDataForComputation = async (
|
||||
|
||||
const locationIds = new Set<string>()
|
||||
if (data.sales_channel_id) {
|
||||
const { data: channelLocations } = await query.graph({
|
||||
entity: "sales_channel_locations",
|
||||
fields: ["stock_location_id"],
|
||||
filters: { sales_channel_id: data.sales_channel_id },
|
||||
})
|
||||
const { data: channelLocations } = await query.graph(
|
||||
{
|
||||
entity: "sales_channel_locations",
|
||||
fields: ["stock_location_id"],
|
||||
filters: { sales_channel_id: data.sales_channel_id },
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
tags: [
|
||||
`SalesChannel:${data.sales_channel_id}`,
|
||||
"StockLocation:list:*",
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
channelLocations.forEach((loc) => locationIds.add(loc.stock_location_id))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user