Repository: nestjs/swagger
Pull request #2505 (2023-06-30 09:51:36)

Support changing the OpenAPI document based on the request
## 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. -->

It is impossible to patch the document after it is generated and before it is served.

Issue Number: #2502


## What is the new behavior?

A new option `patchDocument` can be passed to `SwaggerModule.setup`.
```typescript
SwaggerModule.setup("/:tenantId/api-docs", app, document, {
  patchDocument: (req: Request, res: Response, document1: OpenAPIObject) => ({
    ...document1,
    info: {
      ...document1.info,
      title: `${req.params.tenantId}'s API document`
    }
  })
})
```

## 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/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts
index 5853e5fa..584a96bb 100644
--- a/lib/interfaces/swagger-custom-options.interface.ts
+++ b/lib/interfaces/swagger-custom-options.interface.ts
@@ -18,4 +18,5 @@ export interface SwaggerCustomOptions {
   urls?: Record<'url' | 'name', string>[];
   jsonDocumentUrl?: string;
   yamlDocumentUrl?: string;
+  patchDocumentOnRequest?: <TRequest = any, TResponse = any> (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject;
 }
diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts
index f5138bed..a04c9bfd 100644
--- a/lib/swagger-module.ts
+++ b/lib/swagger-module.ts
@@ -102,6 +102,10 @@ export class SwaggerModule {
 
         if (!document) {
           document = lazyBuildDocument();
+
+          if (options.swaggerOptions.patchDocumentOnRequest) {
+            document = options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+          }
         }
 
         if (!swaggerInitJS) {
@@ -126,6 +130,14 @@ export class SwaggerModule {
 
           if (!document) {
             document = lazyBuildDocument();
+
+            if (options.swaggerOptions.patchDocumentOnRequest) {
+              document = options.swaggerOptions.patchDocumentOnRequest(
+                req,
+                res,
+                document
+              );
+            }
           }
 
           if (!swaggerInitJS) {
@@ -150,6 +162,10 @@ export class SwaggerModule {
 
       if (!document) {
         document = lazyBuildDocument();
+
+        if (options.swaggerOptions.patchDocumentOnRequest) {
+          document = options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+        }
       }
 
       if (!html) {
@@ -170,6 +186,10 @@ export class SwaggerModule {
 
         if (!document) {
           document = lazyBuildDocument();
+
+          if (options.swaggerOptions.patchDocumentOnRequest) {
+            document = options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+          }
         }
 
         if (!html) {
@@ -199,6 +219,10 @@ export class SwaggerModule {
 
       if (!document) {
         document = lazyBuildDocument();
+
+        if (options.swaggerOptions.patchDocumentOnRequest) {
+          document = options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+        }
       }
 
       if (!jsonDocument) {
@@ -213,6 +237,10 @@ export class SwaggerModule {
 
       if (!document) {
         document = lazyBuildDocument();
+
+        if (options.swaggerOptions.patchDocumentOnRequest) {
+          document = options.swaggerOptions.patchDocumentOnRequest(req, res, document);
+        }
       }
 
       if (!yamlDocument) {


Test patch:
diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts
index ca205e23..7194de81 100644
--- a/e2e/express.e2e-spec.ts
+++ b/e2e/express.e2e-spec.ts
@@ -113,7 +113,14 @@ describe('Express Swagger', () => {
       );
       SwaggerModule.setup('api', app, swaggerDocument, {
         jsonDocumentUrl: JSON_CUSTOM_URL,
-        yamlDocumentUrl: YAML_CUSTOM_URL
+        yamlDocumentUrl: YAML_CUSTOM_URL,
+        patchDocumentOnRequest: (req, res, document) => ({
+          ...document,
+          info: {
+            ...document.info,
+            description: (req as Record<string, any>).query.description
+          }
+        })
       });
 
       await app.init();
@@ -130,12 +137,27 @@ describe('Express Swagger', () => {
       expect(Object.keys(response.body).length).toBeGreaterThan(0);
     });
 
+    it('patched JSON document should be served', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${JSON_CUSTOM_URL}?description=My%20custom%20description`
+      );
+
+      expect(response.body.info.description).toBe("My custom description");
+    });
+
     it('yaml document should be server in the custom url', async () => {
       const response = await request(app.getHttpServer()).get(YAML_CUSTOM_URL);
 
       expect(response.status).toEqual(200);
       expect(response.text.length).toBeGreaterThan(0);
     });
+
+    it('patched YAML document should be served', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${YAML_CUSTOM_URL}?description=My%20custom%20description`
+      );
+      expect(response.text).toContain("My custom description");
+    });
   });
 
   describe('custom documents endpoints with global prefix', () => {
@@ -209,7 +231,15 @@ describe('Express Swagger', () => {
         customJsStr: CUSTOM_JS_STR,
         customfavIcon: CUSTOM_FAVICON,
         customSiteTitle: CUSTOM_SITE_TITLE,
-        customCssUrl: CUSTOM_CSS_URL
+        customCssUrl: CUSTOM_CSS_URL,
+        patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
+          return {
+            ...document,
+            info: {
+              description: req.query.description
+            }
+          }
+        }
       });
 
       await app.init();
@@ -249,6 +279,46 @@ describe('Express Swagger', () => {
       );
     });
 
+    it('should patch the OpenAPI document', async () => {
+      const response: Response = await request(app.getHttpServer()).get(
+        '/swagger-ui-init.js?description=Custom%20Swagger%20description%20passed%20by%20query%20param'
+      );
+      expect(response.text).toContain(
+        `"description": "Custom Swagger description passed by query param"`
+      );
+    });
+
+    it('should patch the OpenAPI document based on path param of the swagger prefix', async () => {
+      const app = await NestFactory.create<NestExpressApplication>(
+        ApplicationModule,
+        new ExpressAdapter(),
+        { logger: false }
+      );
+
+      const swaggerDocument = SwaggerModule.createDocument(
+        app,
+        builder.build()
+      );
+
+      SwaggerModule.setup('/:customer/', app, swaggerDocument, {
+        patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
+          return {
+            ...document,
+            info: {
+              description: `${req.params.customer}'s API documentation`
+            }
+          }
+        }
+      });
+
+      await app.init();
+
+      const response: Response = await request(app.getHttpServer()).get('/customer-1/swagger-ui-init.js');
+
+      await app.close();
+      expect(response.text).toContain("customer-1's API documentation");
+    })
+
     afterEach(async () => {
       await app.close();
     });
diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts
index b40c8eca..432d3eb0 100644
--- a/e2e/fastify.e2e-spec.ts
+++ b/e2e/fastify.e2e-spec.ts
@@ -116,7 +116,14 @@ describe('Fastify Swagger', () => {
       );
       SwaggerModule.setup('api', app, swaggerDocument, {
         jsonDocumentUrl: JSON_CUSTOM_URL,
-        yamlDocumentUrl: YAML_CUSTOM_URL
+        yamlDocumentUrl: YAML_CUSTOM_URL,
+        patchDocumentOnRequest: (req, res, document) => ({
+          ...document,
+          info: {
+            ...document.info,
+            description: (req as Record<string, any>).query.description
+          }
+        })
       });
 
       await app.init();
@@ -134,12 +141,27 @@ describe('Fastify Swagger', () => {
       expect(Object.keys(response.body).length).toBeGreaterThan(0);
     });
 
+    it('patched JSON document should be served', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${JSON_CUSTOM_URL}?description=My%20custom%20description`
+      );
+
+      expect(response.body.info.description).toBe("My custom description");
+    });
+
     it('yaml document should be server in the custom url', async () => {
       const response = await request(app.getHttpServer()).get(YAML_CUSTOM_URL);
 
       expect(response.status).toEqual(200);
       expect(response.text.length).toBeGreaterThan(0);
     });
+
+    it('patched YAML document should be served', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${YAML_CUSTOM_URL}?description=My%20custom%20description`
+      );
+      expect(response.text).toContain("My custom description");
+    });
   });
 
   describe('custom documents endpoints with global prefix', () => {
@@ -214,7 +236,14 @@ describe('Fastify Swagger', () => {
         customJsStr: CUSTOM_JS_STR,
         customfavIcon: CUSTOM_FAVICON,
         customSiteTitle: CUSTOM_SITE_TITLE,
-        customCssUrl: CUSTOM_CSS_URL
+        customCssUrl: CUSTOM_CSS_URL,
+        patchDocumentOnRequest: (req, res, document) => ({
+          ...document,
+          info: {
+            ...document.info,
+            description: (req as Record<string, any>).query.description
+          }
+        })
       });
 
       await app.init();
@@ -267,6 +296,47 @@ describe('Fastify Swagger', () => {
       );
     });
 
+    it('should patch the OpenAPI document', async function () {
+      const response: Response = await request(app.getHttpServer()).get(
+        "/custom/swagger-ui-init.js?description=Custom%20Swagger%20description%20passed%20by%20query%20param"
+      );
+      expect(response.text).toContain(
+        `"description": "Custom Swagger description passed by query param"`
+      );
+    });
+
+    it('should patch the OpenAPI document based on path param of the swagger prefix', async () => {
+      const app = await NestFactory.create<NestFastifyApplication>(
+        ApplicationModule,
+        new FastifyAdapter(),
+        { logger: false }
+      );
+
+      const swaggerDocument = SwaggerModule.createDocument(
+        app,
+        builder.build()
+      );
+
+      SwaggerModule.setup('/:tenantId/', app, swaggerDocument, {
+        patchDocumentOnRequest<ExpressRequest, ExpressResponse> (req, res, document) {
+          return {
+            ...document,
+            info: {
+              description: `${req.params.tenantId}'s API documentation`
+            }
+          }
+        }
+      });
+
+      await app.init();
+      await app.getHttpAdapter().getInstance().ready();
+
+      const response: Response = await request(app.getHttpServer()).get('/tenant-1/swagger-ui-init.js');
+
+      await app.close();
+      expect(response.text).toContain("tenant-1's API documentation");
+    })
+
     afterEach(async () => {
       await app.close();
     });
diff --git a/e2e/manual-e2e.ts b/e2e/manual-e2e.ts
index 32ff1dd2..367df6cf 100644
--- a/e2e/manual-e2e.ts
+++ b/e2e/manual-e2e.ts
@@ -123,6 +123,16 @@ async function bootstrap() {
     }
   });
 
+  SwaggerModule.setup("/:tenantId/api-docs", app, document, {
+    patchDocumentOnRequest: (req, res, document1) => ({
+      ...document1,
+      info: {
+        ...document1.info,
+        title: `${(req as Record<string, any>).params.tenantId}'s API document`
+      }
+    })
+  })
+
   USE_FASTIFY
     ? (app as NestFastifyApplication).useStaticAssets({
         root: publicFolderPath,