string.js 16 KB

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