151 lines
5.3 KiB
JavaScript
151 lines
5.3 KiB
JavaScript
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)}`;
|
|
}
|
|
}
|