import { RawQueryFragment, ReferenceKind, Utils, inspect } from '@mikro-orm/core'; /** * Helper for working with deeply nested where/orderBy/having criteria. Uses composite pattern to build tree from the payload. * Auto-joins relations and converts payload from { books: { publisher: { name: '...' } } } to { 'publisher_alias.name': '...' } * @internal */ export class CriteriaNode { metadata; entityName; parent; key; validate; strict; payload; prop; index; constructor(metadata, entityName, parent, key, validate = true, strict = false) { this.metadata = metadata; this.entityName = entityName; this.parent = parent; this.key = key; this.validate = validate; this.strict = strict; const meta = parent && metadata.find(parent.entityName); if (meta && key && !RawQueryFragment.isKnownFragmentSymbol(key)) { const pks = Utils.splitPrimaryKeys(key); if (pks.length > 1) { return; } for (const k of pks) { this.prop = meta.props.find( prop => prop.name === k || (prop.fieldNames?.length === 1 && prop.fieldNames[0] === k && prop.persist !== false), ); 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)) { throw new Error(`Trying to query by not existing property ${Utils.className(entityName)}.${k}`); } } } } process(qb, options) { return this.payload; } unwrap() { return this.payload; } shouldInline(payload) { return false; } willAutoJoin(qb, alias, options) { return false; } shouldRename(payload) { const type = this.prop ? this.prop.kind : null; const composite = this.prop?.joinColumns ? this.prop.joinColumns.length > 1 : false; const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key); const scalar = payload === null || Utils.isPrimaryKey(payload) || payload instanceof RegExp || payload instanceof Date || rawField; const operator = Utils.isPlainObject(payload) && Utils.getObjectQueryKeys(payload).every(k => Utils.isOperator(k, false)); if (composite) { return true; } switch (type) { case ReferenceKind.MANY_TO_ONE: return false; case ReferenceKind.ONE_TO_ONE: return !this.prop.owner; case ReferenceKind.ONE_TO_MANY: return scalar || operator; case ReferenceKind.MANY_TO_MANY: return scalar || operator; default: return false; } } renameFieldToPK(qb, ownerAlias) { const joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true }); if ( !joinAlias && this.parent && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) && this.prop.owner ) { const alias = qb.getAliasForJoinPath(this.parent.getPath()) ?? ownerAlias ?? qb.alias; return Utils.getPrimaryKeyHash(this.prop.joinColumns.map(col => `${alias}.${col}`)); } const alias = joinAlias ?? ownerAlias ?? qb.alias; if (this.prop.kind === ReferenceKind.MANY_TO_MANY) { return Utils.getPrimaryKeyHash(this.prop.inverseJoinColumns.map(col => `${alias}.${col}`)); } return Utils.getPrimaryKeyHash(this.prop.referencedColumnNames.map(col => `${alias}.${col}`)); } getPath(opts) { // use index on parent only if we are processing to-many relation const addParentIndex = this.prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(this.prop.kind); const parentPath = opts?.parentPath ?? this.parent?.getPath({ addIndex: addParentIndex }) ?? Utils.className(this.entityName); const index = opts?.addIndex && this.index != null ? `[${this.index}]` : ''; // ignore group operators to allow easier mapping (e.g. for orderBy) const key = this.key && !RawQueryFragment.isKnownFragmentSymbol(this.key) && !['$and', '$or', '$not'].includes(this.key) ? '.' + this.key : ''; const ret = parentPath + index + key; if (this.isPivotJoin()) { // distinguish pivot table join from target entity join return this.getPivotPath(ret); } return ret; } isPivotJoin() { if (!this.key || !this.prop) { return false; } const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key); const scalar = this.payload === null || Utils.isPrimaryKey(this.payload) || this.payload instanceof RegExp || this.payload instanceof Date || rawField; const operator = Utils.isObject(this.payload) && Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false)); return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator); } getPivotPath(path) { return `${path}[pivot]`; } aliased(field, alias) { return alias ? `${alias}.${field}` : field; } isStrict() { return this.strict; } /** @ignore */ /* v8 ignore next */ [Symbol.for('nodejs.util.inspect.custom')]() { const o = {}; ['entityName', 'key', 'index', 'payload'].filter(k => this[k] !== undefined).forEach(k => (o[k] = this[k])); return `${this.constructor.name} ${inspect(o)}`; } }