string.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const SchemaType = require('../schematype');
  6. const MongooseError = require('../error/index');
  7. const SchemaStringOptions = require('../options/SchemaStringOptions');
  8. const castString = require('../cast/string');
  9. const utils = require('../utils');
  10. const CastError = SchemaType.CastError;
  11. /**
  12. * String SchemaType constructor.
  13. *
  14. * @param {String} key
  15. * @param {Object} options
  16. * @inherits SchemaType
  17. * @api public
  18. */
  19. function SchemaString(key, options) {
  20. this.enumValues = [];
  21. this.regExp = null;
  22. SchemaType.call(this, key, options, 'String');
  23. }
  24. /**
  25. * This schema type's name, to defend against minifiers that mangle
  26. * function names.
  27. *
  28. * @api public
  29. */
  30. SchemaString.schemaName = 'String';
  31. SchemaString.defaultOptions = {};
  32. /*!
  33. * Inherits from SchemaType.
  34. */
  35. SchemaString.prototype = Object.create(SchemaType.prototype);
  36. SchemaString.prototype.constructor = SchemaString;
  37. Object.defineProperty(SchemaString.prototype, 'OptionsConstructor', {
  38. configurable: false,
  39. enumerable: false,
  40. writable: false,
  41. value: SchemaStringOptions
  42. });
  43. /*!
  44. * ignore
  45. */
  46. SchemaString._cast = castString;
  47. /**
  48. * Get/set the function used to cast arbitrary values to strings.
  49. *
  50. * ####Example:
  51. *
  52. * // Throw an error if you pass in an object. Normally, Mongoose allows
  53. * // objects with custom `toString()` functions.
  54. * const original = mongoose.Schema.Types.String.cast();
  55. * mongoose.Schema.Types.String.cast(v => {
  56. * assert.ok(v == null || typeof v !== 'object');
  57. * return original(v);
  58. * });
  59. *
  60. * // Or disable casting entirely
  61. * mongoose.Schema.Types.String.cast(false);
  62. *
  63. * @param {Function} caster
  64. * @return {Function}
  65. * @function get
  66. * @static
  67. * @api public
  68. */
  69. SchemaString.cast = function cast(caster) {
  70. if (arguments.length === 0) {
  71. return this._cast;
  72. }
  73. if (caster === false) {
  74. caster = this._defaultCaster;
  75. }
  76. this._cast = caster;
  77. return this._cast;
  78. };
  79. /*!
  80. * ignore
  81. */
  82. SchemaString._defaultCaster = v => {
  83. if (v != null && typeof v !== 'string') {
  84. throw new Error();
  85. }
  86. return v;
  87. };
  88. /**
  89. * Attaches a getter for all String instances.
  90. *
  91. * ####Example:
  92. *
  93. * // Make all numbers round down
  94. * mongoose.Schema.String.get(v => v.toLowerCase());
  95. *
  96. * const Model = mongoose.model('Test', new Schema({ test: String }));
  97. * new Model({ test: 'FOO' }).test; // 'foo'
  98. *
  99. * @param {Function} getter
  100. * @return {this}
  101. * @function get
  102. * @static
  103. * @api public
  104. */
  105. SchemaString.get = SchemaType.get;
  106. /**
  107. * Sets a default option for all String instances.
  108. *
  109. * ####Example:
  110. *
  111. * // Make all strings have option `trim` equal to true.
  112. * mongoose.Schema.String.set('trim', true);
  113. *
  114. * const User = mongoose.model('User', new Schema({ name: String }));
  115. * new User({ name: ' John Doe ' }).name; // 'John Doe'
  116. *
  117. * @param {String} option - The option you'd like to set the value for
  118. * @param {*} value - value for option
  119. * @return {undefined}
  120. * @function set
  121. * @static
  122. * @api public
  123. */
  124. SchemaString.set = SchemaType.set;
  125. /*!
  126. * ignore
  127. */
  128. SchemaString._checkRequired = v => (v instanceof String || typeof v === 'string') && v.length;
  129. /**
  130. * Override the function the required validator uses to check whether a string
  131. * passes the `required` check.
  132. *
  133. * ####Example:
  134. *
  135. * // Allow empty strings to pass `required` check
  136. * mongoose.Schema.Types.String.checkRequired(v => v != null);
  137. *
  138. * const M = mongoose.model({ str: { type: String, required: true } });
  139. * new M({ str: '' }).validateSync(); // `null`, validation passes!
  140. *
  141. * @param {Function} fn
  142. * @return {Function}
  143. * @function checkRequired
  144. * @static
  145. * @api public
  146. */
  147. SchemaString.checkRequired = SchemaType.checkRequired;
  148. /**
  149. * Adds an enum validator
  150. *
  151. * ####Example:
  152. *
  153. * const states = ['opening', 'open', 'closing', 'closed']
  154. * const s = new Schema({ state: { type: String, enum: states }})
  155. * const M = db.model('M', s)
  156. * const m = new M({ state: 'invalid' })
  157. * m.save(function (err) {
  158. * console.error(String(err)) // ValidationError: `invalid` is not a valid enum value for path `state`.
  159. * m.state = 'open'
  160. * m.save(callback) // success
  161. * })
  162. *
  163. * // or with custom error messages
  164. * const enum = {
  165. * values: ['opening', 'open', 'closing', 'closed'],
  166. * message: 'enum validator failed for path `{PATH}` with value `{VALUE}`'
  167. * }
  168. * const s = new Schema({ state: { type: String, enum: enum })
  169. * const M = db.model('M', s)
  170. * const m = new M({ state: 'invalid' })
  171. * m.save(function (err) {
  172. * console.error(String(err)) // ValidationError: enum validator failed for path `state` with value `invalid`
  173. * m.state = 'open'
  174. * m.save(callback) // success
  175. * })
  176. *
  177. * @param {String|Object} [args...] enumeration values
  178. * @return {SchemaType} this
  179. * @see Customized Error Messages #error_messages_MongooseError-messages
  180. * @api public
  181. */
  182. SchemaString.prototype.enum = function() {
  183. if (this.enumValidator) {
  184. this.validators = this.validators.filter(function(v) {
  185. return v.validator !== this.enumValidator;
  186. }, this);
  187. this.enumValidator = false;
  188. }
  189. if (arguments[0] === void 0 || arguments[0] === false) {
  190. return this;
  191. }
  192. let values;
  193. let errorMessage;
  194. if (utils.isObject(arguments[0])) {
  195. if (Array.isArray(arguments[0].values)) {
  196. values = arguments[0].values;
  197. errorMessage = arguments[0].message;
  198. } else {
  199. values = utils.object.vals(arguments[0]);
  200. errorMessage = MongooseError.messages.String.enum;
  201. }
  202. } else {
  203. values = arguments;
  204. errorMessage = MongooseError.messages.String.enum;
  205. }
  206. for (const value of values) {
  207. if (value !== undefined) {
  208. this.enumValues.push(this.cast(value));
  209. }
  210. }
  211. const vals = this.enumValues;
  212. this.enumValidator = function(v) {
  213. return undefined === v || ~vals.indexOf(v);
  214. };
  215. this.validators.push({
  216. validator: this.enumValidator,
  217. message: errorMessage,
  218. type: 'enum',
  219. enumValues: vals
  220. });
  221. return this;
  222. };
  223. /**
  224. * Adds a lowercase [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set).
  225. *
  226. * ####Example:
  227. *
  228. * const s = new Schema({ email: { type: String, lowercase: true }})
  229. * const M = db.model('M', s);
  230. * const m = new M({ email: 'SomeEmail@example.COM' });
  231. * console.log(m.email) // someemail@example.com
  232. * M.find({ email: 'SomeEmail@example.com' }); // Queries by 'someemail@example.com'
  233. *
  234. * Note that `lowercase` does **not** affect regular expression queries:
  235. *
  236. * ####Example:
  237. * // Still queries for documents whose `email` matches the regular
  238. * // expression /SomeEmail/. Mongoose does **not** convert the RegExp
  239. * // to lowercase.
  240. * M.find({ email: /SomeEmail/ });
  241. *
  242. * @api public
  243. * @return {SchemaType} this
  244. */
  245. SchemaString.prototype.lowercase = function(shouldApply) {
  246. if (arguments.length > 0 && !shouldApply) {
  247. return this;
  248. }
  249. return this.set(v => {
  250. if (typeof v !== 'string') {
  251. v = this.cast(v);
  252. }
  253. if (v) {
  254. return v.toLowerCase();
  255. }
  256. return v;
  257. });
  258. };
  259. /**
  260. * Adds an uppercase [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set).
  261. *
  262. * ####Example:
  263. *
  264. * const s = new Schema({ caps: { type: String, uppercase: true }})
  265. * const M = db.model('M', s);
  266. * const m = new M({ caps: 'an example' });
  267. * console.log(m.caps) // AN EXAMPLE
  268. * M.find({ caps: 'an example' }) // Matches documents where caps = 'AN EXAMPLE'
  269. *
  270. * Note that `uppercase` does **not** affect regular expression queries:
  271. *
  272. * ####Example:
  273. * // Mongoose does **not** convert the RegExp to uppercase.
  274. * M.find({ email: /an example/ });
  275. *
  276. * @api public
  277. * @return {SchemaType} this
  278. */
  279. SchemaString.prototype.uppercase = function(shouldApply) {
  280. if (arguments.length > 0 && !shouldApply) {
  281. return this;
  282. }
  283. return this.set(v => {
  284. if (typeof v !== 'string') {
  285. v = this.cast(v);
  286. }
  287. if (v) {
  288. return v.toUpperCase();
  289. }
  290. return v;
  291. });
  292. };
  293. /**
  294. * Adds a trim [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set).
  295. *
  296. * The string value will be [trimmed](https://masteringjs.io/tutorials/fundamentals/trim-string) when set.
  297. *
  298. * ####Example:
  299. *
  300. * const s = new Schema({ name: { type: String, trim: true }});
  301. * const M = db.model('M', s);
  302. * const string = ' some name ';
  303. * console.log(string.length); // 11
  304. * const m = new M({ name: string });
  305. * console.log(m.name.length); // 9
  306. *
  307. * // Equivalent to `findOne({ name: string.trim() })`
  308. * M.findOne({ name: string });
  309. *
  310. * Note that `trim` does **not** affect regular expression queries:
  311. *
  312. * ####Example:
  313. * // Mongoose does **not** trim whitespace from the RegExp.
  314. * M.find({ name: / some name / });
  315. *
  316. * @api public
  317. * @return {SchemaType} this
  318. */
  319. SchemaString.prototype.trim = function(shouldTrim) {
  320. if (arguments.length > 0 && !shouldTrim) {
  321. return this;
  322. }
  323. return this.set(v => {
  324. if (typeof v !== 'string') {
  325. v = this.cast(v);
  326. }
  327. if (v) {
  328. return v.trim();
  329. }
  330. return v;
  331. });
  332. };
  333. /**
  334. * Sets a minimum length validator.
  335. *
  336. * ####Example:
  337. *
  338. * const schema = new Schema({ postalCode: { type: String, minlength: 5 })
  339. * const Address = db.model('Address', schema)
  340. * const address = new Address({ postalCode: '9512' })
  341. * address.save(function (err) {
  342. * console.error(err) // validator error
  343. * address.postalCode = '95125';
  344. * address.save() // success
  345. * })
  346. *
  347. * // custom error messages
  348. * // We can also use the special {MINLENGTH} token which will be replaced with the minimum allowed length
  349. * const minlength = [5, 'The value of path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'];
  350. * const schema = new Schema({ postalCode: { type: String, minlength: minlength })
  351. * const Address = mongoose.model('Address', schema);
  352. * const address = new Address({ postalCode: '9512' });
  353. * address.validate(function (err) {
  354. * console.log(String(err)) // ValidationError: The value of path `postalCode` (`9512`) is shorter than the minimum length (5).
  355. * })
  356. *
  357. * @param {Number} value minimum string length
  358. * @param {String} [message] optional custom error message
  359. * @return {SchemaType} this
  360. * @see Customized Error Messages #error_messages_MongooseError-messages
  361. * @api public
  362. */
  363. SchemaString.prototype.minlength = function(value, message) {
  364. if (this.minlengthValidator) {
  365. this.validators = this.validators.filter(function(v) {
  366. return v.validator !== this.minlengthValidator;
  367. }, this);
  368. }
  369. if (value !== null && value !== undefined) {
  370. let msg = message || MongooseError.messages.String.minlength;
  371. msg = msg.replace(/{MINLENGTH}/, value);
  372. this.validators.push({
  373. validator: this.minlengthValidator = function(v) {
  374. return v === null || v.length >= value;
  375. },
  376. message: msg,
  377. type: 'minlength',
  378. minlength: value
  379. });
  380. }
  381. return this;
  382. };
  383. SchemaString.prototype.minLength = SchemaString.prototype.minlength;
  384. /**
  385. * Sets a maximum length validator.
  386. *
  387. * ####Example:
  388. *
  389. * const schema = new Schema({ postalCode: { type: String, maxlength: 9 })
  390. * const Address = db.model('Address', schema)
  391. * const address = new Address({ postalCode: '9512512345' })
  392. * address.save(function (err) {
  393. * console.error(err) // validator error
  394. * address.postalCode = '95125';
  395. * address.save() // success
  396. * })
  397. *
  398. * // custom error messages
  399. * // We can also use the special {MAXLENGTH} token which will be replaced with the maximum allowed length
  400. * const maxlength = [9, 'The value of path `{PATH}` (`{VALUE}`) exceeds the maximum allowed length ({MAXLENGTH}).'];
  401. * const schema = new Schema({ postalCode: { type: String, maxlength: maxlength })
  402. * const Address = mongoose.model('Address', schema);
  403. * const address = new Address({ postalCode: '9512512345' });
  404. * address.validate(function (err) {
  405. * console.log(String(err)) // ValidationError: The value of path `postalCode` (`9512512345`) exceeds the maximum allowed length (9).
  406. * })
  407. *
  408. * @param {Number} value maximum string length
  409. * @param {String} [message] optional custom error message
  410. * @return {SchemaType} this
  411. * @see Customized Error Messages #error_messages_MongooseError-messages
  412. * @api public
  413. */
  414. SchemaString.prototype.maxlength = function(value, message) {
  415. if (this.maxlengthValidator) {
  416. this.validators = this.validators.filter(function(v) {
  417. return v.validator !== this.maxlengthValidator;
  418. }, this);
  419. }
  420. if (value !== null && value !== undefined) {
  421. let msg = message || MongooseError.messages.String.maxlength;
  422. msg = msg.replace(/{MAXLENGTH}/, value);
  423. this.validators.push({
  424. validator: this.maxlengthValidator = function(v) {
  425. return v === null || v.length <= value;
  426. },
  427. message: msg,
  428. type: 'maxlength',
  429. maxlength: value
  430. });
  431. }
  432. return this;
  433. };
  434. SchemaString.prototype.maxLength = SchemaString.prototype.maxlength;
  435. /**
  436. * Sets a regexp validator.
  437. *
  438. * Any value that does not pass `regExp`.test(val) will fail validation.
  439. *
  440. * ####Example:
  441. *
  442. * const s = new Schema({ name: { type: String, match: /^a/ }})
  443. * const M = db.model('M', s)
  444. * const m = new M({ name: 'I am invalid' })
  445. * m.validate(function (err) {
  446. * console.error(String(err)) // "ValidationError: Path `name` is invalid (I am invalid)."
  447. * m.name = 'apples'
  448. * m.validate(function (err) {
  449. * assert.ok(err) // success
  450. * })
  451. * })
  452. *
  453. * // using a custom error message
  454. * const match = [ /\.html$/, "That file doesn't end in .html ({VALUE})" ];
  455. * const s = new Schema({ file: { type: String, match: match }})
  456. * const M = db.model('M', s);
  457. * const m = new M({ file: 'invalid' });
  458. * m.validate(function (err) {
  459. * console.log(String(err)) // "ValidationError: That file doesn't end in .html (invalid)"
  460. * })
  461. *
  462. * Empty strings, `undefined`, and `null` values always pass the match validator. If you require these values, enable the `required` validator also.
  463. *
  464. * const s = new Schema({ name: { type: String, match: /^a/, required: true }})
  465. *
  466. * @param {RegExp} regExp regular expression to test against
  467. * @param {String} [message] optional custom error message
  468. * @return {SchemaType} this
  469. * @see Customized Error Messages #error_messages_MongooseError-messages
  470. * @api public
  471. */
  472. SchemaString.prototype.match = function match(regExp, message) {
  473. // yes, we allow multiple match validators
  474. const msg = message || MongooseError.messages.String.match;
  475. const matchValidator = function(v) {
  476. if (!regExp) {
  477. return false;
  478. }
  479. // In case RegExp happens to have `/g` flag set, we need to reset the
  480. // `lastIndex`, otherwise `match` will intermittently fail.
  481. regExp.lastIndex = 0;
  482. const ret = ((v != null && v !== '')
  483. ? regExp.test(v)
  484. : true);
  485. return ret;
  486. };
  487. this.validators.push({
  488. validator: matchValidator,
  489. message: msg,
  490. type: 'regexp',
  491. regexp: regExp
  492. });
  493. return this;
  494. };
  495. /**
  496. * Check if the given value satisfies the `required` validator. The value is
  497. * considered valid if it is a string (that is, not `null` or `undefined`) and
  498. * has positive length. The `required` validator **will** fail for empty
  499. * strings.
  500. *
  501. * @param {Any} value
  502. * @param {Document} doc
  503. * @return {Boolean}
  504. * @api public
  505. */
  506. SchemaString.prototype.checkRequired = function checkRequired(value, doc) {
  507. if (SchemaType._isRef(this, value, doc, true)) {
  508. return !!value;
  509. }
  510. // `require('util').inherits()` does **not** copy static properties, and
  511. // plugins like mongoose-float use `inherits()` for pre-ES6.
  512. const _checkRequired = typeof this.constructor.checkRequired == 'function' ?
  513. this.constructor.checkRequired() :
  514. SchemaString.checkRequired();
  515. return _checkRequired(value);
  516. };
  517. /**
  518. * Casts to String
  519. *
  520. * @api private
  521. */
  522. SchemaString.prototype.cast = function(value, doc, init) {
  523. if (SchemaType._isRef(this, value, doc, init)) {
  524. if (typeof value === 'string') {
  525. return value;
  526. }
  527. return this._castRef(value, doc, init);
  528. }
  529. let castString;
  530. if (typeof this._castFunction === 'function') {
  531. castString = this._castFunction;
  532. } else if (typeof this.constructor.cast === 'function') {
  533. castString = this.constructor.cast();
  534. } else {
  535. castString = SchemaString.cast();
  536. }
  537. try {
  538. return castString(value);
  539. } catch (error) {
  540. throw new CastError('string', value, this.path, null, this);
  541. }
  542. };
  543. /*!
  544. * ignore
  545. */
  546. function handleSingle(val) {
  547. return this.castForQuery(val);
  548. }
  549. function handleArray(val) {
  550. const _this = this;
  551. if (!Array.isArray(val)) {
  552. return [this.castForQuery(val)];
  553. }
  554. return val.map(function(m) {
  555. return _this.castForQuery(m);
  556. });
  557. }
  558. const $conditionalHandlers = utils.options(SchemaType.prototype.$conditionalHandlers, {
  559. $all: handleArray,
  560. $gt: handleSingle,
  561. $gte: handleSingle,
  562. $lt: handleSingle,
  563. $lte: handleSingle,
  564. $options: String,
  565. $regex: handleSingle,
  566. $not: handleSingle
  567. });
  568. Object.defineProperty(SchemaString.prototype, '$conditionalHandlers', {
  569. configurable: false,
  570. enumerable: false,
  571. writable: false,
  572. value: Object.freeze($conditionalHandlers)
  573. });
  574. /**
  575. * Casts contents for queries.
  576. *
  577. * @param {String} $conditional
  578. * @param {any} [val]
  579. * @api private
  580. */
  581. SchemaString.prototype.castForQuery = function($conditional, val) {
  582. let handler;
  583. if (arguments.length === 2) {
  584. handler = this.$conditionalHandlers[$conditional];
  585. if (!handler) {
  586. throw new Error('Can\'t use ' + $conditional + ' with String.');
  587. }
  588. return handler.call(this, val);
  589. }
  590. val = $conditional;
  591. if (Object.prototype.toString.call(val) === '[object RegExp]') {
  592. return val;
  593. }
  594. return this._castForQuery(val);
  595. };
  596. /*!
  597. * Module exports.
  598. */
  599. module.exports = SchemaString;