From b445d6dd3781522d7d9ddfdea802b20a32da4559 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 22 Dec 2025 00:10:57 +0100 Subject: [PATCH] refactor dashboard module --- .../domain/dashboard/DashboardProviders.ts | 5 -- .../src/domain/dashboard/DashboardService.ts | 9 ++- .../DashboardOverviewPresenter.test.ts | 3 +- .../presenters/DashboardOverviewPresenter.ts | 50 ++++++++-------- .../DashboardOverviewUseCase.test.ts | 58 ++++--------------- .../use-cases/DashboardOverviewUseCase.ts | 7 +-- 6 files changed, 46 insertions(+), 86 deletions(-) diff --git a/apps/api/src/domain/dashboard/DashboardProviders.ts b/apps/api/src/domain/dashboard/DashboardProviders.ts index 2ad26d47a..e65cc90ff 100644 --- a/apps/api/src/domain/dashboard/DashboardProviders.ts +++ b/apps/api/src/domain/dashboard/DashboardProviders.ts @@ -2,8 +2,6 @@ import { Provider } from '@nestjs/common'; // Import core interfaces import type { Logger } from '@core/shared/application/Logger'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -import type { DashboardOverviewResult } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; @@ -141,7 +139,6 @@ export const DashboardProviders: Provider[] = [ feedRepo: IFeedRepository, socialRepo: ISocialGraphRepository, imageService: ImageServicePort, - output: UseCaseOutputPort, ) => new DashboardOverviewUseCase( driverRepo, @@ -155,7 +152,6 @@ export const DashboardProviders: Provider[] = [ socialRepo, async (driverId: string) => imageService.getDriverAvatar(driverId), () => null, - output, ), inject: [ DRIVER_REPOSITORY_TOKEN, @@ -168,7 +164,6 @@ export const DashboardProviders: Provider[] = [ FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, - DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, ], }, ]; \ No newline at end of file diff --git a/apps/api/src/domain/dashboard/DashboardService.ts b/apps/api/src/domain/dashboard/DashboardService.ts index 549968e8a..26bf02a16 100644 --- a/apps/api/src/domain/dashboard/DashboardService.ts +++ b/apps/api/src/domain/dashboard/DashboardService.ts @@ -7,14 +7,15 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen import type { Logger } from '@core/shared/application/Logger'; // Tokens -import { DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN } from './DashboardProviders'; +import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN } from './DashboardProviders'; @Injectable() export class DashboardService { + private readonly presenter = new DashboardOverviewPresenter(); + constructor( @Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase, - @Inject(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN) private readonly dashboardOverviewPresenter: DashboardOverviewPresenter, // TODO no presenter injection ) {} async getDashboardOverview(driverId: string): Promise { @@ -26,6 +27,8 @@ export class DashboardService { throw new Error(result.unwrapErr().details?.message ?? 'Failed to get dashboard overview'); } - return this.dashboardOverviewPresenter.getResponseModel(); + this.presenter.present(result); + + return this.presenter.getResponseModel(); } } \ No newline at end of file diff --git a/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.test.ts b/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.test.ts index 3559e36d2..1c95cc4e3 100644 --- a/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.test.ts +++ b/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.test.ts @@ -5,6 +5,7 @@ import { Race } from '@core/racing/domain/entities/Race'; import { Standing } from '@core/racing/domain/entities/Standing'; import { Result as RaceResult } from '@core/racing/domain/entities/result/Result'; import type { FeedItem } from '@core/social/domain/types/FeedItem'; +import { Result } from '@core/shared/application/Result'; import { beforeEach, describe, expect, it } from 'vitest'; import { DashboardOverviewPresenter } from './DashboardOverviewPresenter'; @@ -143,7 +144,7 @@ describe('DashboardOverviewPresenter', () => { it('maps DashboardOverviewResult to DashboardOverviewDTO correctly', () => { const output = createOutput(); - presenter.present(output); + presenter.present(Result.ok(output)); const dto = presenter.getResponseModel(); expect(dto.activeLeaguesCount).toBe(2); diff --git a/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.ts b/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.ts index 5fe0a45f9..f5f481d9e 100644 --- a/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.ts +++ b/apps/api/src/domain/dashboard/presenters/DashboardOverviewPresenter.ts @@ -1,4 +1,4 @@ -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import { Result } from '@core/shared/application/Result'; import type { DashboardOverviewResult, } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; @@ -13,22 +13,24 @@ import { DashboardFriendSummaryDTO, } from '../dtos/DashboardOverviewDTO'; -export class DashboardOverviewPresenter implements UseCaseOutputPort { +export class DashboardOverviewPresenter { private responseModel: DashboardOverviewDTO | null = null; - present(result: DashboardOverviewResult): void { - const currentDriver: DashboardDriverSummaryDTO | null = result.currentDriver + present(result: Result): void { + const data = result.unwrap(); + + const currentDriver: DashboardDriverSummaryDTO | null = data.currentDriver ? { - id: result.currentDriver.driver.id, - name: String(result.currentDriver.driver.name), - country: String(result.currentDriver.driver.country), - avatarUrl: result.currentDriver.avatarUrl, - rating: result.currentDriver.rating, - globalRank: result.currentDriver.globalRank, - totalRaces: result.currentDriver.totalRaces, - wins: result.currentDriver.wins, - podiums: result.currentDriver.podiums, - consistency: result.currentDriver.consistency, + id: data.currentDriver.driver.id, + name: String(data.currentDriver.driver.name), + country: String(data.currentDriver.driver.country), + avatarUrl: data.currentDriver.avatarUrl, + rating: data.currentDriver.rating, + globalRank: data.currentDriver.globalRank, + totalRaces: data.currentDriver.totalRaces, + wins: data.currentDriver.wins, + podiums: data.currentDriver.podiums, + consistency: data.currentDriver.consistency, } : null; @@ -43,13 +45,13 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort ({ + const recentResults: DashboardRecentResultDTO[] = data.recentResults.map(resultSummary => ({ raceId: resultSummary.race.id, raceName: String(resultSummary.race.track), leagueId: resultSummary.league?.id ? String(resultSummary.league.id) : null, @@ -60,7 +62,7 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort ({ + data.leagueStandingsSummaries.map(standing => ({ leagueId: String(standing.league.id), leagueName: String(standing.league.name), position: standing.standing?.position ? Number(standing.standing.position) : null, @@ -68,7 +70,7 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort ({ + const feedItems: DashboardFeedItemSummaryDTO[] = data.feedSummary.items.map(item => ({ id: item.id, type: String(item.type), headline: item.headline, @@ -79,11 +81,11 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort ({ + const friends: DashboardFriendSummaryDTO[] = data.friends.map(friend => ({ id: friend.driver.id, name: String(friend.driver.name), country: String(friend.driver.country), @@ -95,7 +97,7 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort { it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => { - const output: UseCaseOutputPort = { - present: vi.fn(), - }; - const driverId = 'driver-1'; const driver = Driver.create({ id: driverId, iracingId: '12345', name: 'Alice Racer', country: 'US' }); @@ -270,21 +266,17 @@ describe('DashboardOverviewUseCase', () => { socialRepository, getDriverAvatar, getDriverStats, - output, ); const input: DashboardOverviewInput = { driverId }; const result: UseCaseResult< - void, + DashboardOverviewResult, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }> > = await useCase.execute(input); expect(result.isOk()).toBe(true); - expect(result.unwrap()).toBeUndefined(); - - expect(output.present).toHaveBeenCalledTimes(1); - const vm = (output.present as any).mock.calls[0][0] as DashboardOverviewResult; + const vm = result.unwrap(); expect(vm.myUpcomingRaces.map(r => r.race.id)).toEqual(['race-1', 'race-3']); @@ -295,10 +287,6 @@ describe('DashboardOverviewUseCase', () => { }); it('builds recentResults sorted by date descending and leagueStandingsSummaries from standings', async () => { - const output: UseCaseOutputPort = { - present: vi.fn(), - }; - const driverId = 'driver-2'; const driver = Driver.create({ id: driverId, iracingId: '67890', name: 'Result Driver', country: 'DE' }); @@ -551,21 +539,17 @@ describe('DashboardOverviewUseCase', () => { socialRepository, getDriverAvatar, getDriverStats, - output, ); const input: DashboardOverviewInput = { driverId }; const result: UseCaseResult< - void, + DashboardOverviewResult, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }> > = await useCase.execute(input); expect(result.isOk()).toBe(true); - expect(result.unwrap()).toBeUndefined(); - - expect(output.present).toHaveBeenCalledTimes(1); - const vm = (output.present as any).mock.calls[0][0] as DashboardOverviewResult; + const vm = result.unwrap(); expect(vm.recentResults.length).toBe(2); expect(vm.recentResults[0]!.race.id).toBe('race-new'); @@ -590,10 +574,6 @@ describe('DashboardOverviewUseCase', () => { }); it('returns empty collections and safe defaults when driver has no races or standings', async () => { - const output: UseCaseOutputPort = { - present: vi.fn(), - }; - const driverId = 'driver-empty'; const driver = Driver.create({ id: driverId, iracingId: '11111', name: 'New Racer', country: 'FR' }); @@ -758,21 +738,17 @@ describe('DashboardOverviewUseCase', () => { socialRepository, getDriverAvatar, getDriverStats, - output, ); const input: DashboardOverviewInput = { driverId }; const result: UseCaseResult< - void, + DashboardOverviewResult, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }> > = await useCase.execute(input); expect(result.isOk()).toBe(true); - expect(result.unwrap()).toBeUndefined(); - - expect(output.present).toHaveBeenCalledTimes(1); - const vm = (output.present as any).mock.calls[0][0] as DashboardOverviewResult; + const vm = result.unwrap(); expect(vm.myUpcomingRaces).toEqual([]); expect(vm.otherUpcomingRaces).toEqual([]); @@ -784,11 +760,7 @@ describe('DashboardOverviewUseCase', () => { expect(vm.feedSummary.items).toEqual([]); }); - it('returns DRIVER_NOT_FOUND error and does not present when driver is missing', async () => { - const output: UseCaseOutputPort = { - present: vi.fn(), - }; - + it('returns DRIVER_NOT_FOUND error when driver is missing', async () => { const driverId = 'missing-driver'; const driverRepository = { @@ -951,13 +923,12 @@ describe('DashboardOverviewUseCase', () => { socialRepository, getDriverAvatar, getDriverStats, - output, ); const input: DashboardOverviewInput = { driverId }; const result: UseCaseResult< - void, + DashboardOverviewResult, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }> > = await useCase.execute(input); @@ -965,15 +936,9 @@ describe('DashboardOverviewUseCase', () => { const err = result.unwrapErr(); expect(err.code).toBe('DRIVER_NOT_FOUND'); expect(err.details?.message).toBe('Driver not found'); - - expect(output.present).not.toHaveBeenCalled(); }); - it('returns REPOSITORY_ERROR when an unexpected error occurs and does not present', async () => { - const output: UseCaseOutputPort = { - present: vi.fn(), - }; - + it('returns REPOSITORY_ERROR when an unexpected error occurs', async () => { const driverId = 'driver-error'; const driver = Driver.create({ id: driverId, iracingId: '99999', name: 'Error Driver', country: 'GB' }); @@ -1140,13 +1105,12 @@ describe('DashboardOverviewUseCase', () => { socialRepository, getDriverAvatar, getDriverStats, - output, ); const input: DashboardOverviewInput = { driverId }; const result: UseCaseResult< - void, + DashboardOverviewResult, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }> > = await useCase.execute(input); @@ -1154,7 +1118,5 @@ describe('DashboardOverviewUseCase', () => { const err = result.unwrapErr(); expect(err.code).toBe('REPOSITORY_ERROR'); expect(err.details?.message).toBe('DB failure'); - - expect(output.present).not.toHaveBeenCalled(); }); }); diff --git a/core/racing/application/use-cases/DashboardOverviewUseCase.ts b/core/racing/application/use-cases/DashboardOverviewUseCase.ts index a04009bf2..99d00795c 100644 --- a/core/racing/application/use-cases/DashboardOverviewUseCase.ts +++ b/core/racing/application/use-cases/DashboardOverviewUseCase.ts @@ -97,14 +97,13 @@ export class DashboardOverviewUseCase { private readonly getDriverStats: ( driverId: string, ) => DashboardDriverStatsAdapter | null, - private readonly output: UseCaseOutputPort, ) {} async execute( input: DashboardOverviewInput, ): Promise< Result< - void, + DashboardOverviewResult, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }> > > { @@ -209,9 +208,7 @@ export class DashboardOverviewUseCase { friends: friendsSummary, }; - this.output.present(result); - - return Result.ok(undefined); + return Result.ok(result); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR',