You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
7.4 KiB

  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 path = this.path;
  41. var fn = this.defaultValue;
  42. if (!('defaultValue' in this) || fn !== void 0) {
  43. this.default(function() {
  44. var arr = fn.call(this);
  45. if (!Array.isArray(arr)) {
  46. arr = [arr];
  47. }
  48. return new MongooseDocumentArray(arr, path, this);
  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. }
  183. i = value.length;
  184. while (i--) {
  185. if (!value[i]) {
  186. continue;
  187. }
  188. // Check if the document has a different schema (re gh-3701)
  189. if ((value[i] instanceof Subdocument) &&
  190. value[i].schema !== this.casterConstructor.schema) {
  191. value[i] = value[i].toObject({virtuals: false});
  192. }
  193. if (!(value[i] instanceof Subdocument) && value[i]) {
  194. if (init) {
  195. selected || (selected = scopePaths(this, doc.$__.selected, init));
  196. subdoc = new this.casterConstructor(null, value, true, selected, i);
  197. value[i] = subdoc.init(value[i]);
  198. } else {
  199. try {
  200. subdoc = prev.id(value[i]._id);
  201. } catch (e) {
  202. }
  203. if (prev && subdoc) {
  204. // handle resetting doc with existing id but differing data
  205. // doc.array = [{ doc: 'val' }]
  206. subdoc.set(value[i]);
  207. // if set() is hooked it will have no return value
  208. // see gh-746
  209. value[i] = subdoc;
  210. } else {
  211. try {
  212. subdoc = new this.casterConstructor(value[i], value, undefined,
  213. undefined, i);
  214. // if set() is hooked it will have no return value
  215. // see gh-746
  216. value[i] = subdoc;
  217. } catch (error) {
  218. var valueInErrorMessage = util.inspect(value[i]);
  219. throw new CastError('embedded', valueInErrorMessage,
  220. value._path, error);
  221. }
  222. }
  223. }
  224. }
  225. }
  226. return value;
  227. };
  228. /*!
  229. * Scopes paths selected in a query to this array.
  230. * Necessary for proper default application of subdocument values.
  231. *
  232. * @param {DocumentArray} array - the array to scope `fields` paths
  233. * @param {Object|undefined} fields - the root fields selected in the query
  234. * @param {Boolean|undefined} init - if we are being created part of a query result
  235. */
  236. function scopePaths(array, fields, init) {
  237. if (!(init && fields)) {
  238. return undefined;
  239. }
  240. var path = array.path + '.',
  241. keys = Object.keys(fields),
  242. i = keys.length,
  243. selected = {},
  244. hasKeys,
  245. key;
  246. while (i--) {
  247. key = keys[i];
  248. if (key.indexOf(path) === 0) {
  249. hasKeys || (hasKeys = true);
  250. selected[key.substring(path.length)] = fields[key];
  251. }
  252. }
  253. return hasKeys && selected || undefined;
  254. }
  255. /*!
  256. * Module exports.
  257. */
  258. module.exports = DocumentArray;