Просмотр исходного кода

feat(stats-reporting): enhance DTOs and controller with Swagger decorators for improved API documentation

Dave 1 месяц назад
Родитель
Сommit
206e3a98e3

+ 37 - 53
apps/box-stats-api/src/feature/stats-reporting/dto/stats-reporting.dto.ts

@@ -1,73 +1,57 @@
-import { Type } from 'class-transformer';
-import {
-  IsDefined,
-  IsEnum,
-  IsInt,
-  IsOptional,
-  IsString,
-  Max,
-  Min,
-} from 'class-validator';
-
-const SORT_ORDER = ['asc', 'desc'] as const;
-
-type SortOrder = (typeof SORT_ORDER)[number];
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
 
 export class BaseRangeQueryDto {
-  @IsDefined()
-  @Type(() => Number)
-  @IsInt()
+  @ApiProperty({
+    description: 'UTC epoch seconds (inclusive)',
+    example: 1767168000,
+  })
   fromSec!: number;
 
-  @IsDefined()
-  @Type(() => Number)
-  @IsInt()
+  @ApiProperty({
+    description: 'UTC epoch seconds (exclusive)',
+    example: 1767175200,
+  })
   toSec!: number;
 
-  @IsOptional()
-  @IsEnum(SORT_ORDER)
-  order?: SortOrder;
-
-  @IsOptional()
-  @Type(() => Number)
-  @IsInt()
-  @Min(1)
-  limit?: number;
-
-  @IsOptional()
-  @IsString()
-  cursor?: string;
-
-  @IsOptional()
-  @Type(() => Number)
-  @IsInt()
-  @Min(1)
+  @ApiPropertyOptional({
+    description: 'Page number (1-based). Default = 1',
+    example: 1,
+    default: 1,
+  })
   page?: number;
 
-  @IsOptional()
-  @Type(() => Number)
-  @IsInt()
-  @Min(1)
-  @Max(200)
+  @ApiPropertyOptional({
+    description: 'Page size. Default = 10',
+    example: 10,
+    default: 10,
+  })
   size?: number;
 }
 
 export class AdsStatsQueryDto extends BaseRangeQueryDto {
-  @IsOptional()
-  @IsString()
+  @ApiPropertyOptional({
+    description: 'Ads ID (supported)',
+    example: '694b8d79d8db191891d56ab6',
+  })
+  adsId?: string;
+
+  @ApiPropertyOptional({
+    description: 'Channel ID (ignored if not present in aggregates)',
+    example: 'channel_001',
+  })
   channelId?: string;
 
-  @IsOptional()
-  @IsString()
+  @ApiPropertyOptional({
+    description: 'Ad type (ignored if not present in aggregates)',
+    example: 'BANNER',
+  })
   adType?: string;
-
-  @IsOptional()
-  @IsString()
-  adsId?: string;
 }
 
 export class ChannelStatsQueryDto extends BaseRangeQueryDto {
-  @IsOptional()
-  @IsString()
+  @ApiPropertyOptional({
+    description: 'Channel ID',
+    example: 'channel_001',
+  })
   channelId?: string;
 }

+ 51 - 8
apps/box-stats-api/src/feature/stats-reporting/stats-reporting.controller.ts

@@ -4,28 +4,71 @@ import {
   ChannelStatsQueryDto,
 } from './dto/stats-reporting.dto';
 import { StatsReportingService } from './stats-reporting.service';
+import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
 
+const PaginatedResponseSchema = {
+  type: 'object',
+  properties: {
+    page: { type: 'number', example: 1 },
+    size: { type: 'number', example: 10 },
+    total: { type: 'number', example: 128 },
+    items: {
+      type: 'array',
+      items: { type: 'object' },
+    },
+  },
+};
+
+@ApiTags('Stats Reporting')
 @Controller('stats/reporting')
 export class StatsReportingController {
   constructor(private readonly statsReportingService: StatsReportingService) {}
 
   @Post('ads/hourly')
-  async getAdsHourly(@Body() query: AdsStatsQueryDto) {
-    return this.statsReportingService.getAdsHourly(query);
+  @ApiOperation({ summary: 'Ads hourly stats (aggregated)' })
+  @ApiBody({ type: AdsStatsQueryDto })
+  @ApiResponse({
+    status: 200,
+    description: 'Paginated ads hourly statistics',
+    schema: PaginatedResponseSchema,
+  })
+  async getAdsHourly(@Body() dto: AdsStatsQueryDto) {
+    return this.statsReportingService.getAdsHourly(dto);
   }
 
   @Post('ads/daily')
-  async getAdsDaily(@Body() query: AdsStatsQueryDto) {
-    return this.statsReportingService.getAdsDaily(query);
+  @ApiOperation({ summary: 'Ads daily stats (derived from hourly)' })
+  @ApiBody({ type: AdsStatsQueryDto })
+  @ApiResponse({
+    status: 200,
+    description: 'Paginated ads daily statistics',
+    schema: PaginatedResponseSchema,
+  })
+  async getAdsDaily(@Body() dto: AdsStatsQueryDto) {
+    return this.statsReportingService.getAdsDaily(dto);
   }
 
   @Post('channel/hourly-users')
-  async getChannelHourlyUsers(@Body() query: ChannelStatsQueryDto) {
-    return this.statsReportingService.getChannelHourlyUsers(query);
+  @ApiOperation({ summary: 'Channel hourly user stats' })
+  @ApiBody({ type: ChannelStatsQueryDto })
+  @ApiResponse({
+    status: 200,
+    description: 'Paginated channel hourly user statistics',
+    schema: PaginatedResponseSchema,
+  })
+  async getChannelHourlyUsers(@Body() dto: ChannelStatsQueryDto) {
+    return this.statsReportingService.getChannelHourlyUsers(dto);
   }
 
   @Post('channel/daily-users')
-  async getChannelDailyUsers(@Body() query: ChannelStatsQueryDto) {
-    return this.statsReportingService.getChannelDailyUsers(query);
+  @ApiOperation({ summary: 'Channel daily user stats' })
+  @ApiBody({ type: ChannelStatsQueryDto })
+  @ApiResponse({
+    status: 200,
+    description: 'Paginated channel daily user statistics',
+    schema: PaginatedResponseSchema,
+  })
+  async getChannelDailyUsers(@Body() dto: ChannelStatsQueryDto) {
+    return this.statsReportingService.getChannelDailyUsers(dto);
   }
 }

+ 32 - 32
apps/box-stats-api/src/feature/stats-reporting/stats-reporting.service.ts

@@ -44,15 +44,15 @@ const MAX_SIZE = 200;
 export class StatsReportingService {
   constructor(private readonly prisma: PrismaMongoService) {}
 
-  async getAdsHourly(query: AdsStatsQueryDto): Promise<any> {
-    assertRange(query.fromSec, query.toSec);
+  async getAdsHourly(dto: AdsStatsQueryDto): Promise<any> {
+    assertRange(dto.fromSec, dto.toSec);
     const { page, size, skip, take } = this.normalizePagination({
-      page: query.page,
-      size: query.size,
+      page: dto.page,
+      size: dto.size,
     });
 
-    const fromAligned = alignHourStart(query.fromSec);
-    const toAligned = alignHourStart(query.toSec);
+    const fromAligned = alignHourStart(dto.fromSec);
+    const toAligned = alignHourStart(dto.toSec);
 
     const where: Prisma.AdsHourlyStatsWhereInput = {
       hourStartAt: {
@@ -61,8 +61,8 @@ export class StatsReportingService {
       },
     };
 
-    if (query.adsId) {
-      where.adsId = query.adsId;
+    if (dto.adsId) {
+      where.adsId = dto.adsId;
     }
 
     const [items, total] = await Promise.all([
@@ -78,15 +78,15 @@ export class StatsReportingService {
     return { page, size, total, items };
   }
 
-  async getAdsDaily(query: AdsStatsQueryDto): Promise<any> {
-    assertRange(query.fromSec, query.toSec);
+  async getAdsDaily(dto: AdsStatsQueryDto): Promise<any> {
+    assertRange(dto.fromSec, dto.toSec);
     const { page, size, skip, take } = this.normalizePagination({
-      page: query.page,
-      size: query.size,
+      page: dto.page,
+      size: dto.size,
     });
 
-    const fromAligned = alignDayStart(query.fromSec);
-    const toAligned = alignDayStart(query.toSec);
+    const fromAligned = alignDayStart(dto.fromSec);
+    const toAligned = alignDayStart(dto.toSec);
 
     const where: Prisma.AdsDailyStatsWhereInput = {
       dayStartAt: {
@@ -95,8 +95,8 @@ export class StatsReportingService {
       },
     };
 
-    if (query.adsId) {
-      where.adsId = query.adsId;
+    if (dto.adsId) {
+      where.adsId = dto.adsId;
     }
 
     const [items, total] = await Promise.all([
@@ -112,15 +112,15 @@ export class StatsReportingService {
     return { page, size, total, items };
   }
 
-  async getChannelHourlyUsers(query: ChannelStatsQueryDto): Promise<any> {
-    assertRange(query.fromSec, query.toSec);
+  async getChannelHourlyUsers(dto: ChannelStatsQueryDto): Promise<any> {
+    assertRange(dto.fromSec, dto.toSec);
     const { page, size, skip, take } = this.normalizePagination({
-      page: query.page,
-      size: query.size,
+      page: dto.page,
+      size: dto.size,
     });
 
-    const fromAligned = alignHourStart(query.fromSec);
-    const toAligned = alignHourStart(query.toSec);
+    const fromAligned = alignHourStart(dto.fromSec);
+    const toAligned = alignHourStart(dto.toSec);
 
     const where: Prisma.ChannelHourlyUserStatsWhereInput = {
       hourStartAt: {
@@ -129,8 +129,8 @@ export class StatsReportingService {
       },
     };
 
-    if (query.channelId) {
-      where.channelId = query.channelId;
+    if (dto.channelId) {
+      where.channelId = dto.channelId;
     }
 
     const [items, total] = await Promise.all([
@@ -146,15 +146,15 @@ export class StatsReportingService {
     return { page, size, total, items };
   }
 
-  async getChannelDailyUsers(query: ChannelStatsQueryDto): Promise<any> {
-    assertRange(query.fromSec, query.toSec);
+  async getChannelDailyUsers(dto: ChannelStatsQueryDto): Promise<any> {
+    assertRange(dto.fromSec, dto.toSec);
     const { page, size, skip, take } = this.normalizePagination({
-      page: query.page,
-      size: query.size,
+      page: dto.page,
+      size: dto.size,
     });
 
-    const fromAligned = alignDayStart(query.fromSec);
-    const toAligned = alignDayStart(query.toSec);
+    const fromAligned = alignDayStart(dto.fromSec);
+    const toAligned = alignDayStart(dto.toSec);
 
     const where: Prisma.ChannelDailyUserStatsWhereInput = {
       dayStartAt: {
@@ -163,8 +163,8 @@ export class StatsReportingService {
       },
     };
 
-    if (query.channelId) {
-      where.channelId = query.channelId;
+    if (dto.channelId) {
+      where.channelId = dto.channelId;
     }
 
     const [items, total] = await Promise.all([