UNPKG

15.3 kBJavaScriptView Raw
1'use strict';
2
3const AppError = require('./appError');
4const { ObjectId } = require('mongodb');
5const parseFloatHuman = require('./parseFloatHuman');
6const moment = require('moment');
7
8const requestValidator = {
9
10 OBJECT_ID_REGEX: /^[a-f0-9]{24}$/i,
11 EMAIL_REGEX: /^[^@\s]+@[^@\s]+\.[a-z]{2,10}$/i,
12 USERNAME_REGEX: /^[a-z0-9_]+$/i,
13 // eslint-disable-next-line
14 ISO_STRING_DATE_TIME_REGEX: /^(\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?))?)?$/,
15
16 /**
17 *
18 * @param {*} value
19 * @param {string} fieldName
20 * @param {*|null} [defaultValue]
21 * @param {boolean} [returnObjects]
22 * @param {boolean} [allowNulls]
23 * @returns {Array|null}
24 */
25 objectIdList (value, fieldName, defaultValue, returnObjects, allowNulls) {
26
27 if (typeof value === 'string') {
28 value = value.split(',');
29 }
30
31 if (defaultValue === true && arguments.length === 3) {
32 defaultValue = undefined;
33 returnObjects = true;
34 }
35
36 if (typeof value === 'object' && value instanceof Array) {
37
38 value = value.map((el) => {
39
40 if (allowNulls && (el === 'null' || el === null)) {
41 return null;
42
43 } else if (!el.match(requestValidator.OBJECT_ID_REGEX)) {
44 throw AppError.badRequest(`A ${el} at ${fieldName} is not a ObjectId`);
45 }
46
47 return returnObjects ? new ObjectId(el) : el;
48 });
49
50 } else if (value) {
51 throw AppError.badRequest(`Bad field list: ${fieldName}`);
52
53 } else {
54
55 if (typeof defaultValue === 'undefined') {
56 defaultValue = null;
57 }
58
59 value = defaultValue;
60 }
61
62 return value;
63 },
64
65 /**
66 *
67 * @param {*} value
68 * @param {string} fieldName
69 * @returns {string}
70 */
71 objectId (value, fieldName) {
72 if (typeof value === 'string') {
73 if (!value.match(requestValidator.OBJECT_ID_REGEX)) {
74 throw AppError.badRequest(`A \`${value}\` at \`${fieldName}\` is not a ObjectId`);
75 }
76 } else if (value) {
77 throw AppError.badRequest(`Bad \`${fieldName}\``);
78 } else {
79 value = null;
80 }
81
82 return value;
83 },
84
85
86 /**
87 *
88 * @param {*} value
89 * @param {*} [defaultValue]
90 * @returns {boolean}
91 */
92 boolean (value, defaultValue) {
93
94 if (typeof value === 'boolean') {
95 return value;
96
97 } else if (typeof value === 'number') {
98 return !!value;
99
100 } else if (typeof value === 'string') {
101 try {
102 // 'true' and '1' => true... 'false' and '0' => false
103 return !!JSON.parse(value);
104 } catch (err) {} // eslint-disable-line
105 }
106
107 if (typeof defaultValue === 'undefined') {
108 defaultValue = null;
109 }
110
111 return defaultValue;
112 },
113
114
115 /**
116 *
117 * @param {*} value
118 * @param {*} [defaultValue=null]
119 * @param {string} [fieldName]
120 */
121 array (value, defaultValue, fieldName) {
122
123 if (Array.isArray(value)) {
124 return value;
125
126 } else if (value != null) { // jshint ignore:line
127 throw AppError.badRequest(`Bad field: '${fieldName}' has to be array`);
128 }
129
130 if (typeof value === 'undefined') {
131
132 if (typeof defaultValue === 'undefined') {
133 defaultValue = null;
134 }
135
136 value = defaultValue;
137 }
138
139 return value;
140 },
141
142 /**
143 *
144 * @param {*} value
145 * @param {*|null} [defaultValue]
146 * @param {string} [fieldName]
147 * @param {{
148 * maxLength?: number
149 * minLength?: number
150 * trim?: boolean
151 * regexp?: RegExp
152 * }} [options]
153 * @returns {string|null}
154 */
155 string (value, defaultValue, fieldName, options = {}) {
156
157 if (typeof value === 'number') {
158 value = value.toString();
159 }
160
161 if (typeof value === 'string') {
162
163 if (options.trim) {
164 value = value.trim();
165 }
166
167 if (value) {
168 value = requestValidator._validateStringRestrictions(value, fieldName, options);
169 }
170
171 if (value) { // empty strings will not pass
172 return value;
173 }
174 }
175
176 if (typeof defaultValue === 'undefined') {
177 defaultValue = null;
178 }
179
180 return defaultValue;
181 },
182
183 /**
184 * @param {*} value
185 * @param {string} [fieldName]
186 * @param {{
187 * maxLength?: number
188 * minLength?: number
189 * regexp?: RegExp
190 * }} [options]
191 */
192 _validateStringRestrictions (value, fieldName, options = {}) {
193
194 if (options.maxLength && value.length > options.maxLength) {
195 throw AppError.badRequest(
196 `Field \`${fieldName}\` can not be longer than ${options.maxLength} characters.`
197 );
198 }
199
200 if (typeof options.minLength === 'number' && value.length < options.minLength) {
201 throw AppError.badRequest(
202 `Field \`${fieldName}\` can not be shorter than ${options.minLength} characters.`
203 );
204 }
205
206 if (options.regexp && !value.match(options.regexp)) {
207 throw AppError.badRequest(
208 `Field \`${fieldName}\` should match to ${options.regexp.toString()} regular expression.`
209 );
210 }
211
212 return value;
213 },
214
215 /**
216 *
217 * @param {*} value
218 * @param {*|null} [defaultValue]
219 * @returns {string|null}
220 */
221 object (value, defaultValue) {
222
223
224 if (typeof value === 'object' && value !== null && !(value instanceof Array)) {
225 return value;
226
227 }
228
229 if (typeof defaultValue === 'undefined') {
230 defaultValue = null;
231 }
232
233 return defaultValue;
234 },
235
236 /**
237 * Throws error, when the field is null
238 *
239 * @param {object} obj
240 * @param {string} property
241 * @param {string} [ofObject]
242 */
243 propFilled (obj, property, ofObject) {
244
245 if (obj[property] === null) {
246
247 if (ofObject) {
248 ofObject = `of ${ofObject}`;
249 }
250
251 throw AppError.badRequest(`Field \`${property}\` ${(ofObject || '')} should be filled`);
252 }
253 },
254
255
256 /**
257 *
258 * @param {*} value
259 * @param {Array} array
260 * @param {string} fieldName
261 * @param {*|null} [defaultValue]
262 * @returns {*|null}
263 */
264 _enum (value, array, fieldName, defaultValue) {
265 let found = null;
266
267 if (value === null || typeof value === 'undefined') {
268
269 if (typeof defaultValue === 'undefined') {
270 defaultValue = null;
271 }
272
273 } else {
274 found = array.indexOf(value);
275 }
276
277 if (found === null) {
278 return defaultValue;
279
280 } else if (found >= 0) {
281 return array[found];
282 }
283
284 throw AppError.badRequest(`Field '${fieldName}' is not matching any required value`);
285 },
286
287 /**
288 *
289 * @param {*} value
290 * @param {Array|Object.<*,string>} array
291 * @param {boolean} strict
292 * @param {string} fieldName
293 * @param {*|null} [defaultValue]
294 * @returns {*|null}
295 */
296 stringEnum (value, array, strict, fieldName, defaultValue) {
297
298 if (!Array.isArray(array) && typeof array === 'object') {
299 array = Object.values(array);
300 }
301
302 if (!strict) {
303 array = array.map((elem) => {
304 if (elem !== null) {
305 elem = elem.toLowerCase();
306 }
307 return elem;
308 });
309
310 if (typeof value === 'string') {
311 value = value.toLowerCase();
312 }
313 }
314
315 return this._enum(value, array, fieldName, defaultValue);
316 },
317
318 /**
319 *
320 * @param {*} value
321 * @param {*|null} [defaultValue]
322 * @returns {Date|null}
323 */
324 date (value, defaultValue) {
325
326 if (value instanceof Date) {
327 return value;
328
329 } else if (typeof value === 'number') {
330 return new Date(value);
331
332 } else if (typeof value === 'string' && value.length > 0) {
333 let date;
334
335 try {
336 date = new Date(value); // can throw RangeError
337 } catch (e) {
338 throw AppError.badRequest(`A ${value} is not ISO date string`);
339 }
340
341 if (!moment(value).isValid() || !this.ISO_STRING_DATE_TIME_REGEX.test(value)) {
342 throw AppError.badRequest(`A ${value} is not ISO date string`);
343 }
344
345 return date;
346
347 }
348
349 if (typeof defaultValue === 'undefined') {
350 defaultValue = null;
351 }
352
353 return defaultValue;
354 },
355
356 /**
357 *
358 * @param {*} value
359 * @param {*} [defaultValue=null]
360 * @param {{ minValue?: number, maxValue?: number }} [options]
361 * @returns {number} Or `defaultValue`
362 */
363 number (value, defaultValue, options = {}) {
364
365 if (typeof value === 'string') {
366 value = parseFloatHuman(value);
367 }
368
369 if (typeof value !== 'number') {
370
371 if (typeof defaultValue === 'undefined') {
372 defaultValue = null;
373 }
374
375 value = defaultValue;
376
377 } else {
378 if (typeof options.minValue === 'number' && value < options.minValue) {
379 throw AppError.badRequest(`The value is lower than ${options.minValue}`);
380 }
381
382 if (typeof options.maxValue === 'number' && value > options.maxValue) {
383 throw AppError.badRequest(`The value is greater than ${options.maxValue}`);
384 }
385 }
386
387 return value;
388 },
389
390 /**
391 *
392 * @param {*} value
393 * @param {*|null} [defaultValue]
394 * @returns {string|null} - or default value
395 */
396 email (value, defaultValue = null) {
397
398 if (typeof value === 'string' && value.length > 0) {
399
400 if (!value.match(requestValidator.EMAIL_REGEX)) {
401 throw AppError.badRequest('Email is not valid');
402 }
403
404 return value;
405 }
406
407 return defaultValue;
408 },
409
410 /**
411 *
412 * @param {Array} value
413 * @param {Object} emptyObject - define values: mandatory: true, optional: false, defaultValue: null
414 * @param {string} fieldName
415 * @param {Function} [customValidationCallback]
416 * @param {*} [thisArg]
417 * @returns {Array|null}
418 */
419 setOfObjects (value, emptyObject, fieldName, customValidationCallback, thisArg) {
420
421 if (!value) {
422 return null;
423 } else if (!(value instanceof Array)) {
424 throw AppError.badRequest(`Field '${fieldName}' should be an array`);
425 } else {
426 return value.map(function (el) {
427 if (typeof el !== 'object' || el instanceof Array) {
428 throw AppError.badRequest(`Array '${fieldName}' should contain only objects`);
429 }
430 let ret = {};
431
432 for (const field in emptyObject) {
433
434 if (!Object.prototype.hasOwnProperty.call(emptyObject, field)) {
435 continue;
436 }
437
438 const fieldType = emptyObject[field];
439 const val = el[field];
440
441 if (typeof val !== 'undefined') { // value is present
442 ret[field] = val;
443 } else if (fieldType === true) { // mandatory field is missing
444 throw AppError.badRequest(`Object in '${fieldName}' should contain '${field}' field`);
445 } else if (fieldType !== false) { // use a default value
446 ret[field] = fieldType;
447 }
448 }
449
450 if (typeof customValidationCallback === 'function') {
451 // function can return new object, otherwise the other object is used
452 ret = customValidationCallback.call(thisArg || this, ret, fieldName) || ret;
453 }
454
455 return ret;
456 });
457 }
458 },
459
460 /**
461 *
462 * @param {*} value
463 * @param {string} fieldName
464 * @param {*|null} [defaultValue]
465 * @returns {Array|null} Or `defaultValue`
466 */
467 commaList (value, fieldName, defaultValue) {
468
469 if (typeof value === 'string') {
470 // split the string
471
472 value = value.split(',');
473 }
474
475 if (typeof value === 'object' && value instanceof Array) {
476
477 value = value.map(el => (el.replace(/[^a-z0-9_-]+/ig, '')));
478
479 } else if (value) {
480 throw AppError.badRequest(`Bad field list: ${fieldName}`);
481 } else {
482
483 if (typeof defaultValue === 'undefined') {
484 defaultValue = null;
485 }
486
487 value = defaultValue;
488 }
489
490 return value;
491 },
492
493 /**
494 *
495 * @param {*} value
496 * @param {*} [defaultValue=null]
497 * @param {string} [fieldName]
498 * @param {{
499 * minValue?: number
500 * }} [options]
501 * @returns {number} Or `defaultValue`
502 */
503 integer (value, defaultValue, fieldName, options) {
504
505 const number = this.number(value, defaultValue, options);
506
507 if (typeof number !== 'number') {
508 return number;
509 }
510
511 return Math.trunc(number);
512 },
513
514 /**
515 *
516 * @param {*} value
517 * @param {*} [defaultValue=null]
518 * @param {{
519 * decimals?: number,
520 * minValue?: number,
521 * maxValue?: number
522 * }} [options]
523 * @returns {number} Or `defaultValue`
524 */
525 decimalNumber (value, defaultValue, options = {}) {
526
527 value = this.number(value, defaultValue, options);
528
529 if (typeof value === 'number') {
530 const decimals = options.decimals || 2;
531 value = parseFloat(value.toFixed(decimals));
532 }
533
534 return value;
535 },
536
537 /**
538 *
539 * @param value
540 * @param defaultValue
541 * @returns {*}
542 */
543 version (value, defaultValue) {
544 if (typeof value === 'number') {
545 return value;
546 } else if (typeof value === 'string' && value.length > 0) {
547 return this._parseVersion(value);
548 }
549
550 if (typeof defaultValue === 'undefined') {
551 defaultValue = 0;
552 }
553
554 return defaultValue;
555 },
556
557 /**
558 *
559 * @param {string} version
560 * @returns {number}
561 */
562 _parseVersion (version) {
563 let value = version;
564 // replace all non-number and non-dots and non-comma characters
565 value = value.replace(/[^0-9\.,]/g, ''); // eslint-disable-line
566 value = value.replace(/,/g, '.'); // all comma to dots
567
568 let i = 0;
569 value = value.replace(/\./g, () => (i++ === 0 ? '.' : ''));
570
571 return parseFloat(value);
572 }
573};
574
575module.exports = requestValidator;