collection.js 12 KB

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