perfect-validator
Version:
A TypeScript-based validation library that supports both static and dynamic validation with serializable models.
1,511 lines (1,504 loc) • 72.1 kB
JavaScript
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function asyncGeneratorStep(n, t, e, r, o, a, c) {
try {
var i = n[a](c),
u = i.value;
} catch (n) {
return void e(n);
}
i.done ? t(u) : Promise.resolve(u).then(r, o);
}
function _asyncToGenerator(n) {
return function () {
var t = this,
e = arguments;
return new Promise(function (r, o) {
var a = n.apply(t, e);
function _next(n) {
asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
}
function _throw(n) {
asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
}
_next(void 0);
});
};
}
function _construct(t, e, r) {
if (_isNativeReflectConstruct()) return Reflect.construct.apply(null, arguments);
var o = [null];
o.push.apply(o, e);
var p = new (t.bind.apply(t, o))();
return r && _setPrototypeOf(p, r.prototype), p;
}
function _createForOfIteratorHelperLoose(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (t) return (t = t.call(r)).next.bind(t);
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
t && (r = t);
var o = 0;
return function () {
return o >= r.length ? {
done: !0
} : {
done: !1,
value: r[o++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _extends() {
return _extends = Object.assign ? Object.assign.bind() : function (n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
}
return n;
}, _extends.apply(null, arguments);
}
function _isNativeReflectConstruct() {
try {
var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
} catch (t) {}
return (_isNativeReflectConstruct = function () {
return !!t;
})();
}
function _regeneratorRuntime() {
_regeneratorRuntime = function () {
return e;
};
var t,
e = {},
r = Object.prototype,
n = r.hasOwnProperty,
o = Object.defineProperty || function (t, e, r) {
t[e] = r.value;
},
i = "function" == typeof Symbol ? Symbol : {},
a = i.iterator || "@@iterator",
c = i.asyncIterator || "@@asyncIterator",
u = i.toStringTag || "@@toStringTag";
function define(t, e, r) {
return Object.defineProperty(t, e, {
value: r,
enumerable: !0,
configurable: !0,
writable: !0
}), t[e];
}
try {
define({}, "");
} catch (t) {
define = function (t, e, r) {
return t[e] = r;
};
}
function wrap(t, e, r, n) {
var i = e && e.prototype instanceof Generator ? e : Generator,
a = Object.create(i.prototype),
c = new Context(n || []);
return o(a, "_invoke", {
value: makeInvokeMethod(t, r, c)
}), a;
}
function tryCatch(t, e, r) {
try {
return {
type: "normal",
arg: t.call(e, r)
};
} catch (t) {
return {
type: "throw",
arg: t
};
}
}
e.wrap = wrap;
var h = "suspendedStart",
l = "suspendedYield",
f = "executing",
s = "completed",
y = {};
function Generator() {}
function GeneratorFunction() {}
function GeneratorFunctionPrototype() {}
var p = {};
define(p, a, function () {
return this;
});
var d = Object.getPrototypeOf,
v = d && d(d(values([])));
v && v !== r && n.call(v, a) && (p = v);
var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p);
function defineIteratorMethods(t) {
["next", "throw", "return"].forEach(function (e) {
define(t, e, function (t) {
return this._invoke(e, t);
});
});
}
function AsyncIterator(t, e) {
function invoke(r, o, i, a) {
var c = tryCatch(t[r], t, o);
if ("throw" !== c.type) {
var u = c.arg,
h = u.value;
return h && "object" == typeof h && n.call(h, "__await") ? e.resolve(h.__await).then(function (t) {
invoke("next", t, i, a);
}, function (t) {
invoke("throw", t, i, a);
}) : e.resolve(h).then(function (t) {
u.value = t, i(u);
}, function (t) {
return invoke("throw", t, i, a);
});
}
a(c.arg);
}
var r;
o(this, "_invoke", {
value: function (t, n) {
function callInvokeWithMethodAndArg() {
return new e(function (e, r) {
invoke(t, n, e, r);
});
}
return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg();
}
});
}
function makeInvokeMethod(e, r, n) {
var o = h;
return function (i, a) {
if (o === f) throw Error("Generator is already running");
if (o === s) {
if ("throw" === i) throw a;
return {
value: t,
done: !0
};
}
for (n.method = i, n.arg = a;;) {
var c = n.delegate;
if (c) {
var u = maybeInvokeDelegate(c, n);
if (u) {
if (u === y) continue;
return u;
}
}
if ("next" === n.method) n.sent = n._sent = n.arg;else if ("throw" === n.method) {
if (o === h) throw o = s, n.arg;
n.dispatchException(n.arg);
} else "return" === n.method && n.abrupt("return", n.arg);
o = f;
var p = tryCatch(e, r, n);
if ("normal" === p.type) {
if (o = n.done ? s : l, p.arg === y) continue;
return {
value: p.arg,
done: n.done
};
}
"throw" === p.type && (o = s, n.method = "throw", n.arg = p.arg);
}
};
}
function maybeInvokeDelegate(e, r) {
var n = r.method,
o = e.iterator[n];
if (o === t) return r.delegate = null, "throw" === n && e.iterator.return && (r.method = "return", r.arg = t, maybeInvokeDelegate(e, r), "throw" === r.method) || "return" !== n && (r.method = "throw", r.arg = new TypeError("The iterator does not provide a '" + n + "' method")), y;
var i = tryCatch(o, e.iterator, r.arg);
if ("throw" === i.type) return r.method = "throw", r.arg = i.arg, r.delegate = null, y;
var a = i.arg;
return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, "return" !== r.method && (r.method = "next", r.arg = t), r.delegate = null, y) : a : (r.method = "throw", r.arg = new TypeError("iterator result is not an object"), r.delegate = null, y);
}
function pushTryEntry(t) {
var e = {
tryLoc: t[0]
};
1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e);
}
function resetTryEntry(t) {
var e = t.completion || {};
e.type = "normal", delete e.arg, t.completion = e;
}
function Context(t) {
this.tryEntries = [{
tryLoc: "root"
}], t.forEach(pushTryEntry, this), this.reset(!0);
}
function values(e) {
if (e || "" === e) {
var r = e[a];
if (r) return r.call(e);
if ("function" == typeof e.next) return e;
if (!isNaN(e.length)) {
var o = -1,
i = function next() {
for (; ++o < e.length;) if (n.call(e, o)) return next.value = e[o], next.done = !1, next;
return next.value = t, next.done = !0, next;
};
return i.next = i;
}
}
throw new TypeError(typeof e + " is not iterable");
}
return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, "constructor", {
value: GeneratorFunctionPrototype,
configurable: !0
}), o(GeneratorFunctionPrototype, "constructor", {
value: GeneratorFunction,
configurable: !0
}), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, "GeneratorFunction"), e.isGeneratorFunction = function (t) {
var e = "function" == typeof t && t.constructor;
return !!e && (e === GeneratorFunction || "GeneratorFunction" === (e.displayName || e.name));
}, e.mark = function (t) {
return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, "GeneratorFunction")), t.prototype = Object.create(g), t;
}, e.awrap = function (t) {
return {
__await: t
};
}, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function () {
return this;
}), e.AsyncIterator = AsyncIterator, e.async = function (t, r, n, o, i) {
void 0 === i && (i = Promise);
var a = new AsyncIterator(wrap(t, r, n, o), i);
return e.isGeneratorFunction(r) ? a : a.next().then(function (t) {
return t.done ? t.value : a.next();
});
}, defineIteratorMethods(g), define(g, u, "Generator"), define(g, a, function () {
return this;
}), define(g, "toString", function () {
return "[object Generator]";
}), e.keys = function (t) {
var e = Object(t),
r = [];
for (var n in e) r.push(n);
return r.reverse(), function next() {
for (; r.length;) {
var t = r.pop();
if (t in e) return next.value = t, next.done = !1, next;
}
return next.done = !0, next;
};
}, e.values = values, Context.prototype = {
constructor: Context,
reset: function (e) {
if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = "next", this.arg = t, this.tryEntries.forEach(resetTryEntry), !e) for (var r in this) "t" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t);
},
stop: function () {
this.done = !0;
var t = this.tryEntries[0].completion;
if ("throw" === t.type) throw t.arg;
return this.rval;
},
dispatchException: function (e) {
if (this.done) throw e;
var r = this;
function handle(n, o) {
return a.type = "throw", a.arg = e, r.next = n, o && (r.method = "next", r.arg = t), !!o;
}
for (var o = this.tryEntries.length - 1; o >= 0; --o) {
var i = this.tryEntries[o],
a = i.completion;
if ("root" === i.tryLoc) return handle("end");
if (i.tryLoc <= this.prev) {
var c = n.call(i, "catchLoc"),
u = n.call(i, "finallyLoc");
if (c && u) {
if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
} else if (c) {
if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
} else {
if (!u) throw Error("try statement without catch or finally");
if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
}
}
}
},
abrupt: function (t, e) {
for (var r = this.tryEntries.length - 1; r >= 0; --r) {
var o = this.tryEntries[r];
if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) {
var i = o;
break;
}
}
i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null);
var a = i ? i.completion : {};
return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a);
},
complete: function (t, e) {
if ("throw" === t.type) throw t.arg;
return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y;
},
finish: function (t) {
for (var e = this.tryEntries.length - 1; e >= 0; --e) {
var r = this.tryEntries[e];
if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y;
}
},
catch: function (t) {
for (var e = this.tryEntries.length - 1; e >= 0; --e) {
var r = this.tryEntries[e];
if (r.tryLoc === t) {
var n = r.completion;
if ("throw" === n.type) {
var o = n.arg;
resetTryEntry(r);
}
return o;
}
}
throw Error("illegal catch attempt");
},
delegateYield: function (e, r, n) {
return this.delegate = {
iterator: values(e),
resultName: r,
nextLoc: n
}, "next" === this.method && (this.arg = t), y;
}
}, e;
}
function _setPrototypeOf(t, e) {
return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {
return t.__proto__ = e, t;
}, _setPrototypeOf(t, e);
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
var PerfectValidator;
(function (PerfectValidator) {
PerfectValidator.ValidationTypes = {
STRING: 'S',
NUMBER: 'N',
BOOLEAN: 'B',
LIST: 'L',
MAP: 'M',
EMAIL: 'EMAIL',
URL: 'URL',
DATE: 'DATE',
PHONE: 'PHONE',
REGEX: 'REGEX'
};
PerfectValidator.DataTypeDescriptions = {
S: 'String type - Text values with optional length and pattern constraints',
N: 'Number type - Numeric values with optional range constraints',
B: 'Boolean type - True/false values',
L: 'List type - Array of values with type validation',
M: 'Map type - Object with defined field structure',
EMAIL: 'Email type - Valid email address format',
URL: 'URL type - Valid URL format',
DATE: 'Date type - Valid date format',
PHONE: 'Phone type - Valid phone number format',
REGEX: 'Regex type - Custom pattern matching'
};
})(PerfectValidator || (PerfectValidator = {}));
// Type guards
function isValidationError(response) {
return response !== null && typeof response === 'object' && 'isValid' in response && !response.isValid;
}
// Constants
var VALID_TYPES = ['S', 'N', 'B', 'L', 'M', 'EMAIL', 'URL', 'DATE', 'PHONE', 'REGEX'];
// Regex patterns
var PATTERNS = {
EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+$/,
URL: /^(https?:\/\/)([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(:[0-9]{1,5})?(\/[^\s]*)?$/,
PHONE: /^\+?[1-9]\d{0,2}[-.\s]?\(?\d{2,4}\)?[-.\s]?\d{3,4}[-.\s]?\d{3,4}$/,
DATE: /^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(?:T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-]([01]\d|2[0-3]):[0-5]\d)?)?$/
};
// Helper functions
function getNestedValue(obj, path) {
// Split path into parts, handling array notation
var parts = path.split(/\.|\[|\]/).filter(Boolean);
var current = obj;
for (var _iterator = _createForOfIteratorHelperLoose(parts), _step; !(_step = _iterator()).done;) {
var part = _step.value;
if (!current) return undefined;
// If it's a number, treat as array index
if (/^\d+$/.test(part)) {
current = current[parseInt(part, 10)];
} else {
current = current[part];
}
}
return current;
}
function parseArrayType(type) {
if (type.startsWith('L<') && type.endsWith('>')) {
return {
isArray: true,
elementType: type.slice(2, -1)
};
}
return {
isArray: false,
elementType: type
};
}
function isValidationRule(rule) {
return typeof rule === 'object' && rule !== null;
}
function applyDefaults(data, model) {
var result = _extends({}, data);
function getDefaultValue(rule) {
// Only apply defaults if explicitly specified
if (rule["default"] !== undefined) {
return typeof rule["default"] === 'function' ? rule["default"]() : JSON.parse(JSON.stringify(rule["default"]));
}
// Don't apply type-based defaults automatically
return undefined;
}
Object.entries(model).forEach(function (_ref) {
var key = _ref[0],
rule = _ref[1];
if (result[key] === undefined && isValidationRule(rule) && !rule.optional) {
result[key] = getDefaultValue(rule);
}
});
return result;
}
function validateType(value, type, rule) {
switch (type) {
case 'S':
return typeof value === 'string';
case 'N':
if (typeof value !== 'number' || isNaN(value)) return false;
return true;
case 'B':
return typeof value === 'boolean';
case 'L':
return Array.isArray(value);
case 'M':
return typeof value === 'object' && value !== null && !Array.isArray(value);
case 'EMAIL':
return typeof value === 'string' && PATTERNS.EMAIL.test(value);
case 'URL':
return typeof value === 'string' && PATTERNS.URL.test(value);
case 'DATE':
return typeof value === 'string' && PATTERNS.DATE.test(value);
case 'PHONE':
return typeof value === 'string' && PATTERNS.PHONE.test(value);
case 'REGEX':
return typeof value === 'string' && (rule != null && rule.pattern ? new RegExp(rule.pattern).test(value) : false);
default:
return false;
}
}
function getTypeDescription(rule) {
if (typeof rule === 'string') return rule;
var type = rule.type;
if (!type) return 'unknown';
switch (type) {
case 'S':
return "string" + (rule.minLength ? " (min length: " + rule.minLength + ")" : '') + (rule.maxLength ? " (max length: " + rule.maxLength + ")" : '');
case 'N':
return "number" + (rule.min !== undefined ? " (min: " + rule.min + ")" : '') + (rule.max !== undefined ? " (max: " + rule.max + ")" : '') + (rule.decimal ? ' (decimal)' : '');
case 'EMAIL':
return 'email address';
case 'URL':
return 'URL';
case 'DATE':
return 'date (YYYY-MM-DD)';
case 'PHONE':
return 'phone number';
case 'B':
return 'boolean';
case 'L':
return "array" + (rule.items ? " of " + getTypeDescription(rule.items) : '');
case 'REGEX':
return "string matching pattern: " + rule.pattern;
default:
return type;
}
}
function validateDataModel(model) {
var errors = [];
function validateField(field, path) {
// Handle simple string type
if (typeof field === 'string') {
if (!VALID_TYPES.includes(field)) {
errors.push("Invalid type \"" + field + "\" at " + path);
}
return;
}
// Must be an object if not a string
if (typeof field !== 'object' || field === null) {
errors.push("Invalid field definition at " + path);
return;
}
var typedField = field;
// Check for valid type if specified
if (typedField.type && !VALID_TYPES.includes(typedField.type)) {
errors.push("Invalid type \"" + typedField.type + "\" at " + path);
}
// Validate values array if present
if (typedField.values !== undefined) {
if (!Array.isArray(typedField.values) || typedField.values.length === 0) {
errors.push("Invalid values at " + path);
}
}
// Validate min/max for numbers
if (typedField.type === 'N') {
if (typedField.min !== undefined && typeof typedField.min !== 'number') {
errors.push("Invalid min value at " + path);
}
if (typedField.max !== undefined && typeof typedField.max !== 'number') {
errors.push("Invalid max value at " + path);
}
if (typedField.min !== undefined && typedField.max !== undefined && typedField.min > typedField.max) {
errors.push("Invalid range at " + path);
}
if (typedField.integer !== undefined && typeof typedField.integer !== 'boolean') {
errors.push("Invalid integer flag at " + path);
}
}
// Validate dependencies
if (typedField.dependsOn) {
var deps = Array.isArray(typedField.dependsOn) ? typedField.dependsOn : [typedField.dependsOn];
deps.forEach(function (dep, index) {
var depPath = path + ".dependsOn[" + index + "]";
if (typeof dep !== 'object' || dep === null) {
errors.push("Invalid dependency definition at " + depPath);
return;
}
if (!dep.field) {
errors.push("Missing field in dependency at " + depPath);
}
if (!dep.condition || typeof dep.condition !== 'function') {
errors.push("Missing or invalid condition function at " + depPath);
}
if (!dep.validate || typeof dep.validate !== 'function') {
errors.push("Missing or invalid validate function at " + depPath);
}
});
}
// Validate nested fields
if (typedField.fields) {
if (typeof typedField.fields !== 'object' || typedField.fields === null) {
errors.push("Invalid fields definition at " + path);
return;
}
Object.entries(typedField.fields).forEach(function (_ref2) {
var key = _ref2[0],
value = _ref2[1];
validateField(value, path ? path + "." + key : key);
});
}
// Validate array items
if (typedField.items) {
validateField(typedField.items, path + ".items");
}
}
// Validate each field in the model
Object.entries(model).forEach(function (_ref3) {
var key = _ref3[0],
field = _ref3[1];
validateField(field, key);
});
return {
isValid: errors.length === 0,
errors: errors.length > 0 ? errors : null
};
}
function validateAgainstModel(data, model, allowUnknownFields, parentPath) {
if (allowUnknownFields === void 0) {
allowUnknownFields = false;
}
if (parentPath === void 0) {
parentPath = '';
}
var errors = [];
var dataWithDefaults = applyDefaults(data, model);
if (typeof dataWithDefaults === 'object' && dataWithDefaults !== null && !allowUnknownFields) {
var modelKeys = Object.keys(model);
var dataKeys = Object.keys(dataWithDefaults);
dataKeys.forEach(function (dataKey) {
// Check if a key from the data exists in the model definition
if (!modelKeys.includes(dataKey)) {
var fieldPath = parentPath ? parentPath + "." + dataKey : dataKey;
errors.push({
field: fieldPath,
message: "Unexpected field '" + dataKey + "' found in data."
});
}
});
}
// **End of New Check**
// If extraneous fields were found, return early
if (errors.length > 0) {
return {
isValid: false,
errors: errors
};
}
function validateValue(value, rule, path) {
// Handle undefined for optional fields
if (value === undefined) {
var isRequiredByDependency = false;
// Check if there's a dependency that might make this required
if (typeof rule === 'object' && rule.dependsOn) {
var deps = Array.isArray(rule.dependsOn) ? rule.dependsOn : [rule.dependsOn];
for (var _iterator2 = _createForOfIteratorHelperLoose(deps), _step2; !(_step2 = _iterator2()).done;) {
var dep = _step2.value;
var depPath = dep.field.includes('.') ? dep.field : parentPath ? parentPath + "." + dep.field : dep.field;
var depValue = getNestedValue(dataWithDefaults, depPath);
if (dep.condition(depValue)) {
// Check if the dependency explicitly marks this as required
if (dep.isRequired) {
isRequiredByDependency = true;
errors.push({
field: path,
message: dep.message || 'Field is required based on dependencies'
});
return;
}
}
}
}
// Only skip validation if field is optional AND not required by dependencies
if (typeof rule === 'object' && rule.optional && !isRequiredByDependency) {
return;
}
// If we get here, the field is required
if (!isRequiredByDependency) {
errors.push({
field: path,
message: 'Field is required'
});
}
return;
}
var typeIndicator = typeof rule === 'string' ? rule : rule.type;
if (!typeIndicator) return;
// Parse array type notation
var _parseArrayType = parseArrayType(typeIndicator),
isArray = _parseArrayType.isArray,
elementType = _parseArrayType.elementType;
if (isArray) {
if (!Array.isArray(value)) {
errors.push({
field: path,
message: "Expected array of " + elementType + ", got " + typeof value
});
return;
}
value.forEach(function (item, index) {
validateValue(item, elementType, path + "[" + index + "]");
});
return;
}
// Type validation
if (!validateType(value, typeIndicator, typeof rule === 'object' ? rule : undefined)) {
errors.push({
field: path,
message: "Invalid type, expected " + getTypeDescription(rule) + ", got " + typeof value
});
return;
}
if (typeof rule === 'string') return;
// Validate constraints
if (rule.values && !rule.values.includes(value)) {
errors.push({
field: path,
message: "Value must be one of: " + rule.values.join(', ')
});
}
// Number constraints
if (rule.type === 'N') {
if (rule.min !== undefined && value < rule.min) {
errors.push({
field: path,
message: "Value must be >= " + rule.min
});
}
if (rule.max !== undefined && value > rule.max) {
errors.push({
field: path,
message: "Value must be <= " + rule.max
});
}
if (rule.integer && !Number.isInteger(value)) {
errors.push({
field: path,
message: 'Value must be an integer'
});
}
// Add specific decimal validation error messages
if (rule.decimal) {
var hasDecimal = value.toString().includes('.');
var isInteger = Number.isInteger(value);
if (!hasDecimal && !isInteger) {
errors.push({
field: path,
message: 'Value must be a decimal number'
});
} else if (rule.decimals !== undefined) {
var decimalPlaces = (value.toString().split('.')[1] || '').length;
if (decimalPlaces !== rule.decimals) {
errors.push({
field: path,
message: "Value must have exactly " + rule.decimals + " decimal places"
});
}
}
}
}
// String constraints
if (rule.type === 'S') {
if (rule.minLength !== undefined && value.length < rule.minLength) {
errors.push({
field: path,
message: "String length must be >= " + rule.minLength
});
}
if (rule.maxLength !== undefined && value.length > rule.maxLength) {
errors.push({
field: path,
message: "String length must be <= " + rule.maxLength
});
}
}
// Custom validation function (standalone validate property)
if (typeof rule === 'object' && rule.validate && typeof rule.validate === 'function') {
try {
var isValid = rule.validate(value);
if (!isValid) {
errors.push({
field: path,
message: rule.message || "Custom validation failed for field " + path
});
}
} catch (error) {
errors.push({
field: path,
message: rule.message || "Custom validation error for field " + path + ": " + (error instanceof Error ? error.message : 'Unknown error')
});
}
}
// Handle array validation
if (rule.items && Array.isArray(value)) {
value.forEach(function (item, index) {
validateValue(item, rule.items, path + "[" + index + "]");
});
}
// Handle object validation
if (rule.fields && typeof value === 'object') {
Object.entries(rule.fields).forEach(function (_ref4) {
var key = _ref4[0],
fieldRule = _ref4[1];
validateValue(value[key], fieldRule, path ? path + "." + key : key);
});
}
// Dependencies validation
if (typeof rule === 'object' && rule.dependsOn) {
var _deps = Array.isArray(rule.dependsOn) ? rule.dependsOn : [rule.dependsOn];
_deps.forEach(function (dep, index) {
// Get the parent path
var parentPath = path.substring(0, path.lastIndexOf('.'));
// For cross-object dependencies, use the absolute path directly
// If dep.field contains a dot, it's a cross-object reference
var depPath = dep.field.includes('.') ? dep.field // Use absolute path as is
: parentPath // For same-object references, prepend parent path
? parentPath + "." + dep.field : dep.field;
var depValue = getNestedValue(dataWithDefaults, depPath);
var conditionResult = dep.condition(depValue);
if (conditionResult) {
var _isValid = dep.validate(value, depValue, dataWithDefaults);
if (!_isValid) {
errors.push({
field: path,
message: dep.message
});
}
}
});
}
}
// Validate each field in the model
Object.entries(model).forEach(function (_ref5) {
var key = _ref5[0],
rule = _ref5[1];
validateValue(key.includes('.') || key.includes('[]') ? getNestedValue(dataWithDefaults, key) : dataWithDefaults[key], rule, parentPath ? parentPath + "." + key : key);
});
return errors.length > 0 ? {
isValid: false,
errors: errors
} : {
isValid: true,
data: dataWithDefaults
};
}
var ValidationTypeParams = {
S: {
type: 'String',
description: 'Text values with optional length and pattern constraints',
params: [{
name: 'minLength',
type: 'number',
description: 'Minimum length of the string'
}, {
name: 'maxLength',
type: 'number',
description: 'Maximum length of the string'
}, {
name: 'values',
type: 'string[]',
description: 'Array of allowed values'
}, {
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'default',
type: 'string',
description: 'Default value if field is not provided'
}]
},
N: {
type: 'Number',
description: 'Numeric values with optional range constraints',
params: [{
name: 'min',
type: 'number',
description: 'Minimum allowed value'
}, {
name: 'max',
type: 'number',
description: 'Maximum allowed value'
}, {
name: 'integer',
type: 'boolean',
description: 'Whether the number must be an integer'
}, {
name: 'decimal',
type: 'boolean',
description: 'can be decimal or an integer'
}, {
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'values',
type: 'number[]',
description: 'Array of allowed values'
}, {
name: 'default',
type: 'number',
description: 'Default value if field is not provided'
}]
},
B: {
type: 'Boolean',
description: 'True/false values',
params: [{
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'default',
type: 'boolean',
description: 'Default value if field is not provided'
}]
},
L: {
type: 'Array',
description: 'Array of values with type validation',
params: [{
name: 'items',
type: 'ValidationRule | string',
description: 'Validation rule for array items',
required: true
}, {
name: 'minLength',
type: 'number',
description: 'Minimum array length'
}, {
name: 'maxLength',
type: 'number',
description: 'Maximum array length'
}, {
name: 'values',
type: 'any[]',
description: 'Array of allowed values'
}, {
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}]
},
M: {
type: 'Map',
description: 'Object with defined field structure',
params: [{
name: 'fields',
type: 'Record<string, ValidationRule | string>',
description: 'Validation rules for object fields',
required: true
}, {
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}]
},
EMAIL: {
type: 'Email',
description: 'Valid email address format',
params: [{
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'default',
type: 'string',
description: 'Default value if field is not provided'
}]
},
URL: {
type: 'URL',
description: 'Valid URL format',
params: [{
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'default',
type: 'string',
description: 'Default value if field is not provided'
}]
},
DATE: {
type: 'Date',
description: 'Valid date format',
params: [{
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'default',
type: 'string',
description: 'Default value if field is not provided'
}]
},
PHONE: {
type: 'Phone',
description: 'Valid phone number format',
params: [{
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}, {
name: 'default',
type: 'string',
description: 'Default value if field is not provided'
}]
},
REGEX: {
type: 'Regex',
description: 'Custom pattern matching',
params: [{
name: 'pattern',
type: 'string',
description: 'Regular expression pattern',
required: true
}, {
name: 'optional',
type: 'boolean',
description: 'Whether the field is optional'
}]
}
};
var deserializeFunction = function deserializeFunction(fnStr) {
try {
var functionString = typeof fnStr === 'object' ? fnStr.original || fnStr.code : fnStr;
// Normalize whitespace and remove 'function' keyword if present
var normalizedStr = functionString.trim().replace(/^function\s*[a-zA-Z0-9_]*\s*/, '');
normalizedStr = normalizedStr.replace(/function anonymous/, '').replace(/\s+/g, ' ').trim();
if (normalizedStr.includes('=>')) {
// Multi-line arrow with block
var blockArrowMatch = normalizedStr.match(/^\s*(?:\((.*?)\)|([^=>\s]+))\s*=>\s*\{([\s\S]*)\}\s*$/);
if (blockArrowMatch) {
var paramsWithParens = blockArrowMatch[1],
singleParam = blockArrowMatch[2],
body = blockArrowMatch[3];
var params = paramsWithParens || singleParam;
return _construct(Function, params.split(',').map(function (p) {
return p.trim();
}).concat([body.trim()]));
}
// Single-line arrow with expression
var simpleArrowMatch = normalizedStr.match(/^\s*(?:\((.*?)\)|([^=>\s]+))\s*=>\s*(.+?)\s*$/);
if (simpleArrowMatch) {
var _paramsWithParens = simpleArrowMatch[1],
_singleParam = simpleArrowMatch[2],
expression = simpleArrowMatch[3];
var _params = _paramsWithParens || _singleParam;
// Handle potential multi-line expressions without braces
var cleanExpression = expression.includes('\n') ? expression.split('\n').map(function (line) {
return line.trim();
}).join(' ') : expression.trim();
return _construct(Function, _params.split(',').map(function (p) {
return p.trim();
}).concat(["return " + cleanExpression + ";"]));
}
}
// Regular function (now without 'function' keyword)
var regularFnMatch = normalizedStr.match(/^\s*\((.*?)\)\s*\{([\s\S]*)\}\s*$/);
if (regularFnMatch) {
var _params2 = regularFnMatch[1],
_body = regularFnMatch[2];
return _construct(Function, _params2.split(',').map(function (p) {
return p.trim();
}).concat([_body.trim()]));
}
throw new Error('Invalid function format');
} catch (error) {
if (error instanceof Error) {
throw new Error("Failed to deserialize function: " + error.message);
}
throw new Error('Failed to deserialize function: Unknown error');
}
};
// Serialize validation model to string
var serializeValidationModel = function serializeValidationModel(model) {
function serializeObject(obj) {
// Handle functions
if (typeof obj === 'function') {
return serializeFunction(obj);
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(function (item) {
return serializeObject(item);
});
}
// Handle objects
if (obj && typeof obj === 'object') {
var result = {};
for (var _i = 0, _Object$entries = Object.entries(obj); _i < _Object$entries.length; _i++) {
var _Object$entries$_i = _Object$entries[_i],
key = _Object$entries$_i[0],
value = _Object$entries$_i[1];
result[key] = serializeObject(value);
}
return result;
}
// Handle primitive values
return obj;
}
try {
return JSON.stringify(serializeObject(model));
} catch (error) {
if (error instanceof Error) {
throw new Error("Failed to serialize model: " + error.message);
}
throw new Error('Failed to serialize model: Unknown error');
}
};
function serializeFunction(fn) {
return {
__type: 'function',
code: fn.toString().replace(/\n/g, ' ').trim(),
original: fn.toString().replace(/\n/g, ' ').trim()
};
}
var deserializeValidationModel = function deserializeValidationModel(serializedModel) {
function deserializeObject(obj) {
if (obj && obj.__type === 'function' && obj.code) {
try {
var fn = deserializeFunction(obj.original || obj.code);
return fn;
} catch (error) {
throw error;
}
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(function (item) {
return deserializeObject(item);
});
}
// Handle objects with dependsOn
if (obj && typeof obj === 'object') {
var result = {};
var _loop = function _loop() {
var _Object$entries2$_i = _Object$entries2[_i2],
key = _Object$entries2$_i[0],
value = _Object$entries2$_i[1];
if (key === 'dependsOn') {
// Type guard for dependency object
var isDependencyObject = function isDependencyObject(v) {
return typeof v === 'object' && v !== null && 'field' in v;
};
// Handle both single dependency and array of dependencies
if (Array.isArray(value)) {
result[key] = value.map(function (dep) {
if (!isDependencyObject(dep)) {
throw new Error('Invalid dependency object structure');
}
return {
field: dep.field,
message: dep.message,
condition: dep.condition ? deserializeObject(dep.condition) : undefined,
validate: dep.validate ? deserializeObject(dep.validate) : undefined
};
});
} else if (isDependencyObject(value)) {
result[key] = {
field: value.field,
message: value.message,
condition: value.condition ? deserializeObject(value.condition) : undefined,
validate: value.validate ? deserializeObject(value.validate) : undefined
};
} else {
throw new Error('Invalid dependency structure');
}
} else {
result[key] = deserializeObject(value);
}
};
for (var _i2 = 0, _Object$entries2 = Object.entries(obj); _i2 < _Object$entries2.length; _i2++) {
_loop();
}
return result;
}
return obj;
}
try {
var parsed = JSON.parse(serializedModel);
var deserialized = deserializeObject(parsed);
// Validate the deserialized model
var validation = validateDataModel(deserialized);
if (!validation.isValid) {
var _validation$errors;
throw new Error("Invalid model after deserialization: " + ((_validation$errors = validation.errors) == null ? void 0 : _validation$errors.join(', ')));
}
return deserialized;
} catch (error) {
throw error;
}
};
function getValidationTypeParams(type) {
var params = ValidationTypeParams[type];
if (!params || params === undefined || params === null) {
throw new Error("Invalid validation type: " + type);
}
return params;
}
var userProfileModel = {
user: {
type: 'M',
fields: {
name: {
type: 'S',
minLength: 2
},
age: {
type: 'N',
min: 13
},
email: {
type: 'EMAIL'
},
subscription: {
type: 'M',
fields: {
plan: {
type: 'S',
values: ['FREE', 'BASIC', 'PREMIUM'],
dependsOn: {
field: 'user.age',
condition: function condition(age) {
return age < 18;
},
validate: function validate(plan) {
return plan !== 'PREMIUM';
},
message: 'Users under 18 cannot have PREMIUM plan'
}
},
features: {
type: 'L',
items: {
type: 'S'
},
dependsOn: {
field: 'user.subscription.plan',
condition: function condition(plan) {
return plan === 'FREE';
},
validate: function validate(features) {
return features.length <= 3;
},
message: 'FREE plan can only have up to 3 features'
}
},
// Payment method with multiple dependencies
paymentMethod: {
type: 'S',
values: ['CREDIT_CARD', 'DEBIT_CARD', 'PAYPAL'],
dependsOn: [{
field: 'user.age',
condition: function condition(age) {
return age < 18;
},
validate: function validate(method) {
return method === 'DEBIT_CARD';
},
message: 'Users under 18 can only use debit cards'
}, {
field: 'user.subscription.plan',
condition: function condition(plan) {
return plan === 'PREMIUM';
},
validate: function validate(method) {
return method === 'CREDIT_CARD';
},
message: 'PREMIUM plan requires credit card payment'
}]
}
}
}
}
}
};
var testCases = [{
name: 'Valid adult premium user',
data: {
user: {
name: 'John Doe',
age: 25,
email: 'john@example.com',
subscription: {
plan: 'PREMIUM',
features: ['feature1', 'feature2', 'feature3', 'feature4'],
paymentMethod: 'CREDIT_CARD'
}
}
}
}, {
name: 'Valid teen user with free plan',
data: {
user: {
name: 'Teen User',
age: 15,
email: 'teen@example.com',
subscription: {
plan: 'FREE',
features: ['feature1', 'feature2'],
paymentMethod: 'DEBIT_CARD'
}
}
}
}, {
name: 'Invalid - Teen trying premium plan',
data: {
user: {
name: 'Teen Premium',
age: 16,
email: 'teen.premium@example.com',
subscription: {
plan: 'PREMIUM',
features: ['feature1', 'feature2'],
paymentMethod: 'DEBIT_CARD'
}
}
}
}, {
name: 'Invalid - Free plan with too many features',
data: {
user: {
name: 'Free User',
age: 20,
email: 'free@example.com',
subscription: {
plan: 'FREE',
features: ['feature1', 'feature2', 'feature3', 'feature4'],
paymentMethod: 'PAYPAL'
}
}
}
}];
var PV = /*#__PURE__*/function () {
function PV(storage) {
this.storage = storage;
}
/**
* Static validation with direct model and data
*/
var _proto = PV.prototype;
_proto.validateStatic = function validateStatic(data, model, allowUnknownFields) {
var modelValidation = this.validateModel(model);
if (!modelValidation.isValid && modelValidation.errors) {
return {
isValid: false,
errors: modelValidation.errors.map(function (error) {
return {
field: 'model',
message: error
};
})
};
}
return validateAgainstModel(data, model, allowUnknownFields);
};
PV.getInstance = function getInstance(storage) {
if (!PV.instance) {
try {
PV.instance = new PV(storage);
} catch (error) {
if (error instanceof Error) {
throw new Error("Failed to create PV instance: " + error.message);
}
throw new Error('Failed to create PV instance: Unknown error');
}
}
return PV.instance;
}
/**
* Dynamic validation using stored model with optional collection
*/;
_proto.validateDynamic =
/*#__PURE__*/
function () {
var _validateDynamic = /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee(data, modelName, version, collection, allowUnknownFields) {
var serializedModel, modelVersion, latestVersion, model;
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (this.storage) {
_context.next = 2;
break;
}
throw new Error('Storage is required for dynamic validation');
case 2:
_context.prev = 2;
if (!(version !== undefined)) {
_context.next = 12;
break;
}
_context.next = 6;
return this.storage.getModelVersion(modelName, version, collection);
case 6:
modelVersion = _context.sent;
if (modelVersion) {
_context.next = 9;
break;
}
throw new Error("Model " + modelName + " version " + version + " not found");
case 9:
serializedModel = modelVersion.model;
_context.next = 18;
break;
case 12:
_context.next = 14;
return this.storage.getLatestModelVersion(modelName, collection);
case 14:
latestVersion = _context.sent;
if (latestVersion) {
_context.next = 17;
break;
}
throw new Error("Model " + modelName + " not found");
case 17:
serializedModel = latestVersion.model;
case 18:
if (serializedModel) {
_context.next = 20;
break;
}
throw new Error("Model " + modelName + " not found");
case 20:
model = deserializeValidationModel(serializedModel);
return _context.abrupt("return", validateAgainstModel(data, model, allowUnknownFields));
case 24:
_context.prev = 24;
_context.t0 = _context["catch"](2);
if (!(_context.t0 instanceof Error)) {
_context.next = 28;
break;
}
return _context.abrupt("return", {
isValid: false,
errors: [{
field: 'model',
message: "Failed to load model: " + _context.t0.message
}]
});
case 28:
return _context.abrupt("return", {
isValid: false,
errors: [{
field: 'model',
message: 'Failed to load model: Unknown error'
}]
});
case 29:
case "end":
return _context.stop();
}
}, _callee, this, [[2, 24]]);
}));
function validateDynamic(_x, _x2, _x3, _x4, _x5) {
return _validateDynamic.apply(this, arguments);
}
return validateDynamic;
}()
/**
* Store model with validation and optional collection
*/
;
_proto.storeModel =
/*#__PURE__*/
function () {
var _storeModel = /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(modelName, model, version, collection) {
var modelValidation, _modelValidation$erro, serialized, latestVersion, newVersion;
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
_context2.prev = 0;
if (this.storage) {
_context2.next = 3;
break;
}
throw new Error('Storage is required for model storage');
case 3:
// Validate and store model with optional collection
modelValidation = this.validateModel(model);
if (modelValidation.isValid) {
_context2.next = 6;
break;
}
throw new Error("Model validation failed: " + ((_modelValidation$erro = modelValidation.errors) == null ? void 0 : _modelValidation$erro.join(', ')));
case 6:
_context2.next = 8;
return this.serializeModelSafely(model);
case 8:
serialized = _context2.sent;
_context2.next = 11;
return this.deserializeAndValidate(serialized);
case 11:
if (!(version !== undefined)) {
_context2.next = 17;
break;
}
_context2.next = 15;
return this.storage.storeModelVersion(modelName, serialized, version, collection);
case 15:
_context2.next = 23;
break;