documentarray.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*!
  2. * Module dependencies.
  3. */
  4. var ArrayType = require('./array');
  5. var Document = require('../document');
  6. var MongooseDocumentArray = require('../types/documentarray');
  7. var SchemaType = require('../schematype');
  8. var Subdocument = require('../types/embedded');
  9. /**
  10. * SubdocsArray SchemaType constructor
  11. *
  12. * @param {String} key
  13. * @param {Schema} schema
  14. * @param {Object} options
  15. * @inherits SchemaArray
  16. * @api private
  17. */
  18. function DocumentArray (key, schema, options) {
  19. // compile an embedded document for this schema
  20. function EmbeddedDocument () {
  21. Subdocument.apply(this, arguments);
  22. }
  23. EmbeddedDocument.prototype = Object.create(Subdocument.prototype);
  24. EmbeddedDocument.prototype.$__setSchema(schema);
  25. EmbeddedDocument.schema = schema;
  26. // apply methods
  27. for (var i in schema.methods)
  28. EmbeddedDocument.prototype[i] = schema.methods[i];
  29. // apply statics
  30. for (var i in schema.statics)
  31. EmbeddedDocument[i] = schema.statics[i];
  32. EmbeddedDocument.options = options;
  33. this.schema = schema;
  34. ArrayType.call(this, key, EmbeddedDocument, options);
  35. this.schema = schema;
  36. var path = this.path;
  37. var fn = this.defaultValue;
  38. this.default(function(){
  39. var arr = fn.call(this);
  40. if (!Array.isArray(arr)) arr = [arr];
  41. return new MongooseDocumentArray(arr, path, this);
  42. });
  43. }
  44. /**
  45. * This schema type's name, to defend against minifiers that mangle
  46. * function names.
  47. *
  48. * @api private
  49. */
  50. DocumentArray.schemaName = 'DocumentArray';
  51. /*!
  52. * Inherits from ArrayType.
  53. */
  54. DocumentArray.prototype = Object.create( ArrayType.prototype );
  55. DocumentArray.prototype.constructor = DocumentArray;
  56. /**
  57. * Performs local validations first, then validations on each embedded doc
  58. *
  59. * @api private
  60. */
  61. DocumentArray.prototype.doValidate = function (array, fn, scope) {
  62. SchemaType.prototype.doValidate.call(this, array, function (err) {
  63. if (err) {
  64. return fn(err);
  65. }
  66. var count = array && array.length;
  67. var error;
  68. if (!count) return fn();
  69. // handle sparse arrays, do not use array.forEach which does not
  70. // iterate over sparse elements yet reports array.length including
  71. // them :(
  72. for (var i = 0, len = count; i < len; ++i) {
  73. // sidestep sparse entries
  74. var doc = array[i];
  75. if (!doc) {
  76. --count || fn(error);
  77. continue;
  78. }
  79. doc.validate(function (err) {
  80. if (err) {
  81. error = err;
  82. }
  83. --count || fn(error);
  84. });
  85. }
  86. }, scope);
  87. };
  88. /**
  89. * Performs local validations first, then validations on each embedded doc.
  90. *
  91. * ####Note:
  92. *
  93. * This method ignores the asynchronous validators.
  94. *
  95. * @return {MongooseError|undefined}
  96. * @api private
  97. */
  98. DocumentArray.prototype.doValidateSync = function (array, scope) {
  99. var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope);
  100. if (schemaTypeError) return schemaTypeError;
  101. var count = array && array.length
  102. , resultError = null;
  103. if (!count) return;
  104. // handle sparse arrays, do not use array.forEach which does not
  105. // iterate over sparse elements yet reports array.length including
  106. // them :(
  107. for (var i = 0, len = count; i < len; ++i) {
  108. // only first error
  109. if ( resultError ) break;
  110. // sidestep sparse entries
  111. var doc = array[i];
  112. if (!doc) continue;
  113. var subdocValidateError = doc.validateSync();
  114. if (subdocValidateError) {
  115. resultError = subdocValidateError;
  116. }
  117. }
  118. return resultError;
  119. };
  120. /**
  121. * Casts contents
  122. *
  123. * @param {Object} value
  124. * @param {Document} document that triggers the casting
  125. * @api private
  126. */
  127. DocumentArray.prototype.cast = function (value, doc, init, prev) {
  128. var selected
  129. , subdoc
  130. , i
  131. if (!Array.isArray(value)) {
  132. // gh-2442 mark whole array as modified if we're initializing a doc from
  133. // the db and the path isn't an array in the document
  134. if (!!doc && init) {
  135. doc.markModified(this.path);
  136. }
  137. return this.cast([value], doc, init, prev);
  138. }
  139. if (!(value && value.isMongooseDocumentArray)) {
  140. value = new MongooseDocumentArray(value, this.path, doc);
  141. if (prev && prev._handlers) {
  142. for (var key in prev._handlers) {
  143. doc.removeListener(key, prev._handlers[key]);
  144. }
  145. }
  146. }
  147. i = value.length;
  148. while (i--) {
  149. if (!(value[i] instanceof Subdocument) && value[i]) {
  150. if (init) {
  151. selected || (selected = scopePaths(this, doc.$__.selected, init));
  152. subdoc = new this.casterConstructor(null, value, true, selected, i);
  153. value[i] = subdoc.init(value[i]);
  154. } else {
  155. try {
  156. subdoc = prev.id(value[i]._id);
  157. } catch(e) {}
  158. if (prev && subdoc) {
  159. // handle resetting doc with existing id but differing data
  160. // doc.array = [{ doc: 'val' }]
  161. subdoc.set(value[i]);
  162. // if set() is hooked it will have no return value
  163. // see gh-746
  164. value[i] = subdoc;
  165. } else {
  166. subdoc = new this.casterConstructor(value[i], value, undefined, undefined, i);
  167. // if set() is hooked it will have no return value
  168. // see gh-746
  169. value[i] = subdoc;
  170. }
  171. }
  172. }
  173. }
  174. return value;
  175. }
  176. /*!
  177. * Scopes paths selected in a query to this array.
  178. * Necessary for proper default application of subdocument values.
  179. *
  180. * @param {DocumentArray} array - the array to scope `fields` paths
  181. * @param {Object|undefined} fields - the root fields selected in the query
  182. * @param {Boolean|undefined} init - if we are being created part of a query result
  183. */
  184. function scopePaths (array, fields, init) {
  185. if (!(init && fields)) return undefined;
  186. var path = array.path + '.'
  187. , keys = Object.keys(fields)
  188. , i = keys.length
  189. , selected = {}
  190. , hasKeys
  191. , key
  192. while (i--) {
  193. key = keys[i];
  194. if (0 === key.indexOf(path)) {
  195. hasKeys || (hasKeys = true);
  196. selected[key.substring(path.length)] = fields[key];
  197. }
  198. }
  199. return hasKeys && selected || undefined;
  200. }
  201. /*!
  202. * Module exports.
  203. */
  204. module.exports = DocumentArray;