documentarray.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const ArrayType = require('./array');
  6. const CastError = require('../error/cast');
  7. const EventEmitter = require('events').EventEmitter;
  8. const SchemaType = require('../schematype');
  9. const discriminator = require('../helpers/model/discriminator');
  10. const util = require('util');
  11. const utils = require('../utils');
  12. const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
  13. let MongooseDocumentArray;
  14. let Subdocument;
  15. /**
  16. * SubdocsArray SchemaType constructor
  17. *
  18. * @param {String} key
  19. * @param {Schema} schema
  20. * @param {Object} options
  21. * @inherits SchemaArray
  22. * @api public
  23. */
  24. function DocumentArray(key, schema, options, schemaOptions) {
  25. const EmbeddedDocument = _createConstructor(schema, options);
  26. EmbeddedDocument.prototype.$basePath = key;
  27. ArrayType.call(this, key, EmbeddedDocument, options);
  28. this.schema = schema;
  29. this.schemaOptions = schemaOptions || {};
  30. this.$isMongooseDocumentArray = true;
  31. this.Constructor = EmbeddedDocument;
  32. EmbeddedDocument.base = schema.base;
  33. const fn = this.defaultValue;
  34. if (!('defaultValue' in this) || fn !== void 0) {
  35. this.default(function() {
  36. let arr = fn.call(this);
  37. if (!Array.isArray(arr)) {
  38. arr = [arr];
  39. }
  40. // Leave it up to `cast()` to convert this to a documentarray
  41. return arr;
  42. });
  43. }
  44. }
  45. /**
  46. * This schema type's name, to defend against minifiers that mangle
  47. * function names.
  48. *
  49. * @api public
  50. */
  51. DocumentArray.schemaName = 'DocumentArray';
  52. /**
  53. * Options for all document arrays.
  54. *
  55. * - `castNonArrays`: `true` by default. If `false`, Mongoose will throw a CastError when a value isn't an array. If `true`, Mongoose will wrap the provided value in an array before casting.
  56. *
  57. * @static options
  58. * @api public
  59. */
  60. DocumentArray.options = { castNonArrays: true };
  61. /*!
  62. * Inherits from ArrayType.
  63. */
  64. DocumentArray.prototype = Object.create(ArrayType.prototype);
  65. DocumentArray.prototype.constructor = DocumentArray;
  66. /*!
  67. * Ignore
  68. */
  69. function _createConstructor(schema, options) {
  70. Subdocument || (Subdocument = require('../types/embedded'));
  71. // compile an embedded document for this schema
  72. function EmbeddedDocument() {
  73. Subdocument.apply(this, arguments);
  74. this.$session(this.ownerDocument().$session());
  75. }
  76. EmbeddedDocument.prototype = Object.create(Subdocument.prototype);
  77. EmbeddedDocument.prototype.$__setSchema(schema);
  78. EmbeddedDocument.schema = schema;
  79. EmbeddedDocument.prototype.constructor = EmbeddedDocument;
  80. EmbeddedDocument.$isArraySubdocument = true;
  81. EmbeddedDocument.events = new EventEmitter();
  82. // apply methods
  83. for (const i in schema.methods) {
  84. EmbeddedDocument.prototype[i] = schema.methods[i];
  85. }
  86. // apply statics
  87. for (const i in schema.statics) {
  88. EmbeddedDocument[i] = schema.statics[i];
  89. }
  90. for (const i in EventEmitter.prototype) {
  91. EmbeddedDocument[i] = EventEmitter.prototype[i];
  92. }
  93. EmbeddedDocument.options = options;
  94. return EmbeddedDocument;
  95. }
  96. /*!
  97. * Ignore
  98. */
  99. DocumentArray.prototype.discriminator = function(name, schema) {
  100. if (typeof name === 'function') {
  101. name = utils.getFunctionName(name);
  102. }
  103. schema = discriminator(this.casterConstructor, name, schema);
  104. const EmbeddedDocument = _createConstructor(schema);
  105. EmbeddedDocument.baseCasterConstructor = this.casterConstructor;
  106. try {
  107. Object.defineProperty(EmbeddedDocument, 'name', {
  108. value: name
  109. });
  110. } catch (error) {
  111. // Ignore error, only happens on old versions of node
  112. }
  113. this.casterConstructor.discriminators[name] = EmbeddedDocument;
  114. return this.casterConstructor.discriminators[name];
  115. };
  116. /**
  117. * Performs local validations first, then validations on each embedded doc
  118. *
  119. * @api private
  120. */
  121. DocumentArray.prototype.doValidate = function(array, fn, scope, options) {
  122. // lazy load
  123. MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
  124. const _this = this;
  125. try {
  126. SchemaType.prototype.doValidate.call(this, array, cb, scope);
  127. } catch (err) {
  128. err.$isArrayValidatorError = true;
  129. return fn(err);
  130. }
  131. function cb(err) {
  132. if (err) {
  133. err.$isArrayValidatorError = true;
  134. return fn(err);
  135. }
  136. let count = array && array.length;
  137. let error;
  138. if (!count) {
  139. return fn();
  140. }
  141. if (options && options.updateValidator) {
  142. return fn();
  143. }
  144. if (!array.isMongooseDocumentArray) {
  145. array = new MongooseDocumentArray(array, _this.path, scope);
  146. }
  147. // handle sparse arrays, do not use array.forEach which does not
  148. // iterate over sparse elements yet reports array.length including
  149. // them :(
  150. function callback(err) {
  151. if (err != null) {
  152. error = err;
  153. if (error.name !== 'ValidationError') {
  154. error.$isArrayValidatorError = true;
  155. }
  156. }
  157. --count || fn(error);
  158. }
  159. for (let i = 0, len = count; i < len; ++i) {
  160. // sidestep sparse entries
  161. let doc = array[i];
  162. if (doc == null) {
  163. --count || fn(error);
  164. continue;
  165. }
  166. // If you set the array index directly, the doc might not yet be
  167. // a full fledged mongoose subdoc, so make it into one.
  168. if (!(doc instanceof Subdocument)) {
  169. doc = array[i] = new _this.casterConstructor(doc, array, undefined,
  170. undefined, i);
  171. }
  172. doc.$__validate(callback);
  173. }
  174. }
  175. };
  176. /**
  177. * Performs local validations first, then validations on each embedded doc.
  178. *
  179. * ####Note:
  180. *
  181. * This method ignores the asynchronous validators.
  182. *
  183. * @return {MongooseError|undefined}
  184. * @api private
  185. */
  186. DocumentArray.prototype.doValidateSync = function(array, scope) {
  187. const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope);
  188. if (schemaTypeError != null) {
  189. schemaTypeError.$isArrayValidatorError = true;
  190. return schemaTypeError;
  191. }
  192. const count = array && array.length;
  193. let resultError = null;
  194. if (!count) {
  195. return;
  196. }
  197. // handle sparse arrays, do not use array.forEach which does not
  198. // iterate over sparse elements yet reports array.length including
  199. // them :(
  200. for (let i = 0, len = count; i < len; ++i) {
  201. // sidestep sparse entries
  202. let doc = array[i];
  203. if (!doc) {
  204. continue;
  205. }
  206. // If you set the array index directly, the doc might not yet be
  207. // a full fledged mongoose subdoc, so make it into one.
  208. if (!(doc instanceof Subdocument)) {
  209. doc = array[i] = new this.casterConstructor(doc, array, undefined,
  210. undefined, i);
  211. }
  212. const subdocValidateError = doc.validateSync();
  213. if (subdocValidateError && resultError == null) {
  214. resultError = subdocValidateError;
  215. }
  216. }
  217. return resultError;
  218. };
  219. /*!
  220. * ignore
  221. */
  222. DocumentArray.prototype.getDefault = function(scope) {
  223. let ret = typeof this.defaultValue === 'function'
  224. ? this.defaultValue.call(scope)
  225. : this.defaultValue;
  226. if (ret == null) {
  227. return ret;
  228. }
  229. // lazy load
  230. MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
  231. if (!Array.isArray(ret)) {
  232. ret = [ret];
  233. }
  234. ret = new MongooseDocumentArray(ret, this.path, scope);
  235. const _parent = ret._parent;
  236. ret._parent = null;
  237. for (let i = 0; i < ret.length; ++i) {
  238. ret[i] = new this.Constructor(ret[i], ret, undefined,
  239. undefined, i);
  240. }
  241. ret._parent = _parent;
  242. return ret;
  243. };
  244. /**
  245. * Casts contents
  246. *
  247. * @param {Object} value
  248. * @param {Document} document that triggers the casting
  249. * @api private
  250. */
  251. DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
  252. // lazy load
  253. MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
  254. let selected;
  255. let subdoc;
  256. let i;
  257. const _opts = { transform: false, virtuals: false };
  258. if (!Array.isArray(value)) {
  259. if (!init && !DocumentArray.options.castNonArrays) {
  260. throw new CastError('DocumentArray', util.inspect(value), this.path);
  261. }
  262. // gh-2442 mark whole array as modified if we're initializing a doc from
  263. // the db and the path isn't an array in the document
  264. if (!!doc && init) {
  265. doc.markModified(this.path);
  266. }
  267. return this.cast([value], doc, init, prev);
  268. }
  269. if (!(value && value.isMongooseDocumentArray) &&
  270. (!options || !options.skipDocumentArrayCast)) {
  271. value = new MongooseDocumentArray(value, this.path, doc);
  272. _clearListeners(prev);
  273. } else if (value && value.isMongooseDocumentArray) {
  274. // We need to create a new array, otherwise change tracking will
  275. // update the old doc (gh-4449)
  276. value = new MongooseDocumentArray(value, this.path, doc);
  277. }
  278. i = value.length;
  279. while (i--) {
  280. if (!value[i]) {
  281. continue;
  282. }
  283. let Constructor = this.casterConstructor;
  284. if (Constructor.discriminators &&
  285. Constructor.schema &&
  286. Constructor.schema.options &&
  287. typeof value[i][Constructor.schema.options.discriminatorKey] === 'string') {
  288. if (Constructor.discriminators[value[i][Constructor.schema.options.discriminatorKey]]) {
  289. Constructor = Constructor.discriminators[value[i][Constructor.schema.options.discriminatorKey]];
  290. } else {
  291. const constructorByValue = getDiscriminatorByValue(Constructor, value[i][Constructor.schema.options.discriminatorKey]);
  292. if (constructorByValue) {
  293. Constructor = constructorByValue;
  294. }
  295. }
  296. }
  297. // Check if the document has a different schema (re gh-3701)
  298. if ((value[i].$__) &&
  299. value[i].schema !== Constructor.schema) {
  300. value[i] = value[i].toObject({ transform: false, virtuals: false });
  301. }
  302. if (value[i] instanceof Subdocument) {
  303. // Might not have the correct index yet, so ensure it does.
  304. value[i].$setIndex(i);
  305. } else if (value[i] != null) {
  306. if (init) {
  307. if (doc) {
  308. selected || (selected = scopePaths(this, doc.$__.selected, init));
  309. } else {
  310. selected = true;
  311. }
  312. subdoc = new Constructor(null, value, true, selected, i);
  313. value[i] = subdoc.init(value[i]);
  314. } else {
  315. if (prev && (subdoc = prev.id(value[i]._id))) {
  316. subdoc = prev.id(value[i]._id);
  317. }
  318. if (prev && subdoc && utils.deepEqual(subdoc.toObject(_opts), value[i])) {
  319. // handle resetting doc with existing id and same data
  320. subdoc.set(value[i]);
  321. // if set() is hooked it will have no return value
  322. // see gh-746
  323. value[i] = subdoc;
  324. } else {
  325. try {
  326. subdoc = new Constructor(value[i], value, undefined,
  327. undefined, i);
  328. // if set() is hooked it will have no return value
  329. // see gh-746
  330. value[i] = subdoc;
  331. } catch (error) {
  332. // Make sure we don't leave listeners dangling because `value`
  333. // won't get back up to the schema type. See gh-6723
  334. _clearListeners(value);
  335. const valueInErrorMessage = util.inspect(value[i]);
  336. throw new CastError('embedded', valueInErrorMessage,
  337. value._path, error);
  338. }
  339. }
  340. }
  341. }
  342. }
  343. return value;
  344. };
  345. /*!
  346. * Removes listeners from parent
  347. */
  348. function _clearListeners(arr) {
  349. if (arr == null || arr._parent == null) {
  350. return;
  351. }
  352. for (const key in arr._handlers) {
  353. arr._parent.removeListener(key, arr._handlers[key]);
  354. }
  355. }
  356. /*!
  357. * Scopes paths selected in a query to this array.
  358. * Necessary for proper default application of subdocument values.
  359. *
  360. * @param {DocumentArray} array - the array to scope `fields` paths
  361. * @param {Object|undefined} fields - the root fields selected in the query
  362. * @param {Boolean|undefined} init - if we are being created part of a query result
  363. */
  364. function scopePaths(array, fields, init) {
  365. if (!(init && fields)) {
  366. return undefined;
  367. }
  368. const path = array.path + '.';
  369. const keys = Object.keys(fields);
  370. let i = keys.length;
  371. const selected = {};
  372. let hasKeys;
  373. let key;
  374. let sub;
  375. while (i--) {
  376. key = keys[i];
  377. if (key.indexOf(path) === 0) {
  378. sub = key.substring(path.length);
  379. if (sub === '$') {
  380. continue;
  381. }
  382. if (sub.indexOf('$.') === 0) {
  383. sub = sub.substr(2);
  384. }
  385. hasKeys || (hasKeys = true);
  386. selected[sub] = fields[key];
  387. }
  388. }
  389. return hasKeys && selected || undefined;
  390. }
  391. /*!
  392. * Module exports.
  393. */
  394. module.exports = DocumentArray;