Skip to content

Instantly share code, notes, and snippets.

@peshkov3
Last active April 17, 2018 10:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peshkov3/f29e76d2f1f66247de503047e8670ec3 to your computer and use it in GitHub Desktop.
Save peshkov3/f29e76d2f1f66247de503047e8670ec3 to your computer and use it in GitHub Desktop.
nodejs-validator
import * as changeCase from 'change-case';
// Laravel's like node js request data validator
// Usage:
//
// let v = new Validator(body, {
// index: 'required|string',
// lang: 'string',
// code: 'required|number',
// phone_number: 'required|string|regex:/^\\+?[1-9]\\d{1,14}$/',
// email: 'string',
// });
//
// if (v.fails()) {
// return res.status(HttpStatus.UNPROCESSABLE_ENTITY).json(v.getErrors());
// }
const DICT = {
en: {
required: 'The field :field is required.',
number: 'The field :field must be a number.',
string: 'The field :field must be a string.',
boolean: 'The field :field must be a boolean.',
in: 'The field :field must be one of the: :list.',
maxStringLength: 'The field :field must be less than: :value.',
maxNumberLength: 'The length of field :field must be less than: :value.',
maxNumber: 'The field :field must be less than: :value.',
date: 'The field :field must be a valid date format.',
regex: 'The :field format is invalid.',
requiredIf: 'The field :field is required when :dependField is not empty.',
requiredIfNot: 'The field :field is required when :dependField is empty.',
},
ru: {
required: 'Поле :field обязательно для заполнения.',
number: 'Поле :field должно быть числом.',
string: 'Поле :field должно быть строкой.',
boolean: 'Поле :field должно быть true или false.',
in: 'Поле :field должно входить в список: :list.',
maxStringLength: 'Поле :field должно содерджать :value символов.',
maxNumberLength: 'Число :field должно быть меньше :value.',
maxNumber: 'Число :field должно быть меньше :value.',
date: 'Неверный формат даты.',
regex: 'Поле :field неверного формата.',
requiredIf: 'Поле :field обязательно, если не пустое поле :dependField.',
requiredIfNot: 'Поле :field обязательно, если пустое поле :dependField.',
},
};
const FIELD_LIST_DICT = {
ru: {
phone_number: `'Номер телефона'`,
index: `'Индекс'`,
sex: `'Пол'`,
email: `'Электронная почта'`,
birthdate: `'День рождения'`
},
en: {
phone_number: `'Phone number'`,
index: `'Index'`,
sex: `'Gender'`,
email: `'Email'`,
birthdate: `'Birthdate'`
},
};
/***
* Validator class
*/
export class Validator {
private errors = {};
private dataTransformed = {};
private RULE_VALUE_DELIMITER = ',';
private RULE_VERSUS_VALUE_DELIMITER = ':';
private lang = 'en';
private RULES_DELIMITER = '|';
constructor(private data?: any, private rules?: any, private messages?: any) {
const fieldsToHandle = Object.keys(this.data);
for (let f of fieldsToHandle) {
this.dataTransformed[changeCase.snakeCase(f)] = this.data[f];
}
if (['ru', 'en'].indexOf(this.data.lang) !== -1) {
this.lang = this.data.lang || 'en';
} else {
this.lang = 'en';
}
Object.keys(this.rules).forEach(fieldNameToCheck => {
const fieldRules = this.rules[fieldNameToCheck].split(this.RULES_DELIMITER);
for (const rule of fieldRules) {
try {
let splitted = rule.split(this.RULE_VERSUS_VALUE_DELIMITER);
// regex
if (splitted.length && splitted[0] === 'regex') {
this.checkRegex(fieldNameToCheck, new RegExp(splitted[1].slice(1, -1)));
continue;
}
// in, max lenght, date, requiredIfNot
if (splitted.length) {
const funcName = this.capitalizeFirstLetter(splitted.shift());
try {
this[`check${funcName}`](fieldNameToCheck, splitted);
} catch (E) {
console.info(`Validator@constructor: validation error funcName `, funcName, fieldNameToCheck);
}
continue;
}
// required. number, string, nullable
this['check' + this.capitalizeFirstLetter(splitted[0])](fieldNameToCheck);
} catch (e) {
console.info(`Validator@constructor: validation type is not supported`, e);
}
}
});
}
getErrors(): any {
return this.errors;
}
isEmpty(): boolean {
return Object.keys(this.errors).length === 0;
}
fails(): boolean {
return !this.isEmpty();
}
addError(fieldName: string, checkType?: string, data?: any): void {
if (!this.errors[fieldName]) {
this.errors[fieldName] = [];
}
let checkMessage;
if (checkType && this.messages) {
checkMessage = this.messages[`${fieldName}.${checkType}`];
}
this.errors[fieldName].push(checkMessage || this.getMessage(checkType, data));
}
// noinspection TsLint
private checkRequired(field: string): void {
if (!this.dataTransformed[field] || this.dataTransformed[field] === '') {
this.addError(field, 'required', {field: field});
}
}
// noinspection TsLint
private checkBoolean(field: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (typeof(this.dataTransformed[field]) !== typeof(true)) {
this.addError(field, 'boolean', {field: field});
}
}
private fieldIsEmpty(field: string): boolean {
return this.dataTransformed[field] === undefined || this.dataTransformed[field] === '';
}
// noinspection TsLint
private checkIn(field: string, list: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (list[0].split(this.RULE_VALUE_DELIMITER).indexOf(this.dataTransformed[field]) === -1) {
this.addError(field, 'in', {field: field, list: list});
}
}
// noinspection TsLint
private checkMaxStringLength(field: string, value: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (this.dataTransformed[field].length >= parseInt(value, 10)) {
this.addError(field, 'maxStringLength', {field: field, value: value});
}
}
// noinspection TsLint
private checkMaxNumber(field: string, value: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (parseInt(this.dataTransformed[field], 10) >= parseInt(value[0], 10)) {
this.addError(field, 'maxNumber', {field: field, value: value});
}
}
// noinspection TsLint
private checkMaxNumberLength(field: string, value: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (`${this.dataTransformed[field]}`.length >= parseInt(value[0], 10)) {
this.addError(field, 'maxNumberLength', {field: field, value: value});
}
}
// noinspection TsLint
private checkNumber(field: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
try {
let pn = parseInt(this.dataTransformed[field], 10);
if (typeof pn !== 'number' || typeof this.dataTransformed[field] !== 'number') {
this.addError(field, 'number', {field: field});
}
return;
} catch (e) {
console.log('not number passed to request');
this.addError(field, 'number', {field: field});
return;
}
}
// noinspection TsLint
private checkString(field: string): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (typeof this.dataTransformed[field] !== 'string') {
this.addError(field, 'string', {field: field});
}
}
// todo add format checking
// noinspection TsLint
private checkDate(field: string) {
if (this.fieldIsEmpty(field)) {
return;
}
if (!this.isValidDate(this.dataTransformed[field])) {
this.addError(field, 'date', {field: field});
}
}
// noinspection TsLint
private checkNullable(field: string): void {
// todo some action may be
}
// noinspection TsLint
private checkRegex(field: string, pattern: RegExp): void {
if (this.fieldIsEmpty(field)) {
return;
}
if (!this.dataTransformed[field]) {
return;
}
const found = this.dataTransformed[field].match(pattern);
if (!found) {
this.addError(field, 'regex', {field: field});
}
}
// noinspection TsLint
private checkRequiredIfNot(field: string, dependFields: string[]): void {
if (this.fieldIsEmpty(field)) {
return;
}
for (const dependField of dependFields) {
if (!this.dataTransformed[dependField]) {
if (!this.dataTransformed[field]) {
this.addError(field, 'requiredIfNot', {field: field, dependField: dependField});
}
}
}
}
// noinspection TsLint
private checkRequiredIf(field: string, dependFields: string[]): void {
if (this.fieldIsEmpty(field)) {
return;
}
for (const dependField of dependFields) {
if (this.dataTransformed[dependField]) {
if (!this.dataTransformed[dependField]) {
this.addError(field, 'requiredIf', {field: field, dependField: dependField});
}
}
}
}
private isValidDate(date: string): boolean {
const matches = /^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})$/.exec(date);
if (matches == null) {
return false;
}
const d = parseInt(matches[2], 10);
const m = parseInt(matches[1], 10) - 1;
const y = parseInt(matches[3], 10);
const composedDate = new Date(y, m, d);
return composedDate.getDate() == d &&
composedDate.getMonth() == m &&
composedDate.getFullYear() == y;
}
private capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* get message from dictionary
*
* @param {string | undefined} checkType
* @param data
* @returns {string}
*/
private getMessage(checkType: string | undefined, data: any): string {
console.log('checkType', checkType);
let letter = DICT[this.lang || 'en'][checkType];
Object.keys(data).forEach(key => {
try {
letter = letter.replace(`:${key}`, key === 'list' ? data[key] : FIELD_LIST_DICT[this.lang || 'en'][data.field]);
} catch (e) {
console.log('Error while parsing error message', e);
}
});
return letter;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment