harden media
This commit is contained in:
@@ -2,6 +2,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { InMemoryDriverRepository } from './InMemoryDriverRepository';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
describe('InMemoryDriverRepository', () => {
|
||||
let repository: InMemoryDriverRepository;
|
||||
@@ -17,13 +18,23 @@ describe('InMemoryDriverRepository', () => {
|
||||
repository = new InMemoryDriverRepository(mockLogger);
|
||||
});
|
||||
|
||||
const createTestDriver = (id: string, iracingId: string, name: string, country: string) => {
|
||||
return Driver.create({
|
||||
const createTestDriver = (id: string, iracingId: string, name: string, country: string, avatarRef?: MediaReference) => {
|
||||
const props: {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarRef?: MediaReference;
|
||||
} = {
|
||||
id,
|
||||
iracingId,
|
||||
name,
|
||||
country,
|
||||
});
|
||||
};
|
||||
if (avatarRef !== undefined) {
|
||||
props.avatarRef = avatarRef;
|
||||
}
|
||||
return Driver.create(props);
|
||||
};
|
||||
|
||||
describe('constructor', () => {
|
||||
@@ -188,4 +199,115 @@ describe('InMemoryDriverRepository', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serialization with MediaReference', () => {
|
||||
it('should serialize driver with uploaded avatarRef', async () => {
|
||||
const driver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createUploaded('media-123'));
|
||||
await repository.create(driver);
|
||||
|
||||
const serialized = repository.serialize(driver);
|
||||
expect(serialized.avatarRef).toEqual({ type: 'uploaded', mediaId: 'media-123' });
|
||||
});
|
||||
|
||||
it('should serialize driver with system-default avatarRef', async () => {
|
||||
const driver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createSystemDefault('avatar'));
|
||||
await repository.create(driver);
|
||||
|
||||
const serialized = repository.serialize(driver);
|
||||
expect(serialized.avatarRef).toEqual({ type: 'system-default', variant: 'avatar' });
|
||||
});
|
||||
|
||||
it('should serialize driver with generated avatarRef', async () => {
|
||||
const driver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createGenerated('gen-123'));
|
||||
await repository.create(driver);
|
||||
|
||||
const serialized = repository.serialize(driver);
|
||||
expect(serialized.avatarRef).toEqual({ type: 'generated', generationRequestId: 'gen-123' });
|
||||
});
|
||||
|
||||
it('should deserialize driver with uploaded avatarRef', () => {
|
||||
const data = {
|
||||
id: '1',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
bio: null,
|
||||
joinedAt: new Date().toISOString(),
|
||||
category: null,
|
||||
avatarRef: { type: 'uploaded', mediaId: 'media-123' },
|
||||
};
|
||||
|
||||
const driver = repository.deserialize(data);
|
||||
expect(driver.id).toBe('1');
|
||||
expect(driver.avatarRef.type).toBe('uploaded');
|
||||
expect(driver.avatarRef.mediaId).toBe('media-123');
|
||||
});
|
||||
|
||||
it('should deserialize driver with system-default avatarRef', () => {
|
||||
const data = {
|
||||
id: '1',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
bio: null,
|
||||
joinedAt: new Date().toISOString(),
|
||||
category: null,
|
||||
avatarRef: { type: 'system-default', variant: 'avatar' },
|
||||
};
|
||||
|
||||
const driver = repository.deserialize(data);
|
||||
expect(driver.id).toBe('1');
|
||||
expect(driver.avatarRef.type).toBe('system-default');
|
||||
expect(driver.avatarRef.variant).toBe('avatar');
|
||||
});
|
||||
|
||||
it('should deserialize driver with generated avatarRef', () => {
|
||||
const data = {
|
||||
id: '1',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
bio: null,
|
||||
joinedAt: new Date().toISOString(),
|
||||
category: null,
|
||||
avatarRef: { type: 'generated', generationRequestId: 'gen-123' },
|
||||
};
|
||||
|
||||
const driver = repository.deserialize(data);
|
||||
expect(driver.id).toBe('1');
|
||||
expect(driver.avatarRef.type).toBe('generated');
|
||||
expect(driver.avatarRef.generationRequestId).toBe('gen-123');
|
||||
});
|
||||
|
||||
it('should deserialize driver without avatarRef (backward compatibility)', () => {
|
||||
const data = {
|
||||
id: '1',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
bio: null,
|
||||
joinedAt: new Date().toISOString(),
|
||||
category: null,
|
||||
};
|
||||
|
||||
const driver = repository.deserialize(data);
|
||||
expect(driver.id).toBe('1');
|
||||
expect(driver.avatarRef.type).toBe('system-default');
|
||||
expect(driver.avatarRef.variant).toBe('avatar');
|
||||
});
|
||||
|
||||
it('should roundtrip serialize and deserialize with avatarRef', async () => {
|
||||
const originalDriver = createTestDriver('1', '12345', 'Test Driver', 'US', MediaReference.createUploaded('media-456'));
|
||||
await repository.create(originalDriver);
|
||||
|
||||
const serialized = repository.serialize(originalDriver);
|
||||
const deserialized = repository.deserialize(serialized);
|
||||
|
||||
expect(deserialized.id).toBe(originalDriver.id);
|
||||
expect(deserialized.iracingId.toString()).toBe(originalDriver.iracingId.toString());
|
||||
expect(deserialized.name.toString()).toBe(originalDriver.name.toString());
|
||||
expect(deserialized.country.toString()).toBe(originalDriver.country.toString());
|
||||
expect(deserialized.avatarRef.equals(originalDriver.avatarRef)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { Logger } from '@core/shared/application';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
export class InMemoryDriverRepository implements IDriverRepository {
|
||||
private drivers: Map<string, Driver> = new Map();
|
||||
@@ -91,4 +92,49 @@ export class InMemoryDriverRepository implements IDriverRepository {
|
||||
this.logger.debug(`[InMemoryDriverRepository] Checking existence of driver with iRacing ID: ${iracingId}`);
|
||||
return Promise.resolve(this.iracingIdIndex.has(iracingId));
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization methods for persistence
|
||||
serialize(driver: Driver): Record<string, unknown> {
|
||||
return {
|
||||
id: driver.id,
|
||||
iracingId: driver.iracingId.toString(),
|
||||
name: driver.name.toString(),
|
||||
country: driver.country.toString(),
|
||||
bio: driver.bio?.toString() ?? null,
|
||||
joinedAt: driver.joinedAt.toDate().toISOString(),
|
||||
category: driver.category ?? null,
|
||||
avatarRef: driver.avatarRef.toJSON(),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: Record<string, unknown>): Driver {
|
||||
const props: {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bio?: string;
|
||||
joinedAt: Date;
|
||||
category?: string;
|
||||
avatarRef?: MediaReference;
|
||||
} = {
|
||||
id: data.id as string,
|
||||
iracingId: data.iracingId as string,
|
||||
name: data.name as string,
|
||||
country: data.country as string,
|
||||
joinedAt: new Date(data.joinedAt as string),
|
||||
};
|
||||
|
||||
if (data.bio !== null && data.bio !== undefined) {
|
||||
props.bio = data.bio as string;
|
||||
}
|
||||
if (data.category !== null && data.category !== undefined) {
|
||||
props.category = data.category as string;
|
||||
}
|
||||
if (data.avatarRef !== null && data.avatarRef !== undefined) {
|
||||
props.avatarRef = MediaReference.fromJSON(data.avatarRef as Record<string, unknown>);
|
||||
}
|
||||
|
||||
return Driver.rehydrate(props);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Logger } from '@core/shared/application';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
export class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
private leagues: Map<string, League> = new Map();
|
||||
@@ -132,4 +133,71 @@ export class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization methods for persistence
|
||||
serialize(league: League): Record<string, unknown> {
|
||||
return {
|
||||
id: league.id.toString(),
|
||||
name: league.name.toString(),
|
||||
description: league.description.toString(),
|
||||
ownerId: league.ownerId.toString(),
|
||||
settings: league.settings,
|
||||
category: league.category ?? null,
|
||||
createdAt: league.createdAt.toDate().toISOString(),
|
||||
participantCount: league.getParticipantCount(),
|
||||
socialLinks: league.socialLinks
|
||||
? {
|
||||
discordUrl: league.socialLinks.discordUrl,
|
||||
youtubeUrl: league.socialLinks.youtubeUrl,
|
||||
websiteUrl: league.socialLinks.websiteUrl,
|
||||
}
|
||||
: undefined,
|
||||
logoRef: league.logoRef.toJSON(),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: Record<string, unknown>): League {
|
||||
const props: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settings: any;
|
||||
category?: string;
|
||||
createdAt: Date;
|
||||
participantCount: number;
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
logoRef?: MediaReference;
|
||||
} = {
|
||||
id: data.id as string,
|
||||
name: data.name as string,
|
||||
description: data.description as string,
|
||||
ownerId: data.ownerId as string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settings: data.settings as any,
|
||||
createdAt: new Date(data.createdAt as string),
|
||||
participantCount: data.participantCount as number,
|
||||
};
|
||||
|
||||
if (data.category !== null && data.category !== undefined) {
|
||||
props.category = data.category as string;
|
||||
}
|
||||
if (data.socialLinks !== null && data.socialLinks !== undefined) {
|
||||
props.socialLinks = data.socialLinks as {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
}
|
||||
if (data.logoRef !== null && data.logoRef !== undefined) {
|
||||
props.logoRef = MediaReference.fromJSON(data.logoRef as Record<string, unknown>);
|
||||
}
|
||||
|
||||
return League.rehydrate(props);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,10 @@
|
||||
* Stores data in a Map structure.
|
||||
*/
|
||||
|
||||
import type { Team } from '@core/racing/domain/entities/Team';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
export class InMemoryTeamRepository implements ITeamRepository {
|
||||
private teams: Map<string, Team>;
|
||||
@@ -122,4 +123,53 @@ export class InMemoryTeamRepository implements ITeamRepository {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization methods for persistence
|
||||
serialize(team: Team): Record<string, unknown> {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name.toString(),
|
||||
tag: team.tag.toString(),
|
||||
description: team.description.toString(),
|
||||
ownerId: team.ownerId.toString(),
|
||||
leagues: team.leagues.map(l => l.toString()),
|
||||
category: team.category ?? null,
|
||||
isRecruiting: team.isRecruiting,
|
||||
createdAt: team.createdAt.toDate().toISOString(),
|
||||
logoRef: team.logoRef.toJSON(),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: Record<string, unknown>): Team {
|
||||
const props: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
category?: string;
|
||||
isRecruiting: boolean;
|
||||
createdAt: Date;
|
||||
logoRef?: MediaReference;
|
||||
} = {
|
||||
id: data.id as string,
|
||||
name: data.name as string,
|
||||
tag: data.tag as string,
|
||||
description: data.description as string,
|
||||
ownerId: data.ownerId as string,
|
||||
leagues: data.leagues as string[],
|
||||
isRecruiting: data.isRecruiting as boolean,
|
||||
createdAt: new Date(data.createdAt as string),
|
||||
};
|
||||
|
||||
if (data.category !== null && data.category !== undefined) {
|
||||
props.category = data.category as string;
|
||||
}
|
||||
if (data.logoRef !== null && data.logoRef !== undefined) {
|
||||
props.logoRef = MediaReference.fromJSON(data.logoRef as Record<string, unknown>);
|
||||
}
|
||||
|
||||
return Team.rehydrate(props);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* Infrastructure Adapter: InMemoryMediaRepository
|
||||
*
|
||||
* In-memory implementation of IMediaRepository.
|
||||
* Stores URLs for static media assets like logos and images.
|
||||
* Stores URLs for media assets like avatars and logos.
|
||||
*/
|
||||
|
||||
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
|
||||
@@ -11,9 +11,8 @@ import type { Logger } from '@core/shared/application';
|
||||
export class InMemoryMediaRepository implements IMediaRepository {
|
||||
private driverAvatars = new Map<string, string>();
|
||||
private teamLogos = new Map<string, string>();
|
||||
private trackImages = new Map<string, string>();
|
||||
private categoryIcons = new Map<string, string>();
|
||||
private sponsorLogos = new Map<string, string>();
|
||||
private leagueLogos = new Map<string, string>();
|
||||
private leagueCovers = new Map<string, string>();
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('[InMemoryMediaRepository] Initialized.');
|
||||
@@ -27,16 +26,12 @@ export class InMemoryMediaRepository implements IMediaRepository {
|
||||
return this.teamLogos.get(teamId) ?? null;
|
||||
}
|
||||
|
||||
async getTrackImage(trackId: string): Promise<string | null> {
|
||||
return this.trackImages.get(trackId) ?? null;
|
||||
async getLeagueLogo(leagueId: string): Promise<string | null> {
|
||||
return this.leagueLogos.get(leagueId) ?? null;
|
||||
}
|
||||
|
||||
async getCategoryIcon(categoryId: string): Promise<string | null> {
|
||||
return this.categoryIcons.get(categoryId) ?? null;
|
||||
}
|
||||
|
||||
async getSponsorLogo(sponsorId: string): Promise<string | null> {
|
||||
return this.sponsorLogos.get(sponsorId) ?? null;
|
||||
async getLeagueCover(leagueId: string): Promise<string | null> {
|
||||
return this.leagueCovers.get(leagueId) ?? null;
|
||||
}
|
||||
|
||||
// Helper methods for seeding
|
||||
@@ -48,23 +43,18 @@ export class InMemoryMediaRepository implements IMediaRepository {
|
||||
this.teamLogos.set(teamId, url);
|
||||
}
|
||||
|
||||
setTrackImage(trackId: string, url: string): void {
|
||||
this.trackImages.set(trackId, url);
|
||||
setLeagueLogo(leagueId: string, url: string): void {
|
||||
this.leagueLogos.set(leagueId, url);
|
||||
}
|
||||
|
||||
setCategoryIcon(categoryId: string, url: string): void {
|
||||
this.categoryIcons.set(categoryId, url);
|
||||
}
|
||||
|
||||
setSponsorLogo(sponsorId: string, url: string): void {
|
||||
this.sponsorLogos.set(sponsorId, url);
|
||||
setLeagueCover(leagueId: string, url: string): void {
|
||||
this.leagueCovers.set(leagueId, url);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.driverAvatars.clear();
|
||||
this.teamLogos.clear();
|
||||
this.trackImages.clear();
|
||||
this.categoryIcons.clear();
|
||||
this.sponsorLogos.clear();
|
||||
this.leagueLogos.clear();
|
||||
this.leagueCovers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,7 @@ export class DriverOrmEntity {
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
category!: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
avatarRef!: Record<string, unknown> | null;
|
||||
}
|
||||
@@ -36,4 +36,7 @@ export class LeagueOrmEntity {
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
websiteUrl!: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
logoRef!: Record<string, unknown> | null;
|
||||
}
|
||||
@@ -28,6 +28,9 @@ export class TeamOrmEntity {
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
logoRef!: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@Entity({ name: 'racing_team_memberships' })
|
||||
|
||||
@@ -5,9 +5,6 @@ export class TeamStatsOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
teamId!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
logoUrl!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
performanceLevel!: string;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
import { DriverOrmEntity } from '../entities/DriverOrmEntity';
|
||||
import { assertDate, assertNonEmptyString, assertOptionalStringOrNull } from '../schema/TypeOrmSchemaGuards';
|
||||
@@ -13,6 +14,8 @@ export class DriverOrmMapper {
|
||||
entity.bio = domain.bio?.toString() ?? null;
|
||||
entity.joinedAt = domain.joinedAt.toDate();
|
||||
entity.category = domain.category ?? null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
entity.avatarRef = domain.avatarRef.toJSON() as any;
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -35,6 +38,7 @@ export class DriverOrmMapper {
|
||||
bio?: string;
|
||||
joinedAt: Date;
|
||||
category?: string;
|
||||
avatarRef?: MediaReference;
|
||||
} = {
|
||||
id: entity.id,
|
||||
iracingId: entity.iracingId,
|
||||
@@ -49,6 +53,10 @@ export class DriverOrmMapper {
|
||||
if (entity.category !== null && entity.category !== undefined) {
|
||||
props.category = entity.category;
|
||||
}
|
||||
if (entity.avatarRef !== null && entity.avatarRef !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
props.avatarRef = MediaReference.fromJSON(entity.avatarRef as any);
|
||||
}
|
||||
|
||||
return Driver.rehydrate(props);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { League, type LeagueSettings } from '@core/racing/domain/entities/League';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
import { LeagueOrmEntity } from '../entities/LeagueOrmEntity';
|
||||
import { TypeOrmPersistenceSchemaError } from '../errors/TypeOrmPersistenceSchemaError';
|
||||
@@ -161,6 +162,8 @@ export class LeagueOrmMapper {
|
||||
entity.discordUrl = domain.socialLinks?.discordUrl ?? null;
|
||||
entity.youtubeUrl = domain.socialLinks?.youtubeUrl ?? null;
|
||||
entity.websiteUrl = domain.socialLinks?.websiteUrl ?? null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
entity.logoRef = domain.logoRef.toJSON() as any;
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -185,6 +188,10 @@ export class LeagueOrmMapper {
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(entity.logoRef !== null && entity.logoRef !== undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
? { logoRef: MediaReference.fromJSON(entity.logoRef as any) }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
import { TypeOrmPersistenceSchemaError } from '../errors/TypeOrmPersistenceSchemaError';
|
||||
|
||||
@@ -28,6 +29,8 @@ export class TeamOrmMapper {
|
||||
entity.category = domain.category ?? null;
|
||||
entity.isRecruiting = domain.isRecruiting;
|
||||
entity.createdAt = domain.createdAt.toDate();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
entity.logoRef = domain.logoRef.toJSON() as any;
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -57,6 +60,7 @@ export class TeamOrmMapper {
|
||||
category?: string;
|
||||
isRecruiting: boolean;
|
||||
createdAt: Date;
|
||||
logoRef?: MediaReference;
|
||||
} = {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
@@ -72,6 +76,11 @@ export class TeamOrmMapper {
|
||||
rehydrateProps.category = entity.category;
|
||||
}
|
||||
|
||||
if (entity.logoRef !== null && entity.logoRef !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
rehydrateProps.logoRef = MediaReference.fromJSON(entity.logoRef as any);
|
||||
}
|
||||
|
||||
return Team.rehydrate(rehydrateProps);
|
||||
} catch {
|
||||
throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: '__root', reason: 'invalid_shape' });
|
||||
|
||||
@@ -15,7 +15,6 @@ 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;
|
||||
@@ -31,7 +30,6 @@ export class TeamStatsOrmMapper {
|
||||
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);
|
||||
@@ -41,7 +39,6 @@ export class TeamStatsOrmMapper {
|
||||
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,
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { DataSource } from 'typeorm';
|
||||
|
||||
import { TypeOrmDriverRepository } from './TypeOrmDriverRepository';
|
||||
import { DriverOrmMapper } from '../mappers/DriverOrmMapper';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
describe('TypeOrmDriverRepository', () => {
|
||||
it('constructor requires injected mapper (no internal mapper instantiation)', () => {
|
||||
@@ -33,4 +35,193 @@ describe('TypeOrmDriverRepository', () => {
|
||||
|
||||
await expect(repo.findById('driver-1')).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('persists and retrieves driver with avatarRef (roundtrip test)', async () => {
|
||||
// Create a driver with a specific avatar reference
|
||||
const driver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: '456789',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
avatarRef: MediaReference.createUploaded('media-abc-123'),
|
||||
});
|
||||
|
||||
// Mock entity that would be saved
|
||||
const mockEntity = {
|
||||
id: 'driver-123',
|
||||
iracingId: '456789',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
bio: null,
|
||||
joinedAt: driver.joinedAt.toDate(),
|
||||
category: null,
|
||||
avatarRef: { type: 'uploaded', mediaId: 'media-abc-123' },
|
||||
};
|
||||
|
||||
const savedEntities: any[] = [];
|
||||
|
||||
const repo = {
|
||||
save: async (entity: any) => {
|
||||
savedEntities.push(entity);
|
||||
return entity;
|
||||
},
|
||||
findOne: async () => savedEntities[0] || null,
|
||||
};
|
||||
|
||||
const mapper = new DriverOrmMapper();
|
||||
|
||||
const typeOrmRepo = new TypeOrmDriverRepository(
|
||||
{ getRepository: () => repo } as unknown as DataSource,
|
||||
mapper,
|
||||
);
|
||||
|
||||
// Test save
|
||||
await typeOrmRepo.create(driver);
|
||||
|
||||
expect(savedEntities).toHaveLength(1);
|
||||
expect(savedEntities[0].avatarRef).toEqual({ type: 'uploaded', mediaId: 'media-abc-123' });
|
||||
|
||||
// Test load
|
||||
const loaded = await typeOrmRepo.findById('driver-123');
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded!.avatarRef.type).toBe('uploaded');
|
||||
expect(loaded!.avatarRef.mediaId).toBe('media-abc-123');
|
||||
});
|
||||
|
||||
it('handles system-default avatarRef correctly', async () => {
|
||||
const driver = Driver.create({
|
||||
id: 'driver-456',
|
||||
iracingId: '98765',
|
||||
name: 'Default Driver',
|
||||
country: 'UK',
|
||||
avatarRef: MediaReference.createSystemDefault('avatar'),
|
||||
});
|
||||
|
||||
const mockEntity = {
|
||||
id: 'driver-456',
|
||||
iracingId: '98765',
|
||||
name: 'Default Driver',
|
||||
country: 'UK',
|
||||
bio: null,
|
||||
joinedAt: driver.joinedAt.toDate(),
|
||||
category: null,
|
||||
avatarRef: { type: 'system-default', variant: 'avatar' },
|
||||
};
|
||||
|
||||
const savedEntities: any[] = [];
|
||||
|
||||
const repo = {
|
||||
save: async (entity: any) => {
|
||||
savedEntities.push(entity);
|
||||
return entity;
|
||||
},
|
||||
findOne: async () => savedEntities[0] || null,
|
||||
};
|
||||
|
||||
const mapper = new DriverOrmMapper();
|
||||
|
||||
const typeOrmRepo = new TypeOrmDriverRepository(
|
||||
{ getRepository: () => repo } as unknown as DataSource,
|
||||
mapper,
|
||||
);
|
||||
|
||||
await typeOrmRepo.create(driver);
|
||||
|
||||
expect(savedEntities[0].avatarRef).toEqual({ type: 'system-default', variant: 'avatar' });
|
||||
|
||||
const loaded = await typeOrmRepo.findById('driver-456');
|
||||
expect(loaded!.avatarRef.type).toBe('system-default');
|
||||
expect(loaded!.avatarRef.variant).toBe('avatar');
|
||||
});
|
||||
|
||||
it('handles generated avatarRef correctly', async () => {
|
||||
const driver = Driver.create({
|
||||
id: 'driver-789',
|
||||
iracingId: '11111',
|
||||
name: 'Generated Driver',
|
||||
country: 'DE',
|
||||
avatarRef: MediaReference.createGenerated('gen-req-xyz'),
|
||||
});
|
||||
|
||||
const mockEntity = {
|
||||
id: 'driver-789',
|
||||
iracingId: '11111',
|
||||
name: 'Generated Driver',
|
||||
country: 'DE',
|
||||
bio: null,
|
||||
joinedAt: driver.joinedAt.toDate(),
|
||||
category: null,
|
||||
avatarRef: { type: 'generated', generationRequestId: 'gen-req-xyz' },
|
||||
};
|
||||
|
||||
const savedEntities: any[] = [];
|
||||
|
||||
const repo = {
|
||||
save: async (entity: any) => {
|
||||
savedEntities.push(entity);
|
||||
return entity;
|
||||
},
|
||||
findOne: async () => savedEntities[0] || null,
|
||||
};
|
||||
|
||||
const mapper = new DriverOrmMapper();
|
||||
|
||||
const typeOrmRepo = new TypeOrmDriverRepository(
|
||||
{ getRepository: () => repo } as unknown as DataSource,
|
||||
mapper,
|
||||
);
|
||||
|
||||
await typeOrmRepo.create(driver);
|
||||
|
||||
expect(savedEntities[0].avatarRef).toEqual({ type: 'generated', generationRequestId: 'gen-req-xyz' });
|
||||
|
||||
const loaded = await typeOrmRepo.findById('driver-789');
|
||||
expect(loaded!.avatarRef.type).toBe('generated');
|
||||
expect(loaded!.avatarRef.generationRequestId).toBe('gen-req-xyz');
|
||||
});
|
||||
|
||||
it('handles update with changed avatarRef', async () => {
|
||||
const driver = Driver.create({
|
||||
id: 'driver-update',
|
||||
iracingId: '22222',
|
||||
name: 'Update Driver',
|
||||
country: 'FR',
|
||||
avatarRef: MediaReference.createSystemDefault('avatar'),
|
||||
});
|
||||
|
||||
const savedEntities: any[] = [];
|
||||
|
||||
const repo = {
|
||||
save: async (entity: any) => {
|
||||
savedEntities.push(entity);
|
||||
return entity;
|
||||
},
|
||||
findOne: async () => savedEntities[savedEntities.length - 1] || null,
|
||||
};
|
||||
|
||||
const mapper = new DriverOrmMapper();
|
||||
|
||||
const typeOrmRepo = new TypeOrmDriverRepository(
|
||||
{ getRepository: () => repo } as unknown as DataSource,
|
||||
mapper,
|
||||
);
|
||||
|
||||
// Initial save
|
||||
await typeOrmRepo.create(driver);
|
||||
expect(savedEntities[0].avatarRef).toEqual({ type: 'system-default', variant: 'avatar' });
|
||||
|
||||
// Update with new avatar
|
||||
const updatedDriver = driver.update({
|
||||
avatarRef: MediaReference.createUploaded('new-media-id'),
|
||||
});
|
||||
|
||||
await typeOrmRepo.update(updatedDriver);
|
||||
|
||||
expect(savedEntities).toHaveLength(2);
|
||||
expect(savedEntities[1].avatarRef).toEqual({ type: 'uploaded', mediaId: 'new-media-id' });
|
||||
|
||||
const loaded = await typeOrmRepo.findById('driver-update');
|
||||
expect(loaded!.avatarRef.type).toBe('uploaded');
|
||||
expect(loaded!.avatarRef.mediaId).toBe('new-media-id');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user