Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
feat(server): swagger platform (#188)
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiisol authored Aug 9, 2023
1 parent 886eee3 commit b317266
Show file tree
Hide file tree
Showing 27 changed files with 2,319 additions and 6 deletions.
38 changes: 37 additions & 1 deletion server/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![Node Decorators](../decorators.png?raw=true)
![Node Decorators](https://github.com/serhiisol/node-decorators/blob/master/decorators.png?raw=true)

## Installation
```
Expand Down Expand Up @@ -193,3 +193,39 @@ function AccessParam() {
// ...
create(@AccessParam() access: string) {}
```

# Swagger
Swagger decorators are available in
```typescript
import { SwaggerModule } from '@decorators/server/swagger';
```

To start with swagger decorators provide `SwaggerModule` in the `AppModule`, for example:

```typescript
import { SwaggerModule } from '@decorators/server/swagger';

@Module({
modules: [
HttpModule.create(ExpressAdapter),
SwaggerModule.forRoot({
description: 'Decorators Example App',
title: '@decorators/server',
}),
...
],
})
export class AppModule { }
```

## Decorators
### Method
* `@ApiOperation(operation: OpenAPIV3_1.OperationObject)` - Registers an operation
* `@ApiResponse(description: string, type?: ClassConstructor)` - Registers simple response for a method. This decorator uses status provided by the route decorator, e.g. `@Get(route, status)`.
* `@ApiResponseSchema(responses: ApiResponses)` - Registers a response for a method. This method accepts more complex types of responses, if method returns more than one.
* `@ApiBearerAuth()` - Defines a bearer authentication method for a route
* `@ApiSecurity(security: OpenAPIV3_1.SecuritySchemeObject)` - Defines more complex authentication methods for a route.

### Property
* `@ApiParameter(parammeter: { description?: string })` - Specifies a description for a property defined in the class-decorator based classes

5 changes: 5 additions & 0 deletions server/example/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { APP_VERSION, GLOBAL_PIPE, Module } from '@server';
import { ExpressAdapter } from '@server/express';
import { HttpModule } from '@server/http';
import { SwaggerModule } from '@server/swagger';

import { MiscModule, PostsModule } from './modules';
import { ServerPipe } from './pipes';

@Module({
modules: [
HttpModule.create(ExpressAdapter),
SwaggerModule.forRoot({
description: 'Decorators Example App',
title: '@decorators/server',
}),
MiscModule,
PostsModule,
],
Expand Down
31 changes: 30 additions & 1 deletion server/example/modules/posts/posts.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import { Controller, Pipe } from '@server';
import { Body, Get, Params, Post, Render } from '@server/http';
import { IsString } from 'class-validator';
import { ApiParameter, ApiResponse, ApiSecurity } from '@server/swagger';
import { IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';

import { ApiResponseSchema } from '../../../src/platforms/swagger/decorators/api-response';
import { PostsService } from '../../services';
import { Access, AccessParam } from './decorators';
import { AccessPipe } from './pipes';

class PostType {
@IsNumber()
@Min(5)
@Max(10)
@ApiParameter({
description: 'Super Count',
})
@IsOptional()
count: number;

@IsString({ each: true })
@IsOptional()
name: string[];

@IsString()
title: string;
}
Expand All @@ -24,6 +39,20 @@ export class PostsController {

@Access('granted')
@Pipe(AccessPipe)
@ApiResponse('Returns newly created post')
@ApiResponseSchema({
200: {
description: 'Returns newly created post',
type: PostType,
},
})
@ApiSecurity({
description: 'Auth',
in: 'query',
name: 'access',
type: 'apiKey',
})
// @ApiBearerAuth()
@Get(':id', 200)
@Render('post')
post(@Params('id', Number) id: number, @AccessParam() access: string) {
Expand Down
8 changes: 8 additions & 0 deletions server/integration/swagger/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Controller } from '@server';
import { Get } from '@server/http';

@Controller()
export class AppController {
@Get('get')
get() { }
}
8 changes: 8 additions & 0 deletions server/integration/swagger/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@server';

import { AppController } from './app.controller';

@Module({
controllers: [AppController],
})
export class AppModule { }
42 changes: 42 additions & 0 deletions server/integration/swagger/test/express.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Application, HttpStatus, Module } from '@server';
import { ExpressAdapter } from '@server/express';
import { HttpModule } from '@server/http';
import { SwaggerModule } from '@server/swagger';
import * as request from 'supertest';

import { AppModule } from '../src/app.module';

@Module({
modules: [
HttpModule.create(ExpressAdapter),
SwaggerModule.forRoot(),
AppModule,
],
})
class TestModule { }

describe('Express Swagger Route', () => {
let app: Application;
let module: HttpModule;

beforeEach(async () => {
app = await Application.create(TestModule);
module = await app.inject<HttpModule>(HttpModule);

await module.listen();
});

afterEach(() => module.close());

it('registers swagger file', async () => {
return request(module.getHttpServer())
.get('/swagger/swagger.json')
.expect(HttpStatus.OK);
});

it('registers swagger-ui page', async () => {
return request(module.getHttpServer())
.get('/swagger')
.expect(301);
});
});
3 changes: 3 additions & 0 deletions server/integration/swagger/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.test.json"
}
25 changes: 23 additions & 2 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@types/express": "4.17.8",
"@types/jest": "^29.5.3",
"@types/supertest": "^2.0.12",
"@types/swagger-ui-dist": "^3.30.1",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"body-parser": "^1.20.2",
Expand All @@ -22,9 +23,11 @@
"eslint-plugin-simple-import-sort": "^10.0.0",
"express": "^4.18.2",
"jest": "^29.5.0",
"openapi-types": "^12.1.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.1",
"supertest": "^6.3.3",
"swagger-ui-dist": "^5.2.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
Expand All @@ -33,7 +36,8 @@
"exports": {
".": "./lib/index.js",
"./express": "./lib/platforms/express/index.js",
"./http": "./lib/platforms/http/index.js"
"./http": "./lib/platforms/http/index.js",
"./swagger": "./lib/platforms/swagger/index.js"
},
"keywords": [
"nodejs",
Expand Down Expand Up @@ -66,8 +70,11 @@
],
"http": [
"./lib/platforms/http/index.d.ts"
],
"swagger": [
"./lib/platforms/swagger/index.d.ts"
]
}
},
"version": "1.0.0-beta.8"
"version": "1.0.0-beta.9"
}
8 changes: 8 additions & 0 deletions server/src/platforms/swagger/decorators/api-operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OpenAPIV3_1 } from 'openapi-types';

import { Decorate } from '../../../core';
import { METHOD_API_OPERATION_METADATA } from '../helpers';

export function ApiOperation(operation: OpenAPIV3_1.OperationObject) {
return Decorate(METHOD_API_OPERATION_METADATA, operation);
}
8 changes: 8 additions & 0 deletions server/src/platforms/swagger/decorators/api-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OpenAPIV3_1 } from 'openapi-types';

import { Decorate } from '../../../core';
import { PROPERTY_API_PARAMETER_METADATA } from '../helpers';

export function ApiParameter(parameter: Pick<OpenAPIV3_1.ParameterObject, 'description'>) {
return Decorate(PROPERTY_API_PARAMETER_METADATA, parameter);
}
13 changes: 13 additions & 0 deletions server/src/platforms/swagger/decorators/api-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ClassConstructor, Decorate } from '../../../core';
import { METHOD_API_RESPONSE_METADATA, METHOD_API_RESPONSES_METADATA } from '../helpers';
import { ApiBasicResponse, ApiResponses } from '../types';

export function ApiResponse(description: string, type?: ClassConstructor) {
return Decorate(METHOD_API_RESPONSE_METADATA, {
description, type,
} as ApiBasicResponse);
}

export function ApiResponseSchema(responses: ApiResponses) {
return Decorate(METHOD_API_RESPONSES_METADATA, responses);
}
15 changes: 15 additions & 0 deletions server/src/platforms/swagger/decorators/api-security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { OpenAPIV3_1 } from 'openapi-types';

import { Decorate } from '../../../core';
import { METHOD_API_SECURITY_METADATA } from '../helpers';

export function ApiSecurity(security: OpenAPIV3_1.SecuritySchemeObject) {
return Decorate(METHOD_API_SECURITY_METADATA, security);
}

export function ApiBearerAuth() {
return Decorate(METHOD_API_SECURITY_METADATA, {
scheme: 'bearer',
type: 'http',
} as OpenAPIV3_1.SecuritySchemeObject);
}
4 changes: 4 additions & 0 deletions server/src/platforms/swagger/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { ApiOperation } from './api-operation';
export { ApiParameter } from './api-parameter';
export { ApiResponse } from './api-response';
export * from './api-security';
5 changes: 5 additions & 0 deletions server/src/platforms/swagger/helpers/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const METHOD_API_OPERATION_METADATA = '__swagger__:method:api-operation';
export const METHOD_API_RESPONSE_METADATA = '__swagger__:method:api-response';
export const METHOD_API_RESPONSES_METADATA = '__swagger__:method:api-responses';
export const METHOD_API_SECURITY_METADATA = '__swagger__:method:template';
export const PROPERTY_API_PARAMETER_METADATA = '__swagger__:property:api-parameter';
2 changes: 2 additions & 0 deletions server/src/platforms/swagger/helpers/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './constants';
export * from './injectables';
3 changes: 3 additions & 0 deletions server/src/platforms/swagger/helpers/constants/injectables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { InjectionToken } from '@decorators/di';

export const SWAGGER_CONFIG = new InjectionToken('__swagger__:config');
2 changes: 2 additions & 0 deletions server/src/platforms/swagger/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './constants';
export * from './swagger-ui';
2 changes: 2 additions & 0 deletions server/src/platforms/swagger/helpers/swagger-ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SwaggerDocument } from './swagger-document';
export { SwaggerResolver } from './swagger-resolver';
Loading

0 comments on commit b317266

Please sign in to comment.