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>
This commit is contained in:
Carlos R. L. Rodrigues
2025-02-12 12:55:09 +00:00
committed by GitHub
co-authored by Adrien de Peretti
parent 8d10731343
commit 22276648ad
19 changed files with 1209 additions and 316 deletions
@@ -509,7 +509,7 @@ describe("IndexModuleService query", function () {
})
it("should query products filtering by variant sku", async () => {
const { data } = await module.query({
const { data, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
@@ -518,6 +518,16 @@ describe("IndexModuleService query", function () {
},
},
},
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
@@ -539,8 +549,8 @@ describe("IndexModuleService query", function () {
])
})
it("should query products filtering by variant sku", async () => {
const { data } = await module.query({
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 },
@@ -552,6 +562,16 @@ describe("IndexModuleService query", function () {
},
},
},
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
@@ -568,8 +588,45 @@ describe("IndexModuleService query", function () {
])
})
it("should query products filtering by price and returning the complete entity", async () => {
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: {
@@ -581,6 +638,16 @@ describe("IndexModuleService query", function () {
},
},
keepFilteredEntities: true,
pagination: {
take: 100,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 100,
})
expect(data).toEqual([
@@ -658,7 +725,7 @@ describe("IndexModuleService query", function () {
})
it("should paginate products", async () => {
const { data } = await module.query({
const { data, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
pagination: {
take: 1,
@@ -666,6 +733,11 @@ describe("IndexModuleService query", function () {
},
})
expect(metadata).toEqual({
count: 2,
skip: 1,
take: 1,
})
expect(data).toEqual([
{
id: "prod_2",
@@ -682,7 +754,7 @@ describe("IndexModuleService query", function () {
})
it("should handle null values on where clause", async () => {
const { data } = await module.query({
const { data: data_, metadata } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
@@ -691,25 +763,69 @@ describe("IndexModuleService query", function () {
},
},
},
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_2",
title: "Product 2 title",
deep: {
a: 1,
obj: {
b: 15,
id: "prod_1",
variants: [
{
id: "var_1",
sku: "aaa test aaa",
prices: [{ id: "money_amount_1", amount: 100 }],
},
},
variants: [],
{
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 } = await module.query({
const { data, metadata } = await module.query({
fields: ["product.*"],
filters: {
product: {
@@ -720,8 +836,17 @@ describe("IndexModuleService query", function () {
},
},
},
pagination: {
take: 1,
skip: 0,
},
})
expect(metadata).toEqual({
count: 1,
skip: 0,
take: 1,
})
expect(data).toEqual([
{
id: "prod_2",