string.js 20 KB

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