Repository: nestjs/swagger
Pull request #1434 (2021-07-08 22:38:56)

fix(): move modifiers properties to items object on array type
## PR Checklist
Please check if your PR fulfills the following requirements:

- [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] 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". -->
```
[x] Bugfix
[ ] 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. -->
Array type under `parameters` has its items' modifiers on the same level as `items` where those modifiers should be children of `items`

```yaml
parameters:
   formatArray:
      type: 'array'
      format: 'uuid'
      items:
         type: string
```

Issue Number: #1391 


## What is the new behavior?
Modifiers are now moved inside of `items`
```diff
parameters:
   formatArray:
      type: 'array'
-     format: 'uuid'
      items:
         type: string
+        format: 'uuid'
```


## 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/services/swagger-types-mapper.ts b/lib/services/swagger-types-mapper.ts
index 3167ed06..be4feead 100644
--- a/lib/services/swagger-types-mapper.ts
+++ b/lib/services/swagger-types-mapper.ts
@@ -1,10 +1,6 @@
-import { isFunction, isUndefined, omit, omitBy } from 'lodash';
+import { isFunction, isString, isUndefined, omit, omitBy, pick } from 'lodash';
 import { ApiPropertyOptions } from '../decorators';
-import {
-  BaseParameterObject,
-  ReferenceObject,
-  SchemaObject
-} from '../interfaces/open-api-spec.interface';
+import { BaseParameterObject, ReferenceObject, SchemaObject } from '../interfaces/open-api-spec.interface';
 import { ParamWithTypeMetadata } from './parameter-metadata-accessor';
 
 export class SwaggerTypesMapper {
@@ -91,6 +87,12 @@ export class SwaggerTypesMapper {
     param: (ParamWithTypeMetadata & SchemaObject) | BaseParameterObject,
     keysToRemove: Array<keyof ApiPropertyOptions | '$ref'>
   ) {
+    const itemsModifierKeys = [
+      'format',
+      'maximum',
+      'minimum',
+      'pattern'
+    ];
     const items =
       (param as SchemaObject).items ||
       omitBy(
@@ -101,12 +103,15 @@ export class SwaggerTypesMapper {
         },
         isUndefined
       );
+    const modifierProperties = pick(param, itemsModifierKeys);
     return {
       ...omit(param, keysToRemove),
       schema: {
-        ...this.getSchemaOptions(param),
+        ...omit(this.getSchemaOptions(param), [...itemsModifierKeys]),
         type: 'array',
-        items
+        items: isString(items.type)
+          ? { type: items.type, ...modifierProperties }
+          : { ...items.type, ...modifierProperties }
       }
     };
   }


Test patch:
diff --git a/test/explorer/swagger-explorer.spec.ts b/test/explorer/swagger-explorer.spec.ts
index 6313a4b8..d5cc1a2d 100644
--- a/test/explorer/swagger-explorer.spec.ts
+++ b/test/explorer/swagger-explorer.spec.ts
@@ -1,13 +1,4 @@
-import {
-  Body,
-  Controller,
-  Get,
-  Param,
-  Post,
-  Query,
-  Version,
-  VersioningType
-} from '@nestjs/common';
+import { Body, Controller, Get, Param, Post, Query, Version, VersioningType } from '@nestjs/common';
 import { ApplicationConfig } from '@nestjs/core';
 import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
 import {
@@ -45,9 +36,11 @@ describe('SwaggerExplorer', () => {
   ) => `${controllerKey}.${methodKey}`;
 
   describe('when module only uses metadata', () => {
-    class Foo {}
+    class Foo {
+    }
 
-    class CreateFoo {}
+    class CreateFoo {
+    }
 
     enum LettersEnum {
       A = 'A',
@@ -77,6 +70,9 @@ describe('SwaggerExplorer', () => {
         isArray: true
       })
       enumArr: LettersEnum;
+
+      @ApiProperty({ type: [String], format: 'uuid' })
+      formatArray: string[];
     }
 
     @Controller('')
@@ -166,7 +162,7 @@ describe('SwaggerExplorer', () => {
       expect(routes[0].root.method).toEqual('post');
       expect(routes[0].root.path).toEqual('/globalPrefix/modulePath/foos');
       expect(routes[0].root.summary).toEqual('Create foo');
-      expect(routes[0].root.parameters.length).toEqual(5);
+      expect(routes[0].root.parameters.length).toEqual(6);
       expect(routes[0].root.parameters).toEqual([
         {
           in: 'query',
@@ -215,6 +211,18 @@ describe('SwaggerExplorer', () => {
             },
             type: 'array'
           }
+        },
+        {
+          in: 'query',
+          name: 'formatArray',
+          required: true,
+          schema: {
+            type: 'array',
+            items: {
+              type: 'string',
+              format: 'uuid'
+            }
+          }
         }
       ]);
       expect(routes[0].root.requestBody).toEqual({
@@ -234,7 +242,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[0].responses['201'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           $ref: '#/components/schemas/Foo'
@@ -273,7 +281,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[1].responses['200'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           type: 'array',
@@ -286,9 +294,11 @@ describe('SwaggerExplorer', () => {
   });
 
   describe('when explicit decorators and metadata are used', () => {
-    class Foo {}
+    class Foo {
+    }
 
-    class CreateFoo {}
+    class CreateFoo {
+    }
 
     @Controller('')
     @ApiBadRequestResponse({ description: 'Bad request' })
@@ -400,7 +410,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[0].responses['201'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           $ref: '#/components/schemas/Foo'
@@ -438,7 +448,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[1].responses['200'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           type: 'array',
@@ -450,7 +460,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[1].responses.default as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           type: 'array',
@@ -462,9 +472,11 @@ describe('SwaggerExplorer', () => {
     };
   });
   describe('when only explicit decorators are used', () => {
-    class Foo {}
+    class Foo {
+    }
 
-    class CreateFoo {}
+    class CreateFoo {
+    }
 
     @Controller('')
     class FooController {
@@ -570,7 +582,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[0].responses['201'] as ResponseObject).content[
           'application/xml'
-        ]
+          ]
       ).toEqual({
         schema: {
           $ref: '#/components/schemas/Foo'
@@ -608,7 +620,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[1].responses['200'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           type: 'array',
@@ -620,9 +632,11 @@ describe('SwaggerExplorer', () => {
     };
   });
   describe('when custom properties are passed', () => {
-    class Foo {}
+    class Foo {
+    }
 
-    class CreateFoo {}
+    class CreateFoo {
+    }
 
     @Controller('')
     class FooController {
@@ -739,7 +753,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[0].responses['201'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           type: 'object',
@@ -798,7 +812,7 @@ describe('SwaggerExplorer', () => {
       expect(
         (routes[1].responses['200'] as ResponseObject).content[
           'application/json'
-        ]
+          ]
       ).toEqual({
         schema: {
           type: 'array',
@@ -822,7 +836,8 @@ describe('SwaggerExplorer', () => {
       F = 'f'
     }
 
-    class Foo {}
+    class Foo {
+    }
 
     @Controller({ path: '', version: '3' })
     class FooController {
@@ -1012,7 +1027,8 @@ describe('SwaggerExplorer', () => {
   });
 
   describe('when headers are defined', () => {
-    class Foo {}
+    class Foo {
+    }
 
     @ApiHeader({
       name: 'Authorization',
@@ -1149,7 +1165,8 @@ describe('SwaggerExplorer', () => {
     });
   });
   describe('when a controller is excluded', () => {
-    class Foo {}
+    class Foo {
+    }
 
     @ApiExcludeController()
     @Controller('')
diff --git a/test/services/fixtures/create-user.dto.ts b/test/services/fixtures/create-user.dto.ts
index 8c9f5fa8..9de80dd1 100644
--- a/test/services/fixtures/create-user.dto.ts
+++ b/test/services/fixtures/create-user.dto.ts
@@ -85,6 +85,9 @@ export class CreateUserDto {
   @ApiProperty()
   createdAt: Date;
 
+  @ApiProperty({type: [String], format: 'uuid'})
+  formatArray: string[];
+
   static _OPENAPI_METADATA_FACTORY() {
     return {
       tags: { type: () => [String] }
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index f10ca163..71c3d9d2 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -174,6 +174,13 @@ describe('SchemaObjectFactory', () => {
               { $ref: '#/components/schemas/Dog' }
             ],
             discriminator: { propertyName: 'pet_type' }
+          },
+          formatArray: {
+            type: 'array',
+            items: {
+              type: 'string',
+              format: 'uuid'
+            }
           }
         },
         required: [
@@ -186,7 +193,8 @@ describe('SchemaObjectFactory', () => {
           'options',
           'allOf',
           'houses',
-          'createdAt'
+          'createdAt',
+          'formatArray'
         ]
       });
       expect(schemas['CreateProfileDto']).toEqual({