feat(index): Index module foundation (#9095)

**What**
Index module foundation

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2024-09-18 21:04:04 +02:00
committed by GitHub
parent 3cfcd075ae
commit 58167b5dfa
53 changed files with 4796 additions and 1201 deletions

View File

@@ -0,0 +1,747 @@
import {
MedusaAppLoader,
configLoader,
container,
logger,
} from "@medusajs/framework"
import { MedusaAppOutput, MedusaModule } from "@medusajs/modules-sdk"
import { EventBusTypes } from "@medusajs/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
Modules,
} from "@medusajs/utils"
import { EntityManager } from "@mikro-orm/postgresql"
import { IndexData, IndexRelation } from "@models"
import { asValue } from "awilix"
import { dbTestUtilFactory } from "medusa-test-utils"
import { initDb } from "medusa-test-utils/dist/medusa-test-runner-utils/use-db"
import * as path from "path"
import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
const eventBusMock = new EventBusServiceMock()
const remoteQueryMock = jest.fn()
const dbUtils = dbTestUtilFactory()
jest.setTimeout(300000)
const productId = "prod_1"
const variantId = "var_1"
const priceSetId = "price_set_1"
const priceId = "money_amount_1"
const linkId = "link_id_1"
const sendEvents = async (eventDataToEmit) => {
let a = 0
remoteQueryMock.mockImplementation((query) => {
query = query.__value
if (query.product) {
return {
id: a++ > 0 ? "aaaa" : productId,
}
} else if (query.product_variant) {
return {
id: variantId,
sku: "aaa test aaa",
product: {
id: productId,
},
}
} else if (query.price_set) {
return {
id: priceSetId,
}
} else if (query.price) {
return {
id: priceId,
amount: 100,
price_set: [
{
id: priceSetId,
},
],
}
} else if (query.product_variant_price_set) {
return {
id: linkId,
variant_id: variantId,
price_set_id: priceSetId,
variant: [
{
id: variantId,
},
],
}
}
return {}
})
await eventBusMock.emit(eventDataToEmit)
}
let isFirstTime = true
let medusaAppLoader!: MedusaAppLoader
const beforeAll_ = async () => {
try {
configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config.js")
console.log(`Creating database ${dbName}`)
await dbUtils.create(dbName)
dbUtils.pgConnection_ = await initDb()
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(null),
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
})
medusaAppLoader = new MedusaAppLoader(container as any)
// Migrations
await medusaAppLoader.runModulesMigrations()
const linkPlanner = await medusaAppLoader.getLinksExecutionPlanner()
const plan = await linkPlanner.createPlan()
await linkPlanner.executePlan(plan)
// Clear partially loaded instances
MedusaModule.clearInstances()
// Bootstrap modules
const globalApp = await medusaAppLoader.load()
const index = container.resolve(Modules.INDEX)
// Mock event bus the index module
;(index as any).eventBusModuleService_ = eventBusMock
await globalApp.onApplicationStart()
jest
.spyOn((index as any).storageProvider_, "remoteQuery_")
.mockImplementation(remoteQueryMock)
return globalApp
} catch (error) {
console.error("Error initializing", error?.message)
throw error
}
}
const beforeEach_ = async (eventDataToEmit) => {
jest.clearAllMocks()
if (isFirstTime) {
isFirstTime = false
await sendEvents(eventDataToEmit)
return
}
try {
await medusaAppLoader.runModulesLoader()
await sendEvents(eventDataToEmit)
} catch (error) {
console.error("Error runner modules loaders", error?.message)
throw error
}
}
const afterEach_ = async () => {
try {
await dbUtils.teardown({ schema: "public" })
} catch (error) {
console.error("Error tearing down database:", error?.message)
throw error
}
}
describe("IndexModuleService", function () {
let medusaApp: MedusaAppOutput
let onApplicationPrepareShutdown!: () => Promise<void>
let onApplicationShutdown!: () => Promise<void>
beforeAll(async () => {
medusaApp = await beforeAll_()
onApplicationPrepareShutdown = medusaApp.onApplicationPrepareShutdown
onApplicationShutdown = medusaApp.onApplicationShutdown
})
afterAll(async () => {
await onApplicationPrepareShutdown()
await onApplicationShutdown()
await dbUtils.shutdown(dbName)
})
describe("on created or attached events", function () {
let manager
const eventDataToEmit: EventBusTypes.Event[] = [
{
name: "product.created",
data: {
id: productId,
},
},
{
name: "product.created",
data: {
id: "PRODUCTASDASDAS",
},
},
{
name: "variant.created",
data: {
id: variantId,
product: {
id: productId,
},
},
},
{
name: "PriceSet.created",
data: {
id: priceSetId,
},
},
{
name: "price.created",
data: {
id: priceId,
price_set: {
id: priceSetId,
},
},
},
{
name: "LinkProductVariantPriceSet.attached",
data: {
id: linkId,
variant_id: variantId,
price_set_id: priceSetId,
},
},
]
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
})
afterEach(afterEach_)
it("should create the corresponding index entries and index relation entries", async function () {
expect(remoteQueryMock).toHaveBeenCalledTimes(6)
/**
* Validate all index entries and index relation entries
*/
const indexEntries: IndexData[] = await manager.find(IndexData, {})
const productIndexEntries = indexEntries.filter((entry) => {
return entry.name === "Product"
})
expect(productIndexEntries).toHaveLength(2)
const variantIndexEntries = indexEntries.filter((entry) => {
return entry.name === "ProductVariant"
})
expect(variantIndexEntries).toHaveLength(1)
const priceSetIndexEntries = indexEntries.filter((entry) => {
return entry.name === "PriceSet"
})
expect(priceSetIndexEntries).toHaveLength(1)
const priceIndexEntries = indexEntries.filter((entry) => {
return entry.name === "Price"
})
expect(priceIndexEntries).toHaveLength(1)
const linkIndexEntries = indexEntries.filter((entry) => {
return entry.name === "LinkProductVariantPriceSet"
})
expect(linkIndexEntries).toHaveLength(1)
const indexRelationEntries: IndexRelation[] = await manager.find(
IndexRelation,
{}
)
expect(indexRelationEntries).toHaveLength(4)
const productVariantIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === productId &&
entry.parent_name === "Product" &&
entry.child_id === variantId &&
entry.child_name === "ProductVariant"
)
}
)
expect(productVariantIndexRelationEntries).toHaveLength(1)
const variantLinkIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === variantId &&
entry.parent_name === "ProductVariant" &&
entry.child_id === linkId &&
entry.child_name === "LinkProductVariantPriceSet"
)
}
)
expect(variantLinkIndexRelationEntries).toHaveLength(1)
const linkPriceSetIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === linkId &&
entry.parent_name === "LinkProductVariantPriceSet" &&
entry.child_id === priceSetId &&
entry.child_name === "PriceSet"
)
}
)
expect(linkPriceSetIndexRelationEntries).toHaveLength(1)
const priceSetPriceIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === priceSetId &&
entry.parent_name === "PriceSet" &&
entry.child_id === priceId &&
entry.child_name === "Price"
)
}
)
expect(priceSetPriceIndexRelationEntries).toHaveLength(1)
})
})
describe("on unordered created or attached events", function () {
let manager
const eventDataToEmit: EventBusTypes.Event[] = [
{
name: "variant.created",
data: {
id: variantId,
product: {
id: productId,
},
},
},
{
name: "product.created",
data: {
id: productId,
},
},
{
name: "product.created",
data: {
id: "PRODUCTASDASDAS",
},
},
{
name: "PriceSet.created",
data: {
id: priceSetId,
},
},
{
name: "price.created",
data: {
id: priceId,
price_set: {
id: priceSetId,
},
},
},
{
name: "LinkProductVariantPriceSet.attached",
data: {
id: linkId,
variant_id: variantId,
price_set_id: priceSetId,
},
},
]
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
})
afterEach(afterEach_)
it("should create the corresponding index entries and index relation entries", async function () {
expect(remoteQueryMock).toHaveBeenCalledTimes(6)
/**
* Validate all index entries and index relation entries
*/
const indexEntries: IndexData[] = await manager.find(IndexData, {})
const productIndexEntries = indexEntries.filter((entry) => {
return entry.name === "Product"
})
expect(productIndexEntries).toHaveLength(2)
expect(productIndexEntries[0].id).toEqual(productId)
const variantIndexEntries = indexEntries.filter((entry) => {
return entry.name === "ProductVariant"
})
expect(variantIndexEntries).toHaveLength(1)
expect(variantIndexEntries[0].id).toEqual(variantId)
const priceSetIndexEntries = indexEntries.filter((entry) => {
return entry.name === "PriceSet"
})
expect(priceSetIndexEntries).toHaveLength(1)
expect(priceSetIndexEntries[0].id).toEqual(priceSetId)
const priceIndexEntries = indexEntries.filter((entry) => {
return entry.name === "Price"
})
expect(priceIndexEntries).toHaveLength(1)
expect(priceIndexEntries[0].id).toEqual(priceId)
const linkIndexEntries = indexEntries.filter((entry) => {
return entry.name === "LinkProductVariantPriceSet"
})
expect(linkIndexEntries).toHaveLength(1)
expect(linkIndexEntries[0].id).toEqual(linkId)
const indexRelationEntries: IndexRelation[] = await manager.find(
IndexRelation,
{}
)
expect(indexRelationEntries).toHaveLength(4)
const productVariantIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === productId &&
entry.parent_name === "Product" &&
entry.child_id === variantId &&
entry.child_name === "ProductVariant"
)
}
)
expect(productVariantIndexRelationEntries).toHaveLength(1)
const variantLinkIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === variantId &&
entry.parent_name === "ProductVariant" &&
entry.child_id === linkId &&
entry.child_name === "LinkProductVariantPriceSet"
)
}
)
expect(variantLinkIndexRelationEntries).toHaveLength(1)
const linkPriceSetIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === linkId &&
entry.parent_name === "LinkProductVariantPriceSet" &&
entry.child_id === priceSetId &&
entry.child_name === "PriceSet"
)
}
)
expect(linkPriceSetIndexRelationEntries).toHaveLength(1)
const priceSetPriceIndexRelationEntries = indexRelationEntries.filter(
(entry) => {
return (
entry.parent_id === priceSetId &&
entry.parent_name === "PriceSet" &&
entry.child_id === priceId &&
entry.child_name === "Price"
)
}
)
expect(priceSetPriceIndexRelationEntries).toHaveLength(1)
})
})
describe("on updated events", function () {
let manager
const updateData = async (manager) => {
const indexRepository = manager.getRepository(IndexData)
await indexRepository.upsertMany([
{
id: productId,
name: "Product",
data: {
id: productId,
},
},
{
id: variantId,
name: "ProductVariant",
data: {
id: variantId,
sku: "aaa test aaa",
product: {
id: productId,
},
},
},
])
}
const eventDataToEmit: EventBusTypes.Event[] = [
{
name: "product.updated",
data: {
id: productId,
},
},
{
name: "variant.updated",
data: {
id: variantId,
product: {
id: productId,
},
},
},
]
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
await updateData(manager)
remoteQueryMock.mockImplementation((query) => {
query = query.__value
if (query.product) {
return {
id: productId,
title: "updated Title",
}
} else if (query.product_variant) {
return {
id: variantId,
sku: "updated sku",
product: [
{
id: productId,
},
],
}
}
return {}
})
await eventBusMock.emit(eventDataToEmit)
})
afterEach(afterEach_)
it("should update the corresponding index entries", async () => {
expect(remoteQueryMock).toHaveBeenCalledTimes(4)
const updatedIndexEntries = await manager.find(IndexData, {})
expect(updatedIndexEntries).toHaveLength(2)
const productEntry = updatedIndexEntries.find((entry) => {
return entry.name === "Product" && entry.id === productId
})
expect(productEntry?.data?.title).toEqual("updated Title")
const variantEntry = updatedIndexEntries.find((entry) => {
return entry.name === "ProductVariant" && entry.id === variantId
})
expect(variantEntry?.data?.sku).toEqual("updated sku")
})
})
describe("on deleted events", function () {
let manager
const eventDataToEmit: EventBusTypes.Event[] = [
{
name: "product.created",
data: {
id: productId,
},
},
{
name: "variant.created",
data: {
id: variantId,
product: {
id: productId,
},
},
},
{
name: "PriceSet.created",
data: {
id: priceSetId,
},
},
{
name: "price.created",
data: {
id: priceId,
price_set: {
id: priceSetId,
},
},
},
{
name: "LinkProductVariantPriceSet.attached",
data: {
id: linkId,
variant_id: variantId,
price_set_id: priceSetId,
},
},
]
const deleteEventDataToEmit: EventBusTypes.Event[] = [
{
name: "product.deleted",
data: {
id: productId,
},
},
{
name: "variant.deleted",
data: {
id: variantId,
},
},
]
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
remoteQueryMock.mockImplementation((query) => {
query = query.__value
if (query.product) {
return {
id: productId,
}
} else if (query.product_variant) {
return {
id: variantId,
product: [
{
id: productId,
},
],
}
}
return {}
})
await eventBusMock.emit(deleteEventDataToEmit)
})
afterEach(afterEach_)
it("should consume all deleted events and delete the index entries", async () => {
expect(remoteQueryMock).toHaveBeenCalledTimes(7)
const indexEntries = await manager.find(IndexData, {})
const indexRelationEntries = await manager.find(IndexRelation, {})
expect(indexEntries).toHaveLength(3)
expect(indexRelationEntries).toHaveLength(2)
const linkIndexEntry = indexEntries.find((entry) => {
return (
entry.name === "LinkProductVariantPriceSet" && entry.id === linkId
)
})!
const priceSetIndexEntry = indexEntries.find((entry) => {
return entry.name === "PriceSet" && entry.id === priceSetId
})!
const priceIndexEntry = indexEntries.find((entry) => {
return entry.name === "Price" && entry.id === priceId
})!
const linkPriceSetIndexRelationEntry = indexRelationEntries.find(
(entry) => {
return (
entry.parent_id === linkId &&
entry.parent_name === "LinkProductVariantPriceSet" &&
entry.child_id === priceSetId &&
entry.child_name === "PriceSet"
)
}
)!
expect(linkPriceSetIndexRelationEntry.parent).toEqual(linkIndexEntry)
expect(linkPriceSetIndexRelationEntry.child).toEqual(priceSetIndexEntry)
const priceSetPriceIndexRelationEntry = indexRelationEntries.find(
(entry) => {
return (
entry.parent_id === priceSetId &&
entry.parent_name === "PriceSet" &&
entry.child_id === priceId &&
entry.child_name === "Price"
)
}
)!
expect(priceSetPriceIndexRelationEntry.parent).toEqual(priceSetIndexEntry)
expect(priceSetPriceIndexRelationEntry.child).toEqual(priceIndexEntry)
})
})
})