Repository: nestjs/swagger
Pull request #1113 (2020-12-28 15:46:21)

fix: Arrays and objects are not managed properly when located in first level of @Query()
## 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. -->

See https://github.com/nestjs/swagger/issues/1112

## What is the new behavior?

Arrays and objects are now correctly interpreted as such in @Query()

![image](https://user-images.githubusercontent.com/28689103/103226326-27336380-492c-11eb-8c81-d177843eec57.png)

## 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/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index 011c0a69..00197656 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -62,36 +62,40 @@ export class SchemaObjectFactory {
         return this.createQueryOrParamSchema(param, schemas, schemaRefsStack);
       }
 
-      const modelName = this.exploreModelSchema(
-        param.type,
-        schemas,
-        schemaRefsStack
-      );
-      const name = param.name || modelName;
-      const schema = {
-        ...((param as BaseParameterObject).schema || {}),
-        $ref: getSchemaPath(modelName)
-      };
-      const isArray = param.isArray;
-      param = omit(param, 'isArray');
-
-      if (isArray) {
-        return {
-          ...param,
-          name,
-          schema: {
-            type: 'array',
-            items: schema
-          }
-        };
-      }
+      return this.getCustomType(param, schemas, schemaRefsStack);
+    });
+    return flatten(parameterObjects);
+  }
+
+  getCustomType(param, schemas, schemaRefsStack) {
+    const modelName = this.exploreModelSchema(
+      param.type,
+      schemas,
+      schemaRefsStack
+    );
+    const name = param.name || modelName;
+    const schema = {
+      ...((param as BaseParameterObject).schema || {}),
+      $ref: getSchemaPath(modelName)
+    };
+    const isArray = param.isArray;
+    param = omit(param, 'isArray');
+
+    if (isArray) {
       return {
         ...param,
         name,
-        schema
+        schema: {
+          type: 'array',
+          items: schema
+        }
       };
-    });
-    return flatten(parameterObjects);
+    }
+    return {
+      ...param,
+      name,
+      schema
+    };
   }
 
   createQueryOrParamSchema(
@@ -111,24 +115,7 @@ export class SchemaObjectFactory {
       };
     }
     if (isFunction(param.type)) {
-      const propertiesWithType = this.extractPropertiesFromType(
-        param.type,
-        schemas,
-        schemaRefsStack
-      );
-      if (!propertiesWithType) {
-        return param;
-      }
-      return propertiesWithType.map(
-        (property: ParameterObject & ParamWithTypeMetadata) => {
-          const parameterObject = {
-            ...(omit(property, 'enumName') as ParameterObject),
-            in: 'query',
-            required: property.required ?? true
-          };
-          return parameterObject;
-        }
-      ) as ParameterObject[];
+      return this.getCustomType(param, schemas, schemaRefsStack);
     }
     return param;
   }


Test patch:
diff --git a/test/explorer/swagger-explorer.spec.ts b/test/explorer/swagger-explorer.spec.ts
index 2fd9b6b6..5d9b5614 100644
--- a/test/explorer/swagger-explorer.spec.ts
+++ b/test/explorer/swagger-explorer.spec.ts
@@ -971,4 +971,116 @@ describe('SwaggerExplorer', () => {
       ]);
     });
   });
+
+  describe('when arrays are used', () => {
+    enum LettersEnum {
+      A = 'A',
+      B = 'B',
+      C = 'C'
+    }
+
+    class NestedDto {
+      @ApiProperty()
+      nestedString: string;
+    }
+
+    class ObjectDto {
+      @ApiProperty()
+      field: string;
+
+      @ApiProperty()
+      nestedObject: NestedDto;
+
+      @ApiProperty({
+        isArray: true,
+        type: NestedDto
+      })
+      nestedArrayOfObjects: NestedDto[];
+
+      @ApiProperty({
+        type: [NestedDto]
+      })
+      nestedArrayOfObjects2: NestedDto[];
+    }
+
+    class FooDto {
+      @ApiProperty({
+        isArray: true,
+        type: ObjectDto
+      })
+      arrayOfObjectsDto: ObjectDto[];
+    }
+
+    class FooController {
+      @Get('/route1')
+      route1(@Query() fooDto: FooDto) {}
+      @Get('/route2')
+      route2(@Query() objectDto: ObjectDto) {}
+    }
+
+    it('should properly define arrays in query', () => {
+      const explorer = new SwaggerExplorer(schemaObjectFactory);
+      const routes = explorer.exploreController(
+        {
+          instance: new FooController(),
+          metatype: FooController
+        } as InstanceWrapper<FooController>,
+        'path'
+      );
+
+      expect(routes[0].root.parameters).toEqual([
+        {
+          name: 'arrayOfObjectsDto',
+          required: true,
+          in: 'query',
+          schema: {
+            items: {
+              $ref: '#/components/schemas/ObjectDto'
+            },
+            type: 'array'
+          }
+        }
+      ]);
+      expect(routes[1].root.parameters).toEqual([
+        {
+          name: 'field',
+          required: true,
+          in: 'query',
+          schema: {
+            type: 'string'
+          }
+        },
+        {
+          name: 'nestedObject',
+          required: true,
+          in: 'query',
+          schema: {
+            $ref: '#/components/schemas/NestedDto'
+          }
+        },
+        {
+          name: 'nestedArrayOfObjects',
+          required: true,
+          in: 'query',
+          schema: {
+            type: 'array',
+            items: {
+              $ref: '#/components/schemas/NestedDto'
+            }
+          }
+        },
+        {
+          name: 'nestedArrayOfObjects2',
+          required: true,
+          in: 'query',
+          schema: {
+            type: 'array',
+            items: {
+              $ref: '#/components/schemas/NestedDto'
+            }
+          }
+        }
+      ]);
+    });
+  });
 });
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index d580fce1..79d29af5 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -1,5 +1,7 @@
 import { ApiProperty } from '../../lib/decorators';
+import { SchemaObject } from '../../lib/interfaces/open-api-spec.interface';
 import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor';
+import { ParamWithTypeMetadata } from '../../lib/services/parameter-metadata-accessor';
 import { SchemaObjectFactory } from '../../lib/services/schema-object-factory';
 import { SwaggerTypesMapper } from '../../lib/services/swagger-types-mapper';
 import { CreateUserDto } from './fixtures/create-user.dto';
@@ -234,5 +236,83 @@ describe('SchemaObjectFactory', () => {
         properties: { name: { type: 'string', minLength: 1 } }
       });
     });
+
+    it('should create arrays of objects', () => {
+      class ObjectDto {
+        @ApiProperty()
+        field: string;
+      }
+
+      class TestDto {
+        @ApiProperty()
+        arrayOfStrings: string[];
+      }
+
+      class Test2Dto {
+        @ApiProperty({
+          isArray: true,
+          type: ObjectDto
+        })
+        arrayOfObjects: ObjectDto[];
+      }
+
+      const schemas = [];
+      schemaObjectFactory.exploreModelSchema(TestDto, schemas);
+      schemaObjectFactory.exploreModelSchema(Test2Dto, schemas);
+
+      expect(schemas[0][TestDto.name]).toEqual({
+        type: 'object',
+        properties: {
+          arrayOfStrings: {
+            type: 'array',
+            items: {
+              type: 'string'
+            }
+          }
+        },
+        required: ['arrayOfStrings']
+      });
+      expect(schemas[2][Test2Dto.name]).toEqual({
+        type: 'object',
+        properties: {
+          arrayOfObjects: {
+            type: 'array',
+            items: {
+              $ref: '#/components/schemas/ObjectDto'
+            }
+          }
+        },
+        required: ['arrayOfObjects']
+      });
+    });
+  });
+
+  describe('createFromModel', () => {
+    it('should create arrays of objects', () => {
+      class ObjectDto {
+        @ApiProperty()
+        field: string;
+      }
+
+      class TestDto {
+        @ApiProperty()
+        arrayOfStrings: string[];
+      }
+
+      class Test2Dto {
+        @ApiProperty({})
+        arrayOfObjects: ObjectDto[];
+      }
+
+      const property = ApiProperty({
+        isArray: true,
+        type: ObjectDto
+      });
+
+      const parameters: ParamWithTypeMetadata[] = [];
+
+      const schemas: SchemaObject[] = [];
+      const result = schemaObjectFactory.createFromModel(parameters, schemas);
+    });
   });
 });