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:
9
backend/.eslintignore
Normal file
9
backend/.eslintignore
Normal 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
28
backend/.eslintrc.cjs
Normal 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
37
backend/README.md
Normal 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
41
backend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
105
backend/prisma/schema.prisma
Normal file
105
backend/prisma/schema.prisma
Normal 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
24
backend/src/app.ts
Normal 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
29
backend/src/index.ts
Normal 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
|
||||
*/
|
||||
11
backend/src/plugins/prisma.ts
Normal file
11
backend/src/plugins/prisma.ts
Normal 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
|
||||
*/
|
||||
11
backend/src/routes/health.ts
Normal file
11
backend/src/routes/health.ts
Normal 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
|
||||
*/
|
||||
21
backend/tests/server.spec.ts
Normal file
21
backend/tests/server.spec.ts
Normal 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
20
backend/tsconfig.json
Normal 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
19
backend/vitest.config.ts
Normal 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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user