732 lines
33 KiB
JavaScript
732 lines
33 KiB
JavaScript
"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;
|
||
}
|
||
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) {
|
||
return this.validateModel({ name, value, modelDefinition: this.models[property.ref], fieldErrors, isBodyParam, parent });
|
||
}
|
||
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 it’s 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 it’s 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 ? JSON.parse(JSON.stringify(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;
|
||
}
|
||
}
|
||
fieldErrors[parent + name] = {
|
||
message: `Could not match the union against any of the items. Issues: ${JSON.stringify(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 it’s 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 = new ValidationService(this.models, {
|
||
noImplicitAdditionalProperties: 'silently-remove-extras',
|
||
bodyCoercion: this.config.bodyCoercion,
|
||
}).ValidateParam(subSchema, JSON.parse(JSON.stringify(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) {
|
||
fieldErrors[parent + name] = {
|
||
message: `Could not match the intersection against every type. Issues: ${JSON.stringify(filtered)}`,
|
||
value,
|
||
};
|
||
return;
|
||
}
|
||
const schemas = this.selfIntersectionCombinations(subSchemas.map(subSchema => this.toModelLike(subSchema)));
|
||
const getRequiredPropError = (schema) => {
|
||
const requiredPropError = {};
|
||
new ValidationService(this.models, {
|
||
noImplicitAdditionalProperties: 'ignore',
|
||
bodyCoercion: this.config.bodyCoercion,
|
||
}).validateModel({
|
||
name,
|
||
value: JSON.parse(JSON.stringify(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;
|
||
}
|
||
}
|
||
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
|