Files
medusa-store/integration-tests/modules/__tests__/index/sync.spec.ts
Carlos R. L. Rodrigues 22276648ad feat: query.index (#11348)
What:
 - `query.index` helper. It queries the index module, and aggregate the rest of requested fields/relations if needed like `query.graph`.
 
Not covered in this PR:
 - Hydrate only sub entities returned by the query. Example: 1 out of 5 variants have returned, it should only hydrate the data of the single entity, currently it will merge all the variants of the product.
 - Generate types of indexed data
 
 example:
 ```ts
 const query = container.resolve(ContainerRegistrationKeys.QUERY)
        
 await query.index({
  entity: "product",
  fields: [
    "id",
    "description",
    "status",
    "variants.sku",
    "variants.barcode",
    "variants.material",
    "variants.options.value",
    "variants.prices.amount",
    "variants.prices.currency_code",
    "variants.inventory_items.inventory.sku",
    "variants.inventory_items.inventory.description",
  ],
  filters: {
    "variants.sku": { $like: "%-1" },
    "variants.prices.amount": { $gt: 30 },
  },
  pagination: {
    order: {
      "variants.prices.amount": "DESC",
    },
  },
})
```
This query return all products where at least one variant has the title ending in `-1` and at least one price bigger than `30`.
 
The Index Module only hold the data used to paginate and filter, and the returned object is:
```json
{
  "id": "prod_01JKEAM2GJZ14K64R0DHK0JE72",
  "title": null,
  "variants": [
    {
      "id": "variant_01JKEAM2HC89GWS95F6GF9C6YA",
      "sku": "extra-variant-1",
      "prices": [
        {
          "id": "price_01JKEAM2JADEWWX72F8QDP6QXT",
          "amount": 80,
          "currency_code": "USD"
        }
      ]
    }
  ]
}
```

All the rest of the fields will be hydrated from their respective modules, and the final result will be:

```json
{
  "id": "prod_01JKEAY2RJTF8TW9A23KTGY1GD",
  "description": "extra description",
  "status": "draft",
  "variants": [
    {
      "sku": "extra-variant-1",
      "barcode": null,
      "material": null,
      "id": "variant_01JKEAY2S945CRZ6X4QZJ7GVBJ",
      "options": [
        {
          "value": "Red"
        }
      ],
      "prices": [
        {
          "amount": 20,
          "currency_code": "CAD",
          "id": "price_01JKEAY2T2EEYSWZHPGG11B7W7"
        },
        {
          "amount": 80,
          "currency_code": "USD",
          "id": "price_01JKEAY2T2NJK2E5468RK84CAR"
        }
      ],
      "inventory_items": [
        {
          "variant_id": "variant_01JKEAY2S945CRZ6X4QZJ7GVBJ",
          "inventory_item_id": "iitem_01JKEAY2SNY2AWEHPZN0DDXVW6",
          "inventory": {
            "sku": "extra-variant-1",
            "description": "extra variant 1",
            "id": "iitem_01JKEAY2SNY2AWEHPZN0DDXVW6"
          }
        }
      ]
    }
  ]
}
```

Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
2025-02-12 12:55:09 +00:00

214 lines
6.3 KiB
TypeScript

import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { IndexTypes } from "@medusajs/types"
import { defaultCurrencies, Modules } from "@medusajs/utils"
import { setTimeout } from "timers/promises"
import {
adminHeaders,
createAdminUser,
} from "../../../helpers/create-admin-user"
jest.setTimeout(300000)
process.env.ENABLE_INDEX_MODULE = "true"
async function populateData(
api: any,
{ productCount = 1, variantCount = 1, priceCount = 1 } = {}
) {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
for (let i = 0; i < productCount; i++) {
const payload = {
title: "Test Giftcard " + i,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description " + i,
options: [{ title: "Denominations", values: ["100"] }],
variants: new Array(variantCount).fill(0).map((_, j) => ({
title: `Test variant ${i} ${j}`,
sku: `test-variant-${i}-${j}`,
prices: new Array(priceCount).fill(0).map((_, k) => ({
currency_code: Object.values(defaultCurrencies)[k].code,
amount: 10 * k,
})),
options: {
Denominations: "100",
},
})),
}
await api.post("/admin/products", payload, adminHeaders)
}
}
medusaIntegrationTestRunner({
testSuite: ({ getContainer, dbConnection, api, dbConfig }) => {
let indexEngine: IndexTypes.IIndexService
let appContainer
beforeAll(() => {
appContainer = getContainer()
indexEngine = appContainer.resolve(Modules.INDEX)
})
afterAll(() => {
process.env.ENABLE_INDEX_MODULE = "false"
})
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
})
describe("Index engine syncing", () => {
it("should sync the data to the index based on the indexation configuration", async () => {
console.info("[Index engine] Creating products")
await populateData(api, {
productCount: 2,
variantCount: 2,
priceCount: 2,
})
console.info("[Index engine] Creating products done")
await setTimeout(1000)
await dbConnection.raw('TRUNCATE TABLE "index_data";')
await dbConnection.raw('TRUNCATE TABLE "index_relation";')
await dbConnection.raw('TRUNCATE TABLE "index_metadata";')
await dbConnection.raw('TRUNCATE TABLE "index_sync";')
const { data: indexedDataAfterCreation } =
await indexEngine.query<"product">({
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
})
expect(indexedDataAfterCreation.length).toBe(0)
// Prevent storage provider to be triggered though
;(indexEngine as any).storageProvider_.onApplicationStart = jest.fn()
console.info("[Index engine] Triggering sync")
// Trigger a sync
await (indexEngine as any).onApplicationStart_()
console.info("[Index engine] Sync done")
// 28 ms - 6511 records
const { data: results } = await indexEngine.query<"product">({
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
})
expect(results.length).toBe(2)
for (const result of results) {
expect(result.variants.length).toBe(2)
for (const variant of result.variants) {
expect(variant.prices.length).toBe(2)
}
}
})
})
it("should sync the data to the index based on the updated indexation configuration", async () => {
console.info("[Index engine] Creating products")
await populateData(api)
console.info("[Index engine] Creating products done")
await setTimeout(1000)
await dbConnection.raw('TRUNCATE TABLE "index_data";')
await dbConnection.raw('TRUNCATE TABLE "index_relation";')
await dbConnection.raw('TRUNCATE TABLE "index_metadata";')
await dbConnection.raw('TRUNCATE TABLE "index_sync";')
const { data: indexedDataAfterCreation } =
await indexEngine.query<"product">({
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
})
expect(indexedDataAfterCreation.length).toBe(0)
// Prevent storage provider to be triggered though
;(indexEngine as any).storageProvider_.onApplicationStart = jest.fn()
console.info("[Index engine] Triggering sync")
// Trigger a sync
await (indexEngine as any).onApplicationStart_()
console.info("[Index engine] Sync done")
const { data: results } = await indexEngine.query<"product">({
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
})
expect(results.length).toBe(1)
expect(results[0].variants.length).toBe(1)
expect(results[0].variants[0].prices.length).toBe(1)
// Manually change the indexation configuration
;(indexEngine as any).schemaObjectRepresentation_ = null
;(indexEngine as any).moduleOptions_ = {
...(indexEngine as any).moduleOptions_,
schema: `
type Product @Listeners(values: ["product.created", "product.updated", "product.deleted"]) {
id: String
title: String
handle: String
variants: [ProductVariant]
}
type ProductVariant @Listeners(values: ["variant.created", "variant.updated", "variant.deleted"]) {
id: String
product_id: String
sku: String
description: String
}
`,
}
// Trigger a sync
await (indexEngine as any).onApplicationStart_()
await setTimeout(3000)
const { data: updatedResults } = await indexEngine.query<"product">({
fields: ["product.*", "product.variants.*"],
})
expect(updatedResults.length).toBe(1)
expect(updatedResults[0].variants.length).toBe(1)
let staledRaws = await dbConnection.raw(
'SELECT * FROM "index_data" WHERE "staled_at" IS NOT NULL'
)
expect(staledRaws.rows.length).toBe(0)
staledRaws = await dbConnection.raw(
'SELECT * FROM "index_relation" WHERE "staled_at" IS NOT NULL'
)
expect(staledRaws.rows.length).toBe(0)
})
},
})