164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
/**
|
|
* Get Driver Rankings Use Case
|
|
*
|
|
* Orchestrates the retrieval of driver rankings data.
|
|
* Aggregates data from repositories and returns drivers with search, filter, and sort capabilities.
|
|
*/
|
|
|
|
import { LeaderboardsRepository } from '../ports/LeaderboardsRepository';
|
|
import { LeaderboardsEventPublisher } from '../ports/LeaderboardsEventPublisher';
|
|
import {
|
|
DriverRankingsQuery,
|
|
DriverRankingsResult,
|
|
DriverRankingEntry,
|
|
PaginationMetadata,
|
|
} from '../ports/DriverRankingsQuery';
|
|
import { ValidationError } from '../../../shared/errors/ValidationError';
|
|
|
|
export interface GetDriverRankingsUseCasePorts {
|
|
leaderboardsRepository: LeaderboardsRepository;
|
|
eventPublisher: LeaderboardsEventPublisher;
|
|
}
|
|
|
|
export class GetDriverRankingsUseCase {
|
|
constructor(private readonly ports: GetDriverRankingsUseCasePorts) {}
|
|
|
|
async execute(query: DriverRankingsQuery = {}): Promise<DriverRankingsResult> {
|
|
try {
|
|
// Validate query parameters
|
|
this.validateQuery(query);
|
|
|
|
const page = query.page ?? 1;
|
|
const limit = query.limit ?? 20;
|
|
|
|
// Fetch all drivers
|
|
const allDrivers = await this.ports.leaderboardsRepository.findAllDrivers();
|
|
|
|
// Apply search filter
|
|
let filteredDrivers = allDrivers;
|
|
if (query.search) {
|
|
const searchLower = query.search.toLowerCase();
|
|
filteredDrivers = filteredDrivers.filter((driver) =>
|
|
driver.name.toLowerCase().includes(searchLower),
|
|
);
|
|
}
|
|
|
|
// Apply rating filter
|
|
if (query.minRating !== undefined) {
|
|
filteredDrivers = filteredDrivers.filter(
|
|
(driver) => driver.rating >= query.minRating!,
|
|
);
|
|
}
|
|
|
|
// Apply team filter
|
|
if (query.teamId) {
|
|
filteredDrivers = filteredDrivers.filter(
|
|
(driver) => driver.teamId === query.teamId,
|
|
);
|
|
}
|
|
|
|
// Sort drivers
|
|
const sortBy = query.sortBy ?? 'rating';
|
|
const sortOrder = query.sortOrder ?? 'desc';
|
|
|
|
filteredDrivers.sort((a, b) => {
|
|
let comparison = 0;
|
|
|
|
switch (sortBy) {
|
|
case 'rating':
|
|
comparison = a.rating - b.rating;
|
|
break;
|
|
case 'name':
|
|
comparison = a.name.localeCompare(b.name);
|
|
break;
|
|
case 'rank':
|
|
comparison = 0;
|
|
break;
|
|
case 'raceCount':
|
|
comparison = a.raceCount - b.raceCount;
|
|
break;
|
|
}
|
|
|
|
// If primary sort is equal, always use name ASC as secondary sort
|
|
if (comparison === 0 && sortBy !== 'name') {
|
|
comparison = a.name.localeCompare(b.name);
|
|
// Secondary sort should not be affected by sortOrder of primary field?
|
|
// Actually, usually secondary sort is always ASC or follows primary.
|
|
// Let's keep it simple: if primary is equal, use name ASC.
|
|
return comparison;
|
|
}
|
|
|
|
return sortOrder === 'asc' ? comparison : -comparison;
|
|
});
|
|
|
|
// Calculate pagination
|
|
const total = filteredDrivers.length;
|
|
const totalPages = Math.ceil(total / limit);
|
|
const startIndex = (page - 1) * limit;
|
|
const endIndex = Math.min(startIndex + limit, total);
|
|
|
|
// Get paginated drivers
|
|
const paginatedDrivers = filteredDrivers.slice(startIndex, endIndex);
|
|
|
|
// Map to ranking entries with rank
|
|
const driverEntries: DriverRankingEntry[] = paginatedDrivers.map(
|
|
(driver, index): DriverRankingEntry => ({
|
|
rank: startIndex + index + 1,
|
|
id: driver.id,
|
|
name: driver.name,
|
|
rating: driver.rating,
|
|
...(driver.teamId !== undefined && { teamId: driver.teamId }),
|
|
...(driver.teamName !== undefined && { teamName: driver.teamName }),
|
|
raceCount: driver.raceCount,
|
|
}),
|
|
);
|
|
|
|
// Publish event
|
|
await this.ports.eventPublisher.publishDriverRankingsAccessed({
|
|
type: 'driver_rankings_accessed',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
return {
|
|
drivers: driverEntries,
|
|
pagination: {
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
// Publish error event
|
|
await this.ports.eventPublisher.publishLeaderboardsError({
|
|
type: 'leaderboards_error',
|
|
error: error instanceof Error ? error.message : String(error),
|
|
timestamp: new Date(),
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private validateQuery(query: DriverRankingsQuery): void {
|
|
if (query.page !== undefined && query.page < 1) {
|
|
throw new ValidationError('Page must be a positive integer');
|
|
}
|
|
|
|
if (query.limit !== undefined && query.limit < 1) {
|
|
throw new ValidationError('Limit must be a positive integer');
|
|
}
|
|
|
|
if (query.minRating !== undefined && query.minRating < 0) {
|
|
throw new ValidationError('Min rating must be a non-negative number');
|
|
}
|
|
|
|
if (query.sortBy && !['rating', 'name', 'rank', 'raceCount'].includes(query.sortBy)) {
|
|
throw new ValidationError('Invalid sort field');
|
|
}
|
|
|
|
if (query.sortOrder && !['asc', 'desc'].includes(query.sortOrder)) {
|
|
throw new ValidationError('Sort order must be "asc" or "desc"');
|
|
}
|
|
}
|
|
}
|