Last active
February 16, 2023 17:07
-
-
Save muhammadfaizan/499c0addfb8f469bd028cb1f11117a06 to your computer and use it in GitHub Desktop.
A comprehensive utility for validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
/*********************************************************/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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