From 66aa2577a2f5b96192c67f9982f60f2fb0ed2d91 Mon Sep 17 00:00:00 2001 From: Wayne Ngo Date: Wed, 18 Mar 2026 21:25:28 -0700 Subject: [PATCH 1/5] server --- cmd/server/main.go | 22 +++++++++++++++++ internal/handlers/assessments.go | 36 ++++++++++++++++++++++++++++ internal/handlers/attempts.go | 41 ++++++++++++++++++++++++++++++++ internal/handlers/invites.go | 31 ++++++++++++++++++++++++ internal/handlers/submissions.go | 23 ++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 internal/handlers/assessments.go create mode 100644 internal/handlers/attempts.go create mode 100644 internal/handlers/invites.go create mode 100644 internal/handlers/submissions.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 6ff402d..9ae339f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/gin-gonic/gin" + "CodeSCE/internal/handlers" ) func main() { @@ -11,5 +12,26 @@ func main() { "message": "pong", }) }) + + api := r.Group("/api") + api.POST("/assessments", handlers.CreateAssessment) + api.GET("/assessments", handlers.ListAssessments) + api.GET("/assessments/:id", handlers.GetAssessment) + api.POST("/assessments/:id/questions", handlers.AddQuestion) + api.POST("/assessments/:id/questions/:qid/test-cases", handlers.AddTestCases) + + api.POST("/assessments/:id/invites", handlers.CreateInvite) + api.GET("/invites", handlers.ListInvites) + api.GET("/invites/:token", handlers.ValidateInvite) + api.POST("/invites/:token/start", handlers.StartAttempt) + + api.GET("/attempts", handlers.ListAttempts) + api.GET("/attempts/:id", handlers.GetAttempt) + api.GET("/attempts/:id/questions", handlers.GetAttemptQuestions) + api.POST("/attempts/:id/answers", handlers.SaveAnswers) + api.POST("/attempts/:id/submit", handlers.SubmitAttempt) + + api.POST("/attempts/:id/questions/:qid/submissions", handlers.CreateSubmission) + api.GET("/submissions/:id", handlers.GetSubmission) r.Run(":6767") } diff --git a/internal/handlers/assessments.go b/internal/handlers/assessments.go new file mode 100644 index 0000000..160cb61 --- /dev/null +++ b/internal/handlers/assessments.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +func CreateAssessment(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "create assessment route wired"}) +} + +func ListAssessments(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "list assessments route wired"}) +} + +func GetAssessment(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "get assessment route wired", + "id": c.Param("id"), + }) +} + +func AddQuestion(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "add question route wired", + "id": c.Param("id"), + }) +} + +func AddTestCases(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "add test cases route wired", + "id": c.Param("id"), + "qid": c.Param("qid"), + }) +} \ No newline at end of file diff --git a/internal/handlers/attempts.go b/internal/handlers/attempts.go new file mode 100644 index 0000000..d9976ac --- /dev/null +++ b/internal/handlers/attempts.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +func ListAttempts(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "list attempts route wired", + "assessmentId": c.Query("assessmentId"), + }) +} + +func GetAttempt(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "get attempt route wired", + "id": c.Param("id"), + }) +} + +func GetAttemptQuestions(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "get attempt questions route wired", + "id": c.Param("id"), + }) +} + +func SaveAnswers(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "save answers route wired", + "id": c.Param("id"), + }) +} + +func SubmitAttempt(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "submit attempt route wired", + "id": c.Param("id"), + }) +} \ No newline at end of file diff --git a/internal/handlers/invites.go b/internal/handlers/invites.go new file mode 100644 index 0000000..2964819 --- /dev/null +++ b/internal/handlers/invites.go @@ -0,0 +1,31 @@ +package handlers + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +func CreateInvite(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "create invite route wired", + "id": c.Param("id"), + }) +} + +func ListInvites(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "list invites route wired"}) +} + +func ValidateInvite(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "validate invite route wired", + "token": c.Param("token"), + }) +} + +func StartAttempt(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "start attempt route wired", + "token": c.Param("token"), + }) +} \ No newline at end of file diff --git a/internal/handlers/submissions.go b/internal/handlers/submissions.go new file mode 100644 index 0000000..d498155 --- /dev/null +++ b/internal/handlers/submissions.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +func CreateSubmission(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "create submission route wired", + "attempt_id": c.Param("id"), + "question_id": c.Param("qid"), + "submission_id": "mock-submission-id", + }) +} + +func GetSubmission(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "get submission route wired", + "id": c.Param("id"), + "status": "queued", + }) +} \ No newline at end of file From b014a2e87e68fc47c427533455dbf14263d6f5ed Mon Sep 17 00:00:00 2001 From: Wayne Ngo Date: Thu, 19 Mar 2026 00:56:04 -0700 Subject: [PATCH 2/5] run code --- cmd/server/main.go | 1 + internal/handlers/attempts.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/cmd/server/main.go b/cmd/server/main.go index 9ae339f..739edda 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -33,5 +33,6 @@ func main() { api.POST("/attempts/:id/questions/:qid/submissions", handlers.CreateSubmission) api.GET("/submissions/:id", handlers.GetSubmission) + api.POST("/attempts/:id/questions/:qid/run", handlers.RunCode) r.Run(":6767") } diff --git a/internal/handlers/attempts.go b/internal/handlers/attempts.go index d9976ac..2d74efe 100644 --- a/internal/handlers/attempts.go +++ b/internal/handlers/attempts.go @@ -38,4 +38,12 @@ func SubmitAttempt(c *gin.Context) { "message": "submit attempt route wired", "id": c.Param("id"), }) +} + +func RunCode(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "attempt_id": c.Param("id"), + "question_id": c.Param("qid"), + "passed_test_cases": []int{}, + }) } \ No newline at end of file From 06a56471e8a5065d26c9a6e1a0ed5861f4dff22d Mon Sep 17 00:00:00 2001 From: Darren Shen Date: Sat, 21 Mar 2026 16:06:17 -0700 Subject: [PATCH 3/5] Set up Docker Compose with frontend, backend, and Postgres services --- .env.example | 14 +++++++++++ .gitignore | 1 + Dockerfile | 41 ++++++++++++++++++++++++++++++ docker-compose.dev.yml | 29 +++++++++++++++++++++ docker-compose.yml | 57 ++++++++++++++++++++++++++++++++++++++++++ frontend/Dockerfile | 38 ++++++++++++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ae4bbc1 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Database +POSTGRES_DB=codesce +POSTGRES_USER=codesce +POSTGRES_PASSWORD=changeme +DB_PORT=5432 + +# Backend +BACKEND_PORT=6767 +GIN_MODE=debug +DATABASE_URL=postgres://codesce:changeme@database:5432/codesce?sslmode=disable + +# Frontend +FRONTEND_PORT=3000 +VITE_API_URL=http://localhost:6767 diff --git a/.gitignore b/.gitignore index 303a196..6d7f9c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .cursor/ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d14f3fb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# Backend Dockerfile (Go/Gin) + +# --- Dev stage --- +FROM golang:1.25-alpine AS dev + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +EXPOSE 6767 + +CMD ["go", "run", "./cmd/server"] +# Note: app should listen on port 6767 + +# --- Build stage --- +FROM golang:1.25-alpine AS build + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o /bin/server ./cmd/server + +# --- Production stage --- +FROM alpine:3.20 AS production + +RUN apk add --no-cache ca-certificates + +WORKDIR /app + +COPY --from=build /bin/server /bin/server + +EXPOSE 6767 + +CMD ["/bin/server"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..a79ab08 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,29 @@ +services: + database: + ports: + - "${DB_PORT:-5432}:5432" + + backend: + build: + target: dev + environment: + GIN_MODE: debug + volumes: + - .:/app + - go-mod-cache:/go/pkg/mod + ports: + - "${BACKEND_PORT:-6767}:6767" + + frontend: + build: + target: dev + environment: + VITE_API_URL: http://localhost:${BACKEND_PORT:-6767} + volumes: + - ./frontend:/app + - /app/node_modules + ports: + - "${FRONTEND_PORT:-3000}:5173" + +volumes: + go-mod-cache: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d64894e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +services: + database: + image: postgres:16-alpine + container_name: codesce-db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - codesce-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: + context: . + dockerfile: Dockerfile + container_name: codesce-backend + environment: + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}?sslmode=disable + GIN_MODE: ${GIN_MODE:-release} + PORT: ${BACKEND_PORT:-6767} + ports: + - "${BACKEND_PORT:-6767}:6767" + depends_on: + database: + condition: service_healthy + networks: + - codesce-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: codesce-frontend + environment: + VITE_API_URL: ${VITE_API_URL:-http://backend:6767} + ports: + - "${FRONTEND_PORT:-3000}:80" + depends_on: + - backend + networks: + - codesce-network + +volumes: + pgdata: + +networks: + codesce-network: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..8b87a17 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,38 @@ +# Frontend Dockerfile (React/Vite) + +# --- Dev stage --- +FROM node:20-alpine AS dev + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] + +# --- Build stage --- +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + +# --- Production stage --- +FROM nginx:alpine AS production + +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] From 9a4ebda998c0b9d42eb9402afdc4012bbcd63ca4 Mon Sep 17 00:00:00 2001 From: Darren Shen Date: Sun, 22 Mar 2026 19:07:44 -0700 Subject: [PATCH 4/5] Split Dockerfiles, updated docker-compose files, and use shared sce network --- Dockerfile | 15 --------------- Dockerfile.dev | 15 +++++++++++++++ docker-compose.dev.yml | 13 +++++++++++-- docker-compose.yml | 11 +++-------- frontend/Dockerfile | 15 --------------- frontend/Dockerfile.dev | 15 +++++++++++++++ 6 files changed, 44 insertions(+), 40 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 frontend/Dockerfile.dev diff --git a/Dockerfile b/Dockerfile index d14f3fb..7a2bbd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,5 @@ # Backend Dockerfile (Go/Gin) -# --- Dev stage --- -FROM golang:1.25-alpine AS dev - -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -EXPOSE 6767 - -CMD ["go", "run", "./cmd/server"] -# Note: app should listen on port 6767 - # --- Build stage --- FROM golang:1.25-alpine AS build diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..6fe56fb --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,15 @@ +# Backend Dev Dockerfile (Go/Gin) + +FROM golang:1.25-alpine + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +EXPOSE 6767 + +CMD ["go", "run", "./cmd/server"] +# Note: app should listen on port 6767 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a79ab08..a308fb0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,18 +5,25 @@ services: backend: build: - target: dev + context: . + dockerfile: Dockerfile.dev environment: GIN_MODE: debug + PORT: ${BACKEND_PORT:-6767} + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}?sslmode=disable volumes: - .:/app - go-mod-cache:/go/pkg/mod ports: - "${BACKEND_PORT:-6767}:6767" + depends_on: + database: + condition: service_healthy frontend: build: - target: dev + context: ./frontend + dockerfile: Dockerfile.dev environment: VITE_API_URL: http://localhost:${BACKEND_PORT:-6767} volumes: @@ -24,6 +31,8 @@ services: - /app/node_modules ports: - "${FRONTEND_PORT:-3000}:5173" + depends_on: + - backend volumes: go-mod-cache: diff --git a/docker-compose.yml b/docker-compose.yml index d64894e..4b8d47c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,6 @@ services: - "${DB_PORT:-5432}:5432" volumes: - pgdata:/var/lib/postgresql/data - networks: - - codesce-network healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s @@ -32,8 +30,6 @@ services: depends_on: database: condition: service_healthy - networks: - - codesce-network frontend: build: @@ -46,12 +42,11 @@ services: - "${FRONTEND_PORT:-3000}:80" depends_on: - backend - networks: - - codesce-network volumes: pgdata: networks: - codesce-network: - driver: bridge + default: + external: true + name: sce diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 8b87a17..4733e29 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,20 +1,5 @@ # Frontend Dockerfile (React/Vite) -# --- Dev stage --- -FROM node:20-alpine AS dev - -WORKDIR /app - -COPY package.json package-lock.json ./ - -RUN npm ci - -COPY . . - -EXPOSE 5173 - -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] - # --- Build stage --- FROM node:20-alpine AS build diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..76df19e --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,15 @@ +# Frontend Dev Dockerfile (React/Vite) + +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] From 2f73e9ab3534939884b644faad80d1d270ad8617 Mon Sep 17 00:00:00 2001 From: Darren Shen Date: Sat, 28 Mar 2026 10:39:15 -0700 Subject: [PATCH 5/5] Set up the database schema --- .gitignore | 1 + cmd/server/main.go | 66 ++++++++++----- go.mod | 9 ++- go.sum | 11 +++ internal/db/schema.go | 135 +++++++++++++++++++++++++++++++ internal/handlers/assessments.go | 2 +- internal/handlers/attempts.go | 2 +- internal/handlers/invites.go | 2 +- 8 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 internal/db/schema.go diff --git a/.gitignore b/.gitignore index 6d7f9c6..2bfc54d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .cursor/ .env +/server \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 739edda..a5c24b3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,11 +1,37 @@ package main import ( + "database/sql" + "log" + "os" + "github.com/gin-gonic/gin" + _ "github.com/jackc/pgx/v5/stdlib" + + "CodeSCE/internal/db" "CodeSCE/internal/handlers" ) func main() { + dsn := os.Getenv("DATABASE_URL") + if dsn == "" { + log.Fatal("DATABASE_URL environment variable is required") + } + + database, err := sql.Open("pgx", dsn) + if err != nil { + log.Fatalf("failed to open database: %v", err) + } + defer database.Close() + + if err := database.Ping(); err != nil { + log.Fatalf("failed to ping database: %v", err) + } + + if err := db.InitSchema(database); err != nil { + log.Fatalf("failed to initialize schema: %v", err) + } + r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ @@ -14,25 +40,25 @@ func main() { }) api := r.Group("/api") - api.POST("/assessments", handlers.CreateAssessment) - api.GET("/assessments", handlers.ListAssessments) - api.GET("/assessments/:id", handlers.GetAssessment) - api.POST("/assessments/:id/questions", handlers.AddQuestion) - api.POST("/assessments/:id/questions/:qid/test-cases", handlers.AddTestCases) - - api.POST("/assessments/:id/invites", handlers.CreateInvite) - api.GET("/invites", handlers.ListInvites) - api.GET("/invites/:token", handlers.ValidateInvite) - api.POST("/invites/:token/start", handlers.StartAttempt) - - api.GET("/attempts", handlers.ListAttempts) - api.GET("/attempts/:id", handlers.GetAttempt) - api.GET("/attempts/:id/questions", handlers.GetAttemptQuestions) - api.POST("/attempts/:id/answers", handlers.SaveAnswers) - api.POST("/attempts/:id/submit", handlers.SubmitAttempt) - - api.POST("/attempts/:id/questions/:qid/submissions", handlers.CreateSubmission) - api.GET("/submissions/:id", handlers.GetSubmission) - api.POST("/attempts/:id/questions/:qid/run", handlers.RunCode) + api.POST("/assessments", handlers.CreateAssessment) + api.GET("/assessments", handlers.ListAssessments) + api.GET("/assessments/:id", handlers.GetAssessment) + api.POST("/assessments/:id/questions", handlers.AddQuestion) + api.POST("/assessments/:id/questions/:qid/test-cases", handlers.AddTestCases) + + api.POST("/assessments/:id/invites", handlers.CreateInvite) + api.GET("/invites", handlers.ListInvites) + api.GET("/invites/:token", handlers.ValidateInvite) + api.POST("/invites/:token/start", handlers.StartAttempt) + + api.GET("/attempts", handlers.ListAttempts) + api.GET("/attempts/:id", handlers.GetAttempt) + api.GET("/attempts/:id/questions", handlers.GetAttemptQuestions) + api.POST("/attempts/:id/answers", handlers.SaveAnswers) + api.POST("/attempts/:id/submit", handlers.SubmitAttempt) + + api.POST("/attempts/:id/questions/:qid/submissions", handlers.CreateSubmission) + api.GET("/submissions/:id", handlers.GetSubmission) + api.POST("/attempts/:id/questions/:qid/run", handlers.RunCode) r.Run(":6767") } diff --git a/go.mod b/go.mod index a52ea44..e21604e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module CodeSCE go 1.25.0 -require github.com/gin-gonic/gin v1.12.0 +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/jackc/pgx/v5 v5.9.1 +) require ( github.com/bytedance/gopkg v0.1.3 // indirect @@ -16,6 +19,9 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -31,6 +37,7 @@ require ( golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/go.sum b/go.sum index 3a33231..8a0b94e 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,14 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -56,6 +64,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -76,6 +85,8 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/internal/db/schema.go b/internal/db/schema.go new file mode 100644 index 0000000..18c3363 --- /dev/null +++ b/internal/db/schema.go @@ -0,0 +1,135 @@ +package db + +import ( + "database/sql" + "fmt" +) + +var schemas = []string{ + // 1. Assessment templates + `CREATE TABLE IF NOT EXISTS assessment_templates ( + id BIGSERIAL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + duration_minutes INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + )`, + + // 2. Questions + `CREATE TABLE IF NOT EXISTS questions ( + id BIGSERIAL PRIMARY KEY, + assessment_template_id BIGINT NOT NULL REFERENCES assessment_templates(id) ON DELETE CASCADE, + type TEXT NOT NULL CHECK (type IN ('mcq', 'multi_select', 'short_answer', 'code')), + prompt TEXT NOT NULL, + language TEXT, + options JSONB, + correct_answer JSONB, + score INTEGER NOT NULL DEFAULT 0, + position INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE (assessment_template_id, position) + )`, + + // 3. Test cases (for code questions) + `CREATE TABLE IF NOT EXISTS test_cases ( + id BIGSERIAL PRIMARY KEY, + question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, + input TEXT NOT NULL, + expected_output TEXT NOT NULL, + is_hidden BOOLEAN NOT NULL DEFAULT FALSE, + score INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + )`, + + // 4. Assessment invites + `CREATE TABLE IF NOT EXISTS assessment_invites ( + id BIGSERIAL PRIMARY KEY, + assessment_template_id BIGINT NOT NULL REFERENCES assessment_templates(id) ON DELETE CASCADE, + candidate_email TEXT NOT NULL, + invite_token TEXT NOT NULL UNIQUE, + expires_at TIMESTAMP NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'started', 'completed', 'expired', 'cancelled')), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + )`, + + // 5. Assessment attempts + `CREATE TABLE IF NOT EXISTS assessment_attempts ( + id BIGSERIAL PRIMARY KEY, + invite_id BIGINT NOT NULL REFERENCES assessment_invites(id) ON DELETE CASCADE, + started_at TIMESTAMP, + completed_at TIMESTAMP, + status TEXT NOT NULL DEFAULT 'in_progress' + CHECK (status IN ('in_progress', 'submitted', 'graded', 'abandoned')), + total_score INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + )`, + + // 6. Answers (non-code responses) + `CREATE TABLE IF NOT EXISTS answers ( + id BIGSERIAL PRIMARY KEY, + attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE, + question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, + response JSONB, + score_awarded INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE (attempt_id, question_id) + )`, + + // 7. Submissions (code responses) + `CREATE TABLE IF NOT EXISTS submissions ( + id BIGSERIAL PRIMARY KEY, + attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE, + question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, + language TEXT NOT NULL, + source_code TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'queued' + CHECK (status IN ('queued', 'running', 'passed', 'failed', 'runtime_error', 'compile_error')), + stdout TEXT, + stderr TEXT, + execution_ms INTEGER, + memory_bytes BIGINT, + score_awarded INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + )`, + + // 8. Submission results (per test case) + `CREATE TABLE IF NOT EXISTS submission_results ( + id BIGSERIAL PRIMARY KEY, + submission_id BIGINT NOT NULL REFERENCES submissions(id) ON DELETE CASCADE, + test_case_id BIGINT NOT NULL REFERENCES test_cases(id) ON DELETE CASCADE, + passed BOOLEAN NOT NULL DEFAULT FALSE, + actual_output TEXT, + execution_ms INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + )`, + + // Indexes on foreign keys + `CREATE INDEX IF NOT EXISTS idx_questions_template ON questions(assessment_template_id)`, + `CREATE INDEX IF NOT EXISTS idx_test_cases_question ON test_cases(question_id)`, + `CREATE INDEX IF NOT EXISTS idx_invites_template ON assessment_invites(assessment_template_id)`, + `CREATE INDEX IF NOT EXISTS idx_attempts_invite ON assessment_attempts(invite_id)`, + `CREATE INDEX IF NOT EXISTS idx_answers_attempt ON answers(attempt_id)`, + `CREATE INDEX IF NOT EXISTS idx_answers_question ON answers(question_id)`, + `CREATE INDEX IF NOT EXISTS idx_submissions_attempt ON submissions(attempt_id)`, + `CREATE INDEX IF NOT EXISTS idx_submissions_question ON submissions(question_id)`, + `CREATE INDEX IF NOT EXISTS idx_submission_results_submission ON submission_results(submission_id)`, + `CREATE INDEX IF NOT EXISTS idx_submission_results_test_case ON submission_results(test_case_id)`, +} + +// InitSchema creates all tables and indexes for the assessment platform. +func InitSchema(db *sql.DB) error { + for i, stmt := range schemas { + if _, err := db.Exec(stmt); err != nil { + return fmt.Errorf("failed to execute schema statement %d: %w", i, err) + } + } + return nil +} diff --git a/internal/handlers/assessments.go b/internal/handlers/assessments.go index 160cb61..6a1e317 100644 --- a/internal/handlers/assessments.go +++ b/internal/handlers/assessments.go @@ -33,4 +33,4 @@ func AddTestCases(c *gin.Context) { "id": c.Param("id"), "qid": c.Param("qid"), }) -} \ No newline at end of file +} diff --git a/internal/handlers/attempts.go b/internal/handlers/attempts.go index 2d74efe..b6fc65d 100644 --- a/internal/handlers/attempts.go +++ b/internal/handlers/attempts.go @@ -46,4 +46,4 @@ func RunCode(c *gin.Context) { "question_id": c.Param("qid"), "passed_test_cases": []int{}, }) -} \ No newline at end of file +} diff --git a/internal/handlers/invites.go b/internal/handlers/invites.go index 2964819..9cfa5dd 100644 --- a/internal/handlers/invites.go +++ b/internal/handlers/invites.go @@ -28,4 +28,4 @@ func StartAttempt(c *gin.Context) { "message": "start attempt route wired", "token": c.Param("token"), }) -} \ No newline at end of file +}