import { GroupOperator, isRaw, JsonType, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core'; import { ObjectCriteriaNode } from './ObjectCriteriaNode.js'; import { ArrayCriteriaNode } from './ArrayCriteriaNode.js'; import { ScalarCriteriaNode } from './ScalarCriteriaNode.js'; import { EMBEDDABLE_ARRAY_OPS } from './enums.js'; /** * @internal */ export class CriteriaNodeFactory { static createNode(metadata, entityName, payload, parent, key, validate = true) { const rawField = RawQueryFragment.isKnownFragmentSymbol(key); const scalar = Utils.isPrimaryKey(payload) || isRaw(payload) || payload instanceof RegExp || payload instanceof Date || rawField; if (Array.isArray(payload) && !scalar) { return this.createArrayNode(metadata, entityName, payload, parent, key, validate); } if (Utils.isPlainObject(payload) && !scalar) { return this.createObjectNode(metadata, entityName, payload, parent, key, validate); } return this.createScalarNode(metadata, entityName, payload, parent, key, validate); } static createScalarNode(metadata, entityName, payload, parent, key, validate = true) { const node = new ScalarCriteriaNode(metadata, entityName, parent, key, validate); node.payload = payload; return node; } static createArrayNode(metadata, entityName, payload, parent, key, validate = true) { const node = new ArrayCriteriaNode(metadata, entityName, parent, key, validate); node.payload = payload.map((item, index) => { const n = this.createNode(metadata, entityName, item, node, undefined, validate); // we care about branching only for $and if (key === '$and' && payload.length > 1) { n.index = index; } return n; }); return node; } static createObjectNode(metadata, entityName, payload, parent, key, validate = true) { const meta = metadata.find(entityName); const node = new ObjectCriteriaNode(metadata, entityName, parent, key, validate, payload.__strict); node.payload = {}; for (const k of Utils.getObjectQueryKeys(payload)) { node.payload[k] = this.createObjectItemNode(metadata, entityName, node, payload, k, meta, validate); } return node; } static createObjectItemNode(metadata, entityName, node, payload, key, meta, validate = true) { const rawField = RawQueryFragment.isKnownFragmentSymbol(key); const prop = rawField ? null : meta?.properties[key]; const childEntity = prop && prop.kind !== ReferenceKind.SCALAR ? prop.targetMeta.class : entityName; const isNotEmbedded = rawField || prop?.kind !== ReferenceKind.EMBEDDED; const val = payload[key]; if (isNotEmbedded && prop?.customType instanceof JsonType) { return this.createScalarNode(metadata, childEntity, val, node, key, validate); } if (prop?.kind === ReferenceKind.SCALAR && val != null && Object.keys(val).some(f => f in GroupOperator)) { throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload); } if (isNotEmbedded) { return this.createNode(metadata, childEntity, val, node, key, validate); } if (val == null) { const map = Object.keys(prop.embeddedProps).reduce((oo, k) => { oo[prop.embeddedProps[k].name] = null; return oo; }, {}); return this.createNode(metadata, entityName, map, node, key, validate); } // For array embeddeds stored as real columns, route property-level queries // as scalar nodes so QueryBuilderHelper generates EXISTS subqueries with // JSON array iteration. Keys containing `~` indicate the property lives // inside a parent's object-mode JSON column (MetadataDiscovery uses `~` as // the glue for object embeds), where JSON path access is used instead. if (prop.array && !String(key).includes('~')) { const keys = Object.keys(val); const hasOnlyArrayOps = keys.every(k => EMBEDDABLE_ARRAY_OPS.includes(k)); if (!hasOnlyArrayOps) { return this.createScalarNode(metadata, entityName, val, node, key, validate); } } // array operators can be used on embedded properties const operator = Object.keys(val).some(f => Utils.isOperator(f) && !EMBEDDABLE_ARRAY_OPS.includes(f)); if (operator) { throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload); } const map = Object.keys(val).reduce((oo, k) => { const embeddedProp = prop.embeddedProps[k] ?? Object.values(prop.embeddedProps).find(p => p.name === k); if (!embeddedProp && !EMBEDDABLE_ARRAY_OPS.includes(k)) { throw ValidationError.invalidEmbeddableQuery(entityName, k, prop.type); } if (embeddedProp) { oo[embeddedProp.name] = val[k]; } else if (typeof val[k] === 'object') { oo[k] = JSON.stringify(val[k]); } else { oo[k] = val[k]; } return oo; }, {}); return this.createNode(metadata, entityName, map, node, key, validate); } }