collection.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const MongooseCollection = require('../../collection');
  6. const MongooseError = require('../../error/mongooseError');
  7. const Collection = require('mongodb').Collection;
  8. const get = require('../../helpers/get');
  9. const sliced = require('sliced');
  10. const stream = require('stream');
  11. const util = require('util');
  12. /**
  13. * A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) collection implementation.
  14. *
  15. * All methods methods from the [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) driver are copied and wrapped in queue management.
  16. *
  17. * @inherits Collection
  18. * @api private
  19. */
  20. function NativeCollection(name, options) {
  21. this.collection = null;
  22. this.Promise = options.Promise || Promise;
  23. this._closed = false;
  24. MongooseCollection.apply(this, arguments);
  25. }
  26. /*!
  27. * Inherit from abstract Collection.
  28. */
  29. NativeCollection.prototype.__proto__ = MongooseCollection.prototype;
  30. /**
  31. * Called when the connection opens.
  32. *
  33. * @api private
  34. */
  35. NativeCollection.prototype.onOpen = function() {
  36. const _this = this;
  37. // always get a new collection in case the user changed host:port
  38. // of parent db instance when re-opening the connection.
  39. if (!_this.opts.capped.size) {
  40. // non-capped
  41. callback(null, _this.conn.db.collection(_this.name));
  42. return _this.collection;
  43. }
  44. if (_this.opts.autoCreate === false) {
  45. _this.collection = _this.conn.db.collection(_this.name);
  46. MongooseCollection.prototype.onOpen.call(_this);
  47. return _this.collection;
  48. }
  49. // capped
  50. return _this.conn.db.collection(_this.name, function(err, c) {
  51. if (err) return callback(err);
  52. // discover if this collection exists and if it is capped
  53. _this.conn.db.listCollections({ name: _this.name }).toArray(function(err, docs) {
  54. if (err) {
  55. return callback(err);
  56. }
  57. const doc = docs[0];
  58. const exists = !!doc;
  59. if (exists) {
  60. if (doc.options && doc.options.capped) {
  61. callback(null, c);
  62. } else {
  63. const msg = 'A non-capped collection exists with the name: ' + _this.name + '\n\n'
  64. + ' To use this collection as a capped collection, please '
  65. + 'first convert it.\n'
  66. + ' http://www.mongodb.org/display/DOCS/Capped+Collections#CappedCollections-Convertingacollectiontocapped';
  67. err = new Error(msg);
  68. callback(err);
  69. }
  70. } else {
  71. // create
  72. const opts = Object.assign({}, _this.opts.capped);
  73. opts.capped = true;
  74. _this.conn.db.createCollection(_this.name, opts, callback);
  75. }
  76. });
  77. });
  78. function callback(err, collection) {
  79. if (err) {
  80. // likely a strict mode error
  81. _this.conn.emit('error', err);
  82. } else {
  83. _this.collection = collection;
  84. MongooseCollection.prototype.onOpen.call(_this);
  85. }
  86. }
  87. };
  88. /**
  89. * Called when the connection closes
  90. *
  91. * @api private
  92. */
  93. NativeCollection.prototype.onClose = function(force) {
  94. MongooseCollection.prototype.onClose.call(this, force);
  95. };
  96. /*!
  97. * ignore
  98. */
  99. const syncCollectionMethods = { watch: true };
  100. /*!
  101. * Copy the collection methods and make them subject to queues
  102. */
  103. function iter(i) {
  104. NativeCollection.prototype[i] = function() {
  105. const collection = this.collection;
  106. const args = Array.from(arguments);
  107. const _this = this;
  108. const debug = get(_this, 'conn.base.options.debug');
  109. const lastArg = arguments[arguments.length - 1];
  110. // If user force closed, queueing will hang forever. See #5664
  111. if (this.conn.$wasForceClosed) {
  112. const error = new MongooseError('Connection was force closed');
  113. if (args.length > 0 &&
  114. typeof args[args.length - 1] === 'function') {
  115. args[args.length - 1](error);
  116. return;
  117. } else {
  118. throw error;
  119. }
  120. }
  121. if (this._shouldBufferCommands() && this.buffer) {
  122. if (syncCollectionMethods[i]) {
  123. throw new Error('Collection method ' + i + ' is synchronous');
  124. }
  125. this.conn.emit('buffer', { method: i, args: args });
  126. let callback;
  127. let _args;
  128. let promise = null;
  129. let timeout = null;
  130. if (typeof lastArg === 'function') {
  131. callback = function collectionOperationCallback() {
  132. if (timeout != null) {
  133. clearTimeout(timeout);
  134. }
  135. return lastArg.apply(this, arguments);
  136. };
  137. _args = args.slice(0, args.length - 1).concat([callback]);
  138. } else {
  139. promise = new this.Promise((resolve, reject) => {
  140. callback = function collectionOperationCallback(err, res) {
  141. if (timeout != null) {
  142. clearTimeout(timeout);
  143. }
  144. if (err != null) {
  145. return reject(err);
  146. }
  147. resolve(res);
  148. };
  149. _args = args.concat([callback]);
  150. this.addQueue(i, _args);
  151. });
  152. }
  153. const bufferTimeoutMS = this._getBufferTimeoutMS();
  154. timeout = setTimeout(() => {
  155. const removed = this.removeQueue(i, _args);
  156. if (removed) {
  157. const message = 'Operation `' + this.name + '.' + i + '()` buffering timed out after ' +
  158. bufferTimeoutMS + 'ms';
  159. callback(new MongooseError(message));
  160. }
  161. }, bufferTimeoutMS);
  162. if (typeof lastArg === 'function') {
  163. this.addQueue(i, _args);
  164. return;
  165. }
  166. return promise;
  167. }
  168. if (debug) {
  169. if (typeof debug === 'function') {
  170. debug.apply(_this,
  171. [_this.name, i].concat(sliced(args, 0, args.length - 1)));
  172. } else if (debug instanceof stream.Writable) {
  173. this.$printToStream(_this.name, i, args, debug);
  174. } else {
  175. const color = debug.color == null ? true : debug.color;
  176. const shell = debug.shell == null ? false : debug.shell;
  177. this.$print(_this.name, i, args, color, shell);
  178. }
  179. }
  180. try {
  181. if (collection == null) {
  182. const message = 'Cannot call `' + this.name + '.' + i + '()` before initial connection ' +
  183. 'is complete if `bufferCommands = false`. Make sure you `await mongoose.connect()` if ' +
  184. 'you have `bufferCommands = false`.';
  185. throw new MongooseError(message);
  186. }
  187. return collection[i].apply(collection, args);
  188. } catch (error) {
  189. // Collection operation may throw because of max bson size, catch it here
  190. // See gh-3906
  191. if (args.length > 0 &&
  192. typeof args[args.length - 1] === 'function') {
  193. args[args.length - 1](error);
  194. } else {
  195. throw error;
  196. }
  197. }
  198. };
  199. }
  200. for (const key of Object.keys(Collection.prototype)) {
  201. // Janky hack to work around gh-3005 until we can get rid of the mongoose
  202. // collection abstraction
  203. const descriptor = Object.getOwnPropertyDescriptor(Collection.prototype, key);
  204. // Skip properties with getters because they may throw errors (gh-8528)
  205. if (descriptor.get !== undefined) {
  206. continue;
  207. }
  208. if (typeof Collection.prototype[key] !== 'function') {
  209. continue;
  210. }
  211. iter(key);
  212. }
  213. /**
  214. * Debug print helper
  215. *
  216. * @api public
  217. * @method $print
  218. */
  219. NativeCollection.prototype.$print = function(name, i, args, color, shell) {
  220. const moduleName = color ? '\x1B[0;36mMongoose:\x1B[0m ' : 'Mongoose: ';
  221. const functionCall = [name, i].join('.');
  222. const _args = [];
  223. for (let j = args.length - 1; j >= 0; --j) {
  224. if (this.$format(args[j]) || _args.length) {
  225. _args.unshift(this.$format(args[j], color, shell));
  226. }
  227. }
  228. const params = '(' + _args.join(', ') + ')';
  229. console.info(moduleName + functionCall + params);
  230. };
  231. /**
  232. * Debug print helper
  233. *
  234. * @api public
  235. * @method $print
  236. */
  237. NativeCollection.prototype.$printToStream = function(name, i, args, stream) {
  238. const functionCall = [name, i].join('.');
  239. const _args = [];
  240. for (let j = args.length - 1; j >= 0; --j) {
  241. if (this.$format(args[j]) || _args.length) {
  242. _args.unshift(this.$format(args[j]));
  243. }
  244. }
  245. const params = '(' + _args.join(', ') + ')';
  246. stream.write(functionCall + params, 'utf8');
  247. };
  248. /**
  249. * Formatter for debug print args
  250. *
  251. * @api public
  252. * @method $format
  253. */
  254. NativeCollection.prototype.$format = function(arg, color, shell) {
  255. const type = typeof arg;
  256. if (type === 'function' || type === 'undefined') return '';
  257. return format(arg, false, color, shell);
  258. };
  259. /*!
  260. * Debug print helper
  261. */
  262. function inspectable(representation) {
  263. const ret = {
  264. inspect: function() { return representation; }
  265. };
  266. if (util.inspect.custom) {
  267. ret[util.inspect.custom] = ret.inspect;
  268. }
  269. return ret;
  270. }
  271. function map(o) {
  272. return format(o, true);
  273. }
  274. function formatObjectId(x, key) {
  275. x[key] = inspectable('ObjectId("' + x[key].toHexString() + '")');
  276. }
  277. function formatDate(x, key, shell) {
  278. if (shell) {
  279. x[key] = inspectable('ISODate("' + x[key].toUTCString() + '")');
  280. } else {
  281. x[key] = inspectable('new Date("' + x[key].toUTCString() + '")');
  282. }
  283. }
  284. function format(obj, sub, color, shell) {
  285. if (obj && typeof obj.toBSON === 'function') {
  286. obj = obj.toBSON();
  287. }
  288. if (obj == null) {
  289. return obj;
  290. }
  291. const clone = require('../../helpers/clone');
  292. let x = clone(obj, { transform: false });
  293. if (x.constructor.name === 'Binary') {
  294. x = 'BinData(' + x.sub_type + ', "' + x.toString('base64') + '")';
  295. } else if (x.constructor.name === 'ObjectID') {
  296. x = inspectable('ObjectId("' + x.toHexString() + '")');
  297. } else if (x.constructor.name === 'Date') {
  298. x = inspectable('new Date("' + x.toUTCString() + '")');
  299. } else if (x.constructor.name === 'Object') {
  300. const keys = Object.keys(x);
  301. const numKeys = keys.length;
  302. let key;
  303. for (let i = 0; i < numKeys; ++i) {
  304. key = keys[i];
  305. if (x[key]) {
  306. let error;
  307. if (typeof x[key].toBSON === 'function') {
  308. try {
  309. // `session.toBSON()` throws an error. This means we throw errors
  310. // in debug mode when using transactions, see gh-6712. As a
  311. // workaround, catch `toBSON()` errors, try to serialize without
  312. // `toBSON()`, and rethrow if serialization still fails.
  313. x[key] = x[key].toBSON();
  314. } catch (_error) {
  315. error = _error;
  316. }
  317. }
  318. if (x[key].constructor.name === 'Binary') {
  319. x[key] = 'BinData(' + x[key].sub_type + ', "' +
  320. x[key].buffer.toString('base64') + '")';
  321. } else if (x[key].constructor.name === 'Object') {
  322. x[key] = format(x[key], true);
  323. } else if (x[key].constructor.name === 'ObjectID') {
  324. formatObjectId(x, key);
  325. } else if (x[key].constructor.name === 'Date') {
  326. formatDate(x, key, shell);
  327. } else if (x[key].constructor.name === 'ClientSession') {
  328. x[key] = inspectable('ClientSession("' +
  329. get(x[key], 'id.id.buffer', '').toString('hex') + '")');
  330. } else if (Array.isArray(x[key])) {
  331. x[key] = x[key].map(map);
  332. } else if (error != null) {
  333. // If there was an error with `toBSON()` and the object wasn't
  334. // already converted to a string representation, rethrow it.
  335. // Open to better ideas on how to handle this.
  336. throw error;
  337. }
  338. }
  339. }
  340. }
  341. if (sub) {
  342. return x;
  343. }
  344. return util.
  345. inspect(x, false, 10, color).
  346. replace(/\n/g, '').
  347. replace(/\s{2,}/g, ' ');
  348. }
  349. /**
  350. * Retrieves information about this collections indexes.
  351. *
  352. * @param {Function} callback
  353. * @method getIndexes
  354. * @api public
  355. */
  356. NativeCollection.prototype.getIndexes = NativeCollection.prototype.indexInformation;
  357. /*!
  358. * Module exports.
  359. */
  360. module.exports = NativeCollection;