seed data

This commit is contained in:
2025-12-30 18:33:15 +01:00
parent 83371ea839
commit 92226800df
306 changed files with 1753 additions and 501 deletions

View File

@@ -1,32 +1,18 @@
/**
* Infrastructure Adapter: InMemoryTeamStatsRepository
*
* In-memory implementation of ITeamStatsRepository.
* Stores computed team statistics for caching and frontend queries.
*/
import type { Logger } from '@core/shared/application/Logger';
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { Logger } from '@core/shared/application';
export class InMemoryTeamStatsRepository implements ITeamStatsRepository {
private stats = new Map<string, TeamStats>();
private readonly stats = new Map<string, TeamStats>();
constructor(private readonly logger: Logger) {
this.logger.info('[InMemoryTeamStatsRepository] Initialized.');
}
constructor(private readonly logger: Logger) {}
async getTeamStats(teamId: string): Promise<TeamStats | null> {
this.logger.debug(`[InMemoryTeamStatsRepository] Getting stats for team: ${teamId}`);
return this.stats.get(teamId) ?? null;
}
getTeamStatsSync(teamId: string): TeamStats | null {
this.logger.debug(`[InMemoryTeamStatsRepository] Getting stats (sync) for team: ${teamId}`);
return this.stats.get(teamId) ?? null;
return this.stats.get(teamId) || null;
}
async saveTeamStats(teamId: string, stats: TeamStats): Promise<void> {
this.logger.debug(`[InMemoryTeamStatsRepository] Saving stats for team: ${teamId}`);
this.logger.debug(`[InMemoryTeamStatsRepository] Saving stats for team: ${teamId}`, stats);
this.stats.set(teamId, stats);
}
@@ -36,7 +22,7 @@ export class InMemoryTeamStatsRepository implements ITeamStatsRepository {
}
async clear(): Promise<void> {
this.logger.info('[InMemoryTeamStatsRepository] Clearing all stats');
this.logger.debug('[InMemoryTeamStatsRepository] Clearing all stats');
this.stats.clear();
}
}
}

View File

@@ -19,4 +19,7 @@ export class DriverOrmEntity {
@Column({ type: 'timestamptz' })
joinedAt!: Date;
@Column({ type: 'text', nullable: true })
category!: string | null;
}

View File

@@ -19,6 +19,9 @@ export class LeagueOrmEntity {
@Column({ type: 'jsonb' })
settings!: SerializedLeagueSettings;
@Column({ type: 'text', nullable: true })
category!: string | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date;

View File

@@ -20,6 +20,12 @@ export class TeamOrmEntity {
@Column({ type: 'uuid', array: true })
leagues!: string[];
@Column({ type: 'text', nullable: true })
category!: string | null;
@Column({ type: 'boolean', default: false })
isRecruiting!: boolean;
@Column({ type: 'timestamptz' })
createdAt!: Date;
}

View File

@@ -0,0 +1,34 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity({ name: 'team_stats' })
export class TeamStatsOrmEntity {
@PrimaryColumn({ type: 'uuid' })
teamId!: string;
@Column({ type: 'text' })
logoUrl!: string;
@Column({ type: 'text' })
performanceLevel!: string;
@Column({ type: 'text' })
specialization!: string;
@Column({ type: 'text' })
region!: string;
@Column({ type: 'text', array: true })
languages!: string[];
@Column({ type: 'integer' })
totalWins!: number;
@Column({ type: 'integer' })
totalRaces!: number;
@Column({ type: 'integer' })
rating!: number;
@Column({ type: 'text', nullable: true })
category!: string | null;
}

View File

@@ -12,6 +12,7 @@ export class DriverOrmMapper {
entity.country = domain.country.toString();
entity.bio = domain.bio?.toString() ?? null;
entity.joinedAt = domain.joinedAt.toDate();
entity.category = domain.category ?? null;
return entity;
}
@@ -24,14 +25,31 @@ export class DriverOrmMapper {
assertNonEmptyString(entityName, 'country', entity.country);
assertDate(entityName, 'joinedAt', entity.joinedAt);
assertOptionalStringOrNull(entityName, 'bio', entity.bio);
assertOptionalStringOrNull(entityName, 'category', entity.category);
return Driver.rehydrate({
const props: {
id: string;
iracingId: string;
name: string;
country: string;
bio?: string;
joinedAt: Date;
category?: string;
} = {
id: entity.id,
iracingId: entity.iracingId,
name: entity.name,
country: entity.country,
...(entity.bio !== null && entity.bio !== undefined ? { bio: entity.bio } : {}),
joinedAt: entity.joinedAt,
});
};
if (entity.bio !== null && entity.bio !== undefined) {
props.bio = entity.bio;
}
if (entity.category !== null && entity.category !== undefined) {
props.category = entity.category;
}
return Driver.rehydrate(props);
}
}

View File

@@ -155,6 +155,7 @@ export class LeagueOrmMapper {
entity.description = domain.description.toString();
entity.ownerId = domain.ownerId.toString();
entity.settings = serializeLeagueSettings(domain.settings);
entity.category = domain.category ?? null;
entity.createdAt = domain.createdAt.toDate();
entity.participantCount = domain.getParticipantCount();
entity.discordUrl = domain.socialLinks?.discordUrl ?? null;
@@ -172,6 +173,7 @@ export class LeagueOrmMapper {
description: entity.description,
ownerId: entity.ownerId,
settings,
category: entity.category ?? undefined,
createdAt: entity.createdAt,
participantCount: entity.participantCount,
...(entity.discordUrl || entity.youtubeUrl || entity.websiteUrl

View File

@@ -25,6 +25,8 @@ export class TeamOrmMapper {
entity.description = domain.description.toString();
entity.ownerId = domain.ownerId.toString();
entity.leagues = domain.leagues.map((l) => l.toString());
entity.category = domain.category ?? null;
entity.isRecruiting = domain.isRecruiting;
entity.createdAt = domain.createdAt.toDate();
return entity;
}
@@ -45,15 +47,32 @@ export class TeamOrmMapper {
}
try {
return Team.rehydrate({
const rehydrateProps: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
category?: string;
isRecruiting: boolean;
createdAt: Date;
} = {
id: entity.id,
name: entity.name,
tag: entity.tag,
description: entity.description,
ownerId: entity.ownerId,
leagues: entity.leagues,
isRecruiting: entity.isRecruiting ?? false,
createdAt: entity.createdAt,
});
};
if (entity.category !== null && entity.category !== undefined) {
rehydrateProps.category = entity.category;
}
return Team.rehydrate(rehydrateProps);
} catch {
throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: '__root', reason: 'invalid_shape' });
}

View File

@@ -0,0 +1,60 @@
import type { TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
import { TeamStatsOrmEntity } from '../entities/TeamStatsOrmEntity';
import {
assertNonEmptyString,
assertInteger,
assertArray,
assertEnumValue
} from '../schema/TypeOrmSchemaGuards';
const PERFORMANCE_LEVELS = ['beginner', 'intermediate', 'advanced', 'pro'] as const;
const SPECIALIZATIONS = ['endurance', 'sprint', 'mixed'] as const;
export class TeamStatsOrmMapper {
toOrmEntity(teamId: string, domain: TeamStats): TeamStatsOrmEntity {
const entity = new TeamStatsOrmEntity();
entity.teamId = teamId;
entity.logoUrl = domain.logoUrl;
entity.performanceLevel = domain.performanceLevel;
entity.specialization = domain.specialization;
entity.region = domain.region;
entity.languages = domain.languages;
entity.totalWins = domain.totalWins;
entity.totalRaces = domain.totalRaces;
entity.rating = domain.rating;
entity.category = domain.category ?? null;
return entity;
}
toDomain(entity: TeamStatsOrmEntity): TeamStats {
const entityName = 'TeamStats';
assertNonEmptyString(entityName, 'teamId', entity.teamId);
assertNonEmptyString(entityName, 'logoUrl', entity.logoUrl);
assertEnumValue(entityName, 'performanceLevel', entity.performanceLevel, PERFORMANCE_LEVELS);
assertEnumValue(entityName, 'specialization', entity.specialization, SPECIALIZATIONS);
assertNonEmptyString(entityName, 'region', entity.region);
assertArray(entityName, 'languages', entity.languages);
assertInteger(entityName, 'totalWins', entity.totalWins);
assertInteger(entityName, 'totalRaces', entity.totalRaces);
assertInteger(entityName, 'rating', entity.rating);
const result: TeamStats = {
logoUrl: entity.logoUrl,
performanceLevel: entity.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro',
specialization: entity.specialization as 'endurance' | 'sprint' | 'mixed',
region: entity.region,
languages: entity.languages,
totalWins: entity.totalWins,
totalRaces: entity.totalRaces,
rating: entity.rating,
};
if (entity.category !== null && entity.category !== undefined) {
result.category = entity.category;
}
return result;
}
}

View File

@@ -0,0 +1,37 @@
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { Repository } from 'typeorm';
import { TeamStatsOrmEntity } from '../entities/TeamStatsOrmEntity';
import { TeamStatsOrmMapper } from '../mappers/TeamStatsOrmMapper';
export class TypeOrmTeamStatsRepository implements ITeamStatsRepository {
constructor(
private readonly repo: Repository<TeamStatsOrmEntity>,
private readonly mapper: TeamStatsOrmMapper,
) {}
async getTeamStats(teamId: string): Promise<TeamStats | null> {
const entity = await this.repo.findOne({ where: { teamId } });
return entity ? this.mapper.toDomain(entity) : null;
}
async saveTeamStats(teamId: string, stats: TeamStats): Promise<void> {
const entity = this.mapper.toOrmEntity(teamId, stats);
await this.repo.save(entity);
}
async getAllStats(): Promise<Map<string, TeamStats>> {
const entities = await this.repo.find();
const statsMap = new Map<string, TeamStats>();
for (const entity of entities) {
statsMap.set(entity.teamId, this.mapper.toDomain(entity));
}
return statsMap;
}
async clear(): Promise<void> {
await this.repo.clear();
}
}