Files
2026-03-03 15:23:00 +00:00

855 lines
38 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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