feat(index): full sync operations (#11178)

Closes: FRMW-2892, FRMW-2893

**What**
Wired up the building block that we merged previously in order to manage data synchronization. The flow is as follow
- On application start
  - Build schema object representation from configuration
  - Check configuration changes
    - if new entities configured
      - Data synchronizer initialize orchestrator and start sync
        - for each entity
          - acquire lock
          - mark existing data as staled
          - sync all data by batch
          - marked them not staled anymore
          - acknowledge each processed batch and renew lock
          - update metadata with last synced cursor for entity X
          - release lock
      - remove all remaining staled data
    - if any entities removed from last configuration
      - remove the index data and relations

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-02-05 16:49:18 +00:00
committed by GitHub
co-authored by Carlos R. L. Rodrigues
parent 60f46e07fd
commit a33aebd895
35 changed files with 1677 additions and 727 deletions
@@ -1,4 +1,8 @@
const { defineConfig, Modules } = require("@medusajs/framework/utils")
const {
defineConfig,
Modules,
ContainerRegistrationKeys,
} = require("@medusajs/framework/utils")
const { schema } = require("./schema")
export const dbName = "medusa-index-integration-2024"
@@ -26,13 +30,19 @@ Object.keys(config.modules).forEach((key) => {
config.modules[Modules.INDEX] = {
resolve: "@medusajs/index",
dependencies: [Modules.EVENT_BUS],
dependencies: [
Modules.EVENT_BUS,
Modules.LOCKING,
ContainerRegistrationKeys.REMOTE_QUERY,
ContainerRegistrationKeys.QUERY,
],
options: {
schema,
},
}
config.modules[Modules.PRODUCT] = true
config.modules[Modules.LOCKING] = true
config.modules[Modules.PRICING] = true
export default config
@@ -10,12 +10,6 @@ export const updateRemovedSchema = `
id: String
product_id: String
sku: String
prices: [Price]
description: String
}
type Price @Listeners(values: ["price.created", "price.updated", "price.deleted"]) {
amount: Float
currency_code: String
}
`
@@ -5,13 +5,10 @@ import {
MedusaAppLoader,
} from "@medusajs/framework"
import { MedusaAppOutput, MedusaModule } from "@medusajs/framework/modules-sdk"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
Modules,
} from "@medusajs/framework/utils"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
import { initDb, TestDatabaseUtils } from "@medusajs/test-utils"
import { EntityManager } from "@mikro-orm/postgresql"
import { IndexTypes, ModulesSdkTypes } from "@medusajs/types"
import { Configuration } from "@utils"
import { asValue } from "awilix"
import path from "path"
import { setTimeout } from "timers/promises"
@@ -21,9 +18,9 @@ import { updateRemovedSchema } from "../__fixtures__/update-removed-schema"
import { updatedSchema } from "../__fixtures__/updated-schema"
const eventBusMock = new EventBusServiceMock()
const queryMock = jest.fn().mockReturnValue({
graph: jest.fn(),
})
const queryMock = {
graph: jest.fn().mockImplementation(async () => ({ data: [] })),
}
const dbUtils = TestDatabaseUtils.dbTestUtilFactory()
@@ -31,6 +28,7 @@ jest.setTimeout(300000)
let isFirstTime = true
let medusaAppLoader!: MedusaAppLoader
let index: IndexTypes.IIndexService
const beforeAll_ = async () => {
try {
@@ -45,11 +43,10 @@ const beforeAll_ = async () => {
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
[ContainerRegistrationKeys.QUERY]: asValue(null),
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
})
medusaAppLoader = new MedusaAppLoader(container as any)
medusaAppLoader = new MedusaAppLoader()
// Migrations
await medusaAppLoader.runModulesMigrations()
@@ -62,14 +59,16 @@ const beforeAll_ = async () => {
// Bootstrap modules
const globalApp = await medusaAppLoader.load()
container.register({
[ContainerRegistrationKeys.QUERY]: asValue(queryMock),
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(queryMock),
[Modules.EVENT_BUS]: asValue(eventBusMock),
})
const index = container.resolve(Modules.INDEX)
// Mock event bus the index module
;(index as any).eventBusModuleService_ = eventBusMock
index = container.resolve(Modules.INDEX)
await globalApp.onApplicationStart()
;(index as any).storageProvider_.query_ = queryMock
await setTimeout(1000)
return globalApp
} catch (error) {
@@ -105,8 +104,9 @@ const afterEach_ = async () => {
describe("IndexModuleService syncIndexConfig", function () {
let medusaApp: MedusaAppOutput
let module: any
let manager: EntityManager
let indexMetadataService: ModulesSdkTypes.IMedusaInternalService<any>
let indexSyncService: ModulesSdkTypes.IMedusaInternalService<any>
let dataSynchronizer: ModulesSdkTypes.IMedusaInternalService<any>
let onApplicationPrepareShutdown!: () => Promise<void>
let onApplicationShutdown!: () => Promise<void>
@@ -125,8 +125,10 @@ describe("IndexModuleService syncIndexConfig", function () {
beforeEach(async () => {
await beforeEach_()
module = medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX)
manager = module.container_.manager as EntityManager
index = container.resolve(Modules.INDEX)
indexMetadataService = (index as any).indexMetadataService_
indexSyncService = (index as any).indexSyncService_
dataSynchronizer = (index as any).dataSynchronizer_
})
afterEach(afterEach_)
@@ -134,7 +136,7 @@ describe("IndexModuleService syncIndexConfig", function () {
it("should full sync all entities when the config has changed", async () => {
await setTimeout(1000)
const currentMetadata = await module.listIndexMetadata()
const currentMetadata = await indexMetadataService.list()
expect(currentMetadata).toHaveLength(7)
expect(currentMetadata).toEqual(
@@ -177,13 +179,25 @@ describe("IndexModuleService syncIndexConfig", function () {
])
)
// update config schema
module.schemaObjectRepresentation_ = null
module.moduleOptions_ ??= {}
module.moduleOptions_.schema = updatedSchema
module.buildSchemaObjectRepresentation_()
let indexSync = await indexSyncService.list({
last_key: null,
})
expect(indexSync).toHaveLength(7)
const syncRequired = await module.syncIndexConfig()
// update config schema
;(index as any).schemaObjectRepresentation_ = null
;(index as any).moduleOptions_ ??= {}
;(index as any).moduleOptions_.schema = updatedSchema
;(index as any).buildSchemaObjectRepresentation_()
let configurationChecker = new Configuration({
schemaObjectRepresentation: (index as any).schemaObjectRepresentation_,
indexMetadataService,
indexSyncService,
dataSynchronizer,
})
const syncRequired = await configurationChecker.checkChanges()
expect(syncRequired).toHaveLength(2)
expect(syncRequired).toEqual(
@@ -201,7 +215,12 @@ describe("IndexModuleService syncIndexConfig", function () {
])
)
const updatedMetadata = await module.listIndexMetadata()
indexSync = await indexSyncService.list({
last_key: null,
})
expect(indexSync).toHaveLength(7)
const updatedMetadata = await indexMetadataService.list()
expect(updatedMetadata).toHaveLength(7)
expect(updatedMetadata).toEqual(
@@ -243,15 +262,28 @@ describe("IndexModuleService syncIndexConfig", function () {
}),
])
)
await module.syncEntities(syncRequired)
await (index as any).dataSynchronizer_.syncEntities(syncRequired)
// Sync again removing entities not linked
module.schemaObjectRepresentation_ = null
module.moduleOptions_ ??= {}
module.moduleOptions_.schema = updateRemovedSchema
module.buildSchemaObjectRepresentation_()
;(index as any).schemaObjectRepresentation_ = null
;(index as any).moduleOptions_ ??= {}
;(index as any).moduleOptions_.schema = updateRemovedSchema
;(index as any).buildSchemaObjectRepresentation_()
const syncRequired2 = await module.syncIndexConfig()
const spyDataSynchronizer_ = jest.spyOn(
(index as any).dataSynchronizer_,
"removeEntities"
)
configurationChecker = new Configuration({
schemaObjectRepresentation: (index as any).schemaObjectRepresentation_,
indexMetadataService,
indexSyncService,
dataSynchronizer,
})
const syncRequired2 = await configurationChecker.checkChanges()
expect(syncRequired2).toHaveLength(1)
expect(syncRequired2).toEqual(
expect.arrayContaining([
@@ -263,8 +295,8 @@ describe("IndexModuleService syncIndexConfig", function () {
])
)
const updatedMetadata2 = await module.listIndexMetadata()
expect(updatedMetadata2).toHaveLength(5)
const updatedMetadata2 = await indexMetadataService.list()
expect(updatedMetadata2).toHaveLength(2)
expect(updatedMetadata2).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -272,27 +304,14 @@ describe("IndexModuleService syncIndexConfig", function () {
fields: "handle,id,title",
status: "done",
}),
expect.objectContaining({
entity: "PriceSet",
fields: "id",
status: "done",
}),
expect.objectContaining({
entity: "Price",
fields: "amount,currency_code,price_set.id",
status: "done",
}),
expect.objectContaining({
entity: "ProductVariant",
fields: "description,id,product.id,product_id,sku",
status: "pending",
}),
expect.objectContaining({
entity: "LinkProductVariantPriceSet",
fields: "id,price_set_id,variant_id",
status: "done",
}),
])
)
expect(spyDataSynchronizer_).toHaveBeenCalledTimes(1)
})
})
@@ -8,18 +8,18 @@ import { MedusaAppOutput, MedusaModule } from "@medusajs/framework/modules-sdk"
import { IndexTypes, InferEntityType } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
Modules,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { initDb, TestDatabaseUtils } from "@medusajs/test-utils"
import { asValue } from "awilix"
import * as path from "path"
import { DataSynchronizer } from "../../src/utils/sync/data-synchronizer"
import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
import { EntityManager } from "@mikro-orm/postgresql"
import { IndexData, IndexRelation } from "@models"
import { DataSynchronizer } from "@services"
import { asValue } from "awilix"
import * as path from "path"
import { setTimeout } from "timers/promises"
import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
const eventBusMock = new EventBusServiceMock()
const queryMock = {
@@ -28,7 +28,7 @@ const queryMock = {
const dbUtils = TestDatabaseUtils.dbTestUtilFactory()
jest.setTimeout(30000)
jest.setTimeout(300000)
const testProductId = "test_prod_1"
const testProductId2 = "test_prod_2"
@@ -80,11 +80,10 @@ const beforeAll_ = async () => {
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
[ContainerRegistrationKeys.QUERY]: asValue(null),
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
})
medusaAppLoader = new MedusaAppLoader(container as any)
medusaAppLoader = new MedusaAppLoader()
// Migrations
await medusaAppLoader.runModulesMigrations()
@@ -97,14 +96,16 @@ const beforeAll_ = async () => {
// Bootstrap modules
const globalApp = await medusaAppLoader.load()
container.register({
[ContainerRegistrationKeys.QUERY]: asValue(queryMock),
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(queryMock),
[Modules.EVENT_BUS]: asValue(eventBusMock),
})
index = container.resolve(Modules.INDEX)
// Mock event bus the index module
;(index as any).eventBusModuleService_ = eventBusMock
await globalApp.onApplicationStart()
;(index as any).storageProvider_.query_ = queryMock
await setTimeout(1000)
return globalApp
} catch (error) {
@@ -125,9 +126,6 @@ describe("DataSynchronizer", () => {
medusaApp = await beforeAll_()
onApplicationPrepareShutdown = medusaApp.onApplicationPrepareShutdown
onApplicationShutdown = medusaApp.onApplicationShutdown
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
})
afterAll(async () => {
@@ -139,55 +137,9 @@ describe("DataSynchronizer", () => {
beforeEach(async () => {
jest.clearAllMocks()
index = container.resolve(Modules.INDEX)
manager = (index as any).container_.manager as EntityManager
const productSchemaObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation =
{
fields: ["id", "title", "updated_at"],
alias: "product",
moduleConfig: {
linkableKeys: {
id: "Product",
product_id: "Product",
product_variant_id: "ProductVariant",
},
},
entity: "Product",
parents: [],
listeners: ["product.created"],
}
const productVariantSchemaObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation =
{
fields: ["id", "title", "product.id", "updated_at"],
alias: "product_variant",
moduleConfig: {
linkableKeys: {
id: "ProductVariant",
product_id: "Product",
product_variant_id: "ProductVariant",
},
},
entity: "ProductVariant",
parents: [
{
ref: productSchemaObjectRepresentation,
inSchemaRef: productSchemaObjectRepresentation,
targetProp: "id",
},
],
listeners: ["product-variant.created"],
}
const mockSchemaRepresentation = {
product: productSchemaObjectRepresentation,
product_variant: productVariantSchemaObjectRepresentation,
}
dataSynchronizer = new DataSynchronizer({
storageProvider: (index as any).storageProvider_,
schemaObjectRepresentation: mockSchemaRepresentation,
query: queryMock as any,
})
dataSynchronizer = (index as any).dataSynchronizer_
})
describe("sync", () => {
@@ -223,8 +175,8 @@ describe("DataSynchronizer", () => {
const ackMock = jest.fn()
const result = await dataSynchronizer.sync({
entityName: "product",
const result = await dataSynchronizer.syncEntity({
entityName: "Product",
ack: ackMock,
})
@@ -237,7 +189,7 @@ describe("DataSynchronizer", () => {
order: {
id: "asc",
},
take: 1000,
take: 100,
},
})
@@ -247,7 +199,7 @@ describe("DataSynchronizer", () => {
filters: {
id: [testProductId],
},
fields: ["id", "title", "updated_at"],
fields: ["id", "title"],
})
// Second loop fetching products
@@ -263,7 +215,7 @@ describe("DataSynchronizer", () => {
order: {
id: "asc",
},
take: 1000,
take: 100,
},
})
@@ -273,7 +225,7 @@ describe("DataSynchronizer", () => {
filters: {
id: [testProductId2],
},
fields: ["id", "title", "updated_at"],
fields: ["id", "title"],
})
expect(ackMock).toHaveBeenNthCalledWith(1, {
@@ -304,8 +256,16 @@ describe("DataSynchronizer", () => {
)
expect(indexData).toHaveLength(2)
expect(indexData[0].id).toEqual(testProductId)
expect(indexData[1].id).toEqual(testProductId2)
expect(indexData).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: testProductId,
}),
expect.objectContaining({
id: testProductId2,
}),
])
)
expect(indexRelationData).toHaveLength(0)
})
@@ -369,15 +329,15 @@ describe("DataSynchronizer", () => {
const ackMock = jest.fn()
await dataSynchronizer.sync({
entityName: "product",
await dataSynchronizer.syncEntity({
entityName: "Product",
ack: ackMock,
})
jest.clearAllMocks()
const result = await dataSynchronizer.sync({
entityName: "product_variant",
const result = await dataSynchronizer.syncEntity({
entityName: "ProductVariant",
ack: ackMock,
})
@@ -390,7 +350,7 @@ describe("DataSynchronizer", () => {
order: {
id: "asc",
},
take: 1000,
take: 100,
},
})
@@ -400,7 +360,7 @@ describe("DataSynchronizer", () => {
filters: {
id: [testVariantId],
},
fields: ["id", "title", "product.id", "updated_at"],
fields: ["id", "product.id", "product_id", "sku"],
})
// Second loop fetching product variants
@@ -416,7 +376,7 @@ describe("DataSynchronizer", () => {
order: {
id: "asc",
},
take: 1000,
take: 100,
},
})
@@ -426,7 +386,7 @@ describe("DataSynchronizer", () => {
filters: {
id: [testVariantId2],
},
fields: ["id", "title", "product.id", "updated_at"],
fields: ["id", "product.id", "product_id", "sku"],
})
expect(ackMock).toHaveBeenNthCalledWith(1, {
@@ -456,29 +416,33 @@ describe("DataSynchronizer", () => {
>(toMikroORMEntity(IndexRelation), {})
expect(indexData).toHaveLength(4)
expect(indexData[0].id).toEqual(testProductId)
expect(indexData[1].id).toEqual(testProductId2)
expect(indexData[2].id).toEqual(testVariantId)
expect(indexData[3].id).toEqual(testVariantId2)
expect(indexData).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: testProductId }),
expect.objectContaining({ id: testProductId2 }),
expect.objectContaining({ id: testVariantId }),
expect.objectContaining({ id: testVariantId2 }),
])
)
expect(indexRelationData).toHaveLength(2)
expect(indexRelationData[0]).toEqual(
expect.objectContaining({
parent_id: testProductId,
child_id: testVariantId,
parent_name: "Product",
child_name: "ProductVariant",
pivot: "Product-ProductVariant",
})
)
expect(indexRelationData[1]).toEqual(
expect.objectContaining({
parent_id: testProductId2,
child_id: testVariantId2,
parent_name: "Product",
child_name: "ProductVariant",
pivot: "Product-ProductVariant",
})
expect(indexRelationData).toEqual(
expect.arrayContaining([
expect.objectContaining({
parent_id: testProductId,
child_id: testVariantId,
parent_name: "Product",
child_name: "ProductVariant",
pivot: "Product-ProductVariant",
}),
expect.objectContaining({
parent_id: testProductId2,
child_id: testVariantId2,
parent_name: "Product",
child_name: "ProductVariant",
pivot: "Product-ProductVariant",
}),
])
)
})
@@ -8,7 +8,6 @@ import { MedusaAppOutput, MedusaModule } from "@medusajs/framework/modules-sdk"
import { EventBusTypes, IndexTypes } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
Modules,
toMikroORMEntity,
} from "@medusajs/framework/utils"
@@ -17,6 +16,7 @@ import { EntityManager } from "@mikro-orm/postgresql"
import { IndexData, IndexRelation } from "@models"
import { asValue } from "awilix"
import * as path from "path"
import { setTimeout } from "timers/promises"
import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
@@ -151,6 +151,7 @@ const beforeEach_ = async (eventDataToEmit) => {
if (isFirstTime) {
isFirstTime = false
await sendEvents(eventDataToEmit)
return
}
@@ -241,11 +242,11 @@ describe("IndexModuleService", function () {
]
beforeEach(async () => {
await setTimeout(1000)
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
manager = (medusaApp.sharedContainer!.resolve(Modules.INDEX) as any)
.container_.manager as EntityManager
})
afterEach(afterEach_)
@@ -404,9 +405,8 @@ describe("IndexModuleService", function () {
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
manager = (medusaApp.sharedContainer!.resolve(Modules.INDEX) as any)
.container_.manager as EntityManager
})
afterEach(afterEach_)
@@ -565,9 +565,8 @@ describe("IndexModuleService", function () {
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
manager = (medusaApp.sharedContainer!.resolve(Modules.INDEX) as any)
.container_.manager as EntityManager
await updateData(manager)
@@ -686,9 +685,8 @@ describe("IndexModuleService", function () {
beforeEach(async () => {
await beforeEach_(eventDataToEmit)
manager = (
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
manager = (medusaApp.sharedContainer!.resolve(Modules.INDEX) as any)
.container_.manager as EntityManager
queryMock.graph = jest.fn().mockImplementation((query) => {
const entity = query.entity
@@ -1,7 +1,7 @@
import { asValue } from "awilix"
import { container } from "@medusajs/framework"
import type { IndexTypes } from "@medusajs/types"
import { Orchestrator } from "../../src/orchestrator"
import { Orchestrator } from "@utils"
function creatingFakeLockingModule() {
return {
@@ -46,15 +46,19 @@ describe("Orchestrator", () => {
locking: asValue(lockingModule),
})
const orchestrator = new Orchestrator(container, entities, {
lockDuration: 60 * 1000,
async taskRunner(entity) {
expect(orchestrator.state).toEqual("processing")
processedEntities.push(entity.entity)
},
})
async function taskRunner(entity: string) {
processedEntities.push(entity)
}
await orchestrator.process()
const orchestrator = new Orchestrator(
container.resolve("locking"),
entities.map((e) => e.entity),
{
lockDuration: 60 * 1000,
}
)
await orchestrator.process(taskRunner)
expect(lockingModule.lockEntities.size).toEqual(0)
expect(orchestrator.state).toEqual("completed")
expect(processedEntities).toEqual(["brand", "product"])
@@ -92,14 +96,19 @@ describe("Orchestrator", () => {
}),
})
const orchestrator = new Orchestrator(container, entities, {
lockDuration: 60 * 1000,
async taskRunner(entity) {
processedEntities.push(entity.entity)
},
})
const orchestrator = new Orchestrator(
container.resolve("locking"),
entities.map((e) => e.entity),
{
lockDuration: 60 * 1000,
}
)
await orchestrator.process()
async function taskRunner(entity: string) {
processedEntities.push(entity)
}
await orchestrator.process(taskRunner)
expect(processedEntities).toEqual([])
})
@@ -130,20 +139,36 @@ describe("Orchestrator", () => {
locking: asValue(lockingModule),
})
const orchestrator = new Orchestrator(container, entities, {
lockDuration: 60 * 1000,
async taskRunner(entity) {
processedEntities.push({ entity: entity.entity, owner: "instance-1" })
},
})
const orchestrator1 = new Orchestrator(container, entities, {
lockDuration: 60 * 1000,
async taskRunner(entity) {
processedEntities.push({ entity: entity.entity, owner: "instance-2" })
},
})
const entityNames = entities.map((e) => e.entity)
await Promise.all([orchestrator.process(), orchestrator1.process()])
async function taskRunner(entity: string) {
processedEntities.push({ entity: entity, owner: "instance-1" })
}
const orchestrator = new Orchestrator(
container.resolve("locking"),
entityNames,
{
lockDuration: 60 * 1000,
}
)
async function taskRunner2(entity: string) {
processedEntities.push({ entity: entity, owner: "instance-2" })
}
const orchestrator1 = new Orchestrator(
container.resolve("locking"),
entityNames,
{
lockDuration: 60 * 1000,
}
)
await Promise.all([
orchestrator.process(taskRunner),
orchestrator1.process(taskRunner2),
])
expect(processedEntities).toEqual([
{
entity: "brand",
@@ -184,17 +209,24 @@ describe("Orchestrator", () => {
locking: asValue(lockingModule),
})
const orchestrator = new Orchestrator(container, entities, {
lockDuration: 60 * 1000,
async taskRunner(entity) {
if (entity.entity === "product") {
throw new Error("Cannot process")
}
processedEntities.push(entity.entity)
},
})
async function taskRunner(entity: string) {
if (entity === "product") {
throw new Error("Cannot process")
}
processedEntities.push(entity)
}
await expect(orchestrator.process()).rejects.toThrow("Cannot process")
const orchestrator = new Orchestrator(
container.resolve("locking"),
entities.map((e) => e.entity),
{
lockDuration: 60 * 1000,
}
)
await expect(orchestrator.process(taskRunner)).rejects.toThrow(
"Cannot process"
)
expect(orchestrator.state).toEqual("error")
expect(processedEntities).toEqual(["brand"])
expect(lockingModule.lockEntities.size).toEqual(0)
@@ -227,16 +259,24 @@ describe("Orchestrator", () => {
locking: asValue(lockingModule),
})
const orchestrator = new Orchestrator(container, entities, {
lockDuration: 60 * 1000,
async taskRunner(entity) {
expect(orchestrator.state).toEqual("processing")
processedEntities.push(entity.entity)
},
})
async function taskRunner(entity: string) {
expect(orchestrator.state).toEqual("processing")
processedEntities.push(entity)
}
const orchestrator = new Orchestrator(
container.resolve("locking"),
entities.map((e) => e.entity),
{
lockDuration: 60 * 1000,
}
)
await expect(
Promise.all([orchestrator.process(), orchestrator.process()])
Promise.all([
orchestrator.process(taskRunner),
orchestrator.process(taskRunner),
])
).rejects.toThrow("Cannot re-run an already running orchestrator instance")
expect(lockingModule.lockEntities.size).toEqual(0)
@@ -6,11 +6,7 @@ import {
} from "@medusajs/framework"
import { MedusaAppOutput, MedusaModule } from "@medusajs/framework/modules-sdk"
import { IndexTypes } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
Modules,
} from "@medusajs/framework/utils"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
import { initDb, TestDatabaseUtils } from "@medusajs/test-utils"
import { EntityManager } from "@mikro-orm/postgresql"
import { IndexData, IndexRelation } from "@models"
@@ -123,11 +119,11 @@ describe("IndexModuleService query", function () {
beforeEach(async () => {
await beforeEach_()
module = medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX)
module = medusaApp.sharedContainer!.resolve(Modules.INDEX)
const manager = (
(medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any)
.container_.manager as EntityManager
(medusaApp.sharedContainer!.resolve(Modules.INDEX) as any).container_
.manager as EntityManager
).fork()
const indexRepository = manager.getRepository(IndexData)
@@ -343,6 +339,65 @@ describe("IndexModuleService query", function () {
])
})
it("should query all products ordered by sku DESC with specific fields", async () => {
const { data } = await module.query({
fields: [
"product.*",
"product.variants.sku",
"product.variants.prices.amount",
],
pagination: {
order: {
product: {
variants: {
sku: "DESC",
},
},
},
},
})
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
},
},
variants: [],
},
{
id: "prod_1",
variants: [
{
id: "var_2",
sku: "sku 123",
prices: [
{
id: "money_amount_2",
amount: 10,
},
],
},
{
id: "var_1",
sku: "aaa test aaa",
prices: [
{
id: "money_amount_1",
amount: 100,
},
],
},
],
},
])
})
it("should query all products ordered by price", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
@@ -484,6 +539,35 @@ describe("IndexModuleService query", function () {
])
})
it("should query products filtering by variant sku", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
joinFilters: {
"product.variants.prices.amount": { $gt: 110 },
},
filters: {
product: {
variants: {
sku: { $like: "aaa%" },
},
},
},
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [],
},
],
},
])
})
it("should query products filtering by price and returning the complete entity", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],