string.js 18 KB

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