This commit is contained in:
2026-03-03 15:23:00 +00:00
parent 5e3726de39
commit 8e223bfbec
3689 changed files with 955330 additions and 1011 deletions

View File

@@ -0,0 +1,855 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidateError = exports.ValidationService = void 0;
exports.ValidateParam = ValidateParam;
const validator_1 = __importDefault(require("validator"));
const assertNever_1 = require("../utils/assertNever");
const tsoa_route_1 = require("./tsoa-route");
// for backwards compatibility with custom templates
function ValidateParam(property, value, generatedModels, name = '', fieldErrors, isBodyParam, parent = '', config) {
return new ValidationService(generatedModels, config).ValidateParam(property, value, name, fieldErrors, isBodyParam, parent);
}
class ValidationService {
constructor(models, config) {
this.models = models;
this.config = config;
this.validationStack = new Set();
}
ValidateParam(property, rawValue, name = '', fieldErrors, isBodyParam, parent = '') {
let value = rawValue;
// If undefined is allowed type, we can move to value validation
if (value === undefined && property.dataType !== 'undefined') {
// If there's either default value or datatype is union with undefined valid, we can just set it and move to validation
if (property.default !== undefined || (property.dataType === 'union' && property.subSchemas?.some(p => p.dataType === 'undefined'))) {
value = property.default;
}
else if (property.required) {
// If value can be typed as undefined, there's no need to check mandatoriness here.
let message = `'${name}' is required`;
if (property.validators) {
const validators = property.validators;
Object.keys(validators).forEach((key) => {
const errorMsg = validators[key]?.errorMsg;
if (key.startsWith('is') && errorMsg) {
message = errorMsg;
}
});
}
fieldErrors[parent + name] = {
message,
value,
};
return;
}
else {
return value;
}
}
switch (property.dataType) {
case 'string':
return this.validateString(name, value, fieldErrors, property.validators, parent);
case 'boolean':
return this.validateBool(name, value, fieldErrors, isBodyParam, property.validators, parent);
case 'integer':
case 'long':
return this.validateInt(name, value, fieldErrors, isBodyParam, property.validators, parent);
case 'float':
case 'double':
return this.validateFloat(name, value, fieldErrors, isBodyParam, property.validators, parent);
case 'enum':
return this.validateEnum(name, value, fieldErrors, property.enums, parent);
case 'array':
return this.validateArray(name, value, fieldErrors, isBodyParam, property.array, property.validators, parent);
case 'date':
return this.validateDate(name, value, fieldErrors, isBodyParam, property.validators, parent);
case 'datetime':
return this.validateDateTime(name, value, fieldErrors, isBodyParam, property.validators, parent);
case 'buffer':
return this.validateBuffer(name, value);
case 'union':
return this.validateUnion(name, value, fieldErrors, isBodyParam, property, parent);
case 'intersection':
return this.validateIntersection(name, value, fieldErrors, isBodyParam, property.subSchemas, parent);
case 'undefined':
return this.validateUndefined(name, value, fieldErrors, parent);
case 'any':
return value;
case 'nestedObjectLiteral':
return this.validateNestedObjectLiteral(name, value, fieldErrors, isBodyParam, property.nestedProperties, property.additionalProperties, parent);
default:
if (property.ref) {
// Detect circular references to prevent stack overflow
const refPath = `${parent}${name}:${property.ref}`;
if (this.validationStack.has(refPath)) {
return value;
}
this.validationStack.add(refPath);
try {
return this.validateModel({ name, value, modelDefinition: this.models[property.ref], fieldErrors, isBodyParam, parent });
}
finally {
this.validationStack.delete(refPath);
}
}
return value;
}
}
hasCorrectJsType(value, type, isBodyParam) {
return !isBodyParam || this.config.bodyCoercion || typeof value === type;
}
validateNestedObjectLiteral(name, value, fieldErrors, isBodyParam, nestedProperties, additionalProperties, parent) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
fieldErrors[parent + name] = {
message: `invalid object`,
value,
};
return;
}
const previousErrors = Object.keys(fieldErrors).length;
if (!nestedProperties) {
throw new Error('internal tsoa error: ' +
'the metadata that was generated should have had nested property schemas since its for a nested object,' +
'however it did not. ' +
'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues');
}
const propHandling = this.config.noImplicitAdditionalProperties;
if (propHandling !== 'ignore') {
const excessProps = this.getExcessPropertiesFor({ dataType: 'refObject', properties: nestedProperties, additionalProperties }, Object.keys(value));
if (excessProps.length > 0) {
if (propHandling === 'silently-remove-extras') {
excessProps.forEach(excessProp => {
delete value[excessProp];
});
}
if (propHandling === 'throw-on-extras') {
fieldErrors[parent + name] = {
message: `"${excessProps.join(',')}" is an excess property and therefore is not allowed`,
value: excessProps.reduce((acc, propName) => ({ [propName]: value[propName], ...acc }), {}),
};
}
}
}
Object.keys(nestedProperties).forEach(key => {
const validatedProp = this.ValidateParam(nestedProperties[key], value[key], key, fieldErrors, isBodyParam, parent + name + '.');
// Add value from validator if it's not undefined or if value is required and unfedined is valid type
if (validatedProp !== undefined || (nestedProperties[key].dataType === 'undefined' && nestedProperties[key].required)) {
value[key] = validatedProp;
}
});
if (typeof additionalProperties === 'object' && typeof value === 'object') {
const keys = Object.keys(value).filter(key => typeof nestedProperties[key] === 'undefined');
keys.forEach(key => {
const validatedProp = this.ValidateParam(additionalProperties, value[key], key, fieldErrors, isBodyParam, parent + name + '.');
// Add value from validator if it's not undefined or if value is required and unfedined is valid type
if (validatedProp !== undefined || (additionalProperties.dataType === 'undefined' && additionalProperties.required)) {
value[key] = validatedProp;
}
});
}
if (Object.keys(fieldErrors).length > previousErrors) {
return;
}
return value;
}
validateInt(name, value, fieldErrors, isBodyParam, validators, parent = '') {
if (!this.hasCorrectJsType(value, 'number', isBodyParam) || !validator_1.default.isInt(String(value))) {
let message = `invalid integer number`;
if (validators) {
if (validators.isInt && validators.isInt.errorMsg) {
message = validators.isInt.errorMsg;
}
if (validators.isLong && validators.isLong.errorMsg) {
message = validators.isLong.errorMsg;
}
}
fieldErrors[parent + name] = {
message,
value,
};
return;
}
const numberValue = validator_1.default.toInt(String(value), 10);
if (!validators) {
return numberValue;
}
if (validators.minimum && validators.minimum.value !== undefined) {
if (validators.minimum.value > numberValue) {
fieldErrors[parent + name] = {
message: validators.minimum.errorMsg || `min ${validators.minimum.value}`,
value,
};
return;
}
}
if (validators.maximum && validators.maximum.value !== undefined) {
if (validators.maximum.value < numberValue) {
fieldErrors[parent + name] = {
message: validators.maximum.errorMsg || `max ${validators.maximum.value}`,
value,
};
return;
}
}
return numberValue;
}
validateFloat(name, value, fieldErrors, isBodyParam, validators, parent = '') {
if (!this.hasCorrectJsType(value, 'number', isBodyParam) || !validator_1.default.isFloat(String(value))) {
let message = 'invalid float number';
if (validators) {
if (validators.isFloat && validators.isFloat.errorMsg) {
message = validators.isFloat.errorMsg;
}
if (validators.isDouble && validators.isDouble.errorMsg) {
message = validators.isDouble.errorMsg;
}
}
fieldErrors[parent + name] = {
message,
value,
};
return;
}
const numberValue = validator_1.default.toFloat(String(value));
if (!validators) {
return numberValue;
}
if (validators.minimum && validators.minimum.value !== undefined) {
if (validators.minimum.value > numberValue) {
fieldErrors[parent + name] = {
message: validators.minimum.errorMsg || `min ${validators.minimum.value}`,
value,
};
return;
}
}
if (validators.maximum && validators.maximum.value !== undefined) {
if (validators.maximum.value < numberValue) {
fieldErrors[parent + name] = {
message: validators.maximum.errorMsg || `max ${validators.maximum.value}`,
value,
};
return;
}
}
return numberValue;
}
validateEnum(name, value, fieldErrors, members, parent = '') {
if (!members || members.length === 0) {
fieldErrors[parent + name] = {
message: 'no member',
value,
};
return;
}
const enumMatchIndex = members.map(member => String(member)).findIndex(member => validator_1.default.equals(member, String(value)));
if (enumMatchIndex === -1) {
const membersInQuotes = members.map(member => (typeof member === 'string' ? `'${member}'` : String(member)));
fieldErrors[parent + name] = {
message: `should be one of the following; [${membersInQuotes.join(',')}]`,
value,
};
return;
}
return members[enumMatchIndex];
}
validateDate(name, value, fieldErrors, isBodyParam, validators, parent = '') {
if (!this.hasCorrectJsType(value, 'string', isBodyParam) || !validator_1.default.isISO8601(String(value), { strict: true })) {
const message = validators && validators.isDate && validators.isDate.errorMsg ? validators.isDate.errorMsg : `invalid ISO 8601 date format, i.e. YYYY-MM-DD`;
fieldErrors[parent + name] = {
message,
value,
};
return;
}
const dateValue = new Date(String(value));
if (!validators) {
return dateValue;
}
if (validators.minDate && validators.minDate.value) {
const minDate = new Date(validators.minDate.value);
if (minDate.getTime() > dateValue.getTime()) {
fieldErrors[parent + name] = {
message: validators.minDate.errorMsg || `minDate '${validators.minDate.value}'`,
value,
};
return;
}
}
if (validators.maxDate && validators.maxDate.value) {
const maxDate = new Date(validators.maxDate.value);
if (maxDate.getTime() < dateValue.getTime()) {
fieldErrors[parent + name] = {
message: validators.maxDate.errorMsg || `maxDate '${validators.maxDate.value}'`,
value,
};
return;
}
}
return dateValue;
}
validateDateTime(name, value, fieldErrors, isBodyParam, validators, parent = '') {
if (!this.hasCorrectJsType(value, 'string', isBodyParam) || !validator_1.default.isISO8601(String(value), { strict: true })) {
const message = validators && validators.isDateTime && validators.isDateTime.errorMsg ? validators.isDateTime.errorMsg : `invalid ISO 8601 datetime format, i.e. YYYY-MM-DDTHH:mm:ss`;
fieldErrors[parent + name] = {
message,
value,
};
return;
}
const datetimeValue = new Date(String(value));
if (!validators) {
return datetimeValue;
}
if (validators.minDate && validators.minDate.value) {
const minDate = new Date(validators.minDate.value);
if (minDate.getTime() > datetimeValue.getTime()) {
fieldErrors[parent + name] = {
message: validators.minDate.errorMsg || `minDate '${validators.minDate.value}'`,
value,
};
return;
}
}
if (validators.maxDate && validators.maxDate.value) {
const maxDate = new Date(validators.maxDate.value);
if (maxDate.getTime() < datetimeValue.getTime()) {
fieldErrors[parent + name] = {
message: validators.maxDate.errorMsg || `maxDate '${validators.maxDate.value}'`,
value,
};
return;
}
}
return datetimeValue;
}
validateString(name, value, fieldErrors, validators, parent = '') {
if (typeof value !== 'string') {
const message = validators && validators.isString && validators.isString.errorMsg ? validators.isString.errorMsg : `invalid string value`;
fieldErrors[parent + name] = {
message,
value,
};
return;
}
const stringValue = String(value);
if (!validators) {
return stringValue;
}
if (validators.minLength && validators.minLength.value !== undefined) {
if (validators.minLength.value > stringValue.length) {
fieldErrors[parent + name] = {
message: validators.minLength.errorMsg || `minLength ${validators.minLength.value}`,
value,
};
return;
}
}
if (validators.maxLength && validators.maxLength.value !== undefined) {
if (validators.maxLength.value < stringValue.length) {
fieldErrors[parent + name] = {
message: validators.maxLength.errorMsg || `maxLength ${validators.maxLength.value}`,
value,
};
return;
}
}
if (validators.pattern && validators.pattern.value) {
if (!validator_1.default.matches(String(stringValue), validators.pattern.value)) {
fieldErrors[parent + name] = {
message: validators.pattern.errorMsg || `Not match in '${validators.pattern.value}'`,
value,
};
return;
}
}
return stringValue;
}
validateBool(name, value, fieldErrors, isBodyParam, validators, parent = '') {
if (value === true || value === false) {
return value;
}
if (!isBodyParam || this.config.bodyCoercion === true) {
if (value === undefined || value === null) {
return false;
}
if (String(value).toLowerCase() === 'true') {
return true;
}
if (String(value).toLowerCase() === 'false') {
return false;
}
}
const message = validators && validators.isBoolean && validators.isBoolean.errorMsg ? validators.isBoolean.errorMsg : `invalid boolean value`;
fieldErrors[parent + name] = {
message,
value,
};
return;
}
validateUndefined(name, value, fieldErrors, parent = '') {
if (value === undefined) {
return undefined;
}
const message = 'invalid undefined value';
fieldErrors[parent + name] = {
message,
value,
};
return;
}
validateArray(name, value, fieldErrors, isBodyParam, schema, validators, parent = '') {
if ((isBodyParam && this.config.bodyCoercion === false && !Array.isArray(value)) || !schema || value === undefined) {
const message = validators && validators.isArray && validators.isArray.errorMsg ? validators.isArray.errorMsg : `invalid array`;
fieldErrors[parent + name] = {
message,
value,
};
return;
}
let arrayValue = [];
const previousErrors = Object.keys(fieldErrors).length;
if (Array.isArray(value)) {
arrayValue = value.map((elementValue, index) => {
return this.ValidateParam(schema, elementValue, `$${index}`, fieldErrors, isBodyParam, name + '.');
});
}
else {
arrayValue = [this.ValidateParam(schema, value, '$0', fieldErrors, isBodyParam, name + '.')];
}
if (Object.keys(fieldErrors).length > previousErrors) {
return;
}
if (!validators) {
return arrayValue;
}
if (validators.minItems && validators.minItems.value) {
if (validators.minItems.value > arrayValue.length) {
fieldErrors[parent + name] = {
message: validators.minItems.errorMsg || `minItems ${validators.minItems.value}`,
value,
};
return;
}
}
if (validators.maxItems && validators.maxItems.value) {
if (validators.maxItems.value < arrayValue.length) {
fieldErrors[parent + name] = {
message: validators.maxItems.errorMsg || `maxItems ${validators.maxItems.value}`,
value,
};
return;
}
}
if (validators.uniqueItems) {
const unique = arrayValue.some((elem, index, arr) => {
const indexOf = arr.indexOf(elem);
return indexOf > -1 && indexOf !== index;
});
if (unique) {
fieldErrors[parent + name] = {
message: validators.uniqueItems.errorMsg || `required unique array`,
value,
};
return;
}
}
return arrayValue;
}
validateBuffer(_name, value) {
return Buffer.from(value);
}
validateUnion(name, value, fieldErrors, isBodyParam, property, parent = '') {
if (!property.subSchemas) {
throw new Error('internal tsoa error: ' +
'the metadata that was generated should have had sub schemas since its for a union, however it did not. ' +
'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues');
}
const subFieldErrors = [];
for (const subSchema of property.subSchemas) {
const subFieldError = {};
// Clean value if it's not undefined or use undefined directly if it's undefined.
// Value can be undefined if undefined is allowed datatype of the union
const validateableValue = value !== undefined ? this.deepClone(value) : value;
const cleanValue = this.ValidateParam({ ...subSchema, validators: { ...property.validators, ...subSchema.validators } }, validateableValue, name, subFieldError, isBodyParam, parent);
subFieldErrors.push(subFieldError);
if (Object.keys(subFieldError).length === 0) {
return cleanValue;
}
}
this.addSummarizedError(fieldErrors, parent + name, 'Could not match the union against any of the items. Issues: ', subFieldErrors, value);
return;
}
validateIntersection(name, value, fieldErrors, isBodyParam, subSchemas, parent = '') {
if (!subSchemas) {
throw new Error('internal tsoa error: ' +
'the metadata that was generated should have had sub schemas since its for a intersection, however it did not. ' +
'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues');
}
const subFieldErrors = [];
let cleanValues = {};
subSchemas.forEach(subSchema => {
const subFieldError = {};
const cleanValue = this.createChildValidationService({
noImplicitAdditionalProperties: 'silently-remove-extras',
}).ValidateParam(subSchema, this.deepClone(value), name, subFieldError, isBodyParam, parent);
cleanValues = {
...cleanValues,
...cleanValue,
};
subFieldErrors.push(subFieldError);
});
const filtered = subFieldErrors.filter(subFieldError => Object.keys(subFieldError).length !== 0);
if (filtered.length > 0) {
this.addSummarizedError(fieldErrors, parent + name, 'Could not match the intersection against every type. Issues: ', filtered, value);
return;
}
const schemas = this.selfIntersectionCombinations(subSchemas.map(subSchema => this.toModelLike(subSchema)));
const getRequiredPropError = (schema) => {
const requiredPropError = {};
this.createChildValidationService({
noImplicitAdditionalProperties: 'ignore',
}).validateModel({
name,
value: this.deepClone(value),
modelDefinition: schema,
fieldErrors: requiredPropError,
isBodyParam,
});
return requiredPropError;
};
const schemasWithRequiredProps = schemas.filter(schema => Object.keys(getRequiredPropError(schema)).length === 0);
if (this.config.noImplicitAdditionalProperties === 'ignore') {
return { ...value, ...cleanValues };
}
if (this.config.noImplicitAdditionalProperties === 'silently-remove-extras') {
if (schemasWithRequiredProps.length > 0) {
return cleanValues;
}
else {
fieldErrors[parent + name] = {
message: `Could not match intersection against any of the possible combinations: ${JSON.stringify(schemas.map(s => Object.keys(s.properties)))}`,
value,
};
return;
}
}
if (schemasWithRequiredProps.length > 0 && schemasWithRequiredProps.some(schema => this.getExcessPropertiesFor(schema, Object.keys(value)).length === 0)) {
return cleanValues;
}
else {
fieldErrors[parent + name] = {
message: `Could not match intersection against any of the possible combinations: ${JSON.stringify(schemas.map(s => Object.keys(s.properties)))}`,
value,
};
return;
}
}
toModelLike(schema) {
if (schema.ref) {
const model = this.models[schema.ref];
if (model.dataType === 'refObject') {
return [model];
}
else if (model.dataType === 'refAlias') {
return [...this.toModelLike(model.type)];
}
else if (model.dataType === 'refEnum') {
throw new Error(`Can't transform an enum into a model like structure because it does not have properties.`);
}
else {
return (0, assertNever_1.assertNever)(model);
}
}
else if (schema.nestedProperties) {
return [{ dataType: 'refObject', properties: schema.nestedProperties, additionalProperties: schema.additionalProperties }];
}
else if (schema.subSchemas && schema.dataType === 'intersection') {
const modelss = schema.subSchemas.map(subSchema => this.toModelLike(subSchema));
return this.selfIntersectionCombinations(modelss);
}
else if (schema.subSchemas && schema.dataType === 'union') {
const modelss = schema.subSchemas.map(subSchema => this.toModelLike(subSchema));
return modelss.reduce((acc, models) => [...acc, ...models], []);
}
else {
// There are no properties to check for excess here.
return [{ dataType: 'refObject', properties: {}, additionalProperties: false }];
}
}
/**
* combine all schemas once, ignoring order ie
* input: [[value1], [value2]] should be [[value1, value2]]
* not [[value1, value2],[value2, value1]]
* and
* input: [[value1, value2], [value3, value4], [value5, value6]] should be [
* [value1, value3, value5],
* [value1, value3, value6],
* [value1, value4, value5],
* [value1, value4, value6],
* [value2, value3, value5],
* [value2, value3, value6],
* [value2, value4, value5],
* [value2, value4, value6],
* ]
* @param modelSchemass
*/
selfIntersectionCombinations(modelSchemass) {
const res = [];
// Picks one schema from each sub-array
const combinations = this.getAllCombinations(modelSchemass);
for (const combination of combinations) {
// Combine all schemas of this combination
let currentCollector = { ...combination[0] };
for (let subSchemaIdx = 1; subSchemaIdx < combination.length; subSchemaIdx++) {
currentCollector = { ...this.combineProperties(currentCollector, combination[subSchemaIdx]) };
}
res.push(currentCollector);
}
return res;
}
getAllCombinations(arrays) {
function combine(current, index) {
if (index === arrays.length) {
result.push(current.slice());
return;
}
for (let i = 0; i < arrays[index].length; i++) {
current.push(arrays[index][i]);
combine(current, index + 1);
current.pop();
}
}
const result = [];
combine([], 0);
return result;
}
combineProperties(a, b) {
return { dataType: 'refObject', properties: { ...a.properties, ...b.properties }, additionalProperties: a.additionalProperties || b.additionalProperties || false };
}
getExcessPropertiesFor(modelDefinition, properties) {
const modelProperties = new Set(Object.keys(modelDefinition.properties));
if (modelDefinition.additionalProperties) {
return [];
}
else if (this.config.noImplicitAdditionalProperties === 'ignore') {
return [];
}
else {
return [...properties].filter(property => !modelProperties.has(property));
}
}
validateModel(input) {
const { name, value, modelDefinition, fieldErrors, isBodyParam, parent = '' } = input;
const previousErrors = Object.keys(fieldErrors).length;
if (modelDefinition) {
if (modelDefinition.dataType === 'refEnum') {
return this.validateEnum(name, value, fieldErrors, modelDefinition.enums, parent);
}
if (modelDefinition.dataType === 'refAlias') {
return this.ValidateParam(modelDefinition.type, value, name, fieldErrors, isBodyParam, parent);
}
const fieldPath = parent + name;
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
fieldErrors[fieldPath] = {
message: `invalid object`,
value,
};
return;
}
const properties = modelDefinition.properties || {};
const keysOnPropertiesModelDefinition = new Set(Object.keys(properties));
const allPropertiesOnData = new Set(Object.keys(value));
Object.entries(properties).forEach(([key, property]) => {
const validatedParam = this.ValidateParam(property, value[key], key, fieldErrors, isBodyParam, fieldPath + '.');
// Add value from validator if it's not undefined or if value is required and unfedined is valid type
if (validatedParam !== undefined || (property.dataType === 'undefined' && property.required)) {
value[key] = validatedParam;
}
});
const isAnExcessProperty = (objectKeyThatMightBeExcess) => {
return allPropertiesOnData.has(objectKeyThatMightBeExcess) && !keysOnPropertiesModelDefinition.has(objectKeyThatMightBeExcess);
};
const additionalProperties = modelDefinition.additionalProperties;
if (additionalProperties === true || (0, tsoa_route_1.isDefaultForAdditionalPropertiesAllowed)(additionalProperties)) {
// then don't validate any of the additional properties
}
else if (additionalProperties === false) {
Object.keys(value).forEach((key) => {
if (isAnExcessProperty(key)) {
if (this.config.noImplicitAdditionalProperties === 'throw-on-extras') {
fieldErrors[`${fieldPath}.${key}`] = {
message: `"${key}" is an excess property and therefore is not allowed`,
value: key,
};
}
else if (this.config.noImplicitAdditionalProperties === 'silently-remove-extras') {
delete value[key];
}
else if (this.config.noImplicitAdditionalProperties === 'ignore') {
// then it's okay to have additionalProperties
}
else {
(0, assertNever_1.assertNever)(this.config.noImplicitAdditionalProperties);
}
}
});
}
else {
Object.keys(value).forEach((key) => {
if (isAnExcessProperty(key)) {
const validatedValue = this.ValidateParam(additionalProperties, value[key], key, fieldErrors, isBodyParam, fieldPath + '.');
// Add value from validator if it's not undefined or if value is required and unfedined is valid type
if (validatedValue !== undefined || (additionalProperties.dataType === 'undefined' && additionalProperties.required)) {
value[key] = validatedValue;
}
else {
fieldErrors[`${fieldPath}.${key}`] = {
message: `No matching model found in additionalProperties to validate ${key}`,
value: key,
};
}
}
});
}
}
if (Object.keys(fieldErrors).length > previousErrors) {
return;
}
return value;
}
/**
* Creates a new ValidationService instance with specific configuration
* @param overrides Configuration overrides
* @returns New ValidationService instance
*/
createChildValidationService(overrides = {}) {
return new ValidationService(this.models, {
...this.config,
...overrides,
});
}
/**
* Deep clones an object without using JSON.stringify/parse to avoid:
* 1. Loss of undefined values
* 2. Loss of functions
* 3. Conversion of dates to strings
* 4. Exponential escaping issues with nested objects
*/
deepClone(obj) {
// Fast path for primitives
if (obj === null || obj === undefined) {
return obj;
}
const type = typeof obj;
if (type !== 'object') {
return obj;
}
// Handle built-in object types
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
if (obj instanceof Array) {
const cloneArr = new Array(obj.length);
for (let i = 0; i < obj.length; i++) {
cloneArr[i] = this.deepClone(obj[i]);
}
return cloneArr;
}
if (Buffer && obj instanceof Buffer) {
return Buffer.from(obj);
}
// Handle plain objects
const cloneObj = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloneObj[key] = this.deepClone(obj[key]);
}
}
return cloneObj;
}
/**
* Adds a summarized error to the fieldErrors object
* @param fieldErrors The errors object to add to
* @param errorKey The key for the error
* @param prefix The error message prefix
* @param subErrors Array of sub-errors to summarize
* @param value The value that failed validation
*/
addSummarizedError(fieldErrors, errorKey, prefix, subErrors, value) {
const maxErrorLength = this.config.maxValidationErrorSize ? this.config.maxValidationErrorSize - prefix.length : undefined;
fieldErrors[errorKey] = {
message: `${prefix}${this.summarizeValidationErrors(subErrors, maxErrorLength)}`,
value,
};
}
/**
* Summarizes validation errors to prevent extremely large error messages
* @param errors Array of field errors from union/intersection validation
* @param maxLength Maximum length of the summarized message
* @returns Summarized error message
*/
summarizeValidationErrors(errors, maxLength) {
const effectiveMaxLength = maxLength || this.config.maxValidationErrorSize || 1000;
// If there are no errors, return empty
if (errors.length === 0) {
return '[]';
}
// Start with a count of total errors
const errorCount = errors.length;
const summary = [];
// Try to include first few errors
let currentLength = 0;
let includedErrors = 0;
// Calculate the size of the suffix if we need to truncate
const truncatedSuffix = `,...and ${errorCount} more errors]`;
const reservedSpace = truncatedSuffix.length + 10; // +10 for safety margin
for (const error of errors) {
const errorStr = JSON.stringify(error);
const projectedLength = currentLength + errorStr.length + (summary.length > 0 ? 1 : 0) + 2; // +1 for comma if not first, +2 for brackets
if (projectedLength + reservedSpace < effectiveMaxLength && includedErrors < 3) {
summary.push(errorStr);
currentLength = projectedLength;
includedErrors++;
}
else {
break;
}
}
// Build final message
if (includedErrors < errorCount) {
const result = `[${summary.join(',')},...and ${errorCount - includedErrors} more errors]`;
// Make sure we don't exceed the limit
if (result.length > effectiveMaxLength) {
// If still too long, remove the last error and try again
if (summary.length > 0) {
summary.pop();
includedErrors--;
return `[${summary.join(',')},...and ${errorCount - includedErrors} more errors]`;
}
}
return result;
}
else {
return `[${summary.join(',')}]`;
}
}
}
exports.ValidationService = ValidationService;
class ValidateError extends Error {
constructor(fields, message) {
super(message);
this.fields = fields;
this.message = message;
this.status = 400;
this.name = 'ValidateError';
Object.setPrototypeOf(this, ValidateError.prototype);
}
}
exports.ValidateError = ValidateError;
//# sourceMappingURL=templateHelpers.js.map