diff --git a/packages/knex/src/query/CriteriaNode.ts b/packages/knex/src/query/CriteriaNode.ts index 11b1e2485db8..8a0f8b8ff344 100644 --- a/packages/knex/src/query/CriteriaNode.ts +++ b/packages/knex/src/query/CriteriaNode.ts @@ -35,7 +35,7 @@ export class CriteriaNode implements ICriteriaNode { return; } - pks.forEach(k => { + for (const k of pks) { this.prop = meta.props.find(prop => prop.name === k || (prop.fieldNames?.length === 1 && prop.fieldNames[0] === k)); const isProp = this.prop || meta.props.find(prop => (prop.fieldNames || []).includes(k)); @@ -43,7 +43,7 @@ export class CriteriaNode implements ICriteriaNode { if (validate && !isProp && !k.includes('.') && !k.includes('::') && !Utils.isOperator(k) && !RawQueryFragment.isKnownFragment(k)) { throw new Error(`Trying to query by not existing property ${entityName}.${k}`); } - }); + } } } diff --git a/packages/knex/src/query/ObjectCriteriaNode.ts b/packages/knex/src/query/ObjectCriteriaNode.ts index 9848716f80e8..ef97a96440f1 100644 --- a/packages/knex/src/query/ObjectCriteriaNode.ts +++ b/packages/knex/src/query/ObjectCriteriaNode.ts @@ -2,6 +2,7 @@ import { ALIAS_REPLACEMENT, type Dictionary, type EntityKey, + type EntityProperty, QueryFlag, raw, RawQueryFragment, @@ -138,6 +139,27 @@ export class ObjectCriteriaNode extends CriteriaNode { return !!this.prop && this.prop.kind !== ReferenceKind.SCALAR && !scalar && !operator; } + private getChildKey(k: EntityKey, prop: EntityProperty, childAlias?: string): string { + const idx = prop.referencedPKs.indexOf(k as EntityKey); + return idx !== -1 && !childAlias && ![ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) ? prop.joinColumns[idx] : k; + } + + private inlineArrayChildPayload(obj: Dictionary, payload: Dictionary[], k: string, prop: EntityProperty, childAlias?: string) { + const key = this.getChildKey(k as EntityKey, prop, childAlias); + const value = payload.map((child: Dictionary) => Object.keys(child).reduce((inner, childKey) => { + if (Utils.isGroupOperator(childKey) && Array.isArray(child[childKey])) { + this.inlineArrayChildPayload(child, child[childKey], childKey, prop, childAlias); + } else { + const key = (this.isPrefixed(childKey) || Utils.isOperator(childKey)) ? childKey : this.aliased(childKey, childAlias); + inner[key] = child[childKey]; + } + + return inner; + }, {} as Dictionary)); + + this.inlineCondition(key, obj, value); + } + private inlineChildPayload(o: Dictionary, payload: Dictionary, field: EntityKey, alias?: string, childAlias?: string) { const prop = this.metadata.find(this.entityName)!.properties[field]; @@ -146,24 +168,11 @@ export class ObjectCriteriaNode extends CriteriaNode { const tmp = payload[k]; delete payload[k]; o[this.aliased(field, alias)] = { [k]: tmp, ...o[this.aliased(field, alias)] }; + } else if (Utils.isGroupOperator(k) && Array.isArray(payload[k])) { + this.inlineArrayChildPayload(o, payload[k], k, prop, childAlias); } else if (this.isPrefixed(k) || Utils.isOperator(k) || !childAlias) { - const idx = prop.referencedPKs.indexOf(k as EntityKey); - const key = idx !== -1 && !childAlias && ![ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) ? prop.joinColumns[idx] : k; - - if (key in o) { - const $and = o.$and ?? []; - $and.push({ [key]: o[key] }, { [key]: payload[k] }); - delete o[key]; - o.$and = $and; - } else if (Utils.isOperator(k) && Array.isArray(payload[k])) { - o[key] = payload[k].map((child: Dictionary) => Object.keys(child).reduce((o, childKey) => { - const key = (this.isPrefixed(childKey) || Utils.isOperator(childKey)) ? childKey : this.aliased(childKey, childAlias); - o[key] = child[childKey]; - return o; - }, {} as Dictionary)); - } else { - o[key] = payload[k]; - } + const key = this.getChildKey(k as EntityKey, prop, childAlias); + this.inlineCondition(key, o, payload[k]); } else if (RawQueryFragment.isKnownFragment(k)) { o[k] = payload[k]; } else { @@ -173,6 +182,17 @@ export class ObjectCriteriaNode extends CriteriaNode { } } + private inlineCondition(key: string, o: Dictionary, value: unknown) { + if (key in o) { + const $and = o.$and ?? []; + $and.push({ [key]: o[key] }, { [key]: value }); + delete o[key]; + o.$and = $and; + } else { + o[key] = value; + } + } + private shouldAutoJoin(qb: IQueryBuilder, nestedAlias: string | undefined): boolean { if (!this.prop || !this.parent) { return false; diff --git a/tests/issues/GH5086.test.ts b/tests/issues/GH5086.test.ts new file mode 100644 index 000000000000..48f045ebfbdd --- /dev/null +++ b/tests/issues/GH5086.test.ts @@ -0,0 +1,204 @@ +import { Collection, Entity, ManyToOne, MikroORM, OneToMany, PrimaryKey, Property } from '@mikro-orm/sqlite'; + +@Entity() +class EntityA { + + @PrimaryKey() + id!: number; + + @Property() + organization!: string; + + @OneToMany({ entity: () => EntityB, mappedBy: 'entityA' }) + entities_b = new Collection(this); + +} + +@Entity() +class FieldB { + + @PrimaryKey() + id!: number; + + @Property() + name!: string; + +} + +@Entity() +class EntityB { + + @PrimaryKey() + id!: number; + + @Property() + organization!: string; + + @Property() + amount!: string; + + @Property() + fieldE!: boolean; + + @Property() + fieldF!: boolean; + + @Property({ nullable: true }) + fieldD?: number; + + @Property({ nullable: true }) + fieldC?: number; + + @ManyToOne({ entity: () => FieldB, nullable: true }) + fieldB?: FieldB; + + @Property() + fieldA!: boolean; + + @ManyToOne({ entity: () => EntityA, nullable: true }) + entityA?: EntityA; + +} + +let orm: MikroORM; + +beforeAll(async () => { + orm = await MikroORM.init({ + dbName: ':memory:', + entities: [EntityA, EntityB], + }); + await orm.schema.refreshDatabase(); + + const entityA = orm.em.create(EntityA, { organization: 'orgId' }); + orm.em.create(EntityB, { + organization: 'orgId', + entityA, + amount: '100', + fieldD: 1, + fieldC: 1, + fieldB: { name: 'anything' }, + fieldA: false, + fieldE: false, + fieldF: false, + }); + await orm.em.flush(); + orm.em.clear(); +}); + +afterAll(async () => { + await orm.close(true); +}); + +test('nesting $and and $or operators with complex conditions 1', async () => { + const results = await orm.em.qb(EntityA) + .select('*') + .where({ + entities_b: { + $and: [ + { + $or: [ + { + fieldB: { + id: { + $nin: [ + 'randomId1', + ], + }, + }, + }, + ], + }, + ], + }, + }); + expect(results).toHaveLength(1); +}); + +test('nesting $and and $or operators with complex conditions 2', async () => { + const results = await orm.em.qb(EntityA) + .select('*') + .where({ + organization: 'orgId', + entities_b: { + organization: 'orgId', + $and: [ + { + $or: [ + { + amount: { + $ne: 0, + }, + }, + { + amount: { + $ne: 0, + }, + }, + ], + }, + { + fieldF: false, + fieldE: false, + }, + { + $and: [ + { + $or: [ + { + fieldD: { + $nin: [ + 2, + 3, + ], + }, + }, + { + fieldD: null, + }, + ], + }, + { + $or: [ + { + fieldC: { + $nin: [ + 2, + 3, + ], + }, + }, + { + fieldC: null, + }, + ], + }, + ], + }, + { + $or: [ + { + fieldB: { + id: { + $nin: [ + 'randomId1', + ], + }, + }, + }, + { + fieldB: null, + }, + ], + }, + { + $or: [ + { + fieldA: false, + }, + ], + }, + ], + }, + }); + expect(results).toHaveLength(1); +});