leaderboard and ranked systems
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Subject" AS ENUM ('MATH', 'READING', 'WRITING');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Difficulty" AS ENUM ('EASY', 'MEDIUM', 'HARD');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SessionStatus" AS ENUM ('IN_PROGRESS', 'COMPLETED', 'ABANDONED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"googleId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"avatarUrl" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Question" (
|
||||
"id" TEXT NOT NULL,
|
||||
"grade" INTEGER NOT NULL,
|
||||
"subject" "Subject" NOT NULL,
|
||||
"strand" TEXT NOT NULL,
|
||||
"difficulty" "Difficulty" NOT NULL,
|
||||
"questionText" TEXT NOT NULL,
|
||||
"options" JSONB NOT NULL,
|
||||
"correctAnswer" TEXT NOT NULL,
|
||||
"explanation" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Question_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GameSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"grade" INTEGER NOT NULL,
|
||||
"subject" "Subject" NOT NULL,
|
||||
"status" "SessionStatus" NOT NULL DEFAULT 'IN_PROGRESS',
|
||||
"score" INTEGER NOT NULL DEFAULT 0,
|
||||
"totalQuestions" INTEGER NOT NULL,
|
||||
"correctAnswers" INTEGER NOT NULL DEFAULT 0,
|
||||
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "GameSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserAnswer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"questionId" TEXT NOT NULL,
|
||||
"selectedAnswer" TEXT NOT NULL,
|
||||
"isCorrect" BOOLEAN NOT NULL,
|
||||
"timeSpentMs" INTEGER NOT NULL,
|
||||
"answeredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "UserAnswer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeaderboardEntry" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"grade" INTEGER NOT NULL,
|
||||
"subject" "Subject" NOT NULL,
|
||||
"bestScore" INTEGER NOT NULL,
|
||||
"gamesPlayed" INTEGER NOT NULL,
|
||||
"totalCorrect" INTEGER NOT NULL,
|
||||
"rank" INTEGER,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LeaderboardEntry_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_googleId_key" ON "User"("googleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LeaderboardEntry_userId_grade_subject_key" ON "LeaderboardEntry"("userId", "grade", "subject");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GameSession" ADD CONSTRAINT "GameSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserAnswer" ADD CONSTRAINT "UserAnswer_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "GameSession"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserAnswer" ADD CONSTRAINT "UserAnswer_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "Question"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LeaderboardEntry" ADD CONSTRAINT "LeaderboardEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Changed the type of `grade` on the `GameSession` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
- Changed the type of `grade` on the `LeaderboardEntry` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
- Changed the type of `grade` on the `Question` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Grade" AS ENUM ('G3', 'G6', 'G9');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "GameSession" DROP COLUMN "grade",
|
||||
ADD COLUMN "grade" "Grade" NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LeaderboardEntry" DROP COLUMN "grade",
|
||||
ADD COLUMN "grade" "Grade" NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Question" DROP COLUMN "grade",
|
||||
ADD COLUMN "grade" "Grade" NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LeaderboardEntry_userId_grade_subject_key" ON "LeaderboardEntry"("userId", "grade", "subject");
|
||||
@@ -0,0 +1,12 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeaderboardScore" (
|
||||
"id" TEXT NOT NULL,
|
||||
"playerName" TEXT NOT NULL,
|
||||
"grade" "Grade" NOT NULL,
|
||||
"score" INTEGER NOT NULL,
|
||||
"totalQuestions" INTEGER NOT NULL,
|
||||
"timeSeconds" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "LeaderboardScore_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "username" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Drop stale table (old schema: subject, bestScore, rank — missing elo, bestTime)
|
||||
DROP TABLE IF EXISTS "LeaderboardEntry";
|
||||
|
||||
-- Drop orphaned table from old migration
|
||||
DROP TABLE IF EXISTS "LeaderboardScore";
|
||||
|
||||
-- Recreate with correct schema
|
||||
CREATE TABLE "LeaderboardEntry" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"grade" "Grade" NOT NULL,
|
||||
"elo" INTEGER NOT NULL DEFAULT 1000,
|
||||
"bestTime" INTEGER,
|
||||
"gamesPlayed" INTEGER NOT NULL DEFAULT 0,
|
||||
"totalCorrect" INTEGER NOT NULL DEFAULT 0,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT "LeaderboardEntry_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Unique per user+grade (no subject)
|
||||
CREATE UNIQUE INDEX "LeaderboardEntry_userId_grade_key" ON "LeaderboardEntry"("userId", "grade");
|
||||
|
||||
-- Foreign key
|
||||
ALTER TABLE "LeaderboardEntry" ADD CONSTRAINT "LeaderboardEntry_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "LeaderboardEntry" ALTER COLUMN "elo" SET DEFAULT 800;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "LeaderboardEntry" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,105 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Grade {
|
||||
G3
|
||||
G6
|
||||
G9
|
||||
}
|
||||
|
||||
enum Subject {
|
||||
MATH
|
||||
READING
|
||||
WRITING
|
||||
}
|
||||
|
||||
enum Difficulty {
|
||||
EASY
|
||||
MEDIUM
|
||||
HARD
|
||||
}
|
||||
|
||||
enum SessionStatus {
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
ABANDONED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
googleId String @unique
|
||||
email String @unique
|
||||
displayName String
|
||||
username String? @unique
|
||||
avatarUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions GameSession[]
|
||||
leaderboardEntries LeaderboardEntry[]
|
||||
}
|
||||
|
||||
model Question {
|
||||
id String @id @default(cuid())
|
||||
grade Grade
|
||||
subject Subject
|
||||
strand String
|
||||
difficulty Difficulty
|
||||
questionText String
|
||||
options Json
|
||||
correctAnswer String
|
||||
explanation String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
answers UserAnswer[]
|
||||
}
|
||||
|
||||
model GameSession {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
grade Grade
|
||||
subject Subject
|
||||
status SessionStatus @default(IN_PROGRESS)
|
||||
score Int @default(0)
|
||||
totalQuestions Int
|
||||
correctAnswers Int @default(0)
|
||||
startedAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
answers UserAnswer[]
|
||||
}
|
||||
|
||||
model UserAnswer {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
questionId String
|
||||
selectedAnswer String
|
||||
isCorrect Boolean
|
||||
timeSpentMs Int
|
||||
answeredAt DateTime @default(now())
|
||||
|
||||
session GameSession @relation(fields: [sessionId], references: [id])
|
||||
question Question @relation(fields: [questionId], references: [id])
|
||||
}
|
||||
|
||||
model LeaderboardEntry {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
grade Grade
|
||||
elo Int @default(800)
|
||||
bestTime Int?
|
||||
gamesPlayed Int @default(0)
|
||||
totalCorrect Int @default(0)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([userId, grade])
|
||||
}
|
||||
Reference in New Issue
Block a user