array.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const $exists = require('./operators/exists');
  6. const $type = require('./operators/type');
  7. const MongooseError = require('../error/mongooseError');
  8. const SchemaType = require('../schematype');
  9. const CastError = SchemaType.CastError;
  10. const Types = {
  11. Array: SchemaArray,
  12. Boolean: require('./boolean'),
  13. Date: require('./date'),
  14. Number: require('./number'),
  15. String: require('./string'),
  16. ObjectId: require('./objectid'),
  17. Buffer: require('./buffer'),
  18. Map: require('./map')
  19. };
  20. const Mixed = require('./mixed');
  21. const cast = require('../cast');
  22. const get = require('../helpers/get');
  23. const util = require('util');
  24. const utils = require('../utils');
  25. const castToNumber = require('./operators/helpers').castToNumber;
  26. const geospatial = require('./operators/geospatial');
  27. const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
  28. let MongooseArray;
  29. let EmbeddedDoc;
  30. /**
  31. * Array SchemaType constructor
  32. *
  33. * @param {String} key
  34. * @param {SchemaType} cast
  35. * @param {Object} options
  36. * @inherits SchemaType
  37. * @api public
  38. */
  39. function SchemaArray(key, cast, options, schemaOptions) {
  40. // lazy load
  41. EmbeddedDoc || (EmbeddedDoc = require('../types').Embedded);
  42. let typeKey = 'type';
  43. if (schemaOptions && schemaOptions.typeKey) {
  44. typeKey = schemaOptions.typeKey;
  45. }
  46. this.schemaOptions = schemaOptions;
  47. if (cast) {
  48. let castOptions = {};
  49. if (utils.isPOJO(cast)) {
  50. if (cast[typeKey]) {
  51. // support { type: Woot }
  52. castOptions = utils.clone(cast); // do not alter user arguments
  53. delete castOptions[typeKey];
  54. cast = cast[typeKey];
  55. } else {
  56. cast = Mixed;
  57. }
  58. }
  59. if (cast === Object) {
  60. cast = Mixed;
  61. }
  62. // support { type: 'String' }
  63. const name = typeof cast === 'string'
  64. ? cast
  65. : utils.getFunctionName(cast);
  66. const caster = name in Types
  67. ? Types[name]
  68. : cast;
  69. this.casterConstructor = caster;
  70. if (typeof caster === 'function' &&
  71. !caster.$isArraySubdocument &&
  72. !caster.$isSchemaMap) {
  73. this.caster = new caster(null, castOptions);
  74. } else {
  75. this.caster = caster;
  76. }
  77. if (!(this.caster instanceof EmbeddedDoc)) {
  78. this.caster.path = key;
  79. }
  80. }
  81. this.$isMongooseArray = true;
  82. SchemaType.call(this, key, options, 'Array');
  83. let defaultArr;
  84. let fn;
  85. if (this.defaultValue != null) {
  86. defaultArr = this.defaultValue;
  87. fn = typeof defaultArr === 'function';
  88. }
  89. if (!('defaultValue' in this) || this.defaultValue !== void 0) {
  90. const defaultFn = function() {
  91. let arr = [];
  92. if (fn) {
  93. arr = defaultArr.call(this);
  94. } else if (defaultArr != null) {
  95. arr = arr.concat(defaultArr);
  96. }
  97. // Leave it up to `cast()` to convert the array
  98. return arr;
  99. };
  100. defaultFn.$runBeforeSetters = true;
  101. this.default(defaultFn);
  102. }
  103. }
  104. /**
  105. * This schema type's name, to defend against minifiers that mangle
  106. * function names.
  107. *
  108. * @api public
  109. */
  110. SchemaArray.schemaName = 'Array';
  111. /**
  112. * Options for all arrays.
  113. *
  114. * - `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.
  115. *
  116. * @static options
  117. * @api public
  118. */
  119. SchemaArray.options = { castNonArrays: true };
  120. /*!
  121. * Inherits from SchemaType.
  122. */
  123. SchemaArray.prototype = Object.create(SchemaType.prototype);
  124. SchemaArray.prototype.constructor = SchemaArray;
  125. /*!
  126. * ignore
  127. */
  128. SchemaArray._checkRequired = SchemaType.prototype.checkRequired;
  129. /**
  130. * Override the function the required validator uses to check whether an array
  131. * passes the `required` check.
  132. *
  133. * ####Example:
  134. *
  135. * // Require non-empty array to pass `required` check
  136. * mongoose.Schema.Types.Array.checkRequired(v => Array.isArray(v) && v.length);
  137. *
  138. * const M = mongoose.model({ arr: { type: Array, required: true } });
  139. * new M({ arr: [] }).validateSync(); // `null`, validation fails!
  140. *
  141. * @param {Function} fn
  142. * @return {Function}
  143. * @function checkRequired
  144. * @static
  145. * @api public
  146. */
  147. SchemaArray.checkRequired = SchemaType.checkRequired;
  148. /**
  149. * Check if the given value satisfies the `required` validator.
  150. *
  151. * @param {Any} value
  152. * @param {Document} doc
  153. * @return {Boolean}
  154. * @api public
  155. */
  156. SchemaArray.prototype.checkRequired = function checkRequired(value, doc) {
  157. if (SchemaType._isRef(this, value, doc, true)) {
  158. return !!value;
  159. }
  160. // `require('util').inherits()` does **not** copy static properties, and
  161. // plugins like mongoose-float use `inherits()` for pre-ES6.
  162. const _checkRequired = typeof this.constructor.checkRequired == 'function' ?
  163. this.constructor.checkRequired() :
  164. SchemaArray.checkRequired();
  165. return _checkRequired(value);
  166. };
  167. /**
  168. * Adds an enum validator if this is an array of strings. Equivalent to
  169. * `SchemaString.prototype.enum()`
  170. *
  171. * @param {String|Object} [args...] enumeration values
  172. * @return {SchemaType} this
  173. */
  174. SchemaArray.prototype.enum = function() {
  175. const instance = get(this, 'caster.instance');
  176. if (instance !== 'String') {
  177. throw new Error('`enum` can only be set on an array of strings, not ' + instance);
  178. }
  179. this.caster.enum.apply(this.caster, arguments);
  180. return this;
  181. };
  182. /**
  183. * Overrides the getters application for the population special-case
  184. *
  185. * @param {Object} value
  186. * @param {Object} scope
  187. * @api private
  188. */
  189. SchemaArray.prototype.applyGetters = function(value, scope) {
  190. if (this.caster.options && this.caster.options.ref) {
  191. // means the object id was populated
  192. return value;
  193. }
  194. return SchemaType.prototype.applyGetters.call(this, value, scope);
  195. };
  196. /**
  197. * Casts values for set().
  198. *
  199. * @param {Object} value
  200. * @param {Document} doc document that triggers the casting
  201. * @param {Boolean} init whether this is an initialization cast
  202. * @api private
  203. */
  204. SchemaArray.prototype.cast = function(value, doc, init) {
  205. // lazy load
  206. MongooseArray || (MongooseArray = require('../types').Array);
  207. let i;
  208. let l;
  209. if (Array.isArray(value)) {
  210. if (!value.length && doc) {
  211. const indexes = doc.schema.indexedPaths();
  212. for (i = 0, l = indexes.length; i < l; ++i) {
  213. const pathIndex = indexes[i][0][this.path];
  214. if (pathIndex === '2dsphere' || pathIndex === '2d') {
  215. return;
  216. }
  217. }
  218. }
  219. if (!(value && value.isMongooseArray)) {
  220. value = new MongooseArray(value, this.path, doc);
  221. } else if (value && value.isMongooseArray) {
  222. // We need to create a new array, otherwise change tracking will
  223. // update the old doc (gh-4449)
  224. value = new MongooseArray(value, this.path, doc);
  225. }
  226. if (this.caster && this.casterConstructor !== Mixed) {
  227. try {
  228. for (i = 0, l = value.length; i < l; i++) {
  229. value[i] = this.caster.cast(value[i], doc, init);
  230. }
  231. } catch (e) {
  232. // rethrow
  233. throw new CastError('[' + e.kind + ']', util.inspect(value), this.path, e);
  234. }
  235. }
  236. return value;
  237. }
  238. if (init || SchemaArray.options.castNonArrays) {
  239. // gh-2442: if we're loading this from the db and its not an array, mark
  240. // the whole array as modified.
  241. if (!!doc && !!init) {
  242. doc.markModified(this.path);
  243. }
  244. return this.cast([value], doc, init);
  245. }
  246. throw new CastError('Array', util.inspect(value), this.path);
  247. };
  248. /*!
  249. * Ignore
  250. */
  251. SchemaArray.prototype.discriminator = function(name, schema) {
  252. let arr = this; // eslint-disable-line consistent-this
  253. while (arr.$isMongooseArray && !arr.$isMongooseDocumentArray) {
  254. arr = arr.casterConstructor;
  255. if (arr == null || typeof arr === 'function') {
  256. throw new MongooseError('You can only add an embedded discriminator on ' +
  257. 'a document array, ' + this.path + ' is a plain array');
  258. }
  259. }
  260. return arr.discriminator(name, schema);
  261. };
  262. /*!
  263. * ignore
  264. */
  265. SchemaArray.prototype.clone = function() {
  266. const options = Object.assign({}, this.options, { validators: this.validators });
  267. return new this.constructor(this.path, this.caster, options, this.schemaOptions);
  268. };
  269. /**
  270. * Casts values for queries.
  271. *
  272. * @param {String} $conditional
  273. * @param {any} [value]
  274. * @api private
  275. */
  276. SchemaArray.prototype.castForQuery = function($conditional, value) {
  277. let handler;
  278. let val;
  279. if (arguments.length === 2) {
  280. handler = this.$conditionalHandlers[$conditional];
  281. if (!handler) {
  282. throw new Error('Can\'t use ' + $conditional + ' with Array.');
  283. }
  284. val = handler.call(this, value);
  285. } else {
  286. val = $conditional;
  287. let Constructor = this.casterConstructor;
  288. if (val &&
  289. Constructor.discriminators &&
  290. Constructor.schema &&
  291. Constructor.schema.options &&
  292. Constructor.schema.options.discriminatorKey) {
  293. if (typeof val[Constructor.schema.options.discriminatorKey] === 'string' &&
  294. Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]) {
  295. Constructor = Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]];
  296. } else {
  297. const constructorByValue = getDiscriminatorByValue(Constructor, val[Constructor.schema.options.discriminatorKey]);
  298. if (constructorByValue) {
  299. Constructor = constructorByValue;
  300. }
  301. }
  302. }
  303. const proto = this.casterConstructor.prototype;
  304. let method = proto && (proto.castForQuery || proto.cast);
  305. if (!method && Constructor.castForQuery) {
  306. method = Constructor.castForQuery;
  307. }
  308. const caster = this.caster;
  309. if (Array.isArray(val)) {
  310. this.setters.reverse().forEach(setter => {
  311. val = setter.call(this, val, this);
  312. });
  313. val = val.map(function(v) {
  314. if (utils.isObject(v) && v.$elemMatch) {
  315. return v;
  316. }
  317. if (method) {
  318. v = method.call(caster, v);
  319. return v;
  320. }
  321. if (v != null) {
  322. v = new Constructor(v);
  323. return v;
  324. }
  325. return v;
  326. });
  327. } else if (method) {
  328. val = method.call(caster, val);
  329. } else if (val != null) {
  330. val = new Constructor(val);
  331. }
  332. }
  333. return val;
  334. };
  335. function cast$all(val) {
  336. if (!Array.isArray(val)) {
  337. val = [val];
  338. }
  339. val = val.map(function(v) {
  340. if (utils.isObject(v)) {
  341. const o = {};
  342. o[this.path] = v;
  343. return cast(this.casterConstructor.schema, o)[this.path];
  344. }
  345. return v;
  346. }, this);
  347. return this.castForQuery(val);
  348. }
  349. function cast$elemMatch(val) {
  350. const keys = Object.keys(val);
  351. const numKeys = keys.length;
  352. for (let i = 0; i < numKeys; ++i) {
  353. const key = keys[i];
  354. const value = val[key];
  355. if (key.indexOf('$') === 0 && value) {
  356. val[key] = this.castForQuery(key, value);
  357. }
  358. }
  359. // Is this an embedded discriminator and is the discriminator key set?
  360. // If so, use the discriminator schema. See gh-7449
  361. const discriminatorKey = get(this,
  362. 'casterConstructor.schema.options.discriminatorKey');
  363. const discriminators = get(this, 'casterConstructor.schema.discriminators', {});
  364. if (discriminatorKey != null &&
  365. val[discriminatorKey] != null &&
  366. discriminators[val[discriminatorKey]] != null) {
  367. return cast(discriminators[val[discriminatorKey]], val);
  368. }
  369. return cast(this.casterConstructor.schema, val);
  370. }
  371. const handle = SchemaArray.prototype.$conditionalHandlers = {};
  372. handle.$all = cast$all;
  373. handle.$options = String;
  374. handle.$elemMatch = cast$elemMatch;
  375. handle.$geoIntersects = geospatial.cast$geoIntersects;
  376. handle.$or = handle.$and = function(val) {
  377. if (!Array.isArray(val)) {
  378. throw new TypeError('conditional $or/$and require array');
  379. }
  380. const ret = [];
  381. for (let i = 0; i < val.length; ++i) {
  382. ret.push(cast(this.casterConstructor.schema, val[i]));
  383. }
  384. return ret;
  385. };
  386. handle.$near =
  387. handle.$nearSphere = geospatial.cast$near;
  388. handle.$within =
  389. handle.$geoWithin = geospatial.cast$within;
  390. handle.$size =
  391. handle.$minDistance =
  392. handle.$maxDistance = castToNumber;
  393. handle.$exists = $exists;
  394. handle.$type = $type;
  395. handle.$eq =
  396. handle.$gt =
  397. handle.$gte =
  398. handle.$lt =
  399. handle.$lte =
  400. handle.$ne =
  401. handle.$nin =
  402. handle.$regex = SchemaArray.prototype.castForQuery;
  403. // `$in` is special because you can also include an empty array in the query
  404. // like `$in: [1, []]`, see gh-5913
  405. handle.$in = SchemaType.prototype.$conditionalHandlers.$in;
  406. /*!
  407. * Module exports.
  408. */
  409. module.exports = SchemaArray;