From 5b794c0419a537b99f4c7164deed22d205acddec Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 23 Jun 2026 07:17:30 +0300 Subject: [PATCH] feat: add Swagger response DTOs for all missing endpoints - Shares: ShareEnvelopeDto, ShareMarketDataEnvelopeDto, DividendsEnvelopeDto, ShareHistoryEnvelopeDto, DividendItemDto, HistoryItemDto - Bonds: BondEnvelopeDto, BondMarketDataEnvelopeDto, BondHistoryEnvelopeDto, BondHistoryItemDto - Candles: CandleItemDto, CandleEnvelopeDto - Securities: SearchResultItemDto, SearchEnvelopeDto - Health: HealthResponseDto, HealthEnvelopeDto - Add @ApiOkResponse decorators to all previously undocumented endpoints - Reuse ApiResponseMeta from common for all envelope DTOs --- .../src/modules/bonds/bonds.controller.ts | 8 +++- .../modules/bonds/dto/bonds-envelope.dto.ts | 28 ++++++++++++++ .../src/modules/bonds/dto/history-item.dto.ts | 15 ++++++++ .../src/modules/candles/candles.controller.ts | 7 +++- .../modules/candles/dto/candle-item.dto.ts | 27 +++++++++++++ .../candles/dto/candles-envelope.dto.ts | 11 ++++++ .../modules/health/dto/health-envelope.dto.ts | 11 ++++++ .../modules/health/dto/health-response.dto.ts | 12 ++++++ .../src/modules/health/health.controller.ts | 6 ++- .../securities/dto/search-response.dto.ts | 33 ++++++++++++++++ .../securities/securities.controller.ts | 6 ++- .../modules/shares/dto/dividend-item.dto.ts | 12 ++++++ .../modules/shares/dto/history-item.dto.ts | 24 ++++++++++++ .../modules/shares/dto/shares-envelope.dto.ts | 38 +++++++++++++++++++ .../src/modules/shares/shares.controller.ts | 14 ++++++- 15 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 apps/backend/src/modules/bonds/dto/bonds-envelope.dto.ts create mode 100644 apps/backend/src/modules/bonds/dto/history-item.dto.ts create mode 100644 apps/backend/src/modules/candles/dto/candle-item.dto.ts create mode 100644 apps/backend/src/modules/candles/dto/candles-envelope.dto.ts create mode 100644 apps/backend/src/modules/health/dto/health-envelope.dto.ts create mode 100644 apps/backend/src/modules/health/dto/health-response.dto.ts create mode 100644 apps/backend/src/modules/securities/dto/search-response.dto.ts create mode 100644 apps/backend/src/modules/shares/dto/dividend-item.dto.ts create mode 100644 apps/backend/src/modules/shares/dto/history-item.dto.ts create mode 100644 apps/backend/src/modules/shares/dto/shares-envelope.dto.ts diff --git a/apps/backend/src/modules/bonds/bonds.controller.ts b/apps/backend/src/modules/bonds/bonds.controller.ts index 613431a..d5af481 100644 --- a/apps/backend/src/modules/bonds/bonds.controller.ts +++ b/apps/backend/src/modules/bonds/bonds.controller.ts @@ -1,26 +1,32 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiOkResponse, ApiExtraModels } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../common/dto/api-response.dto'; import { BondsService } from './bonds.service'; +import { BondEnvelopeDto, BondMarketDataEnvelopeDto, BondHistoryEnvelopeDto } from './dto/bonds-envelope.dto'; @ApiTags('Bonds') +@ApiExtraModels(ApiResponseMeta) @Controller('securities/bonds') export class BondsController { constructor(private readonly bondsService: BondsService) {} @Get(':secid') @ApiOperation({ summary: 'Получить спецификацию облигации' }) + @ApiOkResponse({ type: BondEnvelopeDto }) async getBond(@Param('secid') secid: string) { return this.bondsService.getBond(secid); } @Get(':secid/marketdata') @ApiOperation({ summary: 'Получить рыночные данные облигации' }) + @ApiOkResponse({ type: BondMarketDataEnvelopeDto }) async getMarketData(@Param('secid') secid: string) { return this.bondsService.getMarketData(secid); } @Get(':secid/history') @ApiOperation({ summary: 'Получить дневную историю торгов облигации' }) + @ApiOkResponse({ type: BondHistoryEnvelopeDto }) async getHistory( @Param('secid') secid: string, @Query('from') from: string, diff --git a/apps/backend/src/modules/bonds/dto/bonds-envelope.dto.ts b/apps/backend/src/modules/bonds/dto/bonds-envelope.dto.ts new file mode 100644 index 0000000..7d17a0a --- /dev/null +++ b/apps/backend/src/modules/bonds/dto/bonds-envelope.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../../common/dto/api-response.dto'; +import { BondMarketDataDto, BondResponseDto } from './bond-response.dto'; +import { BondHistoryItemDto } from './history-item.dto'; + +export class BondEnvelopeDto { + @ApiProperty({ type: BondResponseDto }) + data!: BondResponseDto; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} + +export class BondMarketDataEnvelopeDto { + @ApiProperty({ type: BondMarketDataDto }) + data!: BondMarketDataDto; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} + +export class BondHistoryEnvelopeDto { + @ApiProperty({ type: [BondHistoryItemDto] }) + data!: BondHistoryItemDto[]; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} diff --git a/apps/backend/src/modules/bonds/dto/history-item.dto.ts b/apps/backend/src/modules/bonds/dto/history-item.dto.ts new file mode 100644 index 0000000..58b2087 --- /dev/null +++ b/apps/backend/src/modules/bonds/dto/history-item.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class BondHistoryItemDto { + @ApiProperty({ example: '2026-06-01' }) + date!: string; + + @ApiProperty({ example: 100.45 }) + closePrice!: number; + + @ApiPropertyOptional({ type: Number, nullable: true, example: 12.71 }) + yieldClose!: number | null; + + @ApiPropertyOptional({ type: Number, nullable: true, example: 4.5 }) + duration!: number | null; +} diff --git a/apps/backend/src/modules/candles/candles.controller.ts b/apps/backend/src/modules/candles/candles.controller.ts index 15747d9..4bfd91b 100644 --- a/apps/backend/src/modules/candles/candles.controller.ts +++ b/apps/backend/src/modules/candles/candles.controller.ts @@ -1,15 +1,19 @@ import { Controller, Get, Param, Query, ValidationPipe } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiOkResponse, ApiExtraModels } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../common/dto/api-response.dto'; import { CandlesService } from './candles.service'; import { CandlesQueryDto } from './dto/candles-query.dto'; +import { CandleEnvelopeDto } from './dto/candles-envelope.dto'; @ApiTags('Candles') +@ApiExtraModels(ApiResponseMeta) @Controller('securities') export class CandlesController { constructor(private readonly candlesService: CandlesService) {} @Get('shares/:secid/candles') @ApiOperation({ summary: 'Получить свечи акции' }) + @ApiOkResponse({ type: CandleEnvelopeDto }) async getShareCandles( @Param('secid') secid: string, @Query(ValidationPipe) query: CandlesQueryDto, @@ -19,6 +23,7 @@ export class CandlesController { @Get('bonds/:secid/candles') @ApiOperation({ summary: 'Получить свечи облигации' }) + @ApiOkResponse({ type: CandleEnvelopeDto }) async getBondCandles( @Param('secid') secid: string, @Query(ValidationPipe) query: CandlesQueryDto, diff --git a/apps/backend/src/modules/candles/dto/candle-item.dto.ts b/apps/backend/src/modules/candles/dto/candle-item.dto.ts new file mode 100644 index 0000000..403ce8b --- /dev/null +++ b/apps/backend/src/modules/candles/dto/candle-item.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CandleItemDto { + @ApiProperty({ example: 321.3 }) + open!: number; + + @ApiProperty({ example: 322.66 }) + high!: number; + + @ApiProperty({ example: 321.2 }) + low!: number; + + @ApiProperty({ example: 322.35 }) + close!: number; + + @ApiProperty({ example: 1925163 }) + volume!: number; + + @ApiProperty({ example: 620184479 }) + value!: number; + + @ApiProperty({ example: '2026-06-01T10:00:00' }) + begin!: string; + + @ApiProperty({ example: '2026-06-01T10:59:00' }) + end!: string; +} diff --git a/apps/backend/src/modules/candles/dto/candles-envelope.dto.ts b/apps/backend/src/modules/candles/dto/candles-envelope.dto.ts new file mode 100644 index 0000000..b78c866 --- /dev/null +++ b/apps/backend/src/modules/candles/dto/candles-envelope.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../../common/dto/api-response.dto'; +import { CandleItemDto } from './candle-item.dto'; + +export class CandleEnvelopeDto { + @ApiProperty({ type: [CandleItemDto] }) + data!: CandleItemDto[]; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} diff --git a/apps/backend/src/modules/health/dto/health-envelope.dto.ts b/apps/backend/src/modules/health/dto/health-envelope.dto.ts new file mode 100644 index 0000000..eab8c7e --- /dev/null +++ b/apps/backend/src/modules/health/dto/health-envelope.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../../common/dto/api-response.dto'; +import { HealthResponseDto } from './health-response.dto'; + +export class HealthEnvelopeDto { + @ApiProperty({ type: HealthResponseDto }) + data!: HealthResponseDto; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} diff --git a/apps/backend/src/modules/health/dto/health-response.dto.ts b/apps/backend/src/modules/health/dto/health-response.dto.ts new file mode 100644 index 0000000..770c4d8 --- /dev/null +++ b/apps/backend/src/modules/health/dto/health-response.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class HealthResponseDto { + @ApiProperty({ example: 'ok' }) + status!: string; + + @ApiProperty({ example: '2026-06-23T06:00:00.000Z' }) + timestamp!: string; + + @ApiProperty({ example: 12345 }) + uptime!: number; +} diff --git a/apps/backend/src/modules/health/health.controller.ts b/apps/backend/src/modules/health/health.controller.ts index e27e33b..b4b6451 100644 --- a/apps/backend/src/modules/health/health.controller.ts +++ b/apps/backend/src/modules/health/health.controller.ts @@ -1,13 +1,17 @@ import { Controller, Get } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiOkResponse, ApiExtraModels } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../common/dto/api-response.dto'; import { Public } from '../auth/decorators/public.decorator'; +import { HealthEnvelopeDto } from './dto/health-envelope.dto'; @ApiTags('Health') +@ApiExtraModels(ApiResponseMeta) @Controller('health') export class HealthController { @Get() @Public() @ApiOperation({ summary: 'Проверка состояния сервиса' }) + @ApiOkResponse({ type: HealthEnvelopeDto }) check() { return { status: 'ok', diff --git a/apps/backend/src/modules/securities/dto/search-response.dto.ts b/apps/backend/src/modules/securities/dto/search-response.dto.ts new file mode 100644 index 0000000..2c9b1f6 --- /dev/null +++ b/apps/backend/src/modules/securities/dto/search-response.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../../common/dto/api-response.dto'; + +export class SearchResultItemDto { + @ApiProperty({ example: 'SBER' }) + secid!: string; + + @ApiProperty({ example: 'RU0009029540' }) + isin!: string; + + @ApiProperty({ example: 'Сбербанк' }) + shortName!: string; + + @ApiProperty({ enum: ['share', 'bond'] }) + type!: 'share' | 'bond'; + + @ApiProperty({ example: 1 }) + listLevel!: number; + + @ApiPropertyOptional({ type: String, nullable: true, example: 'RUB' }) + currency!: string | null; + + @ApiPropertyOptional({ type: Number, nullable: true, example: 322.35 }) + price!: number | null; +} + +export class SearchEnvelopeDto { + @ApiProperty({ type: [SearchResultItemDto] }) + data!: SearchResultItemDto[]; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} diff --git a/apps/backend/src/modules/securities/securities.controller.ts b/apps/backend/src/modules/securities/securities.controller.ts index be26469..5f17949 100644 --- a/apps/backend/src/modules/securities/securities.controller.ts +++ b/apps/backend/src/modules/securities/securities.controller.ts @@ -1,12 +1,15 @@ import { Controller, Get, Query, ValidationPipe } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiOkResponse } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiOkResponse, ApiExtraModels } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../common/dto/api-response.dto'; import { SecuritiesService } from './securities.service'; import { ScreenerService } from './screener.service'; import { SearchQueryDto, SecurityType } from './dto/search-query.dto'; import { ScreenerQueryDto } from './dto/screener-query.dto'; import { ScreenerResponseDto } from './dto/screener-response.dto'; +import { SearchEnvelopeDto } from './dto/search-response.dto'; @ApiTags('Securities') +@ApiExtraModels(ApiResponseMeta) @Controller('securities') export class SecuritiesController { constructor( @@ -16,6 +19,7 @@ export class SecuritiesController { @Get('search') @ApiOperation({ summary: 'Поиск по инструментам' }) + @ApiOkResponse({ type: SearchEnvelopeDto }) async search(@Query(ValidationPipe) query: SearchQueryDto) { const results = await this.securitiesService.search( query.q, diff --git a/apps/backend/src/modules/shares/dto/dividend-item.dto.ts b/apps/backend/src/modules/shares/dto/dividend-item.dto.ts new file mode 100644 index 0000000..25fb894 --- /dev/null +++ b/apps/backend/src/modules/shares/dto/dividend-item.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DividendItemDto { + @ApiProperty({ example: '2026-05-15' }) + registryCloseDate!: string; + + @ApiProperty({ example: 33.47 }) + value!: number; + + @ApiProperty({ example: 'RUB' }) + currency!: string; +} diff --git a/apps/backend/src/modules/shares/dto/history-item.dto.ts b/apps/backend/src/modules/shares/dto/history-item.dto.ts new file mode 100644 index 0000000..8cb6c0d --- /dev/null +++ b/apps/backend/src/modules/shares/dto/history-item.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class HistoryItemDto { + @ApiProperty({ example: '2026-06-01' }) + date!: string; + + @ApiProperty({ example: 321.3 }) + open!: number; + + @ApiProperty({ example: 322.66 }) + high!: number; + + @ApiProperty({ example: 321.2 }) + low!: number; + + @ApiProperty({ example: 322.35 }) + close!: number; + + @ApiProperty({ example: 1925163 }) + volume!: number; + + @ApiProperty({ example: 620184479 }) + value!: number; +} diff --git a/apps/backend/src/modules/shares/dto/shares-envelope.dto.ts b/apps/backend/src/modules/shares/dto/shares-envelope.dto.ts new file mode 100644 index 0000000..0ade6df --- /dev/null +++ b/apps/backend/src/modules/shares/dto/shares-envelope.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../../common/dto/api-response.dto'; +import { ShareResponseDto } from './share-response.dto'; +import { ShareMarketDataResponseDto } from './share-marketdata-response.dto'; +import { HistoryItemDto } from './history-item.dto'; +import { DividendItemDto } from './dividend-item.dto'; + +export class ShareEnvelopeDto { + @ApiProperty({ type: ShareResponseDto }) + data!: ShareResponseDto; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} + +export class ShareMarketDataEnvelopeDto { + @ApiProperty({ type: ShareMarketDataResponseDto }) + data!: ShareMarketDataResponseDto; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} + +export class DividendsEnvelopeDto { + @ApiProperty({ type: [DividendItemDto] }) + data!: DividendItemDto[]; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} + +export class ShareHistoryEnvelopeDto { + @ApiProperty({ type: [HistoryItemDto] }) + data!: HistoryItemDto[]; + + @ApiProperty({ type: ApiResponseMeta }) + meta!: ApiResponseMeta; +} diff --git a/apps/backend/src/modules/shares/shares.controller.ts b/apps/backend/src/modules/shares/shares.controller.ts index ea8a41d..1f082c8 100644 --- a/apps/backend/src/modules/shares/shares.controller.ts +++ b/apps/backend/src/modules/shares/shares.controller.ts @@ -1,14 +1,23 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiOkResponse, ApiExtraModels } from '@nestjs/swagger'; +import { ApiResponseMeta } from '../../common/dto/api-response.dto'; import { SharesService } from './shares.service'; +import { + ShareEnvelopeDto, + ShareMarketDataEnvelopeDto, + DividendsEnvelopeDto, + ShareHistoryEnvelopeDto, +} from './dto/shares-envelope.dto'; @ApiTags('Shares') +@ApiExtraModels(ApiResponseMeta) @Controller('securities/shares') export class SharesController { constructor(private readonly sharesService: SharesService) {} @Get(':secid') @ApiOperation({ summary: 'Получить спецификацию акции' }) + @ApiOkResponse({ type: ShareEnvelopeDto }) async getShare(@Param('secid') secid: string) { const share = await this.sharesService.getShare(secid); return { data: share, meta: { cachedAt: null, fromCache: false } }; @@ -16,18 +25,21 @@ export class SharesController { @Get(':secid/marketdata') @ApiOperation({ summary: 'Получить рыночные данные акции' }) + @ApiOkResponse({ type: ShareMarketDataEnvelopeDto }) async getMarketData(@Param('secid') secid: string) { return this.sharesService.getMarketData(secid); } @Get(':secid/dividends') @ApiOperation({ summary: 'Получить дивиденды' }) + @ApiOkResponse({ type: DividendsEnvelopeDto }) async getDividends(@Param('secid') secid: string) { return this.sharesService.getDividends(secid); } @Get(':secid/history') @ApiOperation({ summary: 'Получить дневную историю торгов акции' }) + @ApiOkResponse({ type: ShareHistoryEnvelopeDto }) async getHistory( @Param('secid') secid: string, @Query('from') from: string,