Files
gridpilot.gg/core/leaderboards/application/use-cases/GetDriverRankingsUseCase.ts
Marc Mintel 597bb48248
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 4m51s
Contract Testing / contract-snapshot (pull_request) Has been skipped
integration tests
2026-01-22 17:29:06 +01:00

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"');
}
}
}