@valkyriestudios/validator
Version:
A lightweight configurable javascript validator
642 lines (641 loc) • 25.3 kB
JavaScript
"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();