Skip to content

Commit

Permalink
Merge pull request #2465 from nestjs/7.0.0
Browse files Browse the repository at this point in the history
chore: laying the grounds for v7.0.0
  • Loading branch information
kamilmysliwiec authored Jun 15, 2023
2 parents 99909fd + b3b3a09 commit a6bc318
Show file tree
Hide file tree
Showing 25 changed files with 819 additions and 317 deletions.
25 changes: 10 additions & 15 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,23 @@ version: 2
aliases:
- &restore-cache
restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
key: dependency-cache-{{ checksum "package.json" }}
- &install-deps
run:
name: Install dependencies
command: npm ci
name: Install dependencies
command: npm ci
- &build-packages
run:
name: Build
command: npm run build
name: Build
command: npm run build
- &run-unit-tests
run:
name: Test
command: npm run test -- --runInBand
- &run-unit-tests
run:
name: Test (TypeScript < v4.8)
command: npm i --no-save -D [email protected] && npm run test -- --runInBand
name: Test
command: npm run test -- --runInBand
- &run-e2e-tests
run:
name: E2E test
command: npm run test:e2e
name: E2E test
command: npm run test:e2e

jobs:
build:
Expand All @@ -46,7 +42,7 @@ jobs:
- ./node_modules
- run:
name: Build
command: npm run build
command: npm run build

unit_tests:
working_directory: ~/nest
Expand Down Expand Up @@ -81,4 +77,3 @@ workflows:
- e2e_tests:
requires:
- build

19 changes: 18 additions & 1 deletion e2e/api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@
"/api/cats/bulk": {
"get": {
"operationId": "CatsController_findAllBulk",
"summary": "Find all cats in bulk",
"parameters": [
{
"name": "header",
Expand All @@ -607,7 +608,17 @@
],
"responses": {
"200": {
"description": ""
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Cat"
}
}
}
}
}
},
"tags": [
Expand Down Expand Up @@ -992,6 +1003,7 @@
"type": "object",
"properties": {
"name": {
"description": "Name of the cat",
"type": "string"
},
"age": {
Expand Down Expand Up @@ -1080,6 +1092,11 @@
"description": "The breed of the Cat"
},
"_tags": {
"description": "Tags of the cat",
"example": [
"tag1",
"tag2"
],
"type": "array",
"items": {
"type": "string"
Expand Down
44 changes: 43 additions & 1 deletion e2e/validate-schema.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { writeFileSync } from 'fs';
import { OpenAPIV3 } from 'openapi-types';
import { join } from 'path';
import * as SwaggerParser from 'swagger-parser';
import {
Expand All @@ -12,7 +13,6 @@ import {
import { ApplicationModule } from './src/app.module';
import { Cat } from './src/cats/classes/cat.class';
import { TagDto } from './src/cats/dto/tag.dto';
import { OpenAPIV3 } from 'openapi-types';

describe('Validate OpenAPI schema', () => {
let app: INestApplication;
Expand Down Expand Up @@ -49,6 +49,48 @@ describe('Validate OpenAPI schema', () => {
});

it('should produce a valid OpenAPI 3.0 schema', async () => {
SwaggerModule.loadPluginMetadata({
metadata: {
'@nestjs/swagger': {
models: [
[
require('./src/cats/classes/cat.class'),
{
Cat: {
tags: {
description: 'Tags of the cat',
example: ['tag1', 'tag2']
}
}
}
],
[
require('./src/cats/dto/create-cat.dto'),
{
CreateCatDto: {
name: {
description: 'Name of the cat'
}
}
}
]
],
controllers: [
[
require('./src/cats/cats.controller'),
{
CatsController: {
findAllBulk: {
type: [require('./src/cats/classes/cat.class').Cat],
summary: 'Find all cats in bulk'
}
}
}
]
]
}
}
});
const document = SwaggerModule.createDocument(app, options);

const doc = JSON.stringify(document, null, 2);
Expand Down
8 changes: 6 additions & 2 deletions lib/decorators/api-operation.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const defaultOperationOptions: ApiOperationOptions = {
summary: ''
};

export function ApiOperation(options: ApiOperationOptions): MethodDecorator {
export function ApiOperation(
options: ApiOperationOptions,
{ overrideExisting } = { overrideExisting: true }
): MethodDecorator {
return createMethodDecorator(
DECORATORS.API_OPERATION,
pickBy(
Expand All @@ -18,6 +21,7 @@ export function ApiOperation(options: ApiOperationOptions): MethodDecorator {
...options
} as ApiOperationOptions,
negate(isUndefined)
)
),
{ overrideExisting }
);
}
23 changes: 16 additions & 7 deletions lib/decorators/api-response.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { HttpStatus, Type } from '@nestjs/common';
import { omit } from 'lodash';
import { DECORATORS } from '../constants';
import {
ReferenceObject,
ResponseObject,
SchemaObject,
ReferenceObject
SchemaObject
} from '../interfaces/open-api-spec.interface';
import { getTypeIsArrayTuple } from './helpers';

Expand All @@ -26,7 +26,8 @@ export interface ApiResponseSchemaHost
export type ApiResponseOptions = ApiResponseMetadata | ApiResponseSchemaHost;

export function ApiResponse(
options: ApiResponseOptions
options: ApiResponseOptions,
{ overrideExisting } = { overrideExisting: true }
): MethodDecorator & ClassDecorator {
const [type, isArray] = getTypeIsArrayTuple(
(options as ApiResponseMetadata).type,
Expand All @@ -46,8 +47,14 @@ export function ApiResponse(
descriptor?: TypedPropertyDescriptor<any>
): any => {
if (descriptor) {
const responses =
Reflect.getMetadata(DECORATORS.API_RESPONSE, descriptor.value) || {};
const responses = Reflect.getMetadata(
DECORATORS.API_RESPONSE,
descriptor.value
);

if (responses && !overrideExisting) {
return descriptor;
}
Reflect.defineMetadata(
DECORATORS.API_RESPONSE,
{
Expand All @@ -58,8 +65,10 @@ export function ApiResponse(
);
return descriptor;
}
const responses =
Reflect.getMetadata(DECORATORS.API_RESPONSE, target) || {};
const responses = Reflect.getMetadata(DECORATORS.API_RESPONSE, target);
if (responses && !overrideExisting) {
return descriptor;
}
Reflect.defineMetadata(
DECORATORS.API_RESPONSE,
{
Expand Down
15 changes: 14 additions & 1 deletion lib/decorators/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,26 @@ import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants';

export function createMethodDecorator<T = any>(
metakey: string,
metadata: T
metadata: T,
{ overrideExisting } = { overrideExisting: true }
): MethodDecorator {
return (
target: object,
key: string | symbol,
descriptor: PropertyDescriptor
) => {
if (typeof metadata === 'object') {
const prevValue = Reflect.getMetadata(metakey, descriptor.value);
if (prevValue && !overrideExisting) {
return descriptor;
}
Reflect.defineMetadata(
metakey,
{ ...prevValue, ...metadata },
descriptor.value
);
return descriptor;
}
Reflect.defineMetadata(metakey, metadata, descriptor.value);
return descriptor;
};
Expand Down
48 changes: 47 additions & 1 deletion lib/explorers/api-operation.explorer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
import { Type } from '@nestjs/common';
import { DECORATORS } from '../constants';
import { ApiOperation } from '../decorators/api-operation.decorator';
import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants';

export const exploreApiOperationMetadata = (
instance: object,
prototype: Type<unknown>,
method: object
) => Reflect.getMetadata(DECORATORS.API_OPERATION, method);
) => {
applyMetadataFactory(prototype);
return Reflect.getMetadata(DECORATORS.API_OPERATION, method);
};

function applyMetadataFactory(prototype: Type<unknown>) {
const classPrototype = prototype;
do {
if (!prototype.constructor) {
return;
}
if (!prototype.constructor[METADATA_FACTORY_NAME]) {
continue;
}
const metadata = prototype.constructor[METADATA_FACTORY_NAME]();
const methodKeys = Object.keys(metadata);
methodKeys.forEach((key) => {
const operationMeta = {};
const { summary, deprecated, tags } = metadata[key];

applyIfNotNil(operationMeta, 'summary', summary);
applyIfNotNil(operationMeta, 'deprecated', deprecated);
applyIfNotNil(operationMeta, 'tags', tags);

if (Object.keys(operationMeta).length === 0) {
return;
}
ApiOperation(operationMeta, { overrideExisting: false })(
classPrototype,
key,
Object.getOwnPropertyDescriptor(classPrototype, key)
);
});
} while (
(prototype = Reflect.getPrototypeOf(prototype) as Type<any>) &&
prototype !== Object.prototype &&
prototype
);
}

function applyIfNotNil(target: Record<string, any>, key: string, value: any) {
if (value !== undefined && value !== null) {
target[key] = value;
}
}
38 changes: 37 additions & 1 deletion lib/explorers/api-response.explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ 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 { DECORATORS } from '../constants';
import { ApiResponseMetadata } from '../decorators';
import { ApiResponse, ApiResponseMetadata } from '../decorators';
import { SchemaObject } from '../interfaces/open-api-spec.interface';
import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants';
import { ResponseObjectFactory } from '../services/response-object-factory';
import { mergeAndUniq } from '../utils/merge-and-uniq.util';

Expand Down Expand Up @@ -32,6 +33,8 @@ export const exploreApiResponseMetadata = (
prototype: Type<unknown>,
method: Function
) => {
applyMetadataFactory(prototype);

const responses = Reflect.getMetadata(DECORATORS.API_RESPONSE, method);
if (responses) {
const classProduces = Reflect.getMetadata(
Expand Down Expand Up @@ -84,3 +87,36 @@ const mapResponsesToSwaggerResponses = (
);
return mapValues(openApiResponses, omitParamType);
};

function applyMetadataFactory(prototype: Type<unknown>) {
const classPrototype = prototype;
do {
if (!prototype.constructor) {
return;
}
if (!prototype.constructor[METADATA_FACTORY_NAME]) {
continue;
}
const metadata = prototype.constructor[METADATA_FACTORY_NAME]();
const methodKeys = Object.keys(metadata);
methodKeys.forEach((key) => {
const { summary, deprecated, tags, ...meta } = metadata[key];

if (Object.keys(meta).length === 0) {
return;
}
if (meta.status === undefined) {
meta.status = getStatusCode(classPrototype[key]);
}
ApiResponse(meta, { overrideExisting: false })(
classPrototype,
key,
Object.getOwnPropertyDescriptor(classPrototype, key)
);
});
} while (
(prototype = Reflect.getPrototypeOf(prototype) as Type<any>) &&
prototype !== Object.prototype &&
prototype
);
}
6 changes: 6 additions & 0 deletions lib/extra/swagger-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,9 @@ export function PickType() {
export function getSchemaPath() {
return () => '';
}
export function before() {
return () => '';
}
export function ReadonlyVisitor() {
return class {};
}
2 changes: 2 additions & 0 deletions lib/interfaces/swagger-custom-options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { SwaggerUiOptions } from './swagger-ui-options.interface';
import { SwaggerDocumentOptions } from './swagger-document-options.interface';
import { OpenAPIObject } from './open-api-spec.interface';

export interface SwaggerCustomOptions {
useGlobalPrefix?: boolean;
Expand Down
Loading

0 comments on commit a6bc318

Please sign in to comment.