UNPKG

@valkyriestudios/validator

Version:

A lightweight configurable javascript validator

642 lines (641 loc) 25.3 kB
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _Validator_plan, _Validator_schema; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.Validator = void 0; const is_1 = require("@valkyriestudios/utils/array/is"); const isNotEmpty_1 = require("@valkyriestudios/utils/array/isNotEmpty"); const is_2 = require("@valkyriestudios/utils/boolean/is"); const is_3 = require("@valkyriestudios/utils/date/is"); const freeze_1 = require("@valkyriestudios/utils/deep/freeze"); const get_1 = require("@valkyriestudios/utils/deep/get"); const is_4 = require("@valkyriestudios/utils/formdata/is"); const toObject_1 = require("@valkyriestudios/utils/formdata/toObject"); const is_5 = require("@valkyriestudios/utils/function/is"); const isAsync_1 = require("@valkyriestudios/utils/function/isAsync"); const is_6 = require("@valkyriestudios/utils/number/is"); const isInteger_1 = require("@valkyriestudios/utils/number/isInteger"); const is_7 = require("@valkyriestudios/utils/object/is"); const isNotEmpty_2 = require("@valkyriestudios/utils/object/isNotEmpty"); const is_8 = require("@valkyriestudios/utils/string/is"); const isNotEmpty_3 = require("@valkyriestudios/utils/string/isNotEmpty"); const equal_1 = require("@valkyriestudios/utils/equal"); const fnv1A_1 = require("@valkyriestudios/utils/hash/fnv1A"); const vAlphaNumSpaces_1 = require("./functions/vAlphaNumSpaces"); const vAlphaNumSpacesMultiline_1 = require("./functions/vAlphaNumSpacesMultiline"); const vBase64_1 = require("./functions/vBase64"); const vBetween_1 = require("./functions/vBetween"); const vBetweenInclusive_1 = require("./functions/vBetweenInclusive"); const vBlob_1 = require("./functions/vBlob"); const vColorHex_1 = require("./functions/vColorHex"); const vContinent_1 = require("./functions/vContinent"); const vCountry_1 = require("./functions/vCountry"); const vCountryAlpha3_1 = require("./functions/vCountryAlpha3"); const vDateString_1 = require("./functions/vDateString"); const vDateSpecs_1 = require("./functions/vDateSpecs"); const vEmail_1 = require("./functions/vEmail"); const vFalse_1 = require("./functions/vFalse"); const vFile_1 = require("./functions/vFile"); const vGeoLatitude_1 = require("./functions/vGeoLatitude"); const vGeoLongitude_1 = require("./functions/vGeoLongitude"); const vGreaterThan_1 = require("./functions/vGreaterThan"); const vGreaterThanOrEqual_1 = require("./functions/vGreaterThanOrEqual"); const vGuid_1 = require("./functions/vGuid"); const vIn_1 = require("./functions/vIn"); const vLessThan_1 = require("./functions/vLessThan"); const vLessThanOrEqual_1 = require("./functions/vLessThanOrEqual"); const vLiteral_1 = require("./functions/vLiteral"); const vNull_1 = require("./functions/vNull"); const vPhone_1 = require("./functions/vPhone"); const vTimeZone_1 = require("./functions/vTimeZone"); const vSize_1 = require("./functions/vSize"); const vSysMac_1 = require("./functions/vSysMac"); const vSysIPv4_1 = require("./functions/vSysIPv4"); const vSysIPv6_1 = require("./functions/vSysIPv6"); const vSysIPv4_or_v6_1 = require("./functions/vSysIPv4_or_v6"); const vSysPort_1 = require("./functions/vSysPort"); const vTrue_1 = require("./functions/vTrue"); const vISBN_1 = require("./functions/vISBN"); const vSSN_1 = require("./functions/vSSN"); const vEAN_1 = require("./functions/vEAN"); const vUlid_1 = require("./functions/vUlid"); const vUndefined_1 = require("./functions/vUndefined"); const vUuid_1 = require("./functions/vUuid"); const vUrl_1 = require("./functions/vUrl"); const vUrlNoQuery_1 = require("./functions/vUrlNoQuery"); const vUrlExtensions_1 = require("./functions/vUrlExtensions"); const RULE_STORE = { alpha_num_spaces: vAlphaNumSpaces_1.vAlphaNumSpaces, alpha_num_spaces_multiline: vAlphaNumSpacesMultiline_1.vAlphaNumSpacesMultiline, array: is_1.isArray, array_ne: isNotEmpty_1.isNotEmptyArray, base64: vBase64_1.vBase64, between: vBetween_1.vBetween, between_inc: vBetweenInclusive_1.vBetweenInclusive, blob: vBlob_1.vBlob, boolean: is_2.isBoolean, color_hex: vColorHex_1.vColorHex, continent: vContinent_1.vContinent, country: vCountry_1.vCountry, country_alpha3: vCountryAlpha3_1.vCountryAlpha3, date: is_3.isDate, date_day: vDateSpecs_1.vDateDay, date_iso: vDateSpecs_1.vDateISO, date_string: vDateString_1.vDateString, ean: vEAN_1.vEAN, ean_8: vEAN_1.vEAN8, ean_13: vEAN_1.vEAN13, email: vEmail_1.vEmail, equal_to: equal_1.equal, false: vFalse_1.vFalse, file: vFile_1.vFile, formdata: is_4.isFormData, function: is_5.isFunction, async_function: isAsync_1.isAsyncFunction, geo_latitude: vGeoLatitude_1.vGeoLatitude, geo_longitude: vGeoLongitude_1.vGeoLongitude, greater_than: vGreaterThan_1.vGreaterThan, greater_than_or_equal: vGreaterThanOrEqual_1.vGreaterThanOrEqual, guid: vGuid_1.vGuid, in: vIn_1.vIn, integer: isInteger_1.isInteger, isbn: vISBN_1.vISBN, isbn_10: vISBN_1.vISBN10, isbn_13: vISBN_1.vISBN13, less_than: vLessThan_1.vLessThan, less_than_or_equal: vLessThanOrEqual_1.vLessThanOrEqual, literal: vLiteral_1.vLiteral, max: vLessThanOrEqual_1.vLessThanOrEqual, min: vGreaterThanOrEqual_1.vGreaterThanOrEqual, null: vNull_1.vNull, number: is_6.isNumber, object: is_7.isObject, object_ne: isNotEmpty_2.isNotEmptyObject, phone: vPhone_1.vPhone, size: vSize_1.vSize, ssn: vSSN_1.vSSN, string: is_8.isString, string_ne: isNotEmpty_3.isNotEmptyString, sys_mac: vSysMac_1.vSysMac, sys_ipv4: vSysIPv4_1.vSysIPv4, sys_ipv6: vSysIPv6_1.vSysIPv6, sys_ipv4_or_v6: vSysIPv4_or_v6_1.vSysIPv4_or_v6, sys_port: vSysPort_1.vSysPort, time_zone: vTimeZone_1.vTimeZone, true: vTrue_1.vTrue, ulid: vUlid_1.vUlid, url: vUrl_1.vUrl, url_noquery: vUrlNoQuery_1.vUrlNoQuery, url_img: vUrlExtensions_1.vUrlImage, url_vid: vUrlExtensions_1.vUrlVideo, url_aud: vUrlExtensions_1.vUrlAudio, url_med: vUrlExtensions_1.vUrlMedia, uuid: vUuid_1.vUuid, uuid_v1: vUuid_1.vUuidV1, uuid_v2: vUuid_1.vUuidV2, uuid_v3: vUuid_1.vUuidV3, uuid_v4: vUuid_1.vUuidV4, uuid_v5: vUuid_1.vUuidV5, gt: vGreaterThan_1.vGreaterThan, gte: vGreaterThanOrEqual_1.vGreaterThanOrEqual, lt: vLessThan_1.vLessThan, lte: vLessThanOrEqual_1.vLessThanOrEqual, eq: equal_1.equal, '?': vUndefined_1.vUndefined, }; const NOEXISTS = () => false; const iterableDictHandler = (val) => { if (!(0, is_7.isObject)(val)) return null; const values = Object.values(val); return { len: values.length, values }; }; const iterableArrayHandler = (val) => { if (!Array.isArray(val)) return null; return { len: val.length, values: val }; }; function getIterableConfig(val, dict) { const unique = val.includes('unique'); const len = val.length; const max_ix = val.indexOf('max:'); const min_ix = val.indexOf('min:'); const rslt = { unique, max: Number.MAX_SAFE_INTEGER, min: -1, handler: dict ? iterableDictHandler : iterableArrayHandler, }; if (max_ix !== -1) { const end_ix = val.indexOf('|', max_ix); rslt.max = parseInt(val.slice(max_ix + 4, end_ix !== -1 ? end_ix : len), 10); } if (min_ix !== -1) { const end_ix = val.indexOf('|', min_ix); rslt.min = parseInt(val.slice(min_ix + 4, end_ix !== -1 ? end_ix : len), 10); } return rslt; } function parseRule(raw) { let iterable = false; let pos = 0; const len = raw.length; if (len > 0 && (raw[0] === '[' || raw[0] === '{')) { const closingChar = raw[0] === '[' ? ']' : '}'; let endPos = -1; for (let i = 1; i < len; i++) { if (raw[i] === closingChar) { endPos = i; break; } } if (endPos === -1) throw new TypeError(`Iterable misconfiguration, verify rule config for ${raw}`); iterable = getIterableConfig(raw.slice(0, endPos), raw[0] !== '['); pos = endPos + 1; } const list = []; while (pos < len) { const partStart = pos; while (pos < len && raw[pos] !== '|') pos++; const part = raw.slice(partStart, pos); let colonPos = -1; for (let i = 0; i < part.length; i++) { if (part[i] !== ':') continue; colonPos = i; break; } let ruleType; let not = false; let params = []; if (colonPos === -1) { ruleType = part; } else { ruleType = part.slice(0, colonPos); } if (ruleType[0] === '!') { not = true; ruleType = ruleType.slice(1); } if (colonPos !== -1) { const paramsPart = part.slice(colonPos + 1); let pPos = 0; const pLen = paramsPart.length; while (pPos < pLen) { const start = pPos; while (pPos < pLen && paramsPart[pPos] !== ',') pPos++; let token = paramsPart.slice(start, pPos); let extract = false; if (token[0] === '<' && token[token.length - 1] === '>') { token = token.slice(1, -1); if (!(0, isNotEmpty_3.isNotEmptyString)(token)) throw new TypeError('Parameterization misconfiguration'); extract = true; } params.push([token, extract]); pPos++; } if (ruleType === 'in' && params.length > 1) { params = [[paramsPart.split(','), false]]; } } list.push({ type: ruleType, not, msg: (not ? 'not_' : '') + ruleType, params, params_length: params.length }); pos++; } return { nested: false, iterable, list, list_length: list.length }; } function constructParams(rule_el, data) { const { params_length, params } = rule_el; const acc = new Array(params_length); for (let i = 0; i < params_length; i++) { const p = params[i]; acc[i] = !p[1] ? p[0] : (0, get_1.deepGet)(data, p[0]); } return acc; } function validateField(cursor, rule, data, idx) { const errors = []; for (let i = 0; i < rule.list_length; i++) { const rule_el = rule.list[i]; const rulefn = RULE_STORE[rule_el.type]; if (!rulefn) { errors.push({ msg: 'rule_not_found', params: [rule_el.type] }); continue; } const params = constructParams(rule_el, data); if (rulefn(cursor, ...params) === rule_el.not) { errors.push({ msg: rule_el.msg, params, ...idx !== undefined && { idx } }); } } return { errors, is_valid: errors.length === 0 }; } function validatePlan(data, plan) { const errors = {}; let count = 0; mainLoop: for (let i = 0; i < plan.length; i++) { const group = plan[i]; const { key, rules, sometimes } = group; const cursor = (0, get_1.deepGet)(data, key); if (cursor === undefined) { if (!sometimes) { count++; errors[key] = [{ msg: 'not_found', params: [] }]; } continue; } const group_errors = []; for (let x = 0; x < rules.length; x++) { const rule = rules[x]; let evaluation; if (rule.nested) { if (!(0, is_7.isObject)(cursor)) { evaluation = [{ msg: 'not_object', params: [] }]; } else { const result = validatePlan(data, rule.plan); if (result.is_valid) continue mainLoop; evaluation = result.errors; } } else if (!rule.iterable) { const result = validateField(cursor, rule, data); if (result.is_valid) continue mainLoop; evaluation = result.errors; } else { const iterable_data = rule.iterable.handler(cursor); if (!iterable_data) { evaluation = [{ msg: 'iterable', params: [] }]; } else { const { len, values } = iterable_data; const error_cursor = []; if (len < rule.iterable.min) { error_cursor.push({ msg: 'iterable_min', params: [rule.iterable.min] }); } else if (len > rule.iterable.max) { error_cursor.push({ msg: 'iterable_max', params: [rule.iterable.max] }); } else { let unique_set = rule.iterable.unique ? new Set() : false; for (let idx = 0; idx < len; idx++) { const cursor_value = values[idx]; const eval_field = validateField(cursor_value, rule, data, idx); if (!eval_field.is_valid) error_cursor.push(...eval_field.errors); if (unique_set) { const hash = (0, fnv1A_1.fnv1A)(cursor_value); if (unique_set.has(hash)) { unique_set = false; error_cursor.unshift({ msg: 'iterable_unique', params: [] }); } else { unique_set.add(hash); } } } } if (error_cursor.length === 0) continue mainLoop; evaluation = error_cursor; } } if (evaluation) group_errors.push(evaluation); } const error = group_errors[0]; if ((0, is_1.isArray)(error)) { if (group.rules.length > 1) { const normalized = []; for (let x = 0; x < group_errors.length; x++) { normalized.push(group_errors[x]); } errors[key] = normalized; } else { errors[key] = error; } count++; } else { Object.assign(errors, error); count += Object.keys(error).length; } } return { is_valid: count === 0, count, errors }; } function checkRule(cursor, rule, data) { const { iterable, list_length, list } = rule; if (!iterable) { for (let i = 0; i < list_length; i++) { const rule_el = list[i]; if ((RULE_STORE[rule_el.type] || NOEXISTS)(cursor, ...constructParams(rule_el, data)) === rule_el.not) return false; } return true; } const iterable_data = iterable.handler(cursor); if (!iterable_data) return false; const { len, values } = iterable_data; if (len < iterable.min || len > iterable.max) return false; const unique_set = new Set(); const param_acc = []; let cursor_value; for (let idx = 0; idx < len; idx++) { cursor_value = values[idx]; for (let i = 0; i < list_length; i++) { const rule_el = list[i]; if (!param_acc[i]) param_acc[i] = constructParams(rule_el, data); if ((RULE_STORE[rule_el.type] || NOEXISTS)(cursor_value, ...param_acc[i]) === rule_el.not) return false; } if (iterable.unique) { unique_set.add((0, fnv1A_1.fnv1A)(cursor_value)); if (unique_set.size !== (idx + 1)) return false; } } return true; } function checkPlan(data, plan) { mainloop: for (let i = 0; i < plan.length; i++) { const { key, sometimes, rules } = plan[i]; const cursor = (0, get_1.deepGet)(data, key); if (cursor === undefined) { if (!sometimes) return false; continue; } for (let x = 0; x < rules.length; x++) { const rule = rules[x]; if (!rule.nested) { if (checkRule(cursor, rule, data)) continue mainloop; } else if ((0, is_7.isObject)(cursor) && checkPlan(data, rule.plan)) { continue mainloop; } } return false; } return true; } function recursor(plan, val, key) { if (!val) throw new TypeError('Invalid rule value'); if ((0, is_8.isString)(val)) { const sometimes = val[0] === '?'; plan.push({ key, sometimes, rules: [parseRule(sometimes ? val.slice(1) : val)], }); } else if ((0, is_1.isArray)(val)) { let sometimes = false; const rules = []; for (let i = 0, len = val.length; i < len; i++) { const branch = val[i]; if (branch === '?') { sometimes = true; } else if (branch) { if ((0, is_8.isString)(branch)) { rules.push(parseRule(branch)); } else if ((0, is_7.isObject)(branch)) { const nested_plan = []; recursor(nested_plan, branch, key); rules.push({ nested: true, plan: nested_plan }); } } else { throw new TypeError('Invalid Conditional group alternative'); } } if (rules.length) { plan.push({ key, sometimes, rules }); } else { throw new TypeError('Invalid rule value'); } } else if ((0, is_7.isObject)(val)) { for (const val_key in val) { recursor(plan, val[val_key], key ? key + '.' + val_key : val_key); } } else { throw new TypeError('Invalid rule value'); } } function freezeStore(dict) { const store = {}; for (const key in dict) store[key] = dict[key]; return Object.freeze(store); } let FROZEN_RULE_STORE = freezeStore(RULE_STORE); class Validator { constructor(schema) { _Validator_plan.set(this, void 0); _Validator_schema.set(this, void 0); const raw = ((0, is_7.isObject)(schema) ? [schema] : (0, is_1.isArray)(schema) ? schema : []); if (!raw.length) throw new TypeError('Validator@ctor: Schema needs to be an object or array of objects'); const plans = []; for (let i = 0; i < raw.length; i++) { const plan = []; recursor(plan, raw[i], ''); plans.push(plan); } if (plans.length > 1) { __classPrivateFieldSet(this, _Validator_plan, { type: 'multi', plans: plans }, "f"); __classPrivateFieldSet(this, _Validator_schema, (0, freeze_1.deepFreeze)(raw), "f"); } else { __classPrivateFieldSet(this, _Validator_plan, { type: 'single', plan: plans[0] }, "f"); __classPrivateFieldSet(this, _Validator_schema, (0, freeze_1.deepFreeze)(raw[0]), "f"); } } get schema() { return JSON.parse(JSON.stringify(__classPrivateFieldGet(this, _Validator_schema, "f"))); } check(raw) { const data = raw instanceof FormData ? (0, toObject_1.toObject)(raw) : raw; if (!(0, is_7.isObject)(data)) return false; if (__classPrivateFieldGet(this, _Validator_plan, "f").type === 'multi') { for (let i = 0; i < __classPrivateFieldGet(this, _Validator_plan, "f").plans.length; i++) { const plan = __classPrivateFieldGet(this, _Validator_plan, "f").plans[i]; if (checkPlan(data, plan)) return true; } return false; } else { return checkPlan(data, __classPrivateFieldGet(this, _Validator_plan, "f").plan); } } checkForm(raw) { if (!(raw instanceof FormData)) return false; const data = (0, toObject_1.toObject)(raw); return this.check(data) ? data : false; } validate(raw) { const data = raw instanceof FormData ? (0, toObject_1.toObject)(raw) : raw; if (!(0, is_7.isObject)(data)) return { is_valid: false, errors: 'NO_DATA', count: 1 }; if (__classPrivateFieldGet(this, _Validator_plan, "f").type === 'multi') { const results = []; for (let i = 0; i < __classPrivateFieldGet(this, _Validator_plan, "f").plans.length; i++) { const result = validatePlan(data, __classPrivateFieldGet(this, _Validator_plan, "f").plans[i]); if (result.is_valid) return result; results.push(result); } return results[0]; } else { return validatePlan(data, __classPrivateFieldGet(this, _Validator_plan, "f").plan); } } static get rules() { return FROZEN_RULE_STORE; } static extend(obj) { if (!(0, is_7.isObject)(obj)) throw new Error('Invalid extension'); const schemas_map = {}; for (const [key, val] of Object.entries(obj)) { if (typeof key !== 'string' || !/^[A-Za-z_0-9-]+$/.test(key)) throw new Error('Invalid extension'); if (val instanceof RegExp || ((0, isNotEmpty_1.isNotEmptyArray)(val) && val.filter(el => (0, isNotEmpty_3.isNotEmptyString)(el) || Number.isFinite(el)).length === val.length) || ((0, is_5.isFunction)(val) && !(0, isAsync_1.isAsyncFunction)(val))) continue; if ((0, isNotEmpty_2.isNotEmptyObject)(val)) { try { schemas_map[key] = new Validator(val); continue; } catch { throw new Error('Invalid extension'); } } throw new Error('Invalid extension'); } for (const [key, value] of Object.entries(obj)) { let builtValue; if (value instanceof RegExp) { builtValue = function (val) { return typeof val === 'string' && this.rgx.test(val); }; builtValue.rgx = new RegExp(value); builtValue = builtValue.bind(builtValue); } else if (Array.isArray(value)) { builtValue = function (val) { return this.set.has(val); }; builtValue.set = new Set([...value]); builtValue = builtValue.bind(builtValue); } else if ((0, is_7.isObject)(value)) { builtValue = function (val) { return this.v.check(val); }; builtValue.v = schemas_map[key]; builtValue = builtValue.bind(builtValue); } else { builtValue = value; } RULE_STORE[key] = builtValue; } FROZEN_RULE_STORE = freezeStore(RULE_STORE); return this; } static create(schema) { if ((0, isNotEmpty_1.isNotEmptyArray)(schema) && schema[0] instanceof Validator) { const normalized = []; for (let i = 0; i < schema.length; i++) normalized.push(schema[i].schema); return new Validator(normalized); } else { return new Validator(schema); } } } exports.Validator = Validator; exports.default = Validator; _Validator_plan = new WeakMap(), _Validator_schema = new WeakMap();