Files
medusa-store/packages/modules/caching/integration-tests/__tests__/invalidation.spec.ts
Adrien de Peretti d58462c9c9 chore(): Caching improvements (#13660)
* chore(): Caching improvements

* Create proud-windows-sleep.md

* naming

* fix tests

* address feedback
2025-10-03 10:05:44 +02:00

436 lines
12 KiB
TypeScript

import { Modules } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { ICachingModuleService } from "@medusajs/framework/types"
import { MedusaModule } from "@medusajs/framework/modules-sdk"
import { EventBusServiceMock } from "../__fixtures__/event-bus-mock"
jest.setTimeout(30000)
jest.spyOn(MedusaModule, "getAllJoinerConfigs").mockReturnValue([
{
schema: `
type Product {
id: ID
title: String
handle: String
status: String
type_id: String
collection_id: String
is_giftcard: Boolean
external_id: String
created_at: DateTime
updated_at: DateTime
variants: [ProductVariant]
sales_channels: [SalesChannel]
}
type ProductVariant {
id: ID
product_id: String
sku: String
prices: [Price]
}
type Price {
id: ID
amount: Float
currency_code: String
variant_id: String
}
type SalesChannel {
id: ID
is_disabled: Boolean
}
type ProductCollection {
id: ID
title: String
handle: String
}
`,
},
])
const mockEventBus = new EventBusServiceMock()
moduleIntegrationTestRunner<ICachingModuleService>({
moduleName: Modules.CACHING,
injectedDependencies: {
[Modules.EVENT_BUS]: mockEventBus,
},
moduleOptions: {
in_memory: {
enable: true,
},
},
testSuite: ({ service }) => {
describe("Cache Invalidation with Entity Relationships", () => {
afterEach(async () => {
await service.clear({ tags: ["*"] }).catch(() => {})
})
describe("Single Entity Caching", () => {
it("should cache and retrieve a single product entity using computed keys", async () => {
const product = {
id: "prod_1",
title: "Test Product",
handle: "test-product",
status: "published",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
const productKey = await service.computeKey(product)
await service.set({
key: productKey,
data: product,
})
const cachedProduct = await service.get({ key: productKey })
expect(cachedProduct).toEqual(product)
})
it("should auto-invalidate single entity when strategy clears computed tags", async () => {
const product = {
id: "prod_1",
title: "Test Product",
handle: "test-product",
}
const productKey = await service.computeKey(product)
await service.set({
key: productKey,
data: product,
})
await mockEventBus.emit(
[{ name: "product.updated", data: { id: product.id } }],
{}
)
const result = await service.get({ key: productKey })
expect(result).toBeNull()
})
it("should not auto-invalidate single entity with autoInvalidate=false", async () => {
const product = {
id: "prod_1",
title: "Test Product",
handle: "test-product",
}
const productKey = await service.computeKey(product)
await service.set({
key: productKey,
data: product,
options: { autoInvalidate: false },
})
await mockEventBus.emit(
[{ name: "product.updated", data: { id: product.id } }],
{}
)
const result = await service.get({ key: productKey })
expect(result).toEqual(product)
})
})
describe("Entity List Caching", () => {
it("should cache and retrieve lists of entities using computed keys", async () => {
const publishedProductsQuery = {
entity: "product",
filters: { status: "published" },
fields: ["id", "title", "status"],
}
const allProductsQuery = {
entity: "product",
filters: {},
fields: ["id", "title", "status"],
}
const publishedProducts = [
{ id: "prod_1", title: "Product 1", status: "published" },
{ id: "prod_2", title: "Product 2", status: "published" },
]
const allProducts = [
...publishedProducts,
{ id: "prod_3", title: "Product 3", status: "draft" },
]
const publishedProductsKey = await service.computeKey(
publishedProductsQuery
)
const allProductsKey = await service.computeKey(allProductsQuery)
await service.set({
key: publishedProductsKey,
data: publishedProducts,
})
await service.set({
key: allProductsKey,
data: allProducts,
})
const cachedPublished = await service.get({
key: publishedProductsKey,
})
const cachedAll = await service.get({ key: allProductsKey })
expect(cachedPublished).toEqual(publishedProducts)
expect(cachedAll).toEqual(allProducts)
})
it("should invalidate related lists when individual product is updated", async () => {
const listQuery = {
entity: "product",
filters: { status: "published" },
includes: ["id", "title"],
}
const products = [
{ id: "prod_1", title: "Product 1", status: "published" },
{ id: "prod_2", title: "Product 2", status: "published" },
]
const listKey = await service.computeKey(listQuery)
await service.set({
key: listKey,
data: products,
})
await mockEventBus.emit(
[
{
name: "product.updated",
data: { id: "prod_1", title: "Updated Product 1" },
},
],
{}
)
const cachedList = await service.get({ key: listKey })
expect(cachedList).toBeNull()
})
})
describe("Nested Entity Caching", () => {
it("should cache products with nested variants and prices using computed keys", async () => {
const productWithVariants = {
id: "prod_1",
title: "Complex Product",
variants: [
{
id: "var_1",
product_id: "prod_1",
sku: "SKU-001",
prices: [
{
id: "price_1",
variant_id: "var_1",
amount: 1000,
currency_code: "USD",
},
{
id: "price_2",
variant_id: "var_1",
amount: 900,
currency_code: "EUR",
},
],
},
{
id: "var_2",
product_id: "prod_1",
sku: "SKU-002",
prices: [
{
id: "price_3",
variant_id: "var_2",
amount: 1200,
currency_code: "USD",
},
],
},
],
}
const productKey = await service.computeKey(productWithVariants)
await service.set({
key: productKey,
data: productWithVariants,
})
const cached = await service.get<typeof productWithVariants>({
key: productKey,
})
expect(cached).toEqual(productWithVariants)
expect(cached!.variants).toHaveLength(2)
expect(cached!.variants[0].prices).toHaveLength(2)
})
it("should invalidate nested product when related variant is updated", async () => {
const productWithVariants = {
id: "prod_1",
title: "Complex Product",
variants: [{ id: "var_1", product_id: "prod_1", sku: "SKU-001" }],
}
const productKey = await service.computeKey(productWithVariants)
await service.set({
key: productKey,
data: productWithVariants,
})
await mockEventBus.emit(
[
{
name: "product_variant.updated",
data: {
id: "var_1",
product_id: "prod_1",
sku: "SKU-001-UPDATED",
},
},
],
{}
)
const result = await service.get({ key: productKey })
expect(result).toBeNull()
})
it("should handle price updates affecting variant and product caches", async () => {
const price = {
id: "price_1",
variant_id: "var_1",
amount: 1000,
currency_code: "USD",
}
const variant = {
id: "var_1",
product_id: "prod_1",
sku: "SKU-001",
prices: [price],
}
const product = {
id: "prod_1",
title: "Product",
variants: [variant],
}
const priceKey = await service.computeKey(price)
const variantKey = await service.computeKey(variant)
const productKey = await service.computeKey(product)
await service.set({
key: priceKey,
data: price,
})
await service.set({
key: variantKey,
data: variant,
})
await service.set({
key: productKey,
data: product,
})
await mockEventBus.emit(
[
{
name: "price.updated",
data: { id: "price_1", variant_id: "var_1", amount: 1100 },
},
],
{}
)
const cachedPrice = await service.get({ key: priceKey })
const cachedVariant = await service.get({ key: variantKey })
const cachedProduct = await service.get({ key: productKey })
expect(cachedPrice).toBeNull()
expect(cachedVariant).toBeNull()
expect(cachedProduct).toBeNull()
})
})
describe("Complex Query Caching", () => {
it("should cache complex queries and invalidate based on entity relationships", async () => {
const complexQuery = {
entity: "product",
filters: { status: "published", collection_id: "col_1" },
includes: {
variants: {
include: {
prices: true,
},
},
collection: true,
},
pagination: { limit: 10, offset: 0 },
}
const queryResult = {
data: [
{
id: "prod_1",
title: "Product 1",
collection_id: "col_1",
variants: [
{
id: "var_1",
product_id: "prod_1",
prices: [{ id: "price_1", amount: 1000 }],
},
],
collection: { id: "col_1", title: "Collection 1" },
},
],
pagination: { total: 1, limit: 10, offset: 0 },
}
const queryKey = await service.computeKey(complexQuery)
await service.set({
key: queryKey,
data: queryResult,
})
const cached = await service.get({ key: queryKey })
expect(cached).toEqual(queryResult)
await mockEventBus.emit(
[
{
name: "price.updated",
data: { id: "price_1", amount: 1100 },
},
],
{}
)
const cachedAfterUpdate = await service.get({ key: queryKey })
expect(cachedAfterUpdate).toBeNull()
})
})
})
},
})