462 lines
15 KiB
Markdown
462 lines
15 KiB
Markdown
# 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"
|
|
```typescript
|
|
// Your plan claims this exists:
|
|
teams table: { id: '123', logoUrl: '/images/logos/team-123.jpg' }
|
|
```
|
|
|
|
**Reality:**
|
|
```typescript
|
|
// 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`
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```bash
|
|
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
|
|
|
|
1. **apps/api/src/domain/media/MediaController.ts**
|
|
- Remove SVG generation logic (lines 214-330)
|
|
- Remove duplicate endpoints
|
|
- Call domain service
|
|
|
|
2. **adapters/bootstrap/SeedRacingData.ts**
|
|
- Use API endpoints for ALL media
|
|
- Remove static file logic
|
|
- Remove getDriverAvatarUrl()
|
|
|
|
3. **adapters/racing/persistence/media/InMemoryMediaRepository.ts**
|
|
- Simplify to store only API endpoints
|
|
- Remove unused methods
|
|
|
|
4. **core/media/domain/services/MediaGenerationService.ts** (NEW)
|
|
- Contains all SVG generation logic
|
|
- Uses Faker for seeds
|
|
|
|
### Files to Delete
|
|
|
|
1. **apps/website/public/images/avatars/** (all static files)
|
|
|
|
### Files to Keep (Already Correct)
|
|
|
|
1. **apps/website/lib/services/media/MediaService.ts** ✅
|
|
2. **apps/website/next.config.mjs** ✅
|
|
3. **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:
|
|
|
|
1. ✅ **No static files** in `apps/website/public/images/avatars/`
|
|
2. ✅ **No SVG generation** in `MediaController`
|
|
3. ✅ **Consistent seed approach** - all API endpoints
|
|
4. ✅ **Clean repository** - single responsibility
|
|
5. ✅ **All TypeScript errors resolved**
|
|
6. ✅ **Website displays all media correctly**
|
|
7. ✅ **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
|
|
|
|
1. **Lack of Analysis:** Written without understanding current state
|
|
2. **Over-Engineering:** Adding layers where simple solutions suffice
|
|
3. **Inconsistent:** Claims to solve problems that don't exist
|
|
4. **Violates Own Rules:** Criticizes URL storage, then proposes it
|
|
5. **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:
|
|
1. Fixes actual problems (controller logic, inconsistent seeds, mixed repository)
|
|
2. Ignores imaginary problems (database schema, rewrites, SVG implementation)
|
|
3. Uses simple, direct architecture
|
|
4. 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. |