Skip to content

Commit

Permalink
Validation
Browse files Browse the repository at this point in the history
  • Loading branch information
WoH committed Aug 17, 2019
1 parent 49f3bb8 commit da71a89
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 48 deletions.
7 changes: 4 additions & 3 deletions src/metadataGeneration/tsoa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ export namespace Tsoa {
validators: Validators;
}

export type TypeStringLiteral = 'string' | 'boolean' | 'double' | 'float' | 'integer' | 'long' | 'enum' | 'array' | 'datetime' | 'date' | 'binary' | 'buffer' | 'byte' | 'void' | 'object' | 'any' | 'refEnum' | 'refObject' | 'objectLiteral';
export type TypeStringLiteral = 'string' | 'boolean' | 'double' | 'float' | 'integer' | 'long' | 'enum' | 'array' | 'datetime' | 'date' | 'binary' | 'buffer' | 'byte' | 'void' | 'object' | 'any' | 'refEnum' | 'refObject' | 'nestedObjectLiteral';

export type RefTypeLiteral = 'refObject' | 'refEnum';

export type PrimitiveTypeLiteral = Exclude<TypeStringLiteral, RefTypeLiteral | 'enum' | 'array' | 'void' | 'objectLiteral'>;
export type PrimitiveTypeLiteral = Exclude<TypeStringLiteral, RefTypeLiteral | 'enum' | 'array' | 'void' | 'nestedObjectLiteral'>;

export interface Type {
dataType: TypeStringLiteral;
Expand All @@ -88,8 +88,9 @@ export namespace Tsoa {
}

export interface ObjectLiteralType extends Type {
dataType: 'objectLiteral';
dataType: 'nestedObjectLiteral';
properties: Property[];
additionalProperties?: Type;
}

export interface ArrayType extends Type {
Expand Down
33 changes: 28 additions & 5 deletions src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const inProgressTypes: { [typeName: string]: boolean } = {};

type UsableDeclaration = ts.InterfaceDeclaration
| ts.ClassDeclaration
| ts.TypeAliasDeclaration;
| ts.TypeAliasDeclaration
| ts.PropertySignature;

export class TypeResolver {
constructor(
Expand Down Expand Up @@ -67,23 +68,45 @@ export class TypeResolver {
}

if (this.typeNode.kind === ts.SyntaxKind.TypeLiteral) {
const properties = (this.typeNode as ts.TypeLiteralNode)
const typeLiteralNode = this.typeNode as ts.TypeLiteralNode;
const properties = typeLiteralNode
.members
.filter(member => member.kind === ts.SyntaxKind.PropertySignature)
.reduce((res, propertySignature: ts.PropertySignature) => {
const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, this.typeNode).resolve();

return [
{
default: getJSDocComment(propertySignature, 'default'),
description: this.getNodeDescription(propertySignature),
format: this.getNodeFormat(propertySignature),
name: (propertySignature.name as ts.Identifier).text,
required: !propertySignature.questionToken,
type,
validators: getPropertyValidators(propertySignature),
} as Tsoa.Property,
...res,
];
}, []);

return { dataType: 'objectLiteral', properties } as Tsoa.ObjectLiteralType;
const indexMember = typeLiteralNode.members.find((member) => member.kind === ts.SyntaxKind.IndexSignature);
let additionalType: Tsoa.Type | undefined;

if (indexMember) {
const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration;
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current).resolve();
if (indexType.dataType !== 'string') {
throw new GenerateMetadataError(`Only string indexers are supported.`);
}

additionalType = new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current).resolve();
}

return {
additionalProperties: indexMember && additionalType,
dataType: 'nestedObjectLiteral',
properties,
} as Tsoa.ObjectLiteralType;
}

if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword) {
Expand Down Expand Up @@ -445,7 +468,7 @@ export class TypeResolver {

const modelTypeDeclaration = node as UsableDeclaration;
return (modelTypeDeclaration.name as ts.Identifier).text === typeName;
}) as UsableDeclaration[];
}) as Array<Exclude<UsableDeclaration, ts.PropertySignature>>;

if (!modelTypes.length) {
throw new GenerateMetadataError(`No matching model found for referenced type ${typeName}. If ${typeName} comes from a dependency, please create an interface in your own code that has the same structure. Tsoa can not utilize interfaces from external dependencies. Read more at https://github.com/lukeautry/tsoa/blob/master/ExternalInterfacesExplanation.MD`);
Expand Down Expand Up @@ -646,7 +669,7 @@ export class TypeResolver {
return undefined;
}

private getModelInheritedProperties(modelTypeDeclaration: UsableDeclaration): Tsoa.Property[] {
private getModelInheritedProperties(modelTypeDeclaration: Exclude<UsableDeclaration, ts.PropertySignature>): Tsoa.Property[] {
const properties = [] as Tsoa.Property[];
if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) {
return [];
Expand Down
11 changes: 11 additions & 0 deletions src/routeGeneration/routeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ export class RouteGenerator {
if (type.dataType === 'enum') {
schema.enums = (type as Tsoa.EnumerateType).enums;
}

if (type.dataType === 'nestedObjectLiteral') {
const objLiteral = type as Tsoa.ObjectLiteralType;

schema.nestedProperties = objLiteral.properties.reduce((acc, prop) => {
return {...acc, [prop.name]: this.buildPropertySchema(prop)};
}, {});

schema.additionalProperties = objLiteral.additionalProperties && this.buildProperty(objLiteral.additionalProperties);
}

return schema;
}
}
76 changes: 76 additions & 0 deletions src/routeGeneration/templateHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export class ValidationService {
return this.validateBuffer(name, value);
case 'any':
return value;
case 'nestedObjectLiteral':
return this.validateNestedObjectLiteral(name, value, fieldErrors, minimalSwaggerConfig, property.nestedProperties, property.additionalProperties, parent);
default:
if (property.ref) {
return this.validateModel({ name, value, refName: property.ref, fieldErrors, parent, minimalSwaggerConfig });
Expand All @@ -67,6 +69,80 @@ export class ValidationService {
}
}

public validateNestedObjectLiteral(
name: string,
value: any,
fieldErrors: FieldErrors,
minimalSwaggerConfig: SwaggerConfigRelatedToRoutes,
nestedProperties: { [name: string]: TsoaRoute.PropertySchema} | undefined,
additionalProperties: TsoaRoute.PropertySchema | boolean | undefined,
parent: string,
) {
if (!nestedProperties) {
return;
}

const propHandling = minimalSwaggerConfig.noImplicitAdditionalProperties;
if (propHandling) {
const excessProps = this.getExcessPropertiesFor({properties: nestedProperties, additionalProperties}, Object.keys(value));
if (excessProps.length > 0) {
if (propHandling) {
excessProps.forEach(excessProp => {
delete value[excessProp];
});
if (propHandling === 'throw-on-extras') {
fieldErrors[parent + name] = {
message: `"${excessProps}" is an excess property and therefore is not allowed`,
value: excessProps,
};
}
}
}
}

Object.keys(value).forEach(key => {
if (!nestedProperties[key]) {
if (additionalProperties && additionalProperties !== true) {
return this.ValidateParam(
additionalProperties,
value[key],
key,
fieldErrors,
parent + name + '.',
minimalSwaggerConfig,
);
} else {
return key;
}
}

return this.ValidateParam(
nestedProperties[key],
value[key],
key,
fieldErrors,
parent + name + '.',
minimalSwaggerConfig,
);
});

return value;
}

private getExcessPropertiesFor(modelDefinition: TsoaRoute.ModelSchema, properties: string[]): string[] {
if (!modelDefinition || !modelDefinition.properties) {
return properties;
}

const modelProperties = new Set(Object.keys(modelDefinition.properties));

if (modelDefinition.additionalProperties) {
return [];
} else {
return [...properties].filter(property => !modelProperties.has(property));
}
}

public validateInt(name: string, value: any, fieldErrors: FieldErrors, validators?: IntegerValidator, parent = '') {
if (!validator.isInt(String(value))) {
let message = `invalid integer number`;
Expand Down
2 changes: 2 additions & 0 deletions src/routeGeneration/tsoa-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export namespace TsoaRoute {
enums?: string[];
validators?: ValidatorSchema;
default?: any;
additionalProperties?: boolean | PropertySchema;
nestedProperties?: {[name: string]: PropertySchema};
}

export interface ParameterSchema extends PropertySchema {
Expand Down
2 changes: 1 addition & 1 deletion src/swagger/specGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class SpecGenerator {
return this.getSwaggerTypeForArrayType(type as Tsoa.ArrayType);
} else if (type.dataType === 'enum') {
return this.getSwaggerTypeForEnumType(type as Tsoa.EnumerateType);
} else if (type.dataType === 'objectLiteral') {
} else if (type.dataType === 'nestedObjectLiteral') {
return this.getSwaggerTypeForObjectLiteral(type as Tsoa.ObjectLiteralType);
} else {
return assertNever(type.dataType);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/validatorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function getParameterValidators(parameter: ts.ParameterDeclaration, param
}, {} as Tsoa.Validators);
}

export function getPropertyValidators(property: ts.PropertyDeclaration): Tsoa.Validators | undefined {
export function getPropertyValidators(property: ts.PropertyDeclaration | ts.PropertySignature): Tsoa.Validators | undefined {
const tags = getJSDocTags(property, (tag) => {
return getParameterTagSupport().some(value => value === tag.tagName.text);
});
Expand Down
5 changes: 4 additions & 1 deletion tests/fixtures/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,13 @@ export interface TestModel extends Model {
nested?: {
bool: boolean,
optional?: number,
allOptional: {
allNestedOptional: {
one?: string,
two?: string,
},
additionals?: {
[name: string]: TypeAliasModel1,
},
},
};
}
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/express-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ describe('Express Server', () => {
});
});

it('removes additional properties', () => {
const model = getFakeModel();
const data = {
...model,
objLiteral: {
...model.objLiteral,
extra: 123,
nested: {
anotherExtra: 123,
},
},
};

return verifyPostRequest(basePath + '/PostTest', data, (err: any, res: any) => {
const resModel = res.body as TestModel;
expect(resModel).to.deep.equal({...model, objLiteral: { ...model.objLiteral, nested: {}}});
expect(res.status).to.eq(200);
});
});

it('correctly returns status code', () => {
const data = getFakeModel();
const path = basePath + '/PostTest/WithDifferentReturnCode';
Expand Down
40 changes: 40 additions & 0 deletions tests/integration/koa-server-no-additional-allowed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => {
}, 400);
});

it('should call out any additionalProperties', () => {
const data = Object.assign({}, getFakeModel(), {
objLiteral: {
extra: 123,
nested: {
anotherExtra: 123,
},
},
});

return verifyPostRequest(basePath + '/PostTest', data, (err: any, res: any) => {
const body = JSON.parse(err.text);
expect(body.fields['model.objLiteral'].message).to.eql('"extra" is an excess property and therefore is not allowed');
expect(body.fields['model.objLiteral.nested'].message).to.eql('"anotherExtra" is an excess property and therefore is not allowed');
}, 400);
});

it('should respect additional props', () => {
const fakeModel = getFakeModel();
const data = {
...fakeModel,
objLiteral: {
name: 'hello',
nested: {
additionals: {
one: { value1: ''},
},
allNestedOptional: {},
bool: true,
},
},
} as TestModel;

return verifyPostRequest(basePath + '/PostTest', data, (err: any, res: any) => {
expect(err).to.equal(false);
const model = res.body as TestModel;
expect(model).to.deep.equal(data);
});
});

it('should be okay if there are no additionalProperties', () => {
const data = getFakeModel();

Expand Down
37 changes: 19 additions & 18 deletions tests/unit/swagger/definitionsGeneration/definitions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,28 +319,29 @@ describe('Definition generation', () => {
expect(propertySchema['x-nullable']).to.eq(true, `for property ${propertyName}[x-nullable]`);
},
objLiteral: (propertyName, propertySchema) => {
expect(propertySchema).to.deep.include({
properties: {
name: {
type: 'string',
},
nested: {
properties: {
allOptional: {
expect(propertySchema).to.deep.equal({
default: undefined,
description: undefined,
format: undefined,
properties: {
name: { type: 'string' },
nested: {
properties: {
additionals: { properties: {}, type: 'object' },
allNestedOptional: {
properties: { one: { type: 'string' }, two: { type: 'string' } },
type: 'object',
},
bool: { type: 'boolean' },
optional: { format: 'double', type: 'number'},
},
required: ['allOptional', 'bool'],
type: 'object',
bool: { type: 'boolean' },
optional: { type: 'number', format: 'double' },
},
},
required: ['name'],
type: 'object',
});
},
required: [ 'allNestedOptional', 'bool' ],
type: 'object' },
},
required: [ 'name' ],
type: 'object',
});
},
};

Object.keys(assertionsPerProperty).forEach(aPropertyName => {
Expand Down
Loading

0 comments on commit da71a89

Please sign in to comment.