Skip to content

Instantly share code, notes, and snippets.

@muhammadfaizan
Last active February 16, 2023 17:07
Show Gist options
  • Save muhammadfaizan/499c0addfb8f469bd028cb1f11117a06 to your computer and use it in GitHub Desktop.
Save muhammadfaizan/499c0addfb8f469bd028cb1f11117a06 to your computer and use it in GitHub Desktop.
A comprehensive utility for validation
/**
* Created by Muhammad Faizan on 7/12/2017.
*/
const ErrorFactory = (name, code) => class CustomError extends Error {
/**
*
* @param {Error|String} error
* @param {Object} options
* @param {String} options.errorCode
* @param {Object} options.details
* @param {Object} options.details.fields
* @param {Object} options.details.params
* @param {Object} options.details.query
*/
constructor (error, options) {
let message
let errorCode
let details
if (options) {
({ errorCode, details } = options)
}
if (error instanceof Error) {
({ message } = error)
} else {
message = error
}
super(message)
this.name = name || 'CustomError'
this.status = code || 500
this.errorCode = errorCode
this.details = details
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = (new Error(message)).stack
}
}
}
module.exports = {
NoContent: ErrorFactory('NoContent', 204),
BadRequestError: ErrorFactory('BadRequestError', 400),
AddressVerificationError: ErrorFactory('AddressVerificationError', 400),
InvalidTypeError: ErrorFactory('InvalidTypeError', 400),
InvalidValueError: ErrorFactory('InvalidValueError', 400),
InvalidParametersError: ErrorFactory('InvalidParametersError', 400),
UnauthorizedError: ErrorFactory('UnauthorizedError', 401),
InvalidCredentialsError: ErrorFactory('InvalidCredentialsError', 401),
PaymentRequiredError: ErrorFactory('PaymentRequiredError', 402),
ForbiddenError: ErrorFactory('ForbiddenError', 403),
NotFound: ErrorFactory('NotFound', 404),
UpgradeRequiredError: ErrorFactory('UpgradeRequiredError', 406),
ConflictError: ErrorFactory('ConflictError', 409),
RangeError: ErrorFactory('RangeError', 412),
PreconditionError: ErrorFactory('PreconditionError', 412),
UnprocessableEntityError: ErrorFactory('UnprocessableEntityError', 422),
BadReferenceGiven: ErrorFactory('BadReferenceGiven', 424),
ServerError: ErrorFactory('ServerError', 500),
DatabaseError: ErrorFactory('DatabaseError', 500),
NotImplementedError: ErrorFactory('NotImplementedError', 501),
ServiceUnavailableError: ErrorFactory('ServiceUnavailableError', 503),
SecurityError: ErrorFactory('SecurityError', 600)
}
// error handler
/* eslint-disable */
app.use((err, req, res, next) => {
res.status(err.status || 500)
debug(err)
res.send({
error: err.message,
name: err.name,
errorCode: err.errorCode,
stack: err.stack,
details: err.details,
})
})
/* eslint-enable */
const { v, CustomErrors } = require('/utilities/index');
const payload = Object.assign({}, req.body)
debug(payload)
const validator = v.compose([
v.atLeastOneOfProperties(['email', 'mobile', 'username']),
v.object({
username: v.optional(v.string),
email: v.optional(v.emailNormalized),
mobile: v.optional(v.mobile),
password: v.string
})
])
validator(payload)
/***************************************************/
const validator = v.compose([
v.atLeastOneOfProperties(['email', 'mobile']),
v.object({
password: v.string,
email: v.optional(v.emailNormalized),
mobile: v.optional(v.phone),
person: v.object({
firstName: v.string,
lastName: v.string,
middleName: v.optional(v.string),
gender: v.optional(v.oneOf(['m', 'f', 'o'])),
uniqueNationalIdentification: v.optional(v.compose([v.string, v.isNotEmpty])),
dateOfBirth: v.optional(v.dateOfBirth)
})
})
])
payload = Object.assign({}, req.body)
validator(payload)
/***************************************************/
const validator = v.object({
title: v.string,
provider: v.mongoId,
startTime: v.dateTimeString,
endTime: v.dateTimeString,
timezone: v.string,
services: v.array(v.object({
service: v.mongoId,
interactionType: v.mongoId
})),
address: v.mongoId,
company: v.mongoId,
tenant: v.optional(v.mongoId)
})
const payload = req.body
validator(payload)
/************************************************/
const validator = v.object({
days: v.weekDaysNumbers,
title: v.string,
provider: v.mongoId,
startTime: v.timeString,
endTime: v.timeString,
startDate: v.dateString,
endDate: v.dateString,
timezone: v.string,
services: v.array(v.object({
service: v.mongoId,
interactionType: v.mongoId
})),
address: v.mongoId,
company: v.mongoId,
tenant: v.optional(v.mongoId)
})
const payload = req.body
validator(payload)
/*********************************************************/
/* eslint-disable */
const CustomErrors = require('./custom-errors');
const validator = require('validator');
const R = require('ramda');
const debug = require('debug')('utilities:validate');
const moment = require('moment-timezone');
const Validate = exports;
/*
function InvalidValueError(message) {
this.message = message;
this.name = 'InvalidValueError';
Error.captureStackTrace(this, InvalidValueError);
}*/
const Validate = exports
const { InvalidValueError } = CustomErrors
InvalidValueError.prepend = function (message, error, key) {
if (error instanceof InvalidValueError) {
let details = error.details || {
[`${key}`]: error.message
}
return new InvalidValueError(message + ': ' + error.message, {
details
})
}
return error
}
Validate.InvalidValueError = InvalidValueError
Validate.InvalidValueError = InvalidValueError;
Validate.acceptAll = function(value) {
return value;
};
Validate.optional = function(validator) {
return function(value) {
return (value === undefined || value === null) ? value : validator(value);
};
};
Validate.that = function(predicate, message) {
return function(value) {
if (predicate(value)) return value;
throw new InvalidValueError(message);
};
};
Validate.number = Validate.that(function(value) {
return typeof value === 'number';
}, 'not a number');
Validate.string = Validate.that(function(value) {
return typeof value === 'string';
}, 'not a string');
Validate.object = function(propertyValidators) {
return function(object) {
let result = {};
let key;
if (!object || typeof object !== 'object') {
throw new InvalidValueError('not an Object');
}
// Validate all properties.
for (key in propertyValidators) {
let validator = propertyValidators[key];
try {
var valid = validator(object[key]);
} catch (error) {
if (key in object) {
throw InvalidValueError.prepend('in property "' + key + '"', error);
} else {
throw new InvalidValueError('missing property "' + key + '"');
}
}
if (key in object && valid !== undefined) {
result[key] = valid;
}
}
// Check for unexpected properties.
for (key in object) {
if (!propertyValidators[key]) {
throw new InvalidValueError('unexpected property "' + key + '"');
}
}
return result;
};
};
Validate.array = function(validator) {
return function(array) {
var result = [];
if (Object.prototype.toString.call(array) !== '[object Array]') {
throw new InvalidValueError('not an Array');
}
for (var i = 0; i < array.length; ++i) {
try {
result[i] = validator(array[i]);
} catch (error) {
throw InvalidValueError.prepend('at index ' + i, error);
}
}
return result;
};
};
Validate.oneOf = function(names) {
var myObject = {};
var quotedNames = [];
names.forEach(function(name) {
myObject[name] = true;
quotedNames.push('"' + name + '"');
});
return function(value) {
if (myObject[value]) return value;
throw new InvalidValueError('not one of ' + quotedNames.join(', '));
};
};
Validate.mutuallyExclusiveProperties = function(names) {
return function(value) {
if (!value) return value;
var present = [];
names.forEach(function(name) {
if (name in value) {
present.push('"' + name + '"');
}
});
if (present.length > 1) {
throw new InvalidValueError(
'cannot specify properties '
+ present.slice(0, -1).join(', ')
+ ' and '
+ present.slice(-1)
+ ' together');
}
return value;
};
};
Validate.compose = function(validators) {
return function(value) {
validators.forEach(function(validate) {
value = validate(value);
});
return value;
};
};
Validate.boolean = Validate.compose([
Validate.that(function(value) {
return typeof value === 'boolean';
}, 'not a boolean'),
function(value) {
// In each API, boolean fields default to false, and the presence of
// a querystring value indicates true, so we omit the value if
// explicitly set to false.
return value ? value : undefined;
}
]);
Validate.atLeastOneOfProperties = (names) => {
return function(value) {
if (!value) return value;
var present = [];
names.forEach(function(name) {
if (name in value) {
present.push('"' + name + '"');
}
});
if (present.length == 0) {
throw new InvalidValueError(`specify at least one of properties "${names.slice(0, -1).join(', "')}" and "${names.slice(-1)}"`)
}
return value;
};
};
Validate.mongoId = Validate.that(value => validator.isMongoId(value), 'not a mongoId');
Validate.lowercase = Validate.that(value => validator.isLowercase(value), 'not a lowercase');
Validate.uppercase = Validate.that(value => validator.isUppercase(value), 'not a uppercase');
Validate.numeric = Validate.that(value => validator.isNumeric(value), 'not numeric');
Validate.email = Validate.that(value => {
return validator.isEmail(value);
}, 'not an email');
Validate.phone = (format) => (value => validator.isMobilePhone(value, format || process.env.MOBILE_FORMAT));
Validate.emailNormalized = Validate.compose([
Validate.email,
Validate.that(value => validator.normalizeEmail(value) === value, 'is not normalized email')
]);
Validate.isNotEmpty = Validate.that(value => !validator.isEmpty(value), 'is empty')
Validate.dateString = Validate.compose([
Validate.string,
Validate.that(value => {
return moment(value, process.env.MOMENT_DATE_FORMAT).isValid()
}, 'is not a date time')
])
Validate.timeString = Validate.compose([
Validate.string,
Validate.that(value => {
return moment(value, process.env.MOMENT_TIME_FORMAT).isValid()
}, 'is not a valid time')
])
Validate.dateTimeString = Validate.compose([
Validate.string,
Validate.that(value => {
return moment(value, process.env.MOMENT_DATE_TIME_FORMAT).isValid()
}, 'is not valid date')
])
Validate.dateOfBirth = Validate.compose([
Validate.object({
day: Validate.number,
month: Validate.number,
year: Validate.number,
}),
Validate.that(payload => {
const dateOfBirthMoment = moment([
payload.year,
payload.month-1, //because moment month is 0 index based
payload.day,
]);
return dateOfBirthMoment.isValid();
}, 'Not a valid date')
])
Validate.weekDaysNumbers = Validate.compose([
Validate.array(Validate.number),
Validate.that(value => {
return value.length <= 7 && value.length >= 0
}, 'not more than 7 days'),
Validate.that(value => {
return value.every(v => {
return v > 0 && v < 8
})
}, 'value out of week range'),
Validate.that(value => R.uniq(value).length === value.length, 'not uniq, days repeated')
])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment