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:
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
133
backend/prisma/migrations/20260208102247_init/migration.sql
Normal file
133
backend/prisma/migrations/20260208102247_init/migration.sql
Normal 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");
|
||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal 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"
|
||||
92
backend/tests/models/game.spec.ts
Normal file
92
backend/tests/models/game.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user