Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/define global responses #1612

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ The following methods have been added:
- `addBasicAuth`
- `addSecurity`
- `addSecurityRequirements`
- `addResponse`

## Support

Expand Down
47 changes: 47 additions & 0 deletions e2e/api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
},
"403": {
"description": "Forbidden."
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -223,6 +229,12 @@
"responses": {
"200": {
"description": ""
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -276,6 +288,12 @@
}
}
}
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -422,6 +440,12 @@
"responses": {
"200": {
"description": ""
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -470,6 +494,12 @@
"responses": {
"201": {
"description": ""
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -528,6 +558,12 @@
},
"403": {
"description": "Forbidden."
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -565,6 +601,12 @@
"responses": {
"200": {
"description": ""
},
"500": {
"description": "Internal server error"
},
"502": {
"$ref": "#/components/responses/502"
}
},
"tags": [
Expand Down Expand Up @@ -731,6 +773,11 @@
"name": "connect.sid"
}
},
"responses": {
"502": {
"description": "Bad gateway"
}
},
"schemas": {
"ExtraModel": {
"type": "object",
Expand Down
1 change: 1 addition & 0 deletions e2e/src/cats/cats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { LettersEnum, PaginationQuery } from './dto/pagination-query.dto';
description: 'Test',
schema: { default: 'test' }
})
@ApiResponse({ status: 500, description: 'Internal server error' })
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
Expand Down
4 changes: 4 additions & 0 deletions e2e/validate-schema.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ describe('Validate OpenAPI schema', () => {
.addCookieAuth()
.addSecurityRequirements('bearer')
.addSecurityRequirements({ basic: [], cookie: [] })
.addResponse({
status: 502,
description: 'Bad gateway'
})
.build();
});

Expand Down
15 changes: 13 additions & 2 deletions lib/document-builder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Logger } from '@nestjs/common';
import { isString, isUndefined, negate, pickBy } from 'lodash';
import { isString, isUndefined, negate, pickBy, omit } from 'lodash';
import { buildDocumentBase } from './fixtures/document.base';
import { OpenAPIObject } from './interfaces';
import { ApiResponseOptions } from './decorators/';
import {
ExternalDocumentationObject,
SecurityRequirementObject,
ResponseObject,
SecuritySchemeObject,
ServerVariableObject,
TagObject
Expand Down Expand Up @@ -182,7 +184,16 @@ export class DocumentBuilder {
return this;
}

public build(): Omit<OpenAPIObject, 'paths'> {
public addResponse(options: ApiResponseOptions): this {
this.document.components.responses = {
...(this.document.components.responses || {}),
[options.status]: omit(options, 'status') as ResponseObject
};

return this;
}

public build(): Omit<OpenAPIObject, 'components' | 'paths'> {
return this.document;
}
}
25 changes: 3 additions & 22 deletions lib/explorers/api-response.explorer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { HttpStatus, RequestMethod, Type } from '@nestjs/common';
import { HTTP_CODE_METADATA, METHOD_METADATA } from '@nestjs/common/constants';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { get, mapValues, omit } from 'lodash';
import { get } from 'lodash';
import { DECORATORS } from '../constants';
import { ApiResponseMetadata } from '../decorators';
import { SchemaObject } from '../interfaces/open-api-spec.interface';
import { ResponseObjectFactory } from '../services/response-object-factory';
import { mapResponsesToSwaggerResponses } from '../utils/map-responses-to-swagger-responses.util';
import { mergeAndUniq } from '../utils/merge-and-uniq.util';

const responseObjectFactory = new ResponseObjectFactory();

export const exploreGlobalApiResponseMetadata = (
schemas: Record<string, SchemaObject>,
metatype: Type<unknown>
) => {
const responses: ApiResponseMetadata[] = Reflect.getMetadata(
const responses: Record<string, ApiResponseMetadata> = Reflect.getMetadata(
DECORATORS.API_RESPONSE,
metatype
);
Expand Down Expand Up @@ -68,19 +65,3 @@ const getStatusCode = (method: Function) => {
return HttpStatus.OK;
}
};

const omitParamType = (param: Record<string, any>) => omit(param, 'type');
const mapResponsesToSwaggerResponses = (
responses: ApiResponseMetadata[],
schemas: Record<string, SchemaObject>,
produces: string[] = ['application/json']
) => {
produces = isEmpty(produces) ? ['application/json'] : produces;

const openApiResponses = mapValues(
responses,
(response: ApiResponseMetadata) =>
responseObjectFactory.create(response, produces, schemas)
);
return mapValues(openApiResponses, omitParamType);
};
23 changes: 18 additions & 5 deletions lib/swagger-explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import {
isArray,
isEmpty,
mapValues,
merge,
omit,
omitBy,
pick
} from 'lodash';
import * as pathToRegexp from 'path-to-regexp';
import { DECORATORS } from './constants';
import { ApiResponseOptions } from './decorators/';
import { exploreApiExcludeControllerMetadata } from './explorers/api-exclude-controller.explorer';
import { exploreApiExcludeEndpointMetadata } from './explorers/api-exclude-endpoint.explorer';
import {
Expand All @@ -57,6 +59,7 @@ import { DenormalizedDocResolvers } from './interfaces/denormalized-doc-resolver
import { DenormalizedDoc } from './interfaces/denormalized-doc.interface';
import {
OpenAPIObject,
ResponsesObject,
SchemaObject
} from './interfaces/open-api-spec.interface';
import { MimetypeContentWrapper } from './services/mimetype-content-wrapper';
Expand All @@ -79,6 +82,7 @@ export class SwaggerExplorer {
applicationConfig: ApplicationConfig,
modulePath?: string | undefined,
globalPrefix?: string | undefined,
globalResponses?: ResponsesObject,
operationIdFactory?: (controllerKey: string, methodKey: string) => string
) {
this.routePathFactory = new RoutePathFactory(applicationConfig);
Expand All @@ -105,7 +109,8 @@ export class SwaggerExplorer {
documentResolvers,
applicationConfig,
modulePath,
globalPrefix
globalPrefix,
globalResponses
);
}

Expand All @@ -120,7 +125,8 @@ export class SwaggerExplorer {
documentResolvers: DenormalizedDocResolvers,
applicationConfig: ApplicationConfig,
modulePath?: string,
globalPrefix?: string
globalPrefix?: string,
globalResponses?: ResponsesObject
): DenormalizedDoc[] {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
Expand All @@ -129,7 +135,12 @@ export class SwaggerExplorer {
if (excludeController) {
return [];
}
const globalMetadata = this.exploreGlobalMetadata(metatype);
const globalMetadata = this.exploreGlobalMetadata(
// Responses is not a property of `OpenAPIObject`, but that's what's
// being returned (incorrect type) by this.exploreGlobalMetadata(...).
{ responses: globalResponses } as Partial<OpenAPIObject>,
metatype
);
const ctrlExtraModels = exploreGlobalApiExtraModelsMetadata(metatype);
this.registerExtraModels(ctrlExtraModels);

Expand Down Expand Up @@ -225,6 +236,7 @@ export class SwaggerExplorer {
}

private exploreGlobalMetadata(
metadataBase: Partial<OpenAPIObject>,
metatype: Type<unknown>
): Partial<OpenAPIObject> {
const globalExplorers = [
Expand All @@ -243,8 +255,9 @@ export class SwaggerExplorer {
chunks: (curr.chunks || []).concat(next)
};
}
return { ...curr, ...next };
}, {});

return merge({}, curr, next);
}, metadataBase);

return globalMetadata;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/swagger-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class SwaggerModule {
options: SwaggerDocumentOptions = {}
): OpenAPIObject {
const swaggerScanner = new SwaggerScanner();
const document = swaggerScanner.scanApplication(app, options);
const document = swaggerScanner.scanApplication(app, options, config);

document.components = assignTwoLevelsDeep(
{},
Expand Down
18 changes: 16 additions & 2 deletions lib/swagger-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import { NestContainer } from '@nestjs/core/injector/container';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { InstanceToken, Module } from '@nestjs/core/injector/module';
import { flatten, isEmpty } from 'lodash';
import { ApiResponseOptions } from './decorators/';
import { OpenAPIObject, SwaggerDocumentOptions } from './interfaces';
import {
ReferenceObject,
ResponsesObject,
SchemaObject
} from './interfaces/open-api-spec.interface';
import { ModelPropertiesAccessor } from './services/model-properties-accessor';
import { SchemaObjectFactory } from './services/schema-object-factory';
import { SwaggerTypesMapper } from './services/swagger-types-mapper';
import { SwaggerExplorer } from './swagger-explorer';
import { SwaggerTransformer } from './swagger-transformer';
import { mapResponsesToSwaggerResponses } from './utils/map-responses-to-swagger-responses.util';
import { getGlobalPrefix } from './utils/get-global-prefix';
import { stripLastSlash } from './utils/strip-last-slash.util';
import { transformResponsesToRefs } from './utils/transform-responses-to-refs.util';

export class SwaggerScanner {
private readonly transfomer = new SwaggerTransformer();
Expand All @@ -28,7 +32,8 @@ export class SwaggerScanner {

public scanApplication(
app: INestApplication,
options: SwaggerDocumentOptions
options: SwaggerDocumentOptions,
config: Omit<OpenAPIObject, 'paths'>
): Omit<OpenAPIObject, 'openapi' | 'info'> {
const {
deepScanRoutes,
Expand All @@ -37,6 +42,7 @@ export class SwaggerScanner {
ignoreGlobalPrefix = false,
operationIdFactory
} = options;
const schemas = this.explorer.getSchemas();

const container = (app as any).container as NestContainer;
const internalConfigRef = (app as any).config as ApplicationConfig;
Expand All @@ -48,6 +54,10 @@ export class SwaggerScanner {
const globalPrefix = !ignoreGlobalPrefix
? stripLastSlash(getGlobalPrefix(app))
: '';
const globalResponses = mapResponsesToSwaggerResponses(
config.components.responses as Record<string, ApiResponseOptions>,
schemas
);

const denormalizedPaths = modules.map(
({ routes, metatype, relatedModules }) => {
Expand All @@ -71,6 +81,7 @@ export class SwaggerScanner {
modulePath,
globalPrefix,
internalConfigRef,
transformResponsesToRefs(globalResponses),
operationIdFactory
)
);
Expand All @@ -83,18 +94,19 @@ export class SwaggerScanner {
modulePath,
globalPrefix,
internalConfigRef,
transformResponsesToRefs(globalResponses),
operationIdFactory
)
);
}
);

const schemas = this.explorer.getSchemas();
this.addExtraModels(schemas, extraModels);

return {
...this.transfomer.normalizePaths(flatten(denormalizedPaths)),
components: {
responses: globalResponses as ResponsesObject,
schemas: schemas as Record<string, SchemaObject | ReferenceObject>
}
};
Expand All @@ -105,6 +117,7 @@ export class SwaggerScanner {
modulePath: string | undefined,
globalPrefix: string | undefined,
applicationConfig: ApplicationConfig,
globalResponses?: ResponsesObject,
operationIdFactory?: (controllerKey: string, methodKey: string) => string
): Array<Omit<OpenAPIObject, 'openapi' | 'info'> & Record<'root', any>> {
const denormalizedArray = [...routes.values()].map((ctrl) =>
Expand All @@ -113,6 +126,7 @@ export class SwaggerScanner {
applicationConfig,
modulePath,
globalPrefix,
globalResponses,
operationIdFactory
)
);
Expand Down
Loading