collection.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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. return _this.collection;
  47. }
  48. // capped
  49. return _this.conn.db.collection(_this.name, function(err, c) {
  50. if (err) return callback(err);
  51. // discover if this collection exists and if it is capped
  52. _this.conn.db.listCollections({ name: _this.name }).toArray(function(err, docs) {
  53. if (err) {
  54. return callback(err);
  55. }
  56. const doc = docs[0];
  57. const exists = !!doc;
  58. if (exists) {
  59. if (doc.options && doc.options.capped) {
  60. callback(null, c);
  61. } else {
  62. const msg = 'A non-capped collection exists with the name: ' + _this.name + '\n\n'
  63. + ' To use this collection as a capped collection, please '
  64. + 'first convert it.\n'
  65. + ' http://www.mongodb.org/display/DOCS/Capped+Collections#CappedCollections-Convertingacollectiontocapped';
  66. err = new Error(msg);
  67. callback(err);
  68. }
  69. } else {
  70. // create
  71. const opts = Object.assign({}, _this.opts.capped);
  72. opts.capped = true;
  73. _this.conn.db.createCollection(_this.name, opts, callback);
  74. }
  75. });
  76. });
  77. function callback(err, collection) {
  78. if (err) {
  79. // likely a strict mode error
  80. _this.conn.emit('error', err);
  81. } else {
  82. _this.collection = collection;
  83. MongooseCollection.prototype.onOpen.call(_this);
  84. }
  85. }
  86. };
  87. /**
  88. * Called when the connection closes
  89. *
  90. * @api private
  91. */
  92. NativeCollection.prototype.onClose = function(force) {
  93. MongooseCollection.prototype.onClose.call(this, force);
  94. };
  95. /*!
  96. * ignore
  97. */
  98. const syncCollectionMethods = { watch: true };
  99. /*!
  100. * Copy the collection methods and make them subject to queues
  101. */
  102. function iter(i) {
  103. NativeCollection.prototype[i] = function() {
  104. const collection = this.collection;
  105. const args = Array.from(arguments);
  106. const _this = this;
  107. const debug = get(_this, 'conn.base.options.debug');
  108. const lastArg = arguments[arguments.length - 1];
  109. // If user force closed, queueing will hang forever. See #5664
  110. if (this.conn.$wasForceClosed) {
  111. const error = new MongooseError('Connection was force closed');
  112. if (args.length > 0 &&
  113. typeof args[args.length - 1] === 'function') {
  114. args[args.length - 1](error);
  115. return;
  116. } else {
  117. throw error;
  118. }
  119. }
  120. if (this.buffer) {
  121. if (syncCollectionMethods[i]) {
  122. throw new Error('Collection method ' + i + ' is synchronous');
  123. }
  124. if (typeof lastArg === 'function') {
  125. this.addQueue(i, args);
  126. return;
  127. }
  128. return new this.Promise((resolve, reject) => {
  129. this.addQueue(i, [].concat(args).concat([(err, res) => {
  130. if (err != null) {
  131. return reject(err);
  132. }
  133. resolve(res);
  134. }]));
  135. });
  136. }
  137. if (debug) {
  138. if (typeof debug === 'function') {
  139. debug.apply(_this,
  140. [_this.name, i].concat(sliced(args, 0, args.length - 1)));
  141. } else if (debug instanceof stream.Writable) {
  142. this.$printToStream(_this.name, i, args, debug);
  143. } else {
  144. this.$print(_this.name, i, args, typeof debug.color === 'undefined' ? true : debug.color);
  145. }
  146. }
  147. try {
  148. return collection[i].apply(collection, args);
  149. } catch (error) {
  150. // Collection operation may throw because of max bson size, catch it here
  151. // See gh-3906
  152. if (args.length > 0 &&
  153. typeof args[args.length - 1] === 'function') {
  154. args[args.length - 1](error);
  155. } else {
  156. throw error;
  157. }
  158. }
  159. };
  160. }
  161. for (const key of Object.keys(Collection.prototype)) {
  162. // Janky hack to work around gh-3005 until we can get rid of the mongoose
  163. // collection abstraction
  164. const descriptor = Object.getOwnPropertyDescriptor(Collection.prototype, key);
  165. // Skip properties with getters because they may throw errors (gh-8528)
  166. if (descriptor.get !== undefined) {
  167. continue;
  168. }
  169. if (typeof Collection.prototype[key] !== 'function') {
  170. continue;
  171. }
  172. iter(key);
  173. }
  174. /**
  175. * Debug print helper
  176. *
  177. * @api public
  178. * @method $print
  179. */
  180. NativeCollection.prototype.$print = function(name, i, args, color) {
  181. const moduleName = color ? '\x1B[0;36mMongoose:\x1B[0m ' : 'Mongoose: ';
  182. const functionCall = [name, i].join('.');
  183. const _args = [];
  184. for (let j = args.length - 1; j >= 0; --j) {
  185. if (this.$format(args[j]) || _args.length) {
  186. _args.unshift(this.$format(args[j], color));
  187. }
  188. }
  189. const params = '(' + _args.join(', ') + ')';
  190. console.info(moduleName + functionCall + params);
  191. };
  192. /**
  193. * Debug print helper
  194. *
  195. * @api public
  196. * @method $print
  197. */
  198. NativeCollection.prototype.$printToStream = function(name, i, args, stream) {
  199. const functionCall = [name, i].join('.');
  200. const _args = [];
  201. for (let j = args.length - 1; j >= 0; --j) {
  202. if (this.$format(args[j]) || _args.length) {
  203. _args.unshift(this.$format(args[j]));
  204. }
  205. }
  206. const params = '(' + _args.join(', ') + ')';
  207. stream.write(functionCall + params, 'utf8');
  208. };
  209. /**
  210. * Formatter for debug print args
  211. *
  212. * @api public
  213. * @method $format
  214. */
  215. NativeCollection.prototype.$format = function(arg, color) {
  216. const type = typeof arg;
  217. if (type === 'function' || type === 'undefined') return '';
  218. return format(arg, false, color);
  219. };
  220. /*!
  221. * Debug print helper
  222. */
  223. function inspectable(representation) {
  224. const ret = {
  225. inspect: function() { return representation; }
  226. };
  227. if (util.inspect.custom) {
  228. ret[util.inspect.custom] = ret.inspect;
  229. }
  230. return ret;
  231. }
  232. function map(o) {
  233. return format(o, true);
  234. }
  235. function formatObjectId(x, key) {
  236. x[key] = inspectable('ObjectId("' + x[key].toHexString() + '")');
  237. }
  238. function formatDate(x, key) {
  239. x[key] = inspectable('new Date("' + x[key].toUTCString() + '")');
  240. }
  241. function format(obj, sub, color) {
  242. if (obj && typeof obj.toBSON === 'function') {
  243. obj = obj.toBSON();
  244. }
  245. if (obj == null) {
  246. return obj;
  247. }
  248. const clone = require('../../helpers/clone');
  249. let x = clone(obj, { transform: false });
  250. if (x.constructor.name === 'Binary') {
  251. x = 'BinData(' + x.sub_type + ', "' + x.toString('base64') + '")';
  252. } else if (x.constructor.name === 'ObjectID') {
  253. x = inspectable('ObjectId("' + x.toHexString() + '")');
  254. } else if (x.constructor.name === 'Date') {
  255. x = inspectable('new Date("' + x.toUTCString() + '")');
  256. } else if (x.constructor.name === 'Object') {
  257. const keys = Object.keys(x);
  258. const numKeys = keys.length;
  259. let key;
  260. for (let i = 0; i < numKeys; ++i) {
  261. key = keys[i];
  262. if (x[key]) {
  263. let error;
  264. if (typeof x[key].toBSON === 'function') {
  265. try {
  266. // `session.toBSON()` throws an error. This means we throw errors
  267. // in debug mode when using transactions, see gh-6712. As a
  268. // workaround, catch `toBSON()` errors, try to serialize without
  269. // `toBSON()`, and rethrow if serialization still fails.
  270. x[key] = x[key].toBSON();
  271. } catch (_error) {
  272. error = _error;
  273. }
  274. }
  275. if (x[key].constructor.name === 'Binary') {
  276. x[key] = 'BinData(' + x[key].sub_type + ', "' +
  277. x[key].buffer.toString('base64') + '")';
  278. } else if (x[key].constructor.name === 'Object') {
  279. x[key] = format(x[key], true);
  280. } else if (x[key].constructor.name === 'ObjectID') {
  281. formatObjectId(x, key);
  282. } else if (x[key].constructor.name === 'Date') {
  283. formatDate(x, key);
  284. } else if (x[key].constructor.name === 'ClientSession') {
  285. x[key] = inspectable('ClientSession("' +
  286. get(x[key], 'id.id.buffer', '').toString('hex') + '")');
  287. } else if (Array.isArray(x[key])) {
  288. x[key] = x[key].map(map);
  289. } else if (error != null) {
  290. // If there was an error with `toBSON()` and the object wasn't
  291. // already converted to a string representation, rethrow it.
  292. // Open to better ideas on how to handle this.
  293. throw error;
  294. }
  295. }
  296. }
  297. }
  298. if (sub) {
  299. return x;
  300. }
  301. return util.
  302. inspect(x, false, 10, color).
  303. replace(/\n/g, '').
  304. replace(/\s{2,}/g, ' ');
  305. }
  306. /**
  307. * Retrieves information about this collections indexes.
  308. *
  309. * @param {Function} callback
  310. * @method getIndexes
  311. * @api public
  312. */
  313. NativeCollection.prototype.getIndexes = NativeCollection.prototype.indexInformation;
  314. /*!
  315. * Module exports.
  316. */
  317. module.exports = NativeCollection;