number.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. 'use strict';
  2. /*!
  3. * Module requirements.
  4. */
  5. const MongooseError = require('../error/index');
  6. const SchemaNumberOptions = require('../options/schemaNumberOptions');
  7. const SchemaType = require('../schemaType');
  8. const castNumber = require('../cast/number');
  9. const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
  10. const handleBitwiseOperator = require('./operators/bitwise');
  11. const utils = require('../utils');
  12. const CastError = SchemaType.CastError;
  13. /**
  14. * Number SchemaType constructor.
  15. *
  16. * @param {String} key
  17. * @param {Object} options
  18. * @param {Object} schemaOptions
  19. * @param {Schema} parentSchema
  20. * @inherits SchemaType
  21. * @api public
  22. */
  23. function SchemaNumber(key, options, _schemaOptions, parentSchema) {
  24. this.enumValues = [];
  25. SchemaType.call(this, key, options, 'Number', parentSchema);
  26. }
  27. /**
  28. * Attaches a getter for all Number instances.
  29. *
  30. * #### Example:
  31. *
  32. * // Make all numbers round down
  33. * mongoose.Number.get(function(v) { return Math.floor(v); });
  34. *
  35. * const Model = mongoose.model('Test', new Schema({ test: Number }));
  36. * new Model({ test: 3.14 }).test; // 3
  37. *
  38. * @param {Function} getter
  39. * @return {this}
  40. * @function get
  41. * @static
  42. * @api public
  43. */
  44. SchemaNumber.get = SchemaType.get;
  45. /**
  46. * Sets a default option for all Number instances.
  47. *
  48. * #### Example:
  49. *
  50. * // Make all numbers have option `min` equal to 0.
  51. * mongoose.Schema.Number.set('min', 0);
  52. *
  53. * const Order = mongoose.model('Order', new Schema({ amount: Number }));
  54. * new Order({ amount: -10 }).validateSync().errors.amount.message; // Path `amount` must be larger than 0.
  55. *
  56. * @param {String} option The option you'd like to set the value for
  57. * @param {Any} value value for option
  58. * @return {undefined}
  59. * @function set
  60. * @static
  61. * @api public
  62. */
  63. SchemaNumber.set = SchemaType.set;
  64. SchemaNumber.setters = [];
  65. /*!
  66. * ignore
  67. */
  68. SchemaNumber._cast = castNumber;
  69. /**
  70. * Get/set the function used to cast arbitrary values to numbers.
  71. *
  72. * #### Example:
  73. *
  74. * // Make Mongoose cast empty strings '' to 0 for paths declared as numbers
  75. * const original = mongoose.Number.cast();
  76. * mongoose.Number.cast(v => {
  77. * if (v === '') { return 0; }
  78. * return original(v);
  79. * });
  80. *
  81. * // Or disable casting entirely
  82. * mongoose.Number.cast(false);
  83. *
  84. * @param {Function} caster
  85. * @return {Function}
  86. * @function cast
  87. * @static
  88. * @api public
  89. */
  90. SchemaNumber.cast = function cast(caster) {
  91. if (arguments.length === 0) {
  92. return this._cast;
  93. }
  94. if (caster === false) {
  95. caster = this._defaultCaster;
  96. }
  97. this._cast = caster;
  98. return this._cast;
  99. };
  100. /*!
  101. * ignore
  102. */
  103. SchemaNumber._defaultCaster = v => {
  104. if (typeof v !== 'number') {
  105. throw new Error();
  106. }
  107. return v;
  108. };
  109. /**
  110. * This schema type's name, to defend against minifiers that mangle
  111. * function names.
  112. *
  113. * @api public
  114. */
  115. SchemaNumber.schemaName = 'Number';
  116. SchemaNumber.defaultOptions = {};
  117. /*!
  118. * Inherits from SchemaType.
  119. */
  120. SchemaNumber.prototype = Object.create(SchemaType.prototype);
  121. SchemaNumber.prototype.constructor = SchemaNumber;
  122. SchemaNumber.prototype.OptionsConstructor = SchemaNumberOptions;
  123. /*!
  124. * ignore
  125. */
  126. SchemaNumber._checkRequired = v => typeof v === 'number' || v instanceof Number;
  127. /**
  128. * Override the function the required validator uses to check whether a string
  129. * passes the `required` check.
  130. *
  131. * @param {Function} fn
  132. * @return {Function}
  133. * @function checkRequired
  134. * @static
  135. * @api public
  136. */
  137. SchemaNumber.checkRequired = SchemaType.checkRequired;
  138. /**
  139. * Check if the given value satisfies a required validator.
  140. *
  141. * @param {Any} value
  142. * @param {Document} doc
  143. * @return {Boolean}
  144. * @api public
  145. */
  146. SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) {
  147. if (typeof value === 'object' && SchemaType._isRef(this, value, doc, true)) {
  148. return value != null;
  149. }
  150. // `require('util').inherits()` does **not** copy static properties, and
  151. // plugins like mongoose-float use `inherits()` for pre-ES6.
  152. const _checkRequired = typeof this.constructor.checkRequired === 'function' ?
  153. this.constructor.checkRequired() :
  154. SchemaNumber.checkRequired();
  155. return _checkRequired(value);
  156. };
  157. /**
  158. * Sets a minimum number validator.
  159. *
  160. * #### Example:
  161. *
  162. * const s = new Schema({ n: { type: Number, min: 10 })
  163. * const M = db.model('M', s)
  164. * const m = new M({ n: 9 })
  165. * m.save(function (err) {
  166. * console.error(err) // validator error
  167. * m.n = 10;
  168. * m.save() // success
  169. * })
  170. *
  171. * // custom error messages
  172. * // We can also use the special {MIN} token which will be replaced with the invalid value
  173. * const min = [10, 'The value of path `{PATH}` ({VALUE}) is beneath the limit ({MIN}).'];
  174. * const schema = new Schema({ n: { type: Number, min: min })
  175. * const M = mongoose.model('Measurement', schema);
  176. * const s= new M({ n: 4 });
  177. * s.validate(function (err) {
  178. * console.log(String(err)) // ValidationError: The value of path `n` (4) is beneath the limit (10).
  179. * })
  180. *
  181. * @param {Number} value minimum number
  182. * @param {String} [message] optional custom error message
  183. * @return {SchemaType} this
  184. * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
  185. * @api public
  186. */
  187. SchemaNumber.prototype.min = function(value, message) {
  188. if (this.minValidator) {
  189. this.validators = this.validators.filter(function(v) {
  190. return v.validator !== this.minValidator;
  191. }, this);
  192. }
  193. if (value != null) {
  194. let msg = message || MongooseError.messages.Number.min;
  195. msg = msg.replace(/{MIN}/, value);
  196. this.validators.push({
  197. validator: this.minValidator = function(v) {
  198. return v == null || v >= value;
  199. },
  200. message: msg,
  201. type: 'min',
  202. min: value
  203. });
  204. }
  205. return this;
  206. };
  207. /**
  208. * Sets a maximum number validator.
  209. *
  210. * #### Example:
  211. *
  212. * const s = new Schema({ n: { type: Number, max: 10 })
  213. * const M = db.model('M', s)
  214. * const m = new M({ n: 11 })
  215. * m.save(function (err) {
  216. * console.error(err) // validator error
  217. * m.n = 10;
  218. * m.save() // success
  219. * })
  220. *
  221. * // custom error messages
  222. * // We can also use the special {MAX} token which will be replaced with the invalid value
  223. * const max = [10, 'The value of path `{PATH}` ({VALUE}) exceeds the limit ({MAX}).'];
  224. * const schema = new Schema({ n: { type: Number, max: max })
  225. * const M = mongoose.model('Measurement', schema);
  226. * const s= new M({ n: 4 });
  227. * s.validate(function (err) {
  228. * console.log(String(err)) // ValidationError: The value of path `n` (4) exceeds the limit (10).
  229. * })
  230. *
  231. * @param {Number} maximum number
  232. * @param {String} [message] optional custom error message
  233. * @return {SchemaType} this
  234. * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
  235. * @api public
  236. */
  237. SchemaNumber.prototype.max = function(value, message) {
  238. if (this.maxValidator) {
  239. this.validators = this.validators.filter(function(v) {
  240. return v.validator !== this.maxValidator;
  241. }, this);
  242. }
  243. if (value != null) {
  244. let msg = message || MongooseError.messages.Number.max;
  245. msg = msg.replace(/{MAX}/, value);
  246. this.validators.push({
  247. validator: this.maxValidator = function(v) {
  248. return v == null || v <= value;
  249. },
  250. message: msg,
  251. type: 'max',
  252. max: value
  253. });
  254. }
  255. return this;
  256. };
  257. /**
  258. * Sets a enum validator
  259. *
  260. * #### Example:
  261. *
  262. * const s = new Schema({ n: { type: Number, enum: [1, 2, 3] });
  263. * const M = db.model('M', s);
  264. *
  265. * const m = new M({ n: 4 });
  266. * await m.save(); // throws validation error
  267. *
  268. * m.n = 3;
  269. * await m.save(); // succeeds
  270. *
  271. * @param {Array} values allowed values
  272. * @param {String} [message] optional custom error message
  273. * @return {SchemaType} this
  274. * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
  275. * @api public
  276. */
  277. SchemaNumber.prototype.enum = function(values, message) {
  278. if (this.enumValidator) {
  279. this.validators = this.validators.filter(function(v) {
  280. return v.validator !== this.enumValidator;
  281. }, this);
  282. this.enumValidator = false;
  283. }
  284. if (values === void 0 || values === false) {
  285. return this;
  286. }
  287. if (!Array.isArray(values)) {
  288. const isObjectSyntax = utils.isPOJO(values) && values.values != null;
  289. if (isObjectSyntax) {
  290. message = values.message;
  291. values = values.values;
  292. } else if (typeof values === 'number') {
  293. values = Array.prototype.slice.call(arguments);
  294. message = null;
  295. }
  296. if (utils.isPOJO(values)) {
  297. // TypeScript numeric enums produce objects with reverse
  298. // mappings, e.g. { 0: 'Zero', 1: 'One', Zero: 0, One: 1 }.
  299. // Object.values on that will yield ['Zero','One',0,1].
  300. // For Number schema enums we only want the numeric values,
  301. // otherwise casting the name strings to Number will throw.
  302. const keys = Object.keys(values).sort();
  303. const objVals = Object.values(values).sort();
  304. // If keys and values are equal and half of values are numbers,
  305. // this is likely a TS enum with reverse mapping, so use only the numbers.
  306. if (
  307. keys.length === objVals.length &&
  308. keys.every((k, i) => k === String(objVals[i])) &&
  309. objVals.filter(v => typeof v === 'number').length === Math.floor(objVals.length / 2)
  310. ) {
  311. values = objVals.filter(v => typeof v === 'number');
  312. } else {
  313. // Avoid sorting the values to preserve user-specified order
  314. values = Object.values(values);
  315. }
  316. }
  317. message = message || MongooseError.messages.Number.enum;
  318. }
  319. message = message == null ? MongooseError.messages.Number.enum : message;
  320. for (const value of values) {
  321. if (value !== undefined) {
  322. this.enumValues.push(this.cast(value));
  323. }
  324. }
  325. const vals = this.enumValues;
  326. this.enumValidator = v => v == null || vals.indexOf(v) !== -1;
  327. this.validators.push({
  328. validator: this.enumValidator,
  329. message: message,
  330. type: 'enum',
  331. enumValues: vals
  332. });
  333. return this;
  334. };
  335. /**
  336. * Casts to number
  337. *
  338. * @param {Object} value value to cast
  339. * @param {Document} doc document that triggers the casting
  340. * @param {Boolean} init
  341. * @api private
  342. */
  343. SchemaNumber.prototype.cast = function(value, doc, init, prev, options) {
  344. if (typeof value !== 'number' && SchemaType._isRef(this, value, doc, init)) {
  345. if (value == null || utils.isNonBuiltinObject(value)) {
  346. return this._castRef(value, doc, init, options);
  347. }
  348. }
  349. const val = value?._id !== undefined ?
  350. value._id : // documents
  351. value;
  352. let castNumber;
  353. if (typeof this._castFunction === 'function') {
  354. castNumber = this._castFunction;
  355. } else if (typeof this.constructor.cast === 'function') {
  356. castNumber = this.constructor.cast();
  357. } else {
  358. castNumber = SchemaNumber.cast();
  359. }
  360. try {
  361. return castNumber(val);
  362. } catch (err) {
  363. throw new CastError('Number', val, this.path, err, this);
  364. }
  365. };
  366. /*!
  367. * ignore
  368. */
  369. function handleSingle(val) {
  370. return this.cast(val);
  371. }
  372. function handleArray(val) {
  373. const _this = this;
  374. if (!Array.isArray(val)) {
  375. return [this.cast(val)];
  376. }
  377. return val.map(function(m) {
  378. return _this.cast(m);
  379. });
  380. }
  381. const $conditionalHandlers = {
  382. ...SchemaType.prototype.$conditionalHandlers,
  383. $bitsAllClear: handleBitwiseOperator,
  384. $bitsAnyClear: handleBitwiseOperator,
  385. $bitsAllSet: handleBitwiseOperator,
  386. $bitsAnySet: handleBitwiseOperator,
  387. $gt: handleSingle,
  388. $gte: handleSingle,
  389. $lt: handleSingle,
  390. $lte: handleSingle,
  391. $mod: handleArray
  392. };
  393. /**
  394. * Contains the handlers for different query operators for this schema type.
  395. * For example, `$conditionalHandlers.$gte` is the function Mongoose calls to cast `$gte` filter operators.
  396. *
  397. * @property $conditionalHandlers
  398. * @memberOf SchemaNumber
  399. * @instance
  400. * @api public
  401. */
  402. Object.defineProperty(SchemaNumber.prototype, '$conditionalHandlers', {
  403. enumerable: false,
  404. value: $conditionalHandlers
  405. });
  406. /**
  407. * Casts contents for queries.
  408. *
  409. * @param {String} $conditional
  410. * @param {any} [value]
  411. * @api private
  412. */
  413. SchemaNumber.prototype.castForQuery = function($conditional, val, context) {
  414. let handler;
  415. if ($conditional != null) {
  416. handler = this.$conditionalHandlers[$conditional];
  417. if (!handler) {
  418. throw new CastError('number', val, this.path, null, this);
  419. }
  420. return handler.call(this, val, context);
  421. }
  422. try {
  423. val = this.applySetters(val, context);
  424. } catch (err) {
  425. if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
  426. err.path = this.$fullPath;
  427. }
  428. throw err;
  429. }
  430. return val;
  431. };
  432. /**
  433. * Returns this schema type's representation in a JSON schema.
  434. *
  435. * @param [options]
  436. * @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
  437. * @returns {Object} JSON schema properties
  438. */
  439. SchemaNumber.prototype.toJSONSchema = function toJSONSchema(options) {
  440. const isRequired = (this.options.required && typeof this.options.required !== 'function') || this.path === '_id';
  441. return createJSONSchemaTypeDefinition('number', 'number', options?.useBsonType, isRequired);
  442. };
  443. /*!
  444. * Module exports.
  445. */
  446. module.exports = SchemaNumber;