feat: 为OPENAPI添加统一返回数据结构

master
luoer 2023-08-02 17:27:38 +08:00
parent a110e3a59d
commit c89a732209
15 changed files with 135 additions and 10 deletions

View File

@ -10,8 +10,8 @@ import { DatabaseModule } from '@/database';
import { ValidationModule } from '@/common/validation'; import { ValidationModule } from '@/common/validation';
import { AuthModule } from '@/modules/auth'; import { AuthModule } from '@/modules/auth';
import { UserModule } from '@/modules/user'; import { UserModule } from '@/modules/user';
import { ResponseModule } from './common/response'; import { ResponseModule } from '@/common/response';
import { SerializationModule } from './common/serialization'; import { SerializationModule } from '@/common/serialization';
@Module({ @Module({
imports: [ imports: [

View File

@ -3,6 +3,10 @@ import { LoggerService } from './logger.service';
import { APP_INTERCEPTOR } from '@nestjs/core'; import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggerInterceptor } from './logger.interceptor'; import { LoggerInterceptor } from './logger.interceptor';
/**
*
* @description
*/
@Global() @Global()
@Module({ @Module({
providers: [ providers: [

View File

@ -1,11 +1,15 @@
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsNumber, IsOptional, Min } from 'class-validator'; import { IsNumber, IsOptional, Min } from 'class-validator';
/**
* DTO
* @example { page: 1, size: 10 }
*/
export class PaginationDto { export class PaginationDto {
/** /**
* *
* @example 1
*/ */
// @IsNumber()
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(1) @Min(1)
@ -14,6 +18,7 @@ export class PaginationDto {
/** /**
* *
* @example 10
*/ */
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()

View File

@ -4,6 +4,10 @@ import { AllExecptionFilter } from './notcaptured.filter';
import { HttpExecptionFilter } from './http.filter'; import { HttpExecptionFilter } from './http.filter';
import { ResponseInterceptor } from './response.interceptor'; import { ResponseInterceptor } from './response.interceptor';
/**
*
* @description /HTTP/
*/
@Module({ @Module({
providers: [ providers: [
/** /**
@ -33,5 +37,3 @@ import { ResponseInterceptor } from './response.interceptor';
], ],
}) })
export class ResponseModule {} export class ResponseModule {}
export const a = 1;

View File

@ -1,6 +1,10 @@
import { ClassSerializerInterceptor, Module } from '@nestjs/common'; import { ClassSerializerInterceptor, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core'; import { APP_INTERCEPTOR } from '@nestjs/core';
/**
*
* @description
*/
@Module({ @Module({
providers: [ providers: [
/** /**

View File

@ -1,6 +1,10 @@
import { ConfigService } from '@/config'; import { ConfigService } from '@/config';
import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static';
/**
*
* @see https://docs.nestjs.com/techniques/mvc#serve-static
*/
export const ServeStaticModule = _ServeStaticModule.forRootAsync({ export const ServeStaticModule = _ServeStaticModule.forRootAsync({
useFactory: (config: ConfigService) => { useFactory: (config: ConfigService) => {
return [ return [

View File

@ -1,7 +1,12 @@
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { ConfigService } from '@/config'; import { ConfigService } from '@/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
import { addResponseWrapper } from './util';
/**
* Swagger
* @param app
*/
export const initSwagger = (app: INestApplication) => { export const initSwagger = (app: INestApplication) => {
const config = app.get(ConfigService); const config = app.get(ConfigService);
const docConfig = new DocumentBuilder() const docConfig = new DocumentBuilder()
@ -16,7 +21,12 @@ export const initSwagger = (app: INestApplication) => {
.addTag('post', '文章管理') .addTag('post', '文章管理')
.addTag('upload', '文件上传') .addTag('upload', '文件上传')
.build(); .build();
const document = SwaggerModule.createDocument(app, docConfig); const options: SwaggerDocumentOptions = {
operationIdFactory(controllerKey, methodKey) {
return `${controllerKey}_${methodKey}`;
},
};
const document = addResponseWrapper(SwaggerModule.createDocument(app, docConfig, options));
SwaggerModule.setup(config.apiDocPrefix, app, document, { SwaggerModule.setup(config.apiDocPrefix, app, document, {
jsonDocumentUrl: `${config.apiDocPrefix}.json`, jsonDocumentUrl: `${config.apiDocPrefix}.json`,
yamlDocumentUrl: `${config.apiDocPrefix}.yaml`, yamlDocumentUrl: `${config.apiDocPrefix}.yaml`,

View File

@ -0,0 +1,70 @@
import { OpenAPIObject } from '@nestjs/swagger';
/**
*
* @param doc OPENAPI
* @example
* ```json
* {
* "code": 2000,
* "message": "请求成功",
* "data": []
* }
* ```
* @returns
*/
export function addResponseWrapper(doc: OpenAPIObject) {
for (const path of Object.keys(doc.paths)) {
const pathItem = doc.paths[path];
if (!pathItem) {
continue;
}
for (const method of Object.keys(pathItem)) {
const responses = doc.paths[path][method].responses;
if (!responses) {
continue;
}
for (const status of Object.keys(responses)) {
const json = responses[status].content?.['application/json'];
if (!json) {
continue;
}
const schema = json.schema;
json.schema = {
allOf: [
{
$ref: '#/components/schemas/Response',
},
{
type: 'object',
description: '返回数据',
properties: {
data: schema,
},
},
],
};
}
}
}
doc.components.schemas.Response = {
type: 'object',
properties: {
code: {
type: 'integer',
description: '状态码',
example: 2000,
format: 'int32',
},
message: {
type: 'string',
description: '提示信息',
example: '请求成功',
},
},
required: ['code', 'message'],
};
return doc;
}

View File

@ -3,6 +3,10 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { validationPipeFactory } from './validation.pipe'; import { validationPipeFactory } from './validation.pipe';
import { ValidationExecptionFilter } from './validation.filter'; import { ValidationExecptionFilter } from './validation.filter';
/**
*
* @description
*/
@Module({ @Module({
providers: [ providers: [
/** /**

View File

@ -2,6 +2,10 @@ import { Global, Module } from '@nestjs/common';
import { ConfigModule as _ConfigModule } from '@nestjs/config'; import { ConfigModule as _ConfigModule } from '@nestjs/config';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
/**
*
* @description `@nestjs/config` 便
*/
@Global() @Global()
@Module({ @Module({
imports: [ imports: [

View File

@ -3,7 +3,12 @@ import { ConfigService as _ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class ConfigService { export class ConfigService {
constructor(public config: _ConfigService) {} constructor(
/**
* `@nestjs/config` ConfigService
*/
public config: _ConfigService,
) {}
/** /**
* get * get

View File

@ -4,6 +4,7 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
/** /**
* *
* @description `typeorm`
*/ */
export const DatabaseModule = TypeOrmModule.forRootAsync({ export const DatabaseModule = TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => { useFactory: (config: ConfigService) => {

View File

@ -1 +1,6 @@
export class CreateUploadDto {} import { ApiProperty } from '@nestjs/swagger';
export class CreateUploadDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: any;
}

View File

@ -1,8 +1,9 @@
import { Controller, Delete, Get, Param, Patch, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; import { Controller, Delete, Get, Param, Patch, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
import { UploadService } from './upload.service'; import { UploadService } from './upload.service';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { Respond } from '@/common/response'; import { Respond } from '@/common/response';
import { CreateUploadDto } from './dto/create-upload.dto';
@ApiTags('upload') @ApiTags('upload')
@Controller('upload') @Controller('upload')
@ -12,6 +13,8 @@ export class UploadController {
@Post() @Post()
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: '上传文件', operationId: 'upload' }) @ApiOperation({ summary: '上传文件', operationId: 'upload' })
@ApiConsumes('multipart/form-data')
@ApiBody({ description: '文件', type: CreateUploadDto })
create(@UploadedFile() file: Express.Multer.File) { create(@UploadedFile() file: Express.Multer.File) {
return this.uploadService.create(file); return this.uploadService.create(file);
} }

View File

@ -3,6 +3,10 @@ import { IsOptional, IsString } from 'class-validator';
import { PaginationDto } from '@/common/response'; import { PaginationDto } from '@/common/response';
export class FindUserDto extends IntersectionType(PaginationDto) { export class FindUserDto extends IntersectionType(PaginationDto) {
/**
*
* @example '绝弹'
*/
@IsOptional() @IsOptional()
@IsString() @IsString()
nickname: string; nickname: string;