Repository: nestjs/swagger
Pull request #1788 (2022-01-21 07:55:26)

feat: added support for decorators Matches, IsPositive, IsNegative, L…
Added support for 
`@Matches`, `@IsPositive`,  `@IsNegative`, `@Length`, `@IsIn`

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Currently decorators: `Matches`, `IsPositive`,  `IsNegative`, `Length` `IsIn` are ignored


## What is the new behavior?
Added support for those decorators

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information



Patch:
diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts
index eb4517bf..e11031ac 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<string, ts.ObjectLiteralExpression>;
 
@@ -396,6 +397,20 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     const assignments = [];
     const decorators = node.decorators;
 
+    this.addPropertyByValidationDecorator(
+      factory,
+      'Matches',
+      'pattern',
+      decorators,
+      assignments
+    );
+    this.addPropertyByValidationDecorator(
+      factory,
+      'IsIn',
+      'enum',
+      decorators,
+      assignments
+    );
     this.addPropertyByValidationDecorator(
       factory,
       'Min',
@@ -424,6 +439,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 +503,38 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     decorators: ts.NodeArray<ts.Decorator>,
     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<ts.Decorator>,
+    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/package-lock.json b/package-lock.json
index e777806d..91199733 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7735,9 +7735,9 @@
       "dev": true
     },
     "lint-staged": {
-      "version": "12.2.2",
-      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.2.2.tgz",
-      "integrity": "sha512-bcHEoM1M/f+K1BYdHcEuIn8K+zMOSJR3mkny6PAuQiTgcSUcRbUWaUD6porAYypxF4k1vYZZ2HutZt1p94Z1jQ==",
+      "version": "12.2.1",
+      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.2.1.tgz",
+      "integrity": "sha512-VCVcA9C2Vt5HHxSR4EZVZFJcQRJH984CGBeY+cJ/xed4mBd+JidbM/xbKcCq5ASaygAV0iITtdsCTnID7h/1OQ==",
       "dev": true,
       "requires": {
         "cli-truncate": "^3.1.0",
diff --git a/package.json b/package.json
index ea8cecc8..51e12385 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
     "fastify-swagger": "4.13.1",
     "husky": "7.0.4",
     "jest": "27.4.7",
-    "lint-staged": "12.2.2",
+    "lint-staged": "12.2.1",
     "prettier": "2.5.1",
     "reflect-metadata": "0.1.13",
     "release-it": "14.12.3",


Test patch:
diff --git a/test/plugin/fixtures/create-cat.dto.ts b/test/plugin/fixtures/create-cat.dto.ts
index 248d5d79..3dc64651 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, IsIn } from 'class-validator';
 
 enum Status {
     ENABLED,
@@ -19,10 +19,22 @@ class OtherNode {
 }
 
 export class CreateCatDto {
+  @IsIn(['a', 'b'])
+  isIn: string;
+  @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 +61,7 @@ export class CreateCatDto {
 }
 `;
 
-export const createCatDtoTextTranspiled = `import { IsString } 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\";
@@ -67,16 +79,36 @@ 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 { 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);
 __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()