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

69
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,69 @@
# CI pipeline for Quasar (Gitea Actions)
# Jobs: build_and_test (install, prisma generate, lint, unit tests)
# e2e (Playwright, runs on pushes to main)
# Note: `prisma generate` is allowed to continue on error to avoid blocking CI when native engines can't be built.
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
DATABASE_URL: file:./backend/dev.db
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Prisma: generate (may fail on some runners)
run: yarn workspace quasar-backend run prisma:generate
continue-on-error: true
- name: Lint (backend)
run: yarn workspace quasar-backend run lint
- name: Run backend unit tests
run: yarn workspace quasar-backend run test
e2e:
runs-on: ubuntu-latest
needs: [build_and_test]
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Install Playwright browsers
run: yarn test:install
continue-on-error: true
- name: Run Playwright E2E
run: yarn test:ci
# Metadatos
# Autor: Quasar (investigación automatizada)
# Última actualización: 2026-02-08

2817
.pnp.cjs generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,17 @@
# Metadatos
# Autor: Quasar (investigación automatizada)
# Última actualización: 2026-02-07
node_modules/ node_modules/
dist/ dist/
build/
coverage/
.vscode/
.env
.env.local
.yarn/ .yarn/
.pnp.cjs .pnp.cjs
**/*.lock
**/dev.db
**/*.sqlite
**/dist

18
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,18 @@
/*
* Metadatos
* Autor: Quasar (investigación automatizada)
* Última actualización: 2026-02-07
*/
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
arrowParens: 'always',
proseWrap: 'preserve',
endOfLine: 'lf',
};

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'],
},
},
});

View File

@@ -21,7 +21,7 @@ Game ↔ PriceHistory (1N)
- `description` (text) — opcional. - `description` (text) — opcional.
- `releaseDate` (DateTime?) — fecha principal. - `releaseDate` (DateTime?) — fecha principal.
- `igdbId`, `rawgId`, `thegamesdbId` (Int?) — ids externos. - `igdbId`, `rawgId`, `thegamesdbId` (Int?) — ids externos.
- `extra` (Json?) — campos flexibles. - `extra` (String?) — JSON serializado (compatible con SQLite); usar `JSON.parse`/`JSON.stringify` al leer/escribir.
- `createdAt`, `updatedAt`. - `createdAt`, `updatedAt`.
### Platform ### Platform
@@ -30,7 +30,7 @@ Game ↔ PriceHistory (1N)
### RomFile ### RomFile
- `id`, `path`, `filename`, `checksum` (unique), `size` (int), `format`, `hashes` (Json opcional), `status` (active/missing), `addedAt`, `lastSeenAt`, `gameId`. - `id`, `path`, `filename`, `checksum` (unique), `size` (int), `format`, `hashes` (String? — JSON serializado, ej.: {"crc32":"...","md5":"..."}), `status` (active/missing), `addedAt`, `lastSeenAt`, `gameId`.
### Artwork ### Artwork
@@ -67,7 +67,7 @@ Game ↔ PriceHistory (1N)
```prisma ```prisma
datasource db { datasource db {
provider = "postgresql" // para dev puede usarse sqlite provider = "sqlite" // usar SQLite para desarrollo; cambiar a postgresql en producción si se necesita JSON nativo
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
@@ -88,7 +88,7 @@ model Game {
romFiles RomFile[] romFiles RomFile[]
artworks Artwork[] artworks Artwork[]
tags Tag[] tags Tag[]
extra Json? extra String? // JSON serializado (usar JSON.parse/JSON.stringify para leer y escribir)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@index([title]) @@index([title])
@@ -109,7 +109,7 @@ model RomFile {
checksum String @unique checksum String @unique
size Int size Int
format String format String
hashes Json? hashes String? // JSON serializado (ej.: {"crc32":"...","md5":"..."})
game Game? @relation(fields: [gameId], references: [id]) game Game? @relation(fields: [gameId], references: [id])
gameId String? gameId String?
addedAt DateTime @default(now()) addedAt DateTime @default(now())
@@ -141,6 +141,16 @@ model Purchase {
receiptPath String? receiptPath String?
} }
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?
}
model Tag { model Tag {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String @unique
@@ -148,7 +158,7 @@ model Tag {
} }
``` ```
> Nota: para monedas se recomienda almacenar `priceCents` (int) para compatibilidad; si se usa Postgres se puede optar por `Decimal`. > Nota: para monedas se recomienda almacenar `priceCents` (int) para compatibilidad; si se usa Postgres se puede optar por `Decimal`. Nota: SQLite no soporta tipos JSON nativos; en este ejemplo se usan `String` para JSON serializado. Si migras a Postgres, puedes usar `Json`/`JsonB` para campos flexibles.
--- ---

79
package-lock.json generated
View File

@@ -1,79 +0,0 @@
{
"name": "quasar",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quasar",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.58.0"
}
},
"node_modules/@playwright/test": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
"integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@@ -1,6 +1,11 @@
{ {
"name": "quasar", "name": "quasar",
"version": "1.0.0", "version": "1.0.0",
"private": true,
"workspaces": [
"backend",
"frontend"
],
"description": "Quasar es una aplicación web para al gestión de una biblioteca personal de videjuegos. Permite a los usuarios catalogar, organizar y buscar sus juegos de manera eficiente. Se pueden agregar videjuegos físicos, digitales y roms de emuladores.", "description": "Quasar es una aplicación web para al gestión de una biblioteca personal de videjuegos. Permite a los usuarios catalogar, organizar y buscar sus juegos de manera eficiente. Se pueden agregar videjuegos físicos, digitales y roms de emuladores.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -0,0 +1,31 @@
## Phase 2 Complete: Requisitos y diseño técnico
TL;DR: Se documentaron y finalizaron los requisitos funcionales y no funcionales del MVP, el diseño de arquitectura (monorepo, stack propuesto) y el modelo de datos inicial para `Game`, `RomFile`, `Platform`, `Artwork`, `Purchase` y `PriceHistory`.
**Files created/changed:**
- `docs/requirements.md`
- `docs/architecture.md`
- `docs/api-integration.md`
- `docs/data-model.md`
- `plans/gestor-coleccion-plan.md` (plan maestro actualizado)
**Functions created/changed:**
- Ninguna (documentación)
**Tests created/changed:**
- Ninguno (recomendación: añadir tests que verifiquen la presencia y metadatos de los documentos claves si se automatiza la validación de docs en CI)
**Review Status:** APPROVED ✅ (con recomendación menor: añadir `docs/legal-considerations.md` si falta para cubrir riesgos legales antes de integrar scraping o descargas masivas)
**Git Commit Message:**
```
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)
```

2495
yarn.lock

File diff suppressed because it is too large Load Diff