rules.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /**
  2. * Module dependencies
  3. */
  4. var util = require('util');
  5. var _ = require('@sailshq/lodash');
  6. var validator = require('validator');
  7. /**
  8. * Type rules
  9. */
  10. var rules = {
  11. // ┬┌─┐┌┐┌┌─┐┬─┐┌─┐ ┌┐┌┬ ┬┬ ┬
  12. // ││ ┬││││ │├┬┘├┤ ││││ ││ │
  13. // ┴└─┘┘└┘└─┘┴└─└─┘ ┘└┘└─┘┴─┘┴─┘
  14. 'isBoolean': {
  15. fn: function(x) {
  16. return typeof x === 'boolean';
  17. },
  18. defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not a boolean.'; },
  19. expectedTypes: ['json', 'ref']
  20. },
  21. 'isNotEmptyString': {
  22. fn: function(x) {
  23. return x !== '';
  24. },
  25. defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was an empty string.'; },
  26. expectedTypes: ['json', 'ref', 'string']
  27. },
  28. 'isInteger': {
  29. fn: function(x) {
  30. return typeof x === 'number' && (parseInt(x) === x);
  31. },
  32. defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not an integer.'; },
  33. expectedTypes: ['json', 'ref', 'number']
  34. },
  35. 'isNumber': {
  36. fn: function(x) {
  37. return typeof x === 'number';
  38. },
  39. defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not a number.'; },
  40. expectedTypes: ['json', 'ref']
  41. },
  42. 'isString': {
  43. fn: function(x) {
  44. return typeof x === 'string';
  45. },
  46. defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not a string.'; },
  47. expectedTypes: ['json', 'ref']
  48. },
  49. 'max': {
  50. fn: function(x, maximum) {
  51. if (typeof x !== 'number') { throw new Error ('Value was not a number.'); }
  52. return x <= maximum;
  53. },
  54. defaultErrorMessage: function(x, maximum) { return 'Value ('+util.inspect(x)+') was greater than the configured maximum (' + maximum + ')'; },
  55. expectedTypes: ['json', 'ref', 'number'],
  56. checkConfig: function(constraint) {
  57. if (typeof constraint !== 'number') {
  58. return 'Maximum must be specified as a number; instead got `' + util.inspect(constraint) + '`.';
  59. }
  60. return false;
  61. }
  62. },
  63. 'min': {
  64. fn: function(x, minimum) {
  65. if (typeof x !== 'number') { throw new Error ('Value was not a number.'); }
  66. return x >= minimum;
  67. },
  68. defaultErrorMessage: function(x, minimum) { return 'Value ('+util.inspect(x)+') was less than the configured minimum (' + minimum + ')'; },
  69. expectedTypes: ['json', 'ref', 'number'],
  70. checkConfig: function(constraint) {
  71. if (typeof constraint !== 'number') {
  72. return 'Minimum must be specified as a number; instead got `' + util.inspect(constraint) + '`.';
  73. }
  74. return false;
  75. }
  76. },
  77. // ┬┌─┐┌┐┌┌─┐┬─┐┌─┐ ┌┐┌┬ ┬┬ ┬ ┌─┐┌┐┌┌┬┐ ┌─┐┌┬┐┌─┐┌┬┐┬ ┬ ┌─┐┌┬┐┬─┐┬┌┐┌┌─┐
  78. // ││ ┬││││ │├┬┘├┤ ││││ ││ │ ├─┤│││ ││ ├┤ │││├─┘ │ └┬┘ └─┐ │ ├┬┘│││││ ┬
  79. // ┴└─┘┘└┘└─┘┴└─└─┘ ┘└┘└─┘┴─┘┴─┘ ┴ ┴┘└┘─┴┘ └─┘┴ ┴┴ ┴ ┴ └─┘ ┴ ┴└─┴┘└┘└─┘
  80. 'isAfter': {
  81. fn: function(x, constraint) {
  82. var normalizedX;
  83. if (_.isNumber(x)) {
  84. normalizedX = new Date(x).getTime();
  85. } else if (_.isDate(x)) {
  86. normalizedX = x.getTime();
  87. } else {
  88. normalizedX = Date.parse(x);
  89. }
  90. var normalizedConstraint;
  91. if (_.isNumber(constraint)) {
  92. normalizedConstraint = new Date(constraint).getTime();
  93. } else if (_.isDate(constraint)) {
  94. normalizedConstraint = constraint.getTime();
  95. } else {
  96. normalizedConstraint = Date.parse(constraint);
  97. }
  98. return normalizedX > normalizedConstraint;
  99. },
  100. expectedTypes: ['json', 'ref', 'string', 'number'],
  101. defaultErrorMessage: function(x, constraint) { return 'Value ('+util.inspect(x)+') was before the configured time (' + constraint + ')'; },
  102. ignoreEmptyString: true,
  103. checkConfig: function(constraint) {
  104. var isValidConstraint = (_.isNumber(constraint) || _.isDate(constraint) || (_.isString(constraint) && _.isNull(validator.toDate(constraint))));
  105. if (!isValidConstraint) {
  106. return 'Validation rule must be specified as a JS timestamp (number of ms since epoch), a natively-parseable date string, or a JavaScript Date instance; instead got `' + util.inspect(constraint) + '`.';
  107. } else {
  108. return false;
  109. }
  110. }
  111. },
  112. 'isBefore': {
  113. fn: function(x, constraint) {
  114. var normalizedX;
  115. if (_.isNumber(x)) {
  116. normalizedX = new Date(x).getTime();
  117. } else if (_.isDate(x)) {
  118. normalizedX = x.getTime();
  119. } else {
  120. normalizedX = Date.parse(x);
  121. }
  122. var normalizedConstraint;
  123. if (_.isNumber(constraint)) {
  124. normalizedConstraint = new Date(constraint).getTime();
  125. } else if (_.isDate(constraint)) {
  126. normalizedConstraint = constraint.getTime();
  127. } else {
  128. normalizedConstraint = Date.parse(constraint);
  129. }
  130. return normalizedX < normalizedConstraint;
  131. },
  132. expectedTypes: ['json', 'ref', 'string', 'number'],
  133. defaultErrorMessage: function(x, constraint) { return 'Value ('+util.inspect(x)+') was after the configured time (' + constraint + ')'; },
  134. ignoreEmptyString: true,
  135. checkConfig: function(constraint) {
  136. var isValidConstraint = (_.isNumber(constraint) || _.isDate(constraint) || (_.isString(constraint) && _.isNull(validator.toDate(constraint))));
  137. if (!isValidConstraint) {
  138. return 'Validation rule must be specified as a JS timestamp (number of ms since epoch), a natively-parseable date string, or a JavaScript Date instance; instead got `' + util.inspect(constraint) + '`.';
  139. } else {
  140. return false;
  141. }
  142. }
  143. },
  144. 'isCreditCard': {
  145. fn: function(x) {
  146. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  147. return validator.isCreditCard(x);
  148. },
  149. expectedTypes: ['json', 'ref', 'string'],
  150. defaultErrorMessage: function () { return 'Value was not a valid credit card.'; },
  151. ignoreEmptyString: true
  152. },
  153. 'isEmail': {
  154. fn: function(x) {
  155. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  156. return validator.isEmail(x);
  157. },
  158. expectedTypes: ['json', 'ref', 'string'],
  159. defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid email address.'; },
  160. ignoreEmptyString: true
  161. },
  162. 'isHexColor': {
  163. fn: function(x) {
  164. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  165. return validator.isHexColor(x);
  166. },
  167. expectedTypes: ['json', 'ref', 'string'],
  168. defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid hex color.'; },
  169. ignoreEmptyString: true
  170. },
  171. 'isIn': {
  172. fn: function(x, constraint) {
  173. return _.contains(constraint, x);
  174. },
  175. expectedTypes: ['json', 'ref', 'string', 'number'],
  176. defaultErrorMessage: function(x, whitelist) { return 'Value ('+util.inspect(x)+') was not in the configured whitelist (' + whitelist.join(', ') + ')'; },
  177. ignoreEmptyString: true,
  178. checkConfig: function(constraint) {
  179. if (!_.isArray(constraint)) {
  180. return 'Allowable values must be specified as an array; instead got `' + util.inspect(constraint) + '`.';
  181. }
  182. return false;
  183. }
  184. },
  185. 'isIP': {
  186. fn: function(x) {
  187. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  188. return validator.isIP(x);
  189. },
  190. expectedTypes: ['json', 'ref', 'string'],
  191. defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid IP address.'; },
  192. ignoreEmptyString: true
  193. },
  194. 'isNotIn': {
  195. fn: function(x, constraint) {
  196. return !_.contains(constraint, x);
  197. },
  198. expectedTypes: ['json', 'ref', 'string', 'number'],
  199. defaultErrorMessage: function(x, blacklist) { return 'Value ('+util.inspect(x)+') was in the configured blacklist (' + blacklist.join(', ') + ')'; },
  200. ignoreEmptyString: true,
  201. checkConfig: function(constraint) {
  202. if (!_.isArray(constraint)) {
  203. return 'Blacklisted values must be specified as an array; instead got `' + util.inspect(constraint) + '`.';
  204. }
  205. return false;
  206. }
  207. },
  208. 'isURL': {
  209. fn: function(x, opt) {
  210. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  211. return validator.isURL(x, opt === true ? undefined : opt);
  212. },
  213. expectedTypes: ['json', 'ref', 'string'],
  214. defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid URL.'; },
  215. ignoreEmptyString: true
  216. },
  217. 'isUUID': {
  218. fn: function(x) {
  219. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  220. return validator.isUUID(x);
  221. },
  222. expectedTypes: ['json', 'ref', 'string'],
  223. defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid UUID.'; },
  224. ignoreEmptyString: true
  225. },
  226. 'minLength': {
  227. fn: function(x, minLength) {
  228. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  229. return x.length >= minLength;
  230. },
  231. expectedTypes: ['json', 'ref', 'string'],
  232. defaultErrorMessage: function(x, minLength) { return 'Value ('+util.inspect(x)+') was shorter than the configured minimum length (' + minLength + ')'; },
  233. ignoreEmptyString: true,
  234. checkConfig: function(constraint) {
  235. if (typeof constraint !== 'number' && parseInt(constraint) !== constraint) {
  236. return 'Minimum length must be specified as an integer; instead got `' + util.inspect(constraint) + '`.';
  237. }
  238. return false;
  239. }
  240. },
  241. 'maxLength': {
  242. fn: function(x, maxLength) {
  243. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  244. return x.length <= maxLength;
  245. },
  246. expectedTypes: ['json', 'ref', 'string'],
  247. defaultErrorMessage: function(x, maxLength) { return 'Value was '+(maxLength-x.length)+' character'+((maxLength-x.length !== 1) ? 's' : '')+' longer than the configured maximum length (' + maxLength + ')'; },
  248. ignoreEmptyString: true,
  249. checkConfig: function(constraint) {
  250. if (typeof constraint !== 'number' && parseInt(constraint) !== constraint) {
  251. return 'Maximum length must be specified as an integer; instead got `' + util.inspect(constraint) + '`.';
  252. }
  253. return false;
  254. }
  255. },
  256. 'regex': {
  257. fn: function(x, regex) {
  258. if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
  259. return validator.matches(x, regex);
  260. },
  261. defaultErrorMessage: function(x, regex) { return 'Value ('+util.inspect(x)+') did not match the configured regular expression (' + regex + ')'; },
  262. expectedTypes: ['json', 'ref', 'string'],
  263. ignoreEmptyString: true,
  264. checkConfig: function(constraint) {
  265. if (!_.isRegExp(constraint)) {
  266. return 'Expected a regular expression as the constraint; instead got `' + util.inspect(constraint) + '`.';
  267. }
  268. return false;
  269. }
  270. },
  271. // ┌─┐┬ ┬┌─┐┌┬┐┌─┐┌┬┐
  272. // │ │ │└─┐ │ │ ││││
  273. // └─┘└─┘└─┘ ┴ └─┘┴ ┴
  274. // Custom rule function.
  275. 'custom': {
  276. fn: function(x, customFn) {
  277. return customFn(x);
  278. },
  279. expectedTypes: ['json', 'ref', 'string', 'number', 'boolean'],
  280. defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') failed custom validation.'; },
  281. checkConfig: function(constraint) {
  282. if (!_.isFunction(constraint)) {
  283. return 'Expected a function as the constraint; instead got `' + util.inspect(constraint) + '`. Please return `true` to indicate success, or otherwise return `false` or throw to indicate failure';
  284. }
  285. if (constraint.constructor.name === 'AsyncFunction') {
  286. return 'Custom validation function cannot be an `async function` -- please use synchronous logic and return `true` to indicate success, or otherwise return `false` or throw to indicate failure.';
  287. }
  288. return false;
  289. }
  290. }
  291. };
  292. // Wrap a rule in a function that handles nulls and empty strings as requested,
  293. // and adds an `expectedTypes` array that users of the rule can check to see
  294. // if their value is of a type that the rule is designed to handle. Note that
  295. // this list of types is not necessarily validated in the rule itself; that is,
  296. // just because it lists "json, ref, string" doesn't necessarily mean that it
  297. // will automatically kick out numbers (it might stringify them). It's up to
  298. // you to decide whether to run the validation based on its `expectedTypes`.
  299. module.exports = _.reduce(rules, function createRule(memo, rule, ruleName) {
  300. // Wrap the original rule in a function that kicks out null and empty string if necessary.
  301. var wrappedRule = function(x) {
  302. // Never allow null or undefined.
  303. if (_.isNull(x) || _.isUndefined(x)) {
  304. return 'Got invalid value `' + x + '`!';
  305. }
  306. // Allow empty strings if we're explicitly ignoring them.
  307. if (x === '' && rule.ignoreEmptyString) {
  308. return false;
  309. }
  310. var passed;
  311. // Run the original rule function.
  312. try {
  313. passed = rule.fn.apply(rule, arguments);
  314. } catch (e) {
  315. // console.error('ERROR:',e);
  316. if (_.isError(e)) {
  317. return e.message;
  318. } else {
  319. return String(e);
  320. }
  321. }
  322. if (passed) { return false; }
  323. return _.isFunction(rule.defaultErrorMessage) ? rule.defaultErrorMessage.apply(rule, arguments) : rule.defaultErrorMessage;
  324. };//ƒ
  325. // If the rule doesn't declare its own config-checker, assume that the constraint is supposed to be `true`.
  326. // This is the case for most of the `is` rules like `isBoolean`, `isCreditCard`, `isEmail`, etc.
  327. if (_.isUndefined(rule.checkConfig)) {
  328. wrappedRule.checkConfig = function (constraint) {
  329. if (constraint !== true) {
  330. return 'This validation only accepts `true` as a constraint. Instead, saw `' + constraint + '`.';
  331. }
  332. return false;
  333. };
  334. } else {
  335. wrappedRule.checkConfig = rule.checkConfig;
  336. }
  337. // Set the `expectedTypes` property of the wrapped function.
  338. wrappedRule.expectedTypes = rule.expectedTypes;
  339. // Return the wrapped function.
  340. memo[ruleName] = wrappedRule;
  341. return memo;
  342. }, {});