From 733f562f7159be389159e4656acc69e1e0431470 Mon Sep 17 00:00:00 2001 From: Nick Papirniy Date: Fri, 21 Jan 2022 09:52:24 +0200 Subject: [PATCH 1/2] feat: added support for decorators Matches, IsPositive, IsNegative, Length --- lib/plugin/visitors/model-class.visitor.ts | 92 ++++++++++++++++++++-- test/plugin/fixtures/create-cat.dto.ts | 33 +++++++- 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts index eb4517bf2..34e399904 100644 --- a/lib/plugin/visitors/model-class.visitor.ts +++ b/lib/plugin/visitors/model-class.visitor.ts @@ -21,6 +21,7 @@ import { replaceImportPath } from '../utils/plugin-utils'; import { AbstractFileVisitor } from './abstract.visitor'; +import { PropertyAssignment } from 'typescript'; type ClassMetadata = Record; @@ -396,6 +397,13 @@ export class ModelClassVisitor extends AbstractFileVisitor { const assignments = []; const decorators = node.decorators; + this.addPropertyByValidationDecorator( + factory, + 'Matches', + 'pattern', + decorators, + assignments + ); this.addPropertyByValidationDecorator( factory, 'Min', @@ -424,6 +432,59 @@ export class ModelClassVisitor extends AbstractFileVisitor { decorators, assignments ); + this.addPropertiesByValidationDecorator( + factory, + 'IsPositive', + decorators, + assignments, + () => { + return [ + factory.createPropertyAssignment( + 'minimum', + createPrimitiveLiteral(factory, 1) + ) + ]; + } + ); + this.addPropertiesByValidationDecorator( + factory, + 'IsNegative', + decorators, + assignments, + () => { + return [ + factory.createPropertyAssignment( + 'maximum', + createPrimitiveLiteral(factory, -1) + ) + ]; + } + ); + this.addPropertiesByValidationDecorator( + factory, + 'Length', + decorators, + assignments, + (decoratorRef: ts.Decorator) => { + const decoratorArguments = getDecoratorArguments(decoratorRef); + + const result = []; + result.push( + factory.createPropertyAssignment( + 'minLength', + head(decoratorArguments) + ) + ); + + if (decoratorArguments.length > 1) { + result.push( + factory.createPropertyAssignment('maxLength', decoratorArguments[1]) + ); + } + + return result; + } + ); return assignments; } @@ -435,17 +496,38 @@ export class ModelClassVisitor extends AbstractFileVisitor { decorators: ts.NodeArray, assignments: ts.PropertyAssignment[] ) { - const decoratorRef = getDecoratorOrUndefinedByNames( + this.addPropertiesByValidationDecorator( + factory, + decoratorName, + decorators, + assignments, + (decoratorRef: ts.Decorator) => { + const argument: ts.Expression = head( + getDecoratorArguments(decoratorRef) + ); + if (argument) { + return [factory.createPropertyAssignment(propertyKey, argument)]; + } + return []; + } + ); + } + + addPropertiesByValidationDecorator( + factory: ts.NodeFactory, + decoratorName: string, + decorators: ts.NodeArray, + assignments: ts.PropertyAssignment[], + addPropertyAssignments: (decoratorRef: ts.Decorator) => PropertyAssignment[] + ) { + const decoratorRef: ts.Decorator = getDecoratorOrUndefinedByNames( [decoratorName], decorators ); if (!decoratorRef) { return; } - const argument: ts.Expression = head(getDecoratorArguments(decoratorRef)); - if (argument) { - assignments.push(factory.createPropertyAssignment(propertyKey, argument)); - } + assignments.push(...addPropertyAssignments(decoratorRef)); } addClassMetadata( diff --git a/test/plugin/fixtures/create-cat.dto.ts b/test/plugin/fixtures/create-cat.dto.ts index 248d5d79b..7fb025c4a 100644 --- a/test/plugin/fixtures/create-cat.dto.ts +++ b/test/plugin/fixtures/create-cat.dto.ts @@ -1,5 +1,5 @@ export const createCatDtoText = ` -import { IsInt, IsString } from 'class-validator'; +import { IsInt, IsString, IsPositive, IsNegative, Length, Matches } from 'class-validator'; enum Status { ENABLED, @@ -19,10 +19,20 @@ class OtherNode { } export class CreateCatDto { + @Matches(/^[+]?abc$/) + pattern: string; name: string; @Min(0) @Max(10) age: number = 3; + @IsPositive() + positive: number = 5; + @IsNegative() + negative: number = -1; + @Length(2) + lengthMin: string; + @Length(3, 5) + lengthMinMax: string; tags: string[]; status: Status = Status.ENABLED; status2?: Status; @@ -49,7 +59,7 @@ export class CreateCatDto { } `; -export const createCatDtoTextTranspiled = `import { IsString } from 'class-validator'; +export const createCatDtoTextTranspiled = `import { IsString, IsPositive, IsNegative, Length, Matches } from 'class-validator'; var Status; (function (Status) { Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; @@ -67,16 +77,33 @@ class OtherNode { export class CreateCatDto { constructor() { this.age = 3; + this.positive = 5; + this.negative = -1; this.status = Status.ENABLED; } static _OPENAPI_METADATA_FACTORY() { - return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] } }; + return { pattern: { required: true, type: () => String, pattern: /^[+]?abc$/ }, name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, positive: { required: true, type: () => Number, default: 5, minimum: 1 }, negative: { required: true, type: () => Number, default: -1, maximum: -1 }, lengthMin: { required: true, type: () => String, minLength: 2 }, lengthMinMax: { required: true, type: () => String, minLength: 3, maxLength: 5 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] } }; } } +__decorate([ + Matches(/^[+]?abc$/) +], CreateCatDto.prototype, \"pattern\", void 0); __decorate([ Min(0), Max(10) ], CreateCatDto.prototype, \"age\", void 0); +__decorate([ + IsPositive() +], CreateCatDto.prototype, \"positive\", void 0); +__decorate([ + IsNegative() +], CreateCatDto.prototype, \"negative\", void 0); +__decorate([ + Length(2) +], CreateCatDto.prototype, \"lengthMin\", void 0); +__decorate([ + Length(3, 5) +], CreateCatDto.prototype, \"lengthMinMax\", void 0); __decorate([ ApiProperty({ description: "this is breed", type: String }), IsString() From 1cee3337283dda299235c0a375cc40a932c435e3 Mon Sep 17 00:00:00 2001 From: Nick Papirniy Date: Fri, 21 Jan 2022 10:13:50 +0200 Subject: [PATCH 2/2] feat: added support for decorators Matches, IsPositive, IsNegative, Length, IsIn --- lib/plugin/visitors/model-class.visitor.ts | 7 +++++++ test/plugin/fixtures/create-cat.dto.ts | 11 ++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts index 34e399904..e11031ac0 100644 --- a/lib/plugin/visitors/model-class.visitor.ts +++ b/lib/plugin/visitors/model-class.visitor.ts @@ -404,6 +404,13 @@ export class ModelClassVisitor extends AbstractFileVisitor { decorators, assignments ); + this.addPropertyByValidationDecorator( + factory, + 'IsIn', + 'enum', + decorators, + assignments + ); this.addPropertyByValidationDecorator( factory, 'Min', diff --git a/test/plugin/fixtures/create-cat.dto.ts b/test/plugin/fixtures/create-cat.dto.ts index 7fb025c4a..3dc646517 100644 --- a/test/plugin/fixtures/create-cat.dto.ts +++ b/test/plugin/fixtures/create-cat.dto.ts @@ -1,5 +1,5 @@ export const createCatDtoText = ` -import { IsInt, IsString, IsPositive, IsNegative, Length, Matches } from 'class-validator'; +import { IsInt, IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; enum Status { ENABLED, @@ -19,6 +19,8 @@ class OtherNode { } export class CreateCatDto { + @IsIn(['a', 'b']) + isIn: string; @Matches(/^[+]?abc$/) pattern: string; name: string; @@ -59,7 +61,7 @@ export class CreateCatDto { } `; -export const createCatDtoTextTranspiled = `import { IsString, IsPositive, IsNegative, Length, Matches } from 'class-validator'; +export const createCatDtoTextTranspiled = `import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; var Status; (function (Status) { Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; @@ -82,9 +84,12 @@ export class CreateCatDto { this.status = Status.ENABLED; } static _OPENAPI_METADATA_FACTORY() { - return { pattern: { required: true, type: () => String, pattern: /^[+]?abc$/ }, name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, positive: { required: true, type: () => Number, default: 5, minimum: 1 }, negative: { required: true, type: () => Number, default: -1, maximum: -1 }, lengthMin: { required: true, type: () => String, minLength: 2 }, lengthMinMax: { required: true, type: () => String, minLength: 3, maxLength: 5 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] } }; + return { isIn: { required: true, type: () => String, enum: ['a', 'b'] }, pattern: { required: true, type: () => String, pattern: /^[+]?abc$/ }, name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, positive: { required: true, type: () => Number, default: 5, minimum: 1 }, negative: { required: true, type: () => Number, default: -1, maximum: -1 }, lengthMin: { required: true, type: () => String, minLength: 2 }, lengthMinMax: { required: true, type: () => String, minLength: 3, maxLength: 5 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] } }; } } +__decorate([ + IsIn(['a', 'b']) +], CreateCatDto.prototype, \"isIn\", void 0); __decorate([ Matches(/^[+]?abc$/) ], CreateCatDto.prototype, \"pattern\", void 0);