diff --git a/lib/model.js b/lib/model.js index a69fcc5..cb7b99a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -9,7 +9,8 @@ var dateToJSON = function () { return { $$date: this.getTime() }; } , util = require('util') , _ = require('underscore') , modifierFunctions = {} - , matcherFunctions = {} + , comparisonFunctions = {} + , logicalOperators = {} ; @@ -230,6 +231,24 @@ function modify (obj, updateQuery) { // Finding documents // ============================================================== +/** + * Get a value from object with dot notation + * @param {Object} obj + * @param {String} field + */ +function getDotValue (obj, field) { + var fieldParts = typeof field === 'string' ? field.split('.') : field; + + if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match + + if (fieldParts.length === 1) { + return obj[fieldParts[0]]; + } else { + return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)); + } +} + + /** * Check whether 'things' are equal * Things are defined as any native types (string, number, boolean, null, date) and objects @@ -268,28 +287,22 @@ function areThingsEqual (a, b) { } -/** - * Get a value from object with dot notation - * @param {Object} obj - * @param {String} field - */ -function getDotValue (obj, field) { - var fieldParts = typeof field === 'string' ? field.split('.') : field; +comparisonFunctions.$lt = function (a, b) { + if (typeof a !== 'string' && typeof a !== 'number' && !util.isDate(a) && + typeof b !== 'string' && typeof b !== 'number' && !util.isDate(b)) { + return false; + } - if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match + if (typeof a !== typeof b) { return false; } - if (fieldParts.length === 1) { - return obj[fieldParts[0]]; - } else { - return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)); - } -} + return a < b; +}; /** * Match any of the subqueries */ -matcherFunctions.$or = function (obj, query) { +logicalOperators.$or = function (obj, query) { var i; if (!util.isArray(query)) { throw "$or operator used without an array"; } @@ -305,7 +318,7 @@ matcherFunctions.$or = function (obj, query) { /** * Match all of the subqueries */ -matcherFunctions.$and = function (obj, query) { +logicalOperators.$and = function (obj, query) { var i; if (!util.isArray(query)) { throw "$and operator used without an array"; } @@ -342,16 +355,18 @@ function match (obj, query) { function matchQueryPart (obj, queryKey, queryValue) { var objValue = getDotValue(obj, queryKey) , i + , keys, firstChars, dollarFirstChars ; // Query part begins with a logical operator: apply it if (queryKey[0] === '$') { - if (!matcherFunctions[queryKey]) { throw "Unknown query operator " + queryKey; } + if (!logicalOperators[queryKey]) { throw "Unknown logical operator " + queryKey; } - return matcherFunctions[queryKey](obj, queryValue); + return logicalOperators[queryKey](obj, queryValue); } - // Check if the object value is an array and we need to rewrite the query + // Check if the object value is an array treat it as an array of { obj, query } + // Where there needs to be at least one match if (util.isArray(objValue)) { for (i = 0; i < objValue.length; i += 1) { if (match({ queryKey: objValue[i] }, { queryKey: queryValue })) { return true; } @@ -359,7 +374,35 @@ function matchQueryPart (obj, queryKey, queryValue) { return false; } - // Normal field matching + // queryValue is an actual object. Determine whether it contains comparison operators + // or only normal fields. Mixed objects are not allowed + if (queryValue !== null && typeof queryValue === 'object') { + keys = Object.keys(queryValue); + firstChars = _.map(keys, function (item) { return item[0]; }) + dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }) + + if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) { + throw "You cannot mix operators and normal fields"; + } + + // queryValue is an object of this form: { $comparisonOperator1: value1, ... } + if (dollarFirstChars.length > 0) { + for (i = 0; i < keys.length; i += 1) { + if (!comparisonFunctions[keys[i]]) { throw "Unknown comparison function " + keys[i]; } + + if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; } + } + return true; + } + } + + // Query part if of the form { field: $logicaloperator } + //if (logicalOperators[queryValue]) { throw "Can't use logical operator " + queryValue + " here"; } + + + + // queryValue is either a native value or a normal object + // Simple matching is possible if (!areThingsEqual(objValue, queryValue)) { return false; } return true; diff --git a/test/model.test.js b/test/model.test.js index 801083c..e2bbe29 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -431,7 +431,7 @@ describe('Model', function () { model.match({ a: { b: 5, c: 3 } }, { a: { b: 5 } }).should.equal(false); model.match({ a: { b: 5 } }, { a: { b: { $lt: 10 } } }).should.equal(false); - model.match({ a: { b: 5 } }, { a: { $or: { b: 10, b: 5 } } }).should.equal(false); + (function () { model.match({ a: { b: 5 } }, { a: { $or: [ { b: 10 }, { b: 5 } ] } }) }).should.throw(); }); });