Files
medusa-store/packages/modules/index/integration-tests/__tests__/query-builder.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

864 lines
18 KiB
TypeScript

import {
configLoader,
container,
logger,
MedusaAppLoader,
} from "@medusajs/framework"
import { MedusaAppOutput, MedusaModule } from "@medusajs/framework/modules-sdk"
import { IndexTypes } from "@medusajs/framework/types"
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"
import { asValue } from "awilix"
import path from "path"
import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
const eventBusMock = new EventBusServiceMock()
const queryMock = jest.fn().mockReturnValue({
graph: jest.fn(),
})
const dbUtils = TestDatabaseUtils.dbTestUtilFactory()
jest.setTimeout(300000)
let isFirstTime = true
let medusaAppLoader!: MedusaAppLoader
const beforeAll_ = async () => {
try {
await configLoader(
path.join(__dirname, "./../__fixtures__"),
"medusa-config"
)
console.log(`Creating database ${dbName}`)
await dbUtils.create(dbName)
dbUtils.pgConnection_ = await initDb()
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
[ContainerRegistrationKeys.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()
;(index as any).storageProvider_.query_ = queryMock
return globalApp
} catch (error) {
console.error("Error initializing", error?.message)
throw error
}
}
const beforeEach_ = async () => {
jest.clearAllMocks()
if (isFirstTime) {
isFirstTime = false
return
}
try {
await medusaAppLoader.runModulesLoader()
} 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 query", function () {
let medusaApp: MedusaAppOutput
let module: IndexTypes.IIndexService
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)
})
beforeEach(async () => {
await beforeEach_()
module = medusaApp.sharedContainer!.resolve(Modules.INDEX)
const manager = (
(medusaApp.sharedContainer!.resolve(Modules.INDEX) as any).container_
.manager as EntityManager
).fork()
const indexRepository = manager.getRepository(IndexData)
await manager.persistAndFlush(
[
{
id: "prod_1",
name: "Product",
data: {
id: "prod_1",
},
},
{
id: "prod_2",
name: "Product",
data: {
id: "prod_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
},
},
},
},
{
id: "var_1",
name: "ProductVariant",
data: {
id: "var_1",
sku: "aaa test aaa",
},
},
{
id: "var_2",
name: "ProductVariant",
data: {
id: "var_2",
sku: "sku 123",
},
},
{
id: "link_id_1",
name: "LinkProductVariantPriceSet",
data: {
id: "link_id_1",
variant_id: "var_1",
price_set_id: "price_set_1",
},
},
{
id: "link_id_2",
name: "LinkProductVariantPriceSet",
data: {
id: "link_id_2",
variant_id: "var_2",
price_set_id: "price_set_2",
},
},
{
id: "price_set_1",
name: "PriceSet",
data: {
id: "price_set_1",
},
},
{
id: "price_set_2",
name: "PriceSet",
data: {
id: "price_set_2",
},
},
{
id: "money_amount_1",
name: "Price",
data: {
id: "money_amount_1",
amount: 100,
},
},
{
id: "money_amount_2",
name: "Price",
data: {
id: "money_amount_2",
amount: 10,
},
},
].map((data) => indexRepository.create(data))
)
const indexRelationRepository = manager.getRepository(IndexRelation)
await manager.persistAndFlush(
[
{
parent_id: "prod_1",
parent_name: "Product",
child_id: "var_1",
child_name: "ProductVariant",
pivot: "Product-ProductVariant",
},
{
parent_id: "prod_1",
parent_name: "Product",
child_id: "var_2",
child_name: "ProductVariant",
pivot: "Product-ProductVariant",
},
{
parent_id: "var_1",
parent_name: "ProductVariant",
child_id: "link_id_1",
child_name: "LinkProductVariantPriceSet",
pivot: "ProductVariant-LinkProductVariantPriceSet",
},
{
parent_id: "var_2",
parent_name: "ProductVariant",
child_id: "link_id_2",
child_name: "LinkProductVariantPriceSet",
pivot: "ProductVariant-LinkProductVariantPriceSet",
},
{
parent_id: "link_id_1",
parent_name: "LinkProductVariantPriceSet",
child_id: "price_set_1",
child_name: "PriceSet",
pivot: "LinkProductVariantPriceSet-PriceSet",
},
{
parent_id: "link_id_2",
parent_name: "LinkProductVariantPriceSet",
child_id: "price_set_2",
child_name: "PriceSet",
pivot: "LinkProductVariantPriceSet-PriceSet",
},
{
parent_id: "price_set_1",
parent_name: "PriceSet",
child_id: "money_amount_1",
child_name: "Price",
pivot: "PriceSet-Price",
},
{
parent_id: "price_set_2",
parent_name: "PriceSet",
child_id: "money_amount_2",
child_name: "Price",
pivot: "PriceSet-Price",
},
].map((data) => indexRelationRepository.create(data))
)
})
afterEach(afterEach_)
it("should query all products ordered by sku DESC", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
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 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.*"],
pagination: {
order: {
product: {
variants: {
prices: {
amount: "DESC",
},
},
},
},
},
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [
{
id: "money_amount_1",
amount: 100,
},
],
},
{
id: "var_2",
sku: "sku 123",
prices: [
{
id: "money_amount_2",
amount: 10,
},
],
},
],
},
{
id: "prod_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
},
},
variants: [],
},
])
const { data: dataAsc } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
pagination: {
order: {
product: {
variants: {
prices: {
amount: "ASC",
},
},
},
},
},
})
expect(dataAsc).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 products filtering by variant sku", async () => {
const { data, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
sku: { $like: "aaa%" },
},
},
},
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [
{
id: "money_amount_1",
amount: 100,
},
],
},
],
},
])
})
it("should query products filtering by variant sku and join filters on prices amount", async () => {
const { data, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
joinFilters: {
"product.variants.prices.amount": { $gt: 110 },
},
filters: {
product: {
variants: {
sku: { $like: "aaa%" },
},
},
},
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [],
},
],
},
])
})
it("should filter using fields not selected", async () => {
const { data } = await module.query({
fields: ["product.id", "product.variants.*"],
pagination: {
order: {
product: {
variants: {
prices: {
amount: "DESC",
},
},
},
},
},
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
},
{
id: "var_2",
sku: "sku 123",
},
],
},
{
id: "prod_2",
variants: [],
},
])
})
it("should query products filtering by price and returning the complete entity", async () => {
const { data, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
prices: {
amount: { $gt: 50 },
},
},
},
},
keepFilteredEntities: true,
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [
{
id: "money_amount_1",
amount: 100,
},
],
},
{
id: "var_2",
sku: "sku 123",
prices: [
{
id: "money_amount_2",
amount: 10,
},
],
},
],
},
])
})
it("should query all products", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [
{
id: "money_amount_1",
amount: 100,
},
],
},
{
id: "var_2",
sku: "sku 123",
prices: [
{
id: "money_amount_2",
amount: 10,
},
],
},
],
},
{
id: "prod_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
},
},
variants: [],
},
])
})
it("should paginate products", async () => {
const { data, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
pagination: {
take: 1,
skip: 1,
},
})
expect(metadata).toEqual({
count: 2,
skip: 1,
take: 1,
})
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
},
},
variants: [],
},
])
})
it("should handle null values on where clause", async () => {
const { data: data_, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
sku: null,
},
},
},
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data_).toEqual([
{
id: "prod_2",
deep: { a: 1, obj: { b: 15 } },
title: "Product 2 title",
variants: [],
},
])
const { data, metadata: metadata2 } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
sku: { $ne: null },
},
},
},
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata2).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [{ id: "money_amount_1", amount: 100 }],
},
{
id: "var_2",
sku: "sku 123",
prices: [{ id: "money_amount_2", amount: 10 }],
},
],
},
])
})
it("should query products filtering by deep nested levels", async () => {
const { data, metadata } = await module.query({
fields: ["product.*"],
filters: {
product: {
deep: {
obj: {
b: 15,
},
},
},
},
pagination: {
take: 1,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 1,
})
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
},
},
},
])
})
})