string.js 18 KB

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