Repository: nestjs/swagger
Pull request #2176 (2022-11-21 14:59:27)

feat: support adding global parameters
## 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)
- [ ] 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. -->

Issue Number: #2158 


## What is the new behavior?

Support adding global parameters.

## 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/document-builder.ts b/lib/document-builder.ts
index 0be9a8c8..b6fb3cc3 100644
--- a/lib/document-builder.ts
+++ b/lib/document-builder.ts
@@ -4,11 +4,13 @@ import { buildDocumentBase } from './fixtures/document.base';
 import { OpenAPIObject } from './interfaces';
 import {
   ExternalDocumentationObject,
+  ParameterObject,
   SecurityRequirementObject,
   SecuritySchemeObject,
   ServerVariableObject,
   TagObject
 } from './interfaces/open-api-spec.interface';
+import { GlobalParametersStorage } from './storages/global-parameters.storage';
 
 export class DocumentBuilder {
   private readonly logger = new Logger(DocumentBuilder.name);
@@ -91,6 +93,11 @@ export class DocumentBuilder {
     return this;
   }
 
+  public addGlobalParameters(...parameters: ParameterObject[]): this {
+    GlobalParametersStorage.add(...parameters);
+    return this;
+  }
+
   public addSecurityRequirements(
     name: string | SecurityRequirementObject,
     requirements: string[] = []
diff --git a/lib/explorers/api-parameters.explorer.ts b/lib/explorers/api-parameters.explorer.ts
index 246db84a..b86ec527 100644
--- a/lib/explorers/api-parameters.explorer.ts
+++ b/lib/explorers/api-parameters.explorer.ts
@@ -10,6 +10,7 @@ import {
 import { ParametersMetadataMapper } from '../services/parameters-metadata-mapper';
 import { SchemaObjectFactory } from '../services/schema-object-factory';
 import { SwaggerTypesMapper } from '../services/swagger-types-mapper';
+import { GlobalParametersStorage } from '../storages/global-parameters.storage';
 
 const parameterMetadataAccessor = new ParameterMetadataAccessor();
 const modelPropertiesAccessor = new ModelPropertiesAccessor();
@@ -32,13 +33,15 @@ export const exploreApiParametersMetadata = (
     DECORATORS.API_PARAMETERS,
     method
   );
+  const globalParameters = GlobalParametersStorage.getAll();
   const parametersMetadata = parameterMetadataAccessor.explore(
     instance,
     prototype,
     method
   );
-  const noExplicitMetadata = isNil(explicitParameters);
-  if (noExplicitMetadata && isNil(parametersMetadata)) {
+  const noExplicitAndGlobalMetadata =
+    isNil(explicitParameters) && isNil(globalParameters);
+  if (noExplicitAndGlobalMetadata && isNil(parametersMetadata)) {
     return undefined;
   }
   const reflectedParametersAsProperties =
@@ -47,7 +50,7 @@ export const exploreApiParametersMetadata = (
     );
 
   let properties = reflectedParametersAsProperties;
-  if (!noExplicitMetadata) {
+  if (!noExplicitAndGlobalMetadata) {
     const mergeImplicitAndExplicit = (item: ParamWithTypeMetadata) =>
       assign(item, find(explicitParameters, ['name', item.name]));
 
@@ -56,9 +59,14 @@ export const exploreApiParametersMetadata = (
       explicitParameters
     );
     properties = map(properties, mergeImplicitAndExplicit);
-    properties = unionWith(properties, explicitParameters, (arrVal, othVal) => {
-      return arrVal.name === othVal.name && arrVal.in === othVal.in;
-    });
+    properties = unionWith(
+      properties,
+      explicitParameters,
+      globalParameters,
+      (arrVal, othVal) => {
+        return arrVal.name === othVal.name && arrVal.in === othVal.in;
+      }
+    );
   }
 
   const paramsWithDefinitions = schemaObjectFactory.createFromModel(
diff --git a/lib/storages/global-parameters.storage.ts b/lib/storages/global-parameters.storage.ts
new file mode 100644
index 00000000..135e5449
--- /dev/null
+++ b/lib/storages/global-parameters.storage.ts
@@ -0,0 +1,23 @@
+import { ParameterObject } from '../interfaces/open-api-spec.interface';
+
+export class GlobalParametersStorageHost {
+  private parameters = new Array<ParameterObject>();
+
+  add(...parameters: ParameterObject[]) {
+    this.parameters.push(...parameters);
+  }
+
+  getAll() {
+    return this.parameters;
+  }
+
+  clear() {
+    this.parameters = [];
+  }
+}
+
+const globalRef = global as any;
+export const GlobalParametersStorage: GlobalParametersStorageHost =
+  globalRef.SwaggerGlobalParametersStorage ||
+  (globalRef.SwaggerGlobalParametersStorage =
+    new GlobalParametersStorageHost());


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index ea765f81..008435c2 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -4,7 +4,15 @@
     "/api/v1": {
       "get": {
         "operationId": "AppController_getHello",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -15,7 +23,15 @@
     "/api/v2": {
       "get": {
         "operationId": "AppController_getHello",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -26,7 +42,15 @@
     "/api/v1/alias1": {
       "get": {
         "operationId": "AppController_withAliases",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -37,7 +61,15 @@
     "/api/v1/alias2": {
       "get": {
         "operationId": "AppController_withAliases",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -48,7 +80,15 @@
     "/api/v2/alias1": {
       "get": {
         "operationId": "AppController_withAliases",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -59,7 +99,15 @@
     "/api/v2/alias2": {
       "get": {
         "operationId": "AppController_withAliases",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -70,7 +118,15 @@
     "/api/v1/express:colon:another/{prop}": {
       "get": {
         "operationId": "AppController_withColonExpress",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -81,7 +137,15 @@
     "/api/v2/express:colon:another/{prop}": {
       "get": {
         "operationId": "AppController_withColonExpress",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -92,7 +156,15 @@
     "/api/v1/fastify:{colon}:{another}/{prop}": {
       "get": {
         "operationId": "AppController_withColonFastify",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -103,7 +175,15 @@
     "/api/v2/fastify:{colon}:{another}/{prop}": {
       "get": {
         "operationId": "AppController_withColonFastify",
-        "parameters": [],
+        "parameters": [
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "description": ""
@@ -128,6 +208,13 @@
               "default": "test",
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "requestBody": {
@@ -265,6 +352,13 @@
               "additionalProperties": true,
               "type": "object"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -313,6 +407,13 @@
             "schema": {
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -429,6 +530,13 @@
               "additionalProperties": true,
               "type": "object"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -466,6 +574,13 @@
               "default": "test",
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -501,6 +616,13 @@
               "default": "test",
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "requestBody": {
@@ -552,6 +674,13 @@
               "default": "test",
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "requestBody": {
@@ -609,6 +738,13 @@
               "default": "test",
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -659,6 +795,13 @@
                 "C"
               ]
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -706,6 +849,13 @@
               "maxLength": 100,
               "type": "string"
             }
+          },
+          {
+            "name": "x-tenant-id",
+            "in": "header",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts
index 380fc3bd..d15e1831 100644
--- a/e2e/express.e2e-spec.ts
+++ b/e2e/express.e2e-spec.ts
@@ -32,7 +32,12 @@ describe('Express Swagger', () => {
       .addApiKey({ type: 'apiKey' }, 'key2')
       .addCookieAuth()
       .addSecurityRequirements('bearer')
-      .addSecurityRequirements({ basic: [], cookie: [] });
+      .addSecurityRequirements({ basic: [], cookie: [] })
+      .addGlobalParameters({
+        name: 'x-tenant-id',
+        in: 'header',
+        schema: { type: 'string' }
+      });
   });
 
   it('should produce a valid OpenAPI 3.0 schema', async () => {
diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts
index 4c7380bc..dcde7139 100644
--- a/e2e/fastify.e2e-spec.ts
+++ b/e2e/fastify.e2e-spec.ts
@@ -32,7 +32,12 @@ describe('Fastify Swagger', () => {
       .addApiKey({ type: 'apiKey' }, 'key2')
       .addCookieAuth()
       .addSecurityRequirements('bearer')
-      .addSecurityRequirements({ basic: [], cookie: [] });
+      .addSecurityRequirements({ basic: [], cookie: [] })
+      .addGlobalParameters({
+        name: 'x-tenant-id',
+        in: 'header',
+        schema: { type: 'string' }
+      });
   });
 
   it('should produce a valid OpenAPI 3.0 schema', async () => {
diff --git a/e2e/validate-schema.e2e-spec.ts b/e2e/validate-schema.e2e-spec.ts
index 06e6dd30..5482cf69 100644
--- a/e2e/validate-schema.e2e-spec.ts
+++ b/e2e/validate-schema.e2e-spec.ts
@@ -40,6 +40,11 @@ describe('Validate OpenAPI schema', () => {
       .addCookieAuth()
       .addSecurityRequirements('bearer')
       .addSecurityRequirements({ basic: [], cookie: [] })
+      .addGlobalParameters({
+        name: 'x-tenant-id',
+        in: 'header',
+        schema: { type: 'string' }
+      })
       .build();
   });
 
diff --git a/test/explorer/swagger-explorer.spec.ts b/test/explorer/swagger-explorer.spec.ts
index 5e91b9e8..3d77bb1a 100644
--- a/test/explorer/swagger-explorer.spec.ts
+++ b/test/explorer/swagger-explorer.spec.ts
@@ -33,6 +33,7 @@ import { ModelPropertiesAccessor } from '../../lib/services/model-properties-acc
 import { SchemaObjectFactory } from '../../lib/services/schema-object-factory';
 import { SwaggerTypesMapper } from '../../lib/services/swagger-types-mapper';
 import { SwaggerExplorer } from '../../lib/swagger-explorer';
+import { GlobalParametersStorage } from '../../lib/storages/global-parameters.storage';
 
 describe('SwaggerExplorer', () => {
   const schemaObjectFactory = new SchemaObjectFactory(
@@ -1424,4 +1425,55 @@ describe('SwaggerExplorer', () => {
       });
     });
   });
+
+  describe('when global paramters are defined', () => {
+    class Foo {}
+
+    @Controller('')
+    class FooController {
+      @Get('foos')
+      find(): Promise<Foo[]> {
+        return Promise.resolve([]);
+      }
+    }
+
+    it('should properly define global paramters', () => {
+      GlobalParametersStorage.add(
+        {
+          name: 'x-tenant-id',
+          in: 'header',
+          schema: { type: 'string' }
+        },
+        {
+          name: 'x-tenant-id-2',
+          in: 'header',
+          schema: { type: 'string' }
+        }
+      );
+      const explorer = new SwaggerExplorer(schemaObjectFactory);
+      const routes = explorer.exploreController(
+        {
+          instance: new FooController(),
+          metatype: FooController
+        } as InstanceWrapper<FooController>,
+        new ApplicationConfig(),
+        'modulePath',
+        'globalPrefix'
+      );
+
+      expect(routes[0].root.parameters).toEqual([
+        {
+          name: 'x-tenant-id',
+          in: 'header',
+          schema: { type: 'string' }
+        },
+        {
+          name: 'x-tenant-id-2',
+          in: 'header',
+          schema: { type: 'string' }
+        }
+      ]);
+      GlobalParametersStorage.clear();
+    });
+  });
 });