string.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /*!
  2. * Module dependencies.
  3. */
  4. var SchemaType = require('../schematype');
  5. var CastError = SchemaType.CastError;
  6. var MongooseError = require('../error');
  7. var utils = require('../utils');
  8. var Document;
  9. /**
  10. * String SchemaType constructor.
  11. *
  12. * @param {String} key
  13. * @param {Object} options
  14. * @inherits SchemaType
  15. * @api public
  16. */
  17. function SchemaString(key, options) {
  18. this.enumValues = [];
  19. this.regExp = null;
  20. SchemaType.call(this, key, options, 'String');
  21. }
  22. /**
  23. * This schema type's name, to defend against minifiers that mangle
  24. * function names.
  25. *
  26. * @api public
  27. */
  28. SchemaString.schemaName = 'String';
  29. /*!
  30. * Inherits from SchemaType.
  31. */
  32. SchemaString.prototype = Object.create(SchemaType.prototype);
  33. SchemaString.prototype.constructor = SchemaString;
  34. /**
  35. * Adds an enum validator
  36. *
  37. * ####Example:
  38. *
  39. * var states = ['opening', 'open', 'closing', 'closed']
  40. * var s = new Schema({ state: { type: String, enum: states }})
  41. * var M = db.model('M', s)
  42. * var m = new M({ state: 'invalid' })
  43. * m.save(function (err) {
  44. * console.error(String(err)) // ValidationError: `invalid` is not a valid enum value for path `state`.
  45. * m.state = 'open'
  46. * m.save(callback) // success
  47. * })
  48. *
  49. * // or with custom error messages
  50. * var enum = {
  51. * values: ['opening', 'open', 'closing', 'closed'],
  52. * message: 'enum validator failed for path `{PATH}` with value `{VALUE}`'
  53. * }
  54. * var s = new Schema({ state: { type: String, enum: enum })
  55. * var M = db.model('M', s)
  56. * var m = new M({ state: 'invalid' })
  57. * m.save(function (err) {
  58. * console.error(String(err)) // ValidationError: enum validator failed for path `state` with value `invalid`
  59. * m.state = 'open'
  60. * m.save(callback) // success
  61. * })
  62. *
  63. * @param {String|Object} [args...] enumeration values
  64. * @return {SchemaType} this
  65. * @see Customized Error Messages #error_messages_MongooseError-messages
  66. * @api public
  67. */
  68. SchemaString.prototype.enum = function() {
  69. if (this.enumValidator) {
  70. this.validators = this.validators.filter(function(v) {
  71. return v.validator !== this.enumValidator;
  72. }, this);
  73. this.enumValidator = false;
  74. }
  75. if (arguments[0] === void 0 || arguments[0] === false) {
  76. return this;
  77. }
  78. var values;
  79. var errorMessage;
  80. if (utils.isObject(arguments[0])) {
  81. values = arguments[0].values;
  82. errorMessage = arguments[0].message;
  83. } else {
  84. values = arguments;
  85. errorMessage = MongooseError.messages.String.enum;
  86. }
  87. for (var i = 0; i < values.length; i++) {
  88. if (undefined !== values[i]) {
  89. this.enumValues.push(this.cast(values[i]));
  90. }
  91. }
  92. var vals = this.enumValues;
  93. this.enumValidator = function(v) {
  94. return undefined === v || ~vals.indexOf(v);
  95. };
  96. this.validators.push({
  97. validator: this.enumValidator,
  98. message: errorMessage,
  99. type: 'enum',
  100. enumValues: vals
  101. });
  102. return this;
  103. };
  104. /**
  105. * Adds a lowercase setter.
  106. *
  107. * ####Example:
  108. *
  109. * var s = new Schema({ email: { type: String, lowercase: true }})
  110. * var M = db.model('M', s);
  111. * var m = new M({ email: 'SomeEmail@example.COM' });
  112. * console.log(m.email) // someemail@example.com
  113. *
  114. * @api public
  115. * @return {SchemaType} this
  116. */
  117. SchemaString.prototype.lowercase = function(shouldApply) {
  118. if (arguments.length > 0 && !shouldApply) {
  119. return this;
  120. }
  121. return this.set(function(v, self) {
  122. if (typeof v !== 'string') {
  123. v = self.cast(v);
  124. }
  125. if (v) {
  126. return v.toLowerCase();
  127. }
  128. return v;
  129. });
  130. };
  131. /**
  132. * Adds an uppercase setter.
  133. *
  134. * ####Example:
  135. *
  136. * var s = new Schema({ caps: { type: String, uppercase: true }})
  137. * var M = db.model('M', s);
  138. * var m = new M({ caps: 'an example' });
  139. * console.log(m.caps) // AN EXAMPLE
  140. *
  141. * @api public
  142. * @return {SchemaType} this
  143. */
  144. SchemaString.prototype.uppercase = function(shouldApply) {
  145. if (arguments.length > 0 && !shouldApply) {
  146. return this;
  147. }
  148. return this.set(function(v, self) {
  149. if (typeof v !== 'string') {
  150. v = self.cast(v);
  151. }
  152. if (v) {
  153. return v.toUpperCase();
  154. }
  155. return v;
  156. });
  157. };
  158. /**
  159. * Adds a trim setter.
  160. *
  161. * The string value will be trimmed when set.
  162. *
  163. * ####Example:
  164. *
  165. * var s = new Schema({ name: { type: String, trim: true }})
  166. * var M = db.model('M', s)
  167. * var string = ' some name '
  168. * console.log(string.length) // 11
  169. * var m = new M({ name: string })
  170. * console.log(m.name.length) // 9
  171. *
  172. * @api public
  173. * @return {SchemaType} this
  174. */
  175. SchemaString.prototype.trim = function(shouldTrim) {
  176. if (arguments.length > 0 && !shouldTrim) {
  177. return this;
  178. }
  179. return this.set(function(v, self) {
  180. if (typeof v !== 'string') {
  181. v = self.cast(v);
  182. }
  183. if (v) {
  184. return v.trim();
  185. }
  186. return v;
  187. });
  188. };
  189. /**
  190. * Sets a minimum length validator.
  191. *
  192. * ####Example:
  193. *
  194. * var schema = new Schema({ postalCode: { type: String, minlength: 5 })
  195. * var Address = db.model('Address', schema)
  196. * var address = new Address({ postalCode: '9512' })
  197. * address.save(function (err) {
  198. * console.error(err) // validator error
  199. * address.postalCode = '95125';
  200. * address.save() // success
  201. * })
  202. *
  203. * // custom error messages
  204. * // We can also use the special {MINLENGTH} token which will be replaced with the minimum allowed length
  205. * var minlength = [5, 'The value of path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'];
  206. * var schema = new Schema({ postalCode: { type: String, minlength: minlength })
  207. * var Address = mongoose.model('Address', schema);
  208. * var address = new Address({ postalCode: '9512' });
  209. * address.validate(function (err) {
  210. * console.log(String(err)) // ValidationError: The value of path `postalCode` (`9512`) is shorter than the minimum length (5).
  211. * })
  212. *
  213. * @param {Number} value minimum string length
  214. * @param {String} [message] optional custom error message
  215. * @return {SchemaType} this
  216. * @see Customized Error Messages #error_messages_MongooseError-messages
  217. * @api public
  218. */
  219. SchemaString.prototype.minlength = function(value, message) {
  220. if (this.minlengthValidator) {
  221. this.validators = this.validators.filter(function(v) {
  222. return v.validator !== this.minlengthValidator;
  223. }, this);
  224. }
  225. if (value !== null && value !== undefined) {
  226. var msg = message || MongooseError.messages.String.minlength;
  227. msg = msg.replace(/{MINLENGTH}/, value);
  228. this.validators.push({
  229. validator: this.minlengthValidator = function(v) {
  230. return v === null || v.length >= value;
  231. },
  232. message: msg,
  233. type: 'minlength',
  234. minlength: value
  235. });
  236. }
  237. return this;
  238. };
  239. /**
  240. * Sets a maximum length validator.
  241. *
  242. * ####Example:
  243. *
  244. * var schema = new Schema({ postalCode: { type: String, maxlength: 9 })
  245. * var Address = db.model('Address', schema)
  246. * var address = new Address({ postalCode: '9512512345' })
  247. * address.save(function (err) {
  248. * console.error(err) // validator error
  249. * address.postalCode = '95125';
  250. * address.save() // success
  251. * })
  252. *
  253. * // custom error messages
  254. * // We can also use the special {MAXLENGTH} token which will be replaced with the maximum allowed length
  255. * var maxlength = [9, 'The value of path `{PATH}` (`{VALUE}`) exceeds the maximum allowed length ({MAXLENGTH}).'];
  256. * var schema = new Schema({ postalCode: { type: String, maxlength: maxlength })
  257. * var Address = mongoose.model('Address', schema);
  258. * var address = new Address({ postalCode: '9512512345' });
  259. * address.validate(function (err) {
  260. * console.log(String(err)) // ValidationError: The value of path `postalCode` (`9512512345`) exceeds the maximum allowed length (9).
  261. * })
  262. *
  263. * @param {Number} value maximum string length
  264. * @param {String} [message] optional custom error message
  265. * @return {SchemaType} this
  266. * @see Customized Error Messages #error_messages_MongooseError-messages
  267. * @api public
  268. */
  269. SchemaString.prototype.maxlength = function(value, message) {
  270. if (this.maxlengthValidator) {
  271. this.validators = this.validators.filter(function(v) {
  272. return v.validator !== this.maxlengthValidator;
  273. }, this);
  274. }
  275. if (value !== null && value !== undefined) {
  276. var msg = message || MongooseError.messages.String.maxlength;
  277. msg = msg.replace(/{MAXLENGTH}/, value);
  278. this.validators.push({
  279. validator: this.maxlengthValidator = function(v) {
  280. return v === null || v.length <= value;
  281. },
  282. message: msg,
  283. type: 'maxlength',
  284. maxlength: value
  285. });
  286. }
  287. return this;
  288. };
  289. /**
  290. * Sets a regexp validator.
  291. *
  292. * Any value that does not pass `regExp`.test(val) will fail validation.
  293. *
  294. * ####Example:
  295. *
  296. * var s = new Schema({ name: { type: String, match: /^a/ }})
  297. * var M = db.model('M', s)
  298. * var m = new M({ name: 'I am invalid' })
  299. * m.validate(function (err) {
  300. * console.error(String(err)) // "ValidationError: Path `name` is invalid (I am invalid)."
  301. * m.name = 'apples'
  302. * m.validate(function (err) {
  303. * assert.ok(err) // success
  304. * })
  305. * })
  306. *
  307. * // using a custom error message
  308. * var match = [ /\.html$/, "That file doesn't end in .html ({VALUE})" ];
  309. * var s = new Schema({ file: { type: String, match: match }})
  310. * var M = db.model('M', s);
  311. * var m = new M({ file: 'invalid' });
  312. * m.validate(function (err) {
  313. * console.log(String(err)) // "ValidationError: That file doesn't end in .html (invalid)"
  314. * })
  315. *
  316. * Empty strings, `undefined`, and `null` values always pass the match validator. If you require these values, enable the `required` validator also.
  317. *
  318. * var s = new Schema({ name: { type: String, match: /^a/, required: true }})
  319. *
  320. * @param {RegExp} regExp regular expression to test against
  321. * @param {String} [message] optional custom error message
  322. * @return {SchemaType} this
  323. * @see Customized Error Messages #error_messages_MongooseError-messages
  324. * @api public
  325. */
  326. SchemaString.prototype.match = function match(regExp, message) {
  327. // yes, we allow multiple match validators
  328. var msg = message || MongooseError.messages.String.match;
  329. var matchValidator = function(v) {
  330. if (!regExp) {
  331. return false;
  332. }
  333. var ret = ((v != null && v !== '')
  334. ? regExp.test(v)
  335. : true);
  336. return ret;
  337. };
  338. this.validators.push({
  339. validator: matchValidator,
  340. message: msg,
  341. type: 'regexp',
  342. regexp: regExp
  343. });
  344. return this;
  345. };
  346. /**
  347. * Check if the given value satisfies a required validator.
  348. *
  349. * @param {Any} value
  350. * @param {Document} doc
  351. * @return {Boolean}
  352. * @api public
  353. */
  354. SchemaString.prototype.checkRequired = function checkRequired(value, doc) {
  355. if (SchemaType._isRef(this, value, doc, true)) {
  356. return !!value;
  357. }
  358. return (value instanceof String || typeof value === 'string') && value.length;
  359. };
  360. /**
  361. * Casts to String
  362. *
  363. * @api private
  364. */
  365. SchemaString.prototype.cast = function(value, doc, init) {
  366. if (SchemaType._isRef(this, value, doc, init)) {
  367. // wait! we may need to cast this to a document
  368. if (value === null || value === undefined) {
  369. return value;
  370. }
  371. // lazy load
  372. Document || (Document = require('./../document'));
  373. if (value instanceof Document) {
  374. value.$__.wasPopulated = true;
  375. return value;
  376. }
  377. // setting a populated path
  378. if (typeof value === 'string') {
  379. return value;
  380. } else if (Buffer.isBuffer(value) || !utils.isObject(value)) {
  381. throw new CastError('string', value, this.path);
  382. }
  383. // Handle the case where user directly sets a populated
  384. // path to a plain object; cast to the Model used in
  385. // the population query.
  386. var path = doc.$__fullPath(this.path);
  387. var owner = doc.ownerDocument ? doc.ownerDocument() : doc;
  388. var pop = owner.populated(path, true);
  389. var ret = new pop.options.model(value);
  390. ret.$__.wasPopulated = true;
  391. return ret;
  392. }
  393. // If null or undefined
  394. if (value === null || value === undefined) {
  395. return value;
  396. }
  397. if (typeof value !== 'undefined') {
  398. // handle documents being passed
  399. if (value._id && typeof value._id === 'string') {
  400. return value._id;
  401. }
  402. // Re: gh-647 and gh-3030, we're ok with casting using `toString()`
  403. // **unless** its the default Object.toString, because "[object Object]"
  404. // doesn't really qualify as useful data
  405. if (value.toString && value.toString !== Object.prototype.toString) {
  406. return value.toString();
  407. }
  408. }
  409. throw new CastError('string', value, this.path);
  410. };
  411. /*!
  412. * ignore
  413. */
  414. function handleSingle(val) {
  415. return this.castForQuery(val);
  416. }
  417. function handleArray(val) {
  418. var _this = this;
  419. if (!Array.isArray(val)) {
  420. return [this.castForQuery(val)];
  421. }
  422. return val.map(function(m) {
  423. return _this.castForQuery(m);
  424. });
  425. }
  426. SchemaString.prototype.$conditionalHandlers =
  427. utils.options(SchemaType.prototype.$conditionalHandlers, {
  428. $all: handleArray,
  429. $gt: handleSingle,
  430. $gte: handleSingle,
  431. $lt: handleSingle,
  432. $lte: handleSingle,
  433. $options: handleSingle,
  434. $regex: handleSingle,
  435. $not: handleSingle
  436. });
  437. /**
  438. * Casts contents for queries.
  439. *
  440. * @param {String} $conditional
  441. * @param {any} [val]
  442. * @api private
  443. */
  444. SchemaString.prototype.castForQuery = function($conditional, val) {
  445. var handler;
  446. if (arguments.length === 2) {
  447. handler = this.$conditionalHandlers[$conditional];
  448. if (!handler) {
  449. throw new Error('Can\'t use ' + $conditional + ' with String.');
  450. }
  451. return handler.call(this, val);
  452. }
  453. val = $conditional;
  454. if (Object.prototype.toString.call(val) === '[object RegExp]') {
  455. return val;
  456. }
  457. return this.cast(val);
  458. };
  459. /*!
  460. * Module exports.
  461. */
  462. module.exports = SchemaString;