From 9a2f5350df3101f67c5609aaf4bde0cc6cd17a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Sat, 5 Nov 2022 16:30:00 +0100 Subject: [PATCH] fix(core): handle `$fulltext` search correctly in nested queries Closes #3696 --- packages/knex/src/query/QueryBuilderHelper.ts | 11 ++- tests/issues/GH3696.test.ts | 90 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/issues/GH3696.test.ts diff --git a/packages/knex/src/query/QueryBuilderHelper.ts b/packages/knex/src/query/QueryBuilderHelper.ts index fd8221e88c57..699230f07730 100644 --- a/packages/knex/src/query/QueryBuilderHelper.ts +++ b/packages/knex/src/query/QueryBuilderHelper.ts @@ -486,10 +486,15 @@ export class QueryBuilderHelper { } if (op === '$fulltext') { - const meta = this.metadata.get(this.entityName); - const columnName = key.includes('.') ? key.split('.')[1] : key; + const [a, f] = this.splitField(key); + const prop = this.getProperty(f, a); - qb[m](this.knex.raw(this.platform.getFullTextWhereClause(meta.properties[columnName]), { + /* istanbul ignore next */ + if (!prop) { + throw new Error(`Cannot use $fulltext operator on ${key}, property not found`); + } + + qb[m](this.knex.raw(this.platform.getFullTextWhereClause(prop), { column: this.mapper(key, type, undefined, null), query: value[op], })); diff --git a/tests/issues/GH3696.test.ts b/tests/issues/GH3696.test.ts new file mode 100644 index 000000000000..40784470c2ef --- /dev/null +++ b/tests/issues/GH3696.test.ts @@ -0,0 +1,90 @@ +import { Collection, Entity, Index, ManyToMany, PrimaryKey, Property, Unique } from '@mikro-orm/core'; +import { FullTextType, MikroORM } from '@mikro-orm/postgresql'; + +@Entity() +@Unique({ properties: ['name'] }) +export class Artist { + + @PrimaryKey() + id!: number; + + @Property() + name: string; + + @Index({ type: 'fulltext' }) + @Property({ type: FullTextType, onUpdate: (artist: Artist) => artist.name }) + searchableName!: string; + + constructor(artist: any) { + this.id = artist.id; + this.name = artist.name; + this.searchableName = artist.name; + } + +} + +@Entity() +export class Song { + + @PrimaryKey() + id!: number; + + @Property() + title: string; + + @ManyToMany(() => Artist) + artists = new Collection(this); + + @Index({ type: 'fulltext' }) + @Property({ type: FullTextType, onUpdate: (song: Song) => song.title }) + searchableTitle!: string; + + constructor(song: any) { + this.id = song.id; + this.title = song.title; + this.searchableTitle = song.title; + } + +} + +let orm: MikroORM; + +beforeAll(async () => { + orm = await MikroORM.init({ + entities: [Song], + dbName: 'mikro_orm_test_3696', + }); + await orm.schema.refreshDatabase(); +}); + +afterAll(async () => { + await orm.close(true); +}); + +test('GH issue 3696', async () => { + const artist = orm.em.create(Artist, { + name: 'Taylor Swift', + searchableName: 'Taylor Swift', + }); + const song = orm.em.create(Song, { + title: 'Anti-Hero', + searchableTitle: 'Anti--Hero', + }); + song.artists.add(artist); + await orm.em.flush(); + orm.em.clear(); + + const results = await orm.em.find(Song, { + searchableTitle: { $fulltext: 'anti' }, + artists: { searchableName: { $fulltext: 'taylor' } }, + }, { populate: ['artists'] }); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + title: 'Anti-Hero', + searchableTitle: "'anti':1 'hero':2", + }); + expect(results[0].artists[0]).toMatchObject({ + name: 'Taylor Swift', + searchableName: "'swift':2 'taylor':1", + }); +});