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:
co-authored by
Carlos R. L. Rodrigues
parent
60f46e07fd
commit
a33aebd895
@@ -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.*"],
|
||||
|
||||
Reference in New Issue
Block a user