chore: update Prisma and dependencies, add initial migration and tests

- Updated Prisma and @prisma/client to version 6.19.2 in package.json and yarn.lock.
- Added package extensions for @prisma/client in .yarnrc.yml.
- Updated backend README.md for clearer setup instructions.
- Created initial migration for Game, Platform, and related tables in Prisma.
- Added migration lock file for version control.
- Implemented tests for Game model using Vitest, including creation and unique slug constraint checks.
This commit is contained in:
2026-02-08 12:36:36 +01:00
parent fb4b279db0
commit f4bee94a16
11 changed files with 886 additions and 112 deletions

View File

@@ -2,31 +2,34 @@
Scaffold mínimo del backend usando Fastify + TypeScript + Prisma (SQLite).
Pasos rápidos:
**Arranque rápido**
1. Instalar dependencias:
```
# desde la raíz
yarn
yarn
# entrar al backend
cd backend
2. Generar el cliente Prisma:
# generar cliente Prisma
yarn prisma:generate
npx prisma generate
# aplicar migraciones (si pide nombre, usar --name init)
yarn prisma:migrate
3. Crear la migración inicial (local):
# abrir Prisma Studio
yarn prisma:studio
npx prisma migrate dev --name init
# ejecutar en desarrollo
yarn dev
4. Ejecutar en modo desarrollo:
yarn dev
5. Tests:
yarn test
# ejecutar tests
yarn test
```
Notas:
- Las migraciones **no** se ejecutaron como parte de este commit. Use `.env.example` como referencia para `DATABASE_URL`.
- Use `.env.example` como referencia para `DATABASE_URL`.
---

View File

@@ -9,9 +9,11 @@
"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",
"test:ci": "vitest run",
"prisma:generate": "prisma generate --schema=./prisma/schema.prisma",
"prisma:migrate": "prisma migrate dev --name init --schema=./prisma/schema.prisma",
"prisma:migrate:deploy": "prisma migrate deploy --schema=./prisma/schema.prisma",
"prisma:studio": "prisma studio --schema=./prisma/schema.prisma",
"lint": "eslint \"src/**/*.{ts,js}\"",
"format": "prettier --write ."
},
@@ -20,7 +22,7 @@
"@fastify/helmet": "^11.0.0",
"@fastify/multipart": "^9.0.0",
"@fastify/rate-limit": "^9.0.0",
"@prisma/client": "5.22.0",
"@prisma/client": "6.19.2",
"dotenv": "^16.0.0",
"fastify": "^4.28.0",
"pino": "^8.0.0"
@@ -29,7 +31,7 @@
"@types/node": "^18.0.0",
"eslint": "^8.0.0",
"prettier": "^2.8.0",
"prisma": "5.22.0",
"prisma": "6.19.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.0",
"vitest": "^0.31.0"

View File

@@ -0,0 +1,133 @@
-- CreateTable
CREATE TABLE "Game" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"releaseDate" DATETIME,
"igdbId" INTEGER,
"rawgId" INTEGER,
"thegamesdbId" INTEGER,
"extra" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Platform" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"generation" INTEGER
);
-- CreateTable
CREATE TABLE "GamePlatform" (
"id" TEXT NOT NULL PRIMARY KEY,
"gameId" TEXT NOT NULL,
"platformId" TEXT NOT NULL,
CONSTRAINT "GamePlatform_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "GamePlatform_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "Platform" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "RomFile" (
"id" TEXT NOT NULL PRIMARY KEY,
"path" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"checksum" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"format" TEXT NOT NULL,
"hashes" TEXT,
"gameId" TEXT,
"addedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeenAt" DATETIME,
"status" TEXT NOT NULL DEFAULT 'active',
CONSTRAINT "RomFile_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Artwork" (
"id" TEXT NOT NULL PRIMARY KEY,
"gameId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"sourceUrl" TEXT NOT NULL,
"localPath" TEXT,
"width" INTEGER,
"height" INTEGER,
"fetchedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Artwork_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Purchase" (
"id" TEXT NOT NULL PRIMARY KEY,
"gameId" TEXT NOT NULL,
"priceCents" INTEGER NOT NULL,
"currency" TEXT NOT NULL,
"store" TEXT,
"date" DATETIME NOT NULL,
"receiptPath" TEXT,
CONSTRAINT "Purchase_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "PriceHistory" (
"id" TEXT NOT NULL PRIMARY KEY,
"gameId" TEXT NOT NULL,
"priceCents" INTEGER NOT NULL,
"currency" TEXT NOT NULL,
"recordedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"source" TEXT,
CONSTRAINT "PriceHistory_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "_GameToTag" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_GameToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Game" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_GameToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Game_slug_key" ON "Game"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Game_igdbId_key" ON "Game"("igdbId");
-- CreateIndex
CREATE UNIQUE INDEX "Game_rawgId_key" ON "Game"("rawgId");
-- CreateIndex
CREATE UNIQUE INDEX "Game_thegamesdbId_key" ON "Game"("thegamesdbId");
-- CreateIndex
CREATE INDEX "Game_title_idx" ON "Game"("title");
-- CreateIndex
CREATE UNIQUE INDEX "Platform_slug_key" ON "Platform"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "GamePlatform_gameId_platformId_key" ON "GamePlatform"("gameId", "platformId");
-- CreateIndex
CREATE UNIQUE INDEX "RomFile_checksum_key" ON "RomFile"("checksum");
-- CreateIndex
CREATE INDEX "RomFile_checksum_idx" ON "RomFile"("checksum");
-- CreateIndex
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
-- CreateIndex
CREATE UNIQUE INDEX "_GameToTag_AB_unique" ON "_GameToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_GameToTag_B_index" ON "_GameToTag"("B");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@@ -0,0 +1,92 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { execSync } from 'child_process';
import { describe, beforeAll, afterAll, it, expect } from 'vitest';
// Import PrismaClient dynamically after running `prisma generate`
// to allow the test setup to run `prisma generate`/`prisma migrate` first.
// Nota: Estos tests siguen TDD. Al principio deben FALLAR hasta que se creen migraciones.
describe('Prisma / Game model', () => {
const tmpDir = os.tmpdir();
const dbFile = path.join(
tmpDir,
`quasar-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`
);
const databaseUrl = `file:${dbFile}`;
let prisma: any;
beforeAll(async () => {
// Asegurarse de que la DB de prueba no exista antes de empezar
try {
fs.unlinkSync(dbFile);
} catch (e) {
/* ignore */
}
// Apuntar Prisma a la DB temporal
process.env.DATABASE_URL = databaseUrl;
// Ejecutar migraciones contra la DB de prueba
// Esto fallará si no hay migraciones: esperado en la fase TDD inicial
execSync('yarn prisma migrate deploy --schema=./prisma/schema.prisma', {
stdio: 'inherit',
cwd: path.resolve(__dirname, '..', '..'),
});
// Intentar requerir el cliente generado; si no existe, intentar generarlo (fallback)
let GeneratedPrismaClient;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
GeneratedPrismaClient = require('@prisma/client').PrismaClient;
} catch (e) {
try {
execSync('yarn prisma generate --schema=./prisma/schema.prisma', {
stdio: 'inherit',
cwd: path.resolve(__dirname, '..', '..'),
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
GeneratedPrismaClient = require('@prisma/client').PrismaClient;
} catch (err) {
// Si generation falla (por ejemplo PnP), reintentar require para mostrar mejor error
// eslint-disable-next-line @typescript-eslint/no-var-requires
GeneratedPrismaClient = require('@prisma/client').PrismaClient;
}
}
prisma = new GeneratedPrismaClient();
await prisma.$connect();
});
afterAll(async () => {
if (prisma) {
await prisma.$disconnect();
}
try {
fs.unlinkSync(dbFile);
} catch (e) {
/* ignore */
}
});
it('can create a Game and read title/slug', async () => {
const created = await prisma.game.create({ data: { title: 'Test Game', slug: 'test-game' } });
const found = await prisma.game.findUnique({ where: { id: created.id } });
expect(found).toBeTruthy();
expect(found?.title).toBe('Test Game');
expect(found?.slug).toBe('test-game');
});
it('enforces unique slug constraint', async () => {
const slug = `unique-${Date.now()}`;
await prisma.game.create({ data: { title: 'G1', slug } });
let threw = false;
try {
await prisma.game.create({ data: { title: 'G2', slug } });
} catch (err) {
threw = true;
}
expect(threw).toBe(true);
});
});