diff --git a/packages/core/src/utils/QueryHelper.ts b/packages/core/src/utils/QueryHelper.ts index 2d09da8d64c7..179344ad30ee 100644 --- a/packages/core/src/utils/QueryHelper.ts +++ b/packages/core/src/utils/QueryHelper.ts @@ -1,7 +1,7 @@ import { Reference } from '../entity/Reference'; import { Utils } from './Utils'; import type { AnyEntity, Dictionary, EntityMetadata, EntityProperty, FilterDef, ObjectQuery, FilterQuery } from '../typings'; -import { ARRAY_OPERATORS, GroupOperator } from '../enums'; +import { ARRAY_OPERATORS, GroupOperator, ReferenceType } from '../enums'; import type { Platform } from '../platforms'; import type { MetadataStorage } from '../metadata/MetadataStorage'; import { JsonType } from '../types'; @@ -56,7 +56,17 @@ export class QueryHelper { } if (meta.primaryKeys.every(pk => pk in where) && Utils.getObjectKeysSize(where) === meta.primaryKeys.length) { - return !GroupOperator[key as string] && Object.keys(where).every(k => !Utils.isPlainObject(where[k]) || Object.keys(where[k]).every(v => !Utils.isOperator(v, false))); + return !GroupOperator[key as string] && Object.keys(where).every(k => !Utils.isPlainObject(where[k]) || Object.keys(where[k]).every(v => { + if (Utils.isOperator(v, false)) { + return false; + } + + if ([ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(meta.properties[k].reference)) { + return this.inlinePrimaryKeyObjects(where[k], meta.properties[k].targetMeta, metadata, v); + } + + return true; + })); } Object.keys(where).forEach(k => { diff --git a/packages/knex/src/query/ObjectCriteriaNode.ts b/packages/knex/src/query/ObjectCriteriaNode.ts index f423c4ecf0b0..7beaa852538b 100644 --- a/packages/knex/src/query/ObjectCriteriaNode.ts +++ b/packages/knex/src/query/ObjectCriteriaNode.ts @@ -105,7 +105,16 @@ export class ObjectCriteriaNode extends CriteriaNode { const embeddable = this.prop.reference === ReferenceType.EMBEDDED; const knownKey = [ReferenceType.SCALAR, ReferenceType.MANY_TO_ONE, ReferenceType.EMBEDDED].includes(this.prop.reference) || (this.prop.reference === ReferenceType.ONE_TO_ONE && this.prop.owner); const operatorKeys = knownKey && Object.keys(this.payload).every(key => Utils.isOperator(key, false)); - const primaryKeys = knownKey && Object.keys(this.payload).every(key => this.metadata.find(this.entityName)!.primaryKeys.includes(key)); + const primaryKeys = knownKey && Object.keys(this.payload).every(key => { + const meta = this.metadata.find(this.entityName)!; + if (!meta.primaryKeys.includes(key)) { + return false; + } + if (!Utils.isPlainObject(this.payload[key].payload) || ![ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(meta.properties[key].reference)) { + return true; + } + return Object.keys(this.payload[key].payload).every(k => meta.properties[key].targetMeta!.primaryKeys.includes(k)); + }); return !primaryKeys && !nestedAlias && !operatorKeys && !embeddable; } diff --git a/tests/issues/GH2648.test.ts b/tests/issues/GH2648.test.ts new file mode 100644 index 000000000000..1d637b1bde0c --- /dev/null +++ b/tests/issues/GH2648.test.ts @@ -0,0 +1,130 @@ +import { Entity, IdentifiedReference, JsonType, ManyToOne, MikroORM, OneToOne, PrimaryKey, PrimaryKeyType, Property } from '@mikro-orm/core'; +import type { SqliteDriver } from '@mikro-orm/sqlite'; + +@Entity() +export class A { + + @PrimaryKey() + id!: number; + + @Property({ type: 'string' }) + test!: string; + +} + +@Entity() +export class B1 { + + [PrimaryKeyType]: number; + @ManyToOne({ entity: () => A, primary: true, wrappedReference: true }) + a!: IdentifiedReference; + +} + +@Entity() +export class B2 { + + @PrimaryKey() + id!: number; + + [PrimaryKeyType]: number; + @OneToOne({ entity: () => A, primary: true, wrappedReference: true }) + a!: IdentifiedReference; + +} + +@Entity() +export class B3 { + + [PrimaryKeyType]: number; + @OneToOne({ entity: () => A, primary: true, wrappedReference: true }) + a!: IdentifiedReference; + +} + +@Entity() +export class B4 { + + @PrimaryKey() + id!: number; + + [PrimaryKeyType]: number; + @OneToOne({ entity: () => A, primary: true, wrappedReference: true }) + a!: IdentifiedReference; + +} + +@Entity() +export class C { + + @PrimaryKey({ type: Number }) + id!: number; + + @ManyToOne({ entity: () => B1, wrappedReference: true }) + b1!: IdentifiedReference; + + @ManyToOne({ entity: () => B2, wrappedReference: true }) + b2!: IdentifiedReference; + + @ManyToOne({ entity: () => B3, wrappedReference: true }) + b3!: IdentifiedReference; + + @ManyToOne({ entity: () => B4, wrappedReference: true }) + b4!: IdentifiedReference; + +} + +interface Test { + t1: string; + t2: string; +} + +@Entity() +export class D { + + @PrimaryKey({ type: JsonType }) + id!: Test; + +} + +describe('GH issue 2648', () => { + + let orm: MikroORM; + + beforeAll(async () => { + orm = await MikroORM.init({ + entities: [A, B1, B2, B3, B4, C, D], + dbName: ':memory:', + type: 'sqlite', + }); + await orm.getSchemaGenerator().createSchema(); + }); + + afterAll(() => orm.close(true)); + + test('JSON as pk', async () => { + const r = await orm.em.findOne(D, { id: { t1: 'a', t2: 'b' } }); + expect(r).toBeNull(); + }); + + test('fk as pk with ManyToOne', async () => { + const r = await orm.em.findOne(C, { b1: { a: { test: 'test' } } }); + expect(r).toBeNull(); + }); + + test('fk as pk with ManyToOne and with additional primary key', async () => { + const r = await orm.em.findOne(C, { b2: { a: { test: 'test' } } }); + expect(r).toBeNull(); + }); + + test('fk as pk with OneToOne', async () => { + const r = await orm.em.findOne(C, { b3: { a: { test: 'test' } } }); + expect(r).toBeNull(); + }); + + test('fk as pk with OneToOne and with additional primary key', async () => { + const r = await orm.em.findOne(C, { b4: { a: { test: 'test' } } }); + expect(r).toBeNull(); + }); + +});