From f49f5b2f850c45273a6db929edafe10dffc288a7 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 11:31:21 +0200 Subject: [PATCH 1/6] feat(@nestjs/swagger): allow to patch document using a custom function Define `patchDocument` option to patch the OpenAPI document based on the request, response and generated OpenAPI document. #2502 --- e2e/express.e2e-spec.ts | 71 ++++++++++++++++++- e2e/fastify.e2e-spec.ts | 50 ++++++++++++- e2e/manual-e2e.ts | 10 +++ .../swagger-custom-options.interface.ts | 1 + lib/swagger-module.ts | 20 ++++++ 5 files changed, 149 insertions(+), 3 deletions(-) diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts index ca205e23d..25d489f75 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, + patchDocument: (req, res, document) => ({ + ...document, + info: { + ...document.info, + description: (req as Record).query.description + } + }) }); await app.init(); @@ -130,12 +137,24 @@ 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 +228,15 @@ describe('Express Swagger', () => { customJsStr: CUSTOM_JS_STR, customfavIcon: CUSTOM_FAVICON, customSiteTitle: CUSTOM_SITE_TITLE, - customCssUrl: CUSTOM_CSS_URL + customCssUrl: CUSTOM_CSS_URL, + patchDocument (req, res, document) { + return { + ...document, + info: { + description: req.query.description + } + } + } }); await app.init(); @@ -249,6 +276,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( + ApplicationModule, + new ExpressAdapter(), + { logger: false } + ); + + app.setGlobalPrefix("/:customer/") + + const swaggerDocument = SwaggerModule.createDocument( + app, + builder.build() + ); + + SwaggerModule.setup('/:customer/', app, swaggerDocument, { + patchDocument (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 b40c8eca5..1e90f4a0f 100644 --- a/e2e/fastify.e2e-spec.ts +++ b/e2e/fastify.e2e-spec.ts @@ -214,7 +214,14 @@ describe('Fastify Swagger', () => { customJsStr: CUSTOM_JS_STR, customfavIcon: CUSTOM_FAVICON, customSiteTitle: CUSTOM_SITE_TITLE, - customCssUrl: CUSTOM_CSS_URL + customCssUrl: CUSTOM_CSS_URL, + patchDocument: (req, res, document) => ({ + ...document, + info: { + ...document.info, + description: (req as Record).query.description + } + }) }); await app.init(); @@ -267,6 +274,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( + ApplicationModule, + new FastifyAdapter(), + { logger: false } + ); + + const swaggerDocument = SwaggerModule.createDocument( + app, + builder.build() + ); + + SwaggerModule.setup('/:tenantId/', app, swaggerDocument, { + patchDocument (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 32ff1dd20..f5d43b75d 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, { + patchDocument: (req, res, document1) => ({ + ...document1, + info: { + ...document1.info, + title: `${(req as Record).params.tenantId}'s API document` + } + }) + }) + USE_FASTIFY ? (app as NestFastifyApplication).useStaticAssets({ root: publicFolderPath, diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts index 5853e5fa1..a35d805a5 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; + patchDocument?: (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject; } diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts index f5138bed3..5f6377039 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.patchDocument) { + document = options.swaggerOptions.patchDocument(req, res, document) + } } if (!swaggerInitJS) { @@ -126,6 +130,10 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); + + if (options.swaggerOptions.patchDocument) { + document = options.swaggerOptions.patchDocument(req, res, document); + } } if (!swaggerInitJS) { @@ -150,6 +158,10 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); + + if (options.swaggerOptions.patchDocument) { + document = options.swaggerOptions.patchDocument(req, res, document) + } } if (!html) { @@ -199,6 +211,10 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); + + if (options.swaggerOptions.patchDocument) { + document = options.swaggerOptions.patchDocument(req, res, document) + } } if (!jsonDocument) { @@ -213,6 +229,10 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); + + if (options.swaggerOptions.patchDocument) { + document = options.swaggerOptions.patchDocument(req, res, document) + } } if (!yamlDocument) { From 3e297ada464e9e5ba44ff9467939dd89c38cfdbb Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 11:37:00 +0200 Subject: [PATCH 2/6] style(@nestjs/swagger): lint --- e2e/express.e2e-spec.ts | 33 ++++++++++++++++++--------------- e2e/fastify.e2e-spec.ts | 12 ++++++------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts index 25d489f75..ec43a8488 100644 --- a/e2e/express.e2e-spec.ts +++ b/e2e/express.e2e-spec.ts @@ -138,10 +138,12 @@ describe('Express Swagger', () => { }); it('patched JSON document should be served', async () => { - const response = await request(app.getHttpServer()).get(`${JSON_CUSTOM_URL}?description=My%20custom%20description`); + const response = await request(app.getHttpServer()).get( + `${JSON_CUSTOM_URL}?description=My%20custom%20description` + ); - expect(response.body.info.description).toBe("My custom description") - }) + 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); @@ -151,10 +153,11 @@ describe('Express Swagger', () => { }); 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") - }) + 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', () => { @@ -277,11 +280,13 @@ 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') + 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( @@ -290,8 +295,6 @@ describe('Express Swagger', () => { { logger: false } ); - app.setGlobalPrefix("/:customer/") - const swaggerDocument = SwaggerModule.createDocument( app, builder.build() @@ -310,10 +313,10 @@ describe('Express Swagger', () => { await app.init(); - const response: Response = await request(app.getHttpServer()).get('/customer-1/swagger-ui-init.js') + 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") + await app.close(); + expect(response.text).toContain("customer-1's API documentation"); }) afterEach(async () => { diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts index 1e90f4a0f..0482efc39 100644 --- a/e2e/fastify.e2e-spec.ts +++ b/e2e/fastify.e2e-spec.ts @@ -277,11 +277,11 @@ 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( @@ -309,10 +309,10 @@ describe('Fastify Swagger', () => { await app.init(); await app.getHttpAdapter().getInstance().ready(); - const response: Response = await request(app.getHttpServer()).get('/tenant-1/swagger-ui-init.js') + 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") + await app.close(); + expect(response.text).toContain("tenant-1's API documentation"); }) afterEach(async () => { From bc70d1348cd3f261d75e39bd36d52c44c58df8fa Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 11:38:45 +0200 Subject: [PATCH 3/6] test(@nestjs/swagger): test patched document for yaml and json route using fastify --- e2e/fastify.e2e-spec.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts index 0482efc39..4479b8eb5 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, + patchDocument: (req, res, document) => ({ + ...document, + info: { + ...document.info, + description: (req as Record).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', () => { From 9117bf66edb5309daae091157169e162ceb4084b Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 11:46:54 +0200 Subject: [PATCH 4/6] fix(@nestjs/swagger): fix document patching for fastify with ignoreTrailingSlash case --- lib/swagger-module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts index 5f6377039..23e135be8 100644 --- a/lib/swagger-module.ts +++ b/lib/swagger-module.ts @@ -182,6 +182,10 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); + + if (options.swaggerOptions.patchDocument) { + document = options.swaggerOptions.patchDocument(req, res, document); + } } if (!html) { From ab23696abdc7c155fbfc2172dbd9267932d97968 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 11:47:38 +0200 Subject: [PATCH 5/6] style(@nestjs/swagger): lint --- lib/swagger-module.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts index 23e135be8..d8be26859 100644 --- a/lib/swagger-module.ts +++ b/lib/swagger-module.ts @@ -104,7 +104,7 @@ export class SwaggerModule { document = lazyBuildDocument(); if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document) + document = options.swaggerOptions.patchDocument(req, res, document); } } @@ -132,7 +132,11 @@ export class SwaggerModule { document = lazyBuildDocument(); if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document); + document = options.swaggerOptions.patchDocument( + req, + res, + document + ); } } @@ -160,7 +164,7 @@ export class SwaggerModule { document = lazyBuildDocument(); if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document) + document = options.swaggerOptions.patchDocument(req, res, document); } } @@ -217,7 +221,7 @@ export class SwaggerModule { document = lazyBuildDocument(); if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document) + document = options.swaggerOptions.patchDocument(req, res, document); } } @@ -235,7 +239,7 @@ export class SwaggerModule { document = lazyBuildDocument(); if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document) + document = options.swaggerOptions.patchDocument(req, res, document); } } From 83c3e8d57de865945c4d6d7dc2eeb4bbc257ece8 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 13:48:25 +0200 Subject: [PATCH 6/6] refactor(@nestjs/swagger): rename the option to `patchDocumentOnRequest` Rename the option as pointed out here. https://github.com/nestjs/swagger/pull/2505#issuecomment-1614506935 --- e2e/express.e2e-spec.ts | 6 ++--- e2e/fastify.e2e-spec.ts | 6 ++--- e2e/manual-e2e.ts | 2 +- .../swagger-custom-options.interface.ts | 2 +- lib/swagger-module.ts | 24 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts index ec43a8488..7194de819 100644 --- a/e2e/express.e2e-spec.ts +++ b/e2e/express.e2e-spec.ts @@ -114,7 +114,7 @@ describe('Express Swagger', () => { SwaggerModule.setup('api', app, swaggerDocument, { jsonDocumentUrl: JSON_CUSTOM_URL, yamlDocumentUrl: YAML_CUSTOM_URL, - patchDocument: (req, res, document) => ({ + patchDocumentOnRequest: (req, res, document) => ({ ...document, info: { ...document.info, @@ -232,7 +232,7 @@ describe('Express Swagger', () => { customfavIcon: CUSTOM_FAVICON, customSiteTitle: CUSTOM_SITE_TITLE, customCssUrl: CUSTOM_CSS_URL, - patchDocument (req, res, document) { + patchDocumentOnRequest (req, res, document) { return { ...document, info: { @@ -301,7 +301,7 @@ describe('Express Swagger', () => { ); SwaggerModule.setup('/:customer/', app, swaggerDocument, { - patchDocument (req, res, document) { + patchDocumentOnRequest (req, res, document) { return { ...document, info: { diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts index 4479b8eb5..432d3eb05 100644 --- a/e2e/fastify.e2e-spec.ts +++ b/e2e/fastify.e2e-spec.ts @@ -117,7 +117,7 @@ describe('Fastify Swagger', () => { SwaggerModule.setup('api', app, swaggerDocument, { jsonDocumentUrl: JSON_CUSTOM_URL, yamlDocumentUrl: YAML_CUSTOM_URL, - patchDocument: (req, res, document) => ({ + patchDocumentOnRequest: (req, res, document) => ({ ...document, info: { ...document.info, @@ -237,7 +237,7 @@ describe('Fastify Swagger', () => { customfavIcon: CUSTOM_FAVICON, customSiteTitle: CUSTOM_SITE_TITLE, customCssUrl: CUSTOM_CSS_URL, - patchDocument: (req, res, document) => ({ + patchDocumentOnRequest: (req, res, document) => ({ ...document, info: { ...document.info, @@ -318,7 +318,7 @@ describe('Fastify Swagger', () => { ); SwaggerModule.setup('/:tenantId/', app, swaggerDocument, { - patchDocument (req, res, document) { + patchDocumentOnRequest (req, res, document) { return { ...document, info: { diff --git a/e2e/manual-e2e.ts b/e2e/manual-e2e.ts index f5d43b75d..367df6cfa 100644 --- a/e2e/manual-e2e.ts +++ b/e2e/manual-e2e.ts @@ -124,7 +124,7 @@ async function bootstrap() { }); SwaggerModule.setup("/:tenantId/api-docs", app, document, { - patchDocument: (req, res, document1) => ({ + patchDocumentOnRequest: (req, res, document1) => ({ ...document1, info: { ...document1.info, diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts index a35d805a5..584a96bb1 100644 --- a/lib/interfaces/swagger-custom-options.interface.ts +++ b/lib/interfaces/swagger-custom-options.interface.ts @@ -18,5 +18,5 @@ export interface SwaggerCustomOptions { urls?: Record<'url' | 'name', string>[]; jsonDocumentUrl?: string; yamlDocumentUrl?: string; - patchDocument?: (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject; + patchDocumentOnRequest?: (req: TRequest, res: TResponse, document: OpenAPIObject) => OpenAPIObject; } diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts index d8be26859..a04c9bfdc 100644 --- a/lib/swagger-module.ts +++ b/lib/swagger-module.ts @@ -103,8 +103,8 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); - if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document); + if (options.swaggerOptions.patchDocumentOnRequest) { + document = options.swaggerOptions.patchDocumentOnRequest(req, res, document); } } @@ -131,8 +131,8 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); - if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument( + if (options.swaggerOptions.patchDocumentOnRequest) { + document = options.swaggerOptions.patchDocumentOnRequest( req, res, document @@ -163,8 +163,8 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); - if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document); + if (options.swaggerOptions.patchDocumentOnRequest) { + document = options.swaggerOptions.patchDocumentOnRequest(req, res, document); } } @@ -187,8 +187,8 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); - if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document); + if (options.swaggerOptions.patchDocumentOnRequest) { + document = options.swaggerOptions.patchDocumentOnRequest(req, res, document); } } @@ -220,8 +220,8 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); - if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document); + if (options.swaggerOptions.patchDocumentOnRequest) { + document = options.swaggerOptions.patchDocumentOnRequest(req, res, document); } } @@ -238,8 +238,8 @@ export class SwaggerModule { if (!document) { document = lazyBuildDocument(); - if (options.swaggerOptions.patchDocument) { - document = options.swaggerOptions.patchDocument(req, res, document); + if (options.swaggerOptions.patchDocumentOnRequest) { + document = options.swaggerOptions.patchDocumentOnRequest(req, res, document); } }