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 09:55:09 -03:00
committed by GitHub
parent 8d10731343
commit 22276648ad
19 changed files with 1209 additions and 316 deletions

View File

@@ -1,6 +1,6 @@
import { ModuleJoinerConfig } from "@medusajs/types"
import { defineJoinerConfig } from "@medusajs/utils"
import { MedusaModule } from "../../medusa-module"
import { ModuleJoinerConfig } from "@medusajs/types"
const customModuleJoinerConfig = defineJoinerConfig("custom_user", {
schema: `
@@ -62,12 +62,12 @@ const pricingJoinerConfig = defineJoinerConfig("pricing", {
}
type Price {
amount: Int
amount: Float
deep_nested_price: DeepNestedPrice
}
type DeepNestedPrice {
amount: Int
amount: Float
}
`,
alias: [

View File

@@ -1,7 +1,9 @@
import {
GraphResultSet,
IIndexService,
RemoteJoinerOptions,
RemoteJoinerQuery,
RemoteQueryFilters,
RemoteQueryFunction,
RemoteQueryFunctionReturnPagination,
RemoteQueryInput,
@@ -21,6 +23,7 @@ import { toRemoteQuery } from "./to-remote-query"
*/
export class Query {
#remoteQuery: RemoteQuery
#indexModule: IIndexService
/**
* Method to wrap execution of the graph query for instrumentation
@@ -54,8 +57,15 @@ export class Query {
},
}
constructor(remoteQuery: RemoteQuery) {
constructor({
remoteQuery,
indexModule,
}: {
remoteQuery: RemoteQuery
indexModule: IIndexService
}) {
this.#remoteQuery = remoteQuery
this.#indexModule = indexModule
}
#unwrapQueryConfig(
@@ -172,14 +182,79 @@ export class Query {
return this.#unwrapRemoteQueryResponse(response)
}
/**
* Index function uses the Index module to query and hydrates the data with query.graph
* returns a result set
*/
async index<const TEntry extends string>(
queryOptions: RemoteQueryInput<TEntry> & {
joinFilters?: RemoteQueryFilters<TEntry>
},
options?: RemoteJoinerOptions
): Promise<GraphResultSet<TEntry>> {
if (!this.#indexModule) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Index module is not loaded."
)
}
const mainEntity = queryOptions.entity
const fields = queryOptions.fields.map((field) => mainEntity + "." + field)
const filters = queryOptions.filters
? { [mainEntity]: queryOptions.filters }
: ({} as any)
const joinFilters = queryOptions.joinFilters
? { [mainEntity]: queryOptions.joinFilters }
: ({} as any)
const pagination = queryOptions.pagination as any
if (pagination?.order) {
pagination.order = { [mainEntity]: pagination.order }
}
const indexResponse = (await this.#indexModule.query({
fields,
filters,
joinFilters,
pagination,
})) as unknown as GraphResultSet<TEntry>
delete queryOptions.pagination
delete queryOptions.filters
let finalResultset: GraphResultSet<TEntry> = indexResponse
if (indexResponse.data.length) {
finalResultset = await this.graph(queryOptions, {
...options,
initialData: indexResponse.data,
})
}
return {
data: finalResultset.data,
metadata: indexResponse.metadata as RemoteQueryFunctionReturnPagination,
}
}
}
/**
* API wrapper around the remoteQuery with backward compatibility support
* @param remoteQuery
*/
export function createQuery(remoteQuery: RemoteQuery) {
const query = new Query(remoteQuery)
export function createQuery({
remoteQuery,
indexModule,
}: {
remoteQuery: RemoteQuery
indexModule: IIndexService
}) {
const query = new Query({
remoteQuery,
indexModule,
})
function backwardCompatibleQuery(...args: any[]) {
return query.query.apply(query, args)
@@ -187,6 +262,7 @@ export function createQuery(remoteQuery: RemoteQuery) {
backwardCompatibleQuery.graph = query.graph.bind(query)
backwardCompatibleQuery.gql = query.gql.bind(query)
backwardCompatibleQuery.index = query.index.bind(query)
return backwardCompatibleQuery as Omit<RemoteQueryFunction, symbol>
}

View File

@@ -14,9 +14,10 @@ import {
RemoteJoinerQuery,
RemoteNestedExpands,
} from "@medusajs/types"
import { isString, toPascalCase } from "@medusajs/utils"
import { isPresent, isString, toPascalCase } from "@medusajs/utils"
import { MedusaModule } from "../medusa-module"
const BASE_PREFIX = ""
export class RemoteQuery {
private remoteJoiner: RemoteJoiner
private modulesMap: Map<string, LoadedModule> = new Map()
@@ -99,7 +100,7 @@ export class RemoteQuery {
public static getAllFieldsAndRelations(
expand: RemoteExpandProperty | RemoteNestedExpands[number],
prefix = "",
prefix = BASE_PREFIX,
args: JoinerArgument = {} as JoinerArgument
): {
select?: string[]
@@ -122,7 +123,14 @@ export class RemoteQuery {
fields.add(prefix ? `${prefix}.${field}` : field)
}
args[prefix] = expand.args
const filters =
expand.args?.find((arg) => arg.name === "filters")?.value ?? {}
if (isPresent(filters)) {
args[prefix] = filters
} else if (isPresent(expand.args)) {
args[prefix] = expand.args
}
for (const property in expand.expands ?? {}) {
const newPrefix = prefix ? `${prefix}.${property}` : property
@@ -147,7 +155,12 @@ export class RemoteQuery {
: shouldSelectAll
? undefined
: []
return { select, relations, args }
return {
select,
relations,
args,
}
}
private hasPagination(options: { [attr: string]: unknown }): boolean {
@@ -225,6 +238,15 @@ export class RemoteQuery {
filters[keyField] = ids
}
delete options.args?.[BASE_PREFIX]
if (Object.keys(options.args ?? {}).length) {
filters = {
...filters,
...options?.args,
}
options.args = {} as any
}
const hasPagination = this.hasPagination(options)
let methodName = hasPagination ? "listAndCount" : "list"