fix(index): Apply various fixes to the index engine (#12501)

This commit is contained in:
Carlos R. L. Rodrigues
2025-05-19 15:14:25 -03:00
committed by GitHub
parent 32be40a2c0
commit 59bbff62d8
20 changed files with 194 additions and 133 deletions

View File

@@ -0,0 +1,9 @@
---
"@medusajs/link-modules": patch
"@medusajs/index": patch
"@medusajs/utils": patch
"@medusajs/modules-sdk": patch
"@medusajs/types": patch
---
fix(index): limit partition table name and index enum fields

View File

@@ -232,7 +232,7 @@ export class Query {
pagination: {
// We pass through `take` to force the `select-in` query strategy
// There might be a better way to do this, but for now this should do
take: queryOptions.pagination?.take,
take: queryOptions.pagination?.take ?? indexResponse.data.length,
},
}

View File

@@ -23,14 +23,34 @@ describe("IndexQueryConfig", () => {
expectTypeOf<IndexConfig["filters"]>().toEqualTypeOf<
| {
id?: string | string[] | OperatorMap<string>
title?: string | string[] | OperatorMap<string>
id?: string | string[] | OperatorMap<string | string[] | null> | null
title?:
| string
| string[]
| OperatorMap<string | string[] | null>
| null
variants?: {
id?: string | string[] | OperatorMap<string>
product_id?: string | string[] | OperatorMap<string>
sku?: string | string[] | OperatorMap<string>
id?:
| string
| string[]
| OperatorMap<string | string[] | null>
| null
product_id?:
| string
| string[]
| OperatorMap<string | string[] | null>
| null
sku?:
| string
| string[]
| OperatorMap<string | string[] | null>
| null
prices?: {
amount?: number | number[] | OperatorMap<number>
amount?:
| number
| number[]
| OperatorMap<number | number[] | null>
| null
}
}
}

View File

@@ -1,12 +1,12 @@
export type IndexOperatorMap<T> = {
$eq: T
$lt: T
$lte: T
$gt: T
$gte: T
$ne: T
$in: T
$is: T
$like: T
$ilike: T
$eq?: T
$lt?: T
$lte?: T
$gt?: T
$gte?: T
$ne?: T
$in?: T
$is?: T
$like?: T
$ilike?: T
}

View File

@@ -19,8 +19,12 @@ type ExtractFiltersOperators<
? never
: Key extends ExcludedProps
? never
: TypeOnly<T[Key]> extends string | number | boolean | Date
? TypeOnly<T[Key]> | TypeOnly<T[Key]>[] | OperatorMap<TypeOnly<T[Key]>>
: TypeOnly<T[Key]> extends string | number | boolean | Date | null
?
| TypeOnly<T[Key]>
| TypeOnly<T[Key]>[]
| OperatorMap<TypeOnly<T[Key]> | TypeOnly<T[Key]>[] | null>
| null
: TypeOnly<T[Key]> extends Array<infer R>
? TypeOnly<R> extends { __typename: any }
? IndexFilters<Key & string, T, [Key & string, ...Exclusion], Depth[Lim]>

View File

@@ -63,7 +63,6 @@ export type IndexQueryConfig<TEntry extends string> = {
filters?: IndexFilters<TEntry>
joinFilters?: IndexFilters<TEntry>
pagination?: Partial<IndexQueryInput<TEntry>["pagination"]>
keepFilteredEntities?: boolean
}
export type QueryFunctionReturnPagination = {

View File

@@ -1,4 +1,5 @@
import { camelToSnakeCase, simpleHash } from "@medusajs/framework/utils"
import { camelToSnakeCase } from "./camel-to-snake-case"
import { simpleHash } from "./simple-hash"
export function compressName(name: string, limit = 58) {
if (name.length <= limit) {

View File

@@ -4,6 +4,7 @@ export * from "./array-intersection"
export * from "./build-query"
export * from "./build-regexp-if-valid"
export * from "./camel-to-snake-case"
export * from "./compress-name"
export * from "./container"
export * from "./convert-item-response-to-update-request"
export * from "./create-container-like"
@@ -80,11 +81,11 @@ export * from "./to-handle"
export * from "./to-kebab-case"
export * from "./to-pascal-case"
export * from "./to-unix-slash"
export * from "./try-convert-to-number"
export * from "./trim-zeros"
export * from "./try-convert-to-boolean"
export * from "./try-convert-to-number"
export * from "./unflatten-object-keys"
export * from "./upper-case-first"
export * from "./validate-handle"
export * from "./wrap-handler"
export * from "./validate-module-name"
export * from "./try-convert-to-boolean"
export * from "./wrap-handler"

View File

@@ -44,8 +44,7 @@ export function buildModuleResourceEventName({
action: string
}): string {
const kebabCaseName = lowerCaseFirst(kebabCase(objectName))
const conventionalePrefix = prefix && lowerCaseFirst(kebabCase(prefix))
return `${prefix ? `${conventionalePrefix}.` : ""}${kebabCaseName}.${action}`
return `${prefix ? `${prefix}.` : ""}${kebabCaseName}.${action}`
}
/**

View File

@@ -284,6 +284,34 @@ describe("IndexModuleService query", function () {
afterEach(afterEach_)
it("should query all products where sku not null", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
sku: { $ne: null },
},
},
},
})
expect(data.length).toEqual(1)
const { data: data2 } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
sku: { $eq: null },
},
},
},
})
expect(data2.length).toEqual(0)
})
it("should query all products ordered by sku DESC", async () => {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
@@ -710,60 +738,6 @@ describe("IndexModuleService query", function () {
])
})
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({
estimate_count: expect.any(Number),
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.*"],

View File

@@ -0,0 +1,59 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20250515161913 extends Migration {
override async up(): Promise<void> {
this.addSql(`
DO $$
DECLARE
r RECORD;
protected_tables TEXT[] := ARRAY[
'cat_linkproductsaleschannel',
'cat_linkproductvariantpriceset',
'cat_price',
'cat_priceset',
'cat_product',
'cat_productvariant',
'cat_saleschannel',
'cat_pivot_linkproductsaleschannelsaleschannel',
'cat_pivot_linkproductvariantpricesetpriceset',
'cat_pivot_pricesetprice',
'cat_pivot_productlinkproductsaleschannel',
'cat_pivot_productproductvariant',
'cat_pivot_productvariantlinkproductvariantpriceset'
];
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public' AND tablename LIKE 'cat\_%'
LOOP
IF r.tablename <> ALL (protected_tables) THEN
EXECUTE format('DROP TABLE IF EXISTS public.%I CASCADE;', r.tablename);
END IF;
END LOOP;
END $$;
UPDATE index_sync SET last_key = NULL WHERE entity NOT IN (
'Product',
'ProductVariant',
'LinkProductVariantPriceSet',
'LinkProductSalesChannel',
'Price',
'PriceSet',
'SalesChannel'
);
UPDATE index_metadata SET status = 'pending' WHERE entity NOT IN (
'Product',
'ProductVariant',
'LinkProductVariantPriceSet',
'LinkProductSalesChannel',
'Price',
'PriceSet',
'SalesChannel'
);
`)
}
override async down(): Promise<void> {}
}

View File

@@ -237,12 +237,7 @@ export class PostgresProvider implements IndexTypes.StorageProvider {
): Promise<IndexTypes.QueryResultSet<TEntry>> {
await this.#isReady_
const {
keepFilteredEntities,
fields = [],
filters = {},
joinFilters = {},
} = config
const { fields = [], filters = {}, joinFilters = {} } = config
const { take, skip, order: inputOrderBy = {} } = config.pagination ?? {}
const select = normalizeFieldsSelection(fields)
@@ -281,7 +276,6 @@ export class PostgresProvider implements IndexTypes.StorageProvider {
options: {
skip,
take,
keepFilteredEntities,
orderBy,
},
rawConfig: config,
@@ -290,7 +284,6 @@ export class PostgresProvider implements IndexTypes.StorageProvider {
const { sql, sqlCount } = qb.buildQuery({
hasPagination,
returnIdOnly: !!keepFilteredEntities,
hasCount,
})
@@ -310,30 +303,6 @@ export class PostgresProvider implements IndexTypes.StorageProvider {
} as IndexTypes.QueryFunctionReturnPagination)
: undefined
if (keepFilteredEntities) {
const mainEntity = Object.keys(select)[0]
const ids = resultSet.map((r) => r[`${mainEntity}.id`])
if (ids.length) {
const result = await this.query<TEntry>(
{
fields,
joinFilters,
filters: {
[mainEntity]: {
id: ids,
},
},
pagination: undefined,
keepFilteredEntities: false,
} as IndexTypes.IndexQueryConfig<TEntry>,
sharedContext
)
result.metadata ??= resultMetadata
return result
}
}
return {
data: qb.buildObjectFromResultset(
resultSet

View File

@@ -29,5 +29,4 @@ export type QueryOptions = {
skip?: number
take?: number
orderBy?: OrderBy | OrderBy[]
keepFilteredEntities?: boolean
}

View File

@@ -1180,7 +1180,9 @@ function buildSchemaFromFilterableLinks(
return
}
const fieldType = fieldRef.type.toString()
const isEnum =
fieldRef.type?.astNode?.kind === GraphQLUtils.Kind.ENUM_TYPE_DEFINITION
const fieldType = isEnum ? "String" : fieldRef.type.toString()
const isArray = fieldType.startsWith("[")
const currentType = fieldType.replace(/\[|\]|\!/g, "")

View File

@@ -1,6 +1,7 @@
import { IndexTypes } from "@medusajs/framework/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { schemaObjectRepresentationPropertiesToOmit } from "@types"
import { getPivotTableName, normalizeTableName } from "./normalze-table-name"
export async function createPartitions(
schemaObjectRepresentation: IndexTypes.SchemaObjectRepresentation,
@@ -18,7 +19,7 @@ export async function createPartitions(
schemaObjectRepresentation[key].listeners.length > 0
)
.map((key) => {
const cName = key.toLowerCase()
const cName = normalizeTableName(key)
if (createdPartitions.has(cName)) {
return []
@@ -35,7 +36,8 @@ export async function createPartitions(
continue
}
const pName = `cat_pivot_${parent.ref.entity}${key}`.toLowerCase()
const pName = getPivotTableName(`${parent.ref.entity}${key}`)
if (createdPartitions.has(pName)) {
continue
}
@@ -63,7 +65,7 @@ export async function createPartitions(
schemaObjectRepresentation[key].listeners.length > 0
)
.map((key) => {
const cName = key.toLowerCase()
const cName = normalizeTableName(key)
const part: string[] = []
part.push(
@@ -79,7 +81,8 @@ export async function createPartitions(
continue
}
const pName = `cat_pivot_${parent.ref.entity}${key}`.toLowerCase()
const pName = getPivotTableName(`${parent.ref.entity}${key}`)
part.push(
`CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_${pName}_child_id" ON ${activeSchema}${pName} ("child_id")`
)

View File

@@ -6,3 +6,4 @@ export * from "./sync/configuration"
export * from "./index-metadata-status"
export * from "./gql-to-types"
export * from "./default-schema"
export * from "./normalze-table-name"

View File

@@ -0,0 +1,10 @@
import { compressName } from "@medusajs/framework/utils"
export function normalizeTableName(name: string): string {
return compressName(name.toLowerCase(), 58).replace(/[^a-z0-9_]/g, "_")
}
export function getPivotTableName(tableName: string) {
const compressedName = normalizeTableName(tableName)
return `cat_pivot_${compressedName}`
}

View File

@@ -7,6 +7,7 @@ import {
} from "@medusajs/framework/utils"
import { Knex } from "@mikro-orm/knex"
import { OrderBy, QueryFormat, QueryOptions, Select } from "@types"
import { getPivotTableName, normalizeTableName } from "./normalze-table-name"
function escapeJsonPathString(val: string): string {
// Escape for JSONPath string
@@ -23,6 +24,10 @@ function buildSafeJsonPathQuery(
jsonPathOperator = "=="
} else if (operator.toUpperCase().includes("LIKE")) {
jsonPathOperator = "like_regex"
} else if (operator === "IS") {
jsonPathOperator = "=="
} else if (operator === "IS NOT") {
jsonPathOperator = "!="
}
if (typeof value === "string") {
@@ -32,6 +37,10 @@ function buildSafeJsonPathQuery(
val = val.replace(/%/g, ".*").replace(/_/g, ".")
}
value = `"${escapeJsonPathString(val)}"`
} else {
if ((operator === "IS" || operator === "IS NOT") && value === null) {
value = "null"
}
}
return `$.${field} ${jsonPathOperator} ${value}`
@@ -530,14 +539,14 @@ export class QueryBuilder {
aliasMapping[currentAliasPath] = alias
if (level > 0) {
const cName = entity.ref.entity.toLowerCase()
const cName = normalizeTableName(entity.ref.entity)
let joinTable = `cat_${cName} AS ${alias}`
if (entity.isInverse || parEntity.isInverse) {
const pName =
`${entity.ref.entity}${parEntity.ref.entity}`.toLowerCase()
const pivotTable = `cat_pivot_${pName}`
const pivotTable = getPivotTableName(pName)
joinBuilder.leftJoin(
`${pivotTable} AS ${alias}_ref`,
@@ -552,7 +561,7 @@ export class QueryBuilder {
} else {
const pName =
`${parEntity.ref.entity}${entity.ref.entity}`.toLowerCase()
const pivotTable = `cat_pivot_${pName}`
const pivotTable = getPivotTableName(pName)
joinBuilder.leftJoin(
`${pivotTable} AS ${alias}_ref`,
@@ -704,11 +713,9 @@ export class QueryBuilder {
public buildQuery({
hasPagination = true,
hasCount = false,
returnIdOnly = false,
}: {
hasPagination?: boolean
hasCount?: boolean
returnIdOnly?: boolean
}): { sql: string; sqlCount?: string } {
const selectOnlyStructure = this.selector.select
const structure = this.requestedFields
@@ -816,7 +823,10 @@ export class QueryBuilder {
}
innerQueryBuilder.from(
`cat_${rootEntity} AS ${this.getShortAlias(aliasMapping, rootKey)}`
`cat_${normalizeTableName(rootEntity)} AS ${this.getShortAlias(
aliasMapping,
rootKey
)}`
)
joinParts.forEach((joinPart) => {
@@ -887,7 +897,10 @@ export class QueryBuilder {
const innerQueryAlias = "paginated_ids"
outerQueryBuilder.from(
`cat_${rootEntity} AS ${this.getShortAlias(aliasMapping, rootKey)}`
`cat_${normalizeTableName(rootEntity)} AS ${this.getShortAlias(
aliasMapping,
rootKey
)}`
)
outerQueryBuilder.joinRaw(
@@ -909,13 +922,11 @@ export class QueryBuilder {
outerQueryBuilder.joinRaw(joinPart)
})
const finalSelectParts = !returnIdOnly
? this.buildSelectParts(
selectOnlyStructure[rootKey] as Select,
rootKey,
aliasMapping
)
: { [`${rootKey}.id`]: `${rootAlias}.id` }
const finalSelectParts = this.buildSelectParts(
selectOnlyStructure[rootKey] as Select,
rootKey,
aliasMapping
)
outerQueryBuilder.select(finalSelectParts)

View File

@@ -4,13 +4,13 @@ import {
} from "@medusajs/framework/types"
import {
composeTableName,
compressName,
mikroOrmSoftDeletableFilterOptions,
simpleHash,
SoftDeletableFilterKey,
} from "@medusajs/framework/utils"
import { EntitySchema } from "@mikro-orm/core"
import { compressName } from "./compress-name"
function getClass(...properties) {
return class LinkModel {