seed data

This commit is contained in:
2025-12-30 00:15:35 +01:00
parent 7a853d4e43
commit ccaa39c39c
22 changed files with 1342 additions and 173 deletions

View File

@@ -2,7 +2,7 @@ import type { Logger } from '@core/shared/application';
import type { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import { SeedRacingData, type RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData';
import { Inject, Module, OnModuleInit } from '@nestjs/common';
import { getApiPersistence, getEnableBootstrap } from '../../env';
import { getApiPersistence, getEnableBootstrap, getForceReseed } from '../../env';
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
import { SocialPersistenceModule } from '../../persistence/social/SocialPersistenceModule';
import { AchievementPersistenceModule } from '../../persistence/achievement/AchievementPersistenceModule';
@@ -48,7 +48,21 @@ export class BootstrapModule implements OnModuleInit {
if (persistence !== 'postgres') return false;
if (process.env.NODE_ENV === 'production') return false;
return this.isRacingDatabaseEmpty();
// Check for force reseed flag
const forceReseed = getForceReseed();
if (forceReseed) {
this.logger.info('[Bootstrap] Force reseed enabled via GRIDPILOT_API_FORCE_RESEED');
return true;
}
// Check if database is empty
const isEmpty = await this.isRacingDatabaseEmpty();
if (!isEmpty) {
// Database has data, check if it needs reseeding
return await this.needsReseed();
}
return true;
}
private async isRacingDatabaseEmpty(): Promise<boolean> {
@@ -58,4 +72,24 @@ export class BootstrapModule implements OnModuleInit {
const leagues = await this.seedDeps.leagueRepository.findAll();
return leagues.length === 0;
}
private async needsReseed(): Promise<boolean> {
// Check if driver count is less than expected (150)
// This indicates old seed data that needs updating
try {
const drivers = await this.seedDeps.driverRepository.findAll();
const driverCount = drivers.length;
// If we have fewer than 150 drivers, we need to reseed
if (driverCount < 150) {
this.logger.info(`[Bootstrap] Found ${driverCount} drivers (expected 150), triggering reseed`);
return true;
}
return false;
} catch (error) {
this.logger.warn('[Bootstrap] Error checking driver count for reseed:', error);
return false;
}
}
}

View File

@@ -18,4 +18,19 @@ export class GetDriverOutputDTO {
@ApiProperty()
joinedAt!: string;
@ApiProperty({ required: false })
rating?: number;
@ApiProperty({ required: false })
experienceLevel?: string;
@ApiProperty({ required: false })
wins?: number;
@ApiProperty({ required: false })
podiums?: number;
@ApiProperty({ required: false })
totalRaces?: number;
}

View File

@@ -1,6 +1,7 @@
import { Result } from '@core/shared/application/Result';
import type { Driver } from '@core/racing/domain/entities/Driver';
import type { GetDriverOutputDTO } from '../dtos/GetDriverOutputDTO';
import { DriverStatsStore } from '@adapters/racing/services/DriverStatsStore';
export class DriverPresenter {
private responseModel: GetDriverOutputDTO | null = null;
@@ -18,6 +19,10 @@ export class DriverPresenter {
return;
}
// Get stats from the store
const statsStore = DriverStatsStore.getInstance();
const stats = statsStore.getDriverStats(driver.id);
this.responseModel = {
id: driver.id,
iracingId: driver.iracingId.toString(),
@@ -25,10 +30,25 @@ export class DriverPresenter {
country: driver.country.toString(),
joinedAt: driver.joinedAt.toDate().toISOString(),
...(driver.bio ? { bio: driver.bio.toString() } : {}),
// Add stats fields
...(stats ? {
rating: stats.rating,
wins: stats.wins,
podiums: stats.podiums,
totalRaces: stats.totalRaces,
experienceLevel: this.getExperienceLevel(stats.rating),
} : {}),
};
}
getResponseModel(): GetDriverOutputDTO | null {
return this.responseModel;
}
private getExperienceLevel(rating: number): string {
if (rating >= 1700) return 'veteran';
if (rating >= 1300) return 'advanced';
if (rating >= 1000) return 'intermediate';
return 'beginner';
}
}

View File

@@ -27,5 +27,20 @@ export class TeamListItemDTO {
@ApiProperty({ type: [String], required: false })
languages?: string[];
@ApiProperty({ required: false })
totalWins?: number;
@ApiProperty({ required: false })
totalRaces?: number;
@ApiProperty({ required: false, enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
@ApiProperty({ required: false })
logoUrl?: string;
@ApiProperty({ required: false })
rating?: number;
}

View File

@@ -1,6 +1,7 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetAllTeamsResult } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO';
import { TeamStatsStore } from '@adapters/racing/services/TeamStatsStore';
export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
private model: GetAllTeamsOutputDTO | null = null;
@@ -10,16 +11,41 @@ export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
}
present(result: GetAllTeamsResult): void {
const statsStore = TeamStatsStore.getInstance();
this.model = {
teams: result.teams.map(team => ({
id: team.id,
name: team.name.toString(),
tag: team.tag.toString(),
description: team.description?.toString() || '',
memberCount: team.memberCount,
leagues: team.leagues?.map(l => l.toString()) || [],
// Note: specialization, region, languages not available in output
})),
teams: result.teams.map(team => {
const stats = statsStore.getTeamStats(team.id.toString());
return {
id: team.id,
name: team.name.toString(),
tag: team.tag.toString(),
description: team.description?.toString() || '',
memberCount: team.memberCount,
leagues: team.leagues?.map(l => l.toString()) || [],
// Add stats fields
...(stats ? {
totalWins: stats.totalWins,
totalRaces: stats.totalRaces,
performanceLevel: stats.performanceLevel,
specialization: stats.specialization,
region: stats.region,
languages: stats.languages,
logoUrl: stats.logoUrl,
rating: stats.rating,
} : {
totalWins: 0,
totalRaces: 0,
performanceLevel: 'beginner',
specialization: 'mixed',
region: '',
languages: [],
logoUrl: '',
rating: 0,
}),
};
}),
totalCount: result.totalCount ?? result.teams.length,
};
}

View File

@@ -57,6 +57,21 @@ export function getEnableBootstrap(): boolean {
return isTruthyEnv(raw);
}
/**
* Force reseeding of racing data in development mode.
*
* `GRIDPILOT_API_FORCE_RESEED` uses "truthy" parsing:
* - false when unset / "0" / "false"
* - true otherwise
*
* Only works in non-production environments.
*/
export function getForceReseed(): boolean {
const raw = process.env.GRIDPILOT_API_FORCE_RESEED;
if (raw === undefined) return false;
return isTruthyEnv(raw);
}
/**
* When set, the API will generate `openapi.json` and optionally reduce logging noise.
*