UNPKG

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
//---------------------------------------- // 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 [] }