Files
quasar/plans/game-search-architecture.md
Benito Rodríguez 2667e11284
Some checks failed
CI / lint (push) Failing after 10s
CI / test-backend (push) Has been skipped
CI / test-frontend (push) Has been skipped
CI / test-e2e (push) Has been skipped
Refactor code structure for improved readability and maintainability
2026-03-22 11:34:38 +01:00

804 lines
29 KiB
Markdown

# Arquitectura: Nueva Pantalla de Búsqueda de Juegos
## Resumen Ejecutivo
Este documento describe la arquitectura para reemplazar el popup actual ([`GameDialog.tsx`](frontend/src/components/games/GameDialog.tsx)) por una página completa de búsqueda de juegos que permite buscar en múltiples proveedores externos (IGDB, RAWG, TheGamesDB) y seleccionar un resultado para guardar en la base de datos.
## 1. Arquitectura del Backend
### 1.1 Modificaciones al Endpoint Existente
**Endpoint actual:** [`GET /api/metadata/search?q=query&platform=optional`](backend/src/routes/metadata.ts:22)
**Problema actual:**
- El endpoint usa [`enrichGame()`](backend/src/services/metadataService.ts:34) que solo devuelve el primer resultado encontrado
- La respuesta es un array con un solo elemento o vacío
**Solución propuesta:**
Crear una nueva función en [`metadataService.ts`](backend/src/services/metadataService.ts) que busque en TODOS los proveedores y devuelva múltiples resultados.
### 1.2 Nueva Función: `searchGames()`
**Ubicación:** [`backend/src/services/metadataService.ts`](backend/src/services/metadataService.ts)
```typescript
export async function searchGames(opts: {
title: string;
platform?: string;
year?: number;
}): Promise<EnrichedGame[]> {
// Buscar en IGDB, RAWG y TheGamesDB simultáneamente
// Devolver todos los resultados encontrados
// Aplicar deduplicación basada en nombre + plataforma + año
}
```
**Lógica de búsqueda:**
1. Ejecutar búsquedas en paralelo en los tres proveedores
2. Normalizar todos los resultados usando la función [`normalize()`](backend/src/services/metadataService.ts:12) existente
3. Aplicar deduplicación para evitar duplicados entre proveedores
4. Ordenar resultados por relevancia (coincidencia exacta primero)
5. Limitar resultados (ej: 20 resultados máximos por proveedor, 50 total)
**Deduplicación:**
- Agrupar por nombre normalizado (lowercase, sin caracteres especiales)
- Si hay duplicados, priorizar: IGDB > RAWG > TheGamesDB
- Mantener `externalIds` de todas las fuentes para cada resultado
### 1.3 Modificación del Endpoint
**Ubicación:** [`backend/src/routes/metadata.ts`](backend/src/routes/metadata.ts)
**Cambios:**
1. Actualizar el esquema de validación para incluir `year` como parámetro opcional
2. Llamar a `searchGames()` en lugar de `enrichGame()`
3. Devolver el array de resultados directamente
```typescript
const searchMetadataSchema = z.object({
q: z.string().min(1, 'El parámetro de búsqueda es requerido'),
platform: z.string().optional(),
year: z.coerce.number().int().min(1900).max(2100).optional(),
});
app.get('/metadata/search', async (request, reply) => {
const validated = searchMetadataSchema.parse(request.query);
const results = await metadataService.searchGames({
title: validated.q,
platform: validated.platform,
year: validated.year,
});
return reply.code(200).send(results);
});
```
### 1.4 Estructura de Respuesta de la API
```typescript
// GET /api/metadata/search?q=mario&year=1990&platform=NES
[
{
source: 'igdb',
externalIds: {
igdb: 1234,
rawg: 5678,
thegamesdb: 9012,
},
name: 'Super Mario Bros.',
title: 'Super Mario Bros.',
slug: 'super-mario-bros',
releaseDate: '1985-09-13T00:00:00.000Z',
genres: ['Platform'],
coverUrl: 'https://example.com/cover.jpg',
platforms: [
{
id: 18,
name: 'Nintendo Entertainment System (NES)',
abbreviation: 'NES',
slug: 'nes',
},
],
},
// ... más resultados
];
```
### 1.5 Nuevo Endpoint para Guardar Resultado
**Endpoint:** `POST /api/games/from-metadata`
**Propósito:** Crear un juego a partir de un resultado de búsqueda de metadatos.
**Body:**
```typescript
{
"metadata": {
"source": "igdb",
"externalIds": { "igdb": 1234 },
"name": "Super Mario Bros.",
"slug": "super-mario-bros",
"releaseDate": "1985-09-13T00:00:00.000Z",
"genres": ["Platform"],
"coverUrl": "https://example.com/cover.jpg"
},
"overrides": {
"platform": "NES",
"year": 1985,
"description": "Descripción personalizada..."
}
}
```
**Lógica:**
1. Mapear metadatos a estructura de [`Game`](backend/prisma/schema.prisma:12)
2. Aplicar overrides si se proporcionan
3. Guardar en base de datos usando [`GamesController.createGame()`](backend/src/controllers/gamesController.ts:73)
4. Devolver el juego creado
### 1.6 Diagrama de Flujo del Backend
```mermaid
flowchart TD
A[Cliente envía búsqueda] --> B[Validar parámetros]
B --> C[searchGames]
C --> D[Paralelo: IGDB]
C --> E[Paralelo: RAWG]
C --> F[Paralelo: TheGamesDB]
D --> G[Normalizar resultados]
E --> G
F --> G
G --> H[Deduplicar]
H --> I[Ordenar por relevancia]
I --> J[Limitar resultados]
J --> K[Devolver array]
K --> L[Cliente muestra resultados]
L --> M[Usuario selecciona resultado]
M --> N[POST /api/games/from-metadata]
N --> O[GamesController.createGame]
O --> P[Guardar en BD]
P --> Q[Devolver juego creado]
```
## 2. Arquitectura del Frontend
### 2.1 Nueva Ruta
**Ruta:** `/games/add` o `/games/search`
**Archivo:** [`frontend/src/app/games/add/page.tsx`](frontend/src/app/games/add/page.tsx)
**Razón:** `/games/add` es más semántico y sigue el patrón RESTful para crear recursos.
### 2.2 Componentes a Crear
#### 2.2.1 SearchForm Component
**Ubicación:** [`frontend/src/components/games/SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx)
**Responsabilidades:**
- Formulario de búsqueda con campos: título (obligatorio), año (opcional), plataforma (opcional)
- Validación de formulario
- Disparar evento de búsqueda
**Componentes shadcn/ui a usar:**
- [`Input`](frontend/src/components/ui/input.tsx) para título y año
- [`Select`](frontend/src/components/ui/select.tsx) para plataforma
- [`Button`](frontend/src/components/ui/button.tsx) para submit
**Props:**
```typescript
interface SearchFormProps {
onSearch: (params: SearchParams) => void;
isLoading: boolean;
}
```
#### 2.2.2 SearchResults Component
**Ubicación:** [`frontend/src/components/games/SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx)
**Responsabilidades:**
- Mostrar lista de resultados de búsqueda
- Permitir selección de un resultado
- Mostrar estado de carga
**Componentes shadcn/ui a usar:**
- [`Card`](frontend/src/components/ui/card.tsx) para cada resultado
- [`Button`](frontend/src/components/ui/button.tsx) para seleccionar
**Props:**
```typescript
interface SearchResultsProps {
results: MetadataGame[];
onSelect: (result: MetadataGame) => void;
isLoading: boolean;
}
```
#### 2.2.3 GamePreviewDialog Component
**Ubicación:** [`frontend/src/components/games/GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx)
**Responsabilidades:**
- Mostrar detalles completos del resultado seleccionado
- Permitir editar campos antes de guardar
- Confirmar guardado
**Componentes shadcn/ui a usar:**
- [`Dialog`](frontend/src/components/ui/dialog.tsx) para el modal
- [`Input`](frontend/src/components/ui/input.tsx) para campos editables
- [`Textarea`](frontend/src/components/ui/textarea.tsx) para descripción
- [`Button`](frontend/src/components/ui/button.tsx) para acciones
**Props:**
```typescript
interface GamePreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
metadata: MetadataGame | null;
onSave: (metadata: MetadataGame, overrides: GameOverrides) => void;
isSaving: boolean;
}
```
### 2.3 Flujo de Usuario
```mermaid
flowchart TD
A[Usuario accede a /games/add] --> B[SearchForm visible]
B --> C[Usuario ingresa título]
C --> D[Usuario hace clic en Buscar]
D --> E[LLAMADA API: GET /api/metadata/search]
E --> F[SearchResults muestra resultados]
F --> G[Usuario ve lista de juegos]
G --> H[Usuario hace clic en un resultado]
H --> I[GamePreviewDialog se abre]
I --> J[Usuario puede editar campos]
J --> K[Usuario hace clic en Guardar]
K --> L[LLAMADA API: POST /api/games/from-metadata]
L --> M{Éxito?}
M -->|Sí| N[Redirigir a /games o /games/id]
M -->|No| O[Mostrar error en dialog]
O --> J
```
### 2.4 Estructura de la Página
```typescript
// frontend/src/app/games/add/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { SearchForm } from '@/components/games/SearchForm';
import { SearchResults } from '@/components/games/SearchResults';
import { GamePreviewDialog } from '@/components/games/GamePreviewDialog';
import { metadataApi, gamesApi } from '@/lib/api';
export default function AddGamePage() {
const router = useRouter();
const [searchParams, setSearchParams] = useState<SearchParams | null>(null);
const [results, setResults] = useState<MetadataGame[]>([]);
const [selectedResult, setSelectedResult] = useState<MetadataGame | null>(null);
const [isSearching, setIsSearching] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const handleSearch = async (params: SearchParams) => {
setIsSearching(true);
try {
const data = await metadataApi.search(params);
setResults(data);
setSearchParams(params);
} finally {
setIsSearching(false);
}
};
const handleSelect = (result: MetadataGame) => {
setSelectedResult(result);
};
const handleSave = async (metadata: MetadataGame, overrides: GameOverrides) => {
setIsSaving(true);
try {
const game = await gamesApi.createFromMetadata(metadata, overrides);
router.push(`/games/${game.id}`);
} finally {
setIsSaving(false);
}
};
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Añadir Juego</h1>
<SearchForm onSearch={handleSearch} isLoading={isSearching} />
{results.length > 0 && (
<SearchResults
results={results}
onSelect={handleSelect}
isLoading={isSearching}
/>
)}
<GamePreviewDialog
open={!!selectedResult}
onOpenChange={(open) => !open && setSelectedResult(null)}
metadata={selectedResult}
onSave={handleSave}
isSaving={isSaving}
/>
</div>
);
}
```
### 2.5 Actualización del Cliente API
**Ubicación:** [`frontend/src/lib/api.ts`](frontend/src/lib/api.ts)
**Nuevas funciones:**
```typescript
// Búsqueda de metadatos
export async function searchMetadata(params: {
q: string;
platform?: string;
year?: number;
}): Promise<MetadataGame[]> {
const queryParams = new URLSearchParams({ q: params.q });
if (params.platform) queryParams.append('platform', params.platform);
if (params.year) queryParams.append('year', String(params.year));
const res = await fetch(`${API_BASE}/metadata/search?${queryParams}`);
if (!res.ok) throw new Error('Error searching metadata');
return res.json();
}
// Crear juego desde metadatos
export async function createGameFromMetadata(
metadata: MetadataGame,
overrides?: GameOverrides
): Promise<Game> {
const res = await fetch(`${API_BASE}/games/from-metadata`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metadata, overrides }),
});
if (!res.ok) throw new Error('Error creating game from metadata');
return res.json();
}
```
### 2.6 Integración con Página de Juegos
**Ubicación:** [`frontend/src/app/games/page.tsx`](frontend/src/app/games/page.tsx)
**Cambio:**
- Reemplazar el botón "Nuevo Juego" que abre [`GameDialog`](frontend/src/components/games/GameDialog.tsx) por un enlace a `/games/add`
- Mantener [`GameDialog`](frontend/src/components/games/GameDialog.tsx) solo para edición de juegos existentes
```typescript
// Antes
<Button onClick={handleCreate}>
<PlusIcon className="w-4 h-4 mr-2" />
Nuevo Juego
</Button>
// Después
<Link href="/games/add">
<Button>
<PlusIcon className="w-4 h-4 mr-2" />
Añadir Juego
</Button>
</Link>
```
## 3. Estrategia TDD (Test Driven Development)
### 3.1 Orden de Implementación
```mermaid
flowchart LR
A[Tests Backend - searchGames] --> B[Implementar searchGames]
B --> C[Tests Backend - Endpoint /metadata/search]
C --> D[Implementar Endpoint]
D --> E[Tests Backend - POST /api/games/from-metadata]
E --> F[Implementar Endpoint from-metadata]
F --> G[Tests Frontend - Componentes Unitarios]
G --> H[Implementar Componentes]
H --> I[Tests E2E - Flujo Completo]
I --> J[Validar Integración]
```
### 3.2 Tests del Backend
#### 3.2.1 Tests de `searchGames()`
**Archivo:** [`backend/tests/services/metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts)
**Casos de prueba:**
1. ✅ Debe devolver resultados de IGDB cuando hay coincidencias
2. ✅ Debe devolver resultados de RAWG cuando IGDB falla
3. ✅ Debe devolver resultados de TheGamesDB cuando IGDB y RAWG fallan
4. ✅ Debe deduplicar resultados entre proveedores
5. ✅ Debe priorizar IGDB sobre RAWG y TheGamesDB
6. ✅ Debe filtrar por plataforma cuando se proporciona
7. ✅ Debe filtrar por año cuando se proporciona
8. ✅ Debe devolver array vacío cuando no hay resultados
9. ✅ Debe manejar errores de API externas gracefully
10. ✅ Debe limitar número de resultados
**Ejemplo de test:**
```typescript
it('debe deduplicar resultados entre proveedores', async () => {
const mockIgdbResult = {
name: 'Super Mario Bros.',
source: 'igdb',
id: 1234,
// ... otros campos
};
const mockRawgResult = {
name: 'Super Mario Bros.',
source: 'rawg',
id: 5678,
// ... otros campos
};
vi.spyOn(igdb, 'searchGames').mockResolvedValue([mockIgdbResult]);
vi.spyOn(rawg, 'searchGames').mockResolvedValue([mockRawgResult]);
const results = await metadataService.searchGames({ title: 'mario' });
// Solo debería haber un resultado con ambos IDs externos
expect(results).toHaveLength(1);
expect(results[0].externalIds.igdb).toBe(1234);
expect(results[0].externalIds.rawg).toBe(5678);
});
```
#### 3.2.2 Tests del Endpoint `/api/metadata/search`
**Archivo:** [`backend/tests/routes/metadata.search.spec.ts`](backend/tests/routes/metadata.search.spec.ts)
**Casos de prueba:**
1. ✅ Debe devolver 200 con array de resultados
2. ✅ Debe devolver 400 si falta parámetro `q`
3. ✅ Debe devolver 400 si `year` no es válido
4. ✅ Debe pasar parámetros a `searchGames()`
5. ✅ Debe manejar errores del servicio
#### 3.2.3 Tests del Endpoint `POST /api/games/from-metadata`
**Archivo:** [`backend/tests/routes/games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts)
**Casos de prueba:**
1. ✅ Debe crear juego con metadatos de IGDB
2. ✅ Debe crear juego con metadatos de RAWG
3. ✅ Debe crear juego con metadatos de TheGamesDB
4. ✅ Debe aplicar overrides cuando se proporcionan
5. ✅ Debe guardar `externalIds` correctamente
6. ✅ Debe devolver 400 si metadata es inválida
7. ✅ Debe devolver 201 con juego creado
### 3.3 Tests del Frontend
#### 3.3.1 Tests Unitarios de Componentes
**Archivo:** [`frontend/src/components/games/__tests__/SearchForm.test.tsx`](frontend/src/components/games/__tests__/SearchForm.test.tsx)
**Casos de prueba:**
1. ✅ Debe mostrar campos de formulario
2. ✅ Debe validar que título es obligatorio
3. ✅ Debe llamar `onSearch` con parámetros correctos
4. ✅ Debe deshabilitar botón cuando `isLoading` es true
**Archivo:** [`frontend/src/components/games/__tests__/SearchResults.test.tsx`](frontend/src/components/games/__tests__/SearchResults.test.tsx)
**Casos de prueba:**
1. ✅ Debe mostrar lista de resultados
2. ✅ Debe mostrar mensaje cuando no hay resultados
3. ✅ Debe llamar `onSelect` cuando se hace clic en resultado
4. ✅ Debe mostrar estado de carga
**Archivo:** [`frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx`](frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx)
**Casos de prueba:**
1. ✅ Debe mostrar metadatos del juego
2. ✅ Debe permitir editar campos
3. ✅ Debe llamar `onSave` con metadatos y overrides
4. ✅ Debe cerrar cuando se cancela
#### 3.3.2 Tests E2E
**Archivo:** [`tests/e2e/game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts)
**Casos de prueba:**
1. ✅ Flujo completo: búsqueda → selección → guardado
2. ✅ Búsqueda sin resultados
3. ✅ Cancelar selección
4. ✅ Editar campos antes de guardar
5. ✅ Validación de errores
**Ejemplo de test E2E:**
```typescript
test('flujo completo de búsqueda y guardado', async ({ page }) => {
await page.goto('/games/add');
// Ingresar búsqueda
await page.fill('input[name="title"]', 'Super Mario');
await page.click('button[type="submit"]');
// Esperar resultados
await page.waitForSelector('[data-testid="search-results"]');
const results = await page.locator('[data-testid="game-result"]').count();
expect(results).toBeGreaterThan(0);
// Seleccionar primer resultado
await page.click('[data-testid="game-result"]:first-child');
// Esperar dialog de preview
await page.waitForSelector('[data-testid="preview-dialog"]');
// Guardar
await page.click('[data-testid="save-button"]');
// Verificar redirección
await page.waitForURL(/\/games\/[a-z0-9]+$/);
});
```
### 3.4 Orden de Implementación Recomendado
1. **Sprint 1: Backend Core**
- Tests de `searchGames()`
- Implementar `searchGames()`
- Tests del endpoint `/api/metadata/search`
- Actualizar endpoint `/api/metadata/search`
2. **Sprint 2: Backend Save**
- Tests del endpoint `POST /api/games/from-metadata`
- Implementar endpoint `POST /api/games/from-metadata`
3. **Sprint 3: Frontend Components**
- Tests unitarios de `SearchForm`
- Implementar `SearchForm`
- Tests unitarios de `SearchResults`
- Implementar `SearchResults`
- Tests unitarios de `GamePreviewDialog`
- Implementar `GamePreviewDialog`
4. **Sprint 4: Frontend Page & Integration**
- Implementar página `/games/add`
- Actualizar cliente API
- Integrar con página `/games`
5. **Sprint 5: E2E & Polish**
- Tests E2E del flujo completo
- Corregir bugs encontrados
- Mejorar UX (loading states, error messages)
## 4. Integración con Código Existente
### 4.1 Código a Reutilizar
#### Backend
| Componente | Archivo | Uso |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------- |
| [`normalize()`](backend/src/services/metadataService.ts:12) | [`metadataService.ts`](backend/src/services/metadataService.ts) | Normalizar resultados de búsqueda |
| [`igdb.searchGames()`](backend/src/services/igdbClient.ts:73) | [`igdbClient.ts`](backend/src/services/igdbClient.ts) | Buscar en IGDB |
| [`rawg.searchGames()`](backend/src/services/rawgClient.ts:13) | [`rawgClient.ts`](backend/src/services/rawgClient.ts) | Buscar en RAWG |
| [`thegamesdb.searchGames()`](backend/src/services/thegamesdbClient.ts:18) | [`thegamesdbClient.ts`](backend/src/services/thegamesdbClient.ts) | Buscar en TheGamesDB |
| [`GamesController.createGame()`](backend/src/controllers/gamesController.ts:73) | [`gamesController.ts`](backend/src/controllers/gamesController.ts) | Crear juego en BD |
| [`Game`](backend/prisma/schema.prisma:12) schema | [`schema.prisma`](backend/prisma/schema.prisma) | Modelo de datos |
#### Frontend
| Componente | Archivo | Uso |
| ------------------------------------------------- | ----------------------------------------------------- | ---------------------- |
| [`Button`](frontend/src/components/ui/button.tsx) | [`button.tsx`](frontend/src/components/ui/button.tsx) | Botones de acción |
| [`Input`](frontend/src/components/ui/input.tsx) | [`input.tsx`](frontend/src/components/ui/input.tsx) | Campos de formulario |
| [`Select`](frontend/src/components/ui/select.tsx) | [`select.tsx`](frontend/src/components/ui/select.tsx) | Selector de plataforma |
| [`Card`](frontend/src/components/ui/card.tsx) | [`card.tsx`](frontend/src/components/ui/card.tsx) | Tarjetas de resultados |
| [`Dialog`](frontend/src/components/ui/dialog.tsx) | [`dialog.tsx`](frontend/src/components/ui/dialog.tsx) | Dialog de preview |
| [`MetadataGame`](frontend/src/lib/api.ts:39) type | [`api.ts`](frontend/src/lib/api.ts) | Tipo de metadatos |
| [`Game`](frontend/src/lib/api.ts:57) type | [`api.ts`](frontend/src/lib/api.ts) | Tipo de juego |
### 4.2 Código a Modificar
#### Backend
| Archivo | Cambio |
| --------------------------------------------------------------- | ------------------------------------------------ |
| [`metadataService.ts`](backend/src/services/metadataService.ts) | Agregar función `searchGames()` |
| [`metadata.ts`](backend/src/routes/metadata.ts) | Actualizar endpoint `/api/metadata/search` |
| [`games.ts`](backend/src/routes/games.ts) | Agregar endpoint `POST /api/games/from-metadata` |
| [`types/index.ts`](backend/src/types/index.ts) | Agregar tipos para búsqueda múltiple |
#### Frontend
| Archivo | Cambio |
| ---------------------------------------------------------------- | ----------------------------------------------------------------- |
| [`api.ts`](frontend/src/lib/api.ts) | Agregar funciones `searchMetadata()` y `createGameFromMetadata()` |
| [`games/page.tsx`](frontend/src/app/games/page.tsx) | Cambiar botón "Nuevo Juego" a enlace `/games/add` |
| [`GameDialog.tsx`](frontend/src/components/games/GameDialog.tsx) | Mantener solo para edición (opcional) |
### 4.3 Código Nuevo a Crear
#### Backend
| Archivo | Propósito |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| [`backend/tests/services/metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts) | Tests de `searchGames()` |
| [`backend/tests/routes/games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts) | Tests del endpoint from-metadata |
#### Frontend
| Archivo | Propósito |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
| [`frontend/src/app/games/add/page.tsx`](frontend/src/app/games/add/page.tsx) | Página de búsqueda de juegos |
| [`frontend/src/components/games/SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx) | Formulario de búsqueda |
| [`frontend/src/components/games/SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx) | Lista de resultados |
| [`frontend/src/components/games/GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx) | Dialog de preview y edición |
| [`frontend/src/components/games/__tests__/SearchForm.test.tsx`](frontend/src/components/games/__tests__/SearchForm.test.tsx) | Tests de SearchForm |
| [`frontend/src/components/games/__tests__/SearchResults.test.tsx`](frontend/src/components/games/__tests__/SearchResults.test.tsx) | Tests de SearchResults |
| [`frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx`](frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx) | Tests de GamePreviewDialog |
| [`tests/e2e/game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts) | Tests E2E del flujo |
## 5. Diagrama de Arquitectura Completa
```mermaid
graph TB
subgraph Frontend
A[Página /games/add] --> B[SearchForm]
A --> C[SearchResults]
A --> D[GamePreviewDialog]
B --> E[api.ts - searchMetadata]
C --> F[api.ts - createGameFromMetadata]
D --> F
end
subgraph Backend
E --> G[GET /api/metadata/search]
F --> H[POST /api/games/from-metadata]
G --> I[metadataService.searchGames]
I --> J[igdbClient.searchGames]
I --> K[rawgClient.searchGames]
I --> L[thegamesdbClient.searchGames]
H --> M[GamesController.createGame]
M --> N[(Prisma - Game)]
end
subgraph External APIs
J --> O[IGDB API]
K --> P[RAWG API]
L --> Q[TheGamesDB API]
end
style Frontend fill:#e1f5fe
style Backend fill:#f3e5f5
style External APIs fill:#fff3e0
```
## 6. Consideraciones Adicionales
### 6.1 Manejo de Errores
**Backend:**
- Capturar errores de APIs externas sin fallar completamente
- Devolver resultados parciales si algún proveedor falla
- Logging de errores para debugging
**Frontend:**
- Mostrar mensajes de error claros al usuario
- Permitir reintentar búsqueda
- Mostrar estado de carga durante búsquedas
### 6.2 Performance
**Backend:**
- Usar `Promise.all()` para búsquedas paralelas
- Implementar caching de resultados (opcional)
- Limitar número de resultados por proveedor
**Frontend:**
- Implementar debounce en búsqueda
- Lazy loading de imágenes de portada
- Paginación de resultados si hay muchos
### 6.3 Seguridad
**Backend:**
- Validar todos los parámetros de entrada
- Sanitizar datos antes de guardar en BD
- Rate limiting en endpoints de búsqueda
**Frontend:**
- Sanitizar HTML antes de mostrar
- Validar en cliente y servidor
### 6.4 Accesibilidad
**Frontend:**
- Labels en todos los campos de formulario
- Focus management en dialogs
- Keyboard navigation
- ARIA labels donde sea necesario
### 6.5 UX Considerations
- Mostrar loading states claros
- Feedback inmediato al usuario
- Permitir cancelar búsqueda en progreso
- Mostrar fuente de cada resultado (IGDB, RAWG, etc.)
- Permitir ver más detalles antes de seleccionar
- Confirmación antes de guardar
## 7. Resumen de Cambios
### Backend
| Archivo | Acción |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------ |
| [`metadataService.ts`](backend/src/services/metadataService.ts) | Agregar `searchGames()` |
| [`metadata.ts`](backend/src/routes/metadata.ts) | Modificar endpoint `/api/metadata/search` |
| [`games.ts`](backend/src/routes/games.ts) | Agregar endpoint `POST /api/games/from-metadata` |
| [`types/index.ts`](backend/src/types/index.ts) | Agregar tipos nuevos |
| [`metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts) | **NUEVO** |
| [`games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts) | **NUEVO** |
### Frontend
| Archivo | Acción |
| ------------------------------------------------------------------------------ | ------------------------------------------------------- |
| [`api.ts`](frontend/src/lib/api.ts) | Agregar `searchMetadata()` y `createGameFromMetadata()` |
| [`games/page.tsx`](frontend/src/app/games/page.tsx) | Cambiar botón a enlace |
| [`games/add/page.tsx`](frontend/src/app/games/add/page.tsx) | **NUEVO** |
| [`SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx) | **NUEVO** |
| [`SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx) | **NUEVO** |
| [`GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx) | **NUEVO** |
| Tests unitarios | **NUEVOS** |
| [`game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts) | **NUEVO** |
---
**Documento creado:** 2025-03-21
**Autor:** Architect Mode
**Estado:** Diseño completo listo para implementación