From d81394cc8a257402a75e82398c9d8832e95e693e Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 16 Oct 2025 21:12:07 +0400 Subject: [PATCH 01/24] ci: attempt 123129037123 to make multiarch images --- .github/workflows/deploy.yaml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6fced6e..5dbd83b 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -14,6 +14,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: @@ -21,10 +24,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GHCR_TOKEN }} - - name: Build and push image + - name: Build and push multiarch image + env: + SHORT_SHA: ${{ github.sha }} run: | - SHORT_SHA=${GITHUB_SHA::7} - docker build -t ghcr.io/voidcontests/api:$SHORT_SHA \ - -t ghcr.io/voidcontests/api:latest . - docker push ghcr.io/voidcontests/api:$SHORT_SHA - docker push ghcr.io/voidcontests/api:latest + SHORT_SHA=${SHORT_SHA::7} + docker buildx build \ + --platform linux/amd64,linux/arm64/v8 --push \ + -t ghcr.io/voidcontests/api:$SHORT_SHA \ + -t ghcr.io/voidcontests/api:latest . From 1da19ac59cf454376af0f35f1516065263cf459f Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 16 Oct 2025 21:21:47 +0400 Subject: [PATCH 02/24] ci: Update Dockerfile to sped up multiarch builds --- Dockerfile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3ef11f1..2968410 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM golang:1.25.1-alpine3.22 AS builder +FROM --platform=$BUILDPLATFORM golang:1.25.1-alpine3.22 AS builder + +ARG TARGETOS +ARG TARGETARCH WORKDIR /app @@ -8,13 +11,14 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN go build -a -ldflags "-w -s \ +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ + go build -a -ldflags "-w -s \ -X github.com/voidcontests/api/internal/version.GIT_COMMIT=$(git rev-parse --short HEAD) \ -X github.com/voidcontests/api/internal/version.GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)" \ -o build/api ./cmd/api -# Lightweight docker container with binaries only -FROM alpine:latest +# lightweight docker container with binaries only +FROM --platform=$TARGETPLATFORM alpine:latest WORKDIR /app From d11d2e23459b86aac23e01ca008a85efd84f7d0f Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 16 Oct 2025 21:25:05 +0400 Subject: [PATCH 03/24] ci: try to fix warn --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2968410..236ba70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ -o build/api ./cmd/api # lightweight docker container with binaries only -FROM --platform=$TARGETPLATFORM alpine:latest +FROM alpine:latest WORKDIR /app From d7a340be99acafa26f4b9f9c789c4fe9d57c2b85 Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 16 Oct 2025 21:39:21 +0400 Subject: [PATCH 04/24] chore: use CONFIG_PATH from .env --- docker-compose.local.yaml | 21 --------------------- docker-compose.yaml | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 docker-compose.local.yaml diff --git a/docker-compose.local.yaml b/docker-compose.local.yaml deleted file mode 100644 index c67960a..0000000 --- a/docker-compose.local.yaml +++ /dev/null @@ -1,21 +0,0 @@ -services: - postgres: - container_name: void-postgres - image: postgres:latest - restart: unless-stopped - volumes: - - ./.db/postgres/data:/var/lib/postgresql/data - environment: - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - ports: - - "5432:5432" - - redis: - container_name: void-redis - image: redis:latest - restart: unless-stopped - command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD} - ports: - - "6379:6379" diff --git a/docker-compose.yaml b/docker-compose.yaml index d17335a..3d3392a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: container_name: void-api restart: unless-stopped environment: - CONFIG_PATH: ./config/dev.yaml + CONFIG_PATH: ${CONFIG_PATH} ports: - "5919:5919" volumes: From 949d6f0327be75721fdfc7c413fac1e9b22be1ca Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 16 Oct 2025 22:07:31 +0400 Subject: [PATCH 05/24] chore: rollback multiarch builds --- .github/workflows/deploy.yaml | 17 ++++++----------- Dockerfile | 8 ++------ 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5dbd83b..6fced6e 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -14,9 +14,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: @@ -24,12 +21,10 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GHCR_TOKEN }} - - name: Build and push multiarch image - env: - SHORT_SHA: ${{ github.sha }} + - name: Build and push image run: | - SHORT_SHA=${SHORT_SHA::7} - docker buildx build \ - --platform linux/amd64,linux/arm64/v8 --push \ - -t ghcr.io/voidcontests/api:$SHORT_SHA \ - -t ghcr.io/voidcontests/api:latest . + SHORT_SHA=${GITHUB_SHA::7} + docker build -t ghcr.io/voidcontests/api:$SHORT_SHA \ + -t ghcr.io/voidcontests/api:latest . + docker push ghcr.io/voidcontests/api:$SHORT_SHA + docker push ghcr.io/voidcontests/api:latest diff --git a/Dockerfile b/Dockerfile index 236ba70..8ea41f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25.1-alpine3.22 AS builder - -ARG TARGETOS -ARG TARGETARCH +FROM golang:1.25.1-alpine3.22 AS builder WORKDIR /app @@ -11,8 +8,7 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ - go build -a -ldflags "-w -s \ +RUN go build -a -ldflags "-w -s \ -X github.com/voidcontests/api/internal/version.GIT_COMMIT=$(git rev-parse --short HEAD) \ -X github.com/voidcontests/api/internal/version.GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)" \ -o build/api ./cmd/api From ef4a34fd17ca99262c896dbe4afbeac579b1698f Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 16 Oct 2025 23:29:37 +0400 Subject: [PATCH 06/24] chore: change 404 to 401 on /api/account --- internal/app/handler/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/handler/account.go b/internal/app/handler/account.go index 89d363a..7978b9c 100644 --- a/internal/app/handler/account.go +++ b/internal/app/handler/account.go @@ -84,7 +84,7 @@ func (h *Handler) GetAccount(c echo.Context) error { user, err := h.repo.User.GetByID(ctx, claims.UserID) if errors.Is(err, pgx.ErrNoRows) { - return Error(http.StatusNotFound, "user not found") + return Error(http.StatusUnauthorized, "invalid or expired token") } if err != nil { return fmt.Errorf("%s: can't get user: %v", op, err) From 92c10cd58293c2f23ad6323323d21f5733f78298 Mon Sep 17 00:00:00 2001 From: jus1d Date: Sun, 19 Oct 2025 17:11:42 +0400 Subject: [PATCH 07/24] chore: return err if we cant publish to q --- internal/app/handler/submission.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/app/handler/submission.go b/internal/app/handler/submission.go index 873b41b..0a326dc 100644 --- a/internal/app/handler/submission.go +++ b/internal/app/handler/submission.go @@ -121,7 +121,9 @@ func (h *Handler) CreateSubmission(c echo.Context) error { if err := h.broker.PublishSubmission(ctx, s); err != nil { log.Error("can't publish submission", sl.Err(err)) - // NOTE: should we return error to user? + // TODO: if error happened, we probably need to: + // set either `cancelled` status, or delay submisison execution + return err } return c.JSON(http.StatusCreated, response.Submission{ From ca76081ef1508ec64f94043d69355713f5fcf972 Mon Sep 17 00:00:00 2001 From: jus1d Date: Tue, 21 Oct 2025 17:19:19 +0400 Subject: [PATCH 08/24] chore: Remove migrations [moved to infra repo] --- migrations/000001_init.down.sql | 12 ---- migrations/000001_init.up.sql | 99 --------------------------------- 2 files changed, 111 deletions(-) delete mode 100644 migrations/000001_init.down.sql delete mode 100644 migrations/000001_init.up.sql diff --git a/migrations/000001_init.down.sql b/migrations/000001_init.down.sql deleted file mode 100644 index 4e501b4..0000000 --- a/migrations/000001_init.down.sql +++ /dev/null @@ -1,12 +0,0 @@ -DROP TABLE IF EXISTS failed_tests; -DROP TABLE IF EXISTS submissions; -DROP TABLE IF EXISTS entries; -DROP TABLE IF EXISTS contest_problems; -DROP TABLE IF EXISTS test_cases; -DROP TABLE IF EXISTS problems; -DROP TABLE IF EXISTS contests; -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS roles; - -DROP TYPE IF EXISTS verdict; -DROP TYPE IF EXISTS problem_kind; diff --git a/migrations/000001_init.up.sql b/migrations/000001_init.up.sql deleted file mode 100644 index a5aa855..0000000 --- a/migrations/000001_init.up.sql +++ /dev/null @@ -1,99 +0,0 @@ -CREATE TABLE roles ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) UNIQUE NOT NULL, - created_problems_limit INTEGER NOT NULL, - created_contests_limit INTEGER NOT NULL, - is_default BOOLEAN DEFAULT false NOT NULL, - created_at TIMESTAMP DEFAULT now() NOT NULL -); - -INSERT INTO roles (name, created_problems_limit, created_contests_limit, is_default) VALUES - ('admin', -1, -1, false), - ('unlimited', -1, -1, false), - ('limited', 10, 2, true), - ('banned', 0, 0, false); - -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, - created_at TIMESTAMP DEFAULT now() NOT NULL -); - -CREATE TYPE problem_kind AS ENUM ('text_answer_problem', 'coding_problem'); -CREATE TYPE verdict AS ENUM ( - 'pending', 'running', 'ok', 'wrong_answer', - 'runtime_error', 'compilation_error', 'time_limit_exceeded' -); - -CREATE TABLE contests ( - id SERIAL PRIMARY KEY, - creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - title VARCHAR(64) NOT NULL, - description VARCHAR(300) DEFAULT '' NOT NULL, - start_time TIMESTAMP NOT NULL, - end_time TIMESTAMP NOT NULL, - duration_mins INTEGER NOT NULL CHECK (duration_mins >= 0), - max_entries INTEGER DEFAULT 0 NOT NULL CHECK (max_entries >= 0), - allow_late_join BOOLEAN DEFAULT true NOT NULL, - created_at TIMESTAMP DEFAULT now() NOT NULL -); - -CREATE TABLE problems ( - id SERIAL PRIMARY KEY, - kind problem_kind NOT NULL, - writer_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - title VARCHAR(64) NOT NULL, - statement TEXT DEFAULT '' NOT NULL, - difficulty VARCHAR(10) NOT NULL CHECK (difficulty IN ('easy', 'medium', 'hard')), - answer TEXT NOT NULL, - time_limit_ms INTEGER DEFAULT 5000 NOT NULL CHECK (time_limit_ms >= 0), - created_at TIMESTAMP DEFAULT now() NOT NULL -); - -CREATE TABLE test_cases ( - id SERIAL PRIMARY KEY, - problem_id INTEGER NOT NULL REFERENCES problems(id) ON DELETE CASCADE, - input TEXT NOT NULL, - output TEXT NOT NULL, - is_example BOOLEAN DEFAULT false NOT NULL -); - -CREATE TABLE contest_problems ( - contest_id INTEGER NOT NULL REFERENCES contests(id) ON DELETE CASCADE, - problem_id INTEGER NOT NULL REFERENCES problems(id) ON DELETE CASCADE, - charcode VARCHAR(2) NOT NULL, - PRIMARY KEY (contest_id, problem_id), - UNIQUE (contest_id, charcode) -); - -CREATE TABLE entries ( - id SERIAL PRIMARY KEY, - contest_id INTEGER NOT NULL REFERENCES contests(id) ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - created_at TIMESTAMP DEFAULT now() NOT NULL, - UNIQUE (contest_id, user_id) -); - -CREATE TABLE submissions ( - id SERIAL PRIMARY KEY, - entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE, - problem_id INTEGER NOT NULL REFERENCES problems(id) ON DELETE CASCADE, - verdict verdict NOT NULL, - answer TEXT NOT NULL, - code TEXT NOT NULL, - language VARCHAR(20) NOT NULL, - passed_tests_count INTEGER DEFAULT 0 NOT NULL CHECK (passed_tests_count >= 0), - stderr TEXT NOT NULL, - created_at TIMESTAMP DEFAULT now() NOT NULL -); - -CREATE TABLE failed_tests ( - id SERIAL PRIMARY KEY, - submission_id INTEGER NOT NULL REFERENCES submissions(id) ON DELETE CASCADE, - input TEXT NOT NULL, - expected_output TEXT NOT NULL, - actual_output TEXT NOT NULL, - created_at TIMESTAMP DEFAULT now() NOT NULL -); From 0459953e7ed078df0068f6d0141c00ae89c43eb3 Mon Sep 17 00:00:00 2001 From: jus1d Date: Tue, 21 Oct 2025 17:19:44 +0400 Subject: [PATCH 09/24] chore: Introduce status field in submission --- build.sh | 88 ------------------- internal/app/handler/dto/response/response.go | 1 + internal/app/handler/submission.go | 50 +++++------ internal/storage/models/models.go | 1 + internal/storage/models/status/status.go | 7 ++ internal/storage/models/verdict/verdict.go | 11 +++ .../postgres/submission/submission.go | 44 ++++++---- internal/storage/repository/repository.go | 4 +- 8 files changed, 74 insertions(+), 132 deletions(-) delete mode 100755 build.sh create mode 100644 internal/storage/models/status/status.go create mode 100644 internal/storage/models/verdict/verdict.go diff --git a/build.sh b/build.sh deleted file mode 100755 index a91d05d..0000000 --- a/build.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash - -# colors -bold='\033[0;1m' -italic='\033[0;3m' -underl='\033[0;4m' -red='\033[0;31m' -green='\033[0;32m' -blue='\033[0;34m' -yellow='\033[0;33m' -normal='\033[0m' - -out="build/server" - -help() { - echo -e "${underl}Usage:${normal}\n" - echo -e " ${bold}$0${normal} [${underl}command${normal}]\n" - echo -e "Here is a list of available commands\n" - echo -e " ${bold}deploy${normal} [${underl}branch${normal}] Run deploy script from current or provided branch" - echo -e " ${bold}image${normal} [push] Build docker image and (optional) push to container registry" - echo -e " ${bold}run${normal} [${underl}env${normal}] Run binary with provided environment (local - default)" - echo -e " ${bold}help${normal} Print this help messages to standard output" -} - -build_executable() { - echo "Building executable..." - go build -o ${out} cmd/server/main.go - - echo -e "Server successfully built into ${bold}\`${out}\`${normal}" -} - -if [ "$1" == "image" ]; then - GIT_COMMIT=$(git rev-parse --short HEAD) - - echo -e "Building a docker image from commit ${bold}$GIT_COMMIT${normal}" - docker build -t jus1d/void-server:latest --platform linux/amd64 . - - if [ "$2" == "push" ]; then - docker tag jus1d/void-server:latest jus1d/void-server:$GIT_COMMIT - - tags=("$GIT_COMMIT" "latest") - for tag in "${tags[@]}"; do - echo -e "Pushing ${bold}jus1d/void-server:$tag${normal} to hub" - docker push jus1d/void-server:"$tag" - done - - echo "Built docker image was successfully pushed to dockerhub" - fi -elif [ "$1" == "deploy" ]; then - if [ -n "$2" ]; then - git checkout "$2" - fi - - echo -e "Deploying ${bold}voidcontests/server${normal} from ${bold}$(git rev-parse --abbrev-ref HEAD)${normal} branch" - - echo "Pulling latest image..." - docker pull jus1d/void-server:latest - - echo "Stopping docker compose..." - docker compose down - - echo "Starting docker compose..." - docker compose up -d - - echo "Server running" -elif [ "$1" == "run" ]; then - build_executable - - if [ -n "$2" ]; then - env="$2" - else - env="local" - fi - - if [ $env == "local" ]; then - echo "Start docker containers with environment" - docker compose -f ./docker-compose.local.yaml up -d - fi - - CONFIG_PATH="./config/${env}.yaml" ./build/server - - echo "Shutting down environment containers" - docker compose down -elif [ "$1" == "help" ]; then - help -else - build -fi diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index a7a1105..323aa60 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -74,6 +74,7 @@ type Submission struct { ID int32 `json:"id"` ProblemID int32 `json:"problem_id"` ProblemKind string `json:"problem_kind"` + Status string `json:"status"` Verdict string `json:"verdict"` Answer string `json:"answer,omitempty"` Code string `json:"code,omitempty"` diff --git a/internal/app/handler/submission.go b/internal/app/handler/submission.go index 0a326dc..dcab8cb 100644 --- a/internal/app/handler/submission.go +++ b/internal/app/handler/submission.go @@ -13,7 +13,8 @@ import ( "github.com/voidcontests/api/internal/app/handler/dto/response" "github.com/voidcontests/api/internal/lib/logger/sl" "github.com/voidcontests/api/internal/storage/models" - "github.com/voidcontests/api/internal/storage/repository/postgres/submission" + "github.com/voidcontests/api/internal/storage/models/status" + "github.com/voidcontests/api/internal/storage/models/verdict" "github.com/voidcontests/api/pkg/requestid" "github.com/voidcontests/api/pkg/validate" ) @@ -79,14 +80,14 @@ func (h *Handler) CreateSubmission(c echo.Context) error { } if body.ProblemKind == models.TextAnswerProblem { - var verdict string + var v string if problem.Answer != body.Answer { - verdict = submission.VerdictWrongAnswer + v = verdict.WA } else { - verdict = submission.VerdictOK + v = verdict.OK } - s, err := h.repo.Submission.Create(ctx, entry.ID, problem.ID, verdict, body.Answer, "", "", 0, "") + s, err := h.repo.Submission.CreateWithTextAnswer(ctx, entry.ID, problem.ID, v, body.Answer) if err != nil { log.Error("can't create submission", sl.Err(err)) return err @@ -96,24 +97,13 @@ func (h *Handler) CreateSubmission(c echo.Context) error { ID: s.ID, ProblemID: s.ProblemID, ProblemKind: s.ProblemKind, - Verdict: string(s.Verdict), - Answer: body.Answer, + Status: s.Status, + Verdict: s.Verdict, + Answer: s.Answer, CreatedAt: s.CreatedAt, }) } else if body.ProblemKind == models.CodingProblem { - tcs, err := h.repo.Problem.GetTestCases(ctx, problem.ID) - if err != nil { - log.Error("can't get test cases for problem", sl.Err(err)) - return err - } - - rtcs := make([]models.TestCaseDTO, len(tcs)) - for i := range rtcs { - rtcs[i].Input = tcs[i].Input - rtcs[i].Output = tcs[i].Output - } - - s, err := h.repo.Submission.Create(ctx, entry.ID, problem.ID, submission.VerdictPending, "", body.Code, body.Language, 0, "") + s, err := h.repo.Submission.CreateWithSolution(ctx, entry.ID, problem.ID, body.Code, body.Language) if err != nil { log.Error("can't create submission", sl.Err(err)) return err @@ -121,8 +111,11 @@ func (h *Handler) CreateSubmission(c echo.Context) error { if err := h.broker.PublishSubmission(ctx, s); err != nil { log.Error("can't publish submission", sl.Err(err)) - // TODO: if error happened, we probably need to: - // set either `cancelled` status, or delay submisison execution + // TODO: if we can't push submission into execution queue, try to save it to local memory, and try to push later (?) + // - but is it really needed, after some time? + if err = h.repo.Submission.UpdateVerdictStatus(ctx, s.ID, verdict.IE, status.Completed); err != nil { + slog.Error("failed to update submission's verdict", sl.Err(err)) + } return err } @@ -130,7 +123,8 @@ func (h *Handler) CreateSubmission(c echo.Context) error { ID: s.ID, ProblemID: s.ProblemID, ProblemKind: s.ProblemKind, - Verdict: submission.VerdictPending, + Status: s.Status, + Verdict: s.Verdict, CreatedAt: s.CreatedAt, }) } @@ -163,6 +157,7 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { ID: s.ID, ProblemID: s.ProblemID, ProblemKind: s.ProblemKind, + Status: s.Status, Verdict: s.Verdict, Answer: s.Answer, CreatedAt: s.CreatedAt, @@ -175,12 +170,14 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { return err } - switch s.Verdict { - case submission.VerdictRunning, submission.VerdictPending: + // no need to provide testing report yet (no testing report) + switch s.Status { + case status.Pending, status.Running: return c.JSON(http.StatusOK, response.Submission{ ID: s.ID, ProblemID: s.ProblemID, ProblemKind: s.ProblemKind, + Status: s.Status, Verdict: s.Verdict, Code: s.Code, Language: s.Language, @@ -195,6 +192,7 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { ID: s.ID, ProblemID: s.ProblemID, ProblemKind: s.ProblemKind, + Status: s.Status, Verdict: s.Verdict, Code: s.Code, Language: s.Language, @@ -215,6 +213,7 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { ID: s.ID, ProblemID: s.ProblemID, ProblemKind: s.ProblemKind, + Status: s.Status, Verdict: s.Verdict, Code: s.Code, Language: s.Language, @@ -281,6 +280,7 @@ func (h *Handler) GetSubmissions(c echo.Context) error { ID: submission.ID, ProblemID: submission.ProblemID, ProblemKind: submission.ProblemKind, + Status: submission.Status, Verdict: submission.Verdict, CreatedAt: submission.CreatedAt, } diff --git a/internal/storage/models/models.go b/internal/storage/models/models.go index c65898e..afd0383 100644 --- a/internal/storage/models/models.go +++ b/internal/storage/models/models.go @@ -86,6 +86,7 @@ type Submission struct { EntryID int32 `db:"entry_id"` ProblemID int32 `db:"problem_id"` ProblemKind string `db:"problem_kind"` + Status string `db:"status"` Verdict string `db:"verdict"` Answer string `db:"answer"` Code string `db:"code"` diff --git a/internal/storage/models/status/status.go b/internal/storage/models/status/status.go new file mode 100644 index 0000000..073615d --- /dev/null +++ b/internal/storage/models/status/status.go @@ -0,0 +1,7 @@ +package status + +const ( + Pending = "pending" + Running = "running" + Completed = "completed" +) diff --git a/internal/storage/models/verdict/verdict.go b/internal/storage/models/verdict/verdict.go new file mode 100644 index 0000000..a4a35e5 --- /dev/null +++ b/internal/storage/models/verdict/verdict.go @@ -0,0 +1,11 @@ +package verdict + +const ( + OK = "ok" + RE = "runtime_error" + CE = "compilation_error" + IE = "internal_error" + WA = "wrong_answer" + TLE = "time_limit_exceeded" + NJ = "not_judged" +) diff --git a/internal/storage/repository/postgres/submission/submission.go b/internal/storage/repository/postgres/submission/submission.go index e8db9fa..802eb11 100644 --- a/internal/storage/repository/postgres/submission/submission.go +++ b/internal/storage/repository/postgres/submission/submission.go @@ -8,17 +8,11 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/voidcontests/api/internal/storage/models" + "github.com/voidcontests/api/internal/storage/models/status" + "github.com/voidcontests/api/internal/storage/models/verdict" ) const ( - VerdictPending = "pending" - VerdictRunning = "running" - VerdictOK = "ok" - VerdictWrongAnswer = "wrong_answer" - VerdictRuntimeError = "runtime_error" - VerdictCompilationError = "compilation_error" - VerdictTimeLimitExceeded = "time_limit_exceeded" - defaultLimit = 100 ) @@ -30,21 +24,28 @@ func New(pool *pgxpool.Pool) *Postgres { return &Postgres{pool} } -func (p *Postgres) Create(ctx context.Context, entryID, problemID int32, verdict, answer, code, language string, passedTestsCount int32, stderr string) (models.Submission, error) { - query := ` - INSERT INTO submissions (entry_id, problem_id, verdict, answer, code, language, passed_tests_count, stderr) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING id, entry_id, problem_id, - (SELECT kind FROM problems WHERE id = $2) AS problem_kind, - verdict, answer, code, language, passed_tests_count, stderr, created_at - ` +func (p *Postgres) CreateWithTextAnswer(ctx context.Context, entryID int32, problemID int32, verdict string, answer string) (models.Submission, error) { + return p.create(ctx, entryID, problemID, status.Completed, verdict, answer, "", "", 0, "") +} + +func (p *Postgres) CreateWithSolution(ctx context.Context, entryID int32, problemID int32, code string, language string) (models.Submission, error) { + return p.create(ctx, entryID, problemID, status.Pending, verdict.NJ, "", code, language, 0, "") +} + +func (p *Postgres) create(ctx context.Context, entryID int32, problemID int32, status string, verdict string, answer string, code string, language string, passedTestsCount int32, stderr string) (models.Submission, error) { + query := `INSERT INTO submissions + (entry_id, problem_id, status, verdict, answer, code, language, passed_tests_count, stderr) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id, entry_id, problem_id, (SELECT kind FROM problems WHERE id = $2) AS problem_kind, + status, verdict, answer, code, language, passed_tests_count, stderr, created_at` var submission models.Submission - err := p.pool.QueryRow(ctx, query, entryID, problemID, verdict, answer, code, language, passedTestsCount, stderr).Scan( + err := p.pool.QueryRow(ctx, query, entryID, problemID, status, verdict, answer, code, language, passedTestsCount, stderr).Scan( &submission.ID, &submission.EntryID, &submission.ProblemID, &submission.ProblemKind, + &submission.Status, &submission.Verdict, &submission.Answer, &submission.Code, @@ -57,6 +58,12 @@ func (p *Postgres) Create(ctx context.Context, entryID, problemID int32, verdict return submission, err } +func (p *Postgres) UpdateVerdictStatus(ctx context.Context, id int32, verdict string, status string) error { + query := `UPDATE submissions SET verdict = $1, status = $2 WHERE id = $3` + _, err := p.pool.Exec(ctx, query, verdict, status, id) + return err +} + func (p *Postgres) CountTestsForProblem(ctx context.Context, problemID int32) (int32, error) { var count int32 err := p.pool.QueryRow(ctx, `SELECT COUNT(*) FROM test_cases WHERE problem_id = $1`, problemID).Scan(&count) @@ -147,7 +154,7 @@ func (p *Postgres) GetProblemStatuses(ctx context.Context, entryID int32) (map[i func (p *Postgres) GetByID(ctx context.Context, userID, submissionID int32) (models.Submission, error) { query := ` - SELECT s.id, s.entry_id, s.problem_id, p.kind AS problem_kind, s.verdict, + SELECT s.id, s.entry_id, s.problem_id, p.kind AS problem_kind, s.status, s.verdict, s.answer, s.code, s.language, s.passed_tests_count, s.stderr, s.created_at FROM submissions s JOIN problems p ON p.id = s.problem_id @@ -162,6 +169,7 @@ func (p *Postgres) GetByID(ctx context.Context, userID, submissionID int32) (mod &s.EntryID, &s.ProblemID, &s.ProblemKind, + &s.Status, &s.Verdict, &s.Answer, &s.Code, diff --git a/internal/storage/repository/repository.go b/internal/storage/repository/repository.go index e02dd20..fc7c270 100644 --- a/internal/storage/repository/repository.go +++ b/internal/storage/repository/repository.go @@ -71,8 +71,10 @@ type Entry interface { } type Submission interface { - Create(ctx context.Context, entryID, problemID int32, verdict, answer, code, language string, passedTestsCount int32, stderr string) (models.Submission, error) + CreateWithSolution(ctx context.Context, entryID int32, problemID int32, code string, language string) (models.Submission, error) + CreateWithTextAnswer(ctx context.Context, entryID int32, problemID int32, verdict string, answer string) (models.Submission, error) CountTestsForProblem(ctx context.Context, problemID int32) (int32, error) + UpdateVerdictStatus(ctx context.Context, id int32, verdict string, status string) error GetFailedTest(ctx context.Context, submissionID int32) (models.FailedTest, error) GetProblemStatus(ctx context.Context, entryID int32, problemID int32) (string, error) GetProblemStatuses(ctx context.Context, entryID int32) (map[int32]string, error) From 61f37022c1931f081d95672d14662431ea2717ce Mon Sep 17 00:00:00 2001 From: jus1d Date: Tue, 21 Oct 2025 21:21:17 +0400 Subject: [PATCH 10/24] chore: remove unused verdicts --- internal/storage/models/verdict/verdict.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/storage/models/verdict/verdict.go b/internal/storage/models/verdict/verdict.go index a4a35e5..d6a4c63 100644 --- a/internal/storage/models/verdict/verdict.go +++ b/internal/storage/models/verdict/verdict.go @@ -1,11 +1,8 @@ package verdict const ( - OK = "ok" - RE = "runtime_error" - CE = "compilation_error" - IE = "internal_error" - WA = "wrong_answer" - TLE = "time_limit_exceeded" - NJ = "not_judged" + OK = "ok" + IE = "internal_error" + WA = "wrong_answer" + NJ = "not_judged" ) From 070b43ef1cd97f0a6ac1f30afae0907293a6e995 Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 23 Oct 2025 20:24:13 +0400 Subject: [PATCH 11/24] refactor: Replace relation `failed_tests` with `testing_report` --- internal/app/handler/dto/request/request.go | 6 +- internal/app/handler/dto/response/response.go | 14 +- internal/app/handler/submission.go | 179 +++++++----------- internal/storage/models/models.go | 32 ++-- .../repository/postgres/problem/problem.go | 46 ++--- .../postgres/submission/submission.go | 131 ++++--------- internal/storage/repository/repository.go | 11 +- 7 files changed, 166 insertions(+), 253 deletions(-) diff --git a/internal/app/handler/dto/request/request.go b/internal/app/handler/dto/request/request.go index 74317a4..97b259e 100644 --- a/internal/app/handler/dto/request/request.go +++ b/internal/app/handler/dto/request/request.go @@ -38,8 +38,6 @@ type CreateProblemRequest struct { } type CreateSubmissionRequest struct { - ProblemKind string `json:"problem_kind" required:"true"` - Answer string `json:"answer"` - Code string `json:"code"` - Language string `json:"language"` + Code string `json:"code"` + Language string `json:"language"` } diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index 323aa60..4e8f8ac 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -73,10 +73,8 @@ type ContestListItem struct { type Submission struct { ID int32 `json:"id"` ProblemID int32 `json:"problem_id"` - ProblemKind string `json:"problem_kind"` Status string `json:"status"` Verdict string `json:"verdict"` - Answer string `json:"answer,omitempty"` Code string `json:"code,omitempty"` Language string `json:"language,omitempty"` TestingReport *TestingReport `json:"testing_report,omitempty"` @@ -84,13 +82,15 @@ type Submission struct { } type TestingReport struct { - Passed int `json:"passed"` - Total int `json:"total"` - Stderr string `json:"stderr,omitempty"` - FailedTest *FailedTest `json:"failed_test,omitempty"` + ID int32 `json:"id"` + PassedTestsCount int32 `json:"passed_tests_count"` + TotalTestsCount int32 `json:"total_tests_count"` + FailedTest *Test `json:"failed_test,omitempty"` + Stderr string `json:"stderr"` + CreatedAt time.Time `json:"created_at"` } -type FailedTest struct { +type Test struct { Input string `json:"input"` ExpectedOutput string `json:"expected_output"` ActualOutput string `json:"actual_output"` diff --git a/internal/app/handler/submission.go b/internal/app/handler/submission.go index dcab8cb..4a594aa 100644 --- a/internal/app/handler/submission.go +++ b/internal/app/handler/submission.go @@ -12,7 +12,6 @@ import ( "github.com/voidcontests/api/internal/app/handler/dto/request" "github.com/voidcontests/api/internal/app/handler/dto/response" "github.com/voidcontests/api/internal/lib/logger/sl" - "github.com/voidcontests/api/internal/storage/models" "github.com/voidcontests/api/internal/storage/models/status" "github.com/voidcontests/api/internal/storage/models/verdict" "github.com/voidcontests/api/pkg/requestid" @@ -79,71 +78,43 @@ func (h *Handler) CreateSubmission(c echo.Context) error { return err } - if body.ProblemKind == models.TextAnswerProblem { - var v string - if problem.Answer != body.Answer { - v = verdict.WA - } else { - v = verdict.OK - } - - s, err := h.repo.Submission.CreateWithTextAnswer(ctx, entry.ID, problem.ID, v, body.Answer) - if err != nil { - log.Error("can't create submission", sl.Err(err)) - return err - } - - return c.JSON(http.StatusCreated, response.Submission{ - ID: s.ID, - ProblemID: s.ProblemID, - ProblemKind: s.ProblemKind, - Status: s.Status, - Verdict: s.Verdict, - Answer: s.Answer, - CreatedAt: s.CreatedAt, - }) - } else if body.ProblemKind == models.CodingProblem { - s, err := h.repo.Submission.CreateWithSolution(ctx, entry.ID, problem.ID, body.Code, body.Language) - if err != nil { - log.Error("can't create submission", sl.Err(err)) - return err - } + s, err := h.repo.Submission.Create(ctx, entry.ID, problem.ID, body.Code, body.Language) + if err != nil { + log.Error("can't create submission", sl.Err(err)) + return err + } - if err := h.broker.PublishSubmission(ctx, s); err != nil { - log.Error("can't publish submission", sl.Err(err)) - // TODO: if we can't push submission into execution queue, try to save it to local memory, and try to push later (?) - // - but is it really needed, after some time? - if err = h.repo.Submission.UpdateVerdictStatus(ctx, s.ID, verdict.IE, status.Completed); err != nil { - slog.Error("failed to update submission's verdict", sl.Err(err)) - } - return err - } + // TODO: create initial testing report in database - return c.JSON(http.StatusCreated, response.Submission{ - ID: s.ID, - ProblemID: s.ProblemID, - ProblemKind: s.ProblemKind, - Status: s.Status, - Verdict: s.Verdict, - CreatedAt: s.CreatedAt, - }) + if err := h.broker.PublishSubmission(ctx, s); err != nil { + log.Error("can't publish submission", sl.Err(err)) + // TODO: if we can't push submission into execution queue, try to save it to local memory, and try to push later (?) + // - but is it really needed, after some time? + return err } - return Error(http.StatusBadRequest, "unknown problem kind") + return c.JSON(http.StatusCreated, response.Submission{ + ID: s.ID, + ProblemID: s.ProblemID, + Status: s.Status, + Verdict: s.Verdict, + CreatedAt: s.CreatedAt, + }) } func (h *Handler) GetSubmissionByID(c echo.Context) error { log := slog.With(slog.String("op", "handler.GetSubmissionByID"), slog.String("request_id", requestid.Get(c))) ctx := c.Request().Context() - claims, _ := ExtractClaims(c) + // TODO: check if submission is submitted by request initiator + _, _ = ExtractClaims(c) submissionID, ok := ExtractParamInt(c, "sid") if !ok { return Error(http.StatusBadRequest, "submission ID should be an integer") } - s, err := h.repo.Submission.GetByID(ctx, claims.UserID, int32(submissionID)) + s, err := h.repo.Submission.GetByID(ctx, int32(submissionID)) if errors.Is(err, pgx.ErrNoRows) { return Error(http.StatusNotFound, "submission not found") } @@ -152,80 +123,71 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { return err } - if s.ProblemKind == models.TextAnswerProblem { + // TODO: Introduce status `failed` it is actually usefull + if s.Status != status.Completed || s.Verdict == verdict.IE { return c.JSON(http.StatusOK, response.Submission{ - ID: s.ID, - ProblemID: s.ProblemID, - ProblemKind: s.ProblemKind, - Status: s.Status, - Verdict: s.Verdict, - Answer: s.Answer, - CreatedAt: s.CreatedAt, + ID: s.ID, + ProblemID: s.ProblemID, + Status: s.Status, + Verdict: s.Verdict, + Code: s.Code, + Language: s.Language, + CreatedAt: s.CreatedAt, }) + } - ttc, err := h.repo.Submission.CountTestsForProblem(ctx, s.ProblemID) + // TODO: create TR in a transaction with setting completed status + tr, err := h.repo.Submission.GetTestingReport(ctx, s.ID) + // NOTE: decide either create in API initial testing report or not if err != nil { - log.Error("can't get total tests count", sl.Err(err)) + log.Error("can't get testing report", sl.Err(err)) return err } - // no need to provide testing report yet (no testing report) - switch s.Status { - case status.Pending, status.Running: - return c.JSON(http.StatusOK, response.Submission{ - ID: s.ID, - ProblemID: s.ProblemID, - ProblemKind: s.ProblemKind, - Status: s.Status, - Verdict: s.Verdict, - Code: s.Code, - Language: s.Language, - CreatedAt: s.CreatedAt, - }) - } - - failedTest, err := h.repo.Submission.GetFailedTest(ctx, s.ID) - // TODO: check if submission.Passed == submission.Total - if errors.Is(err, pgx.ErrNoRows) { + if tr.FirstFailedTestID == nil { return c.JSON(http.StatusOK, response.Submission{ - ID: s.ID, - ProblemID: s.ProblemID, - ProblemKind: s.ProblemKind, - Status: s.Status, - Verdict: s.Verdict, - Code: s.Code, - Language: s.Language, + ID: s.ID, + ProblemID: s.ProblemID, + Status: s.Status, + Verdict: s.Verdict, + Code: s.Code, + Language: s.Language, TestingReport: &response.TestingReport{ - Passed: int(s.PassedTestsCount), - Total: int(ttc), - Stderr: s.Stderr, + ID: tr.ID, + PassedTestsCount: tr.PassedTestsCount, + TotalTestsCount: tr.TotalTestsCount, + Stderr: tr.Stderr, + CreatedAt: tr.CreatedAt, }, CreatedAt: s.CreatedAt, }) } + + ftc, err := h.repo.Problem.GetTestCaseByID(ctx, *tr.FirstFailedTestID) if err != nil { - log.Error("can't get submissions", sl.Err(err)) + log.Error("can't get test case", sl.Err(err)) return err } return c.JSON(http.StatusOK, response.Submission{ - ID: s.ID, - ProblemID: s.ProblemID, - ProblemKind: s.ProblemKind, - Status: s.Status, - Verdict: s.Verdict, - Code: s.Code, - Language: s.Language, + ID: s.ID, + ProblemID: s.ProblemID, + Status: s.Status, + Verdict: s.Verdict, + Code: s.Code, + Language: s.Language, TestingReport: &response.TestingReport{ - Passed: int(s.PassedTestsCount), - Total: int(ttc), - Stderr: s.Stderr, - FailedTest: &response.FailedTest{ - Input: failedTest.Input, - ExpectedOutput: failedTest.ExpectedOutput, - ActualOutput: failedTest.ActualOutput, + ID: tr.ID, + PassedTestsCount: tr.PassedTestsCount, + TotalTestsCount: tr.TotalTestsCount, + FailedTest: &response.Test{ + Input: ftc.Input, + ExpectedOutput: ftc.Output, + ActualOutput: *tr.FirstFailedTestOutput, }, + Stderr: tr.Stderr, + CreatedAt: tr.CreatedAt, }, CreatedAt: s.CreatedAt, }) @@ -277,12 +239,11 @@ func (h *Handler) GetSubmissions(c echo.Context) error { items := make([]response.Submission, n, n) for i, submission := range submissions { items[i] = response.Submission{ - ID: submission.ID, - ProblemID: submission.ProblemID, - ProblemKind: submission.ProblemKind, - Status: submission.Status, - Verdict: submission.Verdict, - CreatedAt: submission.CreatedAt, + ID: submission.ID, + ProblemID: submission.ProblemID, + Status: submission.Status, + Verdict: submission.Verdict, + CreatedAt: submission.CreatedAt, } } diff --git a/internal/storage/models/models.go b/internal/storage/models/models.go index afd0383..a880f35 100644 --- a/internal/storage/models/models.go +++ b/internal/storage/models/models.go @@ -63,6 +63,7 @@ type Problem struct { type TestCase struct { ID int32 `db:"id"` ProblemID int32 `db:"problem_id"` + Ordinal int32 `db:"ordinal"` Input string `db:"input"` Output string `db:"output"` IsExample bool `db:"is_example"` @@ -82,18 +83,25 @@ type Entry struct { } type Submission struct { - ID int32 `db:"id"` - EntryID int32 `db:"entry_id"` - ProblemID int32 `db:"problem_id"` - ProblemKind string `db:"problem_kind"` - Status string `db:"status"` - Verdict string `db:"verdict"` - Answer string `db:"answer"` - Code string `db:"code"` - Language string `db:"language"` - PassedTestsCount int32 `db:"passed_tests_count"` - Stderr string `db:"stderr"` - CreatedAt time.Time `db:"created_at"` + ID int32 `db:"id"` + EntryID int32 `db:"entry_id"` + ProblemID int32 `db:"problem_id"` + Status string `db:"status"` + Verdict string `db:"verdict"` + Code string `db:"code"` + Language string `db:"language"` + CreatedAt time.Time `db:"created_at"` +} + +type TestingReport struct { + ID int32 `db:"id"` + SubmissionID int32 `db:"submission_id"` + PassedTestsCount int32 `db:"passed_tests_count"` + TotalTestsCount int32 `db:"total_tests_count"` + FirstFailedTestID *int32 `db:"first_failed_test_id"` + FirstFailedTestOutput *string `db:"first_failed_test_output"` + Stderr string `db:"stderr"` + CreatedAt time.Time `db:"created_at"` } type LeaderboardEntry struct { diff --git a/internal/storage/repository/postgres/problem/problem.go b/internal/storage/repository/postgres/problem/problem.go index 6031a1a..5ab88df 100644 --- a/internal/storage/repository/postgres/problem/problem.go +++ b/internal/storage/repository/postgres/problem/problem.go @@ -37,11 +37,11 @@ func (p *Postgres) CreateWithTCs(ctx context.Context, kind string, writerID int3 if len(tcs) > 0 { batch := &pgx.Batch{} - for _, tc := range tcs { + for i, tc := range tcs { batch.Queue(` - INSERT INTO test_cases (problem_id, input, output, is_example) - VALUES ($1, $2, $3, $4) - `, problemID, tc.Input, tc.Output, tc.IsExample) + INSERT INTO test_cases (problem_id, ordinal, input, output, is_example) + VALUES ($1, $2, $3, $4, $5) + `, problemID, i+1, tc.Input, tc.Output, tc.IsExample) } br := tx.SendBatch(ctx, batch) @@ -114,8 +114,9 @@ func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem return problem, err } -func (p *Postgres) GetTestCases(ctx context.Context, problemID int32) ([]models.TestCase, error) { - query := `SELECT id, problem_id, input, output, is_example FROM test_cases WHERE problem_id = $1` +func (p *Postgres) GetExampleCases(ctx context.Context, problemID int32) ([]models.TestCase, error) { + query := `SELECT id, problem_id, ordinal, input, output, is_example FROM test_cases WHERE problem_id = $1 AND is_example = true` + rows, err := p.pool.Query(ctx, query, problemID) if err != nil { return nil, err @@ -125,34 +126,33 @@ func (p *Postgres) GetTestCases(ctx context.Context, problemID int32) ([]models. tcs := make([]models.TestCase, 0) for rows.Next() { var tc models.TestCase - if err := rows.Scan(&tc.ID, &tc.ProblemID, &tc.Input, &tc.Output, &tc.IsExample); err != nil { + if err := rows.Scan(&tc.ID, &tc.ProblemID, &tc.Ordinal, &tc.Input, &tc.Output, &tc.IsExample); err != nil { return nil, err } tcs = append(tcs, tc) } - return tcs, nil + return tcs, rows.Err() } -func (p *Postgres) GetExampleCases(ctx context.Context, problemID int32) ([]models.TestCase, error) { - query := `SELECT * FROM test_cases WHERE problem_id = $1 AND is_example = true` +func (p *Postgres) GetTestCaseByID(ctx context.Context, testCaseID int32) (models.TestCase, error) { + query := `SELECT id, problem_id, ordinal, input, output, is_example FROM test_cases WHERE id = $1` + + var tc models.TestCase + err := p.pool.QueryRow(ctx, query, testCaseID).Scan( + &tc.ID, + &tc.ProblemID, + &tc.Ordinal, + &tc.Input, + &tc.Output, + &tc.IsExample, + ) - rows, err := p.pool.Query(ctx, query, problemID) if err != nil { - return nil, err + return models.TestCase{}, fmt.Errorf("failed to get test case by ID: %w", err) } - defer rows.Close() - tcs := make([]models.TestCase, 0) - for rows.Next() { - var tc models.TestCase - if err := rows.Scan(&tc.ID, &tc.ProblemID, &tc.Input, &tc.Output, &tc.IsExample); err != nil { - return nil, err - } - tcs = append(tcs, tc) - } - - return tcs, rows.Err() + return tc, nil } func (p *Postgres) GetAll(ctx context.Context) ([]models.Problem, error) { diff --git a/internal/storage/repository/postgres/submission/submission.go b/internal/storage/repository/postgres/submission/submission.go index 802eb11..6a9f9df 100644 --- a/internal/storage/repository/postgres/submission/submission.go +++ b/internal/storage/repository/postgres/submission/submission.go @@ -5,11 +5,8 @@ import ( "database/sql" "fmt" - "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/voidcontests/api/internal/storage/models" - "github.com/voidcontests/api/internal/storage/models/status" - "github.com/voidcontests/api/internal/storage/models/verdict" ) const ( @@ -24,66 +21,26 @@ func New(pool *pgxpool.Pool) *Postgres { return &Postgres{pool} } -func (p *Postgres) CreateWithTextAnswer(ctx context.Context, entryID int32, problemID int32, verdict string, answer string) (models.Submission, error) { - return p.create(ctx, entryID, problemID, status.Completed, verdict, answer, "", "", 0, "") -} - -func (p *Postgres) CreateWithSolution(ctx context.Context, entryID int32, problemID int32, code string, language string) (models.Submission, error) { - return p.create(ctx, entryID, problemID, status.Pending, verdict.NJ, "", code, language, 0, "") -} - -func (p *Postgres) create(ctx context.Context, entryID int32, problemID int32, status string, verdict string, answer string, code string, language string, passedTestsCount int32, stderr string) (models.Submission, error) { - query := `INSERT INTO submissions - (entry_id, problem_id, status, verdict, answer, code, language, passed_tests_count, stderr) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING id, entry_id, problem_id, (SELECT kind FROM problems WHERE id = $2) AS problem_kind, - status, verdict, answer, code, language, passed_tests_count, stderr, created_at` +func (p *Postgres) Create(ctx context.Context, entryID int32, problemID int32, code string, language string) (models.Submission, error) { + query := `INSERT INTO submissions (entry_id, problem_id, code, language) + VALUES ($1, $2, $3, $4) + RETURNING id, entry_id, problem_id, status, verdict, code, language, created_at` var submission models.Submission - err := p.pool.QueryRow(ctx, query, entryID, problemID, status, verdict, answer, code, language, passedTestsCount, stderr).Scan( + err := p.pool.QueryRow(ctx, query, entryID, problemID, code, language).Scan( &submission.ID, &submission.EntryID, &submission.ProblemID, - &submission.ProblemKind, &submission.Status, &submission.Verdict, - &submission.Answer, &submission.Code, &submission.Language, - &submission.PassedTestsCount, - &submission.Stderr, &submission.CreatedAt, ) return submission, err } -func (p *Postgres) UpdateVerdictStatus(ctx context.Context, id int32, verdict string, status string) error { - query := `UPDATE submissions SET verdict = $1, status = $2 WHERE id = $3` - _, err := p.pool.Exec(ctx, query, verdict, status, id) - return err -} - -func (p *Postgres) CountTestsForProblem(ctx context.Context, problemID int32) (int32, error) { - var count int32 - err := p.pool.QueryRow(ctx, `SELECT COUNT(*) FROM test_cases WHERE problem_id = $1`, problemID).Scan(&count) - return count, err -} - -func (p *Postgres) GetFailedTest(ctx context.Context, submissionID int32) (models.FailedTest, error) { - query := `SELECT id, submission_id, input, expected_output, actual_output, created_at FROM failed_tests WHERE submission_id = $1` - var ft models.FailedTest - err := p.pool.QueryRow(ctx, query, submissionID).Scan( - &ft.ID, - &ft.SubmissionID, - &ft.Input, - &ft.ExpectedOutput, - &ft.ActualOutput, - &ft.CreatedAt, - ) - return ft, err -} - func (p *Postgres) GetProblemStatus(ctx context.Context, entryID int32, problemID int32) (string, error) { query := ` SELECT @@ -152,30 +109,19 @@ func (p *Postgres) GetProblemStatuses(ctx context.Context, entryID int32) (map[i return statuses, nil } -func (p *Postgres) GetByID(ctx context.Context, userID, submissionID int32) (models.Submission, error) { - query := ` - SELECT s.id, s.entry_id, s.problem_id, p.kind AS problem_kind, s.status, s.verdict, - s.answer, s.code, s.language, s.passed_tests_count, s.stderr, s.created_at - FROM submissions s - JOIN problems p ON p.id = s.problem_id - JOIN entries e ON s.entry_id = e.id - JOIN users u ON e.user_id = u.id - WHERE s.id = $1 AND u.id = $2 - ` +func (p *Postgres) GetByID(ctx context.Context, submissionID int32) (models.Submission, error) { + query := `SELECT s.id, s.entry_id, s.problem_id, s.status, s.verdict, s.code, s.language, s.created_at + FROM submissions s WHERE s.id = $1` var s models.Submission - err := p.pool.QueryRow(ctx, query, submissionID, userID).Scan( + err := p.pool.QueryRow(ctx, query, submissionID).Scan( &s.ID, &s.EntryID, &s.ProblemID, - &s.ProblemKind, &s.Status, &s.Verdict, - &s.Answer, &s.Code, &s.Language, - &s.PassedTestsCount, - &s.Stderr, &s.CreatedAt, ) @@ -187,33 +133,21 @@ func (p *Postgres) ListByProblem(ctx context.Context, entryID int32, charcode st limit = defaultLimit } - batch := &pgx.Batch{} - batch.Queue(` - SELECT s.id, s.entry_id, s.problem_id, p.kind AS problem_kind, s.verdict, - s.answer, s.code, s.language, s.passed_tests_count, s.stderr, s.created_at + query := ` + SELECT s.id, s.entry_id, s.problem_id, s.status, s.verdict, s.code, s.language, s.created_at, COUNT(*) OVER() as total_count FROM submissions s JOIN problems p ON p.id = s.problem_id JOIN entries e ON s.entry_id = e.id JOIN contest_problems cp ON cp.contest_id = e.contest_id AND cp.problem_id = s.problem_id WHERE s.entry_id = $1 AND cp.charcode = $2 - ORDER BY s.created_at DESC LIMIT $3 OFFSET $4 - `, entryID, charcode, limit, offset) + ORDER BY s.created_at DESC + LIMIT $3 OFFSET $4` - batch.Queue(` - SELECT COUNT(*) - FROM submissions s - JOIN entries e ON s.entry_id = e.id - JOIN contest_problems cp ON cp.contest_id = e.contest_id AND cp.problem_id = s.problem_id - WHERE s.entry_id = $1 AND cp.charcode = $2 - `, entryID, charcode) - - br := p.pool.SendBatch(ctx, batch) - defer br.Close() - - rows, err := br.Query() + rows, err := p.pool.Query(ctx, query, entryID, charcode, limit, offset) if err != nil { return nil, 0, fmt.Errorf("query rows failed: %w", err) } + defer rows.Close() items = make([]models.Submission, 0) for rows.Next() { @@ -222,29 +156,44 @@ func (p *Postgres) ListByProblem(ctx context.Context, entryID int32, charcode st &s.ID, &s.EntryID, &s.ProblemID, - &s.ProblemKind, + &s.Status, &s.Verdict, - &s.Answer, &s.Code, &s.Language, - &s.PassedTestsCount, - &s.Stderr, &s.CreatedAt, + &total, ); err != nil { - rows.Close() return nil, 0, fmt.Errorf("row scan failed: %w", err) } items = append(items, s) } if err := rows.Err(); err != nil { - rows.Close() return nil, 0, fmt.Errorf("row iteration error: %w", err) } - rows.Close() - if err := br.QueryRow().Scan(&total); err != nil { - return nil, 0, fmt.Errorf("count query failed: %w", err) + return items, total, nil +} + +func (p *Postgres) GetTestingReport(ctx context.Context, submissionID int32) (models.TestingReport, error) { + query := `SELECT id, submission_id, passed_tests_count, total_tests_count, + first_failed_test_id, first_failed_test_output, stderr, created_at + FROM testing_reports WHERE submission_id = $1` + + var report models.TestingReport + err := p.pool.QueryRow(ctx, query, submissionID).Scan( + &report.ID, + &report.SubmissionID, + &report.PassedTestsCount, + &report.TotalTestsCount, + &report.FirstFailedTestID, + &report.FirstFailedTestOutput, + &report.Stderr, + &report.CreatedAt, + ) + + if err != nil { + return models.TestingReport{}, err } - return items, total, nil + return report, nil } diff --git a/internal/storage/repository/repository.go b/internal/storage/repository/repository.go index fc7c270..671360d 100644 --- a/internal/storage/repository/repository.go +++ b/internal/storage/repository/repository.go @@ -58,8 +58,8 @@ type Problem interface { Create(ctx context.Context, kind string, writerID int32, title, statement, difficulty, answer string, timeLimitMS int32) (int32, error) Get(ctx context.Context, contestID int32, charcode string) (models.Problem, error) GetByID(ctx context.Context, problemID int32) (models.Problem, error) - GetTestCases(ctx context.Context, problemID int32) ([]models.TestCase, error) GetExampleCases(ctx context.Context, problemID int32) ([]models.TestCase, error) + GetTestCaseByID(ctx context.Context, testCaseID int32) (models.TestCase, error) GetAll(ctx context.Context) ([]models.Problem, error) GetWithWriterID(ctx context.Context, writerID int32, limit, offset int) (problems []models.Problem, total int, err error) IsTitleOccupied(ctx context.Context, title string) (bool, error) @@ -71,13 +71,10 @@ type Entry interface { } type Submission interface { - CreateWithSolution(ctx context.Context, entryID int32, problemID int32, code string, language string) (models.Submission, error) - CreateWithTextAnswer(ctx context.Context, entryID int32, problemID int32, verdict string, answer string) (models.Submission, error) - CountTestsForProblem(ctx context.Context, problemID int32) (int32, error) - UpdateVerdictStatus(ctx context.Context, id int32, verdict string, status string) error - GetFailedTest(ctx context.Context, submissionID int32) (models.FailedTest, error) + Create(ctx context.Context, entryID int32, problemID int32, code string, language string) (models.Submission, error) GetProblemStatus(ctx context.Context, entryID int32, problemID int32) (string, error) GetProblemStatuses(ctx context.Context, entryID int32) (map[int32]string, error) - GetByID(ctx context.Context, userID, submissionID int32) (models.Submission, error) + GetByID(ctx context.Context, submissionID int32) (models.Submission, error) ListByProblem(ctx context.Context, entryID int32, charcode string, limit int, offset int) (items []models.Submission, total int, err error) + GetTestingReport(ctx context.Context, submissionID int32) (models.TestingReport, error) } From dc550815a8eca4f84f70c62875dec30c2b5d0822 Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 23 Oct 2025 20:31:35 +0400 Subject: [PATCH 12/24] chore: Introduce `failed` status --- internal/app/handler/submission.go | 6 +----- internal/storage/models/status/status.go | 7 ++++--- internal/storage/models/verdict/verdict.go | 12 ++++++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/app/handler/submission.go b/internal/app/handler/submission.go index 4a594aa..bc20665 100644 --- a/internal/app/handler/submission.go +++ b/internal/app/handler/submission.go @@ -13,7 +13,6 @@ import ( "github.com/voidcontests/api/internal/app/handler/dto/response" "github.com/voidcontests/api/internal/lib/logger/sl" "github.com/voidcontests/api/internal/storage/models/status" - "github.com/voidcontests/api/internal/storage/models/verdict" "github.com/voidcontests/api/pkg/requestid" "github.com/voidcontests/api/pkg/validate" ) @@ -123,8 +122,7 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { return err } - // TODO: Introduce status `failed` it is actually usefull - if s.Status != status.Completed || s.Verdict == verdict.IE { + if s.Status != status.Success { return c.JSON(http.StatusOK, response.Submission{ ID: s.ID, ProblemID: s.ProblemID, @@ -137,9 +135,7 @@ func (h *Handler) GetSubmissionByID(c echo.Context) error { } - // TODO: create TR in a transaction with setting completed status tr, err := h.repo.Submission.GetTestingReport(ctx, s.ID) - // NOTE: decide either create in API initial testing report or not if err != nil { log.Error("can't get testing report", sl.Err(err)) return err diff --git a/internal/storage/models/status/status.go b/internal/storage/models/status/status.go index 073615d..ae62e11 100644 --- a/internal/storage/models/status/status.go +++ b/internal/storage/models/status/status.go @@ -1,7 +1,8 @@ package status const ( - Pending = "pending" - Running = "running" - Completed = "completed" + Pending = "pending" + Judging = "judging" + Success = "success" + Failed = "failed" ) diff --git a/internal/storage/models/verdict/verdict.go b/internal/storage/models/verdict/verdict.go index d6a4c63..6802b35 100644 --- a/internal/storage/models/verdict/verdict.go +++ b/internal/storage/models/verdict/verdict.go @@ -1,8 +1,12 @@ package verdict const ( - OK = "ok" - IE = "internal_error" - WA = "wrong_answer" - NJ = "not_judged" + NJ = "not_judged" + OK = "ok" + RE = "runtime_error" + CE = "compilation_error" + IE = "internal_error" + WA = "wrong_answer" + PE = "presentation_error" + TLE = "time_limit_exceeded" ) From 048121ccd845e95ac50204111fb937bd1086df95 Mon Sep 17 00:00:00 2001 From: jus1d Date: Fri, 24 Oct 2025 16:10:45 +0400 Subject: [PATCH 13/24] chore: Remove text answer problem support --- internal/app/handler/contest.go | 5 ++- internal/app/handler/dto/request/request.go | 2 -- internal/app/handler/dto/response/response.go | 3 -- internal/app/handler/problem.go | 28 ++++++--------- internal/storage/models/models.go | 7 ---- .../repository/postgres/contest/contest.go | 2 +- .../repository/postgres/problem/problem.go | 36 +++++++++---------- internal/storage/repository/repository.go | 4 +-- 8 files changed, 33 insertions(+), 54 deletions(-) diff --git a/internal/app/handler/contest.go b/internal/app/handler/contest.go index 7be6625..540a063 100644 --- a/internal/app/handler/contest.go +++ b/internal/app/handler/contest.go @@ -106,9 +106,8 @@ func (h *Handler) GetContestByID(c echo.Context) error { for i := range n { cdetailed.Problems[i] = response.ContestProblemListItem{ - ID: problems[i].ID, - Charcode: problems[i].Charcode, - ContestID: contest.ID, + ID: problems[i].ID, + Charcode: problems[i].Charcode, Writer: response.User{ ID: problems[i].WriterID, Username: problems[i].WriterUsername, diff --git a/internal/app/handler/dto/request/request.go b/internal/app/handler/dto/request/request.go index 97b259e..b887042 100644 --- a/internal/app/handler/dto/request/request.go +++ b/internal/app/handler/dto/request/request.go @@ -29,12 +29,10 @@ type CreateContestRequest struct { type CreateProblemRequest struct { Title string `json:"title" required:"true"` - Kind string `json:"kind" required:"true"` Statement string `json:"statement" required:"true"` Difficulty string `json:"difficulty" required:"true"` TimeLimitMS int `json:"time_limit_ms"` TestCases []models.TestCaseDTO `json:"test_cases"` - Answer string `json:"answer"` } type CreateSubmissionRequest struct { diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index 4e8f8ac..65b9846 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -101,7 +101,6 @@ type ContestProblemDetailed struct { Charcode string `json:"charcode"` ContestID int32 `json:"contest_id"` Writer User `json:"writer"` - Kind string `json:"kind"` Title string `json:"title"` Statement string `json:"statement"` Examples []TC `json:"examples,omitempty"` @@ -114,7 +113,6 @@ type ContestProblemDetailed struct { type ContestProblemListItem struct { ID int32 `json:"id"` Charcode string `json:"charcode"` - ContestID int32 `json:"contest_id"` Writer User `json:"writer"` Title string `json:"title"` Difficulty string `json:"difficulty"` @@ -125,7 +123,6 @@ type ContestProblemListItem struct { type ProblemDetailed struct { ID int32 `json:"id"` Writer User `json:"writer"` - Kind string `json:"kind"` Title string `json:"title"` Statement string `json:"statement"` Examples []TC `json:"examples,omitempty"` diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index 6921266..4404072 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -45,24 +45,18 @@ func (h *Handler) CreateProblem(c echo.Context) error { } } - var problemID int32 - if body.Kind == models.TextAnswerProblem { - problemID, err = h.repo.Problem.Create(ctx, models.TextAnswerProblem, claims.UserID, body.Title, body.Statement, body.Difficulty, body.Answer, 0) - } else if body.Kind == models.CodingProblem { - examplesCount := 0 - for i := range body.TestCases { - if body.TestCases[i].IsExample { - examplesCount++ - } - - if examplesCount > 3 && body.TestCases[i].IsExample { - body.TestCases[i].IsExample = false - } + // Forbid to create more examples than 3 + examplesCount := 0 + for i := range body.TestCases { + if body.TestCases[i].IsExample { + examplesCount++ + } + + if examplesCount > 3 && body.TestCases[i].IsExample { + body.TestCases[i].IsExample = false } - problemID, err = h.repo.Problem.CreateWithTCs(ctx, models.CodingProblem, claims.UserID, body.Title, body.Statement, body.Difficulty, "", body.TimeLimitMS, body.TestCases) - } else { - return Error(http.StatusBadRequest, "unknown problem kind") } + problemID, err := h.repo.Problem.CreateWithTCs(ctx, claims.UserID, body.Title, body.Statement, body.Difficulty, body.TimeLimitMS, body.TestCases) if err != nil { return fmt.Errorf("%s: can't create problem: %v", op, err) @@ -177,7 +171,6 @@ func (h *Handler) GetContestProblem(c echo.Context) error { ID: p.ID, Charcode: p.Charcode, ContestID: int32(contestID), - Kind: p.Kind, Title: p.Title, Statement: p.Statement, Examples: examples, @@ -233,7 +226,6 @@ func (h *Handler) GetProblemByID(c echo.Context) error { pdetailed := response.ProblemDetailed{ ID: problem.ID, - Kind: problem.Kind, Title: problem.Title, Statement: problem.Statement, Examples: examples, diff --git a/internal/storage/models/models.go b/internal/storage/models/models.go index a880f35..151dc17 100644 --- a/internal/storage/models/models.go +++ b/internal/storage/models/models.go @@ -9,11 +9,6 @@ const ( RoleBanned = "banned" ) -const ( - TextAnswerProblem = "text_answer_problem" - CodingProblem = "coding_problem" -) - type User struct { ID int32 `db:"id"` Username string `db:"username"` @@ -49,13 +44,11 @@ type Contest struct { type Problem struct { ID int32 `db:"id"` Charcode string `db:"charcode"` - Kind string `db:"kind"` WriterID int32 `db:"writer_id"` WriterUsername string `db:"writer_username"` Title string `db:"title"` Statement string `db:"statement"` Difficulty string `db:"difficulty"` - Answer string `db:"answer"` TimeLimitMS int32 `db:"time_limit_ms"` CreatedAt time.Time `db:"created_at"` } diff --git a/internal/storage/repository/postgres/contest/contest.go b/internal/storage/repository/postgres/contest/contest.go index a11cca7..944c5b7 100644 --- a/internal/storage/repository/postgres/contest/contest.go +++ b/internal/storage/repository/postgres/contest/contest.go @@ -108,7 +108,7 @@ func (p *Postgres) GetProblemset(ctx context.Context, contestID int32) ([]models var problems []models.Problem for rows.Next() { var problem models.Problem - if err := rows.Scan(&problem.Charcode, &problem.ID, &problem.Kind, &problem.WriterID, &problem.Title, &problem.Statement, &problem.Difficulty, &problem.Answer, &problem.TimeLimitMS, &problem.CreatedAt, &problem.WriterUsername); err != nil { + if err := rows.Scan(&problem.Charcode, &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, &problem.Difficulty, &problem.TimeLimitMS, &problem.CreatedAt, &problem.WriterUsername); err != nil { return nil, err } problems = append(problems, problem) diff --git a/internal/storage/repository/postgres/problem/problem.go b/internal/storage/repository/postgres/problem/problem.go index 5ab88df..c61dd34 100644 --- a/internal/storage/repository/postgres/problem/problem.go +++ b/internal/storage/repository/postgres/problem/problem.go @@ -18,7 +18,7 @@ func New(pool *pgxpool.Pool) *Postgres { return &Postgres{pool} } -func (p *Postgres) CreateWithTCs(ctx context.Context, kind string, writerID int32, title, statement, difficulty, answer string, timeLimitMS int, tcs []models.TestCaseDTO) (int32, error) { +func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, statement, difficulty string, timeLimitMS int, tcs []models.TestCaseDTO) (int32, error) { tx, err := p.pool.BeginTx(ctx, pgx.TxOptions{}) if err != nil { return 0, fmt.Errorf("tx begin failed: %w", err) @@ -27,10 +27,10 @@ func (p *Postgres) CreateWithTCs(ctx context.Context, kind string, writerID int3 var problemID int32 err = tx.QueryRow(ctx, ` - INSERT INTO problems (kind, writer_id, title, statement, difficulty, answer, time_limit_ms) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms) + VALUES ($1, $2, $3, $4, $5) RETURNING id - `, kind, writerID, title, statement, difficulty, answer, timeLimitMS).Scan(&problemID) + `, writerID, title, statement, difficulty, timeLimitMS).Scan(&problemID) if err != nil { return 0, fmt.Errorf("insert problem failed: %w", err) } @@ -65,12 +65,12 @@ func (p *Postgres) CreateWithTCs(ctx context.Context, kind string, writerID int3 return problemID, nil } -func (p *Postgres) Create(ctx context.Context, kind string, writerID int32, title, statement, difficulty, answer string, timeLimitMS int32) (int32, error) { +func (p *Postgres) Create(ctx context.Context, writerID int32, title, statement, difficulty, timeLimitMS int32) (int32, error) { var id int32 - query := `INSERT INTO problems (kind, writer_id, title, statement, difficulty, answer, time_limit_ms) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id` + query := `INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms) + VALUES ($1, $2, $3, $4, $5) RETURNING id` - err := p.pool.QueryRow(ctx, query, kind, writerID, title, statement, difficulty, answer, timeLimitMS).Scan(&id) + err := p.pool.QueryRow(ctx, query, writerID, title, statement, difficulty, timeLimitMS).Scan(&id) return id, err } @@ -85,8 +85,8 @@ func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (m var problem models.Problem err := row.Scan( - &problem.ID, &problem.Kind, &problem.WriterID, &problem.Title, &problem.Statement, - &problem.Difficulty, &problem.Answer, &problem.TimeLimitMS, &problem.CreatedAt, + &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, + &problem.Difficulty, &problem.TimeLimitMS, &problem.CreatedAt, &problem.Charcode, &problem.WriterUsername, ) @@ -95,8 +95,8 @@ func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (m func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem, error) { query := `SELECT - p.id, p.kind, p.writer_id, p.title, p.statement, - p.difficulty, p.answer, p.time_limit_ms, p.created_at, + p.id, p.writer_id, p.title, p.statement, + p.difficulty, p.time_limit_ms, p.created_at, u.username AS writer_username FROM problems p JOIN users u ON u.id = p.writer_id @@ -106,8 +106,8 @@ func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem var problem models.Problem err := row.Scan( - &problem.ID, &problem.Kind, &problem.WriterID, &problem.Title, &problem.Statement, - &problem.Difficulty, &problem.Answer, &problem.TimeLimitMS, &problem.CreatedAt, + &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, + &problem.Difficulty, &problem.TimeLimitMS, &problem.CreatedAt, &problem.WriterUsername, ) @@ -168,8 +168,8 @@ func (p *Postgres) GetAll(ctx context.Context) ([]models.Problem, error) { for rows.Next() { var p models.Problem if err := rows.Scan( - &p.ID, &p.Kind, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, - &p.Answer, &p.TimeLimitMS, &p.CreatedAt, &p.WriterUsername, + &p.ID, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, + &p.TimeLimitMS, &p.CreatedAt, &p.WriterUsername, ); err != nil { return nil, err } @@ -207,8 +207,8 @@ func (p *Postgres) GetWithWriterID(ctx context.Context, writerID int32, limit, o for rows.Next() { var p models.Problem if err := rows.Scan( - &p.ID, &p.Kind, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, - &p.Answer, &p.TimeLimitMS, &p.CreatedAt, &p.WriterUsername, + &p.ID, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, + &p.TimeLimitMS, &p.CreatedAt, &p.WriterUsername, ); err != nil { rows.Close() br.Close() diff --git a/internal/storage/repository/repository.go b/internal/storage/repository/repository.go index 671360d..9142ee4 100644 --- a/internal/storage/repository/repository.go +++ b/internal/storage/repository/repository.go @@ -54,8 +54,8 @@ type Contest interface { } type Problem interface { - CreateWithTCs(ctx context.Context, kind string, writerID int32, title, statement, difficulty, answer string, timeLimitMS int, tcs []models.TestCaseDTO) (int32, error) - Create(ctx context.Context, kind string, writerID int32, title, statement, difficulty, answer string, timeLimitMS int32) (int32, error) + CreateWithTCs(ctx context.Context, writerID int32, title string, statement string, difficulty string, timeLimitMS int, tcs []models.TestCaseDTO) (int32, error) + Create(ctx context.Context, writerID int32, title, statement, difficulty, timeLimitMS int32) (int32, error) Get(ctx context.Context, contestID int32, charcode string) (models.Problem, error) GetByID(ctx context.Context, problemID int32) (models.Problem, error) GetExampleCases(ctx context.Context, problemID int32) ([]models.TestCase, error) From 4161deeffa615b3c79a130214f0cd64712aef468 Mon Sep 17 00:00:00 2001 From: jus1d Date: Fri, 24 Oct 2025 16:33:09 +0400 Subject: [PATCH 14/24] chore: Remove log of /api/healthcheck --- Dockerfile | 4 ++-- internal/version/version.go | 8 ++++---- pkg/requestlog/requestlog.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ea41f8..45227bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,8 @@ RUN go mod download COPY . . RUN go build -a -ldflags "-w -s \ - -X github.com/voidcontests/api/internal/version.GIT_COMMIT=$(git rev-parse --short HEAD) \ - -X github.com/voidcontests/api/internal/version.GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)" \ + -X github.com/voidcontests/api/internal/version.Commit=$(git rev-parse --short HEAD) \ + -X github.com/voidcontests/api/internal/version.Branch=$(git rev-parse --abbrev-ref HEAD)" \ -o build/api ./cmd/api # lightweight docker container with binaries only diff --git a/internal/version/version.go b/internal/version/version.go index b2ce0d9..ad1dbce 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,8 +2,8 @@ package version import "log/slog" -var GIT_COMMIT string -var GIT_BRANCH string +var Commit string +var Branch string -var CommitAttr = slog.Attr{Key: "commit", Value: slog.StringValue(GIT_COMMIT)} -var BranchAttr = slog.Attr{Key: "branch", Value: slog.StringValue(GIT_BRANCH)} +var CommitAttr = slog.Attr{Key: "commit", Value: slog.StringValue(Commit)} +var BranchAttr = slog.Attr{Key: "branch", Value: slog.StringValue(Branch)} diff --git a/pkg/requestlog/requestlog.go b/pkg/requestlog/requestlog.go index d20f93e..ffdadfb 100644 --- a/pkg/requestlog/requestlog.go +++ b/pkg/requestlog/requestlog.go @@ -12,7 +12,7 @@ import ( func Completed(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - if c.Request().Method == "OPTIONS" { + if c.Request().Method == "OPTIONS" || c.Path() == "/api/healthcheck" { return next(c) } From bdf20adc2be69c53e2402b6fbbf9b17da1c58ec1 Mon Sep 17 00:00:00 2001 From: jus1d Date: Fri, 24 Oct 2025 16:35:09 +0400 Subject: [PATCH 15/24] chore: skip only 200 healthchecks --- pkg/requestlog/requestlog.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/requestlog/requestlog.go b/pkg/requestlog/requestlog.go index ffdadfb..93226a8 100644 --- a/pkg/requestlog/requestlog.go +++ b/pkg/requestlog/requestlog.go @@ -12,12 +12,11 @@ import ( func Completed(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - if c.Request().Method == "OPTIONS" || c.Path() == "/api/healthcheck" { + if c.Request().Method == "OPTIONS" { return next(c) } start := time.Now() - err := next(c) status := c.Response().Status @@ -29,6 +28,10 @@ func Completed(next echo.HandlerFunc) echo.HandlerFunc { } } + if c.Path() == "/api/healthcheck" && status == 200 { + return err + } + slog.Info("request completed", slog.String("id", requestid.Get(c)), slog.String("method", c.Request().Method), From bc8efde312382a0ca8d5a54982810e86a251ec9c Mon Sep 17 00:00:00 2001 From: jus1d Date: Sun, 26 Oct 2025 03:23:48 +0400 Subject: [PATCH 16/24] chore: Add submission deadline --- internal/app/handler/contest.go | 3 ++ internal/app/handler/dto/response/response.go | 50 ++++++++++--------- internal/app/handler/problem.go | 28 +++++++---- internal/app/handler/submission.go | 28 +++++++---- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/internal/app/handler/contest.go b/internal/app/handler/contest.go index 540a063..ee8bdab 100644 --- a/internal/app/handler/contest.go +++ b/internal/app/handler/contest.go @@ -134,6 +134,9 @@ func (h *Handler) GetContestByID(c echo.Context) error { cdetailed.IsParticipant = true + _, deadline := AllowSubmitAt(contest, entry) + cdetailed.SubmissionDeadline = &deadline + statuses, err := h.repo.Submission.GetProblemStatuses(ctx, entry.ID) if err != nil { return fmt.Errorf("%s: can't get submissions: %v", op, err) diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index 65b9846..dd29934 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -43,19 +43,20 @@ type User struct { } type ContestDetailed struct { - ID int32 `json:"id"` - Creator User `json:"creator"` - Title string `json:"title"` - Description string `json:"description"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - DurationMins int32 `json:"duration_mins"` - MaxEntries int32 `json:"max_entries,omitempty"` - Participants int32 `json:"participants"` - AllowLateJoin bool `json:"allow_late_join"` - IsParticipant bool `json:"is_participant,omitempty"` - Problems []ContestProblemListItem `json:"problems"` - CreatedAt time.Time `json:"created_at"` + ID int32 `json:"id"` + Creator User `json:"creator"` + Title string `json:"title"` + Description string `json:"description"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + DurationMins int32 `json:"duration_mins"` + MaxEntries int32 `json:"max_entries,omitempty"` + Participants int32 `json:"participants"` + AllowLateJoin bool `json:"allow_late_join"` + IsParticipant bool `json:"is_participant,omitempty"` + SubmissionDeadline *time.Time `json:"submission_deadline,omitempty"` + Problems []ContestProblemListItem `json:"problems"` + CreatedAt time.Time `json:"created_at"` } type ContestListItem struct { @@ -97,17 +98,18 @@ type Test struct { } type ContestProblemDetailed struct { - ID int32 `json:"id"` - Charcode string `json:"charcode"` - ContestID int32 `json:"contest_id"` - Writer User `json:"writer"` - Title string `json:"title"` - Statement string `json:"statement"` - Examples []TC `json:"examples,omitempty"` - Difficulty string `json:"difficulty"` - Status string `json:"status,omitempty"` - TimeLimitMS int32 `json:"time_limit_ms"` - CreatedAt time.Time `json:"created_at"` + ID int32 `json:"id"` + Charcode string `json:"charcode"` + ContestID int32 `json:"contest_id"` + Writer User `json:"writer"` + Title string `json:"title"` + Statement string `json:"statement"` + Examples []TC `json:"examples,omitempty"` + Difficulty string `json:"difficulty"` + Status string `json:"status,omitempty"` + TimeLimitMS int32 `json:"time_limit_ms"` + SubmissionDeadline *time.Time `json:"submission_deadline,omitempty"` + CreatedAt time.Time `json:"created_at"` } type ContestProblemListItem struct { diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index 4404072..4137063 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -167,17 +167,25 @@ func (h *Handler) GetContestProblem(c echo.Context) error { return err } + contest, err := h.repo.Contest.GetByID(ctx, int32(contestID)) + if err != nil { + return err + } + + _, deadline := AllowSubmitAt(contest, entry) + pdetailed := response.ContestProblemDetailed{ - ID: p.ID, - Charcode: p.Charcode, - ContestID: int32(contestID), - Title: p.Title, - Statement: p.Statement, - Examples: examples, - Difficulty: p.Difficulty, - Status: status, - CreatedAt: p.CreatedAt, - TimeLimitMS: p.TimeLimitMS, + ID: p.ID, + Charcode: p.Charcode, + ContestID: int32(contestID), + Title: p.Title, + Statement: p.Statement, + Examples: examples, + Difficulty: p.Difficulty, + Status: status, + CreatedAt: p.CreatedAt, + TimeLimitMS: p.TimeLimitMS, + SubmissionDeadline: &deadline, Writer: response.User{ ID: p.WriterID, Username: p.WriterUsername, diff --git a/internal/app/handler/submission.go b/internal/app/handler/submission.go index bc20665..a8f560a 100644 --- a/internal/app/handler/submission.go +++ b/internal/app/handler/submission.go @@ -12,6 +12,7 @@ import ( "github.com/voidcontests/api/internal/app/handler/dto/request" "github.com/voidcontests/api/internal/app/handler/dto/response" "github.com/voidcontests/api/internal/lib/logger/sl" + "github.com/voidcontests/api/internal/storage/models" "github.com/voidcontests/api/internal/storage/models/status" "github.com/voidcontests/api/pkg/requestid" "github.com/voidcontests/api/pkg/validate" @@ -49,15 +50,6 @@ func (h *Handler) CreateSubmission(c echo.Context) error { return err } - if contest.StartTime.After(time.Now()) { - return Error(http.StatusForbidden, "contest is not started yet") - } - - // TODO: maybe allow to submit solutions after end time if `contest.keep_as_training` is enabled - if contest.EndTime.Before(time.Now()) { - return Error(http.StatusForbidden, "contest alreay ended") - } - entry, err := h.repo.Entry.Get(ctx, int32(contestID), claims.UserID) if errors.Is(err, pgx.ErrNoRows) { log.Debug("trying to create submission without entry") @@ -68,6 +60,12 @@ func (h *Handler) CreateSubmission(c echo.Context) error { return err } + now := time.Now() + earliest, deadline := AllowSubmitAt(contest, entry) + if earliest.After(now) || deadline.Before(now) { + return Error(http.StatusForbidden, "submission window is currently closed") + } + problem, err := h.repo.Problem.Get(ctx, int32(contestID), charcode) if errors.Is(err, pgx.ErrNoRows) { return Error(http.StatusNotFound, "problem not found") @@ -254,3 +252,15 @@ func (h *Handler) GetSubmissions(c echo.Context) error { Items: items, }) } + +func AllowSubmitAt(contest models.Contest, entry models.Entry) (earliest time.Time, deadline time.Time) { + if contest.DurationMins == 0 { + return contest.StartTime, contest.EndTime + } + + deadline = entry.CreatedAt.Add(time.Duration(contest.DurationMins) * time.Minute) + if deadline.Before(contest.EndTime) { + return entry.CreatedAt, deadline + } + return entry.CreatedAt, contest.EndTime +} From b00ff7a30627732d31695b67ae3c9aa3e5a4d9ca Mon Sep 17 00:00:00 2001 From: jus1d Date: Sun, 26 Oct 2025 17:28:04 +0400 Subject: [PATCH 17/24] FIX: personal deadline counts with respect to contest.start_time --- internal/app/handler/submission.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/app/handler/submission.go b/internal/app/handler/submission.go index a8f560a..14cf7bc 100644 --- a/internal/app/handler/submission.go +++ b/internal/app/handler/submission.go @@ -258,9 +258,17 @@ func AllowSubmitAt(contest models.Contest, entry models.Entry) (earliest time.Ti return contest.StartTime, contest.EndTime } - deadline = entry.CreatedAt.Add(time.Duration(contest.DurationMins) * time.Minute) - if deadline.Before(contest.EndTime) { - return entry.CreatedAt, deadline + earliest = entry.CreatedAt + if contest.StartTime.After(earliest) { + earliest = contest.StartTime } - return entry.CreatedAt, contest.EndTime + + personalDeadline := earliest.Add(time.Duration(contest.DurationMins) * time.Minute) + if personalDeadline.Before(contest.EndTime) { + deadline = personalDeadline + } else { + deadline = contest.EndTime + } + + return earliest, deadline } From b8b0ade22918ac208e155e6d2cba91404e8f5dd3 Mon Sep 17 00:00:00 2001 From: jus1d Date: Sun, 26 Oct 2025 18:58:02 +0400 Subject: [PATCH 18/24] fix: remove submission deadline if contest is not started --- internal/app/handler/contest.go | 4 +++- internal/app/handler/problem.go | 29 ++++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/internal/app/handler/contest.go b/internal/app/handler/contest.go index ee8bdab..062e1e8 100644 --- a/internal/app/handler/contest.go +++ b/internal/app/handler/contest.go @@ -135,7 +135,9 @@ func (h *Handler) GetContestByID(c echo.Context) error { cdetailed.IsParticipant = true _, deadline := AllowSubmitAt(contest, entry) - cdetailed.SubmissionDeadline = &deadline + if contest.StartTime.Before(time.Now()) { + cdetailed.SubmissionDeadline = &deadline + } statuses, err := h.repo.Submission.GetProblemStatuses(ctx, entry.ID) if err != nil { diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index 4137063..91358ac 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" @@ -172,26 +173,28 @@ func (h *Handler) GetContestProblem(c echo.Context) error { return err } - _, deadline := AllowSubmitAt(contest, entry) - pdetailed := response.ContestProblemDetailed{ - ID: p.ID, - Charcode: p.Charcode, - ContestID: int32(contestID), - Title: p.Title, - Statement: p.Statement, - Examples: examples, - Difficulty: p.Difficulty, - Status: status, - CreatedAt: p.CreatedAt, - TimeLimitMS: p.TimeLimitMS, - SubmissionDeadline: &deadline, + ID: p.ID, + Charcode: p.Charcode, + ContestID: int32(contestID), + Title: p.Title, + Statement: p.Statement, + Examples: examples, + Difficulty: p.Difficulty, + Status: status, + CreatedAt: p.CreatedAt, + TimeLimitMS: p.TimeLimitMS, Writer: response.User{ ID: p.WriterID, Username: p.WriterUsername, }, } + _, deadline := AllowSubmitAt(contest, entry) + if contest.StartTime.Before(time.Now()) { + pdetailed.SubmissionDeadline = &deadline + } + return c.JSON(http.StatusOK, pdetailed) } From cb5311fdcaa6880e40ff7dd130956e073a68d638 Mon Sep 17 00:00:00 2001 From: jus1d Date: Sun, 26 Oct 2025 23:36:49 +0400 Subject: [PATCH 19/24] chore: add memory limit --- internal/app/handler/dto/request/request.go | 11 +++-- internal/app/handler/dto/response/response.go | 18 +++---- internal/app/handler/problem.go | 47 ++++++++++++------- internal/storage/models/models.go | 1 + .../repository/postgres/contest/contest.go | 2 +- .../repository/postgres/problem/problem.go | 40 ++++------------ internal/storage/repository/repository.go | 4 +- 7 files changed, 57 insertions(+), 66 deletions(-) diff --git a/internal/app/handler/dto/request/request.go b/internal/app/handler/dto/request/request.go index b887042..2f31e76 100644 --- a/internal/app/handler/dto/request/request.go +++ b/internal/app/handler/dto/request/request.go @@ -28,11 +28,12 @@ type CreateContestRequest struct { } type CreateProblemRequest struct { - Title string `json:"title" required:"true"` - Statement string `json:"statement" required:"true"` - Difficulty string `json:"difficulty" required:"true"` - TimeLimitMS int `json:"time_limit_ms"` - TestCases []models.TestCaseDTO `json:"test_cases"` + Title string `json:"title" required:"true"` + Statement string `json:"statement" required:"true"` + Difficulty string `json:"difficulty" required:"true"` + TimeLimitMS int `json:"time_limit_ms"` + MemoryLimitMB int `json:"memory_limit_mb"` + TestCases []models.TestCaseDTO `json:"test_cases"` } type CreateSubmissionRequest struct { diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index dd29934..64948ee 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -108,6 +108,7 @@ type ContestProblemDetailed struct { Difficulty string `json:"difficulty"` Status string `json:"status,omitempty"` TimeLimitMS int32 `json:"time_limit_ms"` + MemoryLimitMB int32 `json:"memory_limit_mb"` SubmissionDeadline *time.Time `json:"submission_deadline,omitempty"` CreatedAt time.Time `json:"created_at"` } @@ -123,14 +124,15 @@ type ContestProblemListItem struct { } type ProblemDetailed struct { - ID int32 `json:"id"` - Writer User `json:"writer"` - Title string `json:"title"` - Statement string `json:"statement"` - Examples []TC `json:"examples,omitempty"` - Difficulty string `json:"difficulty"` - TimeLimitMS int32 `json:"time_limit_ms"` - CreatedAt time.Time `json:"created_at"` + ID int32 `json:"id"` + Writer User `json:"writer"` + Title string `json:"title"` + Statement string `json:"statement"` + Examples []TC `json:"examples,omitempty"` + Difficulty string `json:"difficulty"` + TimeLimitMS int32 `json:"time_limit_ms"` + MemoryLimitMB int32 `json:"memory_limit_mb"` + CreatedAt time.Time `json:"created_at"` } type ProblemListItem struct { diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index 91358ac..4bd9368 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -46,6 +46,15 @@ func (h *Handler) CreateProblem(c echo.Context) error { } } + if body.TimeLimitMS < 500 || body.TimeLimitMS > 10000 { + return Error(http.StatusBadRequest, "time_limit_ms must be between 500 and 10000") + } + + if body.MemoryLimitMB < 16 || body.MemoryLimitMB > 512 { + return Error(http.StatusBadRequest, "memory_limit_mb must be between 16 and 512") + } + + // TODO: Remove examples as database entity // Forbid to create more examples than 3 examplesCount := 0 for i := range body.TestCases { @@ -57,7 +66,7 @@ func (h *Handler) CreateProblem(c echo.Context) error { body.TestCases[i].IsExample = false } } - problemID, err := h.repo.Problem.CreateWithTCs(ctx, claims.UserID, body.Title, body.Statement, body.Difficulty, body.TimeLimitMS, body.TestCases) + problemID, err := h.repo.Problem.CreateWithTCs(ctx, claims.UserID, body.Title, body.Statement, body.Difficulty, body.TimeLimitMS, body.MemoryLimitMB, body.TestCases) if err != nil { return fmt.Errorf("%s: can't create problem: %v", op, err) @@ -174,16 +183,17 @@ func (h *Handler) GetContestProblem(c echo.Context) error { } pdetailed := response.ContestProblemDetailed{ - ID: p.ID, - Charcode: p.Charcode, - ContestID: int32(contestID), - Title: p.Title, - Statement: p.Statement, - Examples: examples, - Difficulty: p.Difficulty, - Status: status, - CreatedAt: p.CreatedAt, - TimeLimitMS: p.TimeLimitMS, + ID: p.ID, + Charcode: p.Charcode, + ContestID: int32(contestID), + Title: p.Title, + Statement: p.Statement, + Examples: examples, + Difficulty: p.Difficulty, + Status: status, + CreatedAt: p.CreatedAt, + TimeLimitMS: p.TimeLimitMS, + MemoryLimitMB: p.MemoryLimitMB, Writer: response.User{ ID: p.WriterID, Username: p.WriterUsername, @@ -236,13 +246,14 @@ func (h *Handler) GetProblemByID(c echo.Context) error { } pdetailed := response.ProblemDetailed{ - ID: problem.ID, - Title: problem.Title, - Statement: problem.Statement, - Examples: examples, - Difficulty: problem.Difficulty, - CreatedAt: problem.CreatedAt, - TimeLimitMS: problem.TimeLimitMS, + ID: problem.ID, + Title: problem.Title, + Statement: problem.Statement, + Examples: examples, + Difficulty: problem.Difficulty, + CreatedAt: problem.CreatedAt, + TimeLimitMS: problem.TimeLimitMS, + MemoryLimitMB: problem.MemoryLimitMB, Writer: response.User{ ID: problem.WriterID, Username: problem.WriterUsername, diff --git a/internal/storage/models/models.go b/internal/storage/models/models.go index 151dc17..4ed7d2f 100644 --- a/internal/storage/models/models.go +++ b/internal/storage/models/models.go @@ -50,6 +50,7 @@ type Problem struct { Statement string `db:"statement"` Difficulty string `db:"difficulty"` TimeLimitMS int32 `db:"time_limit_ms"` + MemoryLimitMB int32 `db:"memory_limit_mb"` CreatedAt time.Time `db:"created_at"` } diff --git a/internal/storage/repository/postgres/contest/contest.go b/internal/storage/repository/postgres/contest/contest.go index 944c5b7..8443fe8 100644 --- a/internal/storage/repository/postgres/contest/contest.go +++ b/internal/storage/repository/postgres/contest/contest.go @@ -108,7 +108,7 @@ func (p *Postgres) GetProblemset(ctx context.Context, contestID int32) ([]models var problems []models.Problem for rows.Next() { var problem models.Problem - if err := rows.Scan(&problem.Charcode, &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, &problem.Difficulty, &problem.TimeLimitMS, &problem.CreatedAt, &problem.WriterUsername); err != nil { + if err := rows.Scan(&problem.Charcode, &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.CreatedAt, &problem.WriterUsername); err != nil { return nil, err } problems = append(problems, problem) diff --git a/internal/storage/repository/postgres/problem/problem.go b/internal/storage/repository/postgres/problem/problem.go index c61dd34..cee41a2 100644 --- a/internal/storage/repository/postgres/problem/problem.go +++ b/internal/storage/repository/postgres/problem/problem.go @@ -3,7 +3,6 @@ package problem import ( "context" "fmt" - "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" @@ -18,7 +17,7 @@ func New(pool *pgxpool.Pool) *Postgres { return &Postgres{pool} } -func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, statement, difficulty string, timeLimitMS int, tcs []models.TestCaseDTO) (int32, error) { +func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, statement, difficulty string, timeLimitMS, memoryLimitMB int, tcs []models.TestCaseDTO) (int32, error) { tx, err := p.pool.BeginTx(ctx, pgx.TxOptions{}) if err != nil { return 0, fmt.Errorf("tx begin failed: %w", err) @@ -27,10 +26,10 @@ func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, sta var problemID int32 err = tx.QueryRow(ctx, ` - INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms, memory_limit_mb) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id - `, writerID, title, statement, difficulty, timeLimitMS).Scan(&problemID) + `, writerID, title, statement, difficulty, timeLimitMS, memoryLimitMB).Scan(&problemID) if err != nil { return 0, fmt.Errorf("insert problem failed: %w", err) } @@ -65,15 +64,6 @@ func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, sta return problemID, nil } -func (p *Postgres) Create(ctx context.Context, writerID int32, title, statement, difficulty, timeLimitMS int32) (int32, error) { - var id int32 - query := `INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms) - VALUES ($1, $2, $3, $4, $5) RETURNING id` - - err := p.pool.QueryRow(ctx, query, writerID, title, statement, difficulty, timeLimitMS).Scan(&id) - return id, err -} - func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (models.Problem, error) { query := `SELECT p.*, cp.charcode, u.username AS writer_username FROM problems p @@ -86,7 +76,7 @@ func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (m var problem models.Problem err := row.Scan( &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, - &problem.Difficulty, &problem.TimeLimitMS, &problem.CreatedAt, + &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.CreatedAt, &problem.Charcode, &problem.WriterUsername, ) @@ -96,7 +86,7 @@ func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (m func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem, error) { query := `SELECT p.id, p.writer_id, p.title, p.statement, - p.difficulty, p.time_limit_ms, p.created_at, + p.difficulty, p.time_limit_ms, p.memory_limit_mb, p.created_at, u.username AS writer_username FROM problems p JOIN users u ON u.id = p.writer_id @@ -107,7 +97,7 @@ func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem var problem models.Problem err := row.Scan( &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, - &problem.Difficulty, &problem.TimeLimitMS, &problem.CreatedAt, + &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.CreatedAt, &problem.WriterUsername, ) @@ -169,7 +159,7 @@ func (p *Postgres) GetAll(ctx context.Context) ([]models.Problem, error) { var p models.Problem if err := rows.Scan( &p.ID, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, - &p.TimeLimitMS, &p.CreatedAt, &p.WriterUsername, + &p.TimeLimitMS, &p.MemoryLimitMB, &p.CreatedAt, &p.WriterUsername, ); err != nil { return nil, err } @@ -208,7 +198,7 @@ func (p *Postgres) GetWithWriterID(ctx context.Context, writerID int32, limit, o var p models.Problem if err := rows.Scan( &p.ID, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, - &p.TimeLimitMS, &p.CreatedAt, &p.WriterUsername, + &p.TimeLimitMS, &p.MemoryLimitMB, &p.CreatedAt, &p.WriterUsername, ); err != nil { rows.Close() br.Close() @@ -229,15 +219,3 @@ func (p *Postgres) GetWithWriterID(ctx context.Context, writerID int32, limit, o return problems, total, nil } - -func (p *Postgres) IsTitleOccupied(ctx context.Context, title string) (bool, error) { - query := `SELECT COUNT(*) FROM problems WHERE LOWER(title) = $1` - - var count int - err := p.pool.QueryRow(ctx, query, strings.ToLower(title)).Scan(&count) - if err != nil { - return false, err - } - - return count > 0, nil -} diff --git a/internal/storage/repository/repository.go b/internal/storage/repository/repository.go index 9142ee4..6b88c5d 100644 --- a/internal/storage/repository/repository.go +++ b/internal/storage/repository/repository.go @@ -54,15 +54,13 @@ type Contest interface { } type Problem interface { - CreateWithTCs(ctx context.Context, writerID int32, title string, statement string, difficulty string, timeLimitMS int, tcs []models.TestCaseDTO) (int32, error) - Create(ctx context.Context, writerID int32, title, statement, difficulty, timeLimitMS int32) (int32, error) + CreateWithTCs(ctx context.Context, writerID int32, title string, statement string, difficulty string, timeLimitMS, memoryLimitMB int, tcs []models.TestCaseDTO) (int32, error) Get(ctx context.Context, contestID int32, charcode string) (models.Problem, error) GetByID(ctx context.Context, problemID int32) (models.Problem, error) GetExampleCases(ctx context.Context, problemID int32) ([]models.TestCase, error) GetTestCaseByID(ctx context.Context, testCaseID int32) (models.TestCase, error) GetAll(ctx context.Context) ([]models.Problem, error) GetWithWriterID(ctx context.Context, writerID int32, limit, offset int) (problems []models.Problem, total int, err error) - IsTitleOccupied(ctx context.Context, title string) (bool, error) } type Entry interface { From b428ab80475fbc8cab5ed23dfceb46e57d1c1f48 Mon Sep 17 00:00:00 2001 From: jus1d Date: Mon, 27 Oct 2025 20:30:26 +0400 Subject: [PATCH 20/24] chore: add time and memory limits to problem items responses --- internal/app/handler/contest.go | 8 ++++-- internal/app/handler/dto/response/response.go | 28 +++++++++++-------- internal/app/handler/problem.go | 10 ++++--- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/internal/app/handler/contest.go b/internal/app/handler/contest.go index 062e1e8..6bd99fc 100644 --- a/internal/app/handler/contest.go +++ b/internal/app/handler/contest.go @@ -112,9 +112,11 @@ func (h *Handler) GetContestByID(c echo.Context) error { ID: problems[i].WriterID, Username: problems[i].WriterUsername, }, - Title: problems[i].Title, - Difficulty: problems[i].Difficulty, - CreatedAt: problems[i].CreatedAt, + Title: problems[i].Title, + Difficulty: problems[i].Difficulty, + TimeLimitMS: problems[i].TimeLimitMS, + MemoryLimitMB: problems[i].MemoryLimitMB, + CreatedAt: problems[i].CreatedAt, } } diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index 64948ee..ce43f19 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -114,13 +114,15 @@ type ContestProblemDetailed struct { } type ContestProblemListItem struct { - ID int32 `json:"id"` - Charcode string `json:"charcode"` - Writer User `json:"writer"` - Title string `json:"title"` - Difficulty string `json:"difficulty"` - Status string `json:"status,omitempty"` - CreatedAt time.Time `json:"created_at"` + ID int32 `json:"id"` + Charcode string `json:"charcode"` + Writer User `json:"writer"` + Title string `json:"title"` + Difficulty string `json:"difficulty"` + Status string `json:"status,omitempty"` + TimeLimitMS int32 `json:"time_limit_ms"` + MemoryLimitMB int32 `json:"memory_limit_mb"` + CreatedAt time.Time `json:"created_at"` } type ProblemDetailed struct { @@ -136,11 +138,13 @@ type ProblemDetailed struct { } type ProblemListItem struct { - ID int32 `json:"id"` - Writer User `json:"writer"` - Title string `json:"title"` - Difficulty string `json:"difficulty"` - CreatedAt time.Time `json:"created_at"` + ID int32 `json:"id"` + Writer User `json:"writer"` + Title string `json:"title"` + Difficulty string `json:"difficulty"` + TimeLimitMS int32 `json:"time_limit_ms"` + MemoryLimitMB int32 `json:"memory_limit_mb"` + CreatedAt time.Time `json:"created_at"` } type TC struct { diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index 4bd9368..b1d06c9 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -102,10 +102,12 @@ func (h *Handler) GetCreatedProblems(c echo.Context) error { problems := make([]response.ProblemListItem, n, n) for i, p := range ps { problems[i] = response.ProblemListItem{ - ID: p.ID, - Title: p.Title, - Difficulty: p.Difficulty, - CreatedAt: p.CreatedAt, + ID: p.ID, + Title: p.Title, + Difficulty: p.Difficulty, + CreatedAt: p.CreatedAt, + TimeLimitMS: p.TimeLimitMS, + MemoryLimitMB: p.MemoryLimitMB, Writer: response.User{ ID: p.WriterID, Username: p.WriterUsername, From 70df59a641f04639fb9e72ad6630412fbfaaf636 Mon Sep 17 00:00:00 2001 From: jus1d Date: Wed, 29 Oct 2025 10:54:05 +0400 Subject: [PATCH 21/24] chore: Introduce different checkers support --- internal/app/handler/contest.go | 1 + internal/app/handler/dto/request/request.go | 1 + internal/app/handler/dto/response/response.go | 4 ++++ internal/app/handler/problem.go | 24 ++++++++++++++----- internal/storage/models/models.go | 1 + .../repository/postgres/contest/contest.go | 2 +- .../repository/postgres/problem/problem.go | 18 +++++++------- internal/storage/repository/repository.go | 2 +- 8 files changed, 36 insertions(+), 17 deletions(-) diff --git a/internal/app/handler/contest.go b/internal/app/handler/contest.go index 6bd99fc..aadfcbc 100644 --- a/internal/app/handler/contest.go +++ b/internal/app/handler/contest.go @@ -116,6 +116,7 @@ func (h *Handler) GetContestByID(c echo.Context) error { Difficulty: problems[i].Difficulty, TimeLimitMS: problems[i].TimeLimitMS, MemoryLimitMB: problems[i].MemoryLimitMB, + Checker: problems[i].Checker, CreatedAt: problems[i].CreatedAt, } } diff --git a/internal/app/handler/dto/request/request.go b/internal/app/handler/dto/request/request.go index 2f31e76..c0bbd7a 100644 --- a/internal/app/handler/dto/request/request.go +++ b/internal/app/handler/dto/request/request.go @@ -33,6 +33,7 @@ type CreateProblemRequest struct { Difficulty string `json:"difficulty" required:"true"` TimeLimitMS int `json:"time_limit_ms"` MemoryLimitMB int `json:"memory_limit_mb"` + Checker string `json:"checker"` TestCases []models.TestCaseDTO `json:"test_cases"` } diff --git a/internal/app/handler/dto/response/response.go b/internal/app/handler/dto/response/response.go index ce43f19..be5ab0c 100644 --- a/internal/app/handler/dto/response/response.go +++ b/internal/app/handler/dto/response/response.go @@ -109,6 +109,7 @@ type ContestProblemDetailed struct { Status string `json:"status,omitempty"` TimeLimitMS int32 `json:"time_limit_ms"` MemoryLimitMB int32 `json:"memory_limit_mb"` + Checker string `json:"checker"` SubmissionDeadline *time.Time `json:"submission_deadline,omitempty"` CreatedAt time.Time `json:"created_at"` } @@ -122,6 +123,7 @@ type ContestProblemListItem struct { Status string `json:"status,omitempty"` TimeLimitMS int32 `json:"time_limit_ms"` MemoryLimitMB int32 `json:"memory_limit_mb"` + Checker string `json:"checker"` CreatedAt time.Time `json:"created_at"` } @@ -134,6 +136,7 @@ type ProblemDetailed struct { Difficulty string `json:"difficulty"` TimeLimitMS int32 `json:"time_limit_ms"` MemoryLimitMB int32 `json:"memory_limit_mb"` + Checker string `json:"checker"` CreatedAt time.Time `json:"created_at"` } @@ -144,6 +147,7 @@ type ProblemListItem struct { Difficulty string `json:"difficulty"` TimeLimitMS int32 `json:"time_limit_ms"` MemoryLimitMB int32 `json:"memory_limit_mb"` + Checker string `json:"checker"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index b1d06c9..32c7dde 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -66,7 +66,13 @@ func (h *Handler) CreateProblem(c echo.Context) error { body.TestCases[i].IsExample = false } } - problemID, err := h.repo.Problem.CreateWithTCs(ctx, claims.UserID, body.Title, body.Statement, body.Difficulty, body.TimeLimitMS, body.MemoryLimitMB, body.TestCases) + + checker := body.Checker + if checker == "" { + checker = "tokens" + } + + problemID, err := h.repo.Problem.CreateWithTCs(ctx, claims.UserID, body.Title, body.Statement, body.Difficulty, body.TimeLimitMS, body.MemoryLimitMB, checker, body.TestCases) if err != nil { return fmt.Errorf("%s: can't create problem: %v", op, err) @@ -108,6 +114,7 @@ func (h *Handler) GetCreatedProblems(c echo.Context) error { CreatedAt: p.CreatedAt, TimeLimitMS: p.TimeLimitMS, MemoryLimitMB: p.MemoryLimitMB, + Checker: p.Checker, Writer: response.User{ ID: p.WriterID, Username: p.WriterUsername, @@ -144,6 +151,14 @@ func (h *Handler) GetContestProblem(c echo.Context) error { } charcode = strings.ToUpper(charcode) + contest, err := h.repo.Contest.GetByID(ctx, int32(contestID)) + if errors.Is(err, pgx.ErrNoRows) { + return Error(http.StatusNotFound, "contest not found") + } + if err != nil { + return err + } + entry, err := h.repo.Entry.Get(ctx, int32(contestID), claims.UserID) if errors.Is(err, pgx.ErrNoRows) { return Error(http.StatusForbidden, "no entry") @@ -179,11 +194,6 @@ func (h *Handler) GetContestProblem(c echo.Context) error { return err } - contest, err := h.repo.Contest.GetByID(ctx, int32(contestID)) - if err != nil { - return err - } - pdetailed := response.ContestProblemDetailed{ ID: p.ID, Charcode: p.Charcode, @@ -196,6 +206,7 @@ func (h *Handler) GetContestProblem(c echo.Context) error { CreatedAt: p.CreatedAt, TimeLimitMS: p.TimeLimitMS, MemoryLimitMB: p.MemoryLimitMB, + Checker: p.Checker, Writer: response.User{ ID: p.WriterID, Username: p.WriterUsername, @@ -256,6 +267,7 @@ func (h *Handler) GetProblemByID(c echo.Context) error { CreatedAt: problem.CreatedAt, TimeLimitMS: problem.TimeLimitMS, MemoryLimitMB: problem.MemoryLimitMB, + Checker: problem.Checker, Writer: response.User{ ID: problem.WriterID, Username: problem.WriterUsername, diff --git a/internal/storage/models/models.go b/internal/storage/models/models.go index 4ed7d2f..ccefcbf 100644 --- a/internal/storage/models/models.go +++ b/internal/storage/models/models.go @@ -51,6 +51,7 @@ type Problem struct { Difficulty string `db:"difficulty"` TimeLimitMS int32 `db:"time_limit_ms"` MemoryLimitMB int32 `db:"memory_limit_mb"` + Checker string `db:"checker"` CreatedAt time.Time `db:"created_at"` } diff --git a/internal/storage/repository/postgres/contest/contest.go b/internal/storage/repository/postgres/contest/contest.go index 8443fe8..329d58f 100644 --- a/internal/storage/repository/postgres/contest/contest.go +++ b/internal/storage/repository/postgres/contest/contest.go @@ -108,7 +108,7 @@ func (p *Postgres) GetProblemset(ctx context.Context, contestID int32) ([]models var problems []models.Problem for rows.Next() { var problem models.Problem - if err := rows.Scan(&problem.Charcode, &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.CreatedAt, &problem.WriterUsername); err != nil { + if err := rows.Scan(&problem.Charcode, &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.Checker, &problem.CreatedAt, &problem.WriterUsername); err != nil { return nil, err } problems = append(problems, problem) diff --git a/internal/storage/repository/postgres/problem/problem.go b/internal/storage/repository/postgres/problem/problem.go index cee41a2..c0a886c 100644 --- a/internal/storage/repository/postgres/problem/problem.go +++ b/internal/storage/repository/postgres/problem/problem.go @@ -17,7 +17,7 @@ func New(pool *pgxpool.Pool) *Postgres { return &Postgres{pool} } -func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, statement, difficulty string, timeLimitMS, memoryLimitMB int, tcs []models.TestCaseDTO) (int32, error) { +func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, statement, difficulty string, timeLimitMS, memoryLimitMB int, checker string, tcs []models.TestCaseDTO) (int32, error) { tx, err := p.pool.BeginTx(ctx, pgx.TxOptions{}) if err != nil { return 0, fmt.Errorf("tx begin failed: %w", err) @@ -26,10 +26,10 @@ func (p *Postgres) CreateWithTCs(ctx context.Context, writerID int32, title, sta var problemID int32 err = tx.QueryRow(ctx, ` - INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms, memory_limit_mb) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO problems (writer_id, title, statement, difficulty, time_limit_ms, memory_limit_mb, checker) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id - `, writerID, title, statement, difficulty, timeLimitMS, memoryLimitMB).Scan(&problemID) + `, writerID, title, statement, difficulty, timeLimitMS, memoryLimitMB, checker).Scan(&problemID) if err != nil { return 0, fmt.Errorf("insert problem failed: %w", err) } @@ -76,7 +76,7 @@ func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (m var problem models.Problem err := row.Scan( &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, - &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.CreatedAt, + &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.Checker, &problem.CreatedAt, &problem.Charcode, &problem.WriterUsername, ) @@ -86,7 +86,7 @@ func (p *Postgres) Get(ctx context.Context, contestID int32, charcode string) (m func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem, error) { query := `SELECT p.id, p.writer_id, p.title, p.statement, - p.difficulty, p.time_limit_ms, p.memory_limit_mb, p.created_at, + p.difficulty, p.time_limit_ms, p.memory_limit_mb, p.checker, p.created_at, u.username AS writer_username FROM problems p JOIN users u ON u.id = p.writer_id @@ -97,7 +97,7 @@ func (p *Postgres) GetByID(ctx context.Context, problemID int32) (models.Problem var problem models.Problem err := row.Scan( &problem.ID, &problem.WriterID, &problem.Title, &problem.Statement, - &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.CreatedAt, + &problem.Difficulty, &problem.TimeLimitMS, &problem.MemoryLimitMB, &problem.Checker, &problem.CreatedAt, &problem.WriterUsername, ) @@ -159,7 +159,7 @@ func (p *Postgres) GetAll(ctx context.Context) ([]models.Problem, error) { var p models.Problem if err := rows.Scan( &p.ID, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, - &p.TimeLimitMS, &p.MemoryLimitMB, &p.CreatedAt, &p.WriterUsername, + &p.TimeLimitMS, &p.MemoryLimitMB, &p.Checker, &p.CreatedAt, &p.WriterUsername, ); err != nil { return nil, err } @@ -198,7 +198,7 @@ func (p *Postgres) GetWithWriterID(ctx context.Context, writerID int32, limit, o var p models.Problem if err := rows.Scan( &p.ID, &p.WriterID, &p.Title, &p.Statement, &p.Difficulty, - &p.TimeLimitMS, &p.MemoryLimitMB, &p.CreatedAt, &p.WriterUsername, + &p.TimeLimitMS, &p.MemoryLimitMB, &p.Checker, &p.CreatedAt, &p.WriterUsername, ); err != nil { rows.Close() br.Close() diff --git a/internal/storage/repository/repository.go b/internal/storage/repository/repository.go index 6b88c5d..c8eda6a 100644 --- a/internal/storage/repository/repository.go +++ b/internal/storage/repository/repository.go @@ -54,7 +54,7 @@ type Contest interface { } type Problem interface { - CreateWithTCs(ctx context.Context, writerID int32, title string, statement string, difficulty string, timeLimitMS, memoryLimitMB int, tcs []models.TestCaseDTO) (int32, error) + CreateWithTCs(ctx context.Context, writerID int32, title string, statement string, difficulty string, timeLimitMS, memoryLimitMB int, checker string, tcs []models.TestCaseDTO) (int32, error) Get(ctx context.Context, contestID int32, charcode string) (models.Problem, error) GetByID(ctx context.Context, problemID int32) (models.Problem, error) GetExampleCases(ctx context.Context, problemID int32) ([]models.TestCase, error) From 1babedf592a93dcc1ccd5512fad1092df4922f8f Mon Sep 17 00:00:00 2001 From: jus1d Date: Wed, 29 Oct 2025 17:36:36 +0400 Subject: [PATCH 22/24] ci: add builds with branch tags --- .github/workflows/deploy.yaml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6fced6e..c75474c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -24,7 +24,18 @@ jobs: - name: Build and push image run: | SHORT_SHA=${GITHUB_SHA::7} + BRANCH_NAME=${GITHUB_REF#refs/heads/} + docker build -t ghcr.io/voidcontests/api:$SHORT_SHA \ - -t ghcr.io/voidcontests/api:latest . - docker push ghcr.io/voidcontests/api:$SHORT_SHA - docker push ghcr.io/voidcontests/api:latest + -t ghcr.io/voidcontests/api:$BRANCH_NAME . + + if [[ "$BRANCH_NAME" == "master" || "$BRANCH_NAME" == "dev" ]]; then + docker push ghcr.io/voidcontests/api:$SHORT_SHA + fi + + docker push ghcr.io/voidcontests/api:$BRANCH_NAME + + if [[ "$BRANCH_NAME" == "master" ]]; then + docker tag ghcr.io/voidcontests/api:$SHORT_SHA ghcr.io/voidcontests/api:latest + docker push ghcr.io/voidcontests/api:latest + fi From 29ff31fb020fb4a11f24a00617cb5efeb01df42f Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 30 Oct 2025 15:50:32 +0400 Subject: [PATCH 23/24] fix(#16): forbid to view contest problems before start --- internal/app/handler/problem.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/app/handler/problem.go b/internal/app/handler/problem.go index 32c7dde..aa9dceb 100644 --- a/internal/app/handler/problem.go +++ b/internal/app/handler/problem.go @@ -159,6 +159,11 @@ func (h *Handler) GetContestProblem(c echo.Context) error { return err } + now := time.Now() + if contest.StartTime.After(now) { + return Error(http.StatusForbidden, "contest not started yet") + } + entry, err := h.repo.Entry.Get(ctx, int32(contestID), claims.UserID) if errors.Is(err, pgx.ErrNoRows) { return Error(http.StatusForbidden, "no entry") From c1f22e61cf84eca07cd3406abff95489a7648e9d Mon Sep 17 00:00:00 2001 From: jus1d Date: Thu, 30 Oct 2025 16:27:00 +0400 Subject: [PATCH 24/24] ci: push images on tag pushed --- .github/workflows/deploy.yaml | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c75474c..1416fc1 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -5,11 +5,16 @@ on: branches: - master - dev + tags: + - '*' jobs: build-image: runs-on: ubuntu-latest + env: + IMAGE_NAME: ghcr.io/voidcontests/api + steps: - name: Checkout code uses: actions/checkout@v4 @@ -22,20 +27,8 @@ jobs: password: ${{ secrets.GHCR_TOKEN }} - name: Build and push image - run: | - SHORT_SHA=${GITHUB_SHA::7} - BRANCH_NAME=${GITHUB_REF#refs/heads/} - - docker build -t ghcr.io/voidcontests/api:$SHORT_SHA \ - -t ghcr.io/voidcontests/api:$BRANCH_NAME . - - if [[ "$BRANCH_NAME" == "master" || "$BRANCH_NAME" == "dev" ]]; then - docker push ghcr.io/voidcontests/api:$SHORT_SHA - fi - - docker push ghcr.io/voidcontests/api:$BRANCH_NAME - - if [[ "$BRANCH_NAME" == "master" ]]; then - docker tag ghcr.io/voidcontests/api:$SHORT_SHA ghcr.io/voidcontests/api:latest - docker push ghcr.io/voidcontests/api:latest - fi + run: curl -sSL https://raw.githubusercontent.com/voidcontests/infra/refs/heads/master/push-image.sh | bash -s -- $IMAGE_NAME + env: + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF: ${{ github.ref }} + IMAGE_NAME: ${{ env.IMAGE_NAME }}