feat(utils): Support free text configuration (#6942)
**What** - Add support for searchable entity properties to configure default free text search when providing a `q` filter - `@Searchable` decorator to mark a property or relation to be searchable form the current entity FIXES CORE-1910
This commit is contained in:
committed by
GitHub
parent
a0005faa14
commit
21990fcd4b
@@ -1,21 +1,20 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Formula,
|
||||
OnInit,
|
||||
OnLoad,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { DAL } from "@medusajs/types"
|
||||
@@ -64,6 +63,7 @@ export class InventoryItem {
|
||||
deleted_at: Date | null = null
|
||||
|
||||
@InventoryItemSkuIndex.MikroORMIndex()
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
sku: string | null = null
|
||||
|
||||
@@ -94,9 +94,11 @@ export class InventoryItem {
|
||||
@Property({ columnType: "boolean" })
|
||||
requires_shipping: boolean = true
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
description: string | null = null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
title: string | null = null
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./inventory-level"
|
||||
export * from "./inventory-item"
|
||||
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { InventoryItem, InventoryLevel } from "@models"
|
||||
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { mikroOrmBaseRepositoryFactory } from "@medusajs/utils"
|
||||
|
||||
export class InventoryItemRepository extends mikroOrmBaseRepositoryFactory<InventoryItem>(
|
||||
InventoryItem
|
||||
) {
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<InventoryItem & { q?: string }> = {
|
||||
where: {},
|
||||
},
|
||||
context: Context = {}
|
||||
): Promise<InventoryItem[]> {
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
this.applyFreeTextSearchFilters<InventoryItem>(
|
||||
findOptions_,
|
||||
this.getFreeTextSearchConstraints
|
||||
)
|
||||
|
||||
return await super.find(findOptions_, context)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<InventoryItem & { q?: string }> = {
|
||||
where: {},
|
||||
},
|
||||
context: Context = {}
|
||||
): Promise<[InventoryItem[], number]> {
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
this.applyFreeTextSearchFilters<InventoryItem>(
|
||||
findOptions_,
|
||||
this.getFreeTextSearchConstraints
|
||||
)
|
||||
|
||||
return await super.findAndCount(findOptions_, context)
|
||||
}
|
||||
|
||||
protected getFreeTextSearchConstraints(q: string) {
|
||||
return [
|
||||
{
|
||||
description: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
sku: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,31 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("should list all product that match the free text search", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
title: "test product",
|
||||
})
|
||||
const data2 = buildProductOnlyData({
|
||||
title: "space X",
|
||||
})
|
||||
|
||||
const products = await service.create([data, data2])
|
||||
|
||||
const result = await service.list({
|
||||
q: "test",
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].title).toEqual("test product")
|
||||
|
||||
const result2 = await service.list({
|
||||
q: "space",
|
||||
})
|
||||
|
||||
expect(result2).toHaveLength(1)
|
||||
expect(result2[0].title).toEqual("space X")
|
||||
})
|
||||
|
||||
describe("soft deleted", function () {
|
||||
let product
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import {
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
@@ -34,6 +35,7 @@ class ProductCollection {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
optionalNumericSerializer,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
@@ -12,8 +13,8 @@ import {
|
||||
Filter,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
@@ -76,9 +77,11 @@ class ProductVariant {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
sku?: string | null
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
ProductUtils,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import ProductCategory from "./product-category"
|
||||
import ProductCollection from "./product-collection"
|
||||
@@ -64,6 +65,7 @@ class Product {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@@ -73,7 +75,11 @@ class Product {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
subtitle?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@Searchable()
|
||||
@Property({
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
})
|
||||
description?: string | null
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
@@ -91,6 +97,7 @@ class Product {
|
||||
})
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@Searchable()
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
@@ -120,6 +127,7 @@ class Product {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@Searchable()
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
|
||||
@@ -15,40 +15,6 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<Product & { q?: string }> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<Product[]> {
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
await this.mutateNotInCategoriesConstraints(findOptions_)
|
||||
|
||||
this.applyFreeTextSearchFilters<Product>(
|
||||
findOptions_,
|
||||
this.getFreeTextSearchConstraints
|
||||
)
|
||||
|
||||
return await super.find(findOptions_, context)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<Product & { q?: string }> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[Product[], number]> {
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
await this.mutateNotInCategoriesConstraints(findOptions_)
|
||||
|
||||
this.applyFreeTextSearchFilters<Product>(
|
||||
findOptions_,
|
||||
this.getFreeTextSearchConstraints
|
||||
)
|
||||
|
||||
return await super.findAndCount(findOptions_, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to be able to have a strict not in categories, and prevent a product
|
||||
* to be return in the case it also belongs to other categories, we need to
|
||||
@@ -88,42 +54,4 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getFreeTextSearchConstraints(q: string) {
|
||||
return [
|
||||
{
|
||||
description: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
collection: {
|
||||
title: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
variants: {
|
||||
$or: [
|
||||
{
|
||||
title: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
sku: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { StockLocationAddress } from "./stock-location-address"
|
||||
@@ -43,6 +44,7 @@ export class StockLocation {
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date | null = null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||
export { StockLocationRepository } from "./stock-location"
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
|
||||
import { StockLocation } from "@models"
|
||||
import { mikroOrmBaseRepositoryFactory } from "@medusajs/utils"
|
||||
|
||||
export class StockLocationRepository extends mikroOrmBaseRepositoryFactory<StockLocation>(
|
||||
StockLocation
|
||||
) {
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<StockLocation & { q?: string }> = {
|
||||
where: {},
|
||||
},
|
||||
context: Context
|
||||
): Promise<StockLocation[]> {
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
this.applyFreeTextSearchFilters<StockLocation>(
|
||||
findOptions_,
|
||||
this.getFreeTextSearchConstraints
|
||||
)
|
||||
|
||||
return await super.find(findOptions_, context)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<StockLocation & { q?: string }> = {
|
||||
where: {},
|
||||
},
|
||||
context: Context
|
||||
): Promise<[StockLocation[], number]> {
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
this.applyFreeTextSearchFilters<StockLocation>(
|
||||
findOptions_,
|
||||
this.getFreeTextSearchConstraints
|
||||
)
|
||||
|
||||
return await super.findAndCount(findOptions_, context)
|
||||
}
|
||||
|
||||
protected getFreeTextSearchConstraints(q: string) {
|
||||
return [
|
||||
{
|
||||
name: {
|
||||
$ilike: `%${q}%`,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
export * from "./mikro-orm/big-number-field"
|
||||
export * from "./mikro-orm/mikro-orm-create-connection"
|
||||
export * from "./mikro-orm/mikro-orm-free-text-search-filter"
|
||||
export * from "./mikro-orm/mikro-orm-repository"
|
||||
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
|
||||
export * from "./mikro-orm/mikro-orm-serializer"
|
||||
export * from "./mikro-orm/utils"
|
||||
export * from "./mikro-orm/decorators/searchable"
|
||||
export * from "./repositories"
|
||||
export * from "./utils"
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { Searchable } from "../decorators/searchable"
|
||||
|
||||
// Circular dependency one level
|
||||
@Entity()
|
||||
@@ -279,6 +280,60 @@ class Entity2WithUnDecoratedProp {
|
||||
entity1: Entity1WithUnDecoratedProp
|
||||
}
|
||||
|
||||
// Searchable fields
|
||||
|
||||
@Entity()
|
||||
class SearchableEntity1 {
|
||||
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@Searchable()
|
||||
@Property()
|
||||
searchableField: string
|
||||
|
||||
@Searchable()
|
||||
@OneToMany(() => SearchableEntity2, (entity2) => entity2.entity1)
|
||||
entity2 = new Collection<SearchableEntity2>(this)
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class SearchableEntity2 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: SearchableEntity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
this.entity1_id = props.entity1.id
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@Searchable()
|
||||
@Property()
|
||||
searchableField: string
|
||||
|
||||
@ManyToOne(() => SearchableEntity1, { mapToPk: true })
|
||||
entity1_id: string
|
||||
|
||||
@ManyToOne(() => SearchableEntity1, { persist: false })
|
||||
entity1: SearchableEntity1
|
||||
}
|
||||
|
||||
export {
|
||||
RecursiveEntity1,
|
||||
RecursiveEntity2,
|
||||
@@ -291,4 +346,6 @@ export {
|
||||
InternalCircularDependencyEntity1,
|
||||
Entity1WithUnDecoratedProp,
|
||||
Entity2WithUnDecoratedProp,
|
||||
SearchableEntity1,
|
||||
SearchableEntity2,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { MikroORM } from "@mikro-orm/core"
|
||||
import { SearchableEntity1, SearchableEntity2 } from "../__fixtures__/utils"
|
||||
import { mikroOrmFreeTextSearchFilterOptionsFactory } from "../mikro-orm-free-text-search-filter"
|
||||
|
||||
describe("mikroOrmFreeTextSearchFilterOptionsFactory", () => {
|
||||
let orm
|
||||
|
||||
beforeEach(async () => {
|
||||
orm = await MikroORM.init({
|
||||
entities: [SearchableEntity1, SearchableEntity2],
|
||||
dbName: "test",
|
||||
type: "postgresql",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return a filter function that filters entities based on the free text search value", async () => {
|
||||
const entityManager = orm.em.fork()
|
||||
const freeTextSearchValue = "search"
|
||||
|
||||
const models = [SearchableEntity1, SearchableEntity2]
|
||||
|
||||
let filterConstraints = mikroOrmFreeTextSearchFilterOptionsFactory(
|
||||
models
|
||||
).cond(
|
||||
{
|
||||
value: freeTextSearchValue,
|
||||
fromEntity: SearchableEntity1.name,
|
||||
},
|
||||
"read",
|
||||
entityManager
|
||||
)
|
||||
|
||||
expect(filterConstraints).toEqual({
|
||||
$or: [
|
||||
{
|
||||
searchableField: {
|
||||
$ilike: `%${freeTextSearchValue}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity2: {
|
||||
$or: [
|
||||
{
|
||||
searchableField: {
|
||||
$ilike: `%${freeTextSearchValue}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
filterConstraints = mikroOrmFreeTextSearchFilterOptionsFactory(models).cond(
|
||||
{
|
||||
value: freeTextSearchValue,
|
||||
fromEntity: SearchableEntity2.name,
|
||||
},
|
||||
"read",
|
||||
entityManager
|
||||
)
|
||||
|
||||
expect(filterConstraints).toEqual({
|
||||
$or: [
|
||||
{
|
||||
searchableField: {
|
||||
$ilike: `%${freeTextSearchValue}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { MetadataStorage } from "@mikro-orm/core"
|
||||
|
||||
export function Searchable() {
|
||||
return function (target, propertyName) {
|
||||
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor)
|
||||
const prop = meta.properties[propertyName] || {}
|
||||
prop["searchable"] = true
|
||||
meta.properties[prop.name] = prop
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModuleServiceInitializeOptions } from "@medusajs/types"
|
||||
import { TSMigrationGenerator } from "@mikro-orm/migrations"
|
||||
import { isString } from "../../common"
|
||||
import { FilterDef } from "@mikro-orm/core/typings"
|
||||
|
||||
// Monkey patch due to the compilation version issue which prevents us from creating a proper class that extends the TSMigrationGenerator
|
||||
const originalCreateStatement = TSMigrationGenerator.prototype.createStatement
|
||||
@@ -67,8 +68,15 @@ TSMigrationGenerator.prototype.createStatement = function (
|
||||
|
||||
export { TSMigrationGenerator }
|
||||
|
||||
export type Filter = {
|
||||
name?: string
|
||||
} & Omit<FilterDef, "name">
|
||||
|
||||
export async function mikroOrmCreateConnection(
|
||||
database: ModuleServiceInitializeOptions["database"] & { connection?: any },
|
||||
database: ModuleServiceInitializeOptions["database"] & {
|
||||
connection?: any
|
||||
filters?: Record<string, Filter>
|
||||
},
|
||||
entities: any[],
|
||||
pathToMigrations: string
|
||||
) {
|
||||
@@ -100,6 +108,7 @@ export async function mikroOrmCreateConnection(
|
||||
driverOptions,
|
||||
tsNode: process.env.APP_ENV === "development",
|
||||
type: "postgresql",
|
||||
filters: database.filters ?? {},
|
||||
migrations: {
|
||||
path: pathToMigrations,
|
||||
generator: TSMigrationGenerator,
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { EntityClass, EntityProperty } from "@mikro-orm/core/typings"
|
||||
import { EntityMetadata, EntitySchema, ReferenceType } from "@mikro-orm/core"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import type { FindOneOptions, FindOptions } from "@mikro-orm/core/drivers"
|
||||
|
||||
export const FreeTextSearchFilterKey = "freeTextSearch"
|
||||
|
||||
interface FilterArgument {
|
||||
value: string
|
||||
fromEntity: string
|
||||
}
|
||||
|
||||
function getEntityProperties(entity: EntityClass<any> | EntitySchema): {
|
||||
[key: string]: EntityProperty<any>
|
||||
} {
|
||||
return (
|
||||
(entity as EntityClass<any>)?.prototype.__meta?.properties ??
|
||||
(entity as EntitySchema).meta?.properties
|
||||
)
|
||||
}
|
||||
|
||||
function retrieveRelationsConstraints(
|
||||
relation: {
|
||||
targetMeta?: EntityMetadata
|
||||
searchable?: boolean
|
||||
mapToPk?: boolean
|
||||
type: string
|
||||
name: string
|
||||
},
|
||||
models: (EntityClass<any> | EntitySchema)[],
|
||||
searchValue: string,
|
||||
visited: Set<string> = new Set(),
|
||||
shouldStop: boolean = false
|
||||
) {
|
||||
if (shouldStop || !relation.searchable) {
|
||||
return
|
||||
}
|
||||
|
||||
const relationClassName = relation.targetMeta!.className
|
||||
|
||||
visited.add(relationClassName)
|
||||
|
||||
const relationFreeTextSearchWhere: any = []
|
||||
|
||||
const relationClass = models.find((m) => m.name === relation.type)!
|
||||
const relationProperties = getEntityProperties(relationClass)
|
||||
|
||||
for (const propertyConfiguration of Object.values(relationProperties)) {
|
||||
if (
|
||||
!(propertyConfiguration as any).searchable ||
|
||||
propertyConfiguration.reference !== ReferenceType.SCALAR
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
relationFreeTextSearchWhere.push({
|
||||
[propertyConfiguration.name]: {
|
||||
$ilike: `%${searchValue}%`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const innerRelations: EntityProperty[] =
|
||||
(relationClass as EntityClass<any>)?.prototype.__meta?.relations ??
|
||||
(relationClass as EntitySchema).meta?.relations
|
||||
|
||||
for (const innerRelation of innerRelations) {
|
||||
const branchVisited = new Set(Array.from(visited))
|
||||
const innerRelationClassName = innerRelation.targetMeta!.className
|
||||
const isSelfCircularDependency =
|
||||
innerRelationClassName === relationClassName
|
||||
|
||||
if (
|
||||
!isSelfCircularDependency &&
|
||||
branchVisited.has(innerRelationClassName)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
branchVisited.add(innerRelationClassName)
|
||||
|
||||
const innerRelationName = !innerRelation.mapToPk
|
||||
? innerRelation.name
|
||||
: relation.targetMeta!.relations.find(
|
||||
(r) => r.type === innerRelation.type && !r.mapToPk
|
||||
)?.name
|
||||
|
||||
if (!innerRelationName) {
|
||||
throw new Error(
|
||||
`Unable to retrieve the counter part relation definition for the mapToPk relation ${innerRelation.name} on entity ${relation.name}`
|
||||
)
|
||||
}
|
||||
|
||||
const relationConstraints = retrieveRelationsConstraints(
|
||||
{
|
||||
name: innerRelationName,
|
||||
targetMeta: innerRelation.targetMeta,
|
||||
searchable: (innerRelation as any).searchable,
|
||||
mapToPk: innerRelation.mapToPk,
|
||||
type: innerRelation.type,
|
||||
},
|
||||
models,
|
||||
searchValue,
|
||||
branchVisited,
|
||||
isSelfCircularDependency
|
||||
)
|
||||
|
||||
if (!relationConstraints?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
relationFreeTextSearchWhere.push({
|
||||
[innerRelationName]: {
|
||||
$or: relationConstraints,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return relationFreeTextSearchWhere
|
||||
}
|
||||
|
||||
export const mikroOrmFreeTextSearchFilterOptionsFactory = (
|
||||
models: (EntityClass<any> | EntitySchema)[]
|
||||
) => {
|
||||
return {
|
||||
cond: (
|
||||
freeTextSearchArgs: FilterArgument,
|
||||
operation: string,
|
||||
manager: SqlEntityManager,
|
||||
options?: (FindOptions<any, any> | FindOneOptions<any, any>) & {
|
||||
visited?: Set<EntityClass<any>>
|
||||
}
|
||||
) => {
|
||||
if (!freeTextSearchArgs || !freeTextSearchArgs.value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { value, fromEntity } = freeTextSearchArgs
|
||||
|
||||
if (options?.visited?.size) {
|
||||
/**
|
||||
* When being in select in strategy, the filter gets applied to all queries even the ones that are not related to the entity
|
||||
*/
|
||||
const hasFilterAlreadyBeenAppliedForEntity = [
|
||||
...options.visited.values(),
|
||||
].some((v) => v.constructor.name === freeTextSearchArgs.fromEntity)
|
||||
if (hasFilterAlreadyBeenAppliedForEntity) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const entityMetadata = manager.getDriver().getMetadata().get(fromEntity)
|
||||
|
||||
const freeTextSearchWhere = retrieveRelationsConstraints(
|
||||
{
|
||||
targetMeta: entityMetadata,
|
||||
mapToPk: false,
|
||||
searchable: true,
|
||||
type: fromEntity,
|
||||
name: entityMetadata.name!,
|
||||
},
|
||||
models,
|
||||
value
|
||||
)
|
||||
|
||||
if (!freeTextSearchWhere.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
$or: freeTextSearchWhere,
|
||||
}
|
||||
},
|
||||
default: true,
|
||||
args: false,
|
||||
entity: models.map((m) => m.name) as string[],
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,20 @@ import {
|
||||
BaseFilterable,
|
||||
Context,
|
||||
FilterQuery,
|
||||
FindConfig,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
FindConfig,
|
||||
ModulesSdkTypes,
|
||||
UpsertWithReplaceConfig,
|
||||
} from "@medusajs/types"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { EntityClass } from "@mikro-orm/core/typings"
|
||||
import {
|
||||
MedusaError,
|
||||
doNotForceTransaction,
|
||||
isDefined,
|
||||
isObject,
|
||||
isString,
|
||||
lowerCaseFirst,
|
||||
MedusaError,
|
||||
shouldForceTransaction,
|
||||
} from "../common"
|
||||
import { buildQuery } from "./build-query"
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "./decorators"
|
||||
import { UpsertWithReplaceConfig } from "@medusajs/types"
|
||||
import { FreeTextSearchFilterKey } from "../dal"
|
||||
|
||||
type SelectorAndData = {
|
||||
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
|
||||
@@ -53,6 +54,20 @@ export function internalModuleServiceFactory<
|
||||
this[propertyRepositoryName] = container[injectedRepositoryName]
|
||||
}
|
||||
|
||||
static applyFreeTextSearchFilter(
|
||||
filters: FilterQuery,
|
||||
config: FindConfig<any>
|
||||
): void {
|
||||
if (isDefined(filters?.q)) {
|
||||
config.filters ??= {}
|
||||
config.filters[FreeTextSearchFilterKey] = {
|
||||
value: filters.q,
|
||||
fromEntity: model.name,
|
||||
}
|
||||
delete filters.q
|
||||
}
|
||||
}
|
||||
|
||||
static retrievePrimaryKeys(entity: EntityClass<any> | EntitySchema<any>) {
|
||||
return (
|
||||
(entity as EntitySchema<any>).meta?.primaryKeys ??
|
||||
@@ -149,6 +164,8 @@ export function internalModuleServiceFactory<
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
AbstractService_.applyDefaultOrdering(config)
|
||||
AbstractService_.applyFreeTextSearchFilter(filters, config)
|
||||
|
||||
const queryOptions = buildQuery(filters, config)
|
||||
|
||||
return await this[propertyRepositoryName].find(
|
||||
@@ -164,6 +181,8 @@ export function internalModuleServiceFactory<
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
AbstractService_.applyDefaultOrdering(config)
|
||||
AbstractService_.applyFreeTextSearchFilter(filters, config)
|
||||
|
||||
const queryOptions = buildQuery(filters, config)
|
||||
|
||||
return await this[propertyRepositoryName].findAndCount(
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
import { PostgreSqlDriver, SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { asValue } from "awilix"
|
||||
import { ContainerRegistrationKeys, MedusaError } from "../../common"
|
||||
import { mikroOrmCreateConnection } from "../../dal"
|
||||
import {
|
||||
FreeTextSearchFilterKey,
|
||||
mikroOrmCreateConnection,
|
||||
mikroOrmFreeTextSearchFilterOptionsFactory,
|
||||
} from "../../dal"
|
||||
import { loadDatabaseConfig } from "../load-module-database-config"
|
||||
|
||||
/**
|
||||
@@ -17,6 +21,7 @@ import { loadDatabaseConfig } from "../load-module-database-config"
|
||||
* @param moduleName
|
||||
* @param container
|
||||
* @param options
|
||||
* @param filters
|
||||
* @param moduleDeclaration
|
||||
* @param entities
|
||||
* @param pathToMigrations
|
||||
@@ -39,6 +44,9 @@ export async function mikroOrmConnectionLoader({
|
||||
logger?: Logger
|
||||
pathToMigrations: string
|
||||
}) {
|
||||
const freeTextSearchGlobalFilter =
|
||||
mikroOrmFreeTextSearchFilterOptionsFactory(entities)
|
||||
|
||||
let manager = (
|
||||
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
)?.manager
|
||||
@@ -62,7 +70,12 @@ export async function mikroOrmConnectionLoader({
|
||||
shouldSwallowError
|
||||
)
|
||||
return await loadShared({
|
||||
database: dbConfig,
|
||||
database: {
|
||||
...dbConfig,
|
||||
filters: {
|
||||
[FreeTextSearchFilterKey as string]: freeTextSearchGlobalFilter,
|
||||
},
|
||||
},
|
||||
container,
|
||||
entities,
|
||||
pathToMigrations,
|
||||
@@ -87,7 +100,12 @@ export async function mikroOrmConnectionLoader({
|
||||
}
|
||||
|
||||
manager ??= await loadDefault({
|
||||
database: dbConfig,
|
||||
database: {
|
||||
...dbConfig,
|
||||
filters: {
|
||||
[FreeTextSearchFilterKey as string]: freeTextSearchGlobalFilter,
|
||||
},
|
||||
},
|
||||
entities,
|
||||
pathToMigrations,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user