'use strict'; var bson = require('bson'); var buffer = require('buffer'); var Decimal = require('decimal.js'); const isArray = Array.isArray; const isBoolean = value => typeof value === 'boolean'; const isDate = value => value instanceof Date; const isNull = value => value === null; const isNullish = value => value === undefined || value === null; const isObjectLike = value => typeof value === 'object' && value !== null; const isPlainObject = value => typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype; const isString = value => typeof value === 'string'; const isUndefined = value => value === undefined; const isFinite = value => typeof value === 'bigint' || Number.isFinite(value); const yes = () => true; const no = () => false; function isOperatorExpression (value) { if (isPlainObject(value)) { const keys = Object.keys(value); return keys.length === 1 && /^\$[a-z][A-Za-z0-9]*$/.test(keys[0]) } else { return false } } function and (fns) { if (!fns.length) { return yes } else if (fns.length === 1) { return fns[0] } return value => { for (const fn of fns) { if (!fn(value)) { return false } } return true } } function or (fns) { if (!fns.length) { return no } else if (fns.length === 1) { return fns[0] } return value => { for (const fn of fns) { if (fn(value)) { return true } } return false } } function not (fn) { return value => !fn(value) } function nor (fns) { return not(or(fns)) } /** * Bind (without context) an operator-like function. */ function bind (fn, options) { return (args, compile, operator) => fn(options, args, compile, operator) } const BSON = { Double: 1, String: 2, Object: 3, Array: 4, Binary: 5, Undefined: 6, ObjectId: 7, Boolean: 8, Date: 9, Null: 10, RegExp: 11, Reference: 12, JavaScript: 13, Symbol: 14, JavaScriptWithScope: 15, Int32: 16, Timestamp: 17, Long: 18, Decimal128: 19, MinKey: -1, MaxKey: 127 }; function isJavaScript (value) { return typeof value === 'function' || Object(value)._bsontype === 'Code' } function isReference (value) { return Object(value)._bsontype === 'DBRef' } function isBinary (value) { return buffer.Buffer.isBuffer(value) || Object(value)._bsontype === 'Binary' } function isObjectId (value) { return Object(value)._bsontype === 'ObjectID' || Object(value)._bsontype === 'ObjectId' } function isRegExp (value) { return value instanceof RegExp || Object(value)._bsontype === 'BSONRegExp' } function isSymbol (value) { return Object(value)._bsontype === 'Symbol' || Object(value)._bsontype === 'BSONSymbol' } function isTimestamp (value) { return Object(value)._bsontype === 'Timestamp' } function isMinKey (value) { return Object(value)._bsontype === 'MinKey' } function isMaxKey (value) { return Object(value)._bsontype === 'MaxKey' } /** * Deletects falsy for MongoDB. * `NaN` is **not** falsy. */ function isFalsy (value) { // TODO: other types? return isNumber(value) ? n(value) === 0 : value === false || value === undefined || value === null } /** * Deletects truthy for MongoDB. * `NaN` is truthy. */ function isTruthy (value) { return !isFalsy(value) } /** * 32 bit signed integer max number. */ const Int32Max = 2147483647; /** * 32 bit signed integer min number. */ const Int32Min = -2147483648; /** * Detects 32 bit signed integer. */ function isInt32 (value) { return typeof value === 'bigint' || Number.isInteger(value) ? value <= Int32Max && value >= Int32Min : Object(value)._bsontype === 'Int32' } /** * Detects IEEE 754-2008 64 bit double-precision floating-point number. * Can be Infinity, NaN, -0, or "small" BigInt. */ function isDouble (value) { return typeof value === 'bigint' ? value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER : typeof value === 'number' || Object(value)._bsontype === 'Double' } /** * Detects 64 bit signed integer. */ function isLong (value) { // TODO: check BigInt limits return Number.isInteger(value) || typeof value === 'bigint' || Object(value)._bsontype === 'Long' } /** * Detects 128 bit signed decimal. */ function isDecimal128 (value) { // TODO: check BigInt limits return Number.isFinite(value) || typeof value === 'bigint' || Object(value)._bsontype === 'Decimal128' } /** * BSON type code for numeric types. */ const numericTypes = [ 'Decimal128', 'Double', 'Int32', 'Long' ]; /** * Detects numeric value. */ function isNumber (value) { const type = Object(value)._bsontype; return typeof type === 'string' ? numericTypes.includes(type) : typeof value === 'bigint' || typeof value === 'number' } /** * Returns the numeric representation of a value. */ function n (value) { switch (Object(value)._bsontype) { case 'Decimal128': throw new Error('Decimal128 values are not supported') case 'Long': value = value.toBigInt(); break case 'Double': case 'Int32': value = value.valueOf(); break } if ( typeof value === 'bigint' && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER ) { value = parseInt(`${value}`); } return value } /** * Get the value's BSON type enum value. */ function getBSONType (value) { if (isMinKey(value)) { return BSON.MinKey } else if (isMaxKey(value)) { return BSON.MaxKey } else if (isReference(value)) { return BSON.Reference } else if (isBoolean(value)) { return BSON.Boolean } else if (isString(value)) { return BSON.String } else if (isPlainObject(value)) { return BSON.Object } else if (isArray(value)) { return BSON.Array } else if (isBinary(value)) { return BSON.Binary } else if (isObjectId(value)) { return BSON.ObjectId } else if (isDate(value)) { return BSON.Date } else if (isUndefined(value)) { return BSON.Undefined } else if (isNull(value)) { return BSON.Null } else if (isRegExp(value)) { return BSON.RegExp } else if (isJavaScript(value)) { return BSON.JavaScript } else if (isSymbol(value)) { return BSON.Symbol } else if (isTimestamp(value)) { return BSON.Timestamp } else if (isDouble(value)) { return BSON.Double } else if (isInt32(value)) { return BSON.Int32 } else if (isLong(value)) { return BSON.Long } else if (isDecimal128(value)) { return BSON.Decimal128 } else { return null // JavaScriptWithScope or future types } } /** * Get type alias (string) by type enum. */ function getTypeAlias (type) { switch (type) { case BSON.Double: return 'double' case BSON.String: return 'string' case BSON.Object: return 'object' case BSON.Array: return 'array' case BSON.Binary: return 'binData' case BSON.Undefined: return 'undefined' case BSON.ObjectId: return 'objectId' case BSON.Boolean: return 'bool' case BSON.Date: return 'date' case BSON.Null: return 'null' case BSON.RegExp: return 'regex' case BSON.Reference: return 'dbPointer' case BSON.JavaScript: return 'javascript' case BSON.Symbol: return 'symbol' case BSON.Int32: return 'int' case BSON.Timestamp: return 'timestamp' case BSON.Long: return 'long' case BSON.Decimal128: return 'decimal' case BSON.MinKey: return 'minKey' case BSON.MaxKey: return 'maxKey' default: return 'unknown' // JavaScriptWithScope or future types } } /** * https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/ */ function getTypeWeight (type) { switch (type) { case BSON.MinKey: // Always first return 1 case BSON.Null: case BSON.Undefined: return 2 case BSON.Decimal128: case BSON.Double: case BSON.Int32: case BSON.Long: return 3 case BSON.String: case BSON.Symbol: return 4 case BSON.Object: return 5 case BSON.Array: return 6 case BSON.Binary: return 7 case BSON.ObjectId: return 8 case BSON.Boolean: return 9 case BSON.Date: return 10 case BSON.Timestamp: return 11 case BSON.RegExp: return 12 case BSON.MaxKey: // Always last return 14 default: return 13 } } function parsePath (value) { if (typeof value !== 'string') { throw new TypeError('Field path must be a string') } else { return value.split('.').map(pathPathSection) } } /** * Returns `true` when value is a valid property key identifier for a MQL expression. */ function isIdentifier (value) { return typeof value === 'string' && /^[a-z0-9_ ]*$/i.test(value) } function pathPathSection (value) { if (typeof value === 'string' && /^\d+$/.test(value)) { return parseInt(value) } else if (isIdentifier(value)) { return value } else { throw new Error(`Found an invalid path segment: ${value}`) } } function compileReader (key) { const identifier = 'document'; const path = parsePath(key); let body = ''; for (let i = 0; i < path.length; i++) { const key = path[i]; const previous = path.slice(0, i); if (Number.isInteger(key)) { body += _checkIndex(identifier, previous, key); } else { body += _checkProperty(identifier, previous); } } body += `\nreturn ${_member(identifier, path)}\n`; return compile$1(body, identifier) } function compileWriter (key) { const identifier = 'document'; const path = parsePath(key); let body = ''; for (let i = 0; i < path.length; i++) { const key = path[i]; const last = i === (path.length - 1); const previous = path.slice(0, i); if (Number.isInteger(key)) { body += last ? _setIndex(identifier, previous, key) : _ensureIndex(identifier, previous, key); } else { body += last ? _setProperty(identifier, previous, key) : _ensureProperty(identifier, previous, key); } } return compile$1(body, identifier, 'value') } function compileDeleter (key) { const identifier = 'document'; const path = Array.isArray(key) ? key : parsePath(key); let body = ''; for (let i = 0; i < path.length; i++) { const key = path[i]; const last = i === (path.length - 1); const previous = path.slice(0, i); if (Number.isInteger(key)) { body += last ? _deleteIndex(identifier, previous, key) : _checkIndex(identifier, previous, key); } else { body += last ? _deleteProperty(identifier, previous, key) : _checkProperty(identifier, previous); } } return compile$1(body, identifier) } function _checkProperty (identifier, path, property) { const _parent = _member(identifier, path); return ` if (${_not(_isPlainObject(_parent))}) { return } ` } function _checkIndex (identifier, path, index) { const _parent = _member(identifier, path); return ` if (${_isArray(_parent)}) { if (${_parent}.length <= ${index}) { return } } else if (${_not(_isPlainObject(_parent))}) { return } ` } function _deleteProperty (identifier, path, property) { const _parent = _member(identifier, path); const _subject = _member(identifier, [...path, property]); return ` if (${_isPlainObject(_parent)}) { delete ${_subject} } ` } function _deleteIndex (identifier, path, index) { const _parent = _member(identifier, path); const _subject = _member(identifier, [...path, index]); return ` if (${_isArray(_parent)}) { if (${_parent}.length >= ${index + 1}) { ${_subject} = null } } else if (${_isPlainObject(_parent)}) { delete ${_subject} } ` } function _setProperty (identifier, path, property) { const _parent = _member(identifier, path); const _subject = _member(identifier, [...path, property]); return ` if (${_not(_isPlainObject(_parent))}) { ${_throw(`Cannot set ${_subject} member`)} } ${_subject} = value ` } function _ensureProperty (identifier, path, property) { const _parent = _member(identifier, path); const _subject = _member(identifier, [...path, property]); return ` if (${_not(_isPlainObject(_parent))}) { ${_throw(`Cannot access ${_subject} member`)} } else if (${_subject} === undefined) { ${_subject} = {} } ` } function _setIndex (identifier, path, index) { const _parent = _member(identifier, path); const _subject = _member(identifier, [...path, index]); return ` if (${_isArray(_parent)}) { while (${_parent}.length <= ${index}) { ${_parent}.push(null) } } else if (${_not(_isPlainObject(_parent))}) { ${_throw(`Cannot set ${_subject} member`)} } ${_subject} = value ` } function _ensureIndex (identifier, path, index) { const _parent = _member(identifier, path); const _subject = _member(identifier, [...path, index]); return ` if (${_isArray(_parent)}) { while (${_parent}.length < ${index}) { ${_parent}.push(null) } if (${_parent}.length === ${index}) { ${_parent}.push({}) } } else if (${_isPlainObject(_parent)}) { if (${_subject} === undefined) { ${_subject} = {} } } else { ${_throw(`Cannot access ${_subject} member`)} } ` } /** * Compile a function with a sane arguments order :) */ function compile$1 (body, ...names) { // eslint-disable-next-line return new Function(...names, body) } function _member (identifier, path) { let txt = identifier; for (const key of path) { if (typeof key === 'number') { txt += `[${key}]`; } else if (/^[a-z_][a-z0-9_]*$/i.test(key)) { txt += `.${key}`; } else { txt += `[${JSON.stringify(key)}]`; } } return txt } function _isArray (identifier) { return `Array.isArray(${identifier})` } function _isPlainObject (identifier) { return `typeof ${identifier} === 'object' && ${identifier} !== null && Object.getPrototypeOf(${identifier}) === Object.prototype` } function _not (code) { return `!(${code})` } function _throw (message = '') { // TODO: use MqlError class return `throw new Error(${JSON.stringify(message)})` } function abs (values) { return Decimal.abs(n(values[0])).toNumber() } function add (values) { const result = values.reduce( (acc, value) => acc.plus(isDate(value) ? value.getTime() : n(value)), Decimal(0) ); return values.length >= 1 && isDate(values[0]) ? new Date(result.toNumber()) : result.toNumber() } function ceil (values) { return Decimal(n(values[0])) .toDecimalPlaces(0, Decimal.ROUND_CEIL) .toNumber() } function divide (values) { const dividend = n(values[0]); const divisor = n(values[1]); if (divisor === 0) { throw new Error("can't $divide by zero") } return Decimal.div(dividend, divisor).toNumber() } function exp (values) { return Decimal(n(values[0])).naturalExponential().toNumber() } function floor (values) { return Decimal(n(values[0])) .toDecimalPlaces(0, Decimal.ROUND_FLOOR) .toNumber() } function log (values) { const value = n(values[0]); const base = n(values[1]); switch (base) { case 2: return Decimal.log2(value).toNumber() case 10: return Decimal.log10(value).toNumber() case Math.E: return Decimal.ln(value).toNumber() default: return Decimal.log(value, base).toNumber() } } function mod (values) { const dividend = n(values[0]); const divisor = n(values[1]); return Decimal.mod(dividend, divisor).toNumber() } function multiply (values) { return values.reduce( (acc, value) => acc.times(n(value)), Decimal(1) ).toNumber() } function pow (values) { const [base, exponent] = values; return Decimal.pow(n(base), n(exponent)).toNumber() } function round (values) { const value = n(values[0]); const place = n(values[1] || 0); if (!isPlace(place)) { throw new TypeError( `cannot apply $round with precision value ${place} value must be in [-20, 100]` ) } if (place < 0) { return Decimal(value) .toSignificantDigits(Math.abs(place), Decimal.ROUND_DOWN) .toNumber() } else { return Decimal(value) .toDecimalPlaces(place, Decimal.ROUND_HALF_EVEN) .toNumber() } } function sqrt (values) { return Decimal.sqrt(n(values[0])).toNumber() } function subtract (values) { const [left, right] = values; const result = Decimal.sub( isDate(left) ? left.getTime() : n(left), isDate(right) ? right.getTime() : n(right) ).toNumber(); return isDate(left) && !isDate(right) ? new Date(result) : result } function trunc (values) { const value = n(values[0]); const place = n(values[1] || 0); if (!isPlace(place)) { throw new TypeError( `cannot apply $trunc with precision value ${place} value must be in [-20, 100]` ) } if (place < 0) { return Decimal(value) .toSignificantDigits(Math.abs(place), Decimal.ROUND_DOWN) .toNumber() } else { return Decimal(value) .toDecimalPlaces(place, Decimal.ROUND_DOWN) .toNumber() } } function isPlace (value) { return Number.isInteger(value) && value > -20 && value < 100 } function $operator$2 (callback, args, compile, operator) { if (operator === '$round' || operator === '$trunc') { if (args.length < 1 || args.length > 2) { throw new Error( `Expression ${operator} takes at least 1 argument, and at most 2` ) } } else if ( operator === '$divide' || operator === '$ln' || operator === '$log' || operator === '$log10' || operator === '$mod' || operator === '$pow' || operator === '$subtract' ) { if (args.length !== 2) { throw new Error(`Expression ${operator} takes exactly 2 arguments`) } } else if (operator !== '$add' && operator !== '$multiply') { if (args.length !== 1) { throw new Error(`Expression ${operator} takes exactly 1 argument`) } } const fns = args.map(compile); return (doc, ctx) => { const values = fns.map(fn => fn(doc, ctx)); for (const value of values) { if (isNullish(value)) { return null } else if (!isNumber(value)) { if (operator !== '$add' && operator !== '$subtract') { throw new TypeError( `Expression ${operator} only supports numeric types` ) } else if (!isDate(value)) { throw new TypeError( `Expression ${operator} only supports numeric or date types` ) } } } return callback(values) } } const $abs = bind($operator$2, abs); const $add = bind($operator$2, add); const $ceil = bind($operator$2, ceil); const $divide = bind($operator$2, divide); const $exp = bind($operator$2, exp); const $floor = bind($operator$2, floor); const $log = bind($operator$2, log); const $mod$1 = bind($operator$2, mod); const $multiply = bind($operator$2, multiply); const $pow = bind($operator$2, pow); const $round = bind($operator$2, round); const $sqrt = bind($operator$2, sqrt); const $subtract = bind($operator$2, subtract); const $trunc = bind($operator$2, trunc); function $log10 (args, compile, operator) { if (args.length !== 1) { throw new Error(`Expression ${operator} takes exactly 1 argument`) } return $log([args[0], 10], compile, operator) } function $ln (args, compile, operator) { if (args.length !== 1) { throw new Error(`Expression ${operator} takes exactly 1 argument`) } return $log([args[0], Math.E], compile, operator) } function eq (left, right) { return compileEq(right)(left) } function gt (left, right) { return compileGt(right)(left) } function gte (left, right) { return compileGte(right)(left) } function lt (left, right) { return compileLt(right)(left) } function lte (left, right) { return compileLte(right)(left) } function ne (left, right) { return !eq(left, right) } /** * Compiles an equality function against the argument. */ function compileEq (right) { const type = getBSONType(right); switch (type) { case BSON.Null: case BSON.Undefined: return isNullish case BSON.Boolean: case BSON.String: return left => left === right case BSON.MinKey: return isMinKey case BSON.MaxKey: return isMaxKey case BSON.Date: { const date = right.toISOString(); return left => isDate(left) && left.toISOString() === date } case BSON.ObjectId: { const id = right.toHexString(); return left => isObjectId(left) && left.toHexString() === id } case BSON.Timestamp: { const low = right.low; const high = right.high; return left => isTimestamp(left) && left.low === low && left.high === high } case BSON.Decimal128: { const decimal = n(right); return left => isDecimal128(left) && n(left) === decimal } case BSON.Double: { const double = n(right); return left => isDouble(left) && n(left) === double } case BSON.Int32: { const int = n(right); return left => isInt32(left) && n(left) === int } case BSON.Long: { const long = n(right); return left => isLong(left) && n(left) === long } case BSON.Symbol: { const symbol = right.valueOf(); return left => isSymbol(left) && left.valueOf() === symbol } case BSON.Array: return compileEqArray(right) case BSON.Object: return compileEqObject(right) default: throw new Error(`Unsupported equality for ${getTypeAlias(type)} type`) } } function compileEqArray (right) { const fns = right.map((item, index) => { const fn = compileEq(item); return left => fn(left[index]) }); const match = and(fns); return left => isArray(left) && left.length === right.length && match(left) } function compileEqObject (right) { const fns = Object.keys(right).map(key => { const fn = compileEq(right[key]); return left => fn(left[key]) }); const match = and(fns); return left => isPlainObject(left) && match(left) } /** * Compiles a "greater than" function against the argument. * The argument is the "right" argument. */ function compileGt (vRight) { const tRight = getBSONType(vRight); const wRight = getTypeWeight(tRight); const greaterThan = compileGtType(vRight, tRight); if (!greaterThan) { return vLeft => getTypeWeight(getBSONType(vLeft)) > wRight } return vLeft => { const tLeft = getBSONType(vLeft); return tLeft === tRight ? greaterThan(vLeft, tLeft) : getTypeWeight(tLeft) > wRight } } function compileGte (vRight) { const tRight = getBSONType(vRight); const wRight = getTypeWeight(tRight); const greaterThan = compileGtType(vRight, tRight); if (!greaterThan) { return vLeft => getTypeWeight(getBSONType(vLeft)) >= wRight } const equals = compileEq(vRight); return vLeft => greaterThan(vLeft) || equals(vLeft) } function compileGtType (right, type) { switch (type) { case BSON.Decimal128: case BSON.Double: case BSON.Int32: case BSON.Long: { const number = n(right); return left => left > number } case BSON.String: return left => left > right case BSON.Symbol: { const symbol = right.valueOf(); return left => left.valueOf() > symbol } case BSON.ObjectId: { const date = right.getTimestamp(); return left => left.getTimestamp() > date } case BSON.Boolean: return left => left === true && right === false case BSON.Date: { const iso = right.toISOString(); return left => left.toISOString() > iso } } } function compileLt (vRight) { const tRight = getBSONType(vRight); const wRight = getTypeWeight(tRight); const lesserThan = compileLtType(vRight, tRight); if (!lesserThan) { return vLeft => getTypeWeight(getBSONType(vLeft)) < wRight } return vLeft => { const tLeft = getBSONType(vLeft); return tLeft === tRight ? lesserThan(vLeft, tLeft) : getTypeWeight(tLeft) < wRight } } function compileLte (vRight) { const tRight = getBSONType(vRight); const wRight = getTypeWeight(tRight); const lesserThan = compileLtType(vRight, tRight); if (!lesserThan) { return vLeft => getTypeWeight(getBSONType(vLeft)) <= wRight } const equals = compileEq(vRight); return vLeft => lesserThan(vLeft) || equals(vLeft) } function compileLtType (right, type) { switch (type) { case BSON.Decimal128: case BSON.Double: case BSON.Int32: case BSON.Long: { const number = n(right); return left => left < number } case BSON.String: return left => left < right case BSON.Symbol: { const symbol = right.valueOf(); return left => left.valueOf() < symbol } case BSON.ObjectId: { const date = right.getTimestamp(); return left => left.getTimestamp() < date } case BSON.Boolean: return left => left === false && right === true case BSON.Date: { const iso = right.toISOString(); return left => left.toISOString() < iso } } } function compileType (type) { switch (type) { case BSON.Double: case 'double': return isDouble case BSON.String: case 'string': return isString case BSON.Object: case 'object': return isPlainObject case BSON.Array: case 'array': return isArray case BSON.Binary: case 'binData': return isBinary case BSON.Undefined: case 'undefined': return isUndefined case BSON.ObjectId: case 'objectId': return isObjectId case BSON.Boolean: case 'bool': return isBoolean case BSON.Date: case 'date': return isDate case BSON.Null: case 'null': return isNull case BSON.RegExp: case 'regex': return isRegExp case BSON.Reference: case 'dbPointer': return isReference case BSON.JavaScript: case 'javascript': return isJavaScript case BSON.Symbol: case 'symbol': return isSymbol case BSON.Int32: case 'int': return isInt32 case BSON.Timestamp: case 'timestamp': return isTimestamp case BSON.Long: case 'long': return isLong case BSON.Decimal128: case 'decimal': return isDecimal128 case BSON.MinKey: case 'minKey': return isMinKey case BSON.MaxKey: case 'maxKey': return isMaxKey case 'number': return isNumber default: throw new Error(`Unsupported type match for ${type}`) } } function $concatArrays (args, compile) { const fns = args.map(compile); return (doc, ctx) => { let result = []; for (const fn of fns) { const value = fn(doc, ctx); if (isNullish(value)) { return null } else if (isArray(value)) { result = result.concat(value); } else { throw new TypeError('Expression $concatArrays only supports arrays') } } return result } } function $in$1 (args, compile) { if (args.length !== 2) { throw new Error('Expression $in takes exactly 2 arguments') } const fns = args.map(compile); return (doc, ctx) => { const [subject, items] = fns.map(fn => fn(doc, ctx)); if (!isArray(items)) { throw new TypeError( 'Expression $in requires an array as a second argument' ) } return items.some(compileEq(subject)) } } function $isArray (args, compile) { if (args.length !== 1) { throw new Error('Expression $isArray takes exactly 1 argument') } const map = compile(args[0]); return (doc, ctx) => isArray(map(doc, ctx)) } function $size$1 (args, compile) { if (args.length !== 1) { throw new Error('Expression $size takes exactly 1 argument') } const map = compile(args[0]); return (doc, ctx) => { const value = map(doc, ctx); if (!isArray(value)) { throw new TypeError('The argument to $size must be an array') } return value.length } } function $and (args, compile) { const fns = args.map(compile); return (doc, ctx) => { for (const fn of fns) { if (isFalsy(fn(doc, ctx))) { return false } } return true } } function $or (args, compile) { const fns = args.map(compile); return (doc, ctx) => { for (const fn of fns) { if (isTruthy(fn(doc, ctx))) { return true } } return false } } function $not (args, compile) { if (args.length !== 1) { throw new Error('Expression $not takes exactly 1 argument') } const map = compile(args[0]); return (doc, ctx) => isFalsy(map(doc, ctx)) } function cmp (left, right) { if (lt(left, right)) { return -1 } else if (gt(left, right)) { return 1 } else { return 0 } } function $operator$1 (callback, args, compile, operator) { if (args.length !== 2) { throw new Error(`Expression ${operator} takes exactly 2 arguments`) } const fns = args.map(compile); return (doc, ctx) => { const [left, right] = fns.map(fn => fn(doc, ctx)); return callback(left, right) } } const $cmp = bind($operator$1, cmp); const $eq = bind($operator$1, eq); const $gt$1 = bind($operator$1, gt); const $gte$1 = bind($operator$1, gte); const $lt$1 = bind($operator$1, lt); const $lte$1 = bind($operator$1, lte); const $ne = bind($operator$1, ne); function $ifNull (args, compile) { if (args.length < 2) { throw new Error('Expression $ifNull needs at least two arguments') } const fns = args.map(compile); return (doc, ctx) => { for (const fn of fns) { const value = fn(doc, ctx); if (!isNullish(value)) { return value } } return null } } function $cond (args, compile) { const fns = parseCondition(args).map(compile); return (doc, ctx) => { if (fns[0](doc, ctx) === true) { return fns[1](doc, ctx) } else { return fns[2](doc, ctx) } } } function parseCondition (args) { if (args.length === 3) { return args } else if (args.length === 1 && isExpressionObject(args[0])) { return [args[0].if, args[0].then, args[0].else] } else { throw new Error('Expression $ceil takes exactly 3 arguments') } } function isExpressionObject (value) { return isPlainObject(value) && !isUndefined(value.if) && !isUndefined(value.then) && !isUndefined(value.else) } function $switch (args, compile) { if (args.length !== 1) { throw new Error('Expression $switch takes exactly 1 argument') } const obj = compileArgument(args[0], compile); return (doc, ctx) => { for (const branch of obj.branches) { if (isTruthy(branch.case(doc, ctx))) { return branch.then(doc, ctx) } } if (!obj.default) { throw new Error( 'One cannot execute a switch statement where all the cases evaluate to false without a default' ) } return obj.default(doc, ctx) } } function compileArgument (arg, compile) { if (!isPlainObject(arg)) { throw new TypeError('$switch requires an object as an argument') } if (!isArray(arg.branches)) { throw new TypeError("$switch expected an array for 'branches'") } if (arg.branches.length < 1) { throw new Error('$switch requires at least one branch') } return { branches: arg.branches.map(branch => compileBranch(branch, compile)), default: isUndefined(arg.default) ? undefined : compile(arg.default) } } function compileBranch (arg, compile) { if (!isPlainObject(arg)) { throw new TypeError('$switch expected each branch to be an object') } if (isUndefined(arg.case)) { throw new Error("$switch requires each branch have a 'case' expression") } if (isUndefined(arg.then)) { throw new Error("$switch requires each branch have a 'then' expression") } return { case: compile(arg.case), then: compile(arg.then) } } function $literal (args) { if (args.length !== 1) { throw new Error('Expression $literal takes exactly 1 argument') } // TODO: validate? return () => args[0] } function toDouble (value, type) { switch (type) { case BSON.Boolean: return value ? 1 : 0 case BSON.Double: return value case BSON.Int32: case BSON.Long: return n(value) case BSON.String: // TODO: some validation? return parseFloat(value) default: throw new TypeError( `Unsupported conversion from ${getTypeAlias(type)} to double` ) } } function toObjectId (value, type) { if (type === BSON.ObjectId) { return value } else if (type === BSON.String && bson.ObjectId.isValid(value)) { return new bson.ObjectId(value) } else { throw new TypeError( `Unsupported conversion from ${getTypeAlias(type)} to objectId` ) } } function toString (value, type) { switch (type) { case BSON.Boolean: return value ? 'true' : 'false' case BSON.ObjectId: return value.toHexString() case BSON.String: return value case BSON.Date: return value.toISOString() case BSON.Double: case BSON.Decimal128: case BSON.Int32: case BSON.Long: return `${n(value)}` default: throw new TypeError( `Unsupported conversion from ${getTypeAlias(type)} to string` ) } } function toBool (value, type) { switch (type) { case BSON.Boolean: return value case BSON.Double: case BSON.Int32: case BSON.Long: return n(value) !== 0 case BSON.ObjectId: case BSON.String: case BSON.Date: return true default: throw new TypeError( `Unsupported conversion from ${getTypeAlias(type)} to boolean` ) } } function type (value, type) { return type === BSON.Undefined ? 'missing' : getTypeAlias(type) } function $operator (callback, args, compile, operator) { if (args.length !== 1) { throw new Error(`Expression ${operator} takes exactly 1 argument`) } const fn = compile(args[0]); return (doc, ctx) => { const value = fn(doc, ctx); if (operator !== '$type' && isNullish(value)) { return null } const type = getBSONType(value); return callback(value, type) } } const $toBool = bind($operator, toBool); const $toDouble = bind($operator, toDouble); const $toObjectId = bind($operator, toObjectId); const $toString = bind($operator, toString); const $type$1 = bind($operator, type); const operators$2 = { $abs, $add, $and, $ceil, $cmp, $concatArrays, $cond, $divide, $eq, $exp, $floor, $gt: $gt$1, $gte: $gte$1, $ifNull, $in: $in$1, $isArray, $literal, $ln, $log, $log10, $lt: $lt$1, $lte: $lte$1, $mod: $mod$1, $multiply, $ne, $not, $or, $pow, $round, $size: $size$1, $sqrt, $subtract, $switch, $toBool, $toDouble, $toObjectId, $toString, $trunc, $type: $type$1 }; function compileAggregationExpression (expression) { const map = compileExpression$1(expression, true); return doc => map(doc, { root: doc, subject: doc }) } function compileExpression$1 (expression, isRoot) { if (isNullish(expression)) { return () => null } else if (expression === '$$CLUSTER_TIME') { return bson.Timestamp.fromNumber(Date.now() / 1000) } else if (expression === '$$NOW') { return () => new Date() } else if (expression === '$$ROOT') { return (doc, ctx) => ctx.root } else if ( isString(expression) && expression[0] === '$' && expression[1] !== '$' ) { return compileReader(expression.substring(1)) } else if (isOperatorExpression(expression)) { return compileOperatorExpression(expression) } else if (isPlainObject(expression)) { return compileObjectExpression(expression, isRoot === true) } else if (isArray(expression)) { const fns = expression.map(compileExpression$1); return (doc, ctx) => fns.map(fn => fn(doc, ctx)) } else if (isSafePrimitive(expression)) { return () => expression } else { throw new Error(`Unsupported aggregation expression: ${expression}`) } } function isSafePrimitive (value) { return isBinary(value) || isBoolean(value) || isDate(value) || isJavaScript(value) || isMaxKey(value) || isMinKey(value) || isNumber(value) || isObjectId(value) || isReference(value) || isRegExp(value) || isString(value) || isSymbol(value) || isTimestamp(value) } function compileOperatorExpression (expression) { const key = Object.keys(expression)[0]; const fn = operators$2[key]; if (!fn) { throw new Error(`Unsupported project operator: ${key}`) } return fn( getExpressionArguments(expression[key]), compileExpression$1, key ) } function getExpressionArguments (value = []) { return isArray(value) ? value : [value] } function compileObjectExpression (expression, isRoot = false) { // This will validate and simplify the expression object expression = parseObjectExpression(expression); const omit = []; const pick = []; const map = []; let idValue; for (const key of Object.keys(expression)) { const value = expression[key]; if (key === '_id' && isRoot) { idValue = value; } else if (value === 0 || value === false) { omit.push(key); } else if (value === 1 || value === true) { pick.push(key); } else { map.push(compileFieldValue(key, value)); } } if (omit.length > 0 && pick.length > 0) { throw new Error('Projection mode mix') } if (isRoot) { if (omit.length > 0) { if (idValue === 0 || idValue === false) { omit.push('_id'); } else if (!isUndefined(idValue) && idValue !== 1 && idValue !== true) { map.push(compileFieldValue('_id', idValue)); } } else if (pick.length > 0) { if (isUndefined(idValue) || idValue === 1 || idValue === true) { pick.push('_id'); } else if (idValue !== 0 && idValue !== false) { map.push(compileFieldValue('_id', idValue)); } } else if (idValue === 0 || idValue === false) { omit.push('_id'); } else if (idValue === 1 || idValue === true) { pick.push('_id'); } else if (!isUndefined(idValue)) { map.push(compileFieldValue('_id', idValue)); } else if (map.length > 0) { pick.push('_id'); } } let project; if (omit.length > 0) { project = compileOmitter(omit); } else if (pick.length > 0) { project = compilePicker(pick); } return (doc, ctx) => { const result = applyProjection(ctx.subject, project); if (isPlainObject(result)) { for (const fn of map) { fn(result, doc, ctx); } } return result } } function applyProjection (value, project) { if (isArray(value)) { return value.map(item => applyProjection(item, project)) } else if (isPlainObject(value)) { return project ? project({ ...value }) : { ...value } } else { return value } } function parseObjectExpression (expression) { const keys = Object.keys(expression); if (!keys.length) { throw new Error('Expression objects expects at least one field') } const result = {}; for (const key of keys) { const path = parsePath(key); const value = expression[key]; if (value === 0 || value === 1 || typeof value === 'boolean') { let subject = result; for (let i = 0; i < path.length; i++) { const chunk = path[i]; if (i === path.length - 1) { subject[chunk] = value; } else { if (subject[chunk] === undefined) { subject[chunk] = {}; } else if (!isPlainObject(subject[chunk])) { throw new Error(`Path collision at ${key}`) } subject = subject[chunk]; } } } else { result[key] = value; } } return result } function compileFieldValue (key, expression) { const map = compileExpression$1(expression); const read = compileReader(key); const write = compileWriter(key); return (result, doc, ctx) => { // Calculate the new subject (needed for projection) const subject = read(ctx.subject); // Write mapped result write( result, map(doc, { ...ctx, subject }) ); } } function compileOmitter (items) { return doc => { const result = {}; for (const key of Object.keys(doc)) { if (!items.includes(key)) { result[key] = doc[key]; } } return result } } function compilePicker (items) { return doc => { const result = {}; for (const key of Object.keys(doc)) { if (items.includes(key)) { result[key] = doc[key]; } } return result } } function $count (key) { if (typeof key !== 'string') { throw new TypeError('Stage $count expects a string as input') } return async function * countStage (iterable) { let count = 0; // eslint-disable-next-line for await (const _ of iterable) { count++; } yield { [key]: count }; } } function $limit (limit) { if (!Number.isInteger(limit) || limit <= 0) { throw new TypeError('Stage $limit expects limit a positive integer') } return async function * limitStage (iterable) { let count = 0; for await (const document of iterable) { count++; yield document; if (count >= limit) { return } } } } function $all (spec) { if (!Array.isArray(spec)) { throw new TypeError('Operator $all expects an array') } if (spec.length <= 0) { throw new Error('Expected array with at least one value') } const hasAllItems = and( spec.map(item => { const fn = compileEq(item); return value => value.findIndex(fn) >= 0 }) ); return value => isArray(value) && hasAllItems(value) } function $gt (right) { const sameType = compileType(getBSONType(right)); const greaterThan = compileGt(right); return left => sameType(left) && greaterThan(left) } function $gte (right) { const sameType = compileType(getBSONType(right)); const greaterOrEqual = compileGte(right); return left => sameType(left) && greaterOrEqual(left) } function $lt (right) { const sameType = compileType(getBSONType(right)); const lesserThan = compileLt(right); return left => sameType(left) && lesserThan(left) } function $lte (right) { const sameType = compileType(getBSONType(right)); const lesserOrEqual = compileLte(right); return left => sameType(left) && lesserOrEqual(left) } function $exists (spec) { if (spec === true) { return not(isUndefined) } else if (spec === false) { return isUndefined } else { throw new TypeError('Equality value must be boolean') } } function $in (spec) { if (!Array.isArray(spec)) { throw new TypeError('Inclusion value must be an array') } return or(spec.map(compileEq)) } function $mod (spec) { if (!isArray(spec)) { throw new TypeError('Operator $mod expects an array') } if (spec.length !== 2) { throw new Error('Operator $mod expects an array with two values') } if (!isNumber(spec[0]) || !isNumber(spec[1])) { throw new TypeError('Operator $mod only supports numeric types') } const divider = n(spec[0]); const result = n(spec[1]); return value => isNumber(value) && Decimal.mod(n(value), divider).equals(result) } function $regex (pattern, flags) { const reg = compile(pattern, flags); return value => isString(value) && reg.test(value) } function compile (pattern, flags) { // TODO: Support custom MongoDB RegExp flags? if (pattern instanceof RegExp) { // TODO: Join flags return pattern } else if (typeof pattern === 'string') { // TODO: Validate flags return new RegExp(pattern, flags) } else { throw new Error('Unknown RegExp type') } } function $size (spec) { if (!Number.isInteger(spec) || spec < 0) { throw new TypeError('Operator $size accepts positive integers or zero') } return value => isArray(value) && value.length === spec } function $type (spec) { return isArray(spec) ? or(spec.map(compileType)) : compileType(spec) } const uselessKeys = [ '$comment', '$options' // RegExp flags ]; const operators$1 = { $all, $and: and, $elemMatch, $eq: compileEq, $exists, $gt, $gte, $in, $lt, $lte, $mod, $ne: spec => not(compileEq(spec)), $nin: spec => not($in(spec)), $nor: nor, $not: spec => not(compileExpressionObject$1(spec)), $or: or, $regex: (spec, obj) => $regex(spec, obj.$options), $size, $type }; /** * Get only useful object keys. */ function getKeys (obj) { return Object.keys(obj).filter(key => !uselessKeys.includes(key)) } /** * Recursive matching function. * This version will return true if **ANY** element inside an array matches with the spec function. */ function positiveMatch (check, path, value) { if (path.length === 0) { if (check(value)) { return true } } if (isArray(value)) { for (const item of value) { if (positiveMatch(check, path, item)) { return true } } } else if (path.length > 0 && isPlainObject(value)) { return positiveMatch(check, path.slice(1), value[path[0]]) } return false } /** * Recursive matching function. * This version will return true if **ALL** elements inside an array matches with the spec function. */ function negativeMatch (check, path, value) { if (path.length === 0) { if (check(value)) { return true } } if (isArray(value)) { for (const item of value) { if (!negativeMatch(check, path, item)) { return false } } return true } else if (path.length > 0 && isPlainObject(value)) { return negativeMatch(check, path.slice(1), value[path[0]]) } return false } /** * This operator is a recursive, leave It here. */ function $elemMatch (spec) { if (!isPlainObject(spec)) { throw new TypeError('Operator $elemMatch needs a query object') } const fn = compileExpressionObject$1(spec); return value => Array.isArray(value) && value.findIndex(fn) >= 0 } function compileOperatorKey (obj, key) { const fn = operators$1[key]; if (!fn) { throw new Error(`Unsupported filter operator: ${key}`) } return fn(obj[key], obj) } function compileFilterQuery (query = {}) { return isPlainObject(query) ? compileExpressionObject$1(query) : compileEq(query) } function compileExpressionObject$1 (obj) { if (!isPlainObject(obj)) { throw new TypeError('Expected expression object') } return and(getKeys(obj).map(key => compileExpressionKey(obj, key))) } function compileExpressionKey (query, key) { const spec = query[key]; if (key === '$and') { return and(compileLogicalSequence(spec)) } else if (key === '$nor') { return nor(compileLogicalSequence(spec)) } else if (key === '$or') { return or(compileLogicalSequence(spec)) } else if (key[0] === '$') { return compileOperatorKey(query, key) } else { const { check, negated } = compileExpressionValue(spec); const match = negated ? negativeMatch : positiveMatch; return match.bind(null, check, parsePath(key)) } } function compileLogicalSequence (sequence) { if (!Array.isArray(sequence)) { throw new TypeError('Expected logical (array) sequence') } return sequence.map(compileExpressionObject$1) } function compileExpressionValue (spec) { const keys = isPlainObject(spec) ? getKeys(spec) : []; if (keys.findIndex(key => key[0] === '$') >= 0) { return { check: and( keys.map( key => compileOperatorKey(spec, key) ) ), negated: keys.some(key => key === '$ne' || key === '$not') } } else { return { check: compileEq(spec), negated: false } } } function $match (query) { const fn = compileFilterQuery(query); return async function * matchStage (iterable) { for await (const document of iterable) { if (fn(document)) { yield document; } } } } function $project (expression) { if (!isPlainObject(expression)) { throw new TypeError('Stage $project expects an object') } const map = compileAggregationExpression(expression); return async function * projectStage (iterable) { for await (const document of iterable) { yield map(document); } } } function $set$1 (expression) { if (!isPlainObject(expression)) { throw new TypeError('Stage $set expects an object') } const fn = compileAggregationExpression({ ...cast(expression), __hack__: 0 // Hack to pick all properties }); return async function * setStage (iterable) { for await (const document of iterable) { yield fn(document); } } } function cast (value) { if (value === undefined) { return null } else if (value === 0 || value === 1 || typeof value === 'boolean') { return { $literal: value } } else if (Array.isArray(value)) { return value.map(cast) } else if (isPlainObject(value)) { return Object.keys(value).reduce( (acc, key) => { acc[key] = cast(value[key]); return acc }, {} ) } else { return value } } function $skip (skip) { if (!Number.isInteger(skip) || skip < 0) { throw new TypeError('Stage $skip expects a positive integer or zero') } return async function * skipStage (iterable) { for await (const document of iterable) { if (skip > 0) { skip--; } else { yield document; } } } } function $sort$1 (expression) { if (!isPlainObject(expression)) { throw new TypeError('Stage $sort expects an object') } const compare = compileExpressionObject(expression); return async function * unsetStage (iterable) { const documents = []; for await (const document of iterable) { documents.push(document); } for (const document of documents.sort(compare)) { yield document; } } } function compileExpressionObject (obj) { const keys = Object.keys(obj); if (keys.length > 32) { throw new Error('Maximum 32 keys are allowed for sorting') } const fns = keys.map(key => { const value = obj[key]; if (value === 1) { return sortAsc(key) } else if (value === -1) { return sortDesc(key) } else { throw new Error(`Unsupported sorting order: ${value}`) } }); return (a, b) => { for (const fn of fns) { const value = fn(a, b); if (value !== 0) { return value } } return 0 } } function sortAsc (key) { const read = compileReader(key); return (a, b) => { const left = read(a); const right = read(b); if (lt(left, right)) { return -1 } else if (gt(left, right)) { return 1 } else { return 0 } } } function sortDesc (key) { const read = compileReader(key); return (a, b) => { const left = read(a); const right = read(b); if (lt(left, right)) { return 1 } else if (gt(left, right)) { return -1 } else { return 0 } } } function $unset$1 (expression) { const fn = compileProjectionAlias(expression); return async function * unsetStage (iterable) { for await (const document of iterable) { yield fn(document); } } } function compileProjectionAlias (expression) { if (typeof expression === 'string') { return compileAggregationExpression({ [expression]: 0 }) } else if (Array.isArray(expression)) { return compileUnsetSeries(expression) } else if (isPlainObject(expression)) { return compileAggregationExpression(expression) } else { throw new TypeError(`Unexpected $unset value: ${expression}`) } } function compileUnsetSeries (items) { return compileAggregationExpression( items.reduce( (acc, item) => { if (typeof item !== 'string') { throw new TypeError('Expected string value') } acc[item] = 0; return acc }, {} ) ) } function $unwind (expression) { const options = typeof expression === 'string' ? { path: expression } : expression; if (!isPlainObject(options)) { throw new TypeError('Unexpected $unwind stage options') } if (!isValidFieldPath(options.path)) { throw new TypeError('Expected valid $unwind path field') } if (options.includeArrayIndex !== undefined && !isIdentifier(options.includeArrayIndex)) { throw new TypeError('Invalid $unwind index field') } const key = options.path.substring(1); const read = compileReader(key); return async function * unwindStage (iterable) { for await (const document of iterable) { const value = read(document); if (isArray(value) && value.length > 0) { for (let i = 0; i < value.length; i++) { const item = value[i]; const result = { ...document, [key]: item }; if (options.includeArrayIndex) { result[options.includeArrayIndex] = i; } yield result; } } else if (options.preserveNullAndEmptyArrays === true || !isNullOrEmptyArray(value)) { const mapped = { ...document }; if (isArray(value)) { delete mapped[key]; } if (options.includeArrayIndex) { mapped[options.includeArrayIndex] = null; } yield mapped; } } } } function isValidFieldPath (value) { return typeof value === 'string' && value[0] === '$' && isIdentifier(value.substring(1)) } function isEmptyArray (value) { return isArray(value) && value.length === 0 } function isNullOrEmptyArray (value) { return isUndefined(value) || isNull(value) || isEmptyArray(value) } function compileAggregationPipeline (stages) { if (!isArray(stages)) { throw new TypeError('An aggregation pipeline must be an array') } const fns = stages.map(compileStage); return function aggregate (iterable) { return fns.reduce((acc, fn) => fn(acc), iterable) } } const stages = { $addFields: $set$1, $count, $limit, $match, $project, $set: $set$1, $skip, $sort: $sort$1, $unset: $unset$1, $unwind }; function compileStage (stage) { if (!isOperatorExpression(stage)) { throw new TypeError('Unexpected aggregation stage') } const key = Object.keys(stage)[0]; const fn = stages[key]; if (!fn) { throw new Error(`Unsupported aggregation stage: ${key}`) } return fn(stage[key]) } function $addToSet (key, arg) { const read = compileReader(key); const write = compileWriter(key); const fn = createMapFunction$1(extractItems(arg)); return doc => { let items = read(doc); if (isNullish(items)) { items = []; write(doc, items); } else if (!isArray(items)) { throw new TypeError('Operator $addToSet expects an array') } fn(items); } } function extractItems (arg) { if (isPlainObject(arg)) { const keys = Object.keys(arg); if (keys.some(key => key[0] === '$')) { if (keys.length === 1 && keys[0] === '$each' && isArray(arg.$each)) { return arg.$each } else { throw new Error('Invalid modifier usage') } } } return [arg] } function createMapFunction$1 (values) { const fns = values.map(compileEq); return items => { for (let i = 0; i < values.length; i++) { if (!items.some(fns[i])) { items.push(values[i]); } } } } function $currentDate (key, arg) { const write = compileWriter(key); if (arg === true || Object(arg).$type === 'date') { return document => write( document, new Date() ) } else if (Object(arg).$type === 'timestamp') { return document => write( document, bson.Timestamp.fromNumber(Date.now() / 1000) ) } else { throw new Error('Operator $currentDate has found an invalid argument') } } function $inc (key, value) { value = n(value); if (!isFinite(value)) { throw new TypeError('Operator $inc expectes a finite number') } const read = compileReader(key); const write = compileWriter(key); return document => { let current = n(read(document)); if (isUndefined(current)) { current = 0; } if (!isNumber(current)) { throw new Error('Cannot apply $inc operator') } write(document, Decimal.add(current, value).toNumber()); } } function $max (key, arg) { const read = compileReader(key); const write = compileWriter(key); const compare = compileLt(arg); return doc => { if (compare(read(doc))) { write(doc, arg); } } } function $min (key, arg) { const read = compileReader(key); const write = compileWriter(key); const compare = compileGt(arg); return doc => { if (compare(read(doc))) { write(doc, arg); } } } function $mul (key, value) { if (!isNumber(value)) { throw new TypeError('Operator $mul expects a finite number') } const read = compileReader(key); const write = compileWriter(key); return document => { let current = read(document); if (isUndefined(current)) { current = 0; } if (!isNumber(current)) { throw new Error(`Cannot apply $mul operator to ${document._id} document`) } write( document, Decimal.mul(n(current), n(value)).toNumber() ); } } function $pop (key, value) { if (value !== 1 && value !== -1) { throw new TypeError('Operator $pop accepts only 1 or -1 as input values') } const readValue = compileReader(key); return document => { const arr = readValue(document); if (!isArray(arr)) { throw new TypeError(`Operator $pop cannot update the ${key} field`) } else if (value === -1) { arr.shift(); } else { arr.pop(); } } } function $pull (key, query) { const readValue = compileReader(key); const writeValue = compileWriter(key); const match = compileFilterQuery(query); return document => { const items = readValue(document); if (isArray(items)) { writeValue(document, items.filter(item => !match(item))); } } } function $push (key, data) { const readValue = compileReader(key); const writeValue = compileWriter(key); const fn = createMapFunction(data); return document => { let items = readValue(document); if (isUndefined(items)) { items = []; writeValue(document, items); } else if (!isArray(items)) { throw new Error('Operator $push expects an array') } fn(items); } } function createMapFunction (data) { if (isPlainObject(data) && isArray(data.$each)) { return compileModifiers(data) } else { return items => items.push(data) } } function compileModifiers (obj) { const fns = [ isUndefined(obj.$position) ? $position(Number.POSITIVE_INFINITY, obj.$each) : $position(obj.$position, obj.$each) ]; if (!isUndefined(obj.$sort)) { fns.push($sort(obj.$sort)); } if (!isUndefined(obj.$slice)) { fns.push($slice(obj.$slice)); } return compose(fns) } function $position (index, newItems) { if (index === Number.POSITIVE_INFINITY) { return oldItems => oldItems.push(...newItems) } else if (Number.isInteger(index)) { return oldItems => oldItems.splice(index, 0, ...newItems) } else { throw new TypeError('Modifier $position expects an integer value') } } function $slice (value) { if (!Number.isInteger(value)) { throw new TypeError('Modifier $slice expects an integer value') } else if (value >= 0) { return items => items.splice(value, items.length) } else { return items => items.splice(0, items.length - value) } } function $sort (obj) { if (!isPlainObject(obj)) { throw new TypeError('Modifier $sort expects an object') } const keys = Object.keys(obj); if (keys.length <= 0) { return () => {} } else if (keys.length > 1) { // TODO: support multiple keys inside the $sort modifier throw new Error('Modifier $sort currently supports only one key') } const [key] = keys; const order = obj[key]; if (order !== 1 && order !== -1) { throw new TypeError('Modifier $sort needs a valid object specification') } const readValue = compileReader(key); return items => { items.sort((a, b) => { const va = readValue(a); const vb = readValue(b); if (va < vb) { return -1 * order } else if (va > vb) { return 1 * order } else { return 0 } }); } } function compose (fns) { return items => { for (const fn of fns) { fn(items); } } } function $pullAll (key, arg) { if (!isArray(arg)) { throw new TypeError('Operator $pullAll expects an array') } const read = compileReader(key); const fns = arg.map(compileEq); return doc => { const items = read(doc); if (isNullish(items)) { return } else if (!isArray(items)) { throw new TypeError('Operator $pullAll expects an array') } for (const fn of fns) { let ok = true; while (ok) { const index = items.findIndex(fn); if (index >= 0) { items.splice(index, 1); } else { ok = false; } } } } } function $rename (oldKey, newKey) { const readValue = compileReader(oldKey); const writeValue = compileWriter(newKey); const deleteValue = compileDeleter(oldKey); return document => { const value = readValue(document); if (value !== undefined) { writeValue(document, value); deleteValue(document); } } } function $set (key, value) { const writeValue = compileWriter(key); return document => { writeValue(document, value); } } function $setOnInsert (key, value) { const writeValue = compileWriter(key); return (document, ctx) => { if (ctx.insert) { writeValue(document, value); } } } function $unset (key, value) { if (value !== '') { throw new Error('Operator $unset expects an empty string') } const deleteValue = compileDeleter(key); return document => { deleteValue(document); } } const operators = { $addToSet, $currentDate, $inc, $max, $min, $mul, $pop, $pull, $pullAll, $push, $rename, $set, $setOnInsert, $unset }; function getOperator (key) { const operator = operators[key]; if (!operator) { throw new Error(`Unsupported update operator: ${key}`) } return operator } function compileExpression (obj, operator) { if (!isPlainObject(obj)) { throw new TypeError('An update expression must be an object') } return Object.keys(obj).map(path => operator(path, obj[path])) } function compileUpdateQuery (query) { if (!isPlainObject(query)) { throw new TypeError('Update query must be a plain object') } const fns = Object.keys(query) .map(key => compileExpression(query[key], getOperator(key))) .reduce((a, b) => a.concat(b), []); return (document, insert) => { if (isObjectLike(document)) { const ctx = { insert: insert === true }; if (ctx.insert && isNullish(document._id)) { document._id = new bson.ObjectId(); } for (const fn of fns) { fn(document, ctx); } } return document } } exports.compileAggregationExpression = compileAggregationExpression; exports.compileAggregationPipeline = compileAggregationPipeline; exports.compileFilterQuery = compileFilterQuery; exports.compileUpdateQuery = compileUpdateQuery;