import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/mongo-stats/client'; import type { AdsDailyStats, AdsHourlyStats, ChannelDailyUserStats, ChannelHourlyUserStats, } from '@prisma/mongo-stats/client'; import { PrismaMongoService } from '../../prisma/prisma-mongo.service'; import { ChannelStatsQueryDto, AdsStatsQueryDto, } from './dto/stats-reporting.dto'; import { assertRange, alignDayStart, alignHourStart, } from './stats-reporting.time'; interface PaginatedResult { page: number; size: number; total: number; items: T[]; } interface PaginationParams { page?: number; size?: number; } interface NormalizedPagination { page: number; size: number; skip: number; take: number; } const DEFAULT_PAGE = 1; const DEFAULT_SIZE = 10; const MAX_SIZE = 200; @Injectable() export class StatsReportingService { constructor(private readonly prisma: PrismaMongoService) {} async getAdsHourly(query: AdsStatsQueryDto): Promise { assertRange(query.fromSec, query.toSec); const { page, size, skip, take } = this.normalizePagination({ page: query.page, size: query.size, }); const fromAligned = alignHourStart(query.fromSec); const toAligned = alignHourStart(query.toSec); const where: Prisma.AdsHourlyStatsWhereInput = { hourStartAt: { gte: BigInt(fromAligned), lt: BigInt(toAligned), }, }; if (query.adsId) { where.adsId = query.adsId; } const [items, total] = await Promise.all([ this.prisma.adsHourlyStats.findMany({ where, orderBy: [{ hourStartAt: 'desc' }, { id: 'desc' }], skip, take, }), this.prisma.adsHourlyStats.count({ where }), ]); return { page, size, total, items }; } async getAdsDaily(query: AdsStatsQueryDto): Promise { assertRange(query.fromSec, query.toSec); const { page, size, skip, take } = this.normalizePagination({ page: query.page, size: query.size, }); const fromAligned = alignDayStart(query.fromSec); const toAligned = alignDayStart(query.toSec); const where: Prisma.AdsDailyStatsWhereInput = { dayStartAt: { gte: BigInt(fromAligned), lt: BigInt(toAligned), }, }; if (query.adsId) { where.adsId = query.adsId; } const [items, total] = await Promise.all([ this.prisma.adsDailyStats.findMany({ where, orderBy: [{ dayStartAt: 'desc' }, { id: 'desc' }], skip, take, }), this.prisma.adsDailyStats.count({ where }), ]); return { page, size, total, items }; } async getChannelHourlyUsers(query: ChannelStatsQueryDto): Promise { assertRange(query.fromSec, query.toSec); const { page, size, skip, take } = this.normalizePagination({ page: query.page, size: query.size, }); const fromAligned = alignHourStart(query.fromSec); const toAligned = alignHourStart(query.toSec); const where: Prisma.ChannelHourlyUserStatsWhereInput = { hourStartAt: { gte: BigInt(fromAligned), lt: BigInt(toAligned), }, }; if (query.channelId) { where.channelId = query.channelId; } const [items, total] = await Promise.all([ this.prisma.channelHourlyUserStats.findMany({ where, orderBy: [{ hourStartAt: 'desc' }, { id: 'desc' }], skip, take, }), this.prisma.channelHourlyUserStats.count({ where }), ]); return { page, size, total, items }; } async getChannelDailyUsers(query: ChannelStatsQueryDto): Promise { assertRange(query.fromSec, query.toSec); const { page, size, skip, take } = this.normalizePagination({ page: query.page, size: query.size, }); const fromAligned = alignDayStart(query.fromSec); const toAligned = alignDayStart(query.toSec); const where: Prisma.ChannelDailyUserStatsWhereInput = { dayStartAt: { gte: BigInt(fromAligned), lt: BigInt(toAligned), }, }; if (query.channelId) { where.channelId = query.channelId; } const [items, total] = await Promise.all([ this.prisma.channelDailyUserStats.findMany({ where, orderBy: [{ dayStartAt: 'desc' }, { id: 'desc' }], skip, take, }), this.prisma.channelDailyUserStats.count({ where }), ]); return { page, size, total, items }; } private normalizePagination({ page, size, }: PaginationParams = {}): NormalizedPagination { const normalizedPage = this.normalizePage(page); const normalizedSize = this.normalizeSize(size); const skip = Math.max(0, (normalizedPage - 1) * normalizedSize); return { page: normalizedPage, size: normalizedSize, skip, take: normalizedSize, }; } private normalizePage(value?: number) { if (!Number.isFinite(value ?? NaN)) { return DEFAULT_PAGE; } return Math.max(DEFAULT_PAGE, Math.floor(value)); } private normalizeSize(value?: number) { if (!Number.isFinite(value ?? NaN)) { return DEFAULT_SIZE; } const normalized = Math.floor(value); if (normalized < 1) { return DEFAULT_SIZE; } return Math.min(MAX_SIZE, normalized); } }