373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
'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
|