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,6 @@
import { Config, RoutesConfig } from '../config';
export interface AdditionalProps {
noImplicitAdditionalProperties: Exclude<Config['noImplicitAdditionalProperties'], undefined>;
bodyCoercion: Exclude<RoutesConfig['bodyCoercion'], undefined>;
maxValidationErrorSize?: number;
}

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=additionalProps.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"additionalProps.js","sourceRoot":"","sources":["../../src/routeGeneration/additionalProps.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,203 @@
import { AdditionalProps } from './additionalProps';
import { TsoaRoute } from './tsoa-route';
export declare function ValidateParam(property: TsoaRoute.PropertySchema, value: any, generatedModels: TsoaRoute.Models, name: string | undefined, fieldErrors: FieldErrors, isBodyParam: boolean, parent: string | undefined, config: AdditionalProps): any;
export declare class ValidationService {
private readonly models;
private readonly config;
private validationStack;
constructor(models: TsoaRoute.Models, config: AdditionalProps);
ValidateParam(property: TsoaRoute.PropertySchema, rawValue: any, name: string | undefined, fieldErrors: FieldErrors, isBodyParam: boolean, parent?: string): any;
hasCorrectJsType(value: any, type: 'object' | 'boolean' | 'number' | 'string', isBodyParam: boolean): boolean;
validateNestedObjectLiteral(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, nestedProperties: {
[name: string]: TsoaRoute.PropertySchema;
} | undefined, additionalProperties: TsoaRoute.PropertySchema | boolean | undefined, parent: string): any;
validateInt(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, validators?: IntegerValidator, parent?: string): number | undefined;
validateFloat(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, validators?: FloatValidator, parent?: string): number | undefined;
validateEnum(name: string, value: unknown, fieldErrors: FieldErrors, members?: Array<string | number | boolean | null>, parent?: string): unknown;
validateDate(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, validators?: DateValidator, parent?: string): Date | undefined;
validateDateTime(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, validators?: DateTimeValidator, parent?: string): Date | undefined;
validateString(name: string, value: any, fieldErrors: FieldErrors, validators?: StringValidator, parent?: string): string | undefined;
validateBool(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, validators?: BooleanValidator, parent?: string): any;
validateUndefined(name: string, value: any, fieldErrors: FieldErrors, parent?: string): undefined;
validateArray(name: string, value: any[], fieldErrors: FieldErrors, isBodyParam: boolean, schema?: TsoaRoute.PropertySchema, validators?: ArrayValidator, parent?: string): any[] | undefined;
validateBuffer(_name: string, value: string): Buffer<ArrayBuffer>;
validateUnion(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, property: TsoaRoute.PropertySchema, parent?: string): any;
validateIntersection(name: string, value: any, fieldErrors: FieldErrors, isBodyParam: boolean, subSchemas: TsoaRoute.PropertySchema[] | undefined, parent?: string): any;
private toModelLike;
/**
* 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
*/
private selfIntersectionCombinations;
private getAllCombinations;
private combineProperties;
private getExcessPropertiesFor;
validateModel(input: {
name: string;
value: any;
modelDefinition: TsoaRoute.ModelSchema;
fieldErrors: FieldErrors;
isBodyParam: boolean;
parent?: string;
}): any;
/**
* Creates a new ValidationService instance with specific configuration
* @param overrides Configuration overrides
* @returns New ValidationService instance
*/
private createChildValidationService;
/**
* 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
*/
private deepClone;
/**
* 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
*/
private addSummarizedError;
/**
* 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
*/
private summarizeValidationErrors;
}
export interface IntegerValidator {
isInt?: {
errorMsg?: string;
};
isLong?: {
errorMsg?: string;
};
minimum?: {
value: number;
errorMsg?: string;
};
maximum?: {
value: number;
errorMsg?: string;
};
}
export interface FloatValidator {
isFloat?: {
errorMsg?: string;
};
isDouble?: {
errorMsg?: string;
};
minimum?: {
value: number;
errorMsg?: string;
};
maximum?: {
value: number;
errorMsg?: string;
};
}
export interface DateValidator {
isDate?: {
errorMsg?: string;
};
minDate?: {
value: string;
errorMsg?: string;
};
maxDate?: {
value: string;
errorMsg?: string;
};
}
export interface DateTimeValidator {
isDateTime?: {
errorMsg?: string;
};
minDate?: {
value: string;
errorMsg?: string;
};
maxDate?: {
value: string;
errorMsg?: string;
};
}
export interface StringValidator {
isString?: {
errorMsg?: string;
};
minLength?: {
value: number;
errorMsg?: string;
};
maxLength?: {
value: number;
errorMsg?: string;
};
pattern?: {
value: string;
errorMsg?: string;
};
title?: {
value: string;
errorMsg?: string;
};
}
export interface BooleanValidator {
isBoolean?: {
errorMsg?: string;
};
}
export interface ArrayValidator {
isArray?: {
errorMsg?: string;
};
minItems?: {
value: number;
errorMsg?: string;
};
maxItems?: {
value: number;
errorMsg?: string;
};
uniqueItems?: {
errorMsg?: string;
};
}
export type Validator = IntegerValidator | FloatValidator | DateValidator | DateTimeValidator | StringValidator | BooleanValidator | ArrayValidator;
export interface FieldErrors {
[name: string]: {
message: string;
value?: any;
};
}
export interface Exception extends Error {
status: number;
}
export declare class ValidateError extends Error implements Exception {
fields: FieldErrors;
message: string;
status: number;
name: string;
constructor(fields: FieldErrors, message: string);
}

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
import { Request as ExRequest, Response as ExResponse, NextFunction as ExNext } from 'express';
import { Controller } from '../../../interfaces/controller';
import { TsoaRoute } from '../../tsoa-route';
import { TemplateService } from '../templateService';
type ExpressApiHandlerParameters = {
methodName: string;
controller: Controller | object;
response: ExResponse;
next: ExNext;
validatedArgs: any[];
successStatus?: number;
};
type ExpressValidationArgsParameters = {
args: Record<string, TsoaRoute.ParameterSchema>;
request: ExRequest;
response: ExResponse;
};
type ExpressReturnHandlerParameters = {
response: ExResponse;
headers: any;
statusCode?: number;
data?: any;
};
export declare class ExpressTemplateService extends TemplateService<ExpressApiHandlerParameters, ExpressValidationArgsParameters, ExpressReturnHandlerParameters> {
apiHandler(params: ExpressApiHandlerParameters): Promise<void>;
getValidatedArgs(params: ExpressValidationArgsParameters): any[];
protected returnHandler(params: ExpressReturnHandlerParameters): void;
}
export {};

View File

@@ -0,0 +1,118 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressTemplateService = void 0;
const templateHelpers_1 = require("../../templateHelpers");
const templateService_1 = require("../templateService");
class ExpressTemplateService extends templateService_1.TemplateService {
async apiHandler(params) {
const { methodName, controller, response, validatedArgs, successStatus, next } = params;
try {
const data = await this.buildPromise(methodName, controller, validatedArgs);
let statusCode = successStatus;
let headers;
if (this.isController(controller)) {
headers = controller.getHeaders();
statusCode = controller.getStatus() || statusCode;
}
this.returnHandler({ response, headers, statusCode, data });
}
catch (error) {
return next(error);
}
}
getValidatedArgs(params) {
const { args, request, response } = params;
const fieldErrors = {};
const values = Object.values(args).map(param => {
const name = param.name;
switch (param.in) {
case 'request':
return request;
case 'request-prop': {
const descriptor = Object.getOwnPropertyDescriptor(request, name);
const value = descriptor ? descriptor.value : undefined;
return this.validationService.ValidateParam(param, value, name, fieldErrors, false, undefined);
}
case 'query':
return this.validationService.ValidateParam(param, request.query[name], name, fieldErrors, false, undefined);
case 'queries':
return this.validationService.ValidateParam(param, request.query, name, fieldErrors, false, undefined);
case 'path':
return this.validationService.ValidateParam(param, request.params[name], name, fieldErrors, false, undefined);
case 'header':
return this.validationService.ValidateParam(param, request.header(name), name, fieldErrors, false, undefined);
case 'body': {
const bodyFieldErrors = {};
const bodyArgs = this.validationService.ValidateParam(param, request.body, name, bodyFieldErrors, true, undefined);
Object.keys(bodyFieldErrors).forEach(key => {
fieldErrors[key] = { message: bodyFieldErrors[key].message };
});
return bodyArgs;
}
case 'body-prop': {
const bodyPropFieldErrors = {};
const bodyPropArgs = this.validationService.ValidateParam(param, request.body?.[name], name, bodyPropFieldErrors, true, 'body.');
Object.keys(bodyPropFieldErrors).forEach(key => {
fieldErrors[key] = { message: bodyPropFieldErrors[key].message };
});
return bodyPropArgs;
}
case 'formData': {
const files = Object.values(args).filter(p => p.dataType === 'file' || (p.dataType === 'array' && p.array && p.array.dataType === 'file'));
if ((param.dataType === 'file' || (param.dataType === 'array' && param.array && param.array.dataType === 'file')) && files.length > 0) {
const requestFiles = request.files;
const fileArgs = this.validationService.ValidateParam(param, requestFiles?.[name], name, fieldErrors, false, undefined);
if (param.dataType === 'array') {
return fileArgs;
}
return Array.isArray(fileArgs) && fileArgs.length === 1 ? fileArgs[0] : fileArgs;
}
return this.validationService.ValidateParam(param, request.body?.[name], name, fieldErrors, false, undefined);
}
case 'res':
return (status, data, headers) => {
this.returnHandler({ response, headers, statusCode: status, data });
};
}
});
if (Object.keys(fieldErrors).length > 0) {
throw new templateHelpers_1.ValidateError(fieldErrors, '');
}
return values;
}
returnHandler(params) {
const { response, statusCode, data } = params;
let { headers } = params;
headers = headers || {};
if (response.headersSent) {
return;
}
Object.keys(headers).forEach((name) => {
response.set(name, headers[name]);
});
// Check if the response is marked to be JSON
const isJsonResponse = response.get('Content-Type')?.includes('json') || false;
if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') {
response.status(statusCode || 200);
data.pipe(response);
}
else if (data !== undefined && (data !== null || isJsonResponse)) {
// allow null response when it is a json response
if (typeof data === 'number' || isJsonResponse) {
// express treats number data as status code so use the json method instead
// or if the response was marked as json then use the json so for example strings are quoted
response.status(statusCode || 200).json(data);
}
else {
// do not use json for every type since internally the send will invoke json if needed
// but for string data it will not quote it, so we can send string as plain/text data
response.status(statusCode || 200).send(data);
}
}
else {
response.status(statusCode || 204).end();
}
}
}
exports.ExpressTemplateService = ExpressTemplateService;
//# sourceMappingURL=expressTemplateService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
import { Request as HRequest, ResponseToolkit as HResponse } from '@hapi/hapi';
import { Controller } from '../../../interfaces/controller';
import { TsoaRoute } from '../../tsoa-route';
import { TemplateService } from '../templateService';
import { AdditionalProps } from '../../additionalProps';
type HapiApiHandlerParameters = {
methodName: string;
controller: Controller | object;
h: HResponse;
validatedArgs: any[];
successStatus?: number;
};
type HapiValidationArgsParameters = {
args: Record<string, TsoaRoute.ParameterSchema>;
request: HRequest;
h: HResponse;
};
type HapiReturnHandlerParameters = {
h: HResponse;
headers: any;
statusCode?: number;
data?: any;
};
export declare class HapiTemplateService extends TemplateService<HapiApiHandlerParameters, HapiValidationArgsParameters, HapiReturnHandlerParameters> {
protected readonly models: TsoaRoute.Models;
protected readonly config: AdditionalProps;
private readonly hapi;
constructor(models: TsoaRoute.Models, config: AdditionalProps, hapi: {
boomify: CallableFunction;
isBoom: CallableFunction;
});
apiHandler(params: HapiApiHandlerParameters): Promise<any>;
getValidatedArgs(params: HapiValidationArgsParameters): any[];
protected returnHandler(params: HapiReturnHandlerParameters): any;
}
export {};

View File

@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HapiTemplateService = void 0;
const templateHelpers_1 = require("../../templateHelpers");
const templateService_1 = require("../templateService");
const hapiTsoaResponsed = Symbol('@tsoa:template_service:hapi:responsed');
class HapiTemplateService extends templateService_1.TemplateService {
constructor(models, config, hapi) {
super(models, config);
this.models = models;
this.config = config;
this.hapi = hapi;
}
async apiHandler(params) {
const { methodName, controller, h, validatedArgs, successStatus } = params;
try {
const data = await this.buildPromise(methodName, controller, validatedArgs);
let statusCode = successStatus;
let headers;
if (this.isController(controller)) {
headers = controller.getHeaders();
statusCode = controller.getStatus() || statusCode;
}
return this.returnHandler({ h, headers, statusCode, data });
}
catch (error) {
if (this.hapi.isBoom(error)) {
throw error;
}
const boomErr = this.hapi.boomify(error instanceof Error ? error : new Error(error.message));
boomErr.output.statusCode = error.status || 500;
boomErr.output.payload = {
name: error.name,
message: error.message,
};
throw boomErr;
}
}
getValidatedArgs(params) {
const { args, request, h } = params;
const errorFields = {};
const values = Object.values(args).map(param => {
const name = param.name;
switch (param.in) {
case 'request':
return request;
case 'request-prop': {
const descriptor = Object.getOwnPropertyDescriptor(request, name);
const value = descriptor ? descriptor.value : undefined;
return this.validationService.ValidateParam(param, value, name, errorFields, false, undefined);
}
case 'query':
return this.validationService.ValidateParam(param, request.query[name], name, errorFields, false, undefined);
case 'queries':
return this.validationService.ValidateParam(param, request.query, name, errorFields, false, undefined);
case 'path':
return this.validationService.ValidateParam(param, request.params[name], name, errorFields, false, undefined);
case 'header':
return this.validationService.ValidateParam(param, request.headers[name], name, errorFields, false, undefined);
case 'body': {
const bodyFieldErrors = {};
const result = this.validationService.ValidateParam(param, request.payload, name, bodyFieldErrors, true, undefined);
Object.keys(bodyFieldErrors).forEach(key => {
errorFields[key] = { message: bodyFieldErrors[key].message };
});
return result;
}
case 'body-prop': {
const descriptor = Object.getOwnPropertyDescriptor(request.payload, name);
const value = descriptor ? descriptor.value : undefined;
const bodyFieldErrors = {};
const result = this.validationService.ValidateParam(param, value, name, bodyFieldErrors, true, 'body.');
Object.keys(bodyFieldErrors).forEach(key => {
errorFields[key] = { message: bodyFieldErrors[key].message };
});
return result;
}
case 'formData': {
const descriptor = Object.getOwnPropertyDescriptor(request.payload, name);
const value = descriptor ? descriptor.value : undefined;
return this.validationService.ValidateParam(param, value, name, errorFields, false, undefined);
}
case 'res':
return (status, data, headers) => {
this.returnHandler({ h, headers, statusCode: status, data });
};
}
});
if (Object.keys(errorFields).length > 0) {
throw new templateHelpers_1.ValidateError(errorFields, '');
}
return values;
}
returnHandler(params) {
const { h, statusCode, data } = params;
let { headers } = params;
headers = headers || {};
const tsoaResponsed = Object.getOwnPropertyDescriptor(h, hapiTsoaResponsed);
if (tsoaResponsed) {
return tsoaResponsed.value;
}
const response = data !== null && data !== undefined ? h.response(data).code(200) : h.response().code(204);
Object.keys(headers).forEach((name) => {
response.header(name, headers[name]);
});
if (statusCode) {
response.code(statusCode);
}
Object.defineProperty(h, hapiTsoaResponsed, {
value: response,
writable: false,
});
return response;
}
}
exports.HapiTemplateService = HapiTemplateService;
//# sourceMappingURL=hapiTemplateService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
export * from "./templateService";
export * from "./express/expressTemplateService";
export * from "./hapi/hapiTemplateService";
export * from "./koa/koaTemplateService";

View File

@@ -0,0 +1,21 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./templateService"), exports);
__exportStar(require("./express/expressTemplateService"), exports);
__exportStar(require("./hapi/hapiTemplateService"), exports);
__exportStar(require("./koa/koaTemplateService"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/routeGeneration/templates/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,mEAAiD;AACjD,6DAA2C;AAC3C,2DAAyC"}

View File

@@ -0,0 +1,29 @@
import type { Context, Next } from 'koa';
import { Controller } from '../../../interfaces/controller';
import { TsoaRoute } from '../../tsoa-route';
import { TemplateService } from '../templateService';
type KoaApiHandlerParameters = {
methodName: string;
controller: Controller | object;
context: Context;
validatedArgs: any[];
successStatus?: number;
};
type KoaValidationArgsParameters = {
args: Record<string, TsoaRoute.ParameterSchema>;
context: Context;
next: Next;
};
type KoaReturnHandlerParameters = {
context: Context;
next?: Next;
headers: any;
statusCode?: number;
data?: any;
};
export declare class KoaTemplateService extends TemplateService<KoaApiHandlerParameters, KoaValidationArgsParameters, KoaReturnHandlerParameters> {
apiHandler(params: KoaApiHandlerParameters): Promise<any>;
getValidatedArgs(params: KoaValidationArgsParameters): any[];
protected returnHandler(params: KoaReturnHandlerParameters): Promise<any> | Context | undefined;
}
export {};

View File

@@ -0,0 +1,116 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KoaTemplateService = void 0;
const templateHelpers_1 = require("../../templateHelpers");
const templateService_1 = require("../templateService");
const koaTsoaResponsed = Symbol('@tsoa:template_service:koa:is_responsed');
class KoaTemplateService extends templateService_1.TemplateService {
async apiHandler(params) {
const { methodName, controller, context, validatedArgs, successStatus } = params;
try {
const data = await this.buildPromise(methodName, controller, validatedArgs);
let statusCode = successStatus;
let headers;
if (this.isController(controller)) {
headers = controller.getHeaders();
statusCode = controller.getStatus() || statusCode;
}
return this.returnHandler({ context, headers, statusCode, data });
}
catch (error) {
context.status = error.status || 500;
context.throw(context.status, error.message, error);
}
}
getValidatedArgs(params) {
const { args, context, next } = params;
const errorFields = {};
const values = Object.values(args).map(param => {
const name = param.name;
switch (param.in) {
case 'request':
return context.request;
case 'request-prop': {
const descriptor = Object.getOwnPropertyDescriptor(context.request, name);
const value = descriptor ? descriptor.value : undefined;
return this.validationService.ValidateParam(param, value, name, errorFields, false, undefined);
}
case 'query':
return this.validationService.ValidateParam(param, context.request.query[name], name, errorFields, false, undefined);
case 'queries':
return this.validationService.ValidateParam(param, context.request.query, name, errorFields, false, undefined);
case 'path':
return this.validationService.ValidateParam(param, context.params[name], name, errorFields, false, undefined);
case 'header':
return this.validationService.ValidateParam(param, context.request.headers[name], name, errorFields, false, undefined);
case 'body': {
const descriptor = Object.getOwnPropertyDescriptor(context.request, 'body');
const value = descriptor ? descriptor.value : undefined;
const bodyFieldErrors = {};
const result = this.validationService.ValidateParam(param, value, name, bodyFieldErrors, true, undefined);
Object.keys(bodyFieldErrors).forEach((key) => {
errorFields[key] = { message: bodyFieldErrors[key].message };
});
return result;
}
case 'body-prop': {
const descriptor = Object.getOwnPropertyDescriptor(context.request, 'body');
const value = descriptor ? descriptor.value[name] : undefined;
const bodyFieldErrors = {};
const result = this.validationService.ValidateParam(param, value, name, bodyFieldErrors, true, 'body.');
Object.keys(bodyFieldErrors).forEach((key) => {
errorFields[key] = { message: bodyFieldErrors[key].message };
});
return result;
}
case 'formData': {
const files = Object.values(args).filter(p => p.dataType === 'file' || (p.dataType === 'array' && p.array && p.array.dataType === 'file'));
const contextRequest = context.request;
if ((param.dataType === 'file' || (param.dataType === 'array' && param.array && param.array.dataType === 'file')) && files.length > 0) {
const fileArgs = this.validationService.ValidateParam(param, contextRequest.files?.[name], name, errorFields, false, undefined);
if (param.dataType === 'array') {
return fileArgs;
}
return Array.isArray(fileArgs) && fileArgs.length === 1 ? fileArgs[0] : fileArgs;
}
return this.validationService.ValidateParam(param, contextRequest.body?.[name], name, errorFields, false, undefined);
}
case 'res':
return async (status, data, headers) => {
await this.returnHandler({ context, headers, statusCode: status, data, next });
};
}
});
if (Object.keys(errorFields).length > 0) {
throw new templateHelpers_1.ValidateError(errorFields, '');
}
return values;
}
returnHandler(params) {
const { context, next, statusCode, data } = params;
let { headers } = params;
headers = headers || {};
const isResponsed = Object.getOwnPropertyDescriptor(context.response, koaTsoaResponsed);
if (!context.headerSent && !isResponsed) {
if (data !== null && data !== undefined) {
context.body = data;
context.status = 200;
}
else {
context.status = 204;
}
if (statusCode) {
context.status = statusCode;
}
context.set(headers);
Object.defineProperty(context.response, koaTsoaResponsed, {
value: true,
writable: false,
});
return next ? next() : context;
}
return undefined;
}
}
exports.KoaTemplateService = KoaTemplateService;
//# sourceMappingURL=koaTemplateService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import { Controller } from '../../interfaces/controller';
import { TsoaRoute } from '../tsoa-route';
import { ValidationService } from '../templateHelpers';
import { AdditionalProps } from '../additionalProps';
export declare abstract class TemplateService<ApiHandlerParameters, ValidationArgsParameters, ReturnHandlerParameters> {
protected readonly models: TsoaRoute.Models;
protected readonly config: AdditionalProps;
protected validationService: ValidationService;
constructor(models: TsoaRoute.Models, config: AdditionalProps);
abstract apiHandler(params: ApiHandlerParameters): Promise<any>;
abstract getValidatedArgs(params: ValidationArgsParameters): any[];
protected abstract returnHandler(params: ReturnHandlerParameters): any;
protected isController(object: Controller | object): object is Controller;
protected buildPromise(methodName: string, controller: Controller | object, validatedArgs: any): Promise<PropertyDescriptor>;
}

View File

@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TemplateService = void 0;
const templateHelpers_1 = require("../templateHelpers");
class TemplateService {
constructor(models, config) {
this.models = models;
this.config = config;
this.validationService = new templateHelpers_1.ValidationService(models, config);
}
isController(object) {
return 'getHeaders' in object && 'getStatus' in object && 'setStatus' in object;
}
buildPromise(methodName, controller, validatedArgs) {
const prototype = Object.getPrototypeOf(controller);
const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
return descriptor.value.apply(controller, validatedArgs);
}
}
exports.TemplateService = TemplateService;
//# sourceMappingURL=templateService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"templateService.js","sourceRoot":"","sources":["../../../src/routeGeneration/templates/templateService.ts"],"names":[],"mappings":";;;AAEA,wDAAuD;AAGvD,MAAsB,eAAe;IAGnC,YACqB,MAAwB,EACxB,MAAuB;QADvB,WAAM,GAAN,MAAM,CAAkB;QACxB,WAAM,GAAN,MAAM,CAAiB;QAE1C,IAAI,CAAC,iBAAiB,GAAG,IAAI,mCAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC;IAQS,YAAY,CAAC,MAA2B;QAChD,OAAO,YAAY,IAAI,MAAM,IAAI,WAAW,IAAI,MAAM,IAAI,WAAW,IAAI,MAAM,CAAC;IAClF,CAAC;IAES,YAAY,CAAC,UAAkB,EAAE,UAA+B,EAAE,aAAkB;QAC5F,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QACpD,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAC1E,OAAQ,UAAW,CAAC,KAA2C,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACnG,CAAC;CACF;AAzBD,0CAyBC"}

View File

@@ -0,0 +1,56 @@
import { Tsoa } from './../metadataGeneration/tsoa';
/**
* For Swagger, additionalProperties is implicitly allowed. So use this function to clarify that undefined should be associated with allowing additional properties
* @param test if this is undefined then you should interpret it as a "yes"
*/
export declare function isDefaultForAdditionalPropertiesAllowed(test: TsoaRoute.RefObjectModelSchema['additionalProperties']): test is undefined;
export declare namespace TsoaRoute {
interface Models {
[name: string]: ModelSchema;
}
/**
* This is a convenience type so you can check .properties on the items in the Record without having TypeScript throw a compiler error. That's because this Record can't have enums in it. If you want that, then just use the base interface
*/
interface RefObjectModels extends TsoaRoute.Models {
[refNames: string]: TsoaRoute.RefObjectModelSchema;
}
interface RefEnumModelSchema {
dataType: 'refEnum';
enums: Array<string | number>;
}
interface RefObjectModelSchema {
dataType: 'refObject';
properties: {
[name: string]: PropertySchema;
};
additionalProperties?: boolean | PropertySchema;
}
interface RefTypeAliasModelSchema {
dataType: 'refAlias';
type: PropertySchema;
}
type ModelSchema = RefEnumModelSchema | RefObjectModelSchema | RefTypeAliasModelSchema;
type ValidatorSchema = Tsoa.Validators;
interface PropertySchema {
dataType?: Tsoa.TypeStringLiteral;
ref?: string;
required?: boolean;
array?: PropertySchema;
enums?: Array<string | number | boolean | null>;
type?: PropertySchema;
subSchemas?: PropertySchema[];
validators?: ValidatorSchema;
default?: unknown;
additionalProperties?: boolean | PropertySchema;
nestedProperties?: {
[name: string]: PropertySchema;
};
}
interface ParameterSchema extends PropertySchema {
name: string;
in: string;
}
interface Security {
[key: string]: string[];
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isDefaultForAdditionalPropertiesAllowed = isDefaultForAdditionalPropertiesAllowed;
/**
* For Swagger, additionalProperties is implicitly allowed. So use this function to clarify that undefined should be associated with allowing additional properties
* @param test if this is undefined then you should interpret it as a "yes"
*/
function isDefaultForAdditionalPropertiesAllowed(test) {
return test === undefined;
}
//# sourceMappingURL=tsoa-route.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"tsoa-route.js","sourceRoot":"","sources":["../../src/routeGeneration/tsoa-route.ts"],"names":[],"mappings":";;AAMA,0FAEC;AAND;;;GAGG;AACH,SAAgB,uCAAuC,CAAC,IAA4D;IAClH,OAAO,IAAI,KAAK,SAAS,CAAC;AAC5B,CAAC"}