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:
Adrien de Peretti authored and GitHub committed 2024-04-05 13:59:40 +00:00
1 parent a0005faa14
commit 21990fcd4b
19 files changed
+424 -205

No files matched your search

@@ -1,21 +1,20 @@
import { import {
BeforeCreate, BeforeCreate,
Cascade,
Collection, Collection,
Entity, Entity,
Filter, Filter,
Formula, Formula,
OnInit,
OnLoad,
OneToMany, OneToMany,
OnInit,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { import {
DALUtils,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
DALUtils,
generateEntityId, generateEntityId,
Searchable,
} from "@medusajs/utils" } from "@medusajs/utils"
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
@@ -64,6 +63,7 @@ export class InventoryItem {
deleted_at: Date | null = null deleted_at: Date | null = null
@InventoryItemSkuIndex.MikroORMIndex() @InventoryItemSkuIndex.MikroORMIndex()
@Searchable()
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
sku: string | null = null sku: string | null = null
@@ -94,9 +94,11 @@ export class InventoryItem {
@Property({ columnType: "boolean" }) @Property({ columnType: "boolean" })
requires_shipping: boolean = true requires_shipping: boolean = true
@Searchable()
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
description: string | null = null description: string | null = null
@Searchable()
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
title: string | null = null title: string | null = null
@@ -1,3 +1,2 @@
export * from "./inventory-level" export * from "./inventory-level"
export * from "./inventory-item"
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" 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", () => { 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 () { describe("soft deleted", function () {
let product let product
@@ -4,17 +4,18 @@ import {
Entity, Entity,
Filter, Filter,
Index, Index,
OnInit,
OneToMany, OneToMany,
OnInit,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { import {
DALUtils,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
DALUtils,
generateEntityId, generateEntityId,
kebabCase, kebabCase,
Searchable,
} from "@medusajs/utils" } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
@@ -34,6 +35,7 @@ class ProductCollection {
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
title: string title: string
@@ -1,8 +1,9 @@
import { import {
DALUtils,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
DALUtils,
generateEntityId, generateEntityId,
optionalNumericSerializer, optionalNumericSerializer,
Searchable,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
BeforeCreate, BeforeCreate,
@@ -12,8 +13,8 @@ import {
Filter, Filter,
Index, Index,
ManyToOne, ManyToOne,
OnInit,
OneToMany, OneToMany,
OnInit,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
@@ -76,9 +77,11 @@ class ProductVariant {
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
title: string title: string
@Searchable()
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
sku?: string | null sku?: string | null
+9 -1
View File
@@ -19,6 +19,7 @@ import {
generateEntityId, generateEntityId,
kebabCase, kebabCase,
ProductUtils, ProductUtils,
Searchable,
} from "@medusajs/utils" } from "@medusajs/utils"
import ProductCategory from "./product-category" import ProductCategory from "./product-category"
import ProductCollection from "./product-collection" import ProductCollection from "./product-collection"
@@ -64,6 +65,7 @@ class Product {
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
title: string title: string
@@ -73,7 +75,11 @@ class Product {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
subtitle?: string | null subtitle?: string | null
@Property({ columnType: "text", nullable: true }) @Searchable()
@Property({
columnType: "text",
nullable: true,
})
description?: string | null description?: string | null
@Property({ columnType: "boolean", default: false }) @Property({ columnType: "boolean", default: false })
@@ -91,6 +97,7 @@ class Product {
}) })
options = new Collection<ProductOption>(this) options = new Collection<ProductOption>(this)
@Searchable()
@OneToMany(() => ProductVariant, (variant) => variant.product, { @OneToMany(() => ProductVariant, (variant) => variant.product, {
cascade: ["soft-remove"] as any, cascade: ["soft-remove"] as any,
}) })
@@ -120,6 +127,7 @@ class Product {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
material?: string | null material?: string | null
@Searchable()
@ManyToOne(() => ProductCollection, { @ManyToOne(() => ProductCollection, {
columnType: "text", columnType: "text",
nullable: true, nullable: true,
@@ -15,40 +15,6 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
super(...arguments) 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 * 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 * 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 { import {
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
generateEntityId, generateEntityId,
Searchable,
} from "@medusajs/utils" } from "@medusajs/utils"
import { StockLocationAddress } from "./stock-location-address" import { StockLocationAddress } from "./stock-location-address"
@@ -43,6 +44,7 @@ export class StockLocation {
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null deleted_at: Date | null = null
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
name: string name: string
@@ -1,2 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" 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}%`,
},
},
]
}
}
+2
View File
@@ -1,8 +1,10 @@
export * from "./mikro-orm/big-number-field" export * from "./mikro-orm/big-number-field"
export * from "./mikro-orm/mikro-orm-create-connection" 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-repository"
export * from "./mikro-orm/mikro-orm-soft-deletable-filter" export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
export * from "./mikro-orm/mikro-orm-serializer" export * from "./mikro-orm/mikro-orm-serializer"
export * from "./mikro-orm/utils" export * from "./mikro-orm/utils"
export * from "./mikro-orm/decorators/searchable"
export * from "./repositories" export * from "./repositories"
export * from "./utils" export * from "./utils"
@@ -6,6 +6,7 @@ import {
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Searchable } from "../decorators/searchable"
// Circular dependency one level // Circular dependency one level
@Entity() @Entity()
@@ -279,6 +280,60 @@ class Entity2WithUnDecoratedProp {
entity1: Entity1WithUnDecoratedProp 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 { export {
RecursiveEntity1, RecursiveEntity1,
RecursiveEntity2, RecursiveEntity2,
@@ -291,4 +346,6 @@ export {
InternalCircularDependencyEntity1, InternalCircularDependencyEntity1,
Entity1WithUnDecoratedProp, Entity1WithUnDecoratedProp,
Entity2WithUnDecoratedProp, 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 { ModuleServiceInitializeOptions } from "@medusajs/types"
import { TSMigrationGenerator } from "@mikro-orm/migrations" import { TSMigrationGenerator } from "@mikro-orm/migrations"
import { isString } from "../../common" 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 // 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 const originalCreateStatement = TSMigrationGenerator.prototype.createStatement
@@ -67,8 +68,15 @@ TSMigrationGenerator.prototype.createStatement = function (
export { TSMigrationGenerator } export { TSMigrationGenerator }
export type Filter = {
name?: string
} & Omit<FilterDef, "name">
export async function mikroOrmCreateConnection( export async function mikroOrmCreateConnection(
database: ModuleServiceInitializeOptions["database"] & { connection?: any }, database: ModuleServiceInitializeOptions["database"] & {
connection?: any
filters?: Record<string, Filter>
},
entities: any[], entities: any[],
pathToMigrations: string pathToMigrations: string
) { ) {
@@ -100,6 +108,7 @@ export async function mikroOrmCreateConnection(
driverOptions, driverOptions,
tsNode: process.env.APP_ENV === "development", tsNode: process.env.APP_ENV === "development",
type: "postgresql", type: "postgresql",
filters: database.filters ?? {},
migrations: { migrations: {
path: pathToMigrations, path: pathToMigrations,
generator: TSMigrationGenerator, 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, BaseFilterable,
Context, Context,
FilterQuery, FilterQuery,
FindConfig,
FilterQuery as InternalFilterQuery, FilterQuery as InternalFilterQuery,
FindConfig,
ModulesSdkTypes, ModulesSdkTypes,
UpsertWithReplaceConfig,
} from "@medusajs/types" } from "@medusajs/types"
import { EntitySchema } from "@mikro-orm/core" import { EntitySchema } from "@mikro-orm/core"
import { EntityClass } from "@mikro-orm/core/typings" import { EntityClass } from "@mikro-orm/core/typings"
import { import {
MedusaError,
doNotForceTransaction, doNotForceTransaction,
isDefined, isDefined,
isObject, isObject,
isString, isString,
lowerCaseFirst, lowerCaseFirst,
MedusaError,
shouldForceTransaction, shouldForceTransaction,
} from "../common" } from "../common"
import { buildQuery } from "./build-query" import { buildQuery } from "./build-query"
@@ -23,7 +24,7 @@ import {
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
} from "./decorators" } from "./decorators"
import { UpsertWithReplaceConfig } from "@medusajs/types" import { FreeTextSearchFilterKey } from "../dal"
type SelectorAndData = { type SelectorAndData = {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>> selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
@@ -53,6 +54,20 @@ export function internalModuleServiceFactory<
this[propertyRepositoryName] = container[injectedRepositoryName] 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>) { static retrievePrimaryKeys(entity: EntityClass<any> | EntitySchema<any>) {
return ( return (
(entity as EntitySchema<any>).meta?.primaryKeys ?? (entity as EntitySchema<any>).meta?.primaryKeys ??
@@ -149,6 +164,8 @@ export function internalModuleServiceFactory<
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> { ): Promise<TEntity[]> {
AbstractService_.applyDefaultOrdering(config) AbstractService_.applyDefaultOrdering(config)
AbstractService_.applyFreeTextSearchFilter(filters, config)
const queryOptions = buildQuery(filters, config) const queryOptions = buildQuery(filters, config)
return await this[propertyRepositoryName].find( return await this[propertyRepositoryName].find(
@@ -164,6 +181,8 @@ export function internalModuleServiceFactory<
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> { ): Promise<[TEntity[], number]> {
AbstractService_.applyDefaultOrdering(config) AbstractService_.applyDefaultOrdering(config)
AbstractService_.applyFreeTextSearchFilter(filters, config)
const queryOptions = buildQuery(filters, config) const queryOptions = buildQuery(filters, config)
return await this[propertyRepositoryName].findAndCount( return await this[propertyRepositoryName].findAndCount(
@@ -8,7 +8,11 @@ import {
import { PostgreSqlDriver, SqlEntityManager } from "@mikro-orm/postgresql" import { PostgreSqlDriver, SqlEntityManager } from "@mikro-orm/postgresql"
import { asValue } from "awilix" import { asValue } from "awilix"
import { ContainerRegistrationKeys, MedusaError } from "../../common" import { ContainerRegistrationKeys, MedusaError } from "../../common"
import { mikroOrmCreateConnection } from "../../dal" import {
FreeTextSearchFilterKey,
mikroOrmCreateConnection,
mikroOrmFreeTextSearchFilterOptionsFactory,
} from "../../dal"
import { loadDatabaseConfig } from "../load-module-database-config" import { loadDatabaseConfig } from "../load-module-database-config"
/** /**
@@ -17,6 +21,7 @@ import { loadDatabaseConfig } from "../load-module-database-config"
* @param moduleName * @param moduleName
* @param container * @param container
* @param options * @param options
* @param filters
* @param moduleDeclaration * @param moduleDeclaration
* @param entities * @param entities
* @param pathToMigrations * @param pathToMigrations
@@ -39,6 +44,9 @@ export async function mikroOrmConnectionLoader({
logger?: Logger logger?: Logger
pathToMigrations: string pathToMigrations: string
}) { }) {
const freeTextSearchGlobalFilter =
mikroOrmFreeTextSearchFilterOptionsFactory(entities)
let manager = ( let manager = (
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
)?.manager )?.manager
@@ -62,7 +70,12 @@ export async function mikroOrmConnectionLoader({
shouldSwallowError shouldSwallowError
) )
return await loadShared({ return await loadShared({
database: dbConfig, database: {
...dbConfig,
filters: {
[FreeTextSearchFilterKey as string]: freeTextSearchGlobalFilter,
},
},
container, container,
entities, entities,
pathToMigrations, pathToMigrations,
@@ -87,7 +100,12 @@ export async function mikroOrmConnectionLoader({
} }
manager ??= await loadDefault({ manager ??= await loadDefault({
database: dbConfig, database: {
...dbConfig,
filters: {
[FreeTextSearchFilterKey as string]: freeTextSearchGlobalFilter,
},
},
entities, entities,
pathToMigrations, pathToMigrations,
}) })