diff --git a/src/app.module.ts b/src/app.module.ts index 0511ae8..0f861fb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { SourcesModule } from "./sources/sources.module"; import { UsersModule } from "./users/users.module"; import { ConfigModule } from "./config/config.module"; import { HealthCheckModule } from "./health-check/health-check.module"; +import { ReportsModule } from "./reports/reports.module"; @Module({ imports: [ @@ -28,6 +29,7 @@ import { HealthCheckModule } from "./health-check/health-check.module"; MusicLibraryModule, ListensModule, HealthCheckModule, + ReportsModule, ], }) export class AppModule {} diff --git a/src/reports/dto/get-listen-report.dto.ts b/src/reports/dto/get-listen-report.dto.ts new file mode 100644 index 0000000..96c8f39 --- /dev/null +++ b/src/reports/dto/get-listen-report.dto.ts @@ -0,0 +1,16 @@ +import { IsEnum, IsISO8601 } from "class-validator"; +import { User } from "../../users/user.entity"; +import { Timeframe } from "../timeframe.enum"; + +export class GetListenReportDto { + user: User; + + @IsEnum(Timeframe) + timeFrame: Timeframe; + + @IsISO8601() + timeStart: string; + + @IsISO8601() + timeEnd: string; +} diff --git a/src/reports/dto/listen-report.dto.ts b/src/reports/dto/listen-report.dto.ts new file mode 100644 index 0000000..b6f522c --- /dev/null +++ b/src/reports/dto/listen-report.dto.ts @@ -0,0 +1,14 @@ +import { Timeframe } from "../timeframe.enum"; + +export class ListenReportDto { + items: { + date: string; + count: number; + }[]; + + timeFrame: Timeframe; + + timeStart: string; + + timeEnd: string; +} diff --git a/src/reports/reports.controller.spec.ts b/src/reports/reports.controller.spec.ts new file mode 100644 index 0000000..0d0b3c7 --- /dev/null +++ b/src/reports/reports.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReportsController } from './reports.controller'; + +describe('Reports Controller', () => { + let controller: ReportsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ReportsController], + }).compile(); + + controller = module.get(ReportsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/reports/reports.controller.ts b/src/reports/reports.controller.ts new file mode 100644 index 0000000..3c7cf04 --- /dev/null +++ b/src/reports/reports.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Get, Query } from "@nestjs/common"; +import { Auth } from "src/auth/decorators/auth.decorator"; +import { ReqUser } from "../auth/decorators/req-user.decorator"; +import { User } from "../users/user.entity"; +import { GetListenReportDto } from "./dto/get-listen-report.dto"; +import { ListenReportDto } from "./dto/listen-report.dto"; +import { ReportsService } from "./reports.service"; + +@Controller("api/v1/reports") +export class ReportsController { + constructor(private readonly reportsService: ReportsService) {} + + @Get("listens") + @Auth() + async getListens( + @Query() options: Omit, + @ReqUser() user: User + ): Promise { + return this.reportsService.getListens({ ...options, user }); + } +} diff --git a/src/reports/reports.module.ts b/src/reports/reports.module.ts new file mode 100644 index 0000000..3d11814 --- /dev/null +++ b/src/reports/reports.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { ReportsService } from "./reports.service"; +import { ReportsController } from "./reports.controller"; +import { ListensModule } from "src/listens/listens.module"; + +@Module({ + imports: [ListensModule], + providers: [ReportsService], + controllers: [ReportsController], +}) +export class ReportsModule {} diff --git a/src/reports/reports.service.spec.ts b/src/reports/reports.service.spec.ts new file mode 100644 index 0000000..79b4fa0 --- /dev/null +++ b/src/reports/reports.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReportsService } from './reports.service'; + +describe('ReportsService', () => { + let service: ReportsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ReportsService], + }).compile(); + + service = module.get(ReportsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/reports/reports.service.ts b/src/reports/reports.service.ts new file mode 100644 index 0000000..a183255 --- /dev/null +++ b/src/reports/reports.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from "@nestjs/common"; +import { + eachDayOfInterval, + eachMonthOfInterval, + eachWeekOfInterval, + eachYearOfInterval, + formatISO, + Interval, + isSameDay, + isSameMonth, + isSameWeek, + isSameYear, + parseISO, +} from "date-fns"; +import { ListensService } from "../listens/listens.service"; +import { GetListenReportDto } from "./dto/get-listen-report.dto"; +import { ListenReportDto } from "./dto/listen-report.dto"; +import { Timeframe } from "./timeframe.enum"; + +const timeframeToDateFns: { + [x in Timeframe]: { + eachOfInterval: (interval: Interval) => Date[]; + isSame: (dateLeft: Date, dateRight: Date) => boolean; + }; +} = { + [Timeframe.Day]: { + eachOfInterval: eachDayOfInterval, + isSame: isSameDay, + }, + [Timeframe.Week]: { + eachOfInterval: eachWeekOfInterval, + isSame: isSameWeek, + }, + [Timeframe.Month]: { + eachOfInterval: eachMonthOfInterval, + isSame: isSameMonth, + }, + [Timeframe.Year]: { + eachOfInterval: eachYearOfInterval, + isSame: isSameYear, + }, +}; + +@Injectable() +export class ReportsService { + constructor(private readonly listensService: ListensService) {} + + async getListens(options: GetListenReportDto): Promise { + const { user, timeFrame, timeStart, timeEnd } = options; + + const { items: listens } = await this.listensService.getListens({ + user, + filter: { time: { start: timeStart, end: timeEnd } }, + page: 1, + limit: 10000000, + }); + + const reportInterval: Interval = { + start: parseISO(timeStart), + end: parseISO(timeEnd), + }; + + const { eachOfInterval, isSame } = timeframeToDateFns[timeFrame]; + + const reportItems = eachOfInterval(reportInterval).map((date) => { + const count = listens.filter((listen) => isSame(date, listen.playedAt)) + .length; + return { date: formatISO(date), count }; + }); + + return { items: reportItems, timeStart, timeEnd, timeFrame }; + } +} diff --git a/src/reports/timeframe.enum.ts b/src/reports/timeframe.enum.ts new file mode 100644 index 0000000..65293f4 --- /dev/null +++ b/src/reports/timeframe.enum.ts @@ -0,0 +1,6 @@ +export enum Timeframe { + Day = "day", + Week = "week", + Month = "month", + Year = "year", +}