subdocument.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. 'use strict';
  2. const Document = require('../document');
  3. const immediate = require('../helpers/immediate');
  4. const internalToObjectOptions = require('../options').internalToObjectOptions;
  5. const promiseOrCallback = require('../helpers/promiseOrCallback');
  6. const util = require('util');
  7. const utils = require('../utils');
  8. module.exports = Subdocument;
  9. /**
  10. * Subdocument constructor.
  11. *
  12. * @inherits Document
  13. * @api private
  14. */
  15. function Subdocument(value, fields, parent, skipId, options) {
  16. if (parent != null) {
  17. // If setting a nested path, should copy isNew from parent re: gh-7048
  18. const parentOptions = { isNew: parent.isNew };
  19. if ('defaults' in parent.$__) {
  20. parentOptions.defaults = parent.$__.defaults;
  21. }
  22. options = Object.assign(parentOptions, options);
  23. }
  24. if (options != null && options.path != null) {
  25. this.$basePath = options.path;
  26. }
  27. Document.call(this, value, fields, skipId, options);
  28. delete this.$__.priorDoc;
  29. }
  30. Subdocument.prototype = Object.create(Document.prototype);
  31. Object.defineProperty(Subdocument.prototype, '$isSubdocument', {
  32. configurable: false,
  33. writable: false,
  34. value: true
  35. });
  36. Object.defineProperty(Subdocument.prototype, '$isSingleNested', {
  37. configurable: false,
  38. writable: false,
  39. value: true
  40. });
  41. /*!
  42. * ignore
  43. */
  44. Subdocument.prototype.toBSON = function() {
  45. return this.toObject(internalToObjectOptions);
  46. };
  47. /**
  48. * Used as a stub for middleware
  49. *
  50. * #### Note:
  51. *
  52. * _This is a no-op. Does not actually save the doc to the db._
  53. *
  54. * @param {Function} [fn]
  55. * @return {Promise} resolved Promise
  56. * @api private
  57. */
  58. Subdocument.prototype.save = function(options, fn) {
  59. if (typeof options === 'function') {
  60. fn = options;
  61. options = {};
  62. }
  63. options = options || {};
  64. if (!options.suppressWarning) {
  65. utils.warn('mongoose: calling `save()` on a subdoc does **not** save ' +
  66. 'the document to MongoDB, it only runs save middleware. ' +
  67. 'Use `subdoc.save({ suppressWarning: true })` to hide this warning ' +
  68. 'if you\'re sure this behavior is right for your app.');
  69. }
  70. return promiseOrCallback(fn, cb => {
  71. this.$__save(cb);
  72. });
  73. };
  74. /*!
  75. * Given a path relative to this document, return the path relative
  76. * to the top-level document.
  77. */
  78. Subdocument.prototype.$__fullPath = function(path) {
  79. if (!this.$__.fullPath) {
  80. this.ownerDocument();
  81. }
  82. return path ?
  83. this.$__.fullPath + '.' + path :
  84. this.$__.fullPath;
  85. };
  86. /*!
  87. * Given a path relative to this document, return the path relative
  88. * to the top-level document.
  89. */
  90. Subdocument.prototype.$__pathRelativeToParent = function(p) {
  91. if (p == null) {
  92. return this.$basePath;
  93. }
  94. return [this.$basePath, p].join('.');
  95. };
  96. /**
  97. * Used as a stub for middleware
  98. *
  99. * #### Note:
  100. *
  101. * _This is a no-op. Does not actually save the doc to the db._
  102. *
  103. * @param {Function} [fn]
  104. * @method $__save
  105. * @api private
  106. */
  107. Subdocument.prototype.$__save = function(fn) {
  108. return immediate(() => fn(null, this));
  109. };
  110. /*!
  111. * ignore
  112. */
  113. Subdocument.prototype.$isValid = function(path) {
  114. const parent = this.$parent();
  115. const fullPath = this.$__pathRelativeToParent(path);
  116. if (parent != null && fullPath != null) {
  117. return parent.$isValid(fullPath);
  118. }
  119. return Document.prototype.$isValid.call(this, path);
  120. };
  121. /*!
  122. * ignore
  123. */
  124. Subdocument.prototype.markModified = function(path) {
  125. Document.prototype.markModified.call(this, path);
  126. const parent = this.$parent();
  127. const fullPath = this.$__pathRelativeToParent(path);
  128. if (parent == null || fullPath == null) {
  129. return;
  130. }
  131. const myPath = this.$__pathRelativeToParent().replace(/\.$/, '');
  132. if (parent.isDirectModified(myPath) || this.isNew) {
  133. return;
  134. }
  135. this.$__parent.markModified(fullPath, this);
  136. };
  137. /*!
  138. * ignore
  139. */
  140. Subdocument.prototype.isModified = function(paths, modifiedPaths) {
  141. const parent = this.$parent();
  142. if (parent != null) {
  143. if (Array.isArray(paths) || typeof paths === 'string') {
  144. paths = (Array.isArray(paths) ? paths : paths.split(' '));
  145. paths = paths.map(p => this.$__pathRelativeToParent(p)).filter(p => p != null);
  146. } else if (!paths) {
  147. paths = this.$__pathRelativeToParent();
  148. }
  149. return parent.$isModified(paths, modifiedPaths);
  150. }
  151. return Document.prototype.isModified.call(this, paths, modifiedPaths);
  152. };
  153. /**
  154. * Marks a path as valid, removing existing validation errors.
  155. *
  156. * @param {String} path the field to mark as valid
  157. * @api private
  158. * @method $markValid
  159. * @receiver Subdocument
  160. */
  161. Subdocument.prototype.$markValid = function(path) {
  162. Document.prototype.$markValid.call(this, path);
  163. const parent = this.$parent();
  164. const fullPath = this.$__pathRelativeToParent(path);
  165. if (parent != null && fullPath != null) {
  166. parent.$markValid(fullPath);
  167. }
  168. };
  169. /*!
  170. * ignore
  171. */
  172. Subdocument.prototype.invalidate = function(path, err, val) {
  173. Document.prototype.invalidate.call(this, path, err, val);
  174. const parent = this.$parent();
  175. const fullPath = this.$__pathRelativeToParent(path);
  176. if (parent != null && fullPath != null) {
  177. parent.invalidate(fullPath, err, val);
  178. } else if (err.kind === 'cast' || err.name === 'CastError' || fullPath == null) {
  179. throw err;
  180. }
  181. return this.ownerDocument().$__.validationError;
  182. };
  183. /*!
  184. * ignore
  185. */
  186. Subdocument.prototype.$ignore = function(path) {
  187. Document.prototype.$ignore.call(this, path);
  188. const parent = this.$parent();
  189. const fullPath = this.$__pathRelativeToParent(path);
  190. if (parent != null && fullPath != null) {
  191. parent.$ignore(fullPath);
  192. }
  193. };
  194. /**
  195. * Returns the top level document of this sub-document.
  196. *
  197. * @return {Document}
  198. */
  199. Subdocument.prototype.ownerDocument = function() {
  200. if (this.$__.ownerDocument) {
  201. return this.$__.ownerDocument;
  202. }
  203. let parent = this; // eslint-disable-line consistent-this
  204. const paths = [];
  205. const seenDocs = new Set([parent]);
  206. while (true) {
  207. if (typeof parent.$__pathRelativeToParent !== 'function') {
  208. break;
  209. }
  210. paths.unshift(parent.$__pathRelativeToParent(void 0, true));
  211. const _parent = parent.$parent();
  212. if (_parent == null) {
  213. break;
  214. }
  215. parent = _parent;
  216. if (seenDocs.has(parent)) {
  217. throw new Error('Infinite subdocument loop: subdoc with _id ' + parent._id + ' is a parent of itself');
  218. }
  219. seenDocs.add(parent);
  220. }
  221. this.$__.fullPath = paths.join('.');
  222. this.$__.ownerDocument = parent;
  223. return this.$__.ownerDocument;
  224. };
  225. /*!
  226. * ignore
  227. */
  228. Subdocument.prototype.$__fullPathWithIndexes = function() {
  229. let parent = this; // eslint-disable-line consistent-this
  230. const paths = [];
  231. const seenDocs = new Set([parent]);
  232. while (true) {
  233. if (typeof parent.$__pathRelativeToParent !== 'function') {
  234. break;
  235. }
  236. paths.unshift(parent.$__pathRelativeToParent(void 0, false));
  237. const _parent = parent.$parent();
  238. if (_parent == null) {
  239. break;
  240. }
  241. parent = _parent;
  242. if (seenDocs.has(parent)) {
  243. throw new Error('Infinite subdocument loop: subdoc with _id ' + parent._id + ' is a parent of itself');
  244. }
  245. seenDocs.add(parent);
  246. }
  247. return paths.join('.');
  248. };
  249. /**
  250. * Returns this sub-documents parent document.
  251. *
  252. * @api public
  253. */
  254. Subdocument.prototype.parent = function() {
  255. return this.$__parent;
  256. };
  257. /**
  258. * Returns this sub-documents parent document.
  259. *
  260. * @api public
  261. * @method $parent
  262. */
  263. Subdocument.prototype.$parent = Subdocument.prototype.parent;
  264. /*!
  265. * no-op for hooks
  266. */
  267. Subdocument.prototype.$__remove = function(cb) {
  268. if (cb == null) {
  269. return;
  270. }
  271. return cb(null, this);
  272. };
  273. Subdocument.prototype.$__removeFromParent = function() {
  274. this.$__parent.set(this.$basePath, null);
  275. };
  276. /**
  277. * Null-out this subdoc
  278. *
  279. * @param {Object} [options]
  280. * @param {Function} [callback] optional callback for compatibility with Document.prototype.remove
  281. */
  282. Subdocument.prototype.remove = function(options, callback) {
  283. if (typeof options === 'function') {
  284. callback = options;
  285. options = null;
  286. }
  287. registerRemoveListener(this);
  288. // If removing entire doc, no need to remove subdoc
  289. if (!options || !options.noop) {
  290. this.$__removeFromParent();
  291. }
  292. return this.$__remove(callback);
  293. };
  294. /*!
  295. * ignore
  296. */
  297. Subdocument.prototype.populate = function() {
  298. throw new Error('Mongoose does not support calling populate() on nested ' +
  299. 'docs. Instead of `doc.nested.populate("path")`, use ' +
  300. '`doc.populate("nested.path")`');
  301. };
  302. /**
  303. * Helper for console.log
  304. *
  305. * @api public
  306. */
  307. Subdocument.prototype.inspect = function() {
  308. return this.toObject({
  309. transform: false,
  310. virtuals: false,
  311. flattenDecimals: false
  312. });
  313. };
  314. if (util.inspect.custom) {
  315. /*!
  316. * Avoid Node deprecation warning DEP0079
  317. */
  318. Subdocument.prototype[util.inspect.custom] = Subdocument.prototype.inspect;
  319. }
  320. /*!
  321. * Registers remove event listeners for triggering
  322. * on subdocuments.
  323. *
  324. * @param {Subdocument} sub
  325. * @api private
  326. */
  327. function registerRemoveListener(sub) {
  328. let owner = sub.ownerDocument();
  329. function emitRemove() {
  330. owner.$removeListener('save', emitRemove);
  331. owner.$removeListener('remove', emitRemove);
  332. sub.emit('remove', sub);
  333. sub.constructor.emit('remove', sub);
  334. owner = sub = null;
  335. }
  336. owner.$on('save', emitRemove);
  337. owner.$on('remove', emitRemove);
  338. }