topkat-utils
Version:
A comprehensive collection of TypeScript/JavaScript utility functions for common programming tasks. Includes validation, object manipulation, date handling, string formatting, and more. Zero dependencies, fully typed, and optimized for performance.
268 lines (231 loc) • 13.5 kB
text/typescript
//----------------------------------------
// VALIDATION UTILS
//----------------------------------------
import { isset } from './isset'
import { isDateIsoOrObjectValid, isDateIntOrStringValid, isTimeStringValid } from './date-utils'
import { asArray } from './array-utils'
import { configFn } from './config'
import { isEmpty } from './is-empty'
import { DescriptiveError } from './error-utils'
import { removeCircularJSONstringify } from './remove-circular-json-stringify'
export type BaseTypes = 'objectId' | 'dateInt6' | 'dateInt' | 'dateInt8' | 'dateInt12' | 'time' | 'humanReadableTimestamp' | 'date' | 'dateObject' | 'array' | 'object' | 'buffer' | 'string' | 'function' | 'boolean' | 'number' | 'bigint' | 'year' | 'email' | 'any'
export function issetOr(...elms) { return elms.some(elm => typeof elm !== 'undefined' && elm !== null) }
export function isEmptyOrNotSet(...elms) { return elms.some(elm => !isset(elm) || isEmpty(elm)) }
export function isDateObject(variable) { return variable instanceof Date }
/** Check all values are set */
export function checkAllObjectValuesAreEmpty(o) { return Object.values(o).every(value => !isset(value)) }
/** Throw an error in case data passed is not a valid ctx */
export function checkCtxIntegrity(ctx) {
if (!isset(ctx) || !isset(ctx.user)) throw new DescriptiveError('ctxNotSet', { code: 500 })
}
/**
## VALIDATOR
@name validator
@description support multiple names, multiple values and multiple type check
@option if nameString ends by $ sign it is considered optional
@function validator([Objects])
@return {error|true/false|testMode} depend on mode (see prop mode)
@param {} mode normal (default) | test (TODO) | boolean
@param {} name 'myName' || [{myVar1: 'blah, myvar2: myvar2}], support multiple names / values
@param {} value myVar,
@param {string} myVar myVar, instead of name / value
@param {array} in ['blah', 'otherPossibleValue', true], equal ONE OF THESE values
@param {any} eq exactly equal to in, both support string or array of values
@param {any} neq not in, both support string or array of values
@param {number} lte 3, less than or equal
@param {number} gte 1, greater or equal
@param {number} lt 3, less than
@param {number} gt 1, greater
@param {string|string[]} type
* possibleTypes: object, number, string, boolean, array, date, dateInt8, dateInt12, dateInt6, time, objectId (mongo), humanReadableTimestamp, buffer
* Notes: multiples value is an OR, /!\ Array is type 'array' and not 'object' like in real JS /!\
@param {regExp} regexp /regexp/, test against regexp
@param {number} minLength for string, array or number length
@param {number} maxLength
@param {number} length
@param {boolean} optional default false
@param {boolean} emptyAllowed default false (to use if must be set but can be empty)
@param {boolean} mustNotBeSet this one must not be set
@param {any} includes check if array or string includes value (like js .includes())
@example
validator(
{ myNumber : 3, type: 'number', gte: 1, lte: 3 }, // use the name directly as a param
{ name: 'email', value: 'nameATsite.com', regexp: /[^\sAT]+AT[^\sAT]+\.[^\sAT]/},
{ name: [{'blahVar': blahVarValue, 'myOtherVar': myOtherVarValue}], type: 'string'} // multiple names for same check
)
----------------------------------------*/
export type ValidatorObject = {
name?: string
value?: any
type?: BaseTypes
eq?: any
neq?: any
in?: any[]
lt?: number
gt?: number
lte?: number
gte?: number
length?: number
minLength?: number
maxLength?: number
emptyAllowed?: boolean
regexp?: RegExp
mustNotBeSet?: boolean
isset?: boolean
optional?: boolean
isArray?: boolean
includes?: any
[k: string]: any
}
export function validator(...paramsToValidate: ValidatorObject[]) {
const errArray = validatorReturnErrArray(...paramsToValidate)
if (errArray.length) throw new DescriptiveError(...(errArray as [string, object]))
}
/** Same as validator but return a boolean
* See {@link validator}
*/
export function isValid(...paramsToValidate: ValidatorObject[]) {
const errArray = validatorReturnErrArray(...paramsToValidate)
return errArray.length ? false : true
}
function parseValueForDisplay(value?) {
try {
if (value === undefined) return 'undefined'
else if (value?.data?.data) return { ...value, data: 'Buffer' }
else return removeCircularJSONstringify(value).substring(0, 999)
} catch (_) {
return value
}
}
/** Default types + custom types
* 'objectId','dateInt6','dateInt','dateInt8','dateInt12','time','humanReadableTimestamp','date','array','object','buffer','string','function','boolean','number','bigint',
*/
export function isType(value, type: BaseTypes) { return isValid({ name: 'Is type check', value, type, emptyAllowed: true }) }
export function validatorReturnErrArray(...paramsToValidate: ValidatorObject[]): [string?, object?] {
const paramsFormatted: ValidatorObject[] = []
// support for multiple names with multiple values for one rule. Eg: {name: [{startDate:'20180101'}, {endDate:'20180101'}], type: 'dateInt8'}
paramsToValidate.forEach(param => {
if (typeof param !== 'object' || Array.isArray(param))
throw new DescriptiveError(`wrongTypeForDataValidatorArgument`, { code: 500, origin: 'Generic validator', expectedType: 'object', actualType: Array.isArray(param) ? 'array' : typeof param })
// parse => name: {myVar1: 'blah, myvar2: myvar2}
if (typeof param.name === 'object' && !Array.isArray(param.name))
Object.keys(param.name).forEach(name => paramsFormatted.push(Object.assign({}, param, { name: name, value: param?.name?.[name] })))
else paramsFormatted.push(param)
})
for (const paramObj of paramsFormatted) {
let name = paramObj.name
const hasValue = paramObj.hasOwnProperty('value')
let value = paramObj.value
let optional = paramObj.optional || false
const emptyAllowed = optional || paramObj.emptyAllowed || false
if (paramObj.isset === false) paramObj.mustNotBeSet = true // ALIAS
const errMess = (msg, extraInfos = {}, errCode = 422): [string, object] => [msg, { code: errCode, origin: 'Generic validator', varName: name, varType: typeof value, gotValue: parseValueForDisplay(value), ...extraInfos }]
// accept syntax { 'myVar.var2': myVar.var2, ... }
if (typeof name !== 'undefined' && !hasValue) {
name = Object.keys(paramObj).find(param => !['name', 'value', 'type', 'eq', 'neq', 'in', 'lt', 'gt', 'lte', 'gte', 'length', 'minLength', 'maxLength', 'emptyAllowed', 'regexp', 'mustNotBeSet', 'isset', 'optional', 'isArray'].includes(param))
if (typeof name !== 'undefined') value = paramObj[name]
}
// if nameString ends by $ sign it is optional
if (typeof name !== 'undefined' && /.*\$$/.test(name)) {
name = name.substr(0, name.length - 1)
optional = true
}
// DEFINED AND NOT EMPTY
if ((typeof value === 'undefined') && optional) continue
if (typeof value !== 'undefined' && value !== null && paramObj.mustNotBeSet) return errMess('variableMustNotBeSet')
if (paramObj.mustNotBeSet) continue // exit
if (typeof value === 'undefined') return errMess('requiredVariableEmptyOrNotSet')
if (!emptyAllowed && value === '') return errMess('requiredVariableEmpty')
const isArray = paramObj.isArray
if (isArray && !Array.isArray(value)) return errMess('wrongTypeForVar', { expectedType: 'array', gotType: typeof value })
// TYPE
if (typeof paramObj.type !== 'undefined') {
const types = asArray(paramObj.type) // support for multiple type
const areSomeTypeValid = types.some(type => {
if (type.endsWith('[]')) {
if (!Array.isArray(value)) errMess('wrongTypeForVar', { expectedType: 'array', gotType: typeof value })
type = type.replace('[]', '')
}
const allTypes: Array<BaseTypes> = [
'objectId',
'dateInt6',
'dateInt', // alias for dateInt8
'dateInt8',
'dateInt12',
'time',
'humanReadableTimestamp',
'date',
'dateObject', // alias
'array',
'object',
'buffer',
'string',
'function',
'boolean',
'number',
'bigint',
'year',
'any',
'email',
//...Object.keys(configFn().customTypes)
]
if (!allTypes.includes(type)) throw new DescriptiveError('typeDoNotExist', { code: 500, type })
const basicTypeCheck = {
objectId: val => /^[0-9a-fA-F-]{24,}$/.test(val), // "0c65940b-6b0c-4dd8-9c7a-7c5fe1ba907a"
dateInt6: val => isDateIntOrStringValid(parseInt(val + '01'), true, 8),
dateInt: val => isDateIntOrStringValid(val, true, 8),
dateInt8: val => isDateIntOrStringValid(val, true, 8),
dateInt12: val => isDateIntOrStringValid(val, true, 12),
time: val => /^\d\d:\d\d$/.test(val) && isTimeStringValid(val),
humanReadableTimestamp: val => (val + '').length === 17,
date: val => isDateIsoOrObjectValid(val, true),
dateObject: val => isDateIsoOrObjectValid(val, true),
array: val => Array.isArray(val),
object: val => !Array.isArray(val) && val !== null && typeof val === type,
buffer: val => Buffer.isBuffer(val),
year: val => /^\d\d\d\d$/.test(val),
email: val => /^[^\s@]+@([^\s@.,]+\.)+[^\s@.,]+$/.test(val),
any: () => true,
}
return typeof basicTypeCheck?.[type] !== 'undefined' && basicTypeCheck?.[type](value) ||
typeof value === type && type !== 'object' || // for string, number, boolean...
typeof configFn()?.customTypes?.[type] !== 'undefined' && configFn()?.customTypes?.[type]?.test(value)
})
if (!areSomeTypeValid) return errMess(`wrongTypeForVar`, { expectedTypes: types.join(', '), gotType: Object.prototype.toString.call(value), gotValue: parseValueForDisplay(value) })
}
// GREATER / LESS
if (typeof paramObj.gte !== 'undefined' && value < paramObj.gte) return errMess(`valueShouldBeSuperiorOrEqualForVar`, { shouldBeSupOrEqTo: paramObj.gte })
if (typeof paramObj.lte !== 'undefined' && value > paramObj.lte) return errMess(`valueShouldBeInferiorOrEqualForVar`, { shouldBeInfOrEqTo: paramObj.lte })
if (typeof paramObj.gt !== 'undefined' && value <= paramObj.gt) return errMess(`valueShouldBeSuperiorForVar`, { shouldBeSupOrEqTo: paramObj.gt })
if (typeof paramObj.lt !== 'undefined' && value >= paramObj.lt) return errMess(`valueShouldBeInferiorForVar`, { shouldBeInfOrEqTo: paramObj.lt })
// IN VALUES
if (typeof paramObj.in !== 'undefined') {
const equals = Array.isArray(paramObj.in) ? paramObj.in : [paramObj.in]
if (!equals.some(equalVal => equalVal === value)) return errMess(`wrongValueForVar`, { supportedValues: parseValueForDisplay(equals) })
}
// EQUAL (exact copy of .in)
if (paramObj.hasOwnProperty('eq')) {
const equals = Array.isArray(paramObj.eq) ? paramObj.eq : [paramObj.eq]
if (!equals.some(equalVal => equalVal === value)) return errMess(`wrongValueForVar`, { supportedValues: parseValueForDisplay(equals), })
}
// NOT EQUAL
if (paramObj.hasOwnProperty('neq')) {
const notEquals = Array.isArray(paramObj.neq) ? paramObj.neq : [paramObj.neq]
if (notEquals.some(equalVal => equalVal === value)) return errMess(`wrongValueForVar`, { NOTsupportedValues: parseValueForDisplay(notEquals) })
}
// INCLUDES
if (typeof paramObj.includes !== 'undefined' && !value.includes(paramObj.includes))
return errMess(`wrongValueForVar`, { shouldIncludes: paramObj.includes })
// REGEXP
if (typeof paramObj.regexp !== 'undefined' && !paramObj.regexp.test(value))
return errMess(`wrongValueForVar`, { shouldMatchRegexp: paramObj.regexp.toString() })
// MIN / MAX LENGTH works for number length. Eg: 20180101.length == 8
if (typeof paramObj.minLength !== 'undefined' && paramObj.minLength > (typeof value == 'number' ? value + '' : value).length)
return errMess(`wrongLengthForVar`, { minLength: paramObj.minLength })
if (typeof paramObj.maxLength !== 'undefined' && paramObj.maxLength < (typeof value == 'number' ? value + '' : value).length)
return errMess(`wrongLengthForVar`, { maxLength: paramObj.maxLength })
if (typeof paramObj.length !== 'undefined' && paramObj.length !== (typeof value == 'number' ? value + '' : value).length)
return errMess(`wrongLengthForVar`, { length: paramObj.length })
}
return []
}