'use strict'; var _ = require('lodash/fp'); var utils = require('@strapi/utils'); var types = require('../../utils/types.js'); var index = require('../../fields/index.js'); var join = require('./join.js'); var transform = require('./transform.js'); var knex = require('../../utils/knex.js'); const isRecord = (value)=>_.isPlainObject(value); const castValue = (value, attribute)=>{ if (!attribute) { return value; } if (types.isScalar(attribute.type) && !knex.isKnexQuery(value)) { const field = index.createField(attribute); return value === null ? null : field.toDB(value); } return value; }; const processSingleAttributeWhere = (attribute, where, operator = '$eq')=>{ if (!isRecord(where)) { if (utils.isOperatorOfType('cast', operator)) { return castValue(where, attribute); } return where; } const filters = {}; for (const key of Object.keys(where)){ const value = where[key]; if (!utils.isOperatorOfType('where', key)) { throw new Error(`Undefined attribute level operator ${key}`); } filters[key] = processAttributeWhere(attribute, value, key); } return filters; }; const processAttributeWhere = (attribute, where, operator = '$eq')=>{ if (_.isArray(where)) { return where.map((sub)=>processSingleAttributeWhere(attribute, sub, operator)); } return processSingleAttributeWhere(attribute, where, operator); }; const processNested = (where, ctx)=>{ if (!isRecord(where)) { return where; } return processWhere(where, ctx); }; const processRelationWhere = (where, ctx)=>{ const { qb, alias } = ctx; const idAlias = qb.aliasColumn('id', alias); if (!isRecord(where)) { return { [idAlias]: where }; } const keys = Object.keys(where); const operatorKeys = keys.filter((key)=>utils.isOperator(key)); if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) { throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`); } if (operatorKeys.length > 1) { throw new Error(`Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`); } if (operatorKeys.length === 1) { const operator = operatorKeys[0]; if (utils.isOperatorOfType('group', operator)) { return processWhere(where, ctx); } return { [idAlias]: { [operator]: processNested(where[operator], ctx) } }; } return processWhere(where, ctx); }; function processWhere(where, ctx) { if (!_.isArray(where) && !isRecord(where)) { throw new Error('Where must be an array or an object'); } if (_.isArray(where)) { return where.map((sub)=>processWhere(sub, ctx)); } const { db, uid, qb, alias } = ctx; const meta = db.metadata.get(uid); const filters = {}; // for each key in where for (const key of Object.keys(where)){ const value = where[key]; // if operator $and $or -> process recursively if (utils.isOperatorOfType('group', key)) { if (!Array.isArray(value)) { throw new Error(`Operator ${key} must be an array`); } filters[key] = value.map((sub)=>processNested(sub, ctx)); continue; } if (key === '$not') { filters[key] = processNested(value, ctx); continue; } if (utils.isOperatorOfType('where', key)) { throw new Error(`Only $and, $or and $not can only be used as root level operators. Found ${key}.`); } const attribute = meta.attributes[key]; if (!attribute) { filters[qb.aliasColumn(key, alias)] = processAttributeWhere(null, value); continue; } if (types.isRelation(attribute.type) && 'target' in attribute) { // attribute const subAlias = join.createJoin(ctx, { alias: alias || qb.alias, attributeName: key, attribute }); const nestedWhere = processRelationWhere(value, { db, qb, alias: subAlias, uid: attribute.target }); // TODO: use a better merge logic (push to $and when collisions) Object.assign(filters, nestedWhere); continue; } if (types.isScalar(attribute.type)) { const columnName = transform.toColumnName(meta, key); const aliasedColumnName = qb.aliasColumn(columnName, alias); filters[aliasedColumnName] = processAttributeWhere(attribute, value); continue; } throw new Error(`You cannot filter on ${attribute.type} types`); } return filters; } // TODO: add type casting per operator at some point const applyOperator = (qb, column, operator, value)=>{ if (Array.isArray(value) && !utils.isOperatorOfType('array', operator)) { return qb.where((subQB)=>{ value.forEach((subValue)=>subQB.orWhere((innerQB)=>{ applyOperator(innerQB, column, operator, subValue); })); }); } switch(operator){ case '$not': { qb.whereNot((qb)=>applyWhereToColumn(qb, column, value)); break; } case '$in': { // @ts-ignore // TODO: fix in v5 qb.whereIn(column, knex.isKnexQuery(value) ? value : _.castArray(value)); break; } case '$notIn': { // @ts-ignore // TODO: fix in v5 qb.whereNotIn(column, knex.isKnexQuery(value) ? value : _.castArray(value)); break; } case '$eq': { if (value === null) { qb.whereNull(column); break; } qb.where(column, value); break; } case '$eqi': { if (value === null) { qb.whereNull(column); break; } qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [ column, `${value}` ]); break; } case '$ne': { if (value === null) { qb.whereNotNull(column); break; } qb.where(column, '<>', value); break; } case '$nei': { if (value === null) { qb.whereNotNull(column); break; } qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [ column, `${value}` ]); break; } case '$gt': { qb.where(column, '>', value); break; } case '$gte': { qb.where(column, '>=', value); break; } case '$lt': { qb.where(column, '<', value); break; } case '$lte': { qb.where(column, '<=', value); break; } case '$null': { if (value) { qb.whereNull(column); } else { qb.whereNotNull(column); } break; } case '$notNull': { if (value) { qb.whereNotNull(column); } else { qb.whereNull(column); } break; } case '$between': { qb.whereBetween(column, value); break; } case '$startsWith': { qb.where(column, 'like', `${value}%`); break; } case '$startsWithi': { qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [ column, `${value}%` ]); break; } case '$endsWith': { qb.where(column, 'like', `%${value}`); break; } case '$endsWithi': { qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [ column, `%${value}` ]); break; } case '$contains': { qb.where(column, 'like', `%${value}%`); break; } case '$notContains': { qb.whereNot(column, 'like', `%${value}%`); break; } case '$containsi': { qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [ column, `%${value}%` ]); break; } case '$notContainsi': { qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [ column, `%${value}%` ]); break; } // Experimental, only for internal use // Only on MySQL, PostgreSQL and CockroachDB. // https://knexjs.org/guide/query-builder.html#wherejsonsupersetof case '$jsonSupersetOf': { qb.whereJsonSupersetOf(column, value); break; } // TODO: Add more JSON operators: whereJsonObject, whereJsonPath, whereJsonSubsetOf // TODO: relational operators every/some/exists/size ... default: { throw new Error(`Undefined attribute level operator ${operator}`); } } }; const applyWhereToColumn = (qb, column, columnWhere)=>{ if (!isRecord(columnWhere)) { if (Array.isArray(columnWhere)) { return qb.whereIn(column, columnWhere); } return qb.where(column, columnWhere); } const keys = Object.keys(columnWhere); keys.forEach((operator)=>{ const value = columnWhere[operator]; applyOperator(qb, column, operator, value); }); }; const applyWhere = (qb, where)=>{ if (!_.isArray(where) && !isRecord(where)) { throw new Error('Where must be an array or an object'); } if (_.isArray(where)) { return qb.where((subQB)=>where.forEach((subWhere)=>applyWhere(subQB, subWhere))); } Object.keys(where).forEach((key)=>{ if (key === '$and') { const value = where[key] ?? []; return qb.where((subQB)=>{ value.forEach((v)=>applyWhere(subQB, v)); }); } if (key === '$or') { const value = where[key] ?? []; return qb.where((subQB)=>{ value.forEach((v)=>subQB.orWhere((inner)=>applyWhere(inner, v))); }); } if (key === '$not') { const value = where[key] ?? {}; return qb.whereNot((qb)=>applyWhere(qb, value)); } applyWhereToColumn(qb, key, where[key]); }); }; const fieldLowerFn = (qb)=>{ // Postgres requires string to be passed if (qb.client.dialect === 'postgresql') { return 'LOWER(CAST(?? AS VARCHAR))'; } return 'LOWER(??)'; }; exports.applyWhere = applyWhere; exports.processWhere = processWhere; //# sourceMappingURL=where.js.map