diff --git a/internal/api/problem/create_version.go b/internal/api/problem/create_version.go index 76284c8..457d6aa 100644 --- a/internal/api/problem/create_version.go +++ b/internal/api/problem/create_version.go @@ -44,7 +44,7 @@ func (h *handler) CreateVersion(c *gin.Context) { } // make sure problem exists - _, status := h.problemService.Query(req.ProblemID, false, false) + _, status := h.problemService.Query(&problem.QueryData{ID: req.ProblemID, Associations: false, ShouldEnable: false}) if status != e.Success { e.Pong[any](c, status, nil) return diff --git a/internal/api/problem/details.go b/internal/api/problem/details.go index 78b7fb7..3e659f2 100644 --- a/internal/api/problem/details.go +++ b/internal/api/problem/details.go @@ -3,6 +3,7 @@ package problem import ( "git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/model" + "git.0x7f.app/WOJ/woj-server/internal/service/problem" "github.com/gin-gonic/gin" ) @@ -34,7 +35,7 @@ func (h *handler) Details(c *gin.Context) { claim, exist := c.Get("claim") shouldEnable := !exist || claim.(*model.Claim).Role < model.RoleAdmin - p, status := h.problemService.Query(req.Pid, true, shouldEnable) + p, status := h.problemService.Query(&problem.QueryData{ID: req.Pid, Associations: true, ShouldEnable: shouldEnable}) if status != e.Success { e.Pong[any](c, status, nil) return diff --git a/internal/api/problem/search.go b/internal/api/problem/search.go index 01bb9ef..eab1c5a 100644 --- a/internal/api/problem/search.go +++ b/internal/api/problem/search.go @@ -2,13 +2,16 @@ package problem import ( "git.0x7f.app/WOJ/woj-server/internal/e" - _ "git.0x7f.app/WOJ/woj-server/internal/model" // swag requires this + "git.0x7f.app/WOJ/woj-server/internal/model" + "git.0x7f.app/WOJ/woj-server/internal/service/problem" "github.com/gin-gonic/gin" ) type searchRequest struct { Keyword string `form:"keyword" json:"keyword"` - Tag string `form:"tag" json:"tag"` + Tag string `form:"tag" json:"tag"` + Offset int `form:"offset" json:"offset"` + Limit int `form:"limit" json:"limit" binding:"required"` } // Search @@ -19,7 +22,9 @@ type searchRequest struct { // @Produce json // @Param keyword formData string false "keyword" // @Param tag formData string false "tag" -// @Response 200 {object} e.Response[[]model.Problem] "problems found" +// @Param offset formData int false "start position" +// @Param limit formData int true "limit number of records" +// @Response 200 {object} e.Response[e.WithCount[model.Problem]] "problems found" // @Router /v1/problem/search [post] func (h *handler) Search(c *gin.Context) { req := new(searchRequest) @@ -28,15 +33,15 @@ func (h *handler) Search(c *gin.Context) { return } - // TODO: pagination - if req.Keyword == "" { - // TODO: query without LIKE - problems, status := h.problemService.QueryFuzz(req.Keyword, req.Tag, true, true) - e.Pong(c, status, problems) - return - } else { - problems, status := h.problemService.QueryFuzz(req.Keyword, req.Tag, true, true) - e.Pong(c, status, problems) - return + var count int64 + param := problem.QueryData{ + Keyword: req.Keyword, + Tag: req.Tag, + Offset: req.Offset, + Limit: req.Limit, + Count: &count, } + + problems, status := h.problemService.QueryFuzz(¶m) + e.Pong(c, status, e.WithCount[*model.Problem]{Count: count, Data: problems}) } diff --git a/internal/api/problem/update.go b/internal/api/problem/update.go index 4cb8f4c..7977640 100644 --- a/internal/api/problem/update.go +++ b/internal/api/problem/update.go @@ -72,7 +72,7 @@ func (h *handler) Update(c *gin.Context) { return } else { // update problem // check if problem exists - p, status := h.problemService.Query(req.Pid, true, false) + p, status := h.problemService.Query(&problem.QueryData{ID: req.Pid, Associations: true, ShouldEnable: false}) if status != e.Success { e.Pong[any](c, status, nil) return diff --git a/internal/service/problem/query.go b/internal/service/problem/query.go index 75a24f5..ea65f6c 100644 --- a/internal/service/problem/query.go +++ b/internal/service/problem/query.go @@ -9,23 +9,24 @@ import ( "gorm.io/gorm/clause" ) -func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) { +func (s *service) Query(data *QueryData) (*model.Problem, e.Status) { problem := new(model.Problem) - query := s.db.Get() - if associations { + + if data.Associations { query = query.Preload(clause.Associations) } - err := query.First(&problem, pid).Error + err := query.First(&problem, data.ID).Error + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, e.ProblemNotFound } if err != nil { - s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid)) + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", data.ID)) return nil, e.DatabaseError } - if shouldEnable && !problem.IsEnabled { + if data.ShouldEnable && !problem.IsEnabled { return nil, e.ProblemNotAvailable } return problem, e.Success diff --git a/internal/service/problem/query_fuzz.go b/internal/service/problem/query_fuzz.go index 57abb8f..030dc69 100644 --- a/internal/service/problem/query_fuzz.go +++ b/internal/service/problem/query_fuzz.go @@ -7,28 +7,52 @@ import ( "gorm.io/gorm/clause" ) -func (s *service) QueryFuzz(keyword string, tag string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) { +type QueryData struct { + // precise + ID uint + + // fuzz + Keyword string + Tag string + + // common + Associations bool + ShouldEnable bool + + // paging + Offset int + Limit int + Count *int64 +} + +func (s *service) QueryFuzz(data *QueryData) ([]*model.Problem, e.Status) { problems := make([]*model.Problem, 0) query := s.db.Get() - if associations { + if data.Associations { query = query.Preload(clause.Associations) } - if shouldEnable { + + if data.ShouldEnable { query = query.Where("is_enabled = true") } - query = query. - Where(s.db.Get().Where("title LIKE ?", "%"+keyword+"%"). - Or("statement LIKE ?", "%"+keyword+"%")) - - if tag != "" { - query = query.Where("EXISTS(SELECT 1 FROM unnest(tags) AS elem WHERE elem LIKE ?)", "%"+tag+"%") + if data.Keyword != "" { + query = query. + Where(s.db.Get().Where("title LIKE ?", "%"+data.Keyword+"%"). + Or("statement LIKE ?", "%"+data.Keyword+"%")) } - err := query.Find(&problems).Error + if data.Tag != "" { + query = query.Where("EXISTS(SELECT 1 FROM unnest(tags) AS elem WHERE elem LIKE ?)", "%"+data.Tag+"%") + } + + err := query.Order("created_at ASC"). + Offset(data.Offset).Limit(data.Limit).Find(&problems). + Offset(-1).Limit(-1).Count(data.Count). + Error if err != nil { - s.log.Warn("DatabaseError", zap.Error(err), zap.Any("keyword", keyword), zap.Any("tag", tag)) + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("QueryData", data)) return nil, e.DatabaseError } diff --git a/internal/service/problem/service.go b/internal/service/problem/service.go index f3fb43e..5996b34 100644 --- a/internal/service/problem/service.go +++ b/internal/service/problem/service.go @@ -14,8 +14,8 @@ var _ Service = (*service)(nil) type Service interface { Create(data *CreateData) (*model.Problem, e.Status) Update(problem *model.Problem) (*model.Problem, e.Status) - Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) - QueryFuzz(keyword string, tag string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) + Query(data *QueryData) (*model.Problem, e.Status) + QueryFuzz(data *QueryData) ([]*model.Problem, e.Status) CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) UpdateVersion(pvid uint, values interface{}) e.Status diff --git a/resource/runner/scripts/import.sh b/resource/runner/scripts/import.sh new file mode 100755 index 0000000..55e287c --- /dev/null +++ b/resource/runner/scripts/import.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) +. "$WORKSPACE"/scripts/common.sh + +# read -p "Enter HTTP API Endpoint: " -r endpoint +# if [ -z "$endpoint" ]; then +# log_error "[-] HTTP API Endpoint cannot be empty" +# exit 1 +# fi +# +# read -p "Enter token: " -r token +# if [ -z "$token" ]; then +# log_error "[-] Token cannot be empty" +# exit 1 +# fi + +endpoint="http://localhost:8000" +token="eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6MzAsInZlcnNpb24iOjEsImV4cCI6MTcwNDAwMzg4MCwibmJmIjoxNzAzMzk5MDgwLCJpYXQiOjE3MDMzOTkwODAsImp0aSI6ImVoOU92dVVNRktaZnV3ZngifQ.5F92odFZ-KZolHNIAc5f93exyugTBt5_4PpjYIdZxcHBfQRq_xAUOnJEdCfOBQtNy0-mfdvylkD7oR288Zew2w" + +for problem in "$WORKSPACE/problem/"*; do + if [ -d "$problem" ]; then + dir_name=$(basename "$problem") + log_info "[+] Importing problem $dir_name" + + if [ ! -f "$problem/config.json" ]; then + log_warn "[-] Skipping: $dir_name/config.json not found" + continue + fi + + if [ ! -f "$problem/description.md" ]; then + log_warn "[-] Skipping: $dir_name/description.md not found" + continue + fi + + read -p "Are you sure you want to import $dir_name? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_warn "[-] Skipping: user cancelled" + continue + fi + + title=$(head -n 1 "$problem/description.md" | sed -e 's/^# //' | xargs) + description=$(cat "$problem/description.md") + log_info "[*] Title: $title" + + zip_file=$(mktemp -u --suffix .zip) + log_info "[*] Compressing $problem into $zip_file" >/dev/null + cd "$problem" && zip -9rq "$zip_file" . -x ".mark.prebuild" && cd .. + + payload=$(jq -nc \ + --arg title "$title" \ + --arg description "$description" \ + '{ pid: 0, title: $title, statement: $description, is_enable: false }') + + # log_info "[*] Creating problem statement" + # response=$(curl -s \ + # -H "Content-Type: application/json" \ + # -H "Authorization: $token" \ + # -X POST \ + # -d "$payload" \ + # "$endpoint/api/v1/problem/update") + + # code=$(echo "$response" | jq -r '.code') + # if [ "$code" != "0" ]; then + # log_error "[-] Failed to create problem statement" + # log_error "[-] Response: $response" + # continue + # fi + + # id=$(echo "$response" | jq -r '.body.meta.ID') + # log_info "[*] Problem statement created with id: $id" + + log_info "[*] Uploading problem package" + response=$(curl -s \ + -H "Authorization: $token" \ + -X POST \ + "$endpoint/api/v1/problem/upload") + + code=$(echo "$response" | jq -r '.code') + if [ "$code" != "0" ]; then + log_error "[-] Failed to get upload url" + log_error "[-] Response: $response" + continue + fi + + upload_url=$(echo "$response" | jq -r '.body.url') + storage_key=$(echo "$response" | jq -r '.body.key') + curl -s -X PUT -T "$zip_file" "$upload_url" + + log_info "[*] Creating problem version" + + payload=$(jq -nc \ + --arg pid "$title" \ + --arg storage_key "$storage_key" \ + '{ pid: $pid, storage_key: $storage_key }') + + + echo "$response" + + fi + +done