UNPKG

21.8 kBJavaScriptView Raw
1'use strict';
2const MongoError = require('./core/error').MongoError;
3const ReadPreference = require('./core/topologies/read_preference');
4const WriteConcern = require('./write_concern');
5
6var shallowClone = function(obj) {
7 var copy = {};
8 for (var name in obj) copy[name] = obj[name];
9 return copy;
10};
11
12// Figure out the read preference
13var translateReadPreference = function(options) {
14 var r = null;
15 if (options.readPreference) {
16 r = options.readPreference;
17 } else {
18 return options;
19 }
20
21 if (typeof r === 'string') {
22 options.readPreference = new ReadPreference(r);
23 } else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
24 const mode = r.mode || r.preference;
25 if (mode && typeof mode === 'string') {
26 options.readPreference = new ReadPreference(mode, r.tags, {
27 maxStalenessSeconds: r.maxStalenessSeconds
28 });
29 }
30 } else if (!(r instanceof ReadPreference)) {
31 throw new TypeError('Invalid read preference: ' + r);
32 }
33
34 return options;
35};
36
37// Set simple property
38var getSingleProperty = function(obj, name, value) {
39 Object.defineProperty(obj, name, {
40 enumerable: true,
41 get: function() {
42 return value;
43 }
44 });
45};
46
47var formatSortValue = (exports.formatSortValue = function(sortDirection) {
48 var value = ('' + sortDirection).toLowerCase();
49
50 switch (value) {
51 case 'ascending':
52 case 'asc':
53 case '1':
54 return 1;
55 case 'descending':
56 case 'desc':
57 case '-1':
58 return -1;
59 default:
60 throw new Error(
61 'Illegal sort clause, must be of the form ' +
62 "[['field1', '(ascending|descending)'], " +
63 "['field2', '(ascending|descending)']]"
64 );
65 }
66});
67
68var formattedOrderClause = (exports.formattedOrderClause = function(sortValue) {
69 var orderBy = {};
70 if (sortValue == null) return null;
71 if (Array.isArray(sortValue)) {
72 if (sortValue.length === 0) {
73 return null;
74 }
75
76 for (var i = 0; i < sortValue.length; i++) {
77 if (sortValue[i].constructor === String) {
78 orderBy[sortValue[i]] = 1;
79 } else {
80 orderBy[sortValue[i][0]] = formatSortValue(sortValue[i][1]);
81 }
82 }
83 } else if (sortValue != null && typeof sortValue === 'object') {
84 orderBy = sortValue;
85 } else if (typeof sortValue === 'string') {
86 orderBy[sortValue] = 1;
87 } else {
88 throw new Error(
89 'Illegal sort clause, must be of the form ' +
90 "[['field1', '(ascending|descending)'], ['field2', '(ascending|descending)']]"
91 );
92 }
93
94 return orderBy;
95});
96
97var checkCollectionName = function checkCollectionName(collectionName) {
98 if ('string' !== typeof collectionName) {
99 throw new MongoError('collection name must be a String');
100 }
101
102 if (!collectionName || collectionName.indexOf('..') !== -1) {
103 throw new MongoError('collection names cannot be empty');
104 }
105
106 if (
107 collectionName.indexOf('$') !== -1 &&
108 collectionName.match(/((^\$cmd)|(oplog\.\$main))/) == null
109 ) {
110 throw new MongoError("collection names must not contain '$'");
111 }
112
113 if (collectionName.match(/^\.|\.$/) != null) {
114 throw new MongoError("collection names must not start or end with '.'");
115 }
116
117 // Validate that we are not passing 0x00 in the collection name
118 if (collectionName.indexOf('\x00') !== -1) {
119 throw new MongoError('collection names cannot contain a null character');
120 }
121};
122
123var handleCallback = function(callback, err, value1, value2) {
124 try {
125 if (callback == null) return;
126
127 if (callback) {
128 return value2 ? callback(err, value1, value2) : callback(err, value1);
129 }
130 } catch (err) {
131 process.nextTick(function() {
132 throw err;
133 });
134 return false;
135 }
136
137 return true;
138};
139
140/**
141 * Wrap a Mongo error document in an Error instance
142 * @ignore
143 * @api private
144 */
145var toError = function(error) {
146 if (error instanceof Error) return error;
147
148 var msg = error.err || error.errmsg || error.errMessage || error;
149 var e = MongoError.create({ message: msg, driver: true });
150
151 // Get all object keys
152 var keys = typeof error === 'object' ? Object.keys(error) : [];
153
154 for (var i = 0; i < keys.length; i++) {
155 try {
156 e[keys[i]] = error[keys[i]];
157 } catch (err) {
158 // continue
159 }
160 }
161
162 return e;
163};
164
165/**
166 * @ignore
167 */
168var normalizeHintField = function normalizeHintField(hint) {
169 var finalHint = null;
170
171 if (typeof hint === 'string') {
172 finalHint = hint;
173 } else if (Array.isArray(hint)) {
174 finalHint = {};
175
176 hint.forEach(function(param) {
177 finalHint[param] = 1;
178 });
179 } else if (hint != null && typeof hint === 'object') {
180 finalHint = {};
181 for (var name in hint) {
182 finalHint[name] = hint[name];
183 }
184 }
185
186 return finalHint;
187};
188
189/**
190 * Create index name based on field spec
191 *
192 * @ignore
193 * @api private
194 */
195var parseIndexOptions = function(fieldOrSpec) {
196 var fieldHash = {};
197 var indexes = [];
198 var keys;
199
200 // Get all the fields accordingly
201 if ('string' === typeof fieldOrSpec) {
202 // 'type'
203 indexes.push(fieldOrSpec + '_' + 1);
204 fieldHash[fieldOrSpec] = 1;
205 } else if (Array.isArray(fieldOrSpec)) {
206 fieldOrSpec.forEach(function(f) {
207 if ('string' === typeof f) {
208 // [{location:'2d'}, 'type']
209 indexes.push(f + '_' + 1);
210 fieldHash[f] = 1;
211 } else if (Array.isArray(f)) {
212 // [['location', '2d'],['type', 1]]
213 indexes.push(f[0] + '_' + (f[1] || 1));
214 fieldHash[f[0]] = f[1] || 1;
215 } else if (isObject(f)) {
216 // [{location:'2d'}, {type:1}]
217 keys = Object.keys(f);
218 keys.forEach(function(k) {
219 indexes.push(k + '_' + f[k]);
220 fieldHash[k] = f[k];
221 });
222 } else {
223 // undefined (ignore)
224 }
225 });
226 } else if (isObject(fieldOrSpec)) {
227 // {location:'2d', type:1}
228 keys = Object.keys(fieldOrSpec);
229 keys.forEach(function(key) {
230 indexes.push(key + '_' + fieldOrSpec[key]);
231 fieldHash[key] = fieldOrSpec[key];
232 });
233 }
234
235 return {
236 name: indexes.join('_'),
237 keys: keys,
238 fieldHash: fieldHash
239 };
240};
241
242var isObject = (exports.isObject = function(arg) {
243 return '[object Object]' === Object.prototype.toString.call(arg);
244});
245
246var debugOptions = function(debugFields, options) {
247 var finaloptions = {};
248 debugFields.forEach(function(n) {
249 finaloptions[n] = options[n];
250 });
251
252 return finaloptions;
253};
254
255var decorateCommand = function(command, options, exclude) {
256 for (var name in options) {
257 if (exclude.indexOf(name) === -1) command[name] = options[name];
258 }
259
260 return command;
261};
262
263var mergeOptions = function(target, source) {
264 for (var name in source) {
265 target[name] = source[name];
266 }
267
268 return target;
269};
270
271// Merge options with translation
272var translateOptions = function(target, source) {
273 var translations = {
274 // SSL translation options
275 sslCA: 'ca',
276 sslCRL: 'crl',
277 sslValidate: 'rejectUnauthorized',
278 sslKey: 'key',
279 sslCert: 'cert',
280 sslPass: 'passphrase',
281 // SocketTimeout translation options
282 socketTimeoutMS: 'socketTimeout',
283 connectTimeoutMS: 'connectionTimeout',
284 // Replicaset options
285 replicaSet: 'setName',
286 rs_name: 'setName',
287 secondaryAcceptableLatencyMS: 'acceptableLatency',
288 connectWithNoPrimary: 'secondaryOnlyConnectionAllowed',
289 // Mongos options
290 acceptableLatencyMS: 'localThresholdMS'
291 };
292
293 for (var name in source) {
294 if (translations[name]) {
295 target[translations[name]] = source[name];
296 } else {
297 target[name] = source[name];
298 }
299 }
300
301 return target;
302};
303
304var filterOptions = function(options, names) {
305 var filterOptions = {};
306
307 for (var name in options) {
308 if (names.indexOf(name) !== -1) filterOptions[name] = options[name];
309 }
310
311 // Filtered options
312 return filterOptions;
313};
314
315// Write concern keys
316var writeConcernKeys = ['w', 'j', 'wtimeout', 'fsync'];
317
318// Merge the write concern options
319var mergeOptionsAndWriteConcern = function(targetOptions, sourceOptions, keys, mergeWriteConcern) {
320 // Mix in any allowed options
321 for (var i = 0; i < keys.length; i++) {
322 if (!targetOptions[keys[i]] && sourceOptions[keys[i]] !== undefined) {
323 targetOptions[keys[i]] = sourceOptions[keys[i]];
324 }
325 }
326
327 // No merging of write concern
328 if (!mergeWriteConcern) return targetOptions;
329
330 // Found no write Concern options
331 var found = false;
332 for (i = 0; i < writeConcernKeys.length; i++) {
333 if (targetOptions[writeConcernKeys[i]]) {
334 found = true;
335 break;
336 }
337 }
338
339 if (!found) {
340 for (i = 0; i < writeConcernKeys.length; i++) {
341 if (sourceOptions[writeConcernKeys[i]]) {
342 targetOptions[writeConcernKeys[i]] = sourceOptions[writeConcernKeys[i]];
343 }
344 }
345 }
346
347 return targetOptions;
348};
349
350/**
351 * Executes the given operation with provided arguments.
352 *
353 * This method reduces large amounts of duplication in the entire codebase by providing
354 * a single point for determining whether callbacks or promises should be used. Additionally
355 * it allows for a single point of entry to provide features such as implicit sessions, which
356 * are required by the Driver Sessions specification in the event that a ClientSession is
357 * not provided
358 *
359 * @param {object} topology The topology to execute this operation on
360 * @param {function} operation The operation to execute
361 * @param {array} args Arguments to apply the provided operation
362 * @param {object} [options] Options that modify the behavior of the method
363 */
364const executeLegacyOperation = (topology, operation, args, options) => {
365 if (topology == null) {
366 throw new TypeError('This method requires a valid topology instance');
367 }
368
369 if (!Array.isArray(args)) {
370 throw new TypeError('This method requires an array of arguments to apply');
371 }
372
373 options = options || {};
374 const Promise = topology.s.promiseLibrary;
375 let callback = args[args.length - 1];
376
377 // The driver sessions spec mandates that we implicitly create sessions for operations
378 // that are not explicitly provided with a session.
379 let session, opOptions, owner;
380 if (!options.skipSessions && topology.hasSessionSupport()) {
381 opOptions = args[args.length - 2];
382 if (opOptions == null || opOptions.session == null) {
383 owner = Symbol();
384 session = topology.startSession({ owner });
385 const optionsIndex = args.length - 2;
386 args[optionsIndex] = Object.assign({}, args[optionsIndex], { session: session });
387 } else if (opOptions.session && opOptions.session.hasEnded) {
388 throw new MongoError('Use of expired sessions is not permitted');
389 }
390 }
391
392 const makeExecuteCallback = (resolve, reject) =>
393 function executeCallback(err, result) {
394 if (session && session.owner === owner && !options.returnsCursor) {
395 session.endSession(() => {
396 delete opOptions.session;
397 if (err) return reject(err);
398 resolve(result);
399 });
400 } else {
401 if (err) return reject(err);
402 resolve(result);
403 }
404 };
405
406 // Execute using callback
407 if (typeof callback === 'function') {
408 callback = args.pop();
409 const handler = makeExecuteCallback(
410 result => callback(null, result),
411 err => callback(err, null)
412 );
413 args.push(handler);
414
415 try {
416 return operation.apply(null, args);
417 } catch (e) {
418 handler(e);
419 throw e;
420 }
421 }
422
423 // Return a Promise
424 if (args[args.length - 1] != null) {
425 throw new TypeError('final argument to `executeLegacyOperation` must be a callback');
426 }
427
428 return new Promise(function(resolve, reject) {
429 const handler = makeExecuteCallback(resolve, reject);
430 args[args.length - 1] = handler;
431
432 try {
433 return operation.apply(null, args);
434 } catch (e) {
435 handler(e);
436 }
437 });
438};
439
440/**
441 * Applies retryWrites: true to a command if retryWrites is set on the command's database.
442 *
443 * @param {object} target The target command to which we will apply retryWrites.
444 * @param {object} db The database from which we can inherit a retryWrites value.
445 */
446function applyRetryableWrites(target, db) {
447 if (db && db.s.options.retryWrites) {
448 target.retryWrites = true;
449 }
450
451 return target;
452}
453
454/**
455 * Applies a write concern to a command based on well defined inheritance rules, optionally
456 * detecting support for the write concern in the first place.
457 *
458 * @param {Object} target the target command we will be applying the write concern to
459 * @param {Object} sources sources where we can inherit default write concerns from
460 * @param {Object} [options] optional settings passed into a command for write concern overrides
461 * @returns {Object} the (now) decorated target
462 */
463function applyWriteConcern(target, sources, options) {
464 options = options || {};
465 const db = sources.db;
466 const coll = sources.collection;
467
468 if (options.session && options.session.inTransaction()) {
469 // writeConcern is not allowed within a multi-statement transaction
470 if (target.writeConcern) {
471 delete target.writeConcern;
472 }
473
474 return target;
475 }
476
477 const writeConcern = WriteConcern.fromOptions(options);
478 if (writeConcern) {
479 return Object.assign(target, { writeConcern });
480 }
481
482 if (coll && coll.writeConcern) {
483 return Object.assign(target, { writeConcern: Object.assign({}, coll.writeConcern) });
484 }
485
486 if (db && db.writeConcern) {
487 return Object.assign(target, { writeConcern: Object.assign({}, db.writeConcern) });
488 }
489
490 return target;
491}
492
493/**
494 * Resolves a read preference based on well-defined inheritance rules. This method will not only
495 * determine the read preference (if there is one), but will also ensure the returned value is a
496 * properly constructed instance of `ReadPreference`.
497 *
498 * @param {Collection|Db|MongoClient} parent The parent of the operation on which to determine the read
499 * preference, used for determining the inherited read preference.
500 * @param {Object} options The options passed into the method, potentially containing a read preference
501 * @returns {(ReadPreference|null)} The resolved read preference
502 */
503function resolveReadPreference(parent, options) {
504 options = options || {};
505 const session = options.session;
506
507 const inheritedReadPreference = parent.readPreference;
508
509 let readPreference;
510 if (options.readPreference) {
511 readPreference = ReadPreference.fromOptions(options);
512 } else if (session && session.inTransaction() && session.transaction.options.readPreference) {
513 // The transaction’s read preference MUST override all other user configurable read preferences.
514 readPreference = session.transaction.options.readPreference;
515 } else if (inheritedReadPreference != null) {
516 readPreference = inheritedReadPreference;
517 } else {
518 throw new Error('No readPreference was provided or inherited.');
519 }
520
521 return typeof readPreference === 'string' ? new ReadPreference(readPreference) : readPreference;
522}
523
524/**
525 * Checks if a given value is a Promise
526 *
527 * @param {*} maybePromise
528 * @return true if the provided value is a Promise
529 */
530function isPromiseLike(maybePromise) {
531 return maybePromise && typeof maybePromise.then === 'function';
532}
533
534/**
535 * Applies collation to a given command.
536 *
537 * @param {object} [command] the command on which to apply collation
538 * @param {(Cursor|Collection)} [target] target of command
539 * @param {object} [options] options containing collation settings
540 */
541function decorateWithCollation(command, target, options) {
542 const topology = (target.s && target.s.topology) || target.topology;
543
544 if (!topology) {
545 throw new TypeError('parameter "target" is missing a topology');
546 }
547
548 const capabilities = topology.capabilities();
549 if (options.collation && typeof options.collation === 'object') {
550 if (capabilities && capabilities.commandsTakeCollation) {
551 command.collation = options.collation;
552 } else {
553 throw new MongoError(`Current topology does not support collation`);
554 }
555 }
556}
557
558/**
559 * Applies a read concern to a given command.
560 *
561 * @param {object} command the command on which to apply the read concern
562 * @param {Collection} coll the parent collection of the operation calling this method
563 */
564function decorateWithReadConcern(command, coll, options) {
565 if (options && options.session && options.session.inTransaction()) {
566 return;
567 }
568 let readConcern = Object.assign({}, command.readConcern || {});
569 if (coll.s.readConcern) {
570 Object.assign(readConcern, coll.s.readConcern);
571 }
572
573 if (Object.keys(readConcern).length > 0) {
574 Object.assign(command, { readConcern: readConcern });
575 }
576}
577
578const emitProcessWarning = msg => process.emitWarning(msg, 'DeprecationWarning');
579const emitConsoleWarning = msg => console.error(msg);
580const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning;
581
582/**
583 * Default message handler for generating deprecation warnings.
584 *
585 * @param {string} name function name
586 * @param {string} option option name
587 * @return {string} warning message
588 * @ignore
589 * @api private
590 */
591function defaultMsgHandler(name, option) {
592 return `${name} option [${option}] is deprecated and will be removed in a later version.`;
593}
594
595/**
596 * Deprecates a given function's options.
597 *
598 * @param {object} config configuration for deprecation
599 * @param {string} config.name function name
600 * @param {Array} config.deprecatedOptions options to deprecate
601 * @param {number} config.optionsIndex index of options object in function arguments array
602 * @param {function} [config.msgHandler] optional custom message handler to generate warnings
603 * @param {function} fn the target function of deprecation
604 * @return {function} modified function that warns once per deprecated option, and executes original function
605 * @ignore
606 * @api private
607 */
608function deprecateOptions(config, fn) {
609 if (process.noDeprecation === true) {
610 return fn;
611 }
612
613 const msgHandler = config.msgHandler ? config.msgHandler : defaultMsgHandler;
614
615 const optionsWarned = new Set();
616 function deprecated() {
617 const options = arguments[config.optionsIndex];
618
619 // ensure options is a valid, non-empty object, otherwise short-circuit
620 if (!isObject(options) || Object.keys(options).length === 0) {
621 return fn.apply(this, arguments);
622 }
623
624 config.deprecatedOptions.forEach(deprecatedOption => {
625 if (options.hasOwnProperty(deprecatedOption) && !optionsWarned.has(deprecatedOption)) {
626 optionsWarned.add(deprecatedOption);
627 const msg = msgHandler(config.name, deprecatedOption);
628 emitDeprecationWarning(msg);
629 if (this && this.getLogger) {
630 const logger = this.getLogger();
631 if (logger) {
632 logger.warn(msg);
633 }
634 }
635 }
636 });
637
638 return fn.apply(this, arguments);
639 }
640
641 // These lines copied from https://github.com/nodejs/node/blob/25e5ae41688676a5fd29b2e2e7602168eee4ceb5/lib/internal/util.js#L73-L80
642 // The wrapper will keep the same prototype as fn to maintain prototype chain
643 Object.setPrototypeOf(deprecated, fn);
644 if (fn.prototype) {
645 // Setting this (rather than using Object.setPrototype, as above) ensures
646 // that calling the unwrapped constructor gives an instanceof the wrapped
647 // constructor.
648 deprecated.prototype = fn.prototype;
649 }
650
651 return deprecated;
652}
653
654const SUPPORTS = {};
655// Test asyncIterator support
656try {
657 require('./async/async_iterator');
658 SUPPORTS.ASYNC_ITERATOR = true;
659} catch (e) {
660 SUPPORTS.ASYNC_ITERATOR = false;
661}
662
663class MongoDBNamespace {
664 constructor(db, collection) {
665 this.db = db;
666 this.collection = collection;
667 }
668
669 toString() {
670 return this.collection ? `${this.db}.${this.collection}` : this.db;
671 }
672
673 withCollection(collection) {
674 return new MongoDBNamespace(this.db, collection);
675 }
676
677 static fromString(namespace) {
678 if (!namespace) {
679 throw new Error(`Cannot parse namespace from "${namespace}"`);
680 }
681
682 const index = namespace.indexOf('.');
683 return new MongoDBNamespace(namespace.substring(0, index), namespace.substring(index + 1));
684 }
685}
686
687function* makeCounter(seed) {
688 let count = seed || 0;
689 while (true) {
690 const newCount = count;
691 count += 1;
692 yield newCount;
693 }
694}
695
696/**
697 * Helper function for either accepting a callback, or returning a promise
698 *
699 * @param {Object} parent an instance of parent with promiseLibrary.
700 * @param {object} parent.s an object containing promiseLibrary.
701 * @param {function} parent.s.promiseLibrary an object containing promiseLibrary.
702 * @param {[Function]} callback an optional callback.
703 * @param {Function} fn A function that takes a callback
704 * @returns {Promise|void} Returns nothing if a callback is supplied, else returns a Promise.
705 */
706function maybePromise(parent, callback, fn) {
707 const PromiseLibrary = (parent && parent.s && parent.s.promiseLibrary) || Promise;
708
709 let result;
710 if (typeof callback !== 'function') {
711 result = new PromiseLibrary((resolve, reject) => {
712 callback = (err, res) => {
713 if (err) return reject(err);
714 resolve(res);
715 };
716 });
717 }
718
719 fn(function(err, res) {
720 if (err != null) {
721 try {
722 callback(err);
723 } catch (error) {
724 return process.nextTick(() => {
725 throw error;
726 });
727 }
728 return;
729 }
730
731 callback(err, res);
732 });
733
734 return result;
735}
736
737module.exports = {
738 filterOptions,
739 mergeOptions,
740 translateOptions,
741 shallowClone,
742 getSingleProperty,
743 checkCollectionName,
744 toError,
745 formattedOrderClause,
746 parseIndexOptions,
747 normalizeHintField,
748 handleCallback,
749 decorateCommand,
750 isObject,
751 debugOptions,
752 MAX_JS_INT: Number.MAX_SAFE_INTEGER + 1,
753 mergeOptionsAndWriteConcern,
754 translateReadPreference,
755 executeLegacyOperation,
756 applyRetryableWrites,
757 applyWriteConcern,
758 isPromiseLike,
759 decorateWithCollation,
760 decorateWithReadConcern,
761 deprecateOptions,
762 SUPPORTS,
763 MongoDBNamespace,
764 resolveReadPreference,
765 emitDeprecationWarning,
766 makeCounter,
767 maybePromise
768};