import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core'; import { CriteriaNode } from './CriteriaNode.js'; import { JoinType, QueryType } from './enums.js'; const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size']; /** * @internal */ export class ObjectCriteriaNode extends CriteriaNode { process(qb, options) { const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind)); const nestedAlias = qb.getAliasForJoinPath(this.getPath(options), { ...options, matchPopulateJoins }); const ownerAlias = options?.alias || qb.alias; const keys = Utils.getObjectQueryKeys(this.payload); let alias = options?.alias; if (nestedAlias) { alias = nestedAlias; } if (this.shouldAutoJoin(qb, nestedAlias)) { if (keys.some(k => COLLECTION_OPERATORS.includes(k))) { if (![ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(this.prop.kind)) { // ignore collection operators when used on a non-relational property - this can happen when they get into // populateWhere via `infer` on m:n properties with select-in strategy if (this.parent?.parent) { // we validate only usage on top level return {}; } throw new Error( `Collection operators can be used only inside a collection property context, but it was used for ${this.getPath()}.`, ); } const $and = []; const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) || (this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner); const parentMeta = this.metadata.find(this.parent.entityName); const primaryKeys = parentMeta.primaryKeys.map(pk => { return [QueryType.SELECT, QueryType.COUNT].includes(qb.type) ? `${knownKey ? alias : ownerAlias}.${pk}` : pk; }); for (const key of keys) { if (typeof key !== 'string' || !COLLECTION_OPERATORS.includes(key)) { throw new Error('Mixing collection operators with other filters is not allowed.'); } const payload = this.payload[key].unwrap(); const qb2 = qb.clone(true, ['schema']); const joinAlias = qb2.getNextAlias(this.prop.targetMeta.class); const sub = qb2 .from(parentMeta.class) // eslint-disable-next-line no-unexpected-multiline [key === '$size' ? 'leftJoin' : 'innerJoin'](this.key, joinAlias) .select(parentMeta.primaryKeys); if (key === '$size') { const sizeCondition = typeof payload === 'number' ? { $eq: payload } : payload; const pks = this.prop.referencedColumnNames; const countExpr = raw( `count(${pks.map(() => '??').join(', ')})`, pks.map(pk => `${joinAlias}.${pk}`), ); sub.groupBy(parentMeta.primaryKeys); sub.having({ $and: Object.keys(sizeCondition).map(op => ({ [countExpr]: { [op]: sizeCondition[op] } })), }); } else if (key === '$every') { sub.where({ $not: { [this.key]: payload } }); } else { sub.where({ [this.key]: payload }); } const op = ['$size', '$some'].includes(key) ? '$in' : '$nin'; $and.push({ [Utils.getPrimaryKeyHash(primaryKeys)]: { [op]: sub.getNativeQuery().toRaw() }, }); } if ($and.length === 1) { return $and[0]; } return { $and }; } alias = this.autoJoin(qb, ownerAlias, options); } if (this.prop && nestedAlias) { const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind); // if the property is nullable and the filter is strict, we need to use left join, so we mimic the inner join behaviour // with an exclusive condition on the join columns: // - if the owning column is null, the row is missing, we don't apply the filter // - if the target column is not null, the row is matched, we apply the filter if (toOneProperty && this.prop.nullable && this.isStrict()) { const key = this.prop.owner ? this.prop.name : this.prop.referencedPKs; qb.andWhere({ $or: [ { [ownerAlias + '.' + key]: null }, { [nestedAlias + '.' + Utils.getPrimaryKeyHash(this.prop.referencedPKs)]: { $ne: null } }, ], }); } } return keys.reduce((o, field) => { const childNode = this.payload[field]; const payload = childNode.process(qb, { ...options, alias: this.prop ? alias : ownerAlias }); const operator = Utils.isOperator(field); const isRawField = RawQueryFragment.isKnownFragmentSymbol(field); // we need to keep the prefixing for formulas otherwise we would lose aliasing context when nesting inside group operators const virtual = childNode.prop?.persist === false && !childNode.prop?.formula && !!options?.type; // if key is missing, we are inside group operator and we need to prefix with alias const primaryKey = this.key && this.metadata.find(this.entityName)?.primaryKeys.includes(field); const isToOne = childNode.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(childNode.prop.kind); if (childNode.shouldInline(payload)) { const childAlias = qb.getAliasForJoinPath(childNode.getPath(), { preferNoBranch: isToOne, ...options }); const a = qb.helper.isTableNameAliasRequired(qb.type) ? alias : undefined; this.inlineChildPayload(o, payload, field, a, childAlias); } else if (childNode.shouldRename(payload)) { this.inlineCondition(childNode.renameFieldToPK(qb, alias), o, payload); } else if (isRawField) { const rawField = RawQueryFragment.getKnownFragment(field); o[raw(rawField.sql.replaceAll(ALIAS_REPLACEMENT, alias), rawField.params)] = payload; } else if (!childNode.validate && !childNode.prop && !field.includes('.') && !operator) { // wrap unknown fields in raw() to prevent alias prefixing (e.g. raw SQL aliases in HAVING) // use '??' placeholder to properly quote the identifier o[raw('??', [field])] = payload; } else if ( primaryKey || virtual || operator || field.includes('.') || ![QueryType.SELECT, QueryType.COUNT].includes(qb.type) ) { this.inlineCondition(field.replaceAll(ALIAS_REPLACEMENT, alias), o, payload); } else { this.inlineCondition(`${alias ?? qb.alias}.${field}`, o, payload); } return o; }, {}); } isStrict() { return ( this.strict || Utils.getObjectQueryKeys(this.payload).some(key => { return this.payload[key].isStrict(); }) ); } unwrap() { return Utils.getObjectQueryKeys(this.payload).reduce((o, field) => { o[field] = this.payload[field].unwrap(); return o; }, {}); } willAutoJoin(qb, alias, options) { const nestedAlias = qb.getAliasForJoinPath(this.getPath(options), options); const ownerAlias = alias || qb.alias; const keys = Utils.getObjectQueryKeys(this.payload); if (nestedAlias) { alias = nestedAlias; } if (this.shouldAutoJoin(qb, nestedAlias)) { return !keys.some(k => COLLECTION_OPERATORS.includes(k)); } return keys.some(field => { const childNode = this.payload[field]; return childNode.willAutoJoin(qb, this.prop ? alias : ownerAlias, options); }); } shouldInline(payload) { const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key); const scalar = Utils.isPrimaryKey(payload) || payload instanceof RegExp || payload instanceof Date || rawField; const operator = Utils.isObject(payload) && Utils.getObjectQueryKeys(payload).every(k => { if (k === '$not' && Utils.isPlainObject(payload[k])) { // $not wrapping non-operator conditions (entity props) should be inlined return Utils.getObjectQueryKeys(payload[k]).every(ik => Utils.isOperator(ik, false)); } return Utils.isOperator(k, false); }); return !!this.prop && this.prop.kind !== ReferenceKind.SCALAR && !scalar && !operator; } getChildKey(k, prop, childAlias, alias) { const idx = prop.referencedPKs.indexOf(k); return idx !== -1 && !childAlias && ![ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) ? this.aliased(prop.joinColumns[idx], alias) : k; } inlineArrayChildPayload(obj, payload, k, prop, childAlias, alias) { const key = this.getChildKey(k, prop, childAlias); const value = payload.map(child => Utils.getObjectQueryKeys(child).reduce((inner, childKey) => { const key = RawQueryFragment.isKnownFragmentSymbol(childKey) || this.isPrefixed(childKey) || Utils.isOperator(childKey) ? childKey : this.aliased(childKey, childAlias); inner[key] = child[childKey]; return inner; }, {}), ); this.inlineCondition(key, obj, value); } inlineChildPayload(o, payload, field, alias, childAlias) { const prop = this.metadata.find(this.entityName).properties[field]; for (const k of Utils.getObjectQueryKeys(payload)) { if (RawQueryFragment.isKnownFragmentSymbol(k)) { o[k] = payload[k]; } else if ( k === '$not' && Utils.isPlainObject(payload[k]) && Utils.getObjectQueryKeys(payload[k]).some(ik => !Utils.isOperator(ik, false)) ) { // $not wraps entity conditions (from auto-join), inline at current level this.inlineCondition(k, o, payload[k]); } else if (Utils.isOperator(k, false)) { const tmp = payload[k]; delete payload[k]; o[this.aliased(field, alias)] = { [k]: tmp, ...o[this.aliased(field, alias)] }; } else if (k in GroupOperator && Array.isArray(payload[k])) { this.inlineArrayChildPayload(o, payload[k], k, prop, childAlias, alias); } else if (this.isPrefixed(k) || Utils.isOperator(k) || !childAlias) { const key = this.getChildKey(k, prop, childAlias, alias); this.inlineCondition(key, o, payload[k]); } else { o[this.aliased(k, childAlias)] = payload[k]; } } } inlineCondition(key, o, value) { if (!(key in o)) { o[key] = value; return; } /* v8 ignore next */ if (key === '$and') { o.$and.push({ [key]: value }); return; } const $and = o.$and ?? []; $and.push({ [key]: o[key] }, { [key]: value }); delete o[key]; o.$and = $and; } shouldAutoJoin(qb, nestedAlias) { if (!this.prop || !this.parent) { return false; } const keys = Utils.getObjectQueryKeys(this.payload); if (keys.every(k => typeof k === 'string' && k.includes('.') && k.startsWith(`${qb.alias}.`))) { return false; } if (keys.some(k => COLLECTION_OPERATORS.includes(k))) { return true; } const meta = this.metadata.find(this.entityName); const embeddable = this.prop.kind === ReferenceKind.EMBEDDED; const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) || (this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner); const operatorKeys = knownKey && keys.every(key => { if (key === '$not') { // $not wraps conditions like $and/$or, check if it wraps entity property conditions (needs auto-join) // vs simple operator conditions on the FK (doesn't need auto-join) const childPayload = this.payload[key].payload; if (Utils.isPlainObject(childPayload)) { return Utils.getObjectQueryKeys(childPayload).every(k => Utils.isOperator(k, false)); } } return Utils.isOperator(key, false); }); const primaryKeys = knownKey && keys.every(key => { if (typeof key !== 'string' || !meta.primaryKeys.includes(key)) { return false; } if ( !Utils.isPlainObject(this.payload[key].payload) || ![ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(meta.properties[key].kind) ) { return true; } return Utils.getObjectQueryKeys(this.payload[key].payload).every( k => typeof k === 'string' && meta.properties[key].targetMeta.primaryKeys.includes(k), ); }); return !primaryKeys && !nestedAlias && !operatorKeys && !embeddable; } autoJoin(qb, alias, options) { const nestedAlias = qb.getNextAlias(this.prop?.pivotEntity ?? this.entityName); const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key); const scalar = Utils.isPrimaryKey(this.payload) || this.payload instanceof RegExp || this.payload instanceof Date || rawField; const operator = Utils.isPlainObject(this.payload) && Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false)); const field = `${alias}.${this.prop.name}`; const method = qb.hasFlag(QueryFlag.INFER_POPULATE) ? 'joinAndSelect' : 'join'; const path = this.getPath(); if (this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator)) { qb.join(field, nestedAlias, undefined, JoinType.pivotJoin, path); } else { const prev = qb.state.fields?.slice(); const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind); const joinType = toOneProperty && !this.prop.nullable ? JoinType.innerJoin : JoinType.leftJoin; qb[method](field, nestedAlias, undefined, joinType, path); if (!qb.hasFlag(QueryFlag.INFER_POPULATE)) { qb.state.fields = prev; } } if (options?.type !== 'orderBy') { qb.scheduleFilterCheck(path); } return nestedAlias; } isPrefixed(field) { return !!/\w+\./.exec(field); } }