* chore(): Caching improvements * Create proud-windows-sleep.md * naming * fix tests * address feedback
436 lines
12 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|
|
},
|
|
})
|