You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
13 KiB

  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'.split(' ')
  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 enu = {
  51. * values: 'opening open closing closed'.split(' '),
  52. * message: 'enum validator failed for path `{PATH}` with value `{VALUE}`'
  53. * }
  54. * var s = new Schema({ state: { type: String, enum: enu })
  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() {
  118. return this.set(function(v, self) {
  119. if (typeof v !== 'string') {
  120. v = self.cast(v);
  121. }
  122. if (v) {
  123. return v.toLowerCase();
  124. }
  125. return v;
  126. });
  127. };
  128. /**
  129. * Adds an uppercase setter.
  130. *
  131. * ####Example:
  132. *
  133. * var s = new Schema({ caps: { type: String, uppercase: true }})
  134. * var M = db.model('M', s);
  135. * var m = new M({ caps: 'an example' });
  136. * console.log(m.caps) // AN EXAMPLE
  137. *
  138. * @api public
  139. * @return {SchemaType} this
  140. */
  141. SchemaString.prototype.uppercase = function() {
  142. return this.set(function(v, self) {
  143. if (typeof v !== 'string') {
  144. v = self.cast(v);
  145. }
  146. if (v) {
  147. return v.toUpperCase();
  148. }
  149. return v;
  150. });
  151. };
  152. /**
  153. * Adds a trim setter.
  154. *
  155. * The string value will be trimmed when set.
  156. *
  157. * ####Example:
  158. *
  159. * var s = new Schema({ name: { type: String, trim: true }})
  160. * var M = db.model('M', s)
  161. * var string = ' some name '
  162. * console.log(string.length) // 11
  163. * var m = new M({ name: string })
  164. * console.log(m.name.length) // 9
  165. *
  166. * @api public
  167. * @return {SchemaType} this
  168. */
  169. SchemaString.prototype.trim = function() {
  170. return this.set(function(v, self) {
  171. if (typeof v !== 'string') {
  172. v = self.cast(v);
  173. }
  174. if (v && self.options.trim) {
  175. return v.trim();
  176. }
  177. return v;
  178. });
  179. };
  180. /**
  181. * Sets a minimum length validator.
  182. *
  183. * ####Example:
  184. *
  185. * var schema = new Schema({ postalCode: { type: String, minlength: 5 })
  186. * var Address = db.model('Address', schema)
  187. * var address = new Address({ postalCode: '9512' })
  188. * address.save(function (err) {
  189. * console.error(err) // validator error
  190. * address.postalCode = '95125';
  191. * address.save() // success
  192. * })
  193. *
  194. * // custom error messages
  195. * // We can also use the special {MINLENGTH} token which will be replaced with the minimum allowed length
  196. * var minlength = [5, 'The value of path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'];
  197. * var schema = new Schema({ postalCode: { type: String, minlength: minlength })
  198. * var Address = mongoose.model('Address', schema);
  199. * var address = new Address({ postalCode: '9512' });
  200. * address.validate(function (err) {
  201. * console.log(String(err)) // ValidationError: The value of path `postalCode` (`9512`) is shorter than the minimum length (5).
  202. * })
  203. *
  204. * @param {Number} value minimum string length
  205. * @param {String} [message] optional custom error message
  206. * @return {SchemaType} this
  207. * @see Customized Error Messages #error_messages_MongooseError-messages
  208. * @api public
  209. */
  210. SchemaString.prototype.minlength = function(value, message) {
  211. if (this.minlengthValidator) {
  212. this.validators = this.validators.filter(function(v) {
  213. return v.validator !== this.minlengthValidator;
  214. }, this);
  215. }
  216. if (value !== null && value !== undefined) {
  217. var msg = message || MongooseError.messages.String.minlength;
  218. msg = msg.replace(/{MINLENGTH}/, value);
  219. this.validators.push({
  220. validator: this.minlengthValidator = function(v) {
  221. return v === null || v.length >= value;
  222. },
  223. message: msg,
  224. type: 'minlength',
  225. minlength: value
  226. });
  227. }
  228. return this;
  229. };
  230. /**
  231. * Sets a maximum length validator.
  232. *
  233. * ####Example:
  234. *
  235. * var schema = new Schema({ postalCode: { type: String, maxlength: 9 })
  236. * var Address = db.model('Address', schema)
  237. * var address = new Address({ postalCode: '9512512345' })
  238. * address.save(function (err) {
  239. * console.error(err) // validator error
  240. * address.postalCode = '95125';
  241. * address.save() // success
  242. * })
  243. *
  244. * // custom error messages
  245. * // We can also use the special {MAXLENGTH} token which will be replaced with the maximum allowed length
  246. * var maxlength = [9, 'The value of path `{PATH}` (`{VALUE}`) exceeds the maximum allowed length ({MAXLENGTH}).'];
  247. * var schema = new Schema({ postalCode: { type: String, maxlength: maxlength })
  248. * var Address = mongoose.model('Address', schema);
  249. * var address = new Address({ postalCode: '9512512345' });
  250. * address.validate(function (err) {
  251. * console.log(String(err)) // ValidationError: The value of path `postalCode` (`9512512345`) exceeds the maximum allowed length (9).
  252. * })
  253. *
  254. * @param {Number} value maximum string length
  255. * @param {String} [message] optional custom error message
  256. * @return {SchemaType} this
  257. * @see Customized Error Messages #error_messages_MongooseError-messages
  258. * @api public
  259. */
  260. SchemaString.prototype.maxlength = function(value, message) {
  261. if (this.maxlengthValidator) {
  262. this.validators = this.validators.filter(function(v) {
  263. return v.validator !== this.maxlengthValidator;
  264. }, this);
  265. }
  266. if (value !== null && value !== undefined) {
  267. var msg = message || MongooseError.messages.String.maxlength;
  268. msg = msg.replace(/{MAXLENGTH}/, value);
  269. this.validators.push({
  270. validator: this.maxlengthValidator = function(v) {
  271. return v === null || v.length <= value;
  272. },
  273. message: msg,
  274. type: 'maxlength',
  275. maxlength: value
  276. });
  277. }
  278. return this;
  279. };
  280. /**
  281. * Sets a regexp validator.
  282. *
  283. * Any value that does not pass `regExp`.test(val) will fail validation.
  284. *
  285. * ####Example:
  286. *
  287. * var s = new Schema({ name: { type: String, match: /^a/ }})
  288. * var M = db.model('M', s)
  289. * var m = new M({ name: 'I am invalid' })
  290. * m.validate(function (err) {
  291. * console.error(String(err)) // "ValidationError: Path `name` is invalid (I am invalid)."
  292. * m.name = 'apples'
  293. * m.validate(function (err) {
  294. * assert.ok(err) // success
  295. * })
  296. * })
  297. *
  298. * // using a custom error message
  299. * var match = [ /\.html$/, "That file doesn't end in .html ({VALUE})" ];
  300. * var s = new Schema({ file: { type: String, match: match }})
  301. * var M = db.model('M', s);
  302. * var m = new M({ file: 'invalid' });
  303. * m.validate(function (err) {
  304. * console.log(String(err)) // "ValidationError: That file doesn't end in .html (invalid)"
  305. * })
  306. *
  307. * Empty strings, `undefined`, and `null` values always pass the match validator. If you require these values, enable the `required` validator also.
  308. *
  309. * var s = new Schema({ name: { type: String, match: /^a/, required: true }})
  310. *
  311. * @param {RegExp} regExp regular expression to test against
  312. * @param {String} [message] optional custom error message
  313. * @return {SchemaType} this
  314. * @see Customized Error Messages #error_messages_MongooseError-messages
  315. * @api public
  316. */
  317. SchemaString.prototype.match = function match(regExp, message) {
  318. // yes, we allow multiple match validators
  319. var msg = message || MongooseError.messages.String.match;
  320. var matchValidator = function(v) {
  321. if (!regExp) {
  322. return false;
  323. }
  324. var ret = ((v != null && v !== '')
  325. ? regExp.test(v)
  326. : true);
  327. return ret;
  328. };
  329. this.validators.push({
  330. validator: matchValidator,
  331. message: msg,
  332. type: 'regexp',
  333. regexp: regExp
  334. });
  335. return this;
  336. };
  337. /**
  338. * Check if the given value satisfies a required validator.
  339. *
  340. * @param {Any} value
  341. * @param {Document} doc
  342. * @return {Boolean}
  343. * @api public
  344. */
  345. SchemaString.prototype.checkRequired = function checkRequired(value, doc) {
  346. if (SchemaType._isRef(this, value, doc, true)) {
  347. return !!value;
  348. }
  349. return (value instanceof String || typeof value === 'string') && value.length;
  350. };
  351. /**
  352. * Casts to String
  353. *
  354. * @api private
  355. */
  356. SchemaString.prototype.cast = function(value, doc, init) {
  357. if (SchemaType._isRef(this, value, doc, init)) {
  358. // wait! we may need to cast this to a document
  359. if (value === null || value === undefined) {
  360. return value;
  361. }
  362. // lazy load
  363. Document || (Document = require('./../document'));
  364. if (value instanceof Document) {
  365. value.$__.wasPopulated = true;
  366. return value;
  367. }
  368. // setting a populated path
  369. if (typeof value === 'string') {
  370. return value;
  371. } else if (Buffer.isBuffer(value) || !utils.isObject(value)) {
  372. throw new CastError('string', value, this.path);
  373. }
  374. // Handle the case where user directly sets a populated
  375. // path to a plain object; cast to the Model used in
  376. // the population query.
  377. var path = doc.$__fullPath(this.path);
  378. var owner = doc.ownerDocument ? doc.ownerDocument() : doc;
  379. var pop = owner.populated(path, true);
  380. var ret = new pop.options.model(value);
  381. ret.$__.wasPopulated = true;
  382. return ret;
  383. }
  384. // If null or undefined
  385. if (value === null || value === undefined) {
  386. return value;
  387. }
  388. if (typeof value !== 'undefined') {
  389. // handle documents being passed
  390. if (value._id && typeof value._id === 'string') {
  391. return value._id;
  392. }
  393. // Re: gh-647 and gh-3030, we're ok with casting using `toString()`
  394. // **unless** its the default Object.toString, because "[object Object]"
  395. // doesn't really qualify as useful data
  396. if (value.toString && value.toString !== Object.prototype.toString) {
  397. return value.toString();
  398. }
  399. }
  400. throw new CastError('string', value, this.path);
  401. };
  402. /*!
  403. * ignore
  404. */
  405. function handleSingle(val) {
  406. return this.castForQuery(val);
  407. }
  408. function handleArray(val) {
  409. var _this = this;
  410. if (!Array.isArray(val)) {
  411. return [this.castForQuery(val)];
  412. }
  413. return val.map(function(m) {
  414. return _this.castForQuery(m);
  415. });
  416. }
  417. SchemaString.prototype.$conditionalHandlers =
  418. utils.options(SchemaType.prototype.$conditionalHandlers, {
  419. $all: handleArray,
  420. $gt: handleSingle,
  421. $gte: handleSingle,
  422. $lt: handleSingle,
  423. $lte: handleSingle,
  424. $options: handleSingle,
  425. $regex: handleSingle
  426. });
  427. /**
  428. * Casts contents for queries.
  429. *
  430. * @param {String} $conditional
  431. * @param {any} [val]
  432. * @api private
  433. */
  434. SchemaString.prototype.castForQuery = function($conditional, val) {
  435. var handler;
  436. if (arguments.length === 2) {
  437. handler = this.$conditionalHandlers[$conditional];
  438. if (!handler) {
  439. throw new Error('Can\'t use ' + $conditional + ' with String.');
  440. }
  441. return handler.call(this, val);
  442. }
  443. val = $conditional;
  444. if (Object.prototype.toString.call(val) === '[object RegExp]') {
  445. return val;
  446. }
  447. return this.cast(val);
  448. };
  449. /*!
  450. * Module exports.
  451. */
  452. module.exports = SchemaString;