documentarray.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /* eslint no-empty: 1 */
  2. /*!
  3. * Module dependencies.
  4. */
  5. var ArrayType = require('./array');
  6. var CastError = require('../error/cast');
  7. var MongooseDocumentArray = require('../types/documentarray');
  8. var SchemaType = require('../schematype');
  9. var Subdocument = require('../types/embedded');
  10. var util = require('util');
  11. /**
  12. * SubdocsArray SchemaType constructor
  13. *
  14. * @param {String} key
  15. * @param {Schema} schema
  16. * @param {Object} options
  17. * @inherits SchemaArray
  18. * @api public
  19. */
  20. function DocumentArray(key, schema, options) {
  21. // compile an embedded document for this schema
  22. function EmbeddedDocument() {
  23. Subdocument.apply(this, arguments);
  24. }
  25. EmbeddedDocument.prototype = Object.create(Subdocument.prototype);
  26. EmbeddedDocument.prototype.$__setSchema(schema);
  27. EmbeddedDocument.schema = schema;
  28. // apply methods
  29. for (var i in schema.methods) {
  30. EmbeddedDocument.prototype[i] = schema.methods[i];
  31. }
  32. // apply statics
  33. for (i in schema.statics) {
  34. EmbeddedDocument[i] = schema.statics[i];
  35. }
  36. EmbeddedDocument.options = options;
  37. ArrayType.call(this, key, EmbeddedDocument, options);
  38. this.schema = schema;
  39. this.$isMongooseDocumentArray = true;
  40. var fn = this.defaultValue;
  41. if (!('defaultValue' in this) || fn !== void 0) {
  42. this.default(function() {
  43. var arr = fn.call(this);
  44. if (!Array.isArray(arr)) {
  45. arr = [arr];
  46. }
  47. // Leave it up to `cast()` to convert this to a documentarray
  48. return arr;
  49. });
  50. }
  51. }
  52. /**
  53. * This schema type's name, to defend against minifiers that mangle
  54. * function names.
  55. *
  56. * @api public
  57. */
  58. DocumentArray.schemaName = 'DocumentArray';
  59. /*!
  60. * Inherits from ArrayType.
  61. */
  62. DocumentArray.prototype = Object.create(ArrayType.prototype);
  63. DocumentArray.prototype.constructor = DocumentArray;
  64. /**
  65. * Performs local validations first, then validations on each embedded doc
  66. *
  67. * @api private
  68. */
  69. DocumentArray.prototype.doValidate = function(array, fn, scope, options) {
  70. var _this = this;
  71. SchemaType.prototype.doValidate.call(this, array, function(err) {
  72. if (err) {
  73. return fn(err);
  74. }
  75. var count = array && array.length;
  76. var error;
  77. if (!count) {
  78. return fn();
  79. }
  80. if (options && options.updateValidator) {
  81. return fn();
  82. }
  83. // handle sparse arrays, do not use array.forEach which does not
  84. // iterate over sparse elements yet reports array.length including
  85. // them :(
  86. function callback(err) {
  87. if (err) {
  88. error = err;
  89. }
  90. --count || fn(error);
  91. }
  92. for (var i = 0, len = count; i < len; ++i) {
  93. // sidestep sparse entries
  94. var doc = array[i];
  95. if (!doc) {
  96. --count || fn(error);
  97. continue;
  98. }
  99. // If you set the array index directly, the doc might not yet be
  100. // a full fledged mongoose subdoc, so make it into one.
  101. if (!(doc instanceof Subdocument)) {
  102. doc = array[i] = new _this.casterConstructor(doc, array, undefined,
  103. undefined, i);
  104. }
  105. // HACK: use $__original_validate to avoid promises so bluebird doesn't
  106. // complain
  107. if (doc.$__original_validate) {
  108. doc.$__original_validate({__noPromise: true}, callback);
  109. } else {
  110. doc.validate({__noPromise: true}, callback);
  111. }
  112. }
  113. }, scope);
  114. };
  115. /**
  116. * Performs local validations first, then validations on each embedded doc.
  117. *
  118. * ####Note:
  119. *
  120. * This method ignores the asynchronous validators.
  121. *
  122. * @return {MongooseError|undefined}
  123. * @api private
  124. */
  125. DocumentArray.prototype.doValidateSync = function(array, scope) {
  126. var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope);
  127. if (schemaTypeError) {
  128. return schemaTypeError;
  129. }
  130. var count = array && array.length,
  131. resultError = null;
  132. if (!count) {
  133. return;
  134. }
  135. // handle sparse arrays, do not use array.forEach which does not
  136. // iterate over sparse elements yet reports array.length including
  137. // them :(
  138. for (var i = 0, len = count; i < len; ++i) {
  139. // only first error
  140. if (resultError) {
  141. break;
  142. }
  143. // sidestep sparse entries
  144. var doc = array[i];
  145. if (!doc) {
  146. continue;
  147. }
  148. var subdocValidateError = doc.validateSync();
  149. if (subdocValidateError) {
  150. resultError = subdocValidateError;
  151. }
  152. }
  153. return resultError;
  154. };
  155. /**
  156. * Casts contents
  157. *
  158. * @param {Object} value
  159. * @param {Document} document that triggers the casting
  160. * @api private
  161. */
  162. DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
  163. var selected,
  164. subdoc,
  165. i;
  166. if (!Array.isArray(value)) {
  167. // gh-2442 mark whole array as modified if we're initializing a doc from
  168. // the db and the path isn't an array in the document
  169. if (!!doc && init) {
  170. doc.markModified(this.path);
  171. }
  172. return this.cast([value], doc, init, prev);
  173. }
  174. if (!(value && value.isMongooseDocumentArray) &&
  175. (!options || !options.skipDocumentArrayCast)) {
  176. value = new MongooseDocumentArray(value, this.path, doc);
  177. if (prev && prev._handlers) {
  178. for (var key in prev._handlers) {
  179. doc.removeListener(key, prev._handlers[key]);
  180. }
  181. }
  182. } else if (value && value.isMongooseDocumentArray) {
  183. // We need to create a new array, otherwise change tracking will
  184. // update the old doc (gh-4449)
  185. value = new MongooseDocumentArray(value, this.path, doc);
  186. }
  187. i = value.length;
  188. while (i--) {
  189. if (!value[i]) {
  190. continue;
  191. }
  192. // Check if the document has a different schema (re gh-3701)
  193. if ((value[i] instanceof Subdocument) &&
  194. value[i].schema !== this.casterConstructor.schema) {
  195. value[i] = value[i].toObject({ transform: false, virtuals: false });
  196. }
  197. if (!(value[i] instanceof Subdocument) && value[i]) {
  198. if (init) {
  199. if (doc) {
  200. selected || (selected = scopePaths(this, doc.$__.selected, init));
  201. } else {
  202. selected = true;
  203. }
  204. subdoc = new this.casterConstructor(null, value, true, selected, i);
  205. value[i] = subdoc.init(value[i]);
  206. } else {
  207. try {
  208. subdoc = prev.id(value[i]._id);
  209. } catch (e) {
  210. }
  211. if (prev && subdoc) {
  212. // handle resetting doc with existing id but differing data
  213. // doc.array = [{ doc: 'val' }]
  214. subdoc.set(value[i]);
  215. // if set() is hooked it will have no return value
  216. // see gh-746
  217. value[i] = subdoc;
  218. } else {
  219. try {
  220. subdoc = new this.casterConstructor(value[i], value, undefined,
  221. undefined, i);
  222. // if set() is hooked it will have no return value
  223. // see gh-746
  224. value[i] = subdoc;
  225. } catch (error) {
  226. var valueInErrorMessage = util.inspect(value[i]);
  227. throw new CastError('embedded', valueInErrorMessage,
  228. value._path, error);
  229. }
  230. }
  231. }
  232. }
  233. }
  234. return value;
  235. };
  236. /*!
  237. * Scopes paths selected in a query to this array.
  238. * Necessary for proper default application of subdocument values.
  239. *
  240. * @param {DocumentArray} array - the array to scope `fields` paths
  241. * @param {Object|undefined} fields - the root fields selected in the query
  242. * @param {Boolean|undefined} init - if we are being created part of a query result
  243. */
  244. function scopePaths(array, fields, init) {
  245. if (!(init && fields)) {
  246. return undefined;
  247. }
  248. var path = array.path + '.',
  249. keys = Object.keys(fields),
  250. i = keys.length,
  251. selected = {},
  252. hasKeys,
  253. key;
  254. while (i--) {
  255. key = keys[i];
  256. if (key.indexOf(path) === 0) {
  257. hasKeys || (hasKeys = true);
  258. selected[key.substring(path.length)] = fields[key];
  259. }
  260. }
  261. return hasKeys && selected || undefined;
  262. }
  263. /*!
  264. * Module exports.
  265. */
  266. module.exports = DocumentArray;