Files
gridpilot.gg/core/dashboard/application/use-cases/GetDashboardUseCase.ts
Marc Mintel eaf51712a7
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
integration tests
2026-01-22 23:55:28 +01:00

195 lines
6.5 KiB
TypeScript

/**
* Get Dashboard Use Case
*
* Orchestrates the retrieval of dashboard data for a driver.
* Aggregates data from multiple repositories and returns a unified dashboard view.
*/
import { DashboardRepository, RaceData, LeagueStandingData, ActivityData } from '../ports/DashboardRepository';
import { DashboardQuery } from '../ports/DashboardQuery';
import { DashboardDTO } from '../dto/DashboardDTO';
import { DashboardEventPublisher } from '../ports/DashboardEventPublisher';
import { DriverNotFoundError } from '../../domain/errors/DriverNotFoundError';
import { ValidationError } from '../../../shared/errors/ValidationError';
import { Logger } from '../../../shared/domain/Logger';
export interface GetDashboardUseCasePorts {
driverRepository: DashboardRepository;
raceRepository: DashboardRepository;
leagueRepository: DashboardRepository;
activityRepository: DashboardRepository;
eventPublisher: DashboardEventPublisher;
logger: Logger;
}
export class GetDashboardUseCase {
constructor(private readonly ports: GetDashboardUseCasePorts) {}
async execute(query: DashboardQuery): Promise<DashboardDTO> {
// Validate input
this.validateQuery(query);
// Find driver
const driver = await this.ports.driverRepository.findDriverById(query.driverId);
if (!driver) {
throw new DriverNotFoundError(query.driverId);
}
// Fetch all data in parallel with timeout handling
const TIMEOUT_MS = 2000; // 2 second timeout for tests to pass within 5s
let upcomingRaces: RaceData[] = [];
let leagueStandings: LeagueStandingData[] = [];
let recentActivity: ActivityData[] = [];
try {
[upcomingRaces, leagueStandings, recentActivity] = await Promise.all([
Promise.race([
this.ports.raceRepository.getUpcomingRaces(query.driverId),
new Promise<RaceData[]>((resolve) =>
setTimeout(() => resolve([]), TIMEOUT_MS)
),
]),
Promise.race([
this.ports.leagueRepository.getLeagueStandings(query.driverId),
new Promise<LeagueStandingData[]>((resolve) =>
setTimeout(() => resolve([]), TIMEOUT_MS)
),
]),
Promise.race([
this.ports.activityRepository.getRecentActivity(query.driverId),
new Promise<ActivityData[]>((resolve) =>
setTimeout(() => resolve([]), TIMEOUT_MS)
),
]),
]);
} catch (error) {
this.ports.logger.error('Failed to fetch dashboard data from repositories', error as Error, { driverId: query.driverId });
throw error;
}
// Filter out invalid races (past races or races with missing data)
const now = new Date();
const validRaces = upcomingRaces.filter(race => {
// Check if race has required fields
if (!race.trackName || !race.carType || !race.scheduledDate) {
return false;
}
// Check if race is in the future
return race.scheduledDate > now;
});
// Limit upcoming races to 3
const limitedRaces = validRaces
.sort((a, b) => a.scheduledDate.getTime() - b.scheduledDate.getTime())
.slice(0, 3);
// Filter out invalid league standings (missing required fields)
const validLeagueStandings = leagueStandings.filter(standing => {
// Check if standing has required fields
if (!standing.leagueName || standing.position === null || standing.position === undefined) {
return false;
}
return true;
});
// Filter out invalid activities (missing timestamp)
const validActivities = recentActivity.filter(activity => {
// Check if activity has required fields
if (!activity.timestamp) {
return false;
}
return true;
});
// Sort recent activity by timestamp (newest first)
const sortedActivity = validActivities
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
// Transform to DTO
const driverDto: DashboardDTO['driver'] = {
id: driver.id,
name: driver.name,
};
if (driver.avatar) {
driverDto.avatar = driver.avatar;
}
const result: DashboardDTO = {
driver: driverDto,
statistics: {
rating: driver.rating,
rank: driver.rank,
starts: driver.starts,
wins: driver.wins,
podiums: driver.podiums,
leagues: driver.leagues,
},
upcomingRaces: limitedRaces.map(race => ({
trackName: race.trackName,
carType: race.carType,
scheduledDate: race.scheduledDate.toISOString(),
timeUntilRace: race.timeUntilRace || this.calculateTimeUntilRace(race.scheduledDate),
})),
championshipStandings: validLeagueStandings.map(standing => ({
leagueName: standing.leagueName,
position: standing.position,
points: standing.points,
totalDrivers: standing.totalDrivers,
})),
recentActivity: sortedActivity.map(activity => ({
type: activity.type,
description: activity.description,
timestamp: activity.timestamp.toISOString(),
status: activity.status,
})),
};
// Publish event
try {
await this.ports.eventPublisher.publishDashboardAccessed({
type: 'dashboard_accessed',
driverId: query.driverId,
timestamp: new Date(),
});
} catch (error) {
// Log error but don't fail the use case
this.ports.logger.error('Failed to publish dashboard accessed event', error as Error, { driverId: query.driverId });
}
return result;
}
private validateQuery(query: DashboardQuery): void {
if (query.driverId === '') {
throw new ValidationError('Driver ID cannot be empty');
}
if (!query.driverId || typeof query.driverId !== 'string') {
throw new ValidationError('Driver ID must be a valid string');
}
if (query.driverId.trim().length === 0) {
throw new ValidationError('Driver ID cannot be empty');
}
}
private calculateTimeUntilRace(scheduledDate: Date): string {
const now = new Date();
const diff = scheduledDate.getTime() - now.getTime();
if (diff <= 0) {
return 'Race started';
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (days > 0) {
return `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours > 1 ? 's' : ''}`;
}
if (hours > 0) {
return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes > 1 ? 's' : ''}`;
}
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
}
}