seed data
This commit is contained in:
@@ -3,21 +3,6 @@ import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/Sig
|
||||
import { CreateAchievementUseCase } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
|
||||
import { createInMemoryRacingSeed } from './inmemory/InMemoryRacingSeed';
|
||||
|
||||
import {
|
||||
DRIVER_ACHIEVEMENTS,
|
||||
STEWARD_ACHIEVEMENTS,
|
||||
@@ -25,26 +10,12 @@ import {
|
||||
COMMUNITY_ACHIEVEMENTS,
|
||||
} from '@core/identity/domain/AchievementConstants';
|
||||
|
||||
export type InMemorySeedDependencies = {
|
||||
driverRepository: IDriverRepository;
|
||||
leagueRepository: ILeagueRepository;
|
||||
raceRepository: IRaceRepository;
|
||||
resultRepository: IResultRepository;
|
||||
standingRepository: IStandingRepository;
|
||||
leagueMembershipRepository: ILeagueMembershipRepository;
|
||||
raceRegistrationRepository: IRaceRegistrationRepository;
|
||||
teamRepository: ITeamRepository;
|
||||
teamMembershipRepository: ITeamMembershipRepository;
|
||||
feedRepository: IFeedRepository;
|
||||
socialGraphRepository: ISocialGraphRepository;
|
||||
};
|
||||
|
||||
export class EnsureInitialData {
|
||||
constructor(
|
||||
private readonly signupUseCase: SignupWithEmailUseCase,
|
||||
private readonly createAchievementUseCase: CreateAchievementUseCase,
|
||||
private readonly logger: Logger,
|
||||
private readonly seedDeps?: InMemorySeedDependencies,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
@@ -88,125 +59,5 @@ export class EnsureInitialData {
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Achievements: ${createdCount} created, ${existingCount} already exist`);
|
||||
|
||||
await this.seedInMemoryRacingDataIfNeeded();
|
||||
}
|
||||
|
||||
private shouldSeedInMemory(): boolean {
|
||||
const configured = (process.env.GRIDPILOT_API_PERSISTENCE ?? '').toLowerCase();
|
||||
if (configured) {
|
||||
return configured === 'inmemory';
|
||||
}
|
||||
|
||||
return process.env.DATABASE_URL === undefined;
|
||||
}
|
||||
|
||||
private async seedInMemoryRacingDataIfNeeded(): Promise<void> {
|
||||
if (!this.shouldSeedInMemory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.seedDeps) {
|
||||
this.logger.info('[Bootstrap] In-memory racing seed skipped (missing dependencies)');
|
||||
return;
|
||||
}
|
||||
|
||||
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
||||
if (existingDrivers.length > 0) {
|
||||
this.logger.info('[Bootstrap] In-memory racing seed skipped (drivers already exist)');
|
||||
return;
|
||||
}
|
||||
|
||||
const seed = createInMemoryRacingSeed();
|
||||
|
||||
for (const driver of seed.drivers) {
|
||||
try {
|
||||
await this.seedDeps.driverRepository.create(driver);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const league of seed.leagues) {
|
||||
try {
|
||||
await this.seedDeps.leagueRepository.create(league);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const race of seed.races) {
|
||||
try {
|
||||
await this.seedDeps.raceRepository.create(race);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.seedDeps.resultRepository.createMany(seed.results);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
|
||||
for (const membership of seed.leagueMemberships) {
|
||||
try {
|
||||
await this.seedDeps.leagueMembershipRepository.saveMembership(membership);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const team of seed.teams) {
|
||||
try {
|
||||
await this.seedDeps.teamRepository.create(team);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const membership of seed.teamMemberships) {
|
||||
try {
|
||||
await this.seedDeps.teamMembershipRepository.saveMembership(membership);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const registration of seed.raceRegistrations) {
|
||||
try {
|
||||
await this.seedDeps.raceRegistrationRepository.register(registration);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.seedDeps.standingRepository.saveMany(seed.standings);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
|
||||
const seedableFeed = this.seedDeps.feedRepository as unknown as { seed?: (input: unknown) => void };
|
||||
if (typeof seedableFeed.seed === 'function') {
|
||||
seedableFeed.seed({
|
||||
drivers: seed.drivers,
|
||||
friendships: seed.friendships,
|
||||
feedEvents: seed.feedEvents,
|
||||
});
|
||||
}
|
||||
|
||||
const seedableSocial = this.seedDeps.socialGraphRepository as unknown as { seed?: (input: unknown) => void };
|
||||
if (typeof seedableSocial.seed === 'function') {
|
||||
seedableSocial.seed({
|
||||
drivers: seed.drivers,
|
||||
friendships: seed.friendships,
|
||||
feedEvents: seed.feedEvents,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[Bootstrap] Seeded in-memory racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
134
adapters/bootstrap/SeedInMemoryRacingData.ts
Normal file
134
adapters/bootstrap/SeedInMemoryRacingData.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { createRacingSeed } from './racing/RacingSeed';
|
||||
|
||||
export type InMemorySeedDependencies = {
|
||||
driverRepository: IDriverRepository;
|
||||
leagueRepository: ILeagueRepository;
|
||||
raceRepository: IRaceRepository;
|
||||
resultRepository: IResultRepository;
|
||||
standingRepository: IStandingRepository;
|
||||
leagueMembershipRepository: ILeagueMembershipRepository;
|
||||
raceRegistrationRepository: IRaceRegistrationRepository;
|
||||
teamRepository: ITeamRepository;
|
||||
teamMembershipRepository: ITeamMembershipRepository;
|
||||
feedRepository: IFeedRepository;
|
||||
socialGraphRepository: ISocialGraphRepository;
|
||||
};
|
||||
|
||||
export class SeedInMemoryRacingData {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly seedDeps: InMemorySeedDependencies,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
||||
if (existingDrivers.length > 0) {
|
||||
this.logger.info('[Bootstrap] In-memory racing seed skipped (drivers already exist)');
|
||||
return;
|
||||
}
|
||||
|
||||
const seed = createRacingSeed();
|
||||
|
||||
for (const driver of seed.drivers) {
|
||||
try {
|
||||
await this.seedDeps.driverRepository.create(driver);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const league of seed.leagues) {
|
||||
try {
|
||||
await this.seedDeps.leagueRepository.create(league);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const race of seed.races) {
|
||||
try {
|
||||
await this.seedDeps.raceRepository.create(race);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.seedDeps.resultRepository.createMany(seed.results);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
|
||||
for (const membership of seed.leagueMemberships) {
|
||||
try {
|
||||
await this.seedDeps.leagueMembershipRepository.saveMembership(membership);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const team of seed.teams) {
|
||||
try {
|
||||
await this.seedDeps.teamRepository.create(team);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const membership of seed.teamMemberships) {
|
||||
try {
|
||||
await this.seedDeps.teamMembershipRepository.saveMembership(membership);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
for (const registration of seed.raceRegistrations) {
|
||||
try {
|
||||
await this.seedDeps.raceRegistrationRepository.register(registration);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.seedDeps.standingRepository.saveMany(seed.standings);
|
||||
} catch {
|
||||
// ignore duplicates
|
||||
}
|
||||
|
||||
const seedableFeed = this.seedDeps.feedRepository as unknown as { seed?: (input: unknown) => void };
|
||||
if (typeof seedableFeed.seed === 'function') {
|
||||
seedableFeed.seed({
|
||||
drivers: seed.drivers,
|
||||
friendships: seed.friendships,
|
||||
feedEvents: seed.feedEvents,
|
||||
});
|
||||
}
|
||||
|
||||
const seedableSocial = this.seedDeps.socialGraphRepository as unknown as { seed?: (input: unknown) => void };
|
||||
if (typeof seedableSocial.seed === 'function') {
|
||||
seedableSocial.seed({
|
||||
drivers: seed.drivers,
|
||||
friendships: seed.friendships,
|
||||
feedEvents: seed.feedEvents,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[Bootstrap] Seeded in-memory racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,557 +0,0 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import type { TeamMembership } from '@core/racing/domain/types/TeamMembership';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import { getPointsSystems } from '../PointsSystems';
|
||||
|
||||
export type Friendship = {
|
||||
driverId: string;
|
||||
friendId: string;
|
||||
};
|
||||
|
||||
export type InMemoryRacingSeed = {
|
||||
drivers: Driver[];
|
||||
leagues: League[];
|
||||
races: Race[];
|
||||
results: RaceResult[];
|
||||
standings: Standing[];
|
||||
leagueMemberships: LeagueMembership[];
|
||||
raceRegistrations: RaceRegistration[];
|
||||
teams: Team[];
|
||||
teamMemberships: TeamMembership[];
|
||||
friendships: Friendship[];
|
||||
feedEvents: FeedItem[];
|
||||
};
|
||||
|
||||
export type InMemoryRacingSeedOptions = {
|
||||
driverCount?: number;
|
||||
baseDate?: Date;
|
||||
};
|
||||
|
||||
export const inMemoryRacingSeedDefaults: Readonly<
|
||||
Required<InMemoryRacingSeedOptions>
|
||||
> = {
|
||||
driverCount: 32,
|
||||
baseDate: new Date('2025-01-15T12:00:00.000Z'),
|
||||
};
|
||||
|
||||
class InMemoryRacingSeedFactory {
|
||||
private readonly driverCount: number;
|
||||
private readonly baseDate: Date;
|
||||
|
||||
constructor(options: InMemoryRacingSeedOptions) {
|
||||
this.driverCount = options.driverCount ?? inMemoryRacingSeedDefaults.driverCount;
|
||||
this.baseDate = options.baseDate ?? inMemoryRacingSeedDefaults.baseDate;
|
||||
}
|
||||
|
||||
create(): InMemoryRacingSeed {
|
||||
const drivers = this.createDrivers();
|
||||
const leagues = this.createLeagues();
|
||||
const races = this.createRaces(leagues);
|
||||
const results = this.createResults(drivers, races);
|
||||
const standings = this.createStandings(leagues, races, results);
|
||||
const leagueMemberships = this.createLeagueMemberships(drivers, leagues);
|
||||
const raceRegistrations = this.createRaceRegistrations(races);
|
||||
const teams = this.createTeams();
|
||||
const teamMemberships = this.createTeamMemberships(drivers, teams);
|
||||
const friendships = this.createFriendships(drivers);
|
||||
const feedEvents = this.createFeedEvents(drivers, friendships, races, leagues);
|
||||
|
||||
return {
|
||||
drivers,
|
||||
leagues,
|
||||
races,
|
||||
results,
|
||||
standings,
|
||||
leagueMemberships,
|
||||
raceRegistrations,
|
||||
teams,
|
||||
teamMemberships,
|
||||
friendships,
|
||||
feedEvents,
|
||||
};
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
private addMinutes(date: Date, minutes: number): Date {
|
||||
return new Date(date.getTime() + minutes * 60 * 1000);
|
||||
}
|
||||
|
||||
private createDrivers(): Driver[] {
|
||||
const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES'] as const;
|
||||
|
||||
return Array.from({ length: this.driverCount }, (_, idx) => {
|
||||
const i = idx + 1;
|
||||
|
||||
return Driver.create({
|
||||
id: `driver-${i}`,
|
||||
iracingId: String(100000 + i),
|
||||
name: `Driver ${i}`,
|
||||
country: countries[idx % countries.length]!,
|
||||
bio: `Demo driver #${i} seeded for in-memory mode.`,
|
||||
joinedAt: this.addDays(this.baseDate, -90 + i),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createLeagues(): League[] {
|
||||
const createdAtBase = this.baseDate;
|
||||
|
||||
return [
|
||||
League.create({
|
||||
id: 'league-1',
|
||||
name: 'GridPilot Sprint Series',
|
||||
description: 'Weekly sprint races with stable grids.',
|
||||
ownerId: 'driver-1',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -200),
|
||||
socialLinks: {
|
||||
discordUrl: 'https://discord.gg/gridpilot-demo',
|
||||
youtubeUrl: 'https://youtube.com/@gridpilot-demo',
|
||||
websiteUrl: 'https://gridpilot-demo.example.com',
|
||||
},
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-2',
|
||||
name: 'GridPilot Endurance Cup',
|
||||
description: 'Longer races with strategy and consistency.',
|
||||
ownerId: 'driver-2',
|
||||
settings: {
|
||||
pointsSystem: 'indycar',
|
||||
maxDrivers: 32,
|
||||
sessionDuration: 120,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -180),
|
||||
socialLinks: { discordUrl: 'https://discord.gg/gridpilot-endurance' },
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-3',
|
||||
name: 'GridPilot Club Ladder',
|
||||
description: 'Casual ladder with fast onboarding.',
|
||||
ownerId: 'driver-3',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 48,
|
||||
sessionDuration: 45,
|
||||
qualifyingFormat: 'single-lap',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -160),
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-4',
|
||||
name: 'Nordic Night Series',
|
||||
description: 'Evening races with tight fields.',
|
||||
ownerId: 'driver-4',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 32,
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -150),
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-5',
|
||||
name: 'Demo League (Admin)',
|
||||
description: 'Primary demo league owned by driver-1.',
|
||||
ownerId: 'driver-1',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -140),
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-6',
|
||||
name: 'Sim Racing Alliance',
|
||||
description: 'Mixed-format season with community events.',
|
||||
ownerId: 'driver-5',
|
||||
settings: {
|
||||
pointsSystem: 'indycar',
|
||||
maxDrivers: 40,
|
||||
sessionDuration: 90,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -130),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private createRaces(leagues: League[]): Race[] {
|
||||
const tracks = [
|
||||
'Monza GP',
|
||||
'Spa-Francorchamps',
|
||||
'Suzuka',
|
||||
'Mount Panorama',
|
||||
'Silverstone GP',
|
||||
'Interlagos',
|
||||
'Imola',
|
||||
'Laguna Seca',
|
||||
];
|
||||
const cars = ['GT3 – Porsche 911', 'GT3 – BMW M4', 'LMP3 Prototype', 'GT4 – Alpine', 'Touring – Civic'];
|
||||
|
||||
const leagueIds = leagues.map((l) => l.id.toString());
|
||||
const demoLeagueId = 'league-5';
|
||||
|
||||
const races: Race[] = [];
|
||||
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
const leagueId = leagueIds[(i - 1) % leagueIds.length] ?? demoLeagueId;
|
||||
const scheduledAt = this.addDays(this.baseDate, i <= 10 ? -35 + i : 1 + (i - 10) * 2);
|
||||
|
||||
const base = {
|
||||
id: `race-${i}`,
|
||||
leagueId,
|
||||
scheduledAt,
|
||||
track: tracks[(i - 1) % tracks.length]!,
|
||||
car: cars[(i - 1) % cars.length]!,
|
||||
};
|
||||
|
||||
if (i === 1) {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
leagueId: demoLeagueId,
|
||||
scheduledAt: this.addMinutes(this.baseDate, -30),
|
||||
status: 'running',
|
||||
strengthOfField: 1530,
|
||||
registeredCount: 16,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scheduledAt < this.baseDate) {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'completed',
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'scheduled',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return races;
|
||||
}
|
||||
|
||||
private createResults(drivers: Driver[], races: Race[]): RaceResult[] {
|
||||
const results: RaceResult[] = [];
|
||||
const completed = races.filter((r) => r.status === 'completed');
|
||||
|
||||
for (const race of completed) {
|
||||
const participants = drivers.slice(0, Math.min(16, drivers.length));
|
||||
|
||||
for (let idx = 0; idx < participants.length; idx++) {
|
||||
const driver = participants[idx]!;
|
||||
const position = idx + 1;
|
||||
const startPosition = ((idx + 3) % participants.length) + 1;
|
||||
|
||||
results.push(
|
||||
RaceResult.create({
|
||||
id: `${race.id}:${driver.id}`,
|
||||
raceId: race.id,
|
||||
driverId: driver.id,
|
||||
position,
|
||||
startPosition,
|
||||
fastestLap: 88_000 + idx * 120,
|
||||
incidents: idx % 4 === 0 ? 2 : 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private resolvePointsSystem(
|
||||
league: League,
|
||||
pointsSystems: Record<string, Record<number, number>>,
|
||||
): Record<number, number> {
|
||||
const settings = league.settings;
|
||||
return settings.customPoints ?? pointsSystems[settings.pointsSystem] ?? pointsSystems['f1-2024'] ?? {};
|
||||
}
|
||||
|
||||
private createStandings(leagues: League[], races: Race[], results: RaceResult[]): Standing[] {
|
||||
const pointsSystems = getPointsSystems();
|
||||
|
||||
const racesByLeague = new Map<string, Set<string>>();
|
||||
for (const race of races) {
|
||||
if (race.status !== 'completed') continue;
|
||||
|
||||
const set = racesByLeague.get(race.leagueId) ?? new Set<string>();
|
||||
set.add(race.id);
|
||||
racesByLeague.set(race.leagueId, set);
|
||||
}
|
||||
|
||||
const standings: Standing[] = [];
|
||||
|
||||
for (const league of leagues) {
|
||||
const leagueId = league.id.toString();
|
||||
const completedRaceIds = racesByLeague.get(leagueId) ?? new Set<string>();
|
||||
if (completedRaceIds.size === 0) continue;
|
||||
|
||||
const pointsTable = this.resolvePointsSystem(league, pointsSystems);
|
||||
|
||||
const byDriver = new Map<string, Standing>();
|
||||
|
||||
for (const result of results) {
|
||||
if (!completedRaceIds.has(result.raceId.toString())) continue;
|
||||
|
||||
const driverId = result.driverId.toString();
|
||||
const previousStanding = byDriver.get(driverId) ?? Standing.create({ leagueId, driverId, position: 1 });
|
||||
const nextStanding = previousStanding.addRaceResult(result.position.toNumber(), pointsTable);
|
||||
byDriver.set(driverId, nextStanding);
|
||||
}
|
||||
|
||||
const sorted = Array.from(byDriver.values()).sort((a, b) => {
|
||||
if (b.points.toNumber() !== a.points.toNumber()) return b.points.toNumber() - a.points.toNumber();
|
||||
if (b.wins !== a.wins) return b.wins - a.wins;
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
});
|
||||
|
||||
sorted.forEach((standing, index) => standings.push(standing.updatePosition(index + 1)));
|
||||
}
|
||||
|
||||
return standings;
|
||||
}
|
||||
|
||||
private createLeagueMemberships(drivers: Driver[], leagues: League[]): LeagueMembership[] {
|
||||
const memberships: LeagueMembership[] = [];
|
||||
|
||||
for (const driver of drivers) {
|
||||
const driverId = driver.id;
|
||||
|
||||
memberships.push(
|
||||
LeagueMembership.create({
|
||||
leagueId: 'league-5',
|
||||
driverId,
|
||||
role: driverId === 'driver-1' ? 'owner' : 'member',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -60),
|
||||
}),
|
||||
);
|
||||
|
||||
const driverNumber = Number(driverId.split('-')[1]);
|
||||
const extraLeague = leagues[(driverNumber % (leagues.length - 1)) + 1];
|
||||
|
||||
if (extraLeague) {
|
||||
memberships.push(
|
||||
LeagueMembership.create({
|
||||
leagueId: extraLeague.id.toString(),
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -40),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
private createRaceRegistrations(races: Race[]): RaceRegistration[] {
|
||||
const registrations: RaceRegistration[] = [];
|
||||
|
||||
const upcomingDemoLeague = races.filter((r) => r.status === 'scheduled' && r.leagueId === 'league-5').slice(0, 3);
|
||||
|
||||
for (const race of upcomingDemoLeague) {
|
||||
registrations.push(
|
||||
RaceRegistration.create({
|
||||
raceId: race.id,
|
||||
driverId: 'driver-1',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return registrations;
|
||||
}
|
||||
|
||||
private createTeams(): Team[] {
|
||||
return [
|
||||
Team.create({
|
||||
id: 'team-1',
|
||||
name: 'Apex Racing',
|
||||
tag: 'APEX',
|
||||
description: 'Demo team focused on clean racing.',
|
||||
ownerId: 'driver-1',
|
||||
leagues: ['league-5'],
|
||||
createdAt: this.addDays(this.baseDate, -100),
|
||||
}),
|
||||
Team.create({
|
||||
id: 'team-2',
|
||||
name: 'Night Owls',
|
||||
tag: 'NITE',
|
||||
description: 'Late-night grinders and endurance lovers.',
|
||||
ownerId: 'driver-2',
|
||||
leagues: ['league-4'],
|
||||
createdAt: this.addDays(this.baseDate, -90),
|
||||
}),
|
||||
Team.create({
|
||||
id: 'team-3',
|
||||
name: 'Club Legends',
|
||||
tag: 'CLUB',
|
||||
description: 'A casual team for ladder climbing.',
|
||||
ownerId: 'driver-3',
|
||||
leagues: ['league-3'],
|
||||
createdAt: this.addDays(this.baseDate, -80),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private createTeamMemberships(drivers: Driver[], teams: Team[]): TeamMembership[] {
|
||||
const memberships: TeamMembership[] = [];
|
||||
|
||||
const team1 = teams.find((t) => t.id === 'team-1');
|
||||
const team2 = teams.find((t) => t.id === 'team-2');
|
||||
const team3 = teams.find((t) => t.id === 'team-3');
|
||||
|
||||
if (team1) {
|
||||
const members = drivers.slice(0, 6);
|
||||
members.forEach((d, idx) => {
|
||||
memberships.push({
|
||||
teamId: team1.id,
|
||||
driverId: d.id,
|
||||
role: d.id === team1.ownerId.toString() ? 'owner' : idx === 1 ? 'manager' : 'driver',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -50),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (team2) {
|
||||
const members = drivers.slice(6, 12);
|
||||
members.forEach((d) => {
|
||||
memberships.push({
|
||||
teamId: team2.id,
|
||||
driverId: d.id,
|
||||
role: d.id === team2.ownerId.toString() ? 'owner' : 'driver',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -45),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (team3) {
|
||||
const members = drivers.slice(12, 18);
|
||||
members.forEach((d) => {
|
||||
memberships.push({
|
||||
teamId: team3.id,
|
||||
driverId: d.id,
|
||||
role: d.id === team3.ownerId.toString() ? 'owner' : 'driver',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -40),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
private createFriendships(drivers: Driver[]): Friendship[] {
|
||||
const friendships: Friendship[] = [];
|
||||
|
||||
for (let i = 0; i < drivers.length; i++) {
|
||||
const driver = drivers[i]!;
|
||||
for (let offset = 1; offset <= 3; offset++) {
|
||||
const friend = drivers[(i + offset) % drivers.length]!;
|
||||
friendships.push({ driverId: driver.id, friendId: friend.id });
|
||||
}
|
||||
}
|
||||
|
||||
return friendships;
|
||||
}
|
||||
|
||||
private createFeedEvents(drivers: Driver[], friendships: Friendship[], races: Race[], leagues: League[]): FeedItem[] {
|
||||
const items: FeedItem[] = [];
|
||||
const friendMap = new Map(friendships.map((f) => [`${f.driverId}:${f.friendId}`, true] as const));
|
||||
|
||||
const completedRace = races.find((r) => r.status === 'completed');
|
||||
const upcomingRace = races.find((r) => r.status === 'scheduled');
|
||||
const league = leagues.find((l) => l.id.toString() === 'league-5') ?? leagues[0]!;
|
||||
|
||||
const now = this.addMinutes(this.baseDate, 10);
|
||||
|
||||
for (let i = 2; i <= 10; i++) {
|
||||
const actor = drivers.find((d) => d.id === `driver-${i}`);
|
||||
if (!actor) continue;
|
||||
|
||||
if (!friendMap.has(`driver-1:${actor.id}`)) continue;
|
||||
|
||||
items.push({
|
||||
id: `feed:${actor.id}:joined:${i}`,
|
||||
type: 'friend-joined-league',
|
||||
timestamp: this.addMinutes(now, -(i * 7)),
|
||||
actorDriverId: actor.id,
|
||||
actorFriendId: actor.id,
|
||||
leagueId: league.id.toString(),
|
||||
headline: `${actor.name} joined ${String(league.name)}`,
|
||||
body: 'Demo activity in in-memory mode.',
|
||||
ctaLabel: 'View league',
|
||||
ctaHref: `/leagues/${league.id.toString()}`,
|
||||
});
|
||||
|
||||
if (completedRace) {
|
||||
items.push({
|
||||
id: `feed:${actor.id}:result:${i}`,
|
||||
type: 'friend-finished-race',
|
||||
timestamp: this.addMinutes(now, -(i * 7 + 3)),
|
||||
actorDriverId: actor.id,
|
||||
actorFriendId: actor.id,
|
||||
leagueId: completedRace.leagueId,
|
||||
raceId: completedRace.id,
|
||||
position: (i % 5) + 1,
|
||||
headline: `${actor.name} finished a race`,
|
||||
body: `Completed at ${completedRace.track}.`,
|
||||
ctaLabel: 'View results',
|
||||
ctaHref: `/races/${completedRace.id}/results`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (upcomingRace) {
|
||||
items.push({
|
||||
id: `feed:system:scheduled:${upcomingRace.id}`,
|
||||
type: 'new-race-scheduled',
|
||||
timestamp: this.addMinutes(now, -3),
|
||||
leagueId: upcomingRace.leagueId,
|
||||
raceId: upcomingRace.id,
|
||||
headline: `New race scheduled at ${upcomingRace.track}`,
|
||||
body: `${upcomingRace.car} • ${upcomingRace.scheduledAt.toISOString()}`,
|
||||
ctaLabel: 'View schedule',
|
||||
ctaHref: `/races/${upcomingRace.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
return items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
export function createInMemoryRacingSeed(options: InMemoryRacingSeedOptions = {}): InMemoryRacingSeed {
|
||||
return new InMemoryRacingSeedFactory(options).create();
|
||||
}
|
||||
29
adapters/bootstrap/racing/RacingDriverFactory.ts
Normal file
29
adapters/bootstrap/racing/RacingDriverFactory.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
|
||||
export class RacingDriverFactory {
|
||||
constructor(
|
||||
private readonly driverCount: number,
|
||||
private readonly baseDate: Date,
|
||||
) {}
|
||||
|
||||
create(): Driver[] {
|
||||
const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES'] as const;
|
||||
|
||||
return Array.from({ length: this.driverCount }, (_, idx) => {
|
||||
const i = idx + 1;
|
||||
|
||||
return Driver.create({
|
||||
id: `driver-${i}`,
|
||||
iracingId: String(100000 + i),
|
||||
name: `Driver ${i}`,
|
||||
country: countries[idx % countries.length]!,
|
||||
bio: `Demo driver #${i} seeded for in-memory mode.`,
|
||||
joinedAt: this.addDays(this.baseDate, -90 + i),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
77
adapters/bootstrap/racing/RacingFeedFactory.ts
Normal file
77
adapters/bootstrap/racing/RacingFeedFactory.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type { Friendship } from './RacingSeed';
|
||||
|
||||
export class RacingFeedFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
|
||||
create(drivers: Driver[], friendships: Friendship[], races: Race[], leagues: League[]): FeedItem[] {
|
||||
const items: FeedItem[] = [];
|
||||
const friendMap = new Map(friendships.map((f) => [`${f.driverId}:${f.friendId}`, true] as const));
|
||||
|
||||
const completedRace = races.find((r) => r.status === 'completed');
|
||||
const upcomingRace = races.find((r) => r.status === 'scheduled');
|
||||
const league = leagues.find((l) => l.id.toString() === 'league-5') ?? leagues[0]!;
|
||||
|
||||
const now = this.addMinutes(this.baseDate, 10);
|
||||
|
||||
for (let i = 2; i <= 10; i++) {
|
||||
const actor = drivers.find((d) => d.id === `driver-${i}`);
|
||||
if (!actor) continue;
|
||||
|
||||
if (!friendMap.has(`driver-1:${actor.id}`)) continue;
|
||||
|
||||
items.push({
|
||||
id: `feed:${actor.id}:joined:${i}`,
|
||||
type: 'friend-joined-league',
|
||||
timestamp: this.addMinutes(now, -(i * 7)),
|
||||
actorDriverId: actor.id,
|
||||
actorFriendId: actor.id,
|
||||
leagueId: league.id.toString(),
|
||||
headline: `${actor.name} joined ${String(league.name)}`,
|
||||
body: 'Demo activity in in-memory mode.',
|
||||
ctaLabel: 'View league',
|
||||
ctaHref: `/leagues/${league.id.toString()}`,
|
||||
});
|
||||
|
||||
if (completedRace) {
|
||||
items.push({
|
||||
id: `feed:${actor.id}:result:${i}`,
|
||||
type: 'friend-finished-race',
|
||||
timestamp: this.addMinutes(now, -(i * 7 + 3)),
|
||||
actorDriverId: actor.id,
|
||||
actorFriendId: actor.id,
|
||||
leagueId: completedRace.leagueId,
|
||||
raceId: completedRace.id,
|
||||
position: (i % 5) + 1,
|
||||
headline: `${actor.name} finished a race`,
|
||||
body: `Completed at ${completedRace.track}.`,
|
||||
ctaLabel: 'View results',
|
||||
ctaHref: `/races/${completedRace.id}/results`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (upcomingRace) {
|
||||
items.push({
|
||||
id: `feed:system:scheduled:${upcomingRace.id}`,
|
||||
type: 'new-race-scheduled',
|
||||
timestamp: this.addMinutes(now, -3),
|
||||
leagueId: upcomingRace.leagueId,
|
||||
raceId: upcomingRace.id,
|
||||
headline: `New race scheduled at ${upcomingRace.track}`,
|
||||
body: `${upcomingRace.car} • ${upcomingRace.scheduledAt.toISOString()}`,
|
||||
ctaLabel: 'View schedule',
|
||||
ctaHref: `/races/${upcomingRace.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
return items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
}
|
||||
|
||||
private addMinutes(date: Date, minutes: number): Date {
|
||||
return new Date(date.getTime() + minutes * 60 * 1000);
|
||||
}
|
||||
}
|
||||
18
adapters/bootstrap/racing/RacingFriendshipFactory.ts
Normal file
18
adapters/bootstrap/racing/RacingFriendshipFactory.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import type { Friendship } from './RacingSeed';
|
||||
|
||||
export class RacingFriendshipFactory {
|
||||
create(drivers: Driver[]): Friendship[] {
|
||||
const friendships: Friendship[] = [];
|
||||
|
||||
for (let i = 0; i < drivers.length; i++) {
|
||||
const driver = drivers[i]!;
|
||||
for (let offset = 1; offset <= 3; offset++) {
|
||||
const friend = drivers[(i + offset) % drivers.length]!;
|
||||
friendships.push({ driverId: driver.id, friendId: friend.id });
|
||||
}
|
||||
}
|
||||
|
||||
return friendships;
|
||||
}
|
||||
}
|
||||
100
adapters/bootstrap/racing/RacingLeagueFactory.ts
Normal file
100
adapters/bootstrap/racing/RacingLeagueFactory.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
|
||||
export class RacingLeagueFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
|
||||
create(): League[] {
|
||||
const createdAtBase = this.baseDate;
|
||||
|
||||
return [
|
||||
League.create({
|
||||
id: 'league-1',
|
||||
name: 'GridPilot Sprint Series',
|
||||
description: 'Weekly sprint races with stable grids.',
|
||||
ownerId: 'driver-1',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -200),
|
||||
socialLinks: {
|
||||
discordUrl: 'https://discord.gg/gridpilot-demo',
|
||||
youtubeUrl: 'https://youtube.com/@gridpilot-demo',
|
||||
websiteUrl: 'https://gridpilot-demo.example.com',
|
||||
},
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-2',
|
||||
name: 'GridPilot Endurance Cup',
|
||||
description: 'Longer races with strategy and consistency.',
|
||||
ownerId: 'driver-2',
|
||||
settings: {
|
||||
pointsSystem: 'indycar',
|
||||
maxDrivers: 32,
|
||||
sessionDuration: 120,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -180),
|
||||
socialLinks: { discordUrl: 'https://discord.gg/gridpilot-endurance' },
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-3',
|
||||
name: 'GridPilot Club Ladder',
|
||||
description: 'Casual ladder with fast onboarding.',
|
||||
ownerId: 'driver-3',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 48,
|
||||
sessionDuration: 45,
|
||||
qualifyingFormat: 'single-lap',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -160),
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-4',
|
||||
name: 'Nordic Night Series',
|
||||
description: 'Evening races with tight fields.',
|
||||
ownerId: 'driver-4',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 32,
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -150),
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-5',
|
||||
name: 'Demo League (Admin)',
|
||||
description: 'Primary demo league owned by driver-1.',
|
||||
ownerId: 'driver-1',
|
||||
settings: {
|
||||
pointsSystem: 'f1-2024',
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -140),
|
||||
}),
|
||||
League.create({
|
||||
id: 'league-6',
|
||||
name: 'Sim Racing Alliance',
|
||||
description: 'Mixed-format season with community events.',
|
||||
ownerId: 'driver-5',
|
||||
settings: {
|
||||
pointsSystem: 'indycar',
|
||||
maxDrivers: 40,
|
||||
sessionDuration: 90,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
createdAt: this.addDays(createdAtBase, -130),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
65
adapters/bootstrap/racing/RacingMembershipFactory.ts
Normal file
65
adapters/bootstrap/racing/RacingMembershipFactory.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
|
||||
export class RacingMembershipFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
|
||||
createLeagueMemberships(drivers: Driver[], leagues: League[]): LeagueMembership[] {
|
||||
const memberships: LeagueMembership[] = [];
|
||||
|
||||
for (const driver of drivers) {
|
||||
const driverId = driver.id;
|
||||
|
||||
memberships.push(
|
||||
LeagueMembership.create({
|
||||
leagueId: 'league-5',
|
||||
driverId,
|
||||
role: driverId === 'driver-1' ? 'owner' : 'member',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -60),
|
||||
}),
|
||||
);
|
||||
|
||||
const driverNumber = Number(driverId.split('-')[1]);
|
||||
const extraLeague = leagues[(driverNumber % (leagues.length - 1)) + 1];
|
||||
|
||||
if (extraLeague) {
|
||||
memberships.push(
|
||||
LeagueMembership.create({
|
||||
leagueId: extraLeague.id.toString(),
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -40),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
createRaceRegistrations(races: Race[]): RaceRegistration[] {
|
||||
const registrations: RaceRegistration[] = [];
|
||||
|
||||
const upcomingDemoLeague = races.filter((r) => r.status === 'scheduled' && r.leagueId === 'league-5').slice(0, 3);
|
||||
|
||||
for (const race of upcomingDemoLeague) {
|
||||
registrations.push(
|
||||
RaceRegistration.create({
|
||||
raceId: race.id,
|
||||
driverId: 'driver-1',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return registrations;
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
79
adapters/bootstrap/racing/RacingRaceFactory.ts
Normal file
79
adapters/bootstrap/racing/RacingRaceFactory.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
|
||||
export class RacingRaceFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
|
||||
create(leagues: League[]): Race[] {
|
||||
const tracks = [
|
||||
'Monza GP',
|
||||
'Spa-Francorchamps',
|
||||
'Suzuka',
|
||||
'Mount Panorama',
|
||||
'Silverstone GP',
|
||||
'Interlagos',
|
||||
'Imola',
|
||||
'Laguna Seca',
|
||||
];
|
||||
const cars = ['GT3 – Porsche 911', 'GT3 – BMW M4', 'LMP3 Prototype', 'GT4 – Alpine', 'Touring – Civic'];
|
||||
|
||||
const leagueIds = leagues.map((l) => l.id.toString());
|
||||
const demoLeagueId = 'league-5';
|
||||
|
||||
const races: Race[] = [];
|
||||
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
const leagueId = leagueIds[(i - 1) % leagueIds.length] ?? demoLeagueId;
|
||||
const scheduledAt = this.addDays(this.baseDate, i <= 10 ? -35 + i : 1 + (i - 10) * 2);
|
||||
|
||||
const base = {
|
||||
id: `race-${i}`,
|
||||
leagueId,
|
||||
scheduledAt,
|
||||
track: tracks[(i - 1) % tracks.length]!,
|
||||
car: cars[(i - 1) % cars.length]!,
|
||||
};
|
||||
|
||||
if (i === 1) {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
leagueId: demoLeagueId,
|
||||
scheduledAt: this.addMinutes(this.baseDate, -30),
|
||||
status: 'running',
|
||||
strengthOfField: 1530,
|
||||
registeredCount: 16,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scheduledAt < this.baseDate) {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'completed',
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'scheduled',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return races;
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
private addMinutes(date: Date, minutes: number): Date {
|
||||
return new Date(date.getTime() + minutes * 60 * 1000);
|
||||
}
|
||||
}
|
||||
34
adapters/bootstrap/racing/RacingResultFactory.ts
Normal file
34
adapters/bootstrap/racing/RacingResultFactory.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
|
||||
|
||||
export class RacingResultFactory {
|
||||
create(drivers: Driver[], races: Race[]): RaceResult[] {
|
||||
const results: RaceResult[] = [];
|
||||
const completed = races.filter((r) => r.status === 'completed');
|
||||
|
||||
for (const race of completed) {
|
||||
const participants = drivers.slice(0, Math.min(16, drivers.length));
|
||||
|
||||
for (let idx = 0; idx < participants.length; idx++) {
|
||||
const driver = participants[idx]!;
|
||||
const position = idx + 1;
|
||||
const startPosition = ((idx + 3) % participants.length) + 1;
|
||||
|
||||
results.push(
|
||||
RaceResult.create({
|
||||
id: `${race.id}:${driver.id}`,
|
||||
raceId: race.id,
|
||||
driverId: driver.id,
|
||||
position,
|
||||
startPosition,
|
||||
fastestLap: 88_000 + idx * 120,
|
||||
incidents: idx % 4 === 0 ? 2 : 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
102
adapters/bootstrap/racing/RacingSeed.ts
Normal file
102
adapters/bootstrap/racing/RacingSeed.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import type { TeamMembership } from '@core/racing/domain/types/TeamMembership';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import { RacingDriverFactory } from './RacingDriverFactory';
|
||||
import { RacingFeedFactory } from './RacingFeedFactory';
|
||||
import { RacingFriendshipFactory } from './RacingFriendshipFactory';
|
||||
import { RacingLeagueFactory } from './RacingLeagueFactory';
|
||||
import { RacingMembershipFactory } from './RacingMembershipFactory';
|
||||
import { RacingRaceFactory } from './RacingRaceFactory';
|
||||
import { RacingResultFactory } from './RacingResultFactory';
|
||||
import { RacingStandingFactory } from './RacingStandingFactory';
|
||||
import { RacingTeamFactory } from './RacingTeamFactory';
|
||||
|
||||
export type Friendship = {
|
||||
driverId: string;
|
||||
friendId: string;
|
||||
};
|
||||
|
||||
export type RacingSeed = {
|
||||
drivers: Driver[];
|
||||
leagues: League[];
|
||||
races: Race[];
|
||||
results: RaceResult[];
|
||||
standings: Standing[];
|
||||
leagueMemberships: LeagueMembership[];
|
||||
raceRegistrations: RaceRegistration[];
|
||||
teams: Team[];
|
||||
teamMemberships: TeamMembership[];
|
||||
friendships: Friendship[];
|
||||
feedEvents: FeedItem[];
|
||||
};
|
||||
|
||||
export type RacingSeedOptions = {
|
||||
driverCount?: number;
|
||||
baseDate?: Date;
|
||||
};
|
||||
|
||||
export const racingSeedDefaults: Readonly<
|
||||
Required<RacingSeedOptions>
|
||||
> = {
|
||||
driverCount: 32,
|
||||
baseDate: new Date('2025-01-15T12:00:00.000Z'),
|
||||
};
|
||||
|
||||
class RacingSeedFactory {
|
||||
private readonly driverCount: number;
|
||||
private readonly baseDate: Date;
|
||||
|
||||
constructor(options: RacingSeedOptions) {
|
||||
this.driverCount = options.driverCount ?? racingSeedDefaults.driverCount;
|
||||
this.baseDate = options.baseDate ?? racingSeedDefaults.baseDate;
|
||||
}
|
||||
|
||||
create(): RacingSeed {
|
||||
const driverFactory = new RacingDriverFactory(this.driverCount, this.baseDate);
|
||||
const leagueFactory = new RacingLeagueFactory(this.baseDate);
|
||||
const raceFactory = new RacingRaceFactory(this.baseDate);
|
||||
const resultFactory = new RacingResultFactory();
|
||||
const standingFactory = new RacingStandingFactory();
|
||||
const membershipFactory = new RacingMembershipFactory(this.baseDate);
|
||||
const teamFactory = new RacingTeamFactory(this.baseDate);
|
||||
const friendshipFactory = new RacingFriendshipFactory();
|
||||
const feedFactory = new RacingFeedFactory(this.baseDate);
|
||||
|
||||
const drivers = driverFactory.create();
|
||||
const leagues = leagueFactory.create();
|
||||
const races = raceFactory.create(leagues);
|
||||
const results = resultFactory.create(drivers, races);
|
||||
const standings = standingFactory.create(leagues, races, results);
|
||||
const leagueMemberships = membershipFactory.createLeagueMemberships(drivers, leagues);
|
||||
const raceRegistrations = membershipFactory.createRaceRegistrations(races);
|
||||
const teams = teamFactory.createTeams();
|
||||
const teamMemberships = teamFactory.createTeamMemberships(drivers, teams);
|
||||
const friendships = friendshipFactory.create(drivers);
|
||||
const feedEvents = feedFactory.create(drivers, friendships, races, leagues);
|
||||
|
||||
return {
|
||||
drivers,
|
||||
leagues,
|
||||
races,
|
||||
results,
|
||||
standings,
|
||||
leagueMemberships,
|
||||
raceRegistrations,
|
||||
teams,
|
||||
teamMemberships,
|
||||
friendships,
|
||||
feedEvents,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createRacingSeed(options: RacingSeedOptions = {}): RacingSeed {
|
||||
return new RacingSeedFactory(options).create();
|
||||
}
|
||||
59
adapters/bootstrap/racing/RacingStandingFactory.ts
Normal file
59
adapters/bootstrap/racing/RacingStandingFactory.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
import { getPointsSystems } from '../PointsSystems';
|
||||
|
||||
export class RacingStandingFactory {
|
||||
create(leagues: League[], races: Race[], results: RaceResult[]): Standing[] {
|
||||
const pointsSystems = getPointsSystems();
|
||||
|
||||
const racesByLeague = new Map<string, Set<string>>();
|
||||
for (const race of races) {
|
||||
if (race.status !== 'completed') continue;
|
||||
|
||||
const set = racesByLeague.get(race.leagueId) ?? new Set<string>();
|
||||
set.add(race.id);
|
||||
racesByLeague.set(race.leagueId, set);
|
||||
}
|
||||
|
||||
const standings: Standing[] = [];
|
||||
|
||||
for (const league of leagues) {
|
||||
const leagueId = league.id.toString();
|
||||
const completedRaceIds = racesByLeague.get(leagueId) ?? new Set<string>();
|
||||
if (completedRaceIds.size === 0) continue;
|
||||
|
||||
const pointsTable = this.resolvePointsSystem(league, pointsSystems);
|
||||
|
||||
const byDriver = new Map<string, Standing>();
|
||||
|
||||
for (const result of results) {
|
||||
if (!completedRaceIds.has(result.raceId.toString())) continue;
|
||||
|
||||
const driverId = result.driverId.toString();
|
||||
const previousStanding = byDriver.get(driverId) ?? Standing.create({ leagueId, driverId, position: 1 });
|
||||
const nextStanding = previousStanding.addRaceResult(result.position.toNumber(), pointsTable);
|
||||
byDriver.set(driverId, nextStanding);
|
||||
}
|
||||
|
||||
const sorted = Array.from(byDriver.values()).sort((a, b) => {
|
||||
if (b.points.toNumber() !== a.points.toNumber()) return b.points.toNumber() - a.points.toNumber();
|
||||
if (b.wins !== a.wins) return b.wins - a.wins;
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
});
|
||||
|
||||
sorted.forEach((standing, index) => standings.push(standing.updatePosition(index + 1)));
|
||||
}
|
||||
|
||||
return standings;
|
||||
}
|
||||
|
||||
private resolvePointsSystem(
|
||||
league: League,
|
||||
pointsSystems: Record<string, Record<number, number>>,
|
||||
): Record<number, number> {
|
||||
const settings = league.settings;
|
||||
return settings.customPoints ?? pointsSystems[settings.pointsSystem] ?? pointsSystems['f1-2024'] ?? {};
|
||||
}
|
||||
}
|
||||
92
adapters/bootstrap/racing/RacingTeamFactory.ts
Normal file
92
adapters/bootstrap/racing/RacingTeamFactory.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import type { TeamMembership } from '@core/racing/domain/types/TeamMembership';
|
||||
|
||||
export class RacingTeamFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
|
||||
createTeams(): Team[] {
|
||||
return [
|
||||
Team.create({
|
||||
id: 'team-1',
|
||||
name: 'Apex Racing',
|
||||
tag: 'APEX',
|
||||
description: 'Demo team focused on clean racing.',
|
||||
ownerId: 'driver-1',
|
||||
leagues: ['league-5'],
|
||||
createdAt: this.addDays(this.baseDate, -100),
|
||||
}),
|
||||
Team.create({
|
||||
id: 'team-2',
|
||||
name: 'Night Owls',
|
||||
tag: 'NITE',
|
||||
description: 'Late-night grinders and endurance lovers.',
|
||||
ownerId: 'driver-2',
|
||||
leagues: ['league-4'],
|
||||
createdAt: this.addDays(this.baseDate, -90),
|
||||
}),
|
||||
Team.create({
|
||||
id: 'team-3',
|
||||
name: 'Club Legends',
|
||||
tag: 'CLUB',
|
||||
description: 'A casual team for ladder climbing.',
|
||||
ownerId: 'driver-3',
|
||||
leagues: ['league-3'],
|
||||
createdAt: this.addDays(this.baseDate, -80),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
createTeamMemberships(drivers: Driver[], teams: Team[]): TeamMembership[] {
|
||||
const memberships: TeamMembership[] = [];
|
||||
|
||||
const team1 = teams.find((t) => t.id === 'team-1');
|
||||
const team2 = teams.find((t) => t.id === 'team-2');
|
||||
const team3 = teams.find((t) => t.id === 'team-3');
|
||||
|
||||
if (team1) {
|
||||
const members = drivers.slice(0, 6);
|
||||
members.forEach((d, idx) => {
|
||||
memberships.push({
|
||||
teamId: team1.id,
|
||||
driverId: d.id,
|
||||
role: d.id === team1.ownerId.toString() ? 'owner' : idx === 1 ? 'manager' : 'driver',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -50),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (team2) {
|
||||
const members = drivers.slice(6, 12);
|
||||
members.forEach((d) => {
|
||||
memberships.push({
|
||||
teamId: team2.id,
|
||||
driverId: d.id,
|
||||
role: d.id === team2.ownerId.toString() ? 'owner' : 'driver',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -45),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (team3) {
|
||||
const members = drivers.slice(12, 18);
|
||||
members.forEach((d) => {
|
||||
memberships.push({
|
||||
teamId: team3.id,
|
||||
driverId: d.id,
|
||||
role: d.id === team3.ownerId.toString() ? 'owner' : 'driver',
|
||||
status: 'active',
|
||||
joinedAt: this.addDays(this.baseDate, -40),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
|
||||
import { SeedInMemoryRacingData } from '../../../../../adapters/bootstrap/SeedInMemoryRacingData';
|
||||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { BootstrapProviders } from './BootstrapProviders';
|
||||
|
||||
@@ -6,16 +7,33 @@ import { BootstrapProviders } from './BootstrapProviders';
|
||||
providers: BootstrapProviders,
|
||||
})
|
||||
export class BootstrapModule implements OnModuleInit {
|
||||
constructor(private readonly ensureInitialData: EnsureInitialData) {}
|
||||
constructor(
|
||||
private readonly ensureInitialData: EnsureInitialData,
|
||||
private readonly seedInMemoryRacingData: SeedInMemoryRacingData,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
console.log('[Bootstrap] Initializing application data...');
|
||||
try {
|
||||
await this.ensureInitialData.execute();
|
||||
|
||||
if (this.shouldSeedInMemory()) {
|
||||
await this.seedInMemoryRacingData.execute();
|
||||
}
|
||||
|
||||
console.log('[Bootstrap] Application data initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('[Bootstrap] Failed to initialize application data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldSeedInMemory(): boolean {
|
||||
const configured = (process.env.GRIDPILOT_API_PERSISTENCE ?? '').toLowerCase();
|
||||
if (configured) {
|
||||
return configured === 'inmemory';
|
||||
}
|
||||
|
||||
return process.env.DATABASE_URL === undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { EnsureInitialData, type InMemorySeedDependencies } from '../../../../../adapters/bootstrap/EnsureInitialData';
|
||||
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
|
||||
import { SeedInMemoryRacingData, type InMemorySeedDependencies } from '../../../../../adapters/bootstrap/SeedInMemoryRacingData';
|
||||
import { SignupWithEmailUseCase, type SignupWithEmailResult } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
|
||||
import {
|
||||
CreateAchievementUseCase,
|
||||
@@ -87,6 +88,19 @@ export const BootstrapProviders: Provider[] = [
|
||||
signupUseCase: SignupWithEmailUseCase,
|
||||
createAchievementUseCase: CreateAchievementUseCase,
|
||||
logger: Logger,
|
||||
) => {
|
||||
return new EnsureInitialData(signupUseCase, createAchievementUseCase, logger);
|
||||
},
|
||||
inject: [
|
||||
SIGNUP_USE_CASE_TOKEN,
|
||||
CREATE_ACHIEVEMENT_USE_CASE_TOKEN,
|
||||
'Logger',
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: SeedInMemoryRacingData,
|
||||
useFactory: (
|
||||
logger: Logger,
|
||||
driverRepository: InMemorySeedDependencies['driverRepository'],
|
||||
leagueRepository: InMemorySeedDependencies['leagueRepository'],
|
||||
raceRepository: InMemorySeedDependencies['raceRepository'],
|
||||
@@ -113,11 +127,9 @@ export const BootstrapProviders: Provider[] = [
|
||||
socialGraphRepository,
|
||||
};
|
||||
|
||||
return new EnsureInitialData(signupUseCase, createAchievementUseCase, logger, deps);
|
||||
return new SeedInMemoryRacingData(logger, deps);
|
||||
},
|
||||
inject: [
|
||||
SIGNUP_USE_CASE_TOKEN,
|
||||
CREATE_ACHIEVEMENT_USE_CASE_TOKEN,
|
||||
'Logger',
|
||||
'IDriverRepository',
|
||||
'ILeagueRepository',
|
||||
|
||||
Reference in New Issue
Block a user