'use strict'; const MongoError = require('./core/error').MongoError; const WriteConcern = require('./write_concern'); var shallowClone = function(obj) { var copy = {}; for (var name in obj) copy[name] = obj[name]; return copy; }; // Set simple property var getSingleProperty = function(obj, name, value) { Object.defineProperty(obj, name, { enumerable: true, get: function() { return value; } }); }; var formatSortValue = (exports.formatSortValue = function(sortDirection) { var value = ('' + sortDirection).toLowerCase(); switch (value) { case 'ascending': case 'asc': case '1': return 1; case 'descending': case 'desc': case '-1': return -1; default: throw new Error( 'Illegal sort clause, must be of the form ' + "[['field1', '(ascending|descending)'], " + "['field2', '(ascending|descending)']]" ); } }); var formattedOrderClause = (exports.formattedOrderClause = function(sortValue) { var orderBy = new Map(); if (sortValue == null) return null; if (Array.isArray(sortValue)) { if (sortValue.length === 0) { return null; } for (var i = 0; i < sortValue.length; i++) { if (sortValue[i].constructor === String) { orderBy.set(`${sortValue[i]}`, 1); } else { orderBy.set(`${sortValue[i][0]}`, formatSortValue(sortValue[i][1])); } } } else if (sortValue != null && typeof sortValue === 'object') { if (sortValue instanceof Map) { orderBy = sortValue; } else { var sortKeys = Object.keys(sortValue); for (var k of sortKeys) { orderBy.set(k, sortValue[k]); } } } else if (typeof sortValue === 'string') { orderBy.set(`${sortValue}`, 1); } else { throw new Error( 'Illegal sort clause, must be of the form ' + "[['field1', '(ascending|descending)'], ['field2', '(ascending|descending)']]" ); } return orderBy; }); var checkCollectionName = function checkCollectionName(collectionName) { if ('string' !== typeof collectionName) { throw new MongoError('collection name must be a String'); } if (!collectionName || collectionName.indexOf('..') !== -1) { throw new MongoError('collection names cannot be empty'); } if ( collectionName.indexOf('$') !== -1 && collectionName.match(/((^\$cmd)|(oplog\.\$main))/) == null ) { throw new MongoError("collection names must not contain '$'"); } if (collectionName.match(/^\.|\.$/) != null) { throw new MongoError("collection names must not start or end with '.'"); } // Validate that we are not passing 0x00 in the collection name if (collectionName.indexOf('\x00') !== -1) { throw new MongoError('collection names cannot contain a null character'); } }; var handleCallback = function(callback, err, value1, value2) { try { if (callback == null) return; if (callback) { return value2 ? callback(err, value1, value2) : callback(err, value1); } } catch (err) { process.nextTick(function() { throw err; }); return false; } return true; }; /** * Wrap a Mongo error document in an Error instance * @ignore * @api private */ var toError = function(error) { if (error instanceof Error) return error; var msg = error.err || error.errmsg || error.errMessage || error; var e = MongoError.create({ message: msg, driver: true }); // Get all object keys var keys = typeof error === 'object' ? Object.keys(error) : []; for (var i = 0; i < keys.length; i++) { try { e[keys[i]] = error[keys[i]]; } catch (err) { // continue } } return e; }; /** * @ignore */ var normalizeHintField = function normalizeHintField(hint) { var finalHint = null; if (typeof hint === 'string') { finalHint = hint; } else if (Array.isArray(hint)) { finalHint = {}; hint.forEach(function(param) { finalHint[param] = 1; }); } else if (hint != null && typeof hint === 'object') { finalHint = {}; for (var name in hint) { finalHint[name] = hint[name]; } } return finalHint; }; /** * Create index name based on field spec * * @ignore * @api private */ var parseIndexOptions = function(fieldOrSpec) { var fieldHash = {}; var indexes = []; var keys; // Get all the fields accordingly if ('string' === typeof fieldOrSpec) { // 'type' indexes.push(fieldOrSpec + '_' + 1); fieldHash[fieldOrSpec] = 1; } else if (Array.isArray(fieldOrSpec)) { fieldOrSpec.forEach(function(f) { if ('string' === typeof f) { // [{location:'2d'}, 'type'] indexes.push(f + '_' + 1); fieldHash[f] = 1; } else if (Array.isArray(f)) { // [['location', '2d'],['type', 1]] indexes.push(f[0] + '_' + (f[1] || 1)); fieldHash[f[0]] = f[1] || 1; } else if (isObject(f)) { // [{location:'2d'}, {type:1}] keys = Object.keys(f); keys.forEach(function(k) { indexes.push(k + '_' + f[k]); fieldHash[k] = f[k]; }); } else { // undefined (ignore) } }); } else if (isObject(fieldOrSpec)) { // {location:'2d', type:1} keys = Object.keys(fieldOrSpec); keys.forEach(function(key) { indexes.push(key + '_' + fieldOrSpec[key]); fieldHash[key] = fieldOrSpec[key]; }); } return { name: indexes.join('_'), keys: keys, fieldHash: fieldHash }; }; var isObject = (exports.isObject = function(arg) { return '[object Object]' === Object.prototype.toString.call(arg); }); var debugOptions = function(debugFields, options) { var finaloptions = {}; debugFields.forEach(function(n) { finaloptions[n] = options[n]; }); return finaloptions; }; var decorateCommand = function(command, options, exclude) { for (var name in options) { if (exclude.indexOf(name) === -1) command[name] = options[name]; } return command; }; var mergeOptions = function(target, source) { for (var name in source) { target[name] = source[name]; } return target; }; // Merge options with translation var translateOptions = function(target, source) { var translations = { // SSL translation options sslCA: 'ca', sslCRL: 'crl', sslValidate: 'rejectUnauthorized', sslKey: 'key', sslCert: 'cert', sslPass: 'passphrase', // SocketTimeout translation options socketTimeoutMS: 'socketTimeout', connectTimeoutMS: 'connectionTimeout', // Replicaset options replicaSet: 'setName', rs_name: 'setName', secondaryAcceptableLatencyMS: 'acceptableLatency', connectWithNoPrimary: 'secondaryOnlyConnectionAllowed', // Mongos options acceptableLatencyMS: 'localThresholdMS' }; for (var name in source) { if (translations[name]) { target[translations[name]] = source[name]; } else { target[name] = source[name]; } } return target; }; var filterOptions = function(options, names) { var filterOptions = {}; for (var name in options) { if (names.indexOf(name) !== -1) filterOptions[name] = options[name]; } // Filtered options return filterOptions; }; // Write concern keys const WRITE_CONCERN_KEYS = ['w', 'j', 'wtimeout', 'fsync', 'writeConcern']; /** * If there is no WriteConcern related options defined on target then inherit from source. * Otherwise, do not inherit **any** options from source. * @internal * @param {object} target - options object conditionally receiving the writeConcern options * @param {object} source - options object containing the potentially inherited writeConcern options */ function conditionallyMergeWriteConcern(target, source) { let found = false; for (const wcKey of WRITE_CONCERN_KEYS) { if (wcKey in target) { // Found a writeConcern option found = true; break; } } if (!found) { for (const wcKey of WRITE_CONCERN_KEYS) { if (source[wcKey]) { if (!('writeConcern' in target)) { target.writeConcern = {}; } target.writeConcern[wcKey] = source[wcKey]; } } } return target; } /** * Executes the given operation with provided arguments. * * This method reduces large amounts of duplication in the entire codebase by providing * a single point for determining whether callbacks or promises should be used. Additionally * it allows for a single point of entry to provide features such as implicit sessions, which * are required by the Driver Sessions specification in the event that a ClientSession is * not provided * * @param {object} topology The topology to execute this operation on * @param {function} operation The operation to execute * @param {array} args Arguments to apply the provided operation * @param {object} [options] Options that modify the behavior of the method */ const executeLegacyOperation = (topology, operation, args, options) => { if (topology == null) { throw new TypeError('This method requires a valid topology instance'); } if (!Array.isArray(args)) { throw new TypeError('This method requires an array of arguments to apply'); } options = options || {}; const Promise = topology.s.promiseLibrary; let callback = args[args.length - 1]; // The driver sessions spec mandates that we implicitly create sessions for operations // that are not explicitly provided with a session. let session, opOptions, owner; if (!options.skipSessions && topology.hasSessionSupport()) { opOptions = args[args.length - 2]; if (opOptions == null || opOptions.session == null) { owner = Symbol(); session = topology.startSession({ owner }); const optionsIndex = args.length - 2; args[optionsIndex] = Object.assign({}, args[optionsIndex], { session: session }); } else if (opOptions.session && opOptions.session.hasEnded) { throw new MongoError('Use of expired sessions is not permitted'); } } const makeExecuteCallback = (resolve, reject) => function executeCallback(err, result) { if (session && session.owner === owner && !options.returnsCursor) { session.endSession(() => { delete opOptions.session; if (err) return reject(err); resolve(result); }); } else { if (err) return reject(err); resolve(result); } }; // Execute using callback if (typeof callback === 'function') { callback = args.pop(); const handler = makeExecuteCallback( result => callback(null, result), err => callback(err, null) ); args.push(handler); try { return operation.apply(null, args); } catch (e) { handler(e); throw e; } } // Return a Promise if (args[args.length - 1] != null) { throw new TypeError('final argument to `executeLegacyOperation` must be a callback'); } return new Promise(function(resolve, reject) { const handler = makeExecuteCallback(resolve, reject); args[args.length - 1] = handler; try { return operation.apply(null, args); } catch (e) { handler(e); } }); }; /** * Applies retryWrites: true to a command if retryWrites is set on the command's database. * * @param {object} target The target command to which we will apply retryWrites. * @param {object} db The database from which we can inherit a retryWrites value. */ function applyRetryableWrites(target, db) { if (db && db.s.options.retryWrites) { target.retryWrites = true; } return target; } /** * Applies a write concern to a command based on well defined inheritance rules, optionally * detecting support for the write concern in the first place. * * @param {Object} target the target command we will be applying the write concern to * @param {Object} sources sources where we can inherit default write concerns from * @param {Object} [options] optional settings passed into a command for write concern overrides * @returns {Object} the (now) decorated target */ function applyWriteConcern(target, sources, options) { options = options || {}; const db = sources.db; const coll = sources.collection; if (options.session && options.session.inTransaction()) { // writeConcern is not allowed within a multi-statement transaction if (target.writeConcern) { delete target.writeConcern; } return target; } const writeConcern = WriteConcern.fromOptions(options); if (writeConcern) { return Object.assign(target, { writeConcern }); } if (coll && coll.writeConcern) { return Object.assign(target, { writeConcern: Object.assign({}, coll.writeConcern) }); } if (db && db.writeConcern) { return Object.assign(target, { writeConcern: Object.assign({}, db.writeConcern) }); } return target; } /** * Checks if a given value is a Promise * * @param {*} maybePromise * @return true if the provided value is a Promise */ function isPromiseLike(maybePromise) { return maybePromise && typeof maybePromise.then === 'function'; } /** * Applies collation to a given command. * * @param {object} [command] the command on which to apply collation * @param {(Cursor|Collection)} [target] target of command * @param {object} [options] options containing collation settings */ function decorateWithCollation(command, target, options) { const topology = (target.s && target.s.topology) || target.topology; if (!topology) { throw new TypeError('parameter "target" is missing a topology'); } const capabilities = topology.capabilities(); if (options.collation && typeof options.collation === 'object') { if (capabilities && capabilities.commandsTakeCollation) { command.collation = options.collation; } else { throw new MongoError(`Current topology does not support collation`); } } } /** * Applies a read concern to a given command. * * @param {object} command the command on which to apply the read concern * @param {Collection} coll the parent collection of the operation calling this method */ function decorateWithReadConcern(command, coll, options) { if (options && options.session && options.session.inTransaction()) { return; } let readConcern = Object.assign({}, command.readConcern || {}); if (coll.s.readConcern) { Object.assign(readConcern, coll.s.readConcern); } if (Object.keys(readConcern).length > 0) { Object.assign(command, { readConcern: readConcern }); } } /** * Applies an explain to a given command. * @internal * * @param {object} command - the command on which to apply the explain * @param {Explain} explain - the options containing the explain verbosity * @return the new command */ function decorateWithExplain(command, explain) { if (command.explain) { return command; } return { explain: command, verbosity: explain.verbosity }; } const nodejsMajorVersion = +process.version.split('.')[0].substring(1); const emitProcessWarning = msg => nodejsMajorVersion <= 6 ? process.emitWarning(msg, 'DeprecationWarning', MONGODB_WARNING_CODE) : process.emitWarning(msg, { type: 'DeprecationWarning', code: MONGODB_WARNING_CODE }); // eslint-disable-next-line no-console const emitConsoleWarning = msg => console.error(msg); const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning; /** * Default message handler for generating deprecation warnings. * * @param {string} name function name * @param {string} option option name * @return {string} warning message * @ignore * @api private */ function defaultMsgHandler(name, option) { return `${name} option [${option}] is deprecated and will be removed in a later version.`; } /** * Deprecates a given function's options. * * @param {object} config configuration for deprecation * @param {string} config.name function name * @param {Array} config.deprecatedOptions options to deprecate * @param {number} config.optionsIndex index of options object in function arguments array * @param {function} [config.msgHandler] optional custom message handler to generate warnings * @param {function} fn the target function of deprecation * @return {function} modified function that warns once per deprecated option, and executes original function * @ignore * @api private */ function deprecateOptions(config, fn) { if (process.noDeprecation === true) { return fn; } const msgHandler = config.msgHandler ? config.msgHandler : defaultMsgHandler; const optionsWarned = new Set(); function deprecated() { const options = arguments[config.optionsIndex]; // ensure options is a valid, non-empty object, otherwise short-circuit if (!isObject(options) || Object.keys(options).length === 0) { return fn.apply(this, arguments); } config.deprecatedOptions.forEach(deprecatedOption => { if ( Object.prototype.hasOwnProperty.call(options, deprecatedOption) && !optionsWarned.has(deprecatedOption) ) { optionsWarned.add(deprecatedOption); const msg = msgHandler(config.name, deprecatedOption); emitDeprecationWarning(msg); if (this && this.getLogger) { const logger = this.getLogger(); if (logger) { logger.warn(msg); } } } }); return fn.apply(this, arguments); } // These lines copied from https://github.com/nodejs/node/blob/25e5ae41688676a5fd29b2e2e7602168eee4ceb5/lib/internal/util.js#L73-L80 // The wrapper will keep the same prototype as fn to maintain prototype chain Object.setPrototypeOf(deprecated, fn); if (fn.prototype) { // Setting this (rather than using Object.setPrototype, as above) ensures // that calling the unwrapped constructor gives an instanceof the wrapped // constructor. deprecated.prototype = fn.prototype; } return deprecated; } const SUPPORTS = {}; // Test asyncIterator support try { require('./async/async_iterator'); SUPPORTS.ASYNC_ITERATOR = true; } catch (e) { SUPPORTS.ASYNC_ITERATOR = false; } class MongoDBNamespace { constructor(db, collection) { this.db = db; this.collection = collection; } toString() { return this.collection ? `${this.db}.${this.collection}` : this.db; } withCollection(collection) { return new MongoDBNamespace(this.db, collection); } static fromString(namespace) { if (!namespace) { throw new Error(`Cannot parse namespace from "${namespace}"`); } const index = namespace.indexOf('.'); return new MongoDBNamespace(namespace.substring(0, index), namespace.substring(index + 1)); } } function* makeCounter(seed) { let count = seed || 0; while (true) { const newCount = count; count += 1; yield newCount; } } /** * Helper function for either accepting a callback, or returning a promise * * @param {Object} parent an instance of parent with promiseLibrary. * @param {object} parent.s an object containing promiseLibrary. * @param {function} parent.s.promiseLibrary an object containing promiseLibrary. * @param {[Function]} callback an optional callback. * @param {Function} fn A function that takes a callback * @returns {Promise|void} Returns nothing if a callback is supplied, else returns a Promise. */ function maybePromise(parent, callback, fn) { const PromiseLibrary = (parent && parent.s && parent.s.promiseLibrary) || Promise; let result; if (typeof callback !== 'function') { result = new PromiseLibrary((resolve, reject) => { callback = (err, res) => { if (err) return reject(err); resolve(res); }; }); } fn(function(err, res) { if (err != null) { try { callback(err); } catch (error) { return process.nextTick(() => { throw error; }); } return; } callback(err, res); }); return result; } function now() { const hrtime = process.hrtime(); return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000); } function calculateDurationInMs(started) { if (typeof started !== 'number') { throw TypeError('numeric value required to calculate duration'); } const elapsed = now() - started; return elapsed < 0 ? 0 : elapsed; } /** * Creates an interval timer which is able to be woken up sooner than * the interval. The timer will also debounce multiple calls to wake * ensuring that the function is only ever called once within a minimum * interval window. * * @param {function} fn An async function to run on an interval, must accept a `callback` as its only parameter * @param {object} [options] Optional settings * @param {number} [options.interval] The interval at which to run the provided function * @param {number} [options.minInterval] The minimum time which must pass between invocations of the provided function * @param {boolean} [options.immediate] Execute the function immediately when the interval is started */ function makeInterruptableAsyncInterval(fn, options) { let timerId; let lastCallTime; let lastWakeTime; let stopped = false; options = options || {}; const interval = options.interval || 1000; const minInterval = options.minInterval || 500; const immediate = typeof options.immediate === 'boolean' ? options.immediate : false; const clock = typeof options.clock === 'function' ? options.clock : now; function wake() { const currentTime = clock(); const timeSinceLastWake = currentTime - lastWakeTime; const timeSinceLastCall = currentTime - lastCallTime; const timeUntilNextCall = interval - timeSinceLastCall; lastWakeTime = currentTime; // For the streaming protocol: there is nothing obviously stopping this // interval from being woken up again while we are waiting "infinitely" // for `fn` to be called again`. Since the function effectively // never completes, the `timeUntilNextCall` will continue to grow // negatively unbounded, so it will never trigger a reschedule here. // debounce multiple calls to wake within the `minInterval` if (timeSinceLastWake < minInterval) { return; } // reschedule a call as soon as possible, ensuring the call never happens // faster than the `minInterval` if (timeUntilNextCall > minInterval) { reschedule(minInterval); } // This is possible in virtualized environments like AWS Lambda where our // clock is unreliable. In these cases the timer is "running" but never // actually completes, so we want to execute immediately and then attempt // to reschedule. if (timeUntilNextCall < 0) { executeAndReschedule(); } } function stop() { stopped = true; if (timerId) { clearTimeout(timerId); timerId = null; } lastCallTime = 0; lastWakeTime = 0; } function reschedule(ms) { if (stopped) return; clearTimeout(timerId); timerId = setTimeout(executeAndReschedule, ms || interval); } function executeAndReschedule() { lastWakeTime = 0; lastCallTime = clock(); fn(err => { if (err) throw err; reschedule(interval); }); } if (immediate) { executeAndReschedule(); } else { lastCallTime = clock(); reschedule(); } return { wake, stop }; } function hasAtomicOperators(doc) { if (Array.isArray(doc)) { return doc.reduce((err, u) => err || hasAtomicOperators(u), null); } return ( Object.keys(typeof doc.toBSON !== 'function' ? doc : doc.toBSON()) .map(k => k[0]) .indexOf('$') >= 0 ); } /** * When the driver used emitWarning the code will be equal to this. * @public * * @example * ```js * process.on('warning', (warning) => { * if (warning.code === MONGODB_WARNING_CODE) console.error('Ah an important warning! :)') * }) * ``` */ const MONGODB_WARNING_CODE = 'MONGODB DRIVER'; /** * @internal * @param {string} message - message to warn about */ function emitWarning(message) { if (process.emitWarning) { return nodejsMajorVersion <= 6 ? process.emitWarning(message, undefined, MONGODB_WARNING_CODE) : process.emitWarning(message, { code: MONGODB_WARNING_CODE }); } else { // Approximate the style of print out on node versions pre 8.x // eslint-disable-next-line no-console return console.error(`[${MONGODB_WARNING_CODE}] Warning:`, message); } } const emittedWarnings = new Set(); /** * Will emit a warning once for the duration of the application. * Uses the message to identify if it has already been emitted * so using string interpolation can cause multiple emits * @internal * @param {string} message - message to warn about */ function emitWarningOnce(message) { if (!emittedWarnings.has(message)) { emittedWarnings.add(message); return emitWarning(message); } } function isSuperset(set, subset) { set = Array.isArray(set) ? new Set(set) : set; subset = Array.isArray(subset) ? new Set(subset) : subset; for (const elem of subset) { if (!set.has(elem)) { return false; } } return true; } function isRecord(value, requiredKeys) { const toString = Object.prototype.toString; const hasOwnProperty = Object.prototype.hasOwnProperty; const isObject = v => toString.call(v) === '[object Object]'; if (!isObject(value)) { return false; } const ctor = value.constructor; if (ctor && ctor.prototype) { if (!isObject(ctor.prototype)) { return false; } // Check to see if some method exists from the Object exists if (!hasOwnProperty.call(ctor.prototype, 'isPrototypeOf')) { return false; } } if (requiredKeys) { const keys = Object.keys(value); return isSuperset(keys, requiredKeys); } return true; } /** * Make a deep copy of an object * * NOTE: This is not meant to be the perfect implementation of a deep copy, * but instead something that is good enough for the purposes of * command monitoring. */ function deepCopy(value) { if (value == null) { return value; } else if (Array.isArray(value)) { return value.map(item => deepCopy(item)); } else if (isRecord(value)) { const res = {}; for (const key in value) { res[key] = deepCopy(value[key]); } return res; } const ctor = value.constructor; if (ctor) { switch (ctor.name.toLowerCase()) { case 'date': return new ctor(Number(value)); case 'map': return new Map(value); case 'set': return new Set(value); case 'buffer': return Buffer.from(value); } } return value; } /** * @param {{version: string}} pkg * @returns {{ major: number; minor: number; patch: number }} */ function parsePackageVersion(pkg) { const versionParts = pkg.version.split('.').map(n => Number.parseInt(n, 10)); return { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] }; } module.exports = { filterOptions, mergeOptions, translateOptions, shallowClone, getSingleProperty, checkCollectionName, toError, formattedOrderClause, parseIndexOptions, normalizeHintField, handleCallback, decorateCommand, isObject, debugOptions, MAX_JS_INT: Number.MAX_SAFE_INTEGER + 1, conditionallyMergeWriteConcern, executeLegacyOperation, applyRetryableWrites, applyWriteConcern, isPromiseLike, decorateWithCollation, decorateWithReadConcern, decorateWithExplain, deprecateOptions, SUPPORTS, MongoDBNamespace, emitDeprecationWarning, makeCounter, maybePromise, now, calculateDurationInMs, makeInterruptableAsyncInterval, hasAtomicOperators, MONGODB_WARNING_CODE, emitWarning, emitWarningOnce, deepCopy, parsePackageVersion };