From 197763f3fc24a91a4353f9f4a67b71b5f19c8bc2 Mon Sep 17 00:00:00 2001 From: Evan Trimboli Date: Tue, 6 Apr 2021 19:51:19 +1000 Subject: [PATCH] feat: Add exclude as a complement to exclude. --- e2e/src/bar/bar.controller.ts | 9 + e2e/src/bar/bar.module.ts | 7 + e2e/src/baz/baz.controller.ts | 9 + e2e/src/baz/baz.module.ts | 7 + e2e/src/foo/foo.controller.ts | 9 + e2e/src/foo/foo.module.ts | 7 + e2e/src/include-exclude.module.ts | 9 + e2e/validate-schema.e2e-spec.ts | 155 +++++++++++++----- .../swagger-document-options.interface.ts | 7 +- lib/swagger-scanner.ts | 26 ++- 10 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 e2e/src/bar/bar.controller.ts create mode 100644 e2e/src/bar/bar.module.ts create mode 100644 e2e/src/baz/baz.controller.ts create mode 100644 e2e/src/baz/baz.module.ts create mode 100644 e2e/src/foo/foo.controller.ts create mode 100644 e2e/src/foo/foo.module.ts create mode 100644 e2e/src/include-exclude.module.ts diff --git a/e2e/src/bar/bar.controller.ts b/e2e/src/bar/bar.controller.ts new file mode 100644 index 000000000..bfba2df8d --- /dev/null +++ b/e2e/src/bar/bar.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('bar') +export class BarController { + @Get() + getBar() { + return 'bar'; + } +} diff --git a/e2e/src/bar/bar.module.ts b/e2e/src/bar/bar.module.ts new file mode 100644 index 000000000..994b960de --- /dev/null +++ b/e2e/src/bar/bar.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { BarController } from './bar.controller'; + +@Module({ + controllers: [BarController] +}) +export class BarModule {} diff --git a/e2e/src/baz/baz.controller.ts b/e2e/src/baz/baz.controller.ts new file mode 100644 index 000000000..90d3dc6a5 --- /dev/null +++ b/e2e/src/baz/baz.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('baz') +export class BazController { + @Get() + getBaz() { + return 'baz'; + } +} diff --git a/e2e/src/baz/baz.module.ts b/e2e/src/baz/baz.module.ts new file mode 100644 index 000000000..ffcdbe3ec --- /dev/null +++ b/e2e/src/baz/baz.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { BazController } from './baz.controller'; + +@Module({ + controllers: [BazController] +}) +export class BazModule {} diff --git a/e2e/src/foo/foo.controller.ts b/e2e/src/foo/foo.controller.ts new file mode 100644 index 000000000..03a226451 --- /dev/null +++ b/e2e/src/foo/foo.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('foo') +export class FooController { + @Get() + getFoo() { + return 'foo'; + } +} diff --git a/e2e/src/foo/foo.module.ts b/e2e/src/foo/foo.module.ts new file mode 100644 index 000000000..c6b3b0aa8 --- /dev/null +++ b/e2e/src/foo/foo.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { FooController } from './foo.controller'; + +@Module({ + controllers: [FooController] +}) +export class FooModule {} diff --git a/e2e/src/include-exclude.module.ts b/e2e/src/include-exclude.module.ts new file mode 100644 index 000000000..fe8db0299 --- /dev/null +++ b/e2e/src/include-exclude.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { BarModule } from './bar/bar.module'; +import { BazModule } from './baz/baz.module'; +import { FooModule } from './foo/foo.module'; + +@Module({ + imports: [FooModule, BarModule, BazModule] +}) +export class IncludeExcludeModule {} diff --git a/e2e/validate-schema.e2e-spec.ts b/e2e/validate-schema.e2e-spec.ts index 836c5daf3..239764d26 100644 --- a/e2e/validate-schema.e2e-spec.ts +++ b/e2e/validate-schema.e2e-spec.ts @@ -2,56 +2,123 @@ import { NestFactory } from '@nestjs/core'; import { writeFileSync } from 'fs'; import { join } from 'path'; import * as SwaggerParser from 'swagger-parser'; -import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '../lib'; +import { + DocumentBuilder, + OpenAPIObject, + SwaggerDocumentOptions, + SwaggerModule +} from '../lib'; import { ApplicationModule } from './src/app.module'; +import { BarModule } from './src/bar/bar.module'; +import { FooModule } from './src/foo/foo.module'; +import { IncludeExcludeModule } from './src/include-exclude.module'; describe('Validate OpenAPI schema', () => { - let document: OpenAPIObject; + describe('general schema', () => { + let document: OpenAPIObject; + beforeEach(async () => { + const app = await NestFactory.create(ApplicationModule, { + logger: false + }); + app.setGlobalPrefix('api/'); - beforeEach(async () => { - const app = await NestFactory.create(ApplicationModule, { - logger: false + const options = new DocumentBuilder() + .setTitle('Cats example') + .setDescription('The cats API description') + .setVersion('1.0') + .setBasePath('api') + .addTag('cats') + .addBasicAuth() + .addBearerAuth() + .addOAuth2() + .addApiKey() + .addApiKey({ type: 'apiKey' }, 'key1') + .addApiKey({ type: 'apiKey' }, 'key2') + .addCookieAuth() + .addSecurityRequirements('bearer') + .addSecurityRequirements({ basic: [], cookie: [] }) + .build(); + + document = SwaggerModule.createDocument(app, options); + }); + + it('should produce a valid OpenAPI 3.0 schema', async () => { + const doc = JSON.stringify(document, null, 2); + writeFileSync(join(__dirname, 'api-spec.json'), doc); + + try { + let api = await SwaggerParser.validate(document as any); + console.log( + 'API name: %s, Version: %s', + api.info.title, + api.info.version + ); + expect(api.info.title).toEqual('Cats example'); + expect( + api.paths['/api/cats']['get']['x-codeSamples'][0]['lang'] + ).toEqual('JavaScript'); + } catch (err) { + console.log(doc); + expect(err).toBeUndefined(); + } }); - app.setGlobalPrefix('api/'); - - const options = new DocumentBuilder() - .setTitle('Cats example') - .setDescription('The cats API description') - .setVersion('1.0') - .setBasePath('api') - .addTag('cats') - .addBasicAuth() - .addBearerAuth() - .addOAuth2() - .addApiKey() - .addApiKey({ type: 'apiKey' }, 'key1') - .addApiKey({ type: 'apiKey' }, 'key2') - .addCookieAuth() - .addSecurityRequirements('bearer') - .addSecurityRequirements({ basic: [], cookie: [] }) - .build(); - - document = SwaggerModule.createDocument(app, options); }); - it('should produce a valid OpenAPI 3.0 schema', async () => { - const doc = JSON.stringify(document, null, 2); - writeFileSync(join(__dirname, 'api-spec.json'), doc); - - try { - let api = await SwaggerParser.validate(document as any); - console.log( - 'API name: %s, Version: %s', - api.info.title, - api.info.version - ); - expect(api.info.title).toEqual('Cats example'); - expect(api.paths['/api/cats']['get']['x-codeSamples'][0]['lang']).toEqual( - 'JavaScript' - ); - } catch (err) { - console.log(doc); - expect(err).toBeUndefined(); - } + describe('include/exclude', () => { + const createDocument = async (swaggerOptions?: SwaggerDocumentOptions) => { + const app = await NestFactory.create(IncludeExcludeModule, { + logger: false + }); + app.setGlobalPrefix('api/'); + + const options = new DocumentBuilder() + .setTitle('Include/Exclude') + .setVersion('1.0') + .setBasePath('api') + .build(); + + return SwaggerModule.createDocument(app, options, swaggerOptions); + }; + + it('should exclude specified modules', async () => { + const doc = await createDocument({ + exclude: [FooModule] + }); + const keys = Object.keys(doc.paths).sort(); + expect(keys).toEqual(['/api/bar', '/api/baz']); + }); + + it('should exclude multiple modules', async () => { + const doc = await createDocument({ + exclude: [FooModule, BarModule] + }); + const keys = Object.keys(doc.paths).sort(); + expect(keys).toEqual(['/api/baz']); + }); + + it('should include specified modules', async () => { + const doc = await createDocument({ + include: [FooModule] + }); + const keys = Object.keys(doc.paths).sort(); + expect(keys).toEqual(['/api/foo']); + }); + + it('should include multiple modules', async () => { + const doc = await createDocument({ + include: [FooModule, BarModule] + }); + const keys = Object.keys(doc.paths).sort(); + expect(keys).toEqual(['/api/bar', '/api/foo']); + }); + + it('should throw if include/exclude are specified together', async () => { + await expect(async () => { + await createDocument({ + include: [FooModule], + exclude: [BarModule] + }); + }).rejects.toThrow('Cannot specify both include and exclude together'); + }); }); }); diff --git a/lib/interfaces/swagger-document-options.interface.ts b/lib/interfaces/swagger-document-options.interface.ts index 041c62227..5faffb43e 100644 --- a/lib/interfaces/swagger-document-options.interface.ts +++ b/lib/interfaces/swagger-document-options.interface.ts @@ -1,9 +1,14 @@ export interface SwaggerDocumentOptions { /** - * List of modules to include in the specification + * List of modules to include in the specification. Cannot be used if `exclude` is specified */ include?: Function[]; + /** + * List of modules to exclude in the specification. Cannot be used if `include` is specified + */ + exclude?: Function[]; + /** * Additional, extra models that should be inspected and included in the specification */ diff --git a/lib/swagger-scanner.ts b/lib/swagger-scanner.ts index 17c1fc849..794d9a187 100644 --- a/lib/swagger-scanner.ts +++ b/lib/swagger-scanner.ts @@ -3,7 +3,7 @@ import { MODULE_PATH } from '@nestjs/common/constants'; import { NestContainer } from '@nestjs/core/injector/container'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { Module } from '@nestjs/core/injector/module'; -import { extend, flatten, isEmpty, reduce } from 'lodash'; +import { extend, flatten, reduce } from 'lodash'; import { OpenAPIObject, SwaggerDocumentOptions } from './interfaces'; import { ReferenceObject, @@ -31,6 +31,7 @@ export class SwaggerScanner { const { deepScanRoutes, include: includedModules = [], + exclude: excludedModules = [], extraModels = [], ignoreGlobalPrefix = false, operationIdFactory @@ -39,7 +40,8 @@ export class SwaggerScanner { const container: NestContainer = (app as any).container; const modules: Module[] = this.getModules( container.getModules(), - includedModules + includedModules, + excludedModules ); const globalPrefix = !ignoreGlobalPrefix ? stripLastSlash(this.getGlobalPrefix(app)) @@ -107,14 +109,22 @@ export class SwaggerScanner { public getModules( modulesContainer: Map, - include: Function[] + include: readonly Function[], + exclude: readonly Function[] ): Module[] { - if (!include || isEmpty(include)) { - return [...modulesContainer.values()]; + const modules = [...modulesContainer.values()]; + if (include.length === 0 && exclude.length === 0) { + return modules; } - return [...modulesContainer.values()].filter(({ metatype }) => - include.some((item) => item === metatype) - ); + + if (include.length && exclude.length) { + throw new Error('Cannot specify both include and exclude together'); + } + + const isInclude = include.length > 0; + const set = new Set(isInclude ? include : exclude); + + return modules.filter(({ metatype }) => set.has(metatype) === isInclude); } public addExtraModels(schemas: SchemaObject[], extraModels: Function[]) {