src/model/buildFilterQuery.js
// @flow
import isArrayLike from 'lodash.isarraylike';
import isObject from 'lodash.isobject';
/*
* build a query for the type model accessing mongodb
* @public
*/
export function buildFilterQuery(args: any): any {
const query = {};
// on primitive types, just return the data as leaf values in the recursion
switch (typeof args) {
case 'boolean':
return args;
case 'number':
return args;
case 'string':
return args;
case 'function':
return args;
case 'object':
// only on "ObjectID"s, we don't go deeper into this Object
// other "objects" will be handled below
if (args._bsontype && args._bsontype === 'ObjectID') {
return args;
}
}
// on array like types, return the interpreted mapped values.
// array elements can be also query objects,
// so we have to analyze each one of them first, before we return them
// back as an array
if (isArrayLike(args) && args.length && args.length > 0) {
return args.map(item => buildFilterQuery(item));
}
// on object like types, analyze the graphql provided query arguments...
if (isObject(args)) {
// get all the keys of the argument
const keys = Object.keys(args);
// analyze all arguments and map them to mongodb query parts
keys.forEach(key => {
// a key might look like: e.g. "bodyText_not_starts_with"
// to analyze it, we need to split off the field name from the operation
const keyParts = key.split('_');
// number of keyParts e.g. 4 as a cloned field, as keyParts changes later
const length = 0 + keyParts.length;
// the fieldName is then e.g. "bodyText", get the first array element
let fieldName = keyParts.shift();
// our "id" field is in mongo "_id"
if (fieldName === 'id') {
fieldName = '_id';
}
// the operation is then: e.g. "not_starts_with"
const operation = keyParts.join('_');
// the value which was passed in to this operation, might be a deep Object
// so it has to be analyzed recursively
const value = args[key];
switch (length) {
// if there is only 1 key part provided, it must be one of the special
// cases such as AND, NOT NOR, OR,
// or the default case, that it had just provided a simple field name
case 1:
switch (fieldName) {
case 'AND':
// $and performs a logical AND operation on an array of two or
// more expressions, (e.g. <expression1>, <expression2>, etc.)
// and selects the documents, that satisfy all the expressions
// in the array.
// $and : [ { tags: "ssl" }, { tags: "security" } ]
query['$and'] = buildFilterQuery(value);
break;
case 'NOR':
// $nor performs a logical NOR operation on an array of one or
// more query expression and selects the documents that fail
// all the query expressions in the array.
// db.inventory.find({ $nor: [ { price: 1.99 }, { sale: true } ]})
query['$nor'] = buildFilterQuery(value);
break;
case 'OR':
// $or operator performs a logical OR operation on an array of
// two or more <expressions> and selects the documents that
// satisfy at least one of the <expressions>.
// db.inventory.find({
// $or: [ { quantity: { $lt: 20 } }, { price: 10 } ]
// })
query['$or'] = buildFilterQuery(value);
break;
default:
// the trivial case
// fieldName = value
query[fieldName] = buildFilterQuery(value);
break;
}
break;
// if there are more than one keyParts, we know, it must be
// one of the following operations,
// provided from the graphql schema as filter arguments
default:
switch (operation) {
case 'all':
// $all operator selects the documents, where the value of
// a field is an array
// that contains all the specified elements. To specify an
// $all expression
// tags: { $all : [ "ssl" , "security" ] }
// field: { $all : [ <value1> , <value2> ... ] }
query[fieldName] = { $all: buildFilterQuery(value) };
break;
case 'eq':
// $eq specifies equality condition. The $eq operator matches
// documents where the value of a field equals the specified value
// db.inventory.find( { qty: { $eq: 20 } } )
query[fieldName] = { $eq: buildFilterQuery(value) };
break;
case 'ne':
// $ne selects the documents where the value of the field is
// not equal (i.e. !=) to the specified value. This includes
// documents that do not contain the field.
// field: { $ne : value }
query[fieldName] = { $ne: buildFilterQuery(value) };
break;
case 'in':
// $in operator selects the documents where the value of a
// field equals any value
// in the specified array.
// db.inventory.find( { qty: { $in: [ 5, 15 ] } } )
query[fieldName] = { $in: buildFilterQuery(value) };
break;
case 'nin':
// $nin selects the documents where:
// * the field value is not in the specified array or
// * the field does not exist.
// db.inventory.find( { qty: { $nin: [ 5, 15 ] } } )
query[fieldName] = { $nin: buildFilterQuery(value) };
break;
case 'lt':
// $lt selects the documents where the value of the field is less
// than (i.e. <) the specified value.
// db.inventory.find( { qty: { $lt: 20 } } )
query[fieldName] = { $lt: buildFilterQuery(value) };
break;
case 'lte':
// $lte selects the documents where the value of the field
// is less than or equal to (i.e. <=) the specified value.
// db.inventory.find( { qty: { $lte: 20 } } )
query[fieldName] = { $lte: buildFilterQuery(value) };
break;
case 'gt':
// $gt selects those documents where the value of the field
// is greater than (i.e. >) the specified value.
// db.inventory.find( { qty: { $gt: 20 } } )
query[fieldName] = { $gt: buildFilterQuery(value) };
break;
case 'gte':
// $gte selects the documents where the value of the field
// is greater than or equal to (i.e. >=) a specified value
// e.g. value.
// db.inventory.find( { qty: { $gte: 20 } } )
query[fieldName] = { $gte: buildFilterQuery(value) };
break;
case 'not':
// $not performs a logical NOT operation on the specified
// <operator-expression> and selects the documents that
// do not match the <operator-expression>.
// This includes documents that do not contain the field.
// db.inventory.find( { price: { $not: { $gt: 1.99 } } } )
const subQuery = buildFilterQuery(value);
if (subQuery[fieldName]) {
query[fieldName] = { $not: subQuery[fieldName] };
} else {
// this shouldn't happen, but for safety reasons
query[fieldName] = { $not: buildFilterQuery(value) };
}
break;
case 'exists':
// Syntax: { field: { $exists: <boolean> } }
// When <boolean> is true, $exists matches the documents
// that contain the field, including documents where the
// field value is null.
// If <boolean> is false, the query returns only the documents
// that do not contain the field.
query[fieldName] = { $exists: buildFilterQuery(value) };
break;
case 'type':
// $type selects the documents where the value of the field
// is an instance of the specified BSON type.
// Querying by data type is useful when dealing with
// highly unstructured data where data types are not predictable
// { field: { $type: <BSON type number> | <String alias> } }
// look for the type: enum BSONType
query[fieldName] = { $type: buildFilterQuery(value) };
break;
case 'not_in':
// $in operator selects the documents where the value of a
// field equals any value
// in the specified array. $not inverses
// db.inventory.find( { qty: { $in: [ 5, 15 ] } } )
query[fieldName] = { $nin: buildFilterQuery(value) };
break;
case 'regex':
// in schema defined graphql type:
// type regex {
// regex: String
// option: Enum (type option { global, multiline, ...})
// }
// so we need to map the options first to mongoDB regex options:
let options = '';
if (value.options) {
value.options.forEach(option => {
switch (option) {
case 'global':
options += 'g';
break;
case 'multiline':
options += 'm';
break;
case 'insensitive':
options += 'i';
break;
case 'sticky':
options += 'y';
break;
case 'unicode':
options += 'u';
break;
}
});
}
// assign it directly, as deeper structures aren't allowed here
query[fieldName] = {
$regex: value.regex,
$options: options
};
break;
case 'contains':
// the field contains this string
query[fieldName] = {
$regex: buildFilterQuery(value)
};
break;
case 'starts_with':
// the field starts with this string
query[fieldName] = {
$regex: `^${buildFilterQuery(value)}`,
$options: 'm'
};
break;
case 'ends_with':
// the field ends with this string
query[fieldName] = {
$regex: `${buildFilterQuery(value)}$`,
$options: 'm'
};
break;
case 'not_contains':
// the field does not contain this string
query[fieldName] = {
$regex: `^(?!.*${buildFilterQuery(value)})`,
$options: 'm'
};
break;
case 'not_starts_with':
// the field ends not with this string
query[fieldName] = {
$regex: `^(?!${buildFilterQuery(value)})`,
$options: 'm'
};
break;
case 'not_ends_with':
// // the field ends with this string
// query[fieldName] = {
// $regex: `(?!${buildFilterQuery(value)})$`,
// $options: 'm'
// };
// the field ends with this string case-in-sensitive
// it is called "negated or negative lookbehind" .*(?<!xyz)$
// Javascript isn't supporting this today
// so have to use a complicated look ahead pattern instead
query[fieldName] = {
$regex: `^(?!.*${buildFilterQuery(value)}$).*$`,
$options: 'gm'
};
break;
case 'contains_ci':
// the field contains this string case-in-sensitive
query[fieldName] = {
$regex: buildFilterQuery(value),
$options: 'im'
};
break;
case 'starts_with_ci':
// the field starts with this string case-in-sensitive
query[fieldName] = {
$regex: `^${buildFilterQuery(value)}`,
$options: 'im'
};
break;
case 'ends_with_ci':
// the field ends with this string case-in-sensitive
query[fieldName] = {
$regex: `${buildFilterQuery(value)}$`,
$options: 'im'
};
break;
case 'not_contains_ci':
// the field does not contain this string case-in-sensitive
query[fieldName] = {
$regex: `^(?!.*${buildFilterQuery(value)})`,
$options: 'im'
};
break;
case 'not_starts_with_ci':
// the field starts with this string case-in-sensitive
query[fieldName] = {
// $not: {
$regex: `^(?!${buildFilterQuery(value)})`,
$options: 'im'
// }
};
break;
case 'not_ends_with_ci':
// the field ends with this string case-in-sensitive
// it is called "negated or negative lookbehind" .*(?<!xyz)$
// Javascript isn't supporting this today
// so have to use a complicated look ahead pattern instead
query[fieldName] = {
$regex: `^(?!.*${buildFilterQuery(value)}$).*$`,
$options: 'gim'
};
break;
}
}
});
}
return query;
}