15 KiB
Media Architecture: Complete Analysis & Corrected Solution
Executive Summary
Your media architecture plans contain fundamental flaws based on misunderstandings of the current codebase. This document provides a complete analysis and the correct, streamlined solution.
Key Finding: Your plans solve non-existent problems while ignoring real ones, and over-engineer simple solutions.
Part 1: What's Wrong with Your Plans
1.1 Critical Flaws
Flaw #1: Solving Non-Existent Problems
Your Claim: "Database stores logoUrl in teams table"
// Your plan claims this exists:
teams table: { id: '123', logoUrl: '/images/logos/team-123.jpg' }
Reality:
// adapters/racing/persistence/typeorm/entities/TeamOrmEntity.ts
@Entity({ name: 'racing_teams' })
export class TeamOrmEntity {
@PrimaryColumn({ type: 'uuid' })
id!: string;
@Column({ type: 'text' })
name!: string;
@Column({ type: 'text' })
tag!: string;
@Column({ type: 'text' })
description!: string;
@Column({ type: 'uuid' })
ownerId!: string;
@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;
}
❌ NO logoUrl column exists! Your plan is solving a problem that doesn't exist.
Flaw #2: Duplicating Existing Work
Your Claim: "Need to implement SVG generation"
Reality: Already exists in MediaController
// apps/api/src/domain/media/MediaController.ts
@Get('avatar/:driverId')
async getDriverAvatar(
@Param('driverId') driverId: string,
@Res() res: Response,
): Promise<void> {
const svg = this.generateDriverAvatarSVG(driverId); // ✅ Already implemented
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.send(svg);
}
private generateDriverAvatarSVG(driverId: string): string {
faker.seed(this.hashCode(driverId)); // ✅ Already using Faker
// ... 50+ lines of SVG generation
}
Your Claim: "Need Next.js rewrites" Reality: Already configured
// apps/website/next.config.mjs
async rewrites() {
const baseUrl = 'http://api:3000';
return [
{
source: '/api/:path*',
destination: `${baseUrl}/:path*`,
},
];
}
Flaw #3: Ignoring Real Problems
Real Problem 1: Controller Business Logic
// apps/api/src/domain/media/MediaController.ts
private generateDriverAvatarSVG(driverId: string): string {
faker.seed(this.hashCode(driverId));
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const initials = ((firstName?.[0] || 'D') + (lastName?.[0] || 'R')).toUpperCase();
const primaryColor = faker.color.rgb({ format: 'hex' });
const secondaryColor = faker.color.rgb({ format: 'hex' });
const patterns = ['gradient', 'stripes', 'circles', 'diamond'];
const pattern = faker.helpers.arrayElement(patterns);
// ... 40 more lines
}
Your Plans: Don't address this
Real Problem 2: Inconsistent Seeds
// adapters/bootstrap/SeedRacingData.ts
for (const driver of seed.drivers) {
const avatarUrl = this.getDriverAvatarUrl(driver.id); // ❌ Static files
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
}
for (const team of seed.teams) {
const logoUrl = `/api/media/teams/${team.id}/logo`; // ✅ API endpoints
mediaRepo.setTeamLogo(team.id, logoUrl);
}
Your Plans: Claim seeds use API (partially true, but inconsistent)
Real Problem 3: Mixed Repository
// adapters/racing/persistence/media/InMemoryMediaRepository.ts
// Stores static file paths AND API endpoints
// Purpose unclear
Your Plans: Don't address this
Flaw #4: Over-Engineering
Simple Problem: Generate SVG for avatar Your Solution: 4+ layers
Controller → Service → Use Case → Generator → Repository → Presenter
Correct Solution: 2 layers
Controller → Domain Service
Flaw #5: Violating Your Own Rules
Your Plans Claim: "Domain should not store URLs" Your Proposed Domain:
// core/media/domain/entities/MediaAsset.ts
export class MediaAsset {
constructor(
public readonly id: MediaId,
public readonly type: MediaType,
public readonly url: MediaUrl, // ❌ Still storing URLs!
public readonly generationParams: MediaGenerationParams
) {}
}
Part 2: The Real Problems
Problem 1: Controller Business Logic
Location: apps/api/src/domain/media/MediaController.ts (lines 214-330)
Issue: 100+ lines of SVG generation in controller
Impact: Violates clean architecture, hard to test
Problem 2: Inconsistent Seed Approach
Location: adapters/bootstrap/SeedRacingData.ts
Issue: Driver avatars use static files, team logos use API
Impact: Inconsistent behavior, static files still needed
Problem 3: Mixed Repository Responsibilities
Location: adapters/racing/persistence/media/InMemoryMediaRepository.ts
Issue: Stores both static URLs and API endpoints
Impact: Unclear purpose, violates single responsibility
Problem 4: No Clean Architecture Separation
Issue: No proper domain layer for media Impact: Infrastructure mixed with application logic
Part 3: Correct Solution
3.1 Architecture Design
┌─────────────────────────────────────────────────────────────┐
│ Presentation (apps/website) │
│ - MediaService returns API endpoints │
│ - Components use <img src="/api/media/..."> │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ HTTP Layer (apps/api) │
│ - MediaController (HTTP only) │
│ - Routes: /api/media/avatar/:id, /api/media/teams/:id/logo│
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer (core/media/domain) │
│ - MediaGenerationService (business logic) │
│ - MediaGenerator (port) │
│ - MediaRepository (port) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure (adapters/media) │
│ - FakerMediaGenerator (seeds) │
│ - InMemoryMediaRepository (seeds) │
└─────────────────────────────────────────────────────────────┘
3.2 Implementation Steps
Step 1: Create Domain Service
// core/media/domain/services/MediaGenerationService.ts
export class MediaGenerationService {
generateDriverAvatar(driverId: string): string {
faker.seed(this.hashCode(driverId));
// ... SVG generation logic
}
generateTeamLogo(teamId: string): string {
faker.seed(this.hashCode(teamId));
// ... SVG generation logic
}
private hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
}
Step 2: Clean Controller
// apps/api/src/domain/media/MediaController.ts
@Get('avatar/:driverId')
async getDriverAvatar(
@Param('driverId') driverId: string,
@Res() res: Response,
): Promise<void> {
const svg = this.mediaGenerationService.generateDriverAvatar(driverId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
// ❌ REMOVE duplicate endpoints
// ❌ REMOVE generateDriverAvatarSVG() method
// ❌ REMOVE generateTeamLogoSVG() method
// ❌ REMOVE hashCode() method
Step 3: Fix Seeds
// adapters/bootstrap/SeedRacingData.ts
private async seedMediaAssets(seed: any): Promise<void> {
// ✅ ALL media uses API endpoints
for (const driver of seed.drivers) {
const avatarUrl = `/api/media/avatar/${driver.id}`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setDriverAvatar) {
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
}
}
for (const team of seed.teams) {
const logoUrl = `/api/media/teams/${team.id}/logo`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setTeamLogo) {
mediaRepo.setTeamLogo(team.id, logoUrl);
}
}
// ✅ Remove static file logic
// ✅ Remove getDriverAvatarUrl() method
}
Step 4: Clean Repository
// adapters/racing/persistence/media/InMemoryMediaRepository.ts
export class InMemoryMediaRepository implements IMediaRepository {
private driverAvatars = new Map<string, string>();
private teamLogos = new Map<string, string>();
setDriverAvatar(driverId: string, apiUrl: string): void {
this.driverAvatars.set(driverId, apiUrl);
}
setTeamLogo(teamId: string, apiUrl: string): void {
this.teamLogos.set(teamId, apiUrl);
}
// ✅ Remove unused methods
// ❌ remove getTrackImage, getCategoryIcon, getSponsorLogo
}
Step 5: Remove Static Files
rm -f apps/website/public/images/avatars/male-default-avatar.jpg
rm -f apps/website/public/images/avatars/female-default-avatar.jpeg
rm -f apps/website/public/images/avatars/neutral-default-avatar.jpeg
Part 4: File Changes Summary
Files to Modify
-
apps/api/src/domain/media/MediaController.ts
- Remove SVG generation logic (lines 214-330)
- Remove duplicate endpoints
- Call domain service
-
adapters/bootstrap/SeedRacingData.ts
- Use API endpoints for ALL media
- Remove static file logic
- Remove getDriverAvatarUrl()
-
adapters/racing/persistence/media/InMemoryMediaRepository.ts
- Simplify to store only API endpoints
- Remove unused methods
-
core/media/domain/services/MediaGenerationService.ts (NEW)
- Contains all SVG generation logic
- Uses Faker for seeds
Files to Delete
- apps/website/public/images/avatars/ (all static files)
Files to Keep (Already Correct)
- apps/website/lib/services/media/MediaService.ts ✅
- apps/website/next.config.mjs ✅
- apps/api/src/domain/media/MediaController.ts (cleaned version)
Part 5: Implementation Timeline
Day 1: Controller Cleanup
- Create MediaGenerationService
- Move SVG logic from controller
- Remove duplicate endpoints
Day 2: Seed Fixes
- Update SeedRacingData to use API endpoints
- Remove static file logic
- Clean up InMemoryMediaRepository
Day 3: Testing & Cleanup
- Remove static files
- TypeScript compilation
- Integration tests
Total: 3 days (vs 10+ days in your plans)
Part 6: Success Criteria
After implementation:
- ✅ No static files in
apps/website/public/images/avatars/ - ✅ No SVG generation in
MediaController - ✅ Consistent seed approach - all API endpoints
- ✅ Clean repository - single responsibility
- ✅ All TypeScript errors resolved
- ✅ Website displays all media correctly
- ✅ Same ID always produces same SVG (via Faker seeding)
Part 7: Comparison Table
| Aspect | Your Plans | Correct Solution |
|---|---|---|
| Database Changes | Remove logoUrl (❌ don't exist) | No changes needed |
| Next.js Config | Add rewrites (❌ already exists) | Keep existing |
| API Endpoints | Add 8 endpoints (❌ duplicates) | Keep 4 existing |
| SVG Generation | Use cases + generators (❌ over-engineered) | Domain service |
| Seeds | Hybrid approach (❌ confusing) | All API endpoints |
| Architecture | Complex layers (❌ over-engineered) | Clean & simple |
| Static Files | Keep some (❌ inconsistent) | Remove all |
| Implementation Time | 10+ days | 3 days |
Part 8: Why Your Plans Fail
- Lack of Analysis: Written without understanding current state
- Over-Engineering: Adding layers where simple solutions suffice
- Inconsistent: Claims to solve problems that don't exist
- Violates Own Rules: Criticizes URL storage, then proposes it
- Duplicates Work: Implements what already exists
Part 9: The Bottom Line
Your Plans Are:
- ❌ Based on incorrect assumptions
- ❌ Solving non-existent problems
- ❌ Ignoring real problems
- ❌ Over-engineering simple solutions
- ❌ Duplicating existing work
Your Plans Should Be:
- ✅ Based on actual current state
- ✅ Solving real problems only
- ✅ Simple and direct
- ✅ Clean architecture without complexity
- ✅ Implementable in 3 days
Part 10: Recommendation
DO NOT implement your current plans.
Instead, implement this streamlined solution that:
- Fixes actual problems (controller logic, inconsistent seeds, mixed repository)
- Ignores imaginary problems (database schema, rewrites, SVG implementation)
- Uses simple, direct architecture
- Can be completed in 3 days
Your plans have good intentions but are fundamentally flawed. This document provides the correct path forward.
Files Created
plans/MEDIA_ARCHITECTURE_COMPLETE_ANALYSIS.md(this file)plans/MEDIA_ARCHITECTURE_ANALYSIS.md(detailed analysis)plans/MEDIA_STREAMLINED_SOLUTION.md(corrected approach)plans/CHALLENGE_TO_YOUR_PLANS.md(point-by-point challenge)
Recommendation: Keep only this file and delete the others.