chore(docs): completar Fase 2 — requisitos y arquitectura

- Añade/actualiza `docs/requirements.md`, `docs/architecture.md`, `docs/api-integration.md`, `docs/data-model.md`
- Documenta criterios de aceptación y decisiones técnico-arquitectónicas
- Recomendación: añadir `docs/legal-considerations.md` (pendiente)
This commit is contained in:
2026-02-08 10:45:22 +01:00
parent b95c7366be
commit fb4b279db0
21 changed files with 5803 additions and 101 deletions

9
backend/.eslintignore Normal file
View File

@@ -0,0 +1,9 @@
# Metadatos
# Autor: Quasar (investigación automatizada)
# Última actualización: 2026-02-07
node_modules/
dist/
.pnp.cjs
.env
**/*.db

28
backend/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,28 @@
/**
* Metadatos
* Autor: Quasar (investigación automatizada)
* Última actualización: 2026-02-07
*/
module.exports = {
env: {
node: true,
es2021: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
rules: {
'no-console': 'off',
},
overrides: [
{
files: ['**/*.ts'],
rules: {},
},
],
};

37
backend/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Backend — Quasar
Scaffold mínimo del backend usando Fastify + TypeScript + Prisma (SQLite).
Pasos rápidos:
1. Instalar dependencias:
yarn
2. Generar el cliente Prisma:
npx prisma generate
3. Crear la migración inicial (local):
npx prisma migrate dev --name init
4. Ejecutar en modo desarrollo:
yarn dev
5. Tests:
yarn test
Notas:
- Las migraciones **no** se ejecutaron como parte de este commit. Use `.env.example` como referencia para `DATABASE_URL`.
---
Metadatos:
Autor: GitHub Copilot
Última actualización: 2026-02-07

41
backend/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "quasar-backend",
"version": "0.1.0",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"test": "vitest",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev --name init",
"prisma:studio": "prisma studio",
"lint": "eslint \"src/**/*.{ts,js}\"",
"format": "prettier --write ."
},
"dependencies": {
"@fastify/cors": "^9.0.0",
"@fastify/helmet": "^11.0.0",
"@fastify/multipart": "^9.0.0",
"@fastify/rate-limit": "^9.0.0",
"@prisma/client": "5.22.0",
"dotenv": "^16.0.0",
"fastify": "^4.28.0",
"pino": "^8.0.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"eslint": "^8.0.0",
"prettier": "^2.8.0",
"prisma": "5.22.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.0",
"vitest": "^0.31.0"
},
"metadata": {
"autor": "GitHub Copilot",
"ultima_actualizacion": "2026-02-07"
}
}

View File

@@ -0,0 +1,105 @@
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Game {
id String @id @default(cuid())
title String
slug String @unique
description String?
releaseDate DateTime?
igdbId Int? @unique
rawgId Int? @unique
thegamesdbId Int? @unique
extra String? // JSON serialized (usar parse/stringify al guardar/leer) para compatibilidad con SQLite
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
romFiles RomFile[]
artworks Artwork[]
purchases Purchase[]
gamePlatforms GamePlatform[]
priceHistories PriceHistory[]
tags Tag[]
@@index([title])
}
model Platform {
id String @id @default(cuid())
name String
slug String @unique
generation Int?
gamePlatforms GamePlatform[]
}
model GamePlatform {
id String @id @default(cuid())
game Game @relation(fields: [gameId], references: [id])
gameId String
platform Platform @relation(fields: [platformId], references: [id])
platformId String
@@unique([gameId, platformId])
}
model RomFile {
id String @id @default(cuid())
path String
filename String
checksum String @unique
size Int
format String
hashes String? // JSON serialized (ej.: {"crc32": "...", "md5": "..."})
game Game? @relation(fields: [gameId], references: [id])
gameId String?
addedAt DateTime @default(now())
lastSeenAt DateTime?
status String @default("active")
@@index([checksum])
}
model Artwork {
id String @id @default(cuid())
game Game @relation(fields: [gameId], references: [id])
gameId String
type String
sourceUrl String
localPath String?
width Int?
height Int?
fetchedAt DateTime @default(now())
}
model Purchase {
id String @id @default(cuid())
game Game @relation(fields: [gameId], references: [id])
gameId String
priceCents Int
currency String
store String?
date DateTime
receiptPath String?
}
model Tag {
id String @id @default(cuid())
name String @unique
games Game[]
}
model PriceHistory {
id String @id @default(cuid())
game Game @relation(fields: [gameId], references: [id])
gameId String
priceCents Int
currency String
recordedAt DateTime @default(now())
source String?
}
// Metadatos:
// Autor: GitHub Copilot
// Última actualización: 2026-02-07

24
backend/src/app.ts Normal file
View File

@@ -0,0 +1,24 @@
import Fastify, { FastifyInstance } from 'fastify';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import rateLimit from '@fastify/rate-limit';
import healthRoutes from './routes/health';
export function buildApp(): FastifyInstance {
const app: FastifyInstance = Fastify({
logger: false,
});
void app.register(cors, { origin: true });
void app.register(helmet);
void app.register(rateLimit, { max: 1000, timeWindow: '1 minute' });
void app.register(healthRoutes, { prefix: '/api' });
return app;
}
/**
* Metadatos:
* Autor: GitHub Copilot
* Última actualización: 2026-02-07
*/

29
backend/src/index.ts Normal file
View File

@@ -0,0 +1,29 @@
import dotenv from 'dotenv';
import { buildApp } from './app';
dotenv.config();
const port = Number(process.env.PORT ?? 3000);
const app = buildApp();
const start = async () => {
try {
await app.listen({ port, host: '0.0.0.0' });
// eslint-disable-next-line no-console
console.log(`Server listening on http://0.0.0.0:${port}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
// Start only when run directly (avoids starting during tests)
if (require.main === module) {
start();
}
/**
* Metadatos:
* Autor: GitHub Copilot
* Última actualización: 2026-02-07
*/

View File

@@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
/**
* Metadatos:
* Autor: GitHub Copilot
* Última actualización: 2026-02-07
*/

View File

@@ -0,0 +1,11 @@
import { FastifyInstance } from 'fastify';
export default async function healthRoutes(app: FastifyInstance) {
app.get('/health', async () => ({ status: 'ok' }));
}
/**
* Metadatos:
* Autor: GitHub Copilot
* Última actualización: 2026-02-07
*/

View File

@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { buildApp } from '../src/app';
describe('Server', () => {
it('GET /api/health devuelve 200 y { status: "ok" }', async () => {
const app = buildApp();
await app.ready();
const res = await app.inject({ method: 'GET', url: '/api/health' });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({ status: 'ok' });
await app.close();
});
});
/**
* Metadatos:
* Autor: GitHub Copilot
* Última actualización: 2026-02-07
*/

20
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"esModuleInterop": true,
"moduleResolution": "node",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"],
"metadata": {
"autor": "GitHub Copilot",
"ultima_actualizacion": "2026-02-07"
}
}

19
backend/vitest.config.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Metadatos
* Autor: Quasar (investigación automatizada)
* Última actualización: 2026-02-07
*/
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['tests/**/*.spec.ts'],
globals: false,
coverage: {
provider: 'c8',
reporter: ['text', 'lcov'],
},
},
});