alpha wip

This commit is contained in:
2025-12-03 00:46:08 +01:00
parent 3b55fd1a63
commit 97e29d3d80
51 changed files with 6321 additions and 237 deletions

View File

@@ -0,0 +1,86 @@
/**
* Infrastructure Adapter: InMemoryDriverRepository
*
* In-memory implementation of IDriverRepository.
* Stores data in Map structure with UUID generation.
*/
import { v4 as uuidv4 } from 'uuid';
import { Driver } from '../../domain/entities/Driver';
import { IDriverRepository } from '../../application/ports/IDriverRepository';
export class InMemoryDriverRepository implements IDriverRepository {
private drivers: Map<string, Driver>;
constructor(seedData?: Driver[]) {
this.drivers = new Map();
if (seedData) {
seedData.forEach(driver => {
this.drivers.set(driver.id, driver);
});
}
}
async findById(id: string): Promise<Driver | null> {
return this.drivers.get(id) ?? null;
}
async findByIRacingId(iracingId: string): Promise<Driver | null> {
const driver = Array.from(this.drivers.values()).find(
d => d.iracingId === iracingId
);
return driver ?? null;
}
async findAll(): Promise<Driver[]> {
return Array.from(this.drivers.values());
}
async create(driver: Driver): Promise<Driver> {
if (await this.exists(driver.id)) {
throw new Error(`Driver with ID ${driver.id} already exists`);
}
if (await this.existsByIRacingId(driver.iracingId)) {
throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`);
}
this.drivers.set(driver.id, driver);
return driver;
}
async update(driver: Driver): Promise<Driver> {
if (!await this.exists(driver.id)) {
throw new Error(`Driver with ID ${driver.id} not found`);
}
this.drivers.set(driver.id, driver);
return driver;
}
async delete(id: string): Promise<void> {
if (!await this.exists(id)) {
throw new Error(`Driver with ID ${id} not found`);
}
this.drivers.delete(id);
}
async exists(id: string): Promise<boolean> {
return this.drivers.has(id);
}
async existsByIRacingId(iracingId: string): Promise<boolean> {
return Array.from(this.drivers.values()).some(
d => d.iracingId === iracingId
);
}
/**
* Utility method to generate a new UUID
*/
static generateId(): string {
return uuidv4();
}
}

View File

@@ -0,0 +1,82 @@
/**
* Infrastructure Adapter: InMemoryLeagueRepository
*
* In-memory implementation of ILeagueRepository.
* Stores data in Map structure with UUID generation.
*/
import { v4 as uuidv4 } from 'uuid';
import { League } from '../../domain/entities/League';
import { ILeagueRepository } from '../../application/ports/ILeagueRepository';
export class InMemoryLeagueRepository implements ILeagueRepository {
private leagues: Map<string, League>;
constructor(seedData?: League[]) {
this.leagues = new Map();
if (seedData) {
seedData.forEach(league => {
this.leagues.set(league.id, league);
});
}
}
async findById(id: string): Promise<League | null> {
return this.leagues.get(id) ?? null;
}
async findAll(): Promise<League[]> {
return Array.from(this.leagues.values());
}
async findByOwnerId(ownerId: string): Promise<League[]> {
return Array.from(this.leagues.values()).filter(
league => league.ownerId === ownerId
);
}
async create(league: League): Promise<League> {
if (await this.exists(league.id)) {
throw new Error(`League with ID ${league.id} already exists`);
}
this.leagues.set(league.id, league);
return league;
}
async update(league: League): Promise<League> {
if (!await this.exists(league.id)) {
throw new Error(`League with ID ${league.id} not found`);
}
this.leagues.set(league.id, league);
return league;
}
async delete(id: string): Promise<void> {
if (!await this.exists(id)) {
throw new Error(`League with ID ${id} not found`);
}
this.leagues.delete(id);
}
async exists(id: string): Promise<boolean> {
return this.leagues.has(id);
}
async searchByName(query: string): Promise<League[]> {
const normalizedQuery = query.toLowerCase();
return Array.from(this.leagues.values()).filter(league =>
league.name.toLowerCase().includes(normalizedQuery)
);
}
/**
* Utility method to generate a new UUID
*/
static generateId(): string {
return uuidv4();
}
}

View File

@@ -0,0 +1,110 @@
/**
* Infrastructure Adapter: InMemoryRaceRepository
*
* In-memory implementation of IRaceRepository.
* Stores data in Map structure with UUID generation.
*/
import { v4 as uuidv4 } from 'uuid';
import { Race, RaceStatus } from '../../domain/entities/Race';
import { IRaceRepository } from '../../application/ports/IRaceRepository';
export class InMemoryRaceRepository implements IRaceRepository {
private races: Map<string, Race>;
constructor(seedData?: Race[]) {
this.races = new Map();
if (seedData) {
seedData.forEach(race => {
this.races.set(race.id, race);
});
}
}
async findById(id: string): Promise<Race | null> {
return this.races.get(id) ?? null;
}
async findAll(): Promise<Race[]> {
return Array.from(this.races.values());
}
async findByLeagueId(leagueId: string): Promise<Race[]> {
return Array.from(this.races.values())
.filter(race => race.leagueId === leagueId)
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
}
async findUpcomingByLeagueId(leagueId: string): Promise<Race[]> {
const now = new Date();
return Array.from(this.races.values())
.filter(race =>
race.leagueId === leagueId &&
race.status === 'scheduled' &&
race.scheduledAt > now
)
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
}
async findCompletedByLeagueId(leagueId: string): Promise<Race[]> {
return Array.from(this.races.values())
.filter(race =>
race.leagueId === leagueId &&
race.status === 'completed'
)
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
}
async findByStatus(status: RaceStatus): Promise<Race[]> {
return Array.from(this.races.values())
.filter(race => race.status === status)
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
}
async findByDateRange(startDate: Date, endDate: Date): Promise<Race[]> {
return Array.from(this.races.values())
.filter(race =>
race.scheduledAt >= startDate &&
race.scheduledAt <= endDate
)
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
}
async create(race: Race): Promise<Race> {
if (await this.exists(race.id)) {
throw new Error(`Race with ID ${race.id} already exists`);
}
this.races.set(race.id, race);
return race;
}
async update(race: Race): Promise<Race> {
if (!await this.exists(race.id)) {
throw new Error(`Race with ID ${race.id} not found`);
}
this.races.set(race.id, race);
return race;
}
async delete(id: string): Promise<void> {
if (!await this.exists(id)) {
throw new Error(`Race with ID ${id} not found`);
}
this.races.delete(id);
}
async exists(id: string): Promise<boolean> {
return this.races.has(id);
}
/**
* Utility method to generate a new UUID
*/
static generateId(): string {
return uuidv4();
}
}

View File

@@ -0,0 +1,125 @@
/**
* Infrastructure Adapter: InMemoryResultRepository
*
* In-memory implementation of IResultRepository.
* Stores data in Map structure with UUID generation.
*/
import { v4 as uuidv4 } from 'uuid';
import { Result } from '../../domain/entities/Result';
import { IResultRepository } from '../../application/ports/IResultRepository';
import { IRaceRepository } from '../../application/ports/IRaceRepository';
export class InMemoryResultRepository implements IResultRepository {
private results: Map<string, Result>;
private raceRepository?: IRaceRepository;
constructor(seedData?: Result[], raceRepository?: IRaceRepository) {
this.results = new Map();
this.raceRepository = raceRepository;
if (seedData) {
seedData.forEach(result => {
this.results.set(result.id, result);
});
}
}
async findById(id: string): Promise<Result | null> {
return this.results.get(id) ?? null;
}
async findAll(): Promise<Result[]> {
return Array.from(this.results.values());
}
async findByRaceId(raceId: string): Promise<Result[]> {
return Array.from(this.results.values())
.filter(result => result.raceId === raceId)
.sort((a, b) => a.position - b.position);
}
async findByDriverId(driverId: string): Promise<Result[]> {
return Array.from(this.results.values())
.filter(result => result.driverId === driverId);
}
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Result[]> {
if (!this.raceRepository) {
return [];
}
const leagueRaces = await this.raceRepository.findByLeagueId(leagueId);
const leagueRaceIds = new Set(leagueRaces.map(race => race.id));
return Array.from(this.results.values())
.filter(result =>
result.driverId === driverId &&
leagueRaceIds.has(result.raceId)
);
}
async create(result: Result): Promise<Result> {
if (await this.exists(result.id)) {
throw new Error(`Result with ID ${result.id} already exists`);
}
this.results.set(result.id, result);
return result;
}
async createMany(results: Result[]): Promise<Result[]> {
const created: Result[] = [];
for (const result of results) {
if (await this.exists(result.id)) {
throw new Error(`Result with ID ${result.id} already exists`);
}
this.results.set(result.id, result);
created.push(result);
}
return created;
}
async update(result: Result): Promise<Result> {
if (!await this.exists(result.id)) {
throw new Error(`Result with ID ${result.id} not found`);
}
this.results.set(result.id, result);
return result;
}
async delete(id: string): Promise<void> {
if (!await this.exists(id)) {
throw new Error(`Result with ID ${id} not found`);
}
this.results.delete(id);
}
async deleteByRaceId(raceId: string): Promise<void> {
const raceResults = await this.findByRaceId(raceId);
raceResults.forEach(result => {
this.results.delete(result.id);
});
}
async exists(id: string): Promise<boolean> {
return this.results.has(id);
}
async existsByRaceId(raceId: string): Promise<boolean> {
return Array.from(this.results.values()).some(
result => result.raceId === raceId
);
}
/**
* Utility method to generate a new UUID
*/
static generateId(): string {
return uuidv4();
}
}

View File

@@ -0,0 +1,188 @@
/**
* Infrastructure Adapter: InMemoryStandingRepository
*
* In-memory implementation of IStandingRepository.
* Stores data in Map structure and calculates standings from race results.
*/
import { Standing } from '../../domain/entities/Standing';
import { IStandingRepository } from '../../application/ports/IStandingRepository';
import { IResultRepository } from '../../application/ports/IResultRepository';
import { IRaceRepository } from '../../application/ports/IRaceRepository';
import { ILeagueRepository } from '../../application/ports/ILeagueRepository';
/**
* Points systems presets
*/
const POINTS_SYSTEMS: Record<string, Record<number, number>> = {
'f1-2024': {
1: 25, 2: 18, 3: 15, 4: 12, 5: 10,
6: 8, 7: 6, 8: 4, 9: 2, 10: 1
},
'indycar': {
1: 50, 2: 40, 3: 35, 4: 32, 5: 30,
6: 28, 7: 26, 8: 24, 9: 22, 10: 20,
11: 19, 12: 18, 13: 17, 14: 16, 15: 15
}
};
export class InMemoryStandingRepository implements IStandingRepository {
private standings: Map<string, Standing>;
private resultRepository?: IResultRepository;
private raceRepository?: IRaceRepository;
private leagueRepository?: ILeagueRepository;
constructor(
seedData?: Standing[],
resultRepository?: IResultRepository,
raceRepository?: IRaceRepository,
leagueRepository?: ILeagueRepository
) {
this.standings = new Map();
this.resultRepository = resultRepository;
this.raceRepository = raceRepository;
this.leagueRepository = leagueRepository;
if (seedData) {
seedData.forEach(standing => {
const key = this.getKey(standing.leagueId, standing.driverId);
this.standings.set(key, standing);
});
}
}
private getKey(leagueId: string, driverId: string): string {
return `${leagueId}:${driverId}`;
}
async findByLeagueId(leagueId: string): Promise<Standing[]> {
return Array.from(this.standings.values())
.filter(standing => standing.leagueId === leagueId)
.sort((a, b) => {
// Sort by position (lower is better)
if (a.position !== b.position) {
return a.position - b.position;
}
// If positions are equal, sort by points (higher is better)
return b.points - a.points;
});
}
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Standing | null> {
const key = this.getKey(leagueId, driverId);
return this.standings.get(key) ?? null;
}
async findAll(): Promise<Standing[]> {
return Array.from(this.standings.values());
}
async save(standing: Standing): Promise<Standing> {
const key = this.getKey(standing.leagueId, standing.driverId);
this.standings.set(key, standing);
return standing;
}
async saveMany(standings: Standing[]): Promise<Standing[]> {
standings.forEach(standing => {
const key = this.getKey(standing.leagueId, standing.driverId);
this.standings.set(key, standing);
});
return standings;
}
async delete(leagueId: string, driverId: string): Promise<void> {
const key = this.getKey(leagueId, driverId);
this.standings.delete(key);
}
async deleteByLeagueId(leagueId: string): Promise<void> {
const toDelete = Array.from(this.standings.values())
.filter(standing => standing.leagueId === leagueId);
toDelete.forEach(standing => {
const key = this.getKey(standing.leagueId, standing.driverId);
this.standings.delete(key);
});
}
async exists(leagueId: string, driverId: string): Promise<boolean> {
const key = this.getKey(leagueId, driverId);
return this.standings.has(key);
}
async recalculate(leagueId: string): Promise<Standing[]> {
if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) {
throw new Error('Cannot recalculate standings: missing required repositories');
}
// Get league to determine points system
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
throw new Error(`League with ID ${leagueId} not found`);
}
// Get points system
const pointsSystem = league.settings.customPoints ??
POINTS_SYSTEMS[league.settings.pointsSystem] ??
POINTS_SYSTEMS['f1-2024'];
// Get all completed races for the league
const races = await this.raceRepository.findCompletedByLeagueId(leagueId);
// Get all results for these races
const allResults = await Promise.all(
races.map(race => this.resultRepository!.findByRaceId(race.id))
);
const results = allResults.flat();
// Calculate standings per driver
const standingsMap = new Map<string, Standing>();
results.forEach(result => {
let standing = standingsMap.get(result.driverId);
if (!standing) {
standing = Standing.create({
leagueId,
driverId: result.driverId,
});
}
// Add points from this result
standing = standing.addRaceResult(result.position, pointsSystem);
standingsMap.set(result.driverId, standing);
});
// Sort by points and assign positions
const sortedStandings = Array.from(standingsMap.values())
.sort((a, b) => {
if (b.points !== a.points) {
return b.points - a.points;
}
// Tie-breaker: most wins
if (b.wins !== a.wins) {
return b.wins - a.wins;
}
// Tie-breaker: most races completed
return b.racesCompleted - a.racesCompleted;
});
// Assign positions
const updatedStandings = sortedStandings.map((standing, index) =>
standing.updatePosition(index + 1)
);
// Save all standings
await this.saveMany(updatedStandings);
return updatedStandings;
}
/**
* Get available points systems
*/
static getPointsSystems(): Record<string, Record<number, number>> {
return POINTS_SYSTEMS;
}
}