const EJSON = require ( 'ejson' ) ; const _ = require ( 'underscore' ) ; const Minimongo = { } ; const MinimongoTest = { } ; const LocalCollection = { _selectorIsId ( ) { return false ; } } ; const MongoId = { ObjectID : { } } ; const isArray = function ( x ) { return _ . isArray ( x ) && ! EJSON . isBinary ( x ) ; } ; const isPlainObject = LocalCollection . _isPlainObject = function ( x ) { return x && LocalCollection . _f . _type ( x ) === 3 ; } ; const isIndexable = function ( x ) { return isArray ( x ) || isPlainObject ( x ) ; } ; const isOperatorObject = function ( valueSelector , inconsistentOK ) { if ( ! isPlainObject ( valueSelector ) ) return false ; var theseAreOperators = undefined ; _ . each ( valueSelector , function ( value , selKey ) { var thisIsOperator = selKey . substr ( 0 , 1 ) === '$' ; if ( theseAreOperators === undefined ) { theseAreOperators = thisIsOperator ; } else if ( theseAreOperators !== thisIsOperator ) { if ( ! inconsistentOK ) throw new Error ( "Inconsistent operator: " + JSON . stringify ( valueSelector ) ) ; theseAreOperators = false ; } } ) ; return ! ! theseAreOperators ; } ; const isNumericKey = function ( s ) { return /^[0-9]+$/ . test ( s ) ; } ; Minimongo . Matcher = function ( selector ) { var self = this ; self . _paths = { } ; self . _hasGeoQuery = false ; self . _hasWhere = false ; self . _isSimple = true ; self . _matchingDocument = undefined ; self . _selector = null ; self . _docMatcher = self . _compileSelector ( selector ) ; } ; _ . extend ( Minimongo . Matcher . prototype , { documentMatches : function ( doc ) { if ( ! doc || typeof doc !== "object" ) { throw Error ( "documentMatches needs a document" ) ; } return this . _docMatcher ( doc ) ; } , hasGeoQuery : function ( ) { return this . _hasGeoQuery ; } , hasWhere : function ( ) { return this . _hasWhere ; } , isSimple : function ( ) { return this . _isSimple ; } , _compileSelector : function ( selector ) { var self = this ; if ( selector instanceof Function ) { self . _isSimple = false ; self . _selector = selector ; self . _recordPathUsed ( '' ) ; return function ( doc ) { return { result : ! ! selector . call ( doc ) } ; } ; } if ( LocalCollection . _selectorIsId ( selector ) ) { self . _selector = { _id : selector } ; self . _recordPathUsed ( '_id' ) ; return function ( doc ) { return { result : EJSON . equals ( doc . _id , selector ) } ; } ; } if ( ! selector || ( ( '_id' in selector ) && ! selector . _id ) ) { self . _isSimple = false ; return nothingMatcher ; } if ( typeof ( selector ) === 'boolean' || isArray ( selector ) || EJSON . isBinary ( selector ) ) throw new Error ( "Invalid selector: " + selector ) ; self . _selector = EJSON . clone ( selector ) ; return compileDocumentSelector ( selector , self , { isRoot : true } ) ; } , _recordPathUsed : function ( path ) { this . _paths [ path ] = true ; } , _getPaths : function ( ) { return _ . keys ( this . _paths ) ; } } ) ; var compileDocumentSelector = function ( docSelector , matcher , options ) { options = options || { } ; var docMatchers = [ ] ; _ . each ( docSelector , function ( subSelector , key ) { if ( key . substr ( 0 , 1 ) === '$' ) { if ( ! _ . has ( LOGICAL_OPERATORS , key ) ) throw new Error ( "Unrecognized logical operator: " + key ) ; matcher . _isSimple = false ; docMatchers . push ( LOGICAL_OPERATORS [ key ] ( subSelector , matcher , options . inElemMatch ) ) ; } else { if ( ! options . inElemMatch ) matcher . _recordPathUsed ( key ) ; var lookUpByIndex = makeLookupFunction ( key ) ; var valueMatcher = compileValueSelector ( subSelector , matcher , options . isRoot ) ; docMatchers . push ( function ( doc ) { var branchValues = lookUpByIndex ( doc ) ; return valueMatcher ( branchValues ) ; } ) ; } } ) ; return andDocumentMatchers ( docMatchers ) ; } ; var compileValueSelector = function ( valueSelector , matcher , isRoot ) { if ( valueSelector instanceof RegExp ) { matcher . _isSimple = false ; return convertElementMatcherToBranchedMatcher ( regexpElementMatcher ( valueSelector ) ) ; } else if ( isOperatorObject ( valueSelector ) ) { return operatorBranchedMatcher ( valueSelector , matcher , isRoot ) ; } else { return convertElementMatcherToBranchedMatcher ( equalityElementMatcher ( valueSelector ) ) ; } } ; var convertElementMatcherToBranchedMatcher = function ( elementMatcher , options ) { options = options || { } ; return function ( branches ) { var expanded = branches ; if ( ! options . dontExpandLeafArrays ) { expanded = expandArraysInBranches ( branches , options . dontIncludeLeafArrays ) ; } var ret = { } ; ret . result = _ . any ( expanded , function ( element ) { var matched = elementMatcher ( element . value ) ; if ( typeof matched === 'number' ) { if ( ! element . arrayIndices ) element . arrayIndices = [ matched ] ; matched = true ; } if ( matched && element . arrayIndices ) ret . arrayIndices = element . arrayIndices ; return matched ; } ) ; return ret ; } ; } ; regexpElementMatcher = function ( regexp ) { return function ( value ) { if ( value instanceof RegExp ) { return _ . isEqual ( value , regexp ) ; } if ( typeof value !== 'string' ) return false ; regexp . lastIndex = 0 ; return regexp . test ( value ) ; } ; } ; equalityElementMatcher = function ( elementSelector ) { if ( isOperatorObject ( elementSelector ) ) throw Error ( "Can't create equalityValueSelector for operator object" ) ; if ( elementSelector == null ) { return function ( value ) { return value == null ; } ; } return function ( value ) { return LocalCollection . _f . _equal ( elementSelector , value ) ; } ; } ; var operatorBranchedMatcher = function ( valueSelector , matcher , isRoot ) { var operatorMatchers = [ ] ; _ . each ( valueSelector , function ( operand , operator ) { var simpleRange = _ . contains ( [ '$lt' , '$lte' , '$gt' , '$gte' ] , operator ) && _ . isNumber ( operand ) ; var simpleEquality = _ . contains ( [ '$ne' , '$eq' ] , operator ) && ! _ . isObject ( operand ) ; var simpleInclusion = _ . contains ( [ '$in' , '$nin' ] , operator ) && _ . isArray ( operand ) && ! _ . any ( operand , _ . isObject ) ; if ( ! ( simpleRange || simpleInclusion || simpleEquality ) ) { matcher . _isSimple = false ; } if ( _ . has ( VALUE_OPERATORS , operator ) ) { operatorMatchers . push ( VALUE_OPERATORS [ operator ] ( operand , valueSelector , matcher , isRoot ) ) ; } else if ( _ . has ( ELEMENT_OPERATORS , operator ) ) { var options = ELEMENT_OPERATORS [ operator ] ; operatorMatchers . push ( convertElementMatcherToBranchedMatcher ( options . compileElementSelector ( operand , valueSelector , matcher ) , options ) ) ; } else { throw new Error ( "Unrecognized operator: " + operator ) ; } } ) ; return andBranchedMatchers ( operatorMatchers ) ; } ; var compileArrayOfDocumentSelectors = function ( selectors , matcher , inElemMatch ) { if ( ! isArray ( selectors ) || _ . isEmpty ( selectors ) ) throw Error ( "$and/$or/$nor must be nonempty array" ) ; return _ . map ( selectors , function ( subSelector ) { if ( ! isPlainObject ( subSelector ) ) throw Error ( "$or/$and/$nor entries need to be full objects" ) ; return compileDocumentSelector ( subSelector , matcher , { inElemMatch : inElemMatch } ) ; } ) ; } ; var LOGICAL_OPERATORS = { $and : function ( subSelector , matcher , inElemMatch ) { var matchers = compileArrayOfDocumentSelectors ( subSelector , matcher , inElemMatch ) ; return andDocumentMatchers ( matchers ) ; } , $or : function ( subSelector , matcher , inElemMatch ) { var matchers = compileArrayOfDocumentSelectors ( subSelector , matcher , inElemMatch ) ; if ( matchers . length === 1 ) return matchers [ 0 ] ; return function ( doc ) { var result = _ . any ( matchers , function ( f ) { return f ( doc ) . result ; } ) ; return { result : result } ; } ; } , $nor : function ( subSelector , matcher , inElemMatch ) { var matchers = compileArrayOfDocumentSelectors ( subSelector , matcher , inElemMatch ) ; return function ( doc ) { var result = _ . all ( matchers , function ( f ) { return ! f ( doc ) . result ; } ) ; return { result : result } ; } ; } , $where : function ( selectorValue , matcher ) { matcher . _recordPathUsed ( '' ) ; matcher . _hasWhere = true ; if ( ! ( selectorValue instanceof Function ) ) { selectorValue = Function ( "obj" , "return " + selectorValue ) ; } return function ( doc ) { return { result : selectorValue . call ( doc , doc ) } ; } ; } , $comment : function ( ) { return function ( ) { return { result : true } ; } ; } } ; var invertBranchedMatcher = function ( branchedMatcher ) { return function ( branchValues ) { var invertMe = branchedMatcher ( branchValues ) ; return { result : ! invertMe . result } ; } ; } ; var VALUE_OPERATORS = { $eq : function ( operand ) { return convertElementMatcherToBranchedMatcher ( equalityElementMatcher ( operand ) ) ; } , $not : function ( operand , valueSelector , matcher ) { return invertBranchedMatcher ( compileValueSelector ( operand , matcher ) ) ; } , $ne : function ( operand ) { return invertBranchedMatcher ( convertElementMatcherToBranchedMatcher ( equalityElementMatcher ( operand ) ) ) ; } , $nin : function ( operand ) { return invertBranchedMatcher ( convertElementMatcherToBranchedMatcher ( ELEMENT_OPERATORS . $in . compileElementSelector ( operand ) ) ) ; } , $exists : function ( operand ) { var exists = convertElementMatcherToBranchedMatcher ( function ( value ) { return value !== undefined ; } ) ; return operand ? exists : invertBranchedMatcher ( exists ) ; } , $options : function ( operand , valueSelector ) { if ( ! _ . has ( valueSelector , '$regex' ) ) throw Error ( "$options needs a $regex" ) ; return everythingMatcher ; } , $maxDistance : function ( operand , valueSelector ) { if ( ! valueSelector . $near ) throw Error ( "$maxDistance needs a $near" ) ; return everythingMatcher ; } , $all : function ( operand , valueSelector , matcher ) { if ( ! isArray ( operand ) ) throw Error ( "$all requires array" ) ; if ( _ . isEmpty ( operand ) ) return nothingMatcher ; var branchedMatchers = [ ] ; _ . each ( operand , function ( criterion ) { if ( isOperatorObject ( criterion ) ) throw Error ( "no $ expressions in $all" ) ; branchedMatchers . push ( compileValueSelector ( criterion , matcher ) ) ; } ) ; return andBranchedMatchers ( branchedMatchers ) ; } , $near : function ( operand , valueSelector , matcher , isRoot ) { if ( ! isRoot ) throw Error ( "$near can't be inside another $ operator" ) ; matcher . _hasGeoQuery = true ; var maxDistance , point , distance ; if ( isPlainObject ( operand ) && _ . has ( operand , '$geometry' ) ) { maxDistance = operand . $maxDistance ; point = operand . $geometry ; distance = function ( value ) { if ( ! value || ! value . type ) return null ; if ( value . type === "Point" ) { return GeoJSON . pointDistance ( point , value ) ; } else { return GeoJSON . geometryWithinRadius ( value , point , maxDistance ) ? 0 : maxDistance + 1 ; } } ; } else { maxDistance = valueSelector . $maxDistance ; if ( ! isArray ( operand ) && ! isPlainObject ( operand ) ) throw Error ( "$near argument must be coordinate pair or GeoJSON" ) ; point = pointToArray ( operand ) ; distance = function ( value ) { if ( ! isArray ( value ) && ! isPlainObject ( value ) ) return null ; return distanceCoordinatePairs ( point , value ) ; } ; } return function ( branchedValues ) { branchedValues = expandArraysInBranches ( branchedValues ) ; var result = { result : false } ; _ . each ( branchedValues , function ( branch ) { var curDistance = distance ( branch . value ) ; if ( curDistance === null || curDistance > maxDistance ) return ; if ( result . distance !== undefined && result . distance <= curDistance ) return ; result . result = true ; result . distance = curDistance ; if ( ! branch . arrayIndices ) delete result . arrayIndices ; else result . arrayIndices = branch . arrayIndices ; } ) ; return result ; } ; } } ; var distanceCoordinatePairs = function ( a , b ) { a = pointToArray ( a ) ; b = pointToArray ( b ) ; var x = a [ 0 ] - b [ 0 ] ; var y = a [ 1 ] - b [ 1 ] ; if ( _ . isNaN ( x ) || _ . isNaN ( y ) ) return null ; return Math . sqrt ( x * x + y * y ) ; } ; var pointToArray = function ( point ) { return _ . map ( point , _ . identity ) ; } ; var makeInequality = function ( cmpValueComparator ) { return { compileElementSelector : function ( operand ) { if ( isArray ( operand ) ) { return function ( ) { return false ; } ; } if ( operand === undefined ) operand = null ; var operandType = LocalCollection . _f . _type ( operand ) ; return function ( value ) { if ( value === undefined ) value = null ; if ( LocalCollection . _f . _type ( value ) !== operandType ) return false ; return cmpValueComparator ( LocalCollection . _f . _cmp ( value , operand ) ) ; } ; } } ; } ; ELEMENT_OPERATORS = { $lt : makeInequality ( function ( cmpValue ) { return cmpValue < 0 ; } ) , $gt : makeInequality ( function ( cmpValue ) { return cmpValue > 0 ; } ) , $lte : makeInequality ( function ( cmpValue ) { return cmpValue <= 0 ; } ) , $gte : makeInequality ( function ( cmpValue ) { return cmpValue >= 0 ; } ) , $mod : { compileElementSelector : function ( operand ) { if ( ! ( isArray ( operand ) && operand . length === 2 && typeof ( operand [ 0 ] ) === 'number' && typeof ( operand [ 1 ] ) === 'number' ) ) { throw Error ( "argument to $mod must be an array of two numbers" ) ; } var divisor = operand [ 0 ] ; var remainder = operand [ 1 ] ; return function ( value ) { return typeof value === 'number' && value % divisor === remainder ; } ; } } , $in : { compileElementSelector : function ( operand ) { if ( ! isArray ( operand ) ) throw Error ( "$in needs an array" ) ; var elementMatchers = [ ] ; _ . each ( operand , function ( option ) { if ( option instanceof RegExp ) elementMatchers . push ( regexpElementMatcher ( option ) ) ; else if ( isOperatorObject ( option ) ) throw Error ( "cannot nest $ under $in" ) ; else elementMatchers . push ( equalityElementMatcher ( option ) ) ; } ) ; return function ( value ) { if ( value === undefined ) value = null ; return _ . any ( elementMatchers , function ( e ) { return e ( value ) ; } ) ; } ; } } , $size : { dontExpandLeafArrays : true , compileElementSelector : function ( operand ) { if ( typeof operand === 'string' ) { operand = 0 ; } else if ( typeof operand !== 'number' ) { throw Error ( "$size needs a number" ) ; } return function ( value ) { return isArray ( value ) && value . length === operand ; } ; } } , $type : { dontIncludeLeafArrays : true , compileElementSelector : function ( operand ) { if ( typeof operand !== 'number' ) throw Error ( "$type needs a number" ) ; return function ( value ) { return value !== undefined && LocalCollection . _f . _type ( value ) === operand ; } ; } } , $regex : { compileElementSelector : function ( operand , valueSelector ) { if ( ! ( typeof operand === 'string' || operand instanceof RegExp ) ) throw Error ( "$regex has to be a string or RegExp" ) ; var regexp ; if ( valueSelector . $options !== undefined ) { if ( /[^gim]/ . test ( valueSelector . $options ) ) throw new Error ( "Only the i, m, and g regexp options are supported" ) ; var regexSource = operand instanceof RegExp ? operand . source : operand ; regexp = new RegExp ( regexSource , valueSelector . $options ) ; } else if ( operand instanceof RegExp ) { regexp = operand ; } else { regexp = new RegExp ( operand ) ; } return regexpElementMatcher ( regexp ) ; } } , $elemMatch : { dontExpandLeafArrays : true , compileElementSelector : function ( operand , valueSelector , matcher ) { if ( ! isPlainObject ( operand ) ) throw Error ( "$elemMatch need an object" ) ; var subMatcher , isDocMatcher ; if ( isOperatorObject ( _ . omit ( operand , _ . keys ( LOGICAL_OPERATORS ) ) , true ) ) { subMatcher = compileValueSelector ( operand , matcher ) ; isDocMatcher = false ; } else { subMatcher = compileDocumentSelector ( operand , matcher , { inElemMatch : true } ) ; isDocMatcher = true ; } return function ( value ) { if ( ! isArray ( value ) ) return false ; for ( var i = 0 ; i < value . length ; ++ i ) { var arrayElement = value [ i ] ; var arg ; if ( isDocMatcher ) { if ( ! isPlainObject ( arrayElement ) && ! isArray ( arrayElement ) ) return false ; arg = arrayElement ; } else { arg = [ { value : arrayElement , dontIterate : true } ] ; } if ( subMatcher ( arg ) . result ) return i ; } return false ; } ; } } } ; makeLookupFunction = function ( key , options ) { options = options || { } ; var parts = key . split ( '.' ) ; var firstPart = parts . length ? parts [ 0 ] : '' ; var firstPartIsNumeric = isNumericKey ( firstPart ) ; var nextPartIsNumeric = parts . length >= 2 && isNumericKey ( parts [ 1 ] ) ; var lookupRest ; if ( parts . length > 1 ) { lookupRest = makeLookupFunction ( parts . slice ( 1 ) . join ( '.' ) ) ; } var omitUnnecessaryFields = function ( retVal ) { if ( ! retVal . dontIterate ) delete retVal . dontIterate ; if ( retVal . arrayIndices && ! retVal . arrayIndices . length ) delete retVal . arrayIndices ; return retVal ; } ; return function ( doc , arrayIndices ) { if ( ! arrayIndices ) arrayIndices = [ ] ; if ( isArray ( doc ) ) { if ( ! ( firstPartIsNumeric && firstPart < doc . length ) ) return [ ] ; arrayIndices = arrayIndices . concat ( + firstPart , 'x' ) ; } var firstLevel = doc [ firstPart ] ; if ( ! lookupRest ) { return [ omitUnnecessaryFields ( { value : firstLevel , dontIterate : isArray ( doc ) && isArray ( firstLevel ) , arrayIndices : arrayIndices } ) ] ; } if ( ! isIndexable ( firstLevel ) ) { if ( isArray ( doc ) ) return [ ] ; return [ omitUnnecessaryFields ( { value : undefined , arrayIndices : arrayIndices } ) ] ; } var result = [ ] ; var appendToResult = function ( more ) { Array . prototype . push . apply ( result , more ) ; } ; appendToResult ( lookupRest ( firstLevel , arrayIndices ) ) ; if ( isArray ( firstLevel ) && ! ( nextPartIsNumeric && options . forSort ) ) { _ . each ( firstLevel , function ( branch , arrayIndex ) { if ( isPlainObject ( branch ) ) { appendToResult ( lookupRest ( branch , arrayIndices . concat ( arrayIndex ) ) ) ; } } ) ; } return result ; } ; } ; MinimongoTest . makeLookupFunction = makeLookupFunction ; expandArraysInBranches = function ( branches , skipTheArrays ) { var branchesOut = [ ] ; _ . each ( branches , function ( branch ) { var thisIsArray = isArray ( branch . value ) ; if ( ! ( skipTheArrays && thisIsArray && ! branch . dontIterate ) ) { branchesOut . push ( { value : branch . value , arrayIndices : branch . arrayIndices } ) ; } if ( thisIsArray && ! branch . dontIterate ) { _ . each ( branch . value , function ( leaf , i ) { branchesOut . push ( { value : leaf , arrayIndices : ( branch . arrayIndices || [ ] ) . concat ( i ) } ) ; } ) ; } } ) ; return branchesOut ; } ; var nothingMatcher = function ( docOrBranchedValues ) { return { result : false } ; } ; var everythingMatcher = function ( docOrBranchedValues ) { return { result : true } ; } ; var andSomeMatchers = function ( subMatchers ) { if ( subMatchers . length === 0 ) return everythingMatcher ; if ( subMatchers . length === 1 ) return subMatchers [ 0 ] ; return function ( docOrBranches ) { var ret = { } ; ret . result = _ . all ( subMatchers , function ( f ) { var subResult = f ( docOrBranches ) ; if ( subResult . result && subResult . distance !== undefined && ret . distance === undefined ) { ret . distance = subResult . distance ; } if ( subResult . result && subResult . arrayIndices ) { ret . arrayIndices = subResult . arrayIndices ; } return subResult . result ; } ) ; if ( ! ret . result ) { delete ret . distance ; delete ret . arrayIndices ; } return ret ; } ; } ; var andDocumentMatchers = andSomeMatchers ; var andBranchedMatchers = andSomeMatchers ; LocalCollection . _f = { _type : function ( v ) { if ( typeof v === "number" ) return 1 ; if ( typeof v === "string" ) return 2 ; if ( typeof v === "boolean" ) return 8 ; if ( isArray ( v ) ) return 4 ; if ( v === null ) return 10 ; if ( v instanceof RegExp ) return 11 ; if ( typeof v === "function" ) return 13 ; if ( v instanceof Date ) return 9 ; if ( EJSON . isBinary ( v ) ) return 5 ; if ( v instanceof MongoID . ObjectID ) return 7 ; return 3 ; } , _equal : function ( a , b ) { return EJSON . equals ( a , b , { keyOrderSensitive : true } ) ; } , _typeorder : function ( t ) { return [ - 1 , 1 , 2 , 3 , 4 , 5 , - 1 , 6 , 7 , 8 , 0 , 9 , - 1 , 100 , 2 , 100 , 1 , 8 , 1 ] [ t ] ; } , _cmp : function ( a , b ) { if ( a === undefined ) return b === undefined ? 0 : - 1 ; if ( b === undefined ) return 1 ; var ta = LocalCollection . _f . _type ( a ) ; var tb = LocalCollection . _f . _type ( b ) ; var oa = LocalCollection . _f . _typeorder ( ta ) ; var ob = LocalCollection . _f . _typeorder ( tb ) ; if ( oa !== ob ) return oa < ob ? - 1 : 1 ; if ( ta !== tb ) throw Error ( "Missing type coercion logic in _cmp" ) ; if ( ta === 7 ) { ta = tb = 2 ; a = a . toHexString ( ) ; b = b . toHexString ( ) ; } if ( ta === 9 ) { ta = tb = 1 ; a = a . getTime ( ) ; b = b . getTime ( ) ; } if ( ta === 1 ) return a - b ; if ( tb === 2 ) return a < b ? - 1 : ( a === b ? 0 : 1 ) ; if ( ta === 3 ) { var to_array = function ( obj ) { var ret = [ ] ; for ( var key in obj ) { ret . push ( key ) ; ret . push ( obj [ key ] ) ; } return ret ; } ; return LocalCollection . _f . _cmp ( to_array ( a ) , to_array ( b ) ) ; } if ( ta === 4 ) { for ( var i = 0 ; ; i ++ ) { if ( i === a . length ) return ( i === b . length ) ? 0 : - 1 ; if ( i === b . length ) return 1 ; var s = LocalCollection . _f . _cmp ( a [ i ] , b [ i ] ) ; if ( s !== 0 ) return s ; } } if ( ta === 5 ) { if ( a . length !== b . length ) return a . length - b . length ; for ( i = 0 ; i < a . length ; i ++ ) { if ( a [ i ] < b [ i ] ) return - 1 ; if ( a [ i ] > b [ i ] ) return 1 ; } return 0 ; } if ( ta === 8 ) { if ( a ) return b ? 0 : 1 ; return b ? - 1 : 0 ; } if ( ta === 10 ) return 0 ; if ( ta === 11 ) throw Error ( "Sorting not supported on regular expression" ) ; if ( ta === 13 ) throw Error ( "Sorting not supported on Javascript code" ) ; throw Error ( "Unknown type to sort" ) ; } } ; LocalCollection . _removeDollarOperators = function ( selector ) { var selectorDoc = { } ; for ( var k in selector ) if ( k . substr ( 0 , 1 ) !== '$' ) selectorDoc [ k ] = selector [ k ] ; return selectorDoc ; } ; module . exports = Minimongo . Matcher ;