query.js 2.77 KB
/*jshint node:true*/
"use strict";


/*!
 * Search query helpers
 */


var queryRegex = /^\/(.*)\/([imx]*)$/;

/* Generate a mongoose query operator from a ?query= request parameter */
function createQueryOperator(query) {
	var or = {
		$or: query.split(" OR ").map(function(orOperand) {
			var and = {
				$and: orOperand.split(" AND ").map(function(andOperand) {
					var colonIndex = andOperand.indexOf(":"),
						bangIndex = andOperand.indexOf("!");

					if (colonIndex === -1 && bangIndex === -1) {
						// Invalid operator, skip
						return {};
					}

					var match = andOperand.match(/^([^!:]+)([!:])(.*)$/);
					var field = match[1];
					var negate = match[2] === "!";
					var value = match[3];
					var operator = {};
					var op, matches;

					matches = value.match(queryRegex);

					if (matches) {
						op = new RegExp(matches[1], matches[2]);
						operator[field] = negate ? { $not: op } : op;
					} else {
						if (negate) {
							// Mongoose does not handle { $not: "value" }
							operator[field] = { $nin: [value] };
						} else {
							operator[field] = value;
						}
					}

					return operator;
				}).filter(function(operator) {
					return Object.keys(operator).length > 0;
				})
			};

			return and.$and.length === 1 ? and.$and[0] : and;
		})
	};

	return or.$or.length === 1 ? or.$or[0] : or;
}


/* Get property path value in a document or in a plain object */
function getPath(obj, path) {
	if (typeof obj.get === "function") {
		return obj.get(path);
	}

	var parts = path.split(".");

	while (parts.length) {
		if (!obj) {
			return;
		}

		obj = obj[parts.shift()];
	}

	return obj;
}


/* Match a mongoose query criterion to a document */
function matchQueryCriterion(crit, doc) {
	return Object.keys(crit).every(function(path) {
		var value = getPath(doc, path) || "",
			match = crit[path],
			negate = false,
			result;

		if (typeof match === "string") {
			result = value.toString() === match;
		} else {
			if ("$not" in match) {
				negate = true;
				match = match.$not;
			}

			if (match instanceof RegExp) {
				result = !!value.toString().match(match);
			} else if ("$nin" in match) {
				result = match.$nin.indexOf(value) === -1;
			} else {
				return false;
			}
		}

		return negate ? !result : result;
	});
}


/* Match a mongoose query operator to a document */
function matchQueryOperator(operator, doc) {
	if ("$or" in operator) {
		return operator.$or.some(function(op) {
			return matchQueryOperator(op, doc);
		});
	} else if ("$and" in operator) {
		return operator.$and.every(function(op) {
			return matchQueryOperator(op, doc);
		});
	} else if ("$not" in operator) {
		return !matchQueryOperator(operator.$not, doc);
	} else {
		return matchQueryCriterion(operator, doc);
	}
}


module.exports = {
	create: createQueryOperator,
	match: matchQueryOperator
};