Skip to content

Commit

Permalink
fix(query-builder): fix aliasing of FK when used in deeply nested and…
Browse files Browse the repository at this point in the history
…/or conditions

Closes #5086
  • Loading branch information
B4nan committed Mar 18, 2024
1 parent 8799461 commit ebb966c
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 19 deletions.
4 changes: 2 additions & 2 deletions packages/knex/src/query/CriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ export class CriteriaNode<T extends object> implements ICriteriaNode<T> {
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));

// do not validate if the key is prefixed or type casted (e.g. `k::text`)
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}`);
}
});
}
}
}

Expand Down
54 changes: 37 additions & 17 deletions packages/knex/src/query/ObjectCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ALIAS_REPLACEMENT,
type Dictionary,
type EntityKey,
type EntityProperty,
QueryFlag,
raw,
RawQueryFragment,
Expand Down Expand Up @@ -138,6 +139,27 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
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<T>(o: Dictionary, payload: Dictionary, field: EntityKey<T>, alias?: string, childAlias?: string) {
const prop = this.metadata.find<T>(this.entityName)!.properties[field];

Expand All @@ -146,24 +168,11 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
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 {
Expand All @@ -173,6 +182,17 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
}
}

private inlineCondition(key: string, o: Dictionary<any>, 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<T>, nestedAlias: string | undefined): boolean {
if (!this.prop || !this.parent) {
return false;
Expand Down
204 changes: 204 additions & 0 deletions tests/issues/GH5086.test.ts
Original file line number Diff line number Diff line change
@@ -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<EntityB>(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);
});

0 comments on commit ebb966c

Please sign in to comment.