Compare commits
53 commits
Author | SHA1 | Date | |
---|---|---|---|
6b15ecadcb | |||
a2e0894728 | |||
adcc1bc8d8 | |||
94cb1f5e66 | |||
|
a28d2506e5 | ||
c3eb127b25 | |||
|
fed73f184e | ||
2ab7a16ddf | |||
27fe519958 | |||
18640ada3c | |||
|
d1d8566b98 | ||
|
a27e311526 | ||
|
318599cfea | ||
|
5536c086e0 | ||
|
3396746d60 | ||
|
f89e89faae | ||
|
b960a923d2 | ||
|
ef696d2836 | ||
|
af6e0b89f6 | ||
|
94fc50e272 | ||
|
ffacc9e3ac | ||
|
dd87d63860 | ||
|
e1720e7f82 | ||
|
2bc625363d | ||
|
50a4f87f53 | ||
|
52d38d07bb | ||
|
251772a049 | ||
|
81d7aa2366 | ||
|
e6088953b9 | ||
|
c67405f584 | ||
|
a560715ae8 | ||
6613b03b6c | |||
a1ebd51404 | |||
e201b12db5 | |||
f7dd1bc806 | |||
4d40159772 | |||
|
c2076338fa | ||
|
568ccea09a | ||
|
6dc8f05675 | ||
|
3ed195bb58 | ||
|
e0f150bb14 | ||
|
07f89b5ab1 | ||
|
0ce0e610f8 | ||
|
6e66be32a1 | ||
|
be25404852 | ||
|
4cdd751b16 | ||
|
d62ae666d5 | ||
|
81e75e5a9c | ||
|
30ac81b789 | ||
|
50d6f4a81f | ||
|
13e23b4396 | ||
|
f4af939cb6 | ||
|
f1a732a090 |
75 changed files with 3462 additions and 2324 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
|||
.env
|
||||
.idea
|
||||
/pkg/go/gen
|
||||
/ms-tester
|
||||
|
|
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
|||
[submodule "proto"]
|
||||
path = proto
|
||||
path = contracts
|
||||
url = https://git.sch9.ru/new_gate/contracts
|
||||
|
|
16
Makefile
16
Makefile
|
@ -1,9 +1,9 @@
|
|||
gen:
|
||||
@buf generate
|
||||
dev:
|
||||
@make gen
|
||||
@go run main.go
|
||||
tag = latest
|
||||
|
||||
build:
|
||||
@make gen
|
||||
# TODO: build dockerfile
|
||||
gen:
|
||||
@oapi-codegen --config=config.yaml ./contracts/tester/v1/openapi.yaml
|
||||
dev: gen
|
||||
@go run main.go
|
||||
build: gen
|
||||
@docker build . -t ms-tester:${tag}
|
||||
@#docker push ms-tester:${tag}
|
||||
|
|
108
README.md
Normal file
108
README.md
Normal file
|
@ -0,0 +1,108 @@
|
|||
# ms-tester
|
||||
|
||||
`ms-tester` is a microservice designed for managing programming competitions. It provides backend functionality for handling problems, contests, participants, and their submissions. The service is developed in Go. PostgreSQL serves as the relational database. Pandoc is used to convert problem statements from LaTeX to HTML.
|
||||
|
||||
For understanding the architecture, see the [documentation](https://git.sch9.ru/new_gate/docs).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following dependencies installed:
|
||||
|
||||
* **Docker** and **Docker Compose**: To run PostgreSQL, Pandoc.
|
||||
* **Goose**: For applying database migrations (`go install github.com/pressly/goose/v3/cmd/goose@latest`).
|
||||
* **oapi-codegen**: For generating OpenAPI code (`go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest`).
|
||||
|
||||
### 1. Running Dependencies
|
||||
|
||||
You can run PostgreSQL and Pandoc using Docker Compose, for example:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
pandoc:
|
||||
image: pandoc/latex
|
||||
ports:
|
||||
- "4000:3030" # Exposes Pandoc server on port 4000 locally
|
||||
command: "server" # Runs Pandoc in server mode
|
||||
postgres:
|
||||
image: postgres:14.1-alpine # Uses PostgreSQL 14.1 Alpine image
|
||||
restart: always # Ensures the container restarts if it stops
|
||||
environment:
|
||||
POSTGRES_USER: postgres # Default user
|
||||
POSTGRES_PASSWORD: supersecretpassword # Default password (change for production!)
|
||||
POSTGRES_DB: postgres # Default database name
|
||||
ports:
|
||||
- '5432:5432' # Exposes PostgreSQL on the standard port 5432
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data # Persists database data locally
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -d postgres # Command to check if PostgreSQL is ready
|
||||
interval: 10s # Check every 10 seconds
|
||||
timeout: 3s # Wait 3 seconds for the check to respond
|
||||
retries: 5 # Try 5 times before marking as unhealthy
|
||||
volumes:
|
||||
postgres-data: # Defines the named volume for data persistence
|
||||
```
|
||||
|
||||
Start the services in detached mode:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
The application uses environment variables for configuration. Create a `.env` file in the project root. The minimum required variables are:
|
||||
|
||||
```dotenv
|
||||
# Environment type (development or production)
|
||||
ENV=dev # or prod
|
||||
|
||||
# Address of the running Pandoc service
|
||||
PANDOC=http://localhost:4000
|
||||
|
||||
# Address and port where the ms-tester service will listen
|
||||
ADDRESS=localhost:8080
|
||||
|
||||
# PostgreSQL connection string (Data Source Name)
|
||||
# Format: postgres://user:password@host:port/database?sslmode=disable
|
||||
POSTGRES_DSN=postgres://username:supersecretpassword@localhost:5432/db_name?sslmode=disable
|
||||
|
||||
# Secret key for signing and verifying JWT tokens
|
||||
JWT_SECRET=your_super_secret_jwt_key
|
||||
```
|
||||
|
||||
**Important:** Replace `supersecretpassword` and `your_super_secret_jwt_key` with secure, unique values, especially for a production environment.
|
||||
|
||||
### 3. Database Migrations
|
||||
|
||||
The project uses `goose` to manage the database schema.
|
||||
|
||||
1. Ensure `goose` is installed:
|
||||
```bash
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
```
|
||||
2. Apply the migrations to the running PostgreSQL database. Make sure the connection string in the command matches the `POSTGRES_DSN` from your `.env` file:
|
||||
```bash
|
||||
goose -dir ./migrations postgres "postgres://postgres:supersecretpassword@localhost:5432/postgres?sslmode=disable" up
|
||||
```
|
||||
|
||||
### 4. OpenAPI Code Generation
|
||||
|
||||
The project uses OpenAPI to define its API. Go code for handlers and models is generated based on this specification using `oapi-codegen`.
|
||||
|
||||
Run the generation command:
|
||||
|
||||
```bash
|
||||
make gen
|
||||
```
|
||||
|
||||
### 5. Running the Application
|
||||
|
||||
Start the `ms-tester` service:
|
||||
|
||||
```bash
|
||||
go run ./main.go
|
||||
```
|
||||
|
||||
After starting, the service will be available at the address specified in the `ADDRESS` variable in your `.env` file (e.g., `http://localhost:8080`).
|
12
buf.gen.yaml
12
buf.gen.yaml
|
@ -1,12 +0,0 @@
|
|||
version: v1
|
||||
managed:
|
||||
enabled: true
|
||||
go_package_prefix:
|
||||
default: git.sch9.ru/new_gate/ms-tester/pkg/go/gen
|
||||
plugins:
|
||||
- name: go
|
||||
out: pkg/go/gen
|
||||
opt: paths=source_relative
|
||||
- name: go-grpc
|
||||
out: pkg/go/gen
|
||||
opt: paths=source_relative
|
7
buf.yaml
7
buf.yaml
|
@ -1,7 +0,0 @@
|
|||
version: v1
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
5
config.yaml
Normal file
5
config.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
package: testerv1
|
||||
generate:
|
||||
fiber-server: true
|
||||
models: true
|
||||
output: ./contracts/tester/v1/tester.go
|
15
config/config.go
Normal file
15
config/config.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
Env string `env:"ENV" env-default:"prod"`
|
||||
Pandoc string `env:"PANDOC" required:"true"`
|
||||
Address string `env:"ADDRESS" required:"true"`
|
||||
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
|
||||
JWTSecret string `env:"JWT_SECRET" required:"true"`
|
||||
|
||||
//RabbitDSN string `env:"RABBIT_DSN" required:"true"`
|
||||
//InstanceName string `env:"INSTANCE_NAME" required:"true"`
|
||||
//RQueueName string `env:"R_QUEUE_NAME" required:"true"`
|
||||
//TQueueName string `env:"T_QUEUE_NAME" required:"true"`
|
||||
//NQueueName string `env:"N_QUEUE_NAME" required:"true"`
|
||||
}
|
1
contracts
Submodule
1
contracts
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f00483d24a53a243734c793fc24e02d52d39fdab
|
68
go.mod
68
go.mod
|
@ -1,59 +1,77 @@
|
|||
module git.sch9.ru/new_gate/ms-tester
|
||||
|
||||
go 1.21.3
|
||||
|
||||
toolchain go1.22.0
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
|
||||
github.com/open-policy-agent/opa v0.67.1
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/open-policy-agent/opa v1.2.0
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/valkey-io/valkey-go v1.0.47
|
||||
go.uber.org/zap v1.27.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.21.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
|
|
213
go.sum
213
go.sum
|
@ -1,35 +1,42 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps=
|
||||
github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA=
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
|
@ -47,27 +54,25 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
|||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
|
||||
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8=
|
||||
github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||
|
@ -84,45 +89,77 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
|||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/open-policy-agent/opa v0.67.1 h1:rzy26J6g1X+CKknAcx0Vfbt41KqjuSzx4E0A8DAZf3E=
|
||||
github.com/open-policy-agent/opa v0.67.1/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/open-policy-agent/opa v1.2.0 h1:88NDVCM0of1eO6Z4AFeL3utTEtMuwloFmWWU7dRV1z0=
|
||||
github.com/open-policy-agent/opa v1.2.0/go.mod h1:30euUmOvuBoebRCcJ7DMF42bRBOPznvt0ACUMYDUGVY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
|
||||
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM=
|
||||
github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/valkey-io/valkey-go v1.0.47 h1:fW5+m2BaLAbxB1EWEEWmj+i2n+YcYFBDG/jKs6qu5j8=
|
||||
github.com/valkey-io/valkey-go v1.0.47/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
|
@ -131,56 +168,60 @@ github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBe
|
|||
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package lib
|
||||
|
||||
type Config struct {
|
||||
Env string `env:"ENV" env-default:"prod"`
|
||||
Pandoc string `env:"PANDOC" required:"true"`
|
||||
Auth string `env:"PANDOC" required:"true"`
|
||||
Address string `env:"ADDRESS" required:"true"`
|
||||
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type code uint8
|
||||
|
||||
const (
|
||||
ErrValidationFailed code = 1
|
||||
ErrInternal code = 2
|
||||
ErrExternal code = 3
|
||||
ErrNoPermission code = 4
|
||||
ErrUnknown code = 5
|
||||
ErrDeadlineExceeded code = 6
|
||||
ErrNotFound code = 7
|
||||
ErrAlreadyExists code = 8
|
||||
ErrConflict code = 9
|
||||
ErrUnimplemented code = 10
|
||||
ErrBadInput code = 11
|
||||
ErrUnauthenticated code = 12
|
||||
)
|
||||
|
||||
func (c code) String() string {
|
||||
switch {
|
||||
case errors.Is(c, ErrValidationFailed):
|
||||
return "validation error"
|
||||
case errors.Is(c, ErrInternal):
|
||||
return "internal error"
|
||||
case errors.Is(c, ErrExternal):
|
||||
return "external error"
|
||||
case errors.Is(c, ErrNoPermission):
|
||||
return "permission error"
|
||||
case errors.Is(c, ErrUnknown):
|
||||
return "unknown error"
|
||||
case errors.Is(c, ErrDeadlineExceeded):
|
||||
return "deadline error"
|
||||
case errors.Is(c, ErrNotFound):
|
||||
return "not found error"
|
||||
case errors.Is(c, ErrAlreadyExists):
|
||||
return "already exists error"
|
||||
case errors.Is(c, ErrConflict):
|
||||
return "conflict error"
|
||||
case errors.Is(c, ErrUnimplemented):
|
||||
return "unimplemented error"
|
||||
case errors.Is(c, ErrBadInput):
|
||||
return "bad input error"
|
||||
}
|
||||
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (c code) Error() string {
|
||||
return c.String()
|
||||
}
|
||||
|
||||
type layer uint8
|
||||
|
||||
const (
|
||||
LayerTransport layer = 1
|
||||
LayerService layer = 2
|
||||
LayerStorage layer = 3
|
||||
)
|
||||
|
||||
func (l layer) String() string {
|
||||
switch l {
|
||||
case LayerTransport:
|
||||
return "transport"
|
||||
case LayerService:
|
||||
return "service"
|
||||
case LayerStorage:
|
||||
return "storage"
|
||||
}
|
||||
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func location(skip int) string {
|
||||
_, file, line, _ := runtime.Caller(skip)
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
src error
|
||||
layer layer
|
||||
code code
|
||||
msg string
|
||||
loc string
|
||||
}
|
||||
|
||||
func wrap(src error, layer layer, class code, msg string, loc string) *Error {
|
||||
return &Error{
|
||||
src: src,
|
||||
layer: layer,
|
||||
code: class,
|
||||
msg: msg,
|
||||
loc: loc,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() []error {
|
||||
return []error{e.src, e.code}
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.code.String(), e.msg)
|
||||
}
|
||||
|
||||
func (e *Error) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
|
||||
if e.src != nil {
|
||||
encoder.AddString("src", e.src.Error())
|
||||
}
|
||||
encoder.AddString("layer", e.layer.String())
|
||||
encoder.AddString("code", e.code.String())
|
||||
encoder.AddString("msg", e.msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TransportError(src error, code code, msg string) error {
|
||||
return wrap(src, LayerTransport, code, msg, location(2))
|
||||
}
|
||||
|
||||
func ServiceError(src error, code code, msg string) error {
|
||||
return wrap(src, LayerService, code, msg, location(2))
|
||||
}
|
||||
|
||||
func StorageError(src error, code code, msg string) error {
|
||||
return wrap(src, LayerStorage, code, msg, location(2))
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func AsTimeP(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
func AsInt32P(v int32) *int32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func AsStringP(str string) *string {
|
||||
return &str
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PandocClient struct {
|
||||
client *http.Client
|
||||
address string
|
||||
}
|
||||
|
||||
func NewPandocClient(client *http.Client, address string) *PandocClient {
|
||||
return &PandocClient{
|
||||
client: client,
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
|
||||
type convertRequest struct {
|
||||
Text string `json:"text"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
func (client *PandocClient) convert(ctx context.Context, text, from, to string) (string, error) {
|
||||
body, err := json.Marshal(convertRequest{
|
||||
Text: text,
|
||||
From: from,
|
||||
To: to,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(body)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.address, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (client *PandocClient) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) {
|
||||
return client.convert(ctx, text, "latex", "html5")
|
||||
}
|
55
internal/models/contest.go
Normal file
55
internal/models/contest.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Contest struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ContestsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ContestsList struct {
|
||||
Contests []*ContestsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type ContestsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
}
|
||||
|
||||
func (f ContestsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ContestUpdate struct {
|
||||
Title *string `json:"title"`
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
Participants []*ParticipantsStat
|
||||
Summary []*ProblemStatSummary
|
||||
}
|
||||
|
||||
type ParticipantsStat struct {
|
||||
Id int32 `db:"id"`
|
||||
Name string `db:"name"`
|
||||
SolvedInTotal int32 `db:"solved_in_total"`
|
||||
PenaltyInTotal int32 `db:"penalty_in_total"`
|
||||
Solutions []*SolutionsListItem `db:"solutions"`
|
||||
}
|
||||
|
||||
type ProblemStatSummary struct {
|
||||
Id int32 `db:"task_id"`
|
||||
Position int32 `db:"position"`
|
||||
Success int32 `db:"success"`
|
||||
Total int32 `db:"total"`
|
||||
}
|
13
internal/models/pagination.go
Normal file
13
internal/models/pagination.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
type Pagination struct {
|
||||
Page int32 `json:"page"`
|
||||
Total int32 `json:"total"`
|
||||
}
|
||||
|
||||
func Total(count int32, pageSize int32) int32 {
|
||||
if count%pageSize == 0 {
|
||||
return count / pageSize
|
||||
}
|
||||
return count/pageSize + 1
|
||||
}
|
41
internal/models/participant.go
Normal file
41
internal/models/participant.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Participant struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsList struct {
|
||||
Participants []*ParticipantsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type ParticipantsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
|
||||
ContestId int32
|
||||
}
|
||||
|
||||
func (f ParticipantsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ParticipantUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
84
internal/models/problem.go
Normal file
84
internal/models/problem.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Problem struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
|
||||
Legend string `db:"legend"`
|
||||
InputFormat string `db:"input_format"`
|
||||
OutputFormat string `db:"output_format"`
|
||||
Notes string `db:"notes"`
|
||||
Scoring string `db:"scoring"`
|
||||
LatexSummary string `db:"latex_summary"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ProblemsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
SolvedCount int32 `db:"solved_count"`
|
||||
}
|
||||
|
||||
type ProblemsList struct {
|
||||
Problems []*ProblemsListItem `json:"problems"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type ProblemsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
}
|
||||
|
||||
func (f ProblemsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ProblemUpdate struct {
|
||||
Title *string `db:"title"`
|
||||
MemoryLimit *int32 `db:"memory_limit"`
|
||||
TimeLimit *int32 `db:"time_limit"`
|
||||
|
||||
Legend *string `db:"legend"`
|
||||
InputFormat *string `db:"input_format"`
|
||||
OutputFormat *string `db:"output_format"`
|
||||
Notes *string `db:"notes"`
|
||||
Scoring *string `db:"scoring"`
|
||||
|
||||
LegendHtml *string `db:"legend_html"`
|
||||
InputFormatHtml *string `db:"input_format_html"`
|
||||
OutputFormatHtml *string `db:"output_format_html"`
|
||||
NotesHtml *string `db:"notes_html"`
|
||||
ScoringHtml *string `db:"scoring_html"`
|
||||
}
|
||||
|
||||
type ProblemStatement struct {
|
||||
Legend string `db:"legend"`
|
||||
InputFormat string `db:"input_format"`
|
||||
OutputFormat string `db:"output_format"`
|
||||
Notes string `db:"notes"`
|
||||
Scoring string `db:"scoring"`
|
||||
}
|
||||
|
||||
type Html5ProblemStatement struct {
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
}
|
162
internal/models/session.go
Normal file
162
internal/models/session.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
)
|
||||
|
||||
type JWT struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
NotBefore int64 `json:"nbf"`
|
||||
Permissions []grant `json:"permissions"`
|
||||
}
|
||||
|
||||
func (j JWT) Valid() error {
|
||||
if uuid.Validate(j.SessionId) != nil {
|
||||
return errors.New("invalid session id")
|
||||
}
|
||||
if j.UserId == 0 {
|
||||
return errors.New("empty user id")
|
||||
}
|
||||
if j.ExpiresAt == 0 {
|
||||
return errors.New("empty expires at")
|
||||
}
|
||||
if j.IssuedAt == 0 {
|
||||
return errors.New("empty issued at")
|
||||
}
|
||||
if j.NotBefore == 0 {
|
||||
return errors.New("empty not before")
|
||||
}
|
||||
if len(j.Permissions) == 0 {
|
||||
return errors.New("empty permissions")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Role int32
|
||||
|
||||
const (
|
||||
RoleGuest Role = -1
|
||||
RoleStudent Role = 0
|
||||
RoleTeacher Role = 1
|
||||
RoleAdmin Role = 2
|
||||
)
|
||||
|
||||
func (r Role) String() string {
|
||||
switch r {
|
||||
case RoleGuest:
|
||||
return "guest"
|
||||
case RoleStudent:
|
||||
return "student"
|
||||
case RoleTeacher:
|
||||
return "teacher"
|
||||
case RoleAdmin:
|
||||
return "admin"
|
||||
}
|
||||
|
||||
panic("invalid role")
|
||||
}
|
||||
|
||||
type Action string
|
||||
|
||||
const (
|
||||
Create Action = "create"
|
||||
Read Action = "read"
|
||||
Update Action = "update"
|
||||
Delete Action = "delete"
|
||||
)
|
||||
|
||||
type Resource string
|
||||
|
||||
const (
|
||||
ResourceAnotherUser Resource = "another-user"
|
||||
ResourceMeUser Resource = "me-user"
|
||||
ResourceListUser Resource = "list-user"
|
||||
|
||||
ResourceOwnSession Resource = "own-session"
|
||||
)
|
||||
|
||||
type grant struct {
|
||||
Action Action `json:"action"`
|
||||
Resource Resource `json:"resource"`
|
||||
}
|
||||
|
||||
var Grants = map[string][]grant{
|
||||
RoleGuest.String(): {},
|
||||
RoleStudent.String(): {
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
RoleTeacher.String(): {
|
||||
{Create, ResourceAnotherUser},
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Read, ResourceListUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
RoleAdmin.String(): {
|
||||
{Create, ResourceAnotherUser},
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Read, ResourceListUser},
|
||||
{Update, ResourceAnotherUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceAnotherUser},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
}
|
||||
|
||||
const module = `package app.rbac
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
some grant in input.role_grants[input.role]
|
||||
|
||||
input.action == grant.action
|
||||
input.resource == grant.resource
|
||||
}
|
||||
`
|
||||
|
||||
var query rego.PreparedEvalQuery
|
||||
|
||||
func (r Role) HasPermission(action Action, resource Resource) bool {
|
||||
ctx := context.TODO()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"action": action,
|
||||
"resource": resource,
|
||||
"role": r.String(),
|
||||
"role_grants": Grants,
|
||||
}
|
||||
|
||||
results, err := query.Eval(ctx, rego.EvalInput(input))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return results.Allowed()
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
ctx := context.TODO()
|
||||
|
||||
query, err = rego.New(
|
||||
rego.Query("data.app.rbac.allow"),
|
||||
rego.Module("ms-auth.rego", module),
|
||||
).PrepareForEval(ctx)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
118
internal/models/solution.go
Normal file
118
internal/models/solution.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Solution struct {
|
||||
Id int32 `db:"id"`
|
||||
|
||||
ParticipantId int32 `db:"participant_id"`
|
||||
ParticipantName string `db:"participant_name"`
|
||||
|
||||
Solution string `db:"solution"`
|
||||
|
||||
State int32 `db:"state"`
|
||||
Score int32 `db:"score"`
|
||||
Penalty int32 `db:"penalty"`
|
||||
TimeStat int32 `db:"time_stat"`
|
||||
MemoryStat int32 `db:"memory_stat"`
|
||||
Language int32 `db:"language"`
|
||||
|
||||
TaskId int32 `db:"task_id"`
|
||||
TaskPosition int32 `db:"task_position"`
|
||||
TaskTitle string `db:"task_title"`
|
||||
|
||||
ContestId int32 `db:"contest_id"`
|
||||
ContestTitle string `db:"contest_title"`
|
||||
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
type SolutionCreation struct {
|
||||
Solution string
|
||||
TaskId int32
|
||||
ParticipantId int32
|
||||
Language int32
|
||||
Penalty int32
|
||||
}
|
||||
|
||||
type SolutionsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
|
||||
ParticipantId int32 `db:"participant_id"`
|
||||
ParticipantName string `db:"participant_name"`
|
||||
|
||||
State int32 `db:"state"`
|
||||
Score int32 `db:"score"`
|
||||
Penalty int32 `db:"penalty"`
|
||||
TimeStat int32 `db:"time_stat"`
|
||||
MemoryStat int32 `db:"memory_stat"`
|
||||
Language int32 `db:"language"`
|
||||
|
||||
TaskId int32 `db:"task_id"`
|
||||
TaskPosition int32 `db:"task_position"`
|
||||
TaskTitle string `db:"task_title"`
|
||||
|
||||
ContestId int32 `db:"contest_id"`
|
||||
ContestTitle string `db:"contest_title"`
|
||||
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
type SolutionsList struct {
|
||||
Solutions []*SolutionsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type SolutionsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
ContestId *int32
|
||||
ParticipantId *int32
|
||||
TaskId *int32
|
||||
Language *int32
|
||||
State *int32
|
||||
Order *int32
|
||||
}
|
||||
|
||||
func (f SolutionsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
//type Result int32
|
||||
//
|
||||
//const (
|
||||
// NotTested Result = 1 // change only with schema change
|
||||
// Accepted Result = 2
|
||||
// WrongAnswer Result = 3
|
||||
// PresentationError Result = 4
|
||||
// CompilationError Result = 5
|
||||
// MemoryLimitExceeded Result = 6
|
||||
// TimeLimitExceeded Result = 7
|
||||
// RuntimeError Result = 8
|
||||
// SystemFailDuringTesting Result = 9
|
||||
// Testing Result = 10
|
||||
//)
|
||||
//
|
||||
//var ErrBadResult = errors.New("bad result")
|
||||
//
|
||||
//func (result Result) Valid() error {
|
||||
// switch result {
|
||||
// case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceeded, CompilationError, SystemFailDuringTesting:
|
||||
// return nil
|
||||
// }
|
||||
// return ErrBadResult
|
||||
//}
|
||||
//
|
||||
//type Language struct {
|
||||
// Name string
|
||||
// CompileCmd []string //source: src;result:executable
|
||||
// RunCmd []string //source: executable
|
||||
//}
|
||||
//
|
||||
//var Languages = []Language{
|
||||
// {Name: "gcc std=c90",
|
||||
// CompileCmd: []string{"gcc", "src", "-std=c90", "-o", "executable"},
|
||||
// RunCmd: []string{"executable"}},
|
||||
//}
|
35
internal/models/task.go
Normal file
35
internal/models/task.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Task struct {
|
||||
Id int32 `db:"id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type TasksListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type ContestStorage interface {
|
||||
CreateContest(ctx context.Context, contest *models.Contest) (int32, error)
|
||||
ReadContestById(ctx context.Context, id int32) (*models.Contest, error)
|
||||
UpdateContest(ctx context.Context, contest *models.Contest) error
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type ContestService struct {
|
||||
contestStorage ContestStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewContestService(
|
||||
contestStorage ContestStorage,
|
||||
permissionService IPermissionService,
|
||||
) *ContestService {
|
||||
return &ContestService{
|
||||
contestStorage: contestStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *ContestService) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.CreateContest(ctx, contest)
|
||||
}
|
||||
|
||||
func (service *ContestService) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return nil, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.ReadContestById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *ContestService) UpdateContest(ctx context.Context, contest *models.Contest) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.UpdateContest(ctx, contest)
|
||||
}
|
||||
|
||||
func (service *ContestService) DeleteContest(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.DeleteContest(ctx, id)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type LanguageStorage interface {
|
||||
ReadLanguageById(ctx context.Context, id int32) (*models.Language, error)
|
||||
}
|
||||
|
||||
type LanguageService struct {
|
||||
languageStorage LanguageStorage
|
||||
}
|
||||
|
||||
func NewLanguageService(
|
||||
languageStorage LanguageStorage,
|
||||
) *LanguageService {
|
||||
return &LanguageService{
|
||||
languageStorage: languageStorage,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *LanguageService) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) {
|
||||
//userId := ctx.Value("user_id").(int32)
|
||||
panic("access control is not implemented yet")
|
||||
return service.languageStorage.ReadLanguageById(ctx, id)
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type ParticipantStorage interface {
|
||||
CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error)
|
||||
ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error)
|
||||
UpdateParticipant(ctx context.Context, participant *models.Participant) error
|
||||
DeleteParticipant(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type ParticipantService struct {
|
||||
participantStorage ParticipantStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewParticipantService(
|
||||
participantStorage ParticipantStorage,
|
||||
permissionService IPermissionService,
|
||||
) *ParticipantService {
|
||||
return &ParticipantService{
|
||||
participantStorage: participantStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *ParticipantService) CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.CreateParticipant(ctx, participant)
|
||||
}
|
||||
|
||||
func (service *ParticipantService) ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return nil, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.ReadParticipantById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *ParticipantService) UpdateParticipant(ctx context.Context, participant *models.Participant) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.UpdateParticipant(ctx, participant)
|
||||
}
|
||||
|
||||
func (service *ParticipantService) DeleteParticipant(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.DeleteParticipant(ctx, id)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
)
|
||||
|
||||
type PermissionService struct {
|
||||
query *rego.PreparedEvalQuery
|
||||
}
|
||||
|
||||
func NewPermissionService() *PermissionService {
|
||||
query, err := rego.New(
|
||||
rego.Query("allow = data.problem.rbac.allow"),
|
||||
rego.Load([]string{"./opa/all.rego"}, nil),
|
||||
).PrepareForEval(context.TODO())
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &PermissionService{
|
||||
query: &query,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PermissionService) Allowed(ctx context.Context, user *models.User, action string) bool {
|
||||
input := map[string]interface{}{
|
||||
"user": user,
|
||||
"action": action,
|
||||
}
|
||||
|
||||
result, err := s.query.Eval(ctx, rego.EvalInput(input))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result[0].Bindings["allow"].(bool)
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type ProblemStorage interface {
|
||||
CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error)
|
||||
ReadProblemById(ctx context.Context, id int32) (*models.Problem, error)
|
||||
UpdateProblem(ctx context.Context, problem *models.Problem) error
|
||||
DeleteProblem(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type PandocClient interface {
|
||||
ConvertLatexToHtml5(ctx context.Context, text string) (string, error)
|
||||
}
|
||||
|
||||
type IPermissionService interface {
|
||||
Allowed(ctx context.Context, user *models.User, action string) bool
|
||||
}
|
||||
|
||||
type ProblemService struct {
|
||||
problemStorage ProblemStorage
|
||||
pandocClient PandocClient
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewProblemService(
|
||||
problemStorage ProblemStorage,
|
||||
pandocClient PandocClient,
|
||||
permissionService IPermissionService,
|
||||
) *ProblemService {
|
||||
return &ProblemService{
|
||||
problemStorage: problemStorage,
|
||||
pandocClient: pandocClient,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func extractUser(ctx context.Context) *models.User {
|
||||
return ctx.Value("user").(*models.User)
|
||||
}
|
||||
|
||||
func (service *ProblemService) CanCreateProblem(ctx context.Context) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *ProblemService) CanReadProblemById(ctx context.Context) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *ProblemService) CanUpdateProblem(ctx context.Context) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *ProblemService) CanDeleteProblem(ctx context.Context) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *ProblemService) CreateProblem(ctx context.Context, problem *models.Problem) (int32, error) {
|
||||
if err := service.CanCreateProblem(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err := service.pandocClient.ConvertLatexToHtml5(ctx, *problem.Description)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return service.problemStorage.CreateProblem(ctx, problem, nil)
|
||||
}
|
||||
|
||||
func (service *ProblemService) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
if err := service.CanReadProblemById(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return service.problemStorage.ReadProblemById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *ProblemService) UpdateProblem(ctx context.Context, problem *models.Problem) error {
|
||||
if err := service.CanUpdateProblem(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.problemStorage.UpdateProblem(ctx, problem)
|
||||
}
|
||||
|
||||
func (service *ProblemService) DeleteProblem(ctx context.Context, id int32) error {
|
||||
if err := service.CanDeleteProblem(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.problemStorage.DeleteProblem(ctx, id)
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type SolutionStorage interface {
|
||||
CreateSolution(ctx context.Context, solution models.Solution) (int32, error)
|
||||
ReadSolutionById(ctx context.Context, id int32) (models.Solution, error)
|
||||
RejudgeSolution(ctx context.Context, id int32) error
|
||||
DeleteSolution(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type SolutionService struct {
|
||||
solutionStorage SolutionStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewSolutionService(
|
||||
solutionStorage SolutionStorage,
|
||||
permissionService IPermissionService,
|
||||
) *SolutionService {
|
||||
return &SolutionService{
|
||||
solutionStorage: solutionStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *SolutionService) CreateSolution(ctx context.Context, solution models.Solution) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.CreateSolution(ctx, solution)
|
||||
}
|
||||
|
||||
func (service *SolutionService) ReadSolutionById(ctx context.Context, id int32) (models.Solution, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return models.Solution{}, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.ReadSolutionById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *SolutionService) RejudgeSolution(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.RejudgeSolution(ctx, id)
|
||||
}
|
||||
|
||||
func (service *SolutionService) DeleteSolution(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.DeleteSolution(ctx, id)
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type TaskStorage interface {
|
||||
CreateTask(ctx context.Context, task models.Task) (int32, error)
|
||||
DeleteTask(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type TaskService struct {
|
||||
taskStorage TaskStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewTaskService(
|
||||
taskStorage TaskStorage,
|
||||
permissionService IPermissionService,
|
||||
) *TaskService {
|
||||
return &TaskService{
|
||||
taskStorage: taskStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *TaskService) CreateTask(ctx context.Context, task models.Task) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.taskStorage.CreateTask(ctx, task)
|
||||
}
|
||||
|
||||
func (service *TaskService) DeleteTask(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.taskStorage.DeleteTask(ctx, id)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type UserStorage interface {
|
||||
CreateUser(ctx context.Context, user *models.User) error
|
||||
ReadUserById(ctx context.Context, userId int32) (*models.User, error)
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
userStorage UserStorage
|
||||
}
|
||||
|
||||
func NewUserService(userStorage UserStorage) *UserService {
|
||||
return &UserService{
|
||||
userStorage: userStorage,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(ctx context.Context, user *models.User) error {
|
||||
return s.userStorage.CreateUser(ctx, user)
|
||||
}
|
||||
|
||||
func (s *UserService) ReadUserById(ctx context.Context, userId int32) (*models.User, error) {
|
||||
return s.userStorage.ReadUserById(ctx, userId)
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ContestStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewContestStorage(db *sqlx.DB, logger *zap.Logger) *ContestStorage {
|
||||
return &ContestStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *ContestStorage) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) {
|
||||
query := storage.db.Rebind(`
|
||||
INSERT INTO contests
|
||||
(name)
|
||||
VALUES (?)
|
||||
RETURNING id
|
||||
`)
|
||||
|
||||
rows, err := storage.db.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
contest.Name,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
err = rows.StructScan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
|
||||
}
|
||||
|
||||
func (storage *ContestStorage) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
var contest models.Contest
|
||||
query := storage.db.Rebind("SELECT * from contests WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &contest, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &contest, nil
|
||||
}
|
||||
|
||||
func (storage *ContestStorage) UpdateContest(ctx context.Context, contest *models.Contest) error {
|
||||
query := storage.db.Rebind("UPDATE contests SET name=? WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, contest.Name, contest.Id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *ContestStorage) DeleteContest(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM contests WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
func handlePgErr(err error) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if !errors.As(err, &pgErr) {
|
||||
return lib.StorageError(err, lib.ErrUnknown, "unexpected error from postgres")
|
||||
}
|
||||
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
|
||||
// TODO: probably should specify which constraint
|
||||
return lib.StorageError(err, lib.ErrConflict, pgErr.Message)
|
||||
}
|
||||
if pgerrcode.IsNoData(pgErr.Code) {
|
||||
return lib.StorageError(err, lib.ErrNotFound, pgErr.Message)
|
||||
}
|
||||
return lib.StorageError(err, lib.ErrUnimplemented, "unimplemented error")
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type LanguageStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewLanguageStorage(db *sqlx.DB, logger *zap.Logger) *LanguageStorage {
|
||||
return &LanguageStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *LanguageStorage) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) {
|
||||
if(id<=int32(len(models.Languages))) {
|
||||
return nil,lib.StorageError(nil,lib.ErrNotFound,"language not found")
|
||||
}
|
||||
return &models.Languages[id], nil
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ParticipantStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewParticipantStorage(db *sqlx.DB, logger *zap.Logger) *ParticipantStorage {
|
||||
return &ParticipantStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error) {
|
||||
query := storage.db.Rebind(`
|
||||
INSERT INTO participants
|
||||
(user_id,contest_id,name)
|
||||
VALUES (?,?,?)
|
||||
RETURNING id
|
||||
`)
|
||||
|
||||
rows, err := storage.db.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
participant.UserId,
|
||||
participant.ContestId,
|
||||
participant.Name,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
err = rows.StructScan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error) {
|
||||
var participant models.Participant
|
||||
query := storage.db.Rebind("SELECT * from participants WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &participant, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &participant, nil
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) UpdateParticipant(ctx context.Context, id int32, participant models.Participant) error {
|
||||
query := storage.db.Rebind("UPDATE participants SET name=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, participant.Name)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) DeleteParticipant(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM participants WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ProblemStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewProblemStorage(db *sqlx.DB, logger *zap.Logger) *ProblemStorage {
|
||||
return &ProblemStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *ProblemStorage) CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error) {
|
||||
tx, err := storage.db.Beginx()
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
query := tx.Rebind(`
|
||||
INSERT INTO problems
|
||||
(name,description,time_limit,memory_limit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`)
|
||||
rows, err := tx.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
problem.Name,
|
||||
problem.Description,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
|
||||
}
|
||||
for _, tgd := range testGroupData {
|
||||
query := tx.Rebind(`
|
||||
INSERT INTO testgroups
|
||||
(problem_id,testing_strategy)
|
||||
VALUES ((select last_value from problems_id_seq),?)
|
||||
RETURNING id
|
||||
`)
|
||||
rows, err = tx.QueryxContext(ctx, query, tgd.Ts)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
|
||||
}
|
||||
var i int32 = 0
|
||||
for ; i < tgd.TestAmount; i++ {
|
||||
query := tx.Rebind(`
|
||||
INSERT INTO tests
|
||||
(testgroup_id)
|
||||
VALUES ((select last_value from testgroups_id_seq))
|
||||
RETURNING id
|
||||
`)
|
||||
rows, err = tx.QueryxContext(ctx, query, tgd.Ts)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
|
||||
}
|
||||
}
|
||||
}
|
||||
err = tx.Commit()
|
||||
//add test saving
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
err = rows.StructScan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (storage *ProblemStorage) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
var problem models.Problem
|
||||
query := storage.db.Rebind("SELECT * from problems WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &problem, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &problem, nil
|
||||
}
|
||||
|
||||
func (storage *ProblemStorage) UpdateProblem(ctx context.Context, problem *models.Problem) error {
|
||||
query := storage.db.Rebind("UPDATE problems SET name=?,description=?,time_limit=?,memory_limit=? WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, problem.Name, problem.Description, problem.TimeLimit, problem.MemoryLimit, problem.Id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *ProblemStorage) DeleteProblem(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM problems WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SolutionStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewSolutionStorage(db *sqlx.DB, logger *zap.Logger) *SolutionStorage {
|
||||
return &SolutionStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: testing graph
|
||||
|
||||
func (storage *SolutionStorage) updateResult(ctx context.Context, participantId int32, taskId int32) error {
|
||||
tx, err := storage.db.Beginx()
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
query := tx.Rebind("UPDATE participant_subtask AS ps SET best_score = (SELECT COALESCE(max(score),0) FROM subtaskruns WHERE subtask_id = ps.subtask_id AND solution_id IN (SELECT id FROM solutions WHERE participant_id=ps.participant_id)) WHERE participant_id = 2 AND subtask_id IN (SELECT id FROM subtasks WHERE task_id = 2)")
|
||||
tx.QueryxContext(ctx, query, participantId, taskId)
|
||||
query = tx.Rebind("UPDATE participant_task SET best_score = (select max(best_score) from participant_subtask WHERE participant_id = ? AND subtask_id IN (SELECT id FROM subtask WHERE task_id = ?)) WHERE participant_id = ? AND task_id = ?")
|
||||
tx.QueryxContext(ctx, query, participantId, taskId, participantId, taskId)
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *SolutionStorage) CreateSolution(ctx context.Context, solution *models.Solution) (int32, error) {
|
||||
query := storage.db.Rebind(`
|
||||
INSERT INTO solutions
|
||||
(participant_id,task_id,language_id,solution_hash,result)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`)
|
||||
|
||||
rows, err := storage.db.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
solution.ParticipantId,
|
||||
solution.TaskId,
|
||||
solution.LanguageId,
|
||||
"", //FIXME
|
||||
models.NotTested,
|
||||
)
|
||||
//TODO: add testing tree
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
err = rows.StructScan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (storage *SolutionStorage) ReadSolutionById(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
var solution models.Solution
|
||||
query := storage.db.Rebind("SELECT * from solutions WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &solution, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &solution, nil
|
||||
}
|
||||
|
||||
func (storage *SolutionStorage) RejudgeSolution(ctx context.Context, id int32) error {
|
||||
tx, err := storage.db.Beginx()
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
query := tx.Rebind("UPDATE solutions SET result = ? WHERE id = ?")
|
||||
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
|
||||
query = tx.Rebind("UPDATE subtaskruns SET result = ?,score = 0 WHERE solution_id = ?")
|
||||
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
|
||||
query = tx.Rebind("UPDATE testruns SET result = ?, score = 0 WHERE testgrouprun_id IN (SELECT id FROM tesgrouprun WHERE solution_id = ?)")
|
||||
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
|
||||
err = tx.Commit()
|
||||
var solution models.Solution
|
||||
query = storage.db.Rebind("SELECT * from solutions WHERE id=? LIMIT 1")
|
||||
err = storage.db.GetContext(ctx, &solution, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
storage.updateResult(ctx, *solution.ParticipantId, *solution.TaskId) // FIXME
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *SolutionStorage) DeleteSolution(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM solutions WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TaskStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTaskStorage(db *sqlx.DB, logger *zap.Logger) *TaskStorage {
|
||||
return &TaskStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) CreateTask(ctx context.Context, task *models.Task) (int32, error) {
|
||||
query := storage.db.Rebind(`
|
||||
INSERT INTO tasks
|
||||
(contest_id,problem_id,position,position_name)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`)
|
||||
|
||||
rows, err := storage.db.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
task.ContestId,
|
||||
task.ProblemId,
|
||||
task.Position,
|
||||
task.PositionName,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
err = rows.StructScan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) ReadTaskById(ctx context.Context, id int32) (*models.Task, error) {
|
||||
var task models.Task
|
||||
query := storage.db.Rebind("SELECT * from tasks WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &task, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) UpdateTask(ctx context.Context, id int32, task models.Task) error {
|
||||
query := storage.db.Rebind("UPDATE tasks SET position=?,position_name=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, task.Position, task.PositionName)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) DeleteTask(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM tasks WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type UserStorage struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewUserStorage(db *sqlx.DB) *UserStorage {
|
||||
return &UserStorage{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *UserStorage) CreateUser(ctx context.Context, user *models.User) error {
|
||||
query := storage.db.Rebind("INSERT INTO users (user_id, role) VALUES (?, ?)")
|
||||
_, err := storage.db.ExecContext(ctx, query, user.UserId, user.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *UserStorage) ReadUserById(ctx context.Context, userId int32) (*models.User, error) {
|
||||
query := storage.db.Rebind(`
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE user_id = ?
|
||||
LIMIT 1;
|
||||
`)
|
||||
|
||||
var user models.User
|
||||
err := storage.db.GetContext(ctx, &user, query, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
30
internal/tester/delivery.go
Normal file
30
internal/tester/delivery.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package tester
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers interface {
|
||||
ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error
|
||||
CreateContest(c *fiber.Ctx) error
|
||||
DeleteContest(c *fiber.Ctx, id int32) error
|
||||
GetContest(c *fiber.Ctx, id int32) error
|
||||
UpdateContest(c *fiber.Ctx, id int32) error
|
||||
DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error
|
||||
ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error
|
||||
UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error
|
||||
AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error
|
||||
ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error
|
||||
CreateProblem(c *fiber.Ctx) error
|
||||
DeleteProblem(c *fiber.Ctx, id int32) error
|
||||
GetProblem(c *fiber.Ctx, id int32) error
|
||||
UpdateProblem(c *fiber.Ctx, id int32) error
|
||||
ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error
|
||||
CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error
|
||||
GetSolution(c *fiber.Ctx, id int32) error
|
||||
DeleteTask(c *fiber.Ctx, id int32) error
|
||||
AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error
|
||||
GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error
|
||||
GetTask(c *fiber.Ctx, id int32) error
|
||||
}
|
605
internal/tester/delivery/rest/handlers.go
Normal file
605
internal/tester/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,605 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"io"
|
||||
)
|
||||
|
||||
type TesterHandlers struct {
|
||||
problemsUC tester.ProblemUseCase
|
||||
contestsUC tester.ContestUseCase
|
||||
}
|
||||
|
||||
func NewTesterHandlers(problemsUC tester.ProblemUseCase, contestsUC tester.ContestUseCase) *TesterHandlers {
|
||||
return &TesterHandlers{
|
||||
problemsUC: problemsUC,
|
||||
contestsUC: contestsUC,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error {
|
||||
contestsList, err := h.contestsUC.ListContests(c.Context(), models.ContestsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListContestsResponse{
|
||||
Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)),
|
||||
Pagination: P2P(contestsList.Pagination),
|
||||
}
|
||||
|
||||
for i, contest := range contestsList.Contests {
|
||||
resp.Contests[i] = CLI2CLI(*contest)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error {
|
||||
problemsList, err := h.problemsUC.ListProblems(c.Context(), models.ProblemsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListProblemsResponse{
|
||||
Problems: make([]testerv1.ProblemsListItem, len(problemsList.Problems)),
|
||||
Pagination: P2P(problemsList.Pagination),
|
||||
}
|
||||
|
||||
for i, problem := range problemsList.Problems {
|
||||
resp.Problems[i] = PLI2PLI(*problem)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) CreateContest(c *fiber.Ctx) error {
|
||||
id, err := h.contestsUC.CreateContest(c.Context(), "Название контеста")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateContestResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteContest(c *fiber.Ctx, id int32) error {
|
||||
err := h.contestsUC.DeleteContest(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error {
|
||||
contest, err := h.contestsUC.ReadContestById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
//token, ok := c.Locals(TokenKey).(*models.JWT)
|
||||
//if !ok {
|
||||
// return c.SendStatus(fiber.StatusUnauthorized)
|
||||
//}
|
||||
|
||||
tasks, err := h.contestsUC.ReadTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
var participantId int32 = 2
|
||||
|
||||
solutions, err := h.contestsUC.ReadBestSolutions(c.Context(), id, participantId)
|
||||
|
||||
m := make(map[int32]*models.Solution)
|
||||
|
||||
for i := 0; i < len(solutions); i++ {
|
||||
m[solutions[i].TaskPosition] = solutions[i]
|
||||
}
|
||||
resp := testerv1.GetContestResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]struct {
|
||||
Solution testerv1.Solution `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}, len(tasks)),
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
solution := testerv1.Solution{}
|
||||
if sol, ok := m[task.Position]; ok {
|
||||
solution = S2S(*sol)
|
||||
}
|
||||
resp.Tasks[i] = struct {
|
||||
Solution testerv1.Solution `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}{
|
||||
Solution: solution,
|
||||
Task: TLI2TLI(*task),
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error {
|
||||
err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error {
|
||||
id, err := h.contestsUC.AddParticipant(c.Context(), params.ContestId, params.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.AddParticipantResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteTask(c *fiber.Ctx, id int32) error {
|
||||
err := h.contestsUC.DeleteTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error {
|
||||
id, err := h.contestsUC.AddTask(c.Context(), params.ContestId, params.ProblemId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.AddTaskResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) CreateProblem(c *fiber.Ctx) error {
|
||||
id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateProblemResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteProblem(c *fiber.Ctx, id int32) error {
|
||||
err := h.problemsUC.DeleteProblem(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetProblem(c *fiber.Ctx, id int32) error {
|
||||
problem, err := h.problemsUC.ReadProblemById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(
|
||||
testerv1.GetProblemResponse{Problem: *PR2PR(problem)},
|
||||
)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error {
|
||||
participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ContestId: params.ContestId,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListParticipantsResponse{
|
||||
Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)),
|
||||
Pagination: P2P(participantsList.Pagination),
|
||||
}
|
||||
|
||||
for i, participant := range participantsList.Participants {
|
||||
resp.Participants[i] = PTLI2PTLI(*participant)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UpdateProblem(c *fiber.Ctx, id int32) error {
|
||||
var req testerv1.UpdateProblemRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.problemsUC.UpdateProblem(c.Context(), id, models.ProblemUpdate{
|
||||
Title: req.Title,
|
||||
MemoryLimit: req.MemoryLimit,
|
||||
TimeLimit: req.TimeLimit,
|
||||
|
||||
Legend: req.Legend,
|
||||
InputFormat: req.InputFormat,
|
||||
OutputFormat: req.OutputFormat,
|
||||
Notes: req.Notes,
|
||||
Scoring: req.Scoring,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UpdateContest(c *fiber.Ctx, id int32) error {
|
||||
var req testerv1.UpdateContestRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateContest(c.Context(), id, models.ContestUpdate{
|
||||
Title: req.Title,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error {
|
||||
var req testerv1.UpdateParticipantRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateParticipant(c.Context(), params.ParticipantId, models.ParticipantUpdate{
|
||||
Name: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
|
||||
solutionsList, err := h.contestsUC.ListSolutions(c.Context(), models.SolutionsFilter{
|
||||
ContestId: params.ContestId,
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ParticipantId: params.ParticipantId,
|
||||
TaskId: params.TaskId,
|
||||
Language: params.Language,
|
||||
Order: params.Order,
|
||||
State: params.State,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListSolutionsResponse{
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)),
|
||||
Pagination: P2P(solutionsList.Pagination),
|
||||
}
|
||||
|
||||
for i, solution := range solutionsList.Solutions {
|
||||
resp.Solutions[i] = SLI2SLI(*solution)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
const (
|
||||
maxSolutionSize int64 = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (h *TesterHandlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error {
|
||||
s, err := c.FormFile("solution")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Size == 0 || s.Size > maxSolutionSize {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
f, err := s.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := h.contestsUC.CreateSolution(c.Context(), &models.SolutionCreation{
|
||||
TaskId: params.TaskId,
|
||||
ParticipantId: 1,
|
||||
Language: params.Language,
|
||||
Penalty: 0,
|
||||
Solution: string(b),
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateSolutionResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetSolution(c *fiber.Ctx, id int32) error {
|
||||
solution, err := h.contestsUC.ReadSolution(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(
|
||||
testerv1.GetSolutionResponse{Solution: S2S(*solution)},
|
||||
)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetTask(c *fiber.Ctx, id int32) error {
|
||||
contest, err := h.contestsUC.ReadContestById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.ReadTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
t, err := h.contestsUC.ReadTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.GetTaskResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Task: *T2T(t),
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
resp.Tasks[i] = TLI2TLI(*task)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error {
|
||||
contest, err := h.contestsUC.ReadContestById(c.Context(), params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
monitor, err := h.contestsUC.ReadMonitor(c.Context(), params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.ReadTasks(c.Context(), params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.GetMonitorResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)),
|
||||
SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)),
|
||||
}
|
||||
|
||||
for i, participant := range monitor.Participants {
|
||||
resp.Participants[i] = testerv1.ParticipantsStat{
|
||||
Id: participant.Id,
|
||||
Name: participant.Name,
|
||||
PenaltyInTotal: participant.PenaltyInTotal,
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)),
|
||||
SolvedInTotal: participant.SolvedInTotal,
|
||||
}
|
||||
|
||||
for j, solution := range participant.Solutions {
|
||||
resp.Participants[i].Solutions[j] = SLI2SLI(*solution)
|
||||
}
|
||||
}
|
||||
|
||||
for i, problem := range monitor.Summary {
|
||||
resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{
|
||||
Id: problem.Id,
|
||||
Success: problem.Success,
|
||||
Total: problem.Total,
|
||||
}
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
resp.Tasks[i] = TLI2TLI(*task)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func P2P(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
func C2C(c models.Contest) testerv1.Contest {
|
||||
return testerv1.Contest{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func CLI2CLI(c models.ContestsListItem) testerv1.ContestsListItem {
|
||||
return testerv1.ContestsListItem{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PLI2PLI(p models.ProblemsListItem) testerv1.ProblemsListItem {
|
||||
return testerv1.ProblemsListItem{
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
MemoryLimit: p.MemoryLimit,
|
||||
TimeLimit: p.TimeLimit,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
SolvedCount: p.SolvedCount,
|
||||
}
|
||||
}
|
||||
|
||||
func TLI2TLI(t models.TasksListItem) testerv1.TasksListItem {
|
||||
return testerv1.TasksListItem{
|
||||
Id: t.Id,
|
||||
Position: t.Position,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
ProblemId: t.ProblemId,
|
||||
TimeLimit: t.TimeLimit,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func T2T(t *models.Task) *testerv1.Task {
|
||||
return &testerv1.Task{
|
||||
Id: t.Id,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
TimeLimit: t.TimeLimit,
|
||||
|
||||
InputFormatHtml: t.InputFormatHtml,
|
||||
LegendHtml: t.LegendHtml,
|
||||
NotesHtml: t.NotesHtml,
|
||||
OutputFormatHtml: t.OutputFormatHtml,
|
||||
Position: t.Position,
|
||||
ScoringHtml: t.ScoringHtml,
|
||||
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PR2PR(p *models.Problem) *testerv1.Problem {
|
||||
return &testerv1.Problem{
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
TimeLimit: p.TimeLimit,
|
||||
MemoryLimit: p.MemoryLimit,
|
||||
|
||||
Legend: p.Legend,
|
||||
InputFormat: p.InputFormat,
|
||||
OutputFormat: p.OutputFormat,
|
||||
Notes: p.Notes,
|
||||
Scoring: p.Scoring,
|
||||
|
||||
LegendHtml: p.LegendHtml,
|
||||
InputFormatHtml: p.InputFormatHtml,
|
||||
OutputFormatHtml: p.OutputFormatHtml,
|
||||
NotesHtml: p.NotesHtml,
|
||||
ScoringHtml: p.ScoringHtml,
|
||||
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PTLI2PTLI(p models.ParticipantsListItem) testerv1.ParticipantsListItem {
|
||||
return testerv1.ParticipantsListItem{
|
||||
Id: p.Id,
|
||||
UserId: p.UserId,
|
||||
Name: p.Name,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func SLI2SLI(s models.SolutionsListItem) testerv1.SolutionsListItem {
|
||||
return testerv1.SolutionsListItem{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func S2S(s models.Solution) testerv1.Solution {
|
||||
return testerv1.Solution{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
Solution: s.Solution,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
71
internal/tester/delivery/rest/middlewares.go
Normal file
71
internal/tester/delivery/rest/middlewares.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenKey = "token"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtSecret string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
const op = "AuthMiddleware"
|
||||
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
authParts := strings.Split(authHeader, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(authParts[1], &models.JWT{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
token, ok := parsedToken.Claims.(*models.JWT)
|
||||
if !ok {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
err = token.Valid()
|
||||
if err != nil {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
//ctx := c.Context()
|
||||
|
||||
// check if session exists
|
||||
//_, err = userUC.ReadSession(ctx, token.SessionId)
|
||||
//if err != nil {
|
||||
// if errors.Is(err, pkg.ErrNotFound) {
|
||||
// c.Locals(TokenKey, nil)
|
||||
// return c.Next()
|
||||
// }
|
||||
//
|
||||
// return c.SendStatus(pkg.ToREST(err))
|
||||
//}
|
||||
|
||||
c.Locals(TokenKey, token)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
53
internal/tester/pg_repository.go
Normal file
53
internal/tester/pg_repository.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package tester
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
Rebind(query string) string
|
||||
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
|
||||
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
}
|
||||
|
||||
type Tx interface {
|
||||
Querier
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
type ProblemPostgresRepository interface {
|
||||
BeginTx(ctx context.Context) (Tx, error)
|
||||
DB() Querier
|
||||
CreateProblem(ctx context.Context, q Querier, title string) (int32, error)
|
||||
ReadProblemById(ctx context.Context, q Querier, id int32) (*models.Problem, error)
|
||||
DeleteProblem(ctx context.Context, q Querier, id int32) error
|
||||
ListProblems(ctx context.Context, q Querier, filter models.ProblemsFilter) (*models.ProblemsList, error)
|
||||
UpdateProblem(ctx context.Context, q Querier, id int32, heading models.ProblemUpdate) error
|
||||
}
|
||||
|
||||
type ContestRepository interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
ReadContestById(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
AddTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
ReadSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
ReadTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error)
|
||||
}
|
27
internal/tester/repository/error.go
Normal file
27
internal/tester/repository/error.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
func handlePgErr(err error, op string) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
|
||||
return pkg.Wrap(pkg.ErrBadInput, err, op, pgErr.Message)
|
||||
}
|
||||
if pgerrcode.IsNoData(pgErr.Code) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, pgErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "no rows found")
|
||||
}
|
||||
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unexpected error")
|
||||
}
|
690
internal/tester/repository/pg_contests_repository.go
Normal file
690
internal/tester/repository/pg_contests_repository.go
Normal file
|
@ -0,0 +1,690 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ContestRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewContestRepository(db *sqlx.DB) *ContestRepository {
|
||||
return &ContestRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const createContestQuery = "INSERT INTO contests (title) VALUES (?) RETURNING id"
|
||||
|
||||
func (r *ContestRepository) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
const op = "ContestRepository.CreateContest"
|
||||
|
||||
query := r.db.Rebind(createContestQuery)
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, query, title)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const readContestByIdQuery = "SELECT * from contests WHERE id=? LIMIT 1"
|
||||
|
||||
func (r *ContestRepository) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
const op = "ContestRepository.ReadContestById"
|
||||
|
||||
var contest models.Contest
|
||||
query := r.db.Rebind(readContestByIdQuery)
|
||||
err := r.db.GetContext(ctx, &contest, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
return &contest, nil
|
||||
}
|
||||
|
||||
const deleteContestQuery = "DELETE FROM contests WHERE id=?"
|
||||
|
||||
func (r *ContestRepository) DeleteContest(ctx context.Context, id int32) error {
|
||||
const op = "ContestRepository.DeleteContest"
|
||||
|
||||
query := r.db.Rebind(deleteContestQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const addTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
|
||||
VALUES (?, ?, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = ?), 0) + 1)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
func (r *ContestRepository) AddTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
|
||||
const op = "ContestRepository.AddTask"
|
||||
|
||||
query := r.db.Rebind(addTaskQuery)
|
||||
rows, err := r.db.QueryxContext(ctx, query, problemId, contestId, contestId)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const deleteTaskQuery = "DELETE FROM tasks WHERE id=?"
|
||||
|
||||
func (r *ContestRepository) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
const op = "ContestRepository.DeleteTask"
|
||||
|
||||
query := r.db.Rebind(deleteTaskQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, taskId)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const addParticipantQuery = "INSERT INTO participants (user_id ,contest_id, name) VALUES (?, ?, ?) RETURNING id"
|
||||
|
||||
func (r *ContestRepository) AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
const op = "ContestRepository.AddParticipant"
|
||||
|
||||
query := r.db.Rebind(addParticipantQuery)
|
||||
name := ""
|
||||
rows, err := r.db.QueryxContext(ctx, query, contestId, userId, name)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const deleteParticipantQuery = "DELETE FROM participants WHERE id=?"
|
||||
|
||||
func (r *ContestRepository) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
const op = "ContestRepository.DeleteParticipant"
|
||||
|
||||
query := r.db.Rebind(deleteParticipantQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, participantId)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const readTasksQuery = `SELECT tasks.id,
|
||||
problem_id,
|
||||
contest_id,
|
||||
position,
|
||||
title,
|
||||
memory_limit,
|
||||
time_limit,
|
||||
tasks.created_at,
|
||||
tasks.updated_at
|
||||
FROM tasks
|
||||
INNER JOIN problems ON tasks.problem_id = problems.id
|
||||
WHERE contest_id = ? ORDER BY position`
|
||||
|
||||
func (r *ContestRepository) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
const op = "ContestRepository.ReadTasks"
|
||||
|
||||
var tasks []*models.TasksListItem
|
||||
query := r.db.Rebind(readTasksQuery)
|
||||
err := r.db.SelectContext(ctx, &tasks, query, contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
const (
|
||||
readContestsListQuery = `SELECT id, title, created_at, updated_at FROM contests LIMIT ? OFFSET ?`
|
||||
countContestsQuery = "SELECT COUNT(*) FROM contests"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
const op = "ContestRepository.ReadTasks"
|
||||
|
||||
var contests []*models.ContestsListItem
|
||||
query := r.db.Rebind(readContestsListQuery)
|
||||
err := r.db.SelectContext(ctx, &contests, query, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = r.db.Rebind(countContestsQuery)
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, query)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ContestsList{
|
||||
Contests: contests,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
readParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = ? LIMIT ? OFFSET ?`
|
||||
countParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
const op = "ContestRepository.ReadParticipants"
|
||||
|
||||
if filter.PageSize > 20 {
|
||||
filter.PageSize = 1
|
||||
}
|
||||
|
||||
var participants []*models.ParticipantsListItem
|
||||
query := r.db.Rebind(readParticipantsListQuery)
|
||||
err := r.db.SelectContext(ctx, &participants, query, filter.ContestId, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = r.db.Rebind(countParticipantsQuery)
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, query, filter.ContestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ParticipantsList{
|
||||
Participants: participants,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateContestQuery = "UPDATE contests SET title = COALESCE(?, title) WHERE id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
const op = "ContestRepository.UpdateContest"
|
||||
|
||||
query := r.db.Rebind(updateContestQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, contestUpdate.Title, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateParticipantQuery = "UPDATE participants SET name = COALESCE(?, name) WHERE id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
const op = "ContestRepository.UpdateParticipant"
|
||||
|
||||
query := r.db.Rebind(updateParticipantQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, participantUpdate.Name, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
readSolutionQuery = "SELECT * FROM solutions WHERE id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
const op = "ContestRepository.ReadSolution"
|
||||
|
||||
query := r.db.Rebind(readSolutionQuery)
|
||||
var solution models.Solution
|
||||
err := r.db.GetContext(ctx, &solution, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &solution, nil
|
||||
}
|
||||
|
||||
const (
|
||||
createSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
const op = "ContestRepository.CreateSolution"
|
||||
|
||||
query := r.db.Rebind(createSolutionQuery)
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx,
|
||||
query,
|
||||
creation.TaskId,
|
||||
creation.ParticipantId,
|
||||
creation.Language,
|
||||
creation.Penalty,
|
||||
creation.Solution,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *ContestRepository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
const op = "ContestRepository.ListSolutions"
|
||||
|
||||
baseQuery := `
|
||||
SELECT s.id,
|
||||
|
||||
s.participant_id,
|
||||
p2.name as participant_name,
|
||||
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
|
||||
s.task_id,
|
||||
t.position as task_position,
|
||||
p.title as task_title,
|
||||
|
||||
t.contest_id,
|
||||
c.title,
|
||||
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM solutions s
|
||||
LEFT JOIN tasks t ON s.task_id = t.id
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
LEFT JOIN contests c ON t.contest_id = c.id
|
||||
LEFT JOIN participants p2 on s.participant_id = p2.id
|
||||
WHERE 1=1
|
||||
`
|
||||
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if filter.ContestId != nil {
|
||||
conditions = append(conditions, "s.contest_id = ?")
|
||||
args = append(args, *filter.ContestId)
|
||||
}
|
||||
if filter.ParticipantId != nil {
|
||||
conditions = append(conditions, "s.participant_id = ?")
|
||||
args = append(args, *filter.ParticipantId)
|
||||
}
|
||||
if filter.TaskId != nil {
|
||||
conditions = append(conditions, "s.task_id = ?")
|
||||
args = append(args, *filter.TaskId)
|
||||
}
|
||||
if filter.Language != nil {
|
||||
conditions = append(conditions, "s.language = ?")
|
||||
args = append(args, *filter.Language)
|
||||
}
|
||||
if filter.State != nil {
|
||||
conditions = append(conditions, "s.state = ?")
|
||||
args = append(args, *filter.State)
|
||||
}
|
||||
|
||||
if len(conditions) > 0 {
|
||||
baseQuery += " AND " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
if filter.Order != nil {
|
||||
orderDirection := "ASC"
|
||||
if *filter.Order < 0 {
|
||||
orderDirection = "DESC"
|
||||
}
|
||||
baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection)
|
||||
}
|
||||
|
||||
countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table"
|
||||
var totalCount int32
|
||||
err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
baseQuery += " LIMIT ? OFFSET ?"
|
||||
args = append(args, filter.PageSize, offset)
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
for rows.Next() {
|
||||
var solution models.SolutionsListItem
|
||||
err = rows.StructScan(&solution)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
solutions = append(solutions, &solution)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.SolutionsList{
|
||||
Solutions: solutions,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(totalCount, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
readTaskQuery = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.position,
|
||||
p.title,
|
||||
p.time_limit,
|
||||
p.memory_limit,
|
||||
t.problem_id,
|
||||
t.contest_id,
|
||||
p.legend_html,
|
||||
p.input_format_html,
|
||||
p.output_format_html,
|
||||
p.notes_html,
|
||||
p.scoring_html,
|
||||
t.created_at,
|
||||
t.updated_at
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
WHERE t.id = ?
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
const op = "ContestRepository.ReadTask"
|
||||
|
||||
query := r.db.Rebind(readTaskQuery)
|
||||
var task models.Task
|
||||
err := r.db.GetContext(ctx, &task, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
readStatisticsQuery = `
|
||||
SELECT t.id as task_id,
|
||||
t.position,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN s.state = 5 THEN 1 END) as success
|
||||
FROM tasks t
|
||||
LEFT JOIN solutions s ON t.id = s.task_id
|
||||
WHERE t.contest_id = ?
|
||||
GROUP BY t.id, t.position
|
||||
ORDER BY t.position;
|
||||
`
|
||||
|
||||
solutionsQuery = `
|
||||
WITH RankedSolutions AS (
|
||||
SELECT
|
||||
s.id,
|
||||
|
||||
s.participant_id,
|
||||
p2.name as participant_name,
|
||||
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
|
||||
s.task_id,
|
||||
t.position as task_position,
|
||||
p.title as task_title,
|
||||
|
||||
t.contest_id,
|
||||
c.title as contest_title,
|
||||
|
||||
s.updated_at,
|
||||
s.created_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.participant_id, s.task_id
|
||||
ORDER BY
|
||||
CASE WHEN s.state = 5 THEN 0 ELSE 1 END,
|
||||
s.created_at
|
||||
) as rn
|
||||
FROM solutions s
|
||||
LEFT JOIN tasks t ON s.task_id = t.id
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
LEFT JOIN contests c ON t.contest_id = c.id
|
||||
LEFT JOIN participants p2 on s.participant_id = p2.id
|
||||
WHERE t.contest_id = ?
|
||||
)
|
||||
SELECT
|
||||
rs.id,
|
||||
|
||||
rs.participant_id,
|
||||
rs.participant_name,
|
||||
|
||||
rs.state,
|
||||
rs.score,
|
||||
rs.penalty,
|
||||
rs.time_stat,
|
||||
rs.memory_stat,
|
||||
rs.language,
|
||||
|
||||
rs.task_id,
|
||||
rs.task_position,
|
||||
rs.task_title,
|
||||
|
||||
rs.contest_id,
|
||||
rs.contest_title,
|
||||
|
||||
rs.updated_at,
|
||||
rs.created_at
|
||||
FROM RankedSolutions rs
|
||||
WHERE rs.rn = 1;
|
||||
|
||||
`
|
||||
|
||||
participantsQuery = `
|
||||
WITH Attempts AS (
|
||||
SELECT
|
||||
s.participant_id,
|
||||
s.task_id,
|
||||
COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < (
|
||||
SELECT MIN(s2.created_at)
|
||||
FROM solutions s2
|
||||
WHERE s2.participant_id = s.participant_id
|
||||
AND s2.task_id = s.task_id
|
||||
AND s2.state = 5
|
||||
)) as failed_attempts,
|
||||
MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty
|
||||
FROM solutions s
|
||||
JOIN tasks t ON t.id = s.task_id
|
||||
WHERE t.contest_id = :contest_id
|
||||
GROUP BY s.participant_id, s.task_id
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total,
|
||||
COALESCE(SUM(CASE WHEN a.success_penalty IS NOT NULL
|
||||
THEN a.failed_attempts * :penalty + a.success_penalty
|
||||
ELSE 0 END), 0) as penalty_in_total
|
||||
FROM participants p
|
||||
LEFT JOIN Attempts a ON a.participant_id = p.id
|
||||
WHERE p.contest_id = :contest_id
|
||||
GROUP BY p.id, p.name
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
const op = "ContestRepository.ReadMonitor"
|
||||
|
||||
query := r.db.Rebind(readStatisticsQuery)
|
||||
rows, err := r.db.QueryxContext(ctx, query, contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var monitor models.Monitor
|
||||
for rows.Next() {
|
||||
var stat models.ProblemStatSummary
|
||||
err = rows.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
monitor.Summary = append(monitor.Summary, &stat)
|
||||
}
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
err = r.db.SelectContext(ctx, &solutions, r.db.Rebind(solutionsQuery), contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
penalty := int32(20) // FIXME
|
||||
namedQuery := r.db.Rebind(participantsQuery)
|
||||
rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{
|
||||
"contest_id": contestId,
|
||||
"penalty": penalty,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows3.Close()
|
||||
|
||||
solutionsMap := make(map[int32][]*models.SolutionsListItem)
|
||||
for _, solution := range solutions {
|
||||
solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution)
|
||||
}
|
||||
|
||||
for rows3.Next() {
|
||||
var stat models.ParticipantsStat
|
||||
err = rows3.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
if sols, ok := solutionsMap[stat.Id]; ok {
|
||||
stat.Solutions = sols
|
||||
}
|
||||
|
||||
monitor.Participants = append(monitor.Participants, &stat)
|
||||
}
|
||||
|
||||
return &monitor, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
readBestSolutions = `
|
||||
WITH contest_tasks AS (
|
||||
SELECT t.id AS task_id,
|
||||
t.position AS task_position,
|
||||
t.contest_id,
|
||||
t.problem_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
p.title AS task_title,
|
||||
c.title AS contest_title
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON p.id = t.problem_id
|
||||
LEFT JOIN contests c ON c.id = t.contest_id
|
||||
WHERE t.contest_id = ?
|
||||
),
|
||||
best_solutions AS (
|
||||
SELECT DISTINCT ON (s.task_id)
|
||||
*
|
||||
FROM solutions s
|
||||
WHERE s.participant_id = ?
|
||||
ORDER BY s.task_id, s.score DESC, s.created_at DESC
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.participant_id,
|
||||
p.name AS participant_name,
|
||||
s.solution,
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
ct.task_id,
|
||||
ct.task_position,
|
||||
ct.task_title,
|
||||
ct.contest_id,
|
||||
ct.contest_title,
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM contest_tasks ct
|
||||
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
|
||||
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
|
||||
ORDER BY ct.task_position
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) {
|
||||
const op = "ContestRepository.ReadBestSolutions"
|
||||
var solutions []*models.Solution
|
||||
query := r.db.Rebind(readBestSolutions)
|
||||
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
|
||||
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return solutions, nil
|
||||
}
|
154
internal/tester/repository/pg_contests_repository_test.go
Normal file
154
internal/tester/repository/pg_contests_repository_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContestRepository_CreateContest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
contestRepo := NewContestRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid contest creation", func(t *testing.T) {
|
||||
title := "Contest title"
|
||||
|
||||
rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
|
||||
|
||||
mock.ExpectQuery(sqlxDB.Rebind(createContestQuery)).WithArgs(title).WillReturnRows(rows)
|
||||
|
||||
id, err := contestRepo.CreateContest(context.Background(), title)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContestRepository_DeleteContest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
contestRepo := NewContestRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid contest deletion", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
rows := sqlmock.NewResult(1, 1)
|
||||
|
||||
mock.ExpectExec(sqlxDB.Rebind(deleteContestQuery)).WithArgs(id).WillReturnResult(rows)
|
||||
|
||||
err = contestRepo.DeleteContest(context.Background(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContestRepository_AddTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
contestRepo := NewContestRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid task additional", func(t *testing.T) {
|
||||
taskId := int32(1)
|
||||
contestId := int32(1)
|
||||
|
||||
rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
|
||||
|
||||
mock.ExpectQuery(sqlxDB.Rebind(addTaskQuery)).WithArgs(taskId, contestId, contestId).WillReturnRows(rows)
|
||||
|
||||
id, err := contestRepo.AddTask(context.Background(), contestId, taskId)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestContestRepository_DeleteTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
contestRepo := NewContestRepository(sqlxDB, zap.NewNop())
|
||||
t.Run("valid task deletion", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
rows := sqlmock.NewResult(1, 1)
|
||||
|
||||
mock.ExpectExec(sqlxDB.Rebind(deleteTaskQuery)).WithArgs(id).WillReturnResult(rows)
|
||||
|
||||
err = contestRepo.DeleteTask(context.Background(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContestRepository_AddParticipant(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
contestRepo := NewContestRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid participant addition", func(t *testing.T) {
|
||||
contestId := int32(1)
|
||||
userId := int32(1)
|
||||
name := ""
|
||||
|
||||
rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
|
||||
|
||||
mock.ExpectQuery(sqlxDB.Rebind(addParticipantQuery)).WithArgs(contestId, userId, name).WillReturnRows(rows)
|
||||
|
||||
id, err := contestRepo.AddParticipant(context.Background(), contestId, userId)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContestRepository_DeleteParticipant(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
contestRepo := NewContestRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid participant deletion", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
rows := sqlmock.NewResult(1, 1)
|
||||
|
||||
mock.ExpectExec(sqlxDB.Rebind(deleteParticipantQuery)).WithArgs(id).WillReturnResult(rows)
|
||||
|
||||
err = contestRepo.DeleteParticipant(context.Background(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
183
internal/tester/repository/pg_problems_repository.go
Normal file
183
internal/tester/repository/pg_problems_repository.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type ProblemRepository struct {
|
||||
_db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewProblemRepository(db *sqlx.DB) *ProblemRepository {
|
||||
return &ProblemRepository{
|
||||
_db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProblemRepository) BeginTx(ctx context.Context) (tester.Tx, error) {
|
||||
tx, err := r._db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (r *ProblemRepository) DB() tester.Querier {
|
||||
return r._db
|
||||
}
|
||||
|
||||
const createProblemQuery = "INSERT INTO problems (title) VALUES (?) RETURNING id"
|
||||
|
||||
func (r *ProblemRepository) CreateProblem(ctx context.Context, q tester.Querier, title string) (int32, error) {
|
||||
const op = "ProblemRepository.CreateProblem"
|
||||
|
||||
query := q.Rebind(createProblemQuery)
|
||||
rows, err := q.QueryxContext(ctx, query, title)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const readProblemQuery = "SELECT * from problems WHERE id=? LIMIT 1"
|
||||
|
||||
func (r *ProblemRepository) ReadProblemById(ctx context.Context, q tester.Querier, id int32) (*models.Problem, error) {
|
||||
const op = "ProblemRepository.ReadProblemById"
|
||||
|
||||
var problem models.Problem
|
||||
query := q.Rebind(readProblemQuery)
|
||||
err := q.GetContext(ctx, &problem, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &problem, nil
|
||||
}
|
||||
|
||||
const deleteProblemQuery = "DELETE FROM problems WHERE id=?"
|
||||
|
||||
func (r *ProblemRepository) DeleteProblem(ctx context.Context, q tester.Querier, id int32) error {
|
||||
const op = "ProblemRepository.DeleteProblem"
|
||||
|
||||
query := q.Rebind(deleteProblemQuery)
|
||||
_, err := q.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ListProblemsQuery = `
|
||||
SELECT
|
||||
p.id,p.title,p.memory_limit,p.time_limit,p.created_at,p.updated_at,
|
||||
COALESCE(solved_count, 0) AS solved_count
|
||||
FROM problems p
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
t.problem_id,
|
||||
COUNT(DISTINCT s.participant_id) AS solved_count
|
||||
FROM solutions s
|
||||
JOIN tasks t ON s.task_id = t.id
|
||||
WHERE s.state = 5
|
||||
GROUP BY t.problem_id
|
||||
) sol ON p.id = sol.problem_id
|
||||
LIMIT ? OFFSET ?`
|
||||
CountProblemsQuery = "SELECT COUNT(*) FROM problems"
|
||||
)
|
||||
|
||||
func (r *ProblemRepository) ListProblems(ctx context.Context, q tester.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
const op = "ContestRepository.ListProblems"
|
||||
|
||||
if filter.PageSize > 20 || filter.PageSize < 1 {
|
||||
filter.PageSize = 1
|
||||
}
|
||||
|
||||
var problems []*models.ProblemsListItem
|
||||
query := q.Rebind(ListProblemsQuery)
|
||||
err := q.SelectContext(ctx, &problems, query, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = q.Rebind(CountProblemsQuery)
|
||||
|
||||
var count int32
|
||||
err = q.GetContext(ctx, &count, query)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ProblemsList{
|
||||
Problems: problems,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UpdateProblemQuery = `UPDATE problems
|
||||
SET title = COALESCE(?, title),
|
||||
time_limit = COALESCE(?, time_limit),
|
||||
memory_limit = COALESCE(?, memory_limit),
|
||||
|
||||
legend = COALESCE(?, legend),
|
||||
input_format = COALESCE(?, input_format),
|
||||
output_format = COALESCE(?, output_format),
|
||||
notes = COALESCE(?, notes),
|
||||
scoring = COALESCE(?, scoring),
|
||||
|
||||
legend_html = COALESCE(?, legend_html),
|
||||
input_format_html = COALESCE(?, input_format_html),
|
||||
output_format_html = COALESCE(?, output_format_html),
|
||||
notes_html = COALESCE(?, notes_html),
|
||||
scoring_html = COALESCE(?, scoring_html)
|
||||
|
||||
WHERE id=?`
|
||||
)
|
||||
|
||||
func (r *ProblemRepository) UpdateProblem(ctx context.Context, q tester.Querier, id int32, problem models.ProblemUpdate) error {
|
||||
const op = "ProblemRepository.UpdateProblem"
|
||||
|
||||
query := q.Rebind(UpdateProblemQuery)
|
||||
_, err := q.ExecContext(ctx, query,
|
||||
problem.Title,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
|
||||
problem.Legend,
|
||||
problem.InputFormat,
|
||||
problem.OutputFormat,
|
||||
problem.Notes,
|
||||
problem.Scoring,
|
||||
|
||||
problem.LegendHtml,
|
||||
problem.InputFormatHtml,
|
||||
problem.OutputFormatHtml,
|
||||
problem.NotesHtml,
|
||||
problem.ScoringHtml,
|
||||
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
58
internal/tester/repository/pg_problems_repository_test.go
Normal file
58
internal/tester/repository/pg_problems_repository_test.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProblemRepository_CreateProblem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
problemRepo := NewProblemRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid problem creation", func(t *testing.T) {
|
||||
title := "Problem title"
|
||||
|
||||
rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
|
||||
|
||||
mock.ExpectQuery(sqlxDB.Rebind(createProblemQuery)).WithArgs(title).WillReturnRows(rows)
|
||||
|
||||
id, err := problemRepo.CreateProblem(context.Background(), title)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProblemRepository_DeleteProblem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
problemRepo := NewProblemRepository(sqlxDB, zap.NewNop())
|
||||
|
||||
t.Run("valid problem deletion", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
rows := sqlmock.NewResult(1, 1)
|
||||
|
||||
mock.ExpectExec(sqlxDB.Rebind(deleteProblemQuery)).WithArgs(id).WillReturnResult(rows)
|
||||
|
||||
err = problemRepo.DeleteProblem(context.Background(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
35
internal/tester/usecase.go
Normal file
35
internal/tester/usecase.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package tester
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ProblemUseCase interface {
|
||||
CreateProblem(ctx context.Context, title string) (int32, error)
|
||||
ReadProblemById(ctx context.Context, id int32) (*models.Problem, error)
|
||||
DeleteProblem(ctx context.Context, id int32) error
|
||||
ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error)
|
||||
UpdateProblem(ctx context.Context, id int32, problem models.ProblemUpdate) error
|
||||
}
|
||||
|
||||
type ContestUseCase interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
ReadContestById(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
AddTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
ReadSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
ReadTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error)
|
||||
}
|
91
internal/tester/usecase/contests_usecase.go
Normal file
91
internal/tester/usecase/contests_usecase.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
)
|
||||
|
||||
type ContestUseCase struct {
|
||||
contestRepo tester.ContestRepository
|
||||
}
|
||||
|
||||
func NewContestUseCase(
|
||||
contestRepo tester.ContestRepository,
|
||||
) *ContestUseCase {
|
||||
return &ContestUseCase{
|
||||
contestRepo: contestRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
return uc.contestRepo.CreateContest(ctx, title)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
return uc.contestRepo.ReadContestById(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error {
|
||||
return uc.contestRepo.DeleteContest(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) AddTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) {
|
||||
return uc.contestRepo.AddTask(ctx, contestId, taskId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
return uc.contestRepo.DeleteTask(ctx, taskId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) AddParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) {
|
||||
return uc.contestRepo.AddParticipant(ctx, contestId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
return uc.contestRepo.DeleteParticipant(ctx, participantId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
return uc.contestRepo.ReadTasks(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
return uc.contestRepo.ListContests(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
return uc.contestRepo.ListParticipants(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
return uc.contestRepo.UpdateContest(ctx, id, contestUpdate)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
return uc.contestRepo.ReadSolution(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
return uc.contestRepo.CreateSolution(ctx, creation)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
return uc.contestRepo.ListSolutions(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
return uc.contestRepo.ReadTask(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
return uc.contestRepo.ReadMonitor(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) {
|
||||
return uc.contestRepo.ReadBestSolutions(ctx, contestId, participantId)
|
||||
}
|
222
internal/tester/usecase/problems_usecase.go
Normal file
222
internal/tester/usecase/problems_usecase.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProblemUseCase struct {
|
||||
problemRepo tester.ProblemPostgresRepository
|
||||
pandocClient pkg.PandocClient
|
||||
}
|
||||
|
||||
func NewProblemUseCase(
|
||||
problemRepo tester.ProblemPostgresRepository,
|
||||
pandocClient pkg.PandocClient,
|
||||
) *ProblemUseCase {
|
||||
return &ProblemUseCase{
|
||||
problemRepo: problemRepo,
|
||||
pandocClient: pandocClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.ReadProblemById(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate models.ProblemUpdate) error {
|
||||
if isEmpty(problemUpdate) {
|
||||
return pkg.Wrap(pkg.ErrBadInput, nil, "UpdateProblem", "empty problem update")
|
||||
}
|
||||
|
||||
tx, err := u.problemRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
problem, err := u.problemRepo.ReadProblemById(ctx, tx, id)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
statement := models.ProblemStatement{
|
||||
Legend: problem.Legend,
|
||||
InputFormat: problem.InputFormat,
|
||||
OutputFormat: problem.OutputFormat,
|
||||
Notes: problem.Notes,
|
||||
Scoring: problem.Scoring,
|
||||
}
|
||||
|
||||
if problemUpdate.Legend != nil {
|
||||
statement.Legend = *problemUpdate.Legend
|
||||
}
|
||||
if problemUpdate.InputFormat != nil {
|
||||
statement.InputFormat = *problemUpdate.InputFormat
|
||||
}
|
||||
if problemUpdate.OutputFormat != nil {
|
||||
statement.OutputFormat = *problemUpdate.OutputFormat
|
||||
}
|
||||
if problemUpdate.Notes != nil {
|
||||
statement.Notes = *problemUpdate.Notes
|
||||
}
|
||||
if problemUpdate.Scoring != nil {
|
||||
statement.Scoring = *problemUpdate.Scoring
|
||||
}
|
||||
|
||||
builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement))
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
if builtStatement.LegendHtml != problem.LegendHtml {
|
||||
problemUpdate.LegendHtml = &builtStatement.LegendHtml
|
||||
}
|
||||
if builtStatement.InputFormatHtml != problem.InputFormatHtml {
|
||||
problemUpdate.InputFormatHtml = &builtStatement.InputFormatHtml
|
||||
}
|
||||
if builtStatement.OutputFormatHtml != problem.OutputFormatHtml {
|
||||
problemUpdate.OutputFormatHtml = &builtStatement.OutputFormatHtml
|
||||
}
|
||||
if builtStatement.NotesHtml != problem.NotesHtml {
|
||||
problemUpdate.NotesHtml = &builtStatement.NotesHtml
|
||||
}
|
||||
if builtStatement.ScoringHtml != problem.ScoringHtml {
|
||||
problemUpdate.ScoringHtml = &builtStatement.ScoringHtml
|
||||
}
|
||||
|
||||
err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isEmpty(p models.ProblemUpdate) bool {
|
||||
return p.Title == nil &&
|
||||
p.Legend == nil &&
|
||||
p.InputFormat == nil &&
|
||||
p.OutputFormat == nil &&
|
||||
p.Notes == nil &&
|
||||
p.Scoring == nil &&
|
||||
p.MemoryLimit == nil &&
|
||||
p.TimeLimit == nil
|
||||
}
|
||||
|
||||
func wrap(s string) string {
|
||||
return fmt.Sprintf("\\begin{document}\n%s\n\\end{document}\n", s)
|
||||
}
|
||||
|
||||
func trimSpaces(statement models.ProblemStatement) models.ProblemStatement {
|
||||
return models.ProblemStatement{
|
||||
Legend: strings.TrimSpace(statement.Legend),
|
||||
InputFormat: strings.TrimSpace(statement.InputFormat),
|
||||
OutputFormat: strings.TrimSpace(statement.OutputFormat),
|
||||
Notes: strings.TrimSpace(statement.Notes),
|
||||
Scoring: strings.TrimSpace(statement.Scoring),
|
||||
}
|
||||
}
|
||||
|
||||
func sanitize(statement models.Html5ProblemStatement) models.Html5ProblemStatement {
|
||||
p := bluemonday.UGCPolicy()
|
||||
|
||||
p.AllowAttrs("class").Globally()
|
||||
p.AllowAttrs("style").Globally()
|
||||
p.AllowStyles("text-align").MatchingEnum("center", "left", "right").Globally()
|
||||
p.AllowStyles("display").MatchingEnum("block", "inline", "inline-block").Globally()
|
||||
|
||||
p.AllowStandardURLs()
|
||||
p.AllowAttrs("cite").OnElements("blockquote", "q")
|
||||
p.AllowAttrs("href").OnElements("a", "area")
|
||||
p.AllowAttrs("src").OnElements("img")
|
||||
|
||||
if statement.LegendHtml != "" {
|
||||
statement.LegendHtml = p.Sanitize(statement.LegendHtml)
|
||||
}
|
||||
if statement.InputFormatHtml != "" {
|
||||
statement.InputFormatHtml = p.Sanitize(statement.InputFormatHtml)
|
||||
}
|
||||
if statement.OutputFormatHtml != "" {
|
||||
statement.OutputFormatHtml = p.Sanitize(statement.OutputFormatHtml)
|
||||
}
|
||||
if statement.NotesHtml != "" {
|
||||
statement.NotesHtml = p.Sanitize(statement.NotesHtml)
|
||||
}
|
||||
if statement.ScoringHtml != "" {
|
||||
statement.ScoringHtml = p.Sanitize(statement.ScoringHtml)
|
||||
}
|
||||
|
||||
return statement
|
||||
}
|
||||
|
||||
func build(ctx context.Context, pandocClient pkg.PandocClient, p models.ProblemStatement) (models.Html5ProblemStatement, error) {
|
||||
p = trimSpaces(p)
|
||||
|
||||
latex := models.ProblemStatement{}
|
||||
|
||||
if p.Legend != "" {
|
||||
latex.Legend = wrap(p.Legend)
|
||||
}
|
||||
if p.InputFormat != "" {
|
||||
latex.InputFormat = wrap(p.InputFormat)
|
||||
}
|
||||
if p.OutputFormat != "" {
|
||||
latex.OutputFormat = wrap(p.OutputFormat)
|
||||
}
|
||||
if p.Notes != "" {
|
||||
latex.Notes = wrap(p.Notes)
|
||||
}
|
||||
if p.Scoring != "" {
|
||||
latex.Scoring = wrap(p.Scoring)
|
||||
}
|
||||
|
||||
req := []string{
|
||||
latex.Legend,
|
||||
latex.InputFormat,
|
||||
latex.OutputFormat,
|
||||
latex.Notes,
|
||||
latex.Scoring,
|
||||
}
|
||||
|
||||
res, err := pandocClient.BatchConvertLatexToHtml5(ctx, req)
|
||||
if err != nil {
|
||||
return models.Html5ProblemStatement{}, err
|
||||
}
|
||||
|
||||
if len(res) != len(req) {
|
||||
return models.Html5ProblemStatement{}, fmt.Errorf("wrong number of fieilds returned: %d", len(res))
|
||||
}
|
||||
|
||||
sanitizedStatement := sanitize(models.Html5ProblemStatement{
|
||||
LegendHtml: res[0],
|
||||
InputFormatHtml: res[1],
|
||||
OutputFormatHtml: res[2],
|
||||
NotesHtml: res[3],
|
||||
ScoringHtml: res[4],
|
||||
})
|
||||
|
||||
return sanitizedStatement, nil
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var defaultUser = &models.User{
|
||||
UserId: nil,
|
||||
Role: models.RoleSpectator.AsPointer(),
|
||||
UpdatedAt: nil,
|
||||
}
|
||||
|
||||
func extractToken(ctx context.Context) string {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
tokens := md.Get("token")
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return tokens[0]
|
||||
}
|
||||
|
||||
func (s *TesterServer) readSessionAndReadUser(ctx context.Context, token string) (*models.User, error) {
|
||||
// FIXME: possible bottle neck: should we cache it? (think of it in future)
|
||||
// FIXME: maybe use single connection instead of multiple requests
|
||||
userId, err := s.sessionClient.Read(ctx, &sessionv1.ReadSessionRequest{Token: token})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := s.userService.ReadUserById(ctx, userId.GetUserId()) // FIXME: must be cached!
|
||||
if err != nil {
|
||||
if errors.Is(err, lib.ErrNotFound) {
|
||||
user = &models.User{
|
||||
UserId: lib.AsInt32P(userId.GetUserId()),
|
||||
Role: models.RoleParticipant.AsPointer(),
|
||||
}
|
||||
err = s.userService.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func insertUser(ctx context.Context, user *models.User) context.Context {
|
||||
return context.WithValue(ctx, "user", user)
|
||||
}
|
||||
|
||||
func (s *TesterServer) AuthUnaryInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
token := extractToken(ctx)
|
||||
if token == "" {
|
||||
return handler(insertUser(ctx, defaultUser), req)
|
||||
}
|
||||
|
||||
user, err := s.readSessionAndReadUser(ctx, token)
|
||||
if err != nil {
|
||||
return handler(insertUser(ctx, defaultUser), req)
|
||||
}
|
||||
|
||||
return handler(insertUser(ctx, user), req)
|
||||
}
|
||||
}
|
||||
|
||||
type ssWrapper struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (s *ssWrapper) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
func (s *TesterServer) AuthStreamInterceptor() grpc.StreamServerInterceptor {
|
||||
return func(server interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
ctx := ss.Context()
|
||||
|
||||
token := extractToken(ctx)
|
||||
if token == "" {
|
||||
return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, defaultUser)})
|
||||
}
|
||||
|
||||
user, err := s.readSessionAndReadUser(ctx, token)
|
||||
if err != nil {
|
||||
return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, defaultUser)})
|
||||
}
|
||||
|
||||
return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, user)})
|
||||
}
|
||||
}
|
||||
|
||||
func ToGrpcError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// should I use map instead?
|
||||
switch {
|
||||
case errors.Is(err, lib.ErrValidationFailed):
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, lib.ErrInternal):
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
case errors.Is(err, lib.ErrExternal):
|
||||
return status.Error(codes.Unavailable, err.Error())
|
||||
case errors.Is(err, lib.ErrNoPermission):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
case errors.Is(err, lib.ErrUnknown):
|
||||
return status.Error(codes.Unknown, err.Error())
|
||||
case errors.Is(err, lib.ErrDeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, err.Error())
|
||||
case errors.Is(err, lib.ErrNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, lib.ErrAlreadyExists):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
case errors.Is(err, lib.ErrConflict):
|
||||
return status.Error(codes.Unimplemented, err.Error())
|
||||
case errors.Is(err, lib.ErrUnimplemented):
|
||||
return status.Error(codes.Unimplemented, err.Error())
|
||||
case errors.Is(err, lib.ErrUnauthenticated):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Unknown, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TesterServer) ErrUnwrappingUnaryInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
resp, err := handler(ctx, req)
|
||||
return resp, ToGrpcError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TesterServer) ErrUnwrappingStreamInterceptor() grpc.StreamServerInterceptor {
|
||||
return func(server interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
err := handler(server, ss)
|
||||
return ToGrpcError(err)
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (s *TesterServer) CreateProblem(server problemv1.ProblemService_CreateProblemServer) error {
|
||||
ctx := server.Context()
|
||||
|
||||
if err := s.problemService.CanCreateProblem(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := server.Recv() // receive problem
|
||||
if err != nil {
|
||||
return lib.TransportError(err, lib.ErrBadInput, "can't receive problem")
|
||||
}
|
||||
problem := req.GetProblem()
|
||||
if problem == nil {
|
||||
return lib.TransportError(nil, lib.ErrBadInput, "empty problem")
|
||||
}
|
||||
|
||||
p := &models.Problem{
|
||||
Name: lib.AsStringP(problem.Name),
|
||||
Description: lib.AsStringP(problem.Description),
|
||||
TimeLimit: lib.AsInt32P(problem.TimeLimit),
|
||||
MemoryLimit: lib.AsInt32P(problem.MemoryLimit),
|
||||
}
|
||||
|
||||
ch := readChunks(ctx, server)
|
||||
|
||||
err = writeChunks(ctx, ch) // temp stub
|
||||
if err != nil {
|
||||
return err // FIXME
|
||||
}
|
||||
|
||||
id, err := s.problemService.CreateProblem(ctx, p) // FIXME
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = server.SendAndClose(&problemv1.CreateProblemResponse{
|
||||
Id: id,
|
||||
})
|
||||
if err != nil {
|
||||
return lib.TransportError(err, lib.ErrBadInput, "can't send response")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeChunks(ctx context.Context, chunks <-chan []byte) error {
|
||||
// use s3
|
||||
// FIXME: use ctx?
|
||||
f, err := os.Create("out.txt") // FIXME: uuidv4 as initial temp name?
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var off int64 = 0
|
||||
for chunk := range chunks {
|
||||
_, err = f.WriteAt(chunk, off)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
off += int64(len(chunk))
|
||||
}
|
||||
|
||||
// TODO: rename file to its hash
|
||||
return nil
|
||||
}
|
||||
|
||||
func readChunks(ctx context.Context, server problemv1.ProblemService_CreateProblemServer) <-chan []byte {
|
||||
ch := make(chan []byte)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return // FIXME
|
||||
default:
|
||||
req, err := server.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return // FIXME
|
||||
}
|
||||
if status.Code(err) == codes.Canceled {
|
||||
return // FIXME
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
test := req.GetTest()
|
||||
if test == nil {
|
||||
return // FIXME
|
||||
}
|
||||
|
||||
ch <- test.Chunk
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *TesterServer) ReadProblem(ctx context.Context, req *problemv1.ReadProblemRequest) (*problemv1.ReadProblemResponse, error) {
|
||||
problem, err := s.problemService.ReadProblemById(ctx, req.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &problemv1.ReadProblemResponse{
|
||||
Problem: &problemv1.ReadProblemResponse_Problem{
|
||||
Id: *problem.Id,
|
||||
Name: *problem.Name,
|
||||
Description: *problem.Description,
|
||||
TimeLimit: *problem.TimeLimit,
|
||||
MemoryLimit: *problem.MemoryLimit,
|
||||
CreatedAt: AsTimestampP(problem.CreatedAt),
|
||||
UpdatedAt: AsTimestampP(problem.UpdatedAt),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
//func (s *TesterServer) UpdateProblem(ctx context.Context, req *problemv1.UpdateProblemRequest) (*emptypb.Empty, error) {
|
||||
// problem := req.GetProblem()
|
||||
// if problem == nil {
|
||||
// return nil, status.Errorf(codes.Unknown, "") // FIXME
|
||||
// }
|
||||
// err := s.problemService.UpdateProblem(
|
||||
// ctx,
|
||||
// &models.Problem{
|
||||
// Id: lib.AsInt32P(problem.Id),
|
||||
// Name: problem.Name,
|
||||
// Description: problem.Description,
|
||||
// TimeLimit: problem.TimeLimit,
|
||||
// MemoryLimit: problem.MemoryLimit,
|
||||
// },
|
||||
// )
|
||||
// if err != nil {
|
||||
// return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME
|
||||
// }
|
||||
//
|
||||
// return &emptypb.Empty{}, nil
|
||||
//}
|
||||
|
||||
func (s *TesterServer) DeleteProblem(ctx context.Context, req *problemv1.DeleteProblemRequest) (*emptypb.Empty, error) {
|
||||
err := s.problemService.DeleteProblem(ctx, req.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1"
|
||||
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type ProblemService interface {
|
||||
CanCreateProblem(ctx context.Context) error
|
||||
CreateProblem(ctx context.Context, problem *models.Problem) (int32, error)
|
||||
ReadProblemById(ctx context.Context, id int32) (*models.Problem, error)
|
||||
UpdateProblem(ctx context.Context, problem *models.Problem) error
|
||||
DeleteProblem(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type SessionClient interface {
|
||||
Read(ctx context.Context,
|
||||
in *sessionv1.ReadSessionRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (*sessionv1.ReadSessionResponse, error)
|
||||
}
|
||||
|
||||
type UserService interface {
|
||||
CreateUser(ctx context.Context, user *models.User) error
|
||||
ReadUserById(ctx context.Context, userId int32) (*models.User, error)
|
||||
}
|
||||
|
||||
type TesterServer struct {
|
||||
problemv1.UnimplementedProblemServiceServer
|
||||
problemService ProblemService
|
||||
|
||||
sessionClient SessionClient
|
||||
userService UserService
|
||||
|
||||
grpcServer *grpc.Server
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTesterServer(
|
||||
problemService ProblemService,
|
||||
sessionClient SessionClient,
|
||||
userService UserService,
|
||||
logger *zap.Logger,
|
||||
) *TesterServer {
|
||||
server := &TesterServer{
|
||||
problemService: problemService,
|
||||
sessionClient: sessionClient,
|
||||
userService: userService,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(server.ErrUnwrappingUnaryInterceptor()),
|
||||
grpc.StreamInterceptor(server.ErrUnwrappingStreamInterceptor()),
|
||||
grpc.UnaryInterceptor(server.AuthUnaryInterceptor()),
|
||||
grpc.StreamInterceptor(server.AuthStreamInterceptor()),
|
||||
)
|
||||
|
||||
problemv1.RegisterProblemServiceServer(grpcServer, server)
|
||||
|
||||
server.grpcServer = grpcServer
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *TesterServer) Start(lis net.Listener) error {
|
||||
return s.grpcServer.Serve(lis)
|
||||
}
|
||||
|
||||
func (s *TesterServer) Stop() {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
|
||||
func AsTimeP(t *timestamppb.Timestamp) *time.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
tt := t.AsTime()
|
||||
return &tt
|
||||
}
|
||||
|
||||
func AsTimestampP(t *time.Time) *timestamppb.Timestamp {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(*t)
|
||||
}
|
71
main.go
71
main.go
|
@ -2,18 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/services"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/storage"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/transport"
|
||||
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/config"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester/delivery/rest"
|
||||
problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/tester/repository"
|
||||
testerUseCase "git.sch9.ru/new_gate/ms-tester/internal/tester/usecase"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -21,7 +19,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
var cfg lib.Config
|
||||
var cfg config.Config
|
||||
err := cleanenv.ReadConfig(".env", &cfg)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error reading config: %s", err.Error()))
|
||||
|
@ -36,44 +34,47 @@ func main() {
|
|||
panic(fmt.Sprintf(`error reading config: env expected "prod" or "dev", got "%s"`, cfg.Env))
|
||||
}
|
||||
|
||||
db, err := sqlx.Connect("pgx", cfg.PostgresDSN)
|
||||
logger.Info("connecting to postgres")
|
||||
db, err := pkg.NewPostgresDB(cfg.PostgresDSN)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
logger.Info("successfully connected to postgres")
|
||||
|
||||
//contestStorage := storage.NewContestStorage(db, logger)
|
||||
//contestService := services.NewContestService(contestStorage)
|
||||
pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc)
|
||||
|
||||
pandocClient := lib.NewPandocClient(&http.Client{}, cfg.Pandoc)
|
||||
problemRepo := problemsRepository.NewProblemRepository(db)
|
||||
problemUC := testerUseCase.NewProblemUseCase(problemRepo, pandocClient)
|
||||
|
||||
grpcSessionClient, err := grpc.NewClient(cfg.Auth, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sessionClient := sessionv1.NewSessionServiceClient(grpcSessionClient)
|
||||
contestRepo := problemsRepository.NewContestRepository(db)
|
||||
contestUC := testerUseCase.NewContestUseCase(contestRepo)
|
||||
|
||||
permissionService := services.NewPermissionService()
|
||||
server := fiber.New()
|
||||
|
||||
problemStorage := storage.NewProblemStorage(db, logger)
|
||||
problemService := services.NewProblemService(problemStorage, pandocClient, permissionService)
|
||||
|
||||
userStorage := storage.NewUserStorage(db)
|
||||
userService := services.NewUserService(userStorage)
|
||||
|
||||
server := transport.NewTesterServer(problemService, sessionClient, userService, logger)
|
||||
|
||||
lis, err := net.Listen("tcp", cfg.Address)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testerv1.RegisterHandlersWithOptions(server, rest.NewTesterHandlers(problemUC, contestUC), testerv1.FiberServerOptions{
|
||||
Middlewares: []testerv1.MiddlewareFunc{
|
||||
fiberlogger.New(),
|
||||
rest.AuthMiddleware(cfg.JWTSecret),
|
||||
//rest.AuthMiddleware(cfg.JWTSecret, userUC),
|
||||
//cors.New(cors.Config{
|
||||
// AllowOrigins: "http://localhost:3000",
|
||||
// AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
||||
// AllowHeaders: "Content-Type,Set-Cookie,Credentials",
|
||||
// AllowCredentials: true,
|
||||
//}),
|
||||
},
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := server.Start(lis); err != nil {
|
||||
logger.Fatal("error starting server", zap.Error(err))
|
||||
err := server.Listen(cfg.Address)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("error starting server: %s", err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info(fmt.Sprintf("server started on %s", cfg.Address))
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
|
|
|
@ -1,280 +1,5 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
--CREATE TABLE IF NOT EXISTS languages
|
||||
--(
|
||||
-- id serial NOT NULL,
|
||||
-- name VARCHAR(60) NOT NULL,
|
||||
-- build_file_hash CHAR(128) NULL,
|
||||
-- execute_file_hash CHAR(128) NULL,
|
||||
-- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
-- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
--
|
||||
-- PRIMARY KEY (id)
|
||||
--);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problems
|
||||
(
|
||||
id serial NOT NULL,
|
||||
name VARCHAR(300) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
time_limit INT NOT NULL,
|
||||
memory_limit INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON problems USING BTREE (id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contests
|
||||
(
|
||||
id serial NOT NULL,
|
||||
name VARCHAR(300) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON contests USING BTREE (id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks
|
||||
(
|
||||
id SERIAL NOT NULL,
|
||||
contest_id INT REFERENCES contests ON DELETE CASCADE,
|
||||
problem_id INT REFERENCES problems ON DELETE CASCADE,
|
||||
position INT NOT NULL,
|
||||
position_name VARCHAR(10) NOT NULL, -- problem name like: A,B,A1,etc
|
||||
UNIQUE (contest_id,problem_id),
|
||||
UNIQUE (contest_id,position),
|
||||
UNIQUE (contest_id,position_name),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON tasks USING BTREE (id);
|
||||
CREATE INDEX ON tasks USING BTREE (contest_id);
|
||||
CREATE INDEX ON tasks USING BTREE (problem_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS testgroups
|
||||
(
|
||||
id serial NOT NULL,
|
||||
problem_id INT REFERENCES problems ON DELETE CASCADE,
|
||||
testing_strategy INT NOT NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON testgroups USING BTREE (id);
|
||||
CREATE INDEX ON testgroups USING BTREE (problem_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subtasks
|
||||
(
|
||||
id SERIAL NOT NULL,
|
||||
contest_id INT REFERENCES contests ON DELETE CASCADE,
|
||||
testgroup_id INT REFERENCES testgroups ON DELETE CASCADE,
|
||||
task_id INT REFERENCES tasks ON DELETE CASCADE,
|
||||
UNIQUE (contest_id,testgroup_id),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON subtasks USING BTREE (id);
|
||||
CREATE INDEX ON subtasks USING BTREE (contest_id);
|
||||
CREATE INDEX ON subtasks USING BTREE (testgroup_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participants
|
||||
(
|
||||
id serial NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
contest_id INT REFERENCES contests ON DELETE CASCADE,
|
||||
name varchar(200) NOT NULL,
|
||||
|
||||
UNIQUE (user_id,contest_id),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON participants USING BTREE (id);
|
||||
CREATE INDEX ON participants USING BTREE (user_id);
|
||||
CREATE INDEX ON participants USING BTREE (contest_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS solutions
|
||||
(
|
||||
id serial NOT NULL,
|
||||
participant_id INT REFERENCES participants ON DELETE CASCADE,
|
||||
task_id INT REFERENCES problems ON DELETE CASCADE,
|
||||
language_id INT NOT NULL,REFERENCES languages ON DELETE CASCADE,
|
||||
solution_hash CHAR(128) NOT NULL,
|
||||
result INT NOT NULL,
|
||||
score INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON solutions USING BTREE (id);
|
||||
CREATE INDEX ON solutions USING BTREE (id,participant_id,task_id,language_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tests
|
||||
(
|
||||
id serial NOT NULL,
|
||||
--problem_id INT NOT NULL,
|
||||
testgroup_id INT REFERENCES testgroups ON DELETE CASCADE,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON tests USING BTREE (id);
|
||||
CREATE INDEX ON tests USING BTREE (testgroup_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subtaskruns
|
||||
(
|
||||
id serial NOT NULL,
|
||||
subtask_id INT REFERENCES subtasks ON DELETE CASCADE,
|
||||
solution_id INT REFERENCES solutions ON DELETE CASCADE,
|
||||
result INT NOT NULL,
|
||||
score INT NOT NULL,
|
||||
|
||||
UNIQUE (subtask_id,solution_id),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON subtaskruns USING BTREE (id);
|
||||
CREATE INDEX ON subtaskruns USING BTREE (result);
|
||||
CREATE INDEX ON subtaskruns USING BTREE (solution_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS testruns
|
||||
(
|
||||
id serial NOT NULL,
|
||||
test_id INT REFERENCES tests ON DELETE CASCADE,
|
||||
--solution_id INT REFERENCES solutions ON DELETE CASCADE,
|
||||
subtaskrun_id INT REFERENCES subtaskruns ON DELETE CASCADE,
|
||||
result INT NOT NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON testruns USING BTREE (id);
|
||||
CREATE INDEX ON testruns USING BTREE (result);
|
||||
CREATE INDEX ON testruns USING BTREE (subtaskrun_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participant_subtask
|
||||
(
|
||||
participant_id INT REFERENCES participants ON DELETE CASCADE,
|
||||
subtask_id INT REFERENCES subtasks ON DELETE CASCADE,
|
||||
--result INT NOT NULL,
|
||||
best_score INT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participant_task
|
||||
(
|
||||
participant_id INT REFERENCES participants ON DELETE CASCADE,
|
||||
task_id INT REFERENCES tasks ON DELETE CASCADE,
|
||||
--result INT NOT NULL,
|
||||
best_score INT NOT NULL,
|
||||
penalty INT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
user_id INT NOT NULL,
|
||||
role INT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CHECK ( role BETWEEN 0 AND 3),
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE FUNCTION on_new_participant() RETURNS TRIGGER AS
|
||||
$$ BEGIN
|
||||
--RAISE NOTICE 'NEW.ID:%, NEW.contest_id:%', NEW.id,NEW.contest_id;
|
||||
INSERT INTO participant_task (participant_id,task_id,best_score,penalty) SELECT NEW.id,id,0,0 FROM tasks WHERE contest_id=NEW.contest_id;
|
||||
INSERT INTO participant_subtask (participant_id,subtask_id,best_score) SELECT NEW.id,id,0 FROM subtasks WHERE contest_id=NEW.contest_id;
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER new_participant_trg AFTER INSERT ON participants FOR EACH ROW EXECUTE FUNCTION on_new_participant();
|
||||
|
||||
CREATE FUNCTION on_new_task() RETURNS TRIGGER AS
|
||||
$$ BEGIN
|
||||
INSERT INTO participant_task (participant_id,task_id,best_score,penalty) SELECT id,NEW.id,0,0 FROM participants WHERE contest_id=NEW.contest_id;
|
||||
INSERT INTO subtasks(contest_id,testgroup_id,task_id) SELECT NEW.contest_id,id,NEW.id FROM testgroups WHERE problem_id = NEW.problem_id;
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER new_task_trg AFTER INSERT ON tasks FOR EACH ROW EXECUTE FUNCTION on_new_task();
|
||||
|
||||
CREATE FUNCTION on_new_subtask() RETURNS TRIGGER AS
|
||||
$$ BEGIN
|
||||
INSERT INTO participant_subtask (participant_id,subtask_id,best_score) SELECT id,NEW.id,0 FROM participants WHERE contest_id=NEW.contest_id;
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER new_subtask_trg AFTER INSERT ON subtasks FOR EACH ROW EXECUTE FUNCTION on_new_subtask();
|
||||
|
||||
|
||||
CREATE FUNCTION on_new_solution() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO subtaskruns (subtask_id,solution_id,result,score) (SELECT id,NEW.id,0,0 FROM subtasks WHERE task_id = NEW.task_id);
|
||||
--INSERT INTO testruns (test_id,subtaskrun_id,result) (SELECT id,str.id,0 FROM tests WHERE testgroup_id IN (SELECT testgroup_id FROM subtasks WHERE id IN (SELECT subtask_id FROM subtaskruns AS str WHERE solution_id=NEW.id)));
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER on_new_solution_trg AFTER INSERT ON solutions FOR EACH ROW EXECUTE FUNCTION on_new_solution();
|
||||
|
||||
CREATE FUNCTION on_new_subtaskrun() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO testruns (test_id,subtaskrun_id,result) (SELECT id,NEW.id,0 FROM tests WHERE testgroup_id IN (SELECT testgroup_id FROM subtasks WHERE id=NEW.subtask_id));
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER on_new_subtaskrun_trg AFTER INSERT ON subtaskruns FOR EACH ROW EXECUTE FUNCTION on_new_subtaskrun();
|
||||
|
||||
CREATE FUNCTION updated_at_update() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS
|
||||
$$
|
||||
|
@ -284,33 +9,157 @@ BEGIN
|
|||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION check_max_tasks() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS
|
||||
$$
|
||||
DECLARE
|
||||
max_on_contest_tasks_amount integer := 50;
|
||||
BEGIN
|
||||
IF (
|
||||
SELECT count(*) FROM tasks
|
||||
WHERE contest_id = NEW.contest_id
|
||||
) >= (
|
||||
max_on_contest_tasks_amount
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Exceeded max tasks for this contest';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problems
|
||||
(
|
||||
id serial NOT NULL,
|
||||
title varchar(64) NOT NULL,
|
||||
time_limit integer NOT NULL DEFAULT 1000,
|
||||
memory_limit integer NOT NULL DEFAULT 64,
|
||||
|
||||
legend varchar(10240) NOT NULL DEFAULT '',
|
||||
input_format varchar(10240) NOT NULL DEFAULT '',
|
||||
output_format varchar(10240) NOT NULL DEFAULT '',
|
||||
notes varchar(10240) NOT NULL DEFAULT '',
|
||||
scoring varchar(10240) NOT NULL DEFAULT '',
|
||||
|
||||
legend_html varchar(10240) NOT NULL DEFAULT '',
|
||||
input_format_html varchar(10240) NOT NULL DEFAULT '',
|
||||
output_format_html varchar(10240) NOT NULL DEFAULT '',
|
||||
notes_html varchar(10240) NOT NULL DEFAULT '',
|
||||
scoring_html varchar(10240) NOT NULL DEFAULT '',
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
CHECK (length(title) != 0),
|
||||
CHECK (memory_limit BETWEEN 4 and 1024),
|
||||
CHECK (time_limit BETWEEN 250 and 5000)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_problems_update
|
||||
BEFORE UPDATE
|
||||
ON problems
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contests
|
||||
(
|
||||
id serial NOT NULL,
|
||||
title varchar(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
CHECK (length(title) != 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_contests_update
|
||||
BEFORE UPDATE
|
||||
ON contests
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks
|
||||
(
|
||||
id serial NOT NULL,
|
||||
problem_id integer REFERENCES problems (id) ON DELETE SET NULL,
|
||||
contest_id integer REFERENCES contests (id) ON DELETE SET NULL,
|
||||
position integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (problem_id, contest_id),
|
||||
UNIQUE (contest_id, position),
|
||||
CHECK (position >= 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER max_tasks_on_contest_check
|
||||
BEFORE INSERT ON tasks
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION check_max_tasks();
|
||||
|
||||
CREATE TRIGGER on_tasks_update
|
||||
BEFORE UPDATE
|
||||
ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participants
|
||||
(
|
||||
id serial NOT NULL,
|
||||
user_id integer NOT NULL,
|
||||
contest_id integer NOT NULL REFERENCES contests (id),
|
||||
name varchar(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (user_id, contest_id),
|
||||
CHECK (length(name) != 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_participants_update
|
||||
BEFORE UPDATE
|
||||
ON participants
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS solutions
|
||||
(
|
||||
id serial NOT NULL,
|
||||
task_id integer REFERENCES tasks (id) ON DELETE SET NULL,
|
||||
participant_id integer REFERENCES participants (id) ON DELETE SET NULL,
|
||||
solution varchar(1048576) NOT NULL,
|
||||
state integer NOT NULL DEFAULT 1,
|
||||
score integer NOT NULL DEFAULT 0,
|
||||
penalty integer NOT NULL,
|
||||
time_stat integer NOT NULL DEFAULT 0,
|
||||
memory_stat integer NOT NULL DEFAULT 0,
|
||||
language integer NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_solutions_update
|
||||
BEFORE UPDATE
|
||||
ON solutions
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
--CREATE TRIGGER languages_upd_trg BEFORE UPDATE ON languages FOR EACH ROW EXECUTE FUNCTION updated_at_update();
|
||||
CREATE TRIGGER problems_upd_trg BEFORE UPDATE ON problems FOR EACH ROW EXECUTE FUNCTION updated_at_update();
|
||||
CREATE TRIGGER contests_upd_trg BEFORE UPDATE ON contests FOR EACH ROW EXECUTE FUNCTION updated_at_update();
|
||||
CREATE TRIGGER users_upd_trg BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION updated_at_update();
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP FUNCTION IF EXISTS updated_at_update CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_participant CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_task CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_subtask CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_solution CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_subtaskrun CASCADE;
|
||||
DROP TABLE IF EXISTS tests CASCADE;
|
||||
DROP TABLE IF EXISTS solutions CASCADE;
|
||||
--DROP TABLE IF EXISTS languages CASCADE;
|
||||
DROP TABLE IF EXISTS testgroups CASCADE;
|
||||
DROP TABLE IF EXISTS testruns CASCADE;
|
||||
DROP TABLE IF EXISTS subtaskruns CASCADE;
|
||||
DROP TABLE IF EXISTS problems CASCADE;
|
||||
DROP TABLE IF EXISTS contests CASCADE;
|
||||
DROP TABLE IF EXISTS tasks CASCADE;
|
||||
DROP TABLE IF EXISTS subtasks CASCADE;
|
||||
DROP TABLE IF EXISTS participants CASCADE;
|
||||
DROP TABLE IF EXISTS participant_task CASCADE;
|
||||
DROP TABLE IF EXISTS participant_subtask CASCADE;
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
-- +goose StatementEnd
|
||||
DROP TRIGGER IF EXISTS on_solutions_update ON solutions;
|
||||
DROP TABLE IF EXISTS solutions;
|
||||
DROP TRIGGER IF EXISTS on_participants_update ON participants;
|
||||
DROP TABLE IF EXISTS participants;
|
||||
DROP TRIGGER IF EXISTS on_tasks_update ON tasks;
|
||||
DROP TRIGGER IF EXISTS max_tasks_on_contest_check ON tasks;
|
||||
DROP TABLE IF EXISTS tasks;
|
||||
DROP TRIGGER IF EXISTS on_problems_update ON problems;
|
||||
DROP TABLE IF EXISTS problems;
|
||||
DROP TRIGGER IF EXISTS on_contests_update ON contests;
|
||||
DROP TABLE IF EXISTS contests;
|
||||
DROP FUNCTION IF EXISTS updated_at_update();
|
||||
DROP FUNCTION IF EXISTS check_max_tasks();
|
||||
-- +goose StatementEnd
|
44
opa/all.rego
44
opa/all.rego
|
@ -1,44 +0,0 @@
|
|||
package problem.rbac
|
||||
|
||||
import rego.v1
|
||||
|
||||
spectator := 0
|
||||
participant := 1
|
||||
moderator := 2
|
||||
admin := 3
|
||||
|
||||
permissions := {
|
||||
"read": is_spectator,
|
||||
"participate": is_participant,
|
||||
"update": is_moderator,
|
||||
"create": is_moderator,
|
||||
"delete": is_moderator,
|
||||
}
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if is_admin
|
||||
|
||||
allow if {
|
||||
permissions[input.action]
|
||||
}
|
||||
|
||||
default is_admin := false
|
||||
is_admin if {
|
||||
input.user.role == admin
|
||||
}
|
||||
|
||||
default is_moderator := false
|
||||
is_moderator if {
|
||||
input.user.role >= moderator
|
||||
}
|
||||
|
||||
default is_participant := false
|
||||
is_participant if {
|
||||
input.user.role >= participant
|
||||
}
|
||||
|
||||
default is_spectator := true
|
||||
is_spectator if {
|
||||
input.user.role >= spectator
|
||||
}
|
1
pkg/aws-s3-client.go
Normal file
1
pkg/aws-s3-client.go
Normal file
|
@ -0,0 +1 @@
|
|||
package pkg
|
37
pkg/errors.go
Normal file
37
pkg/errors.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
NoPermission = errors.New("no permission")
|
||||
ErrUnauthenticated = errors.New("unauthenticated")
|
||||
ErrUnhandled = errors.New("unhandled")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrBadInput = errors.New("bad input")
|
||||
ErrInternal = errors.New("internal")
|
||||
)
|
||||
|
||||
func Wrap(basic error, err error, op string, msg string) error {
|
||||
return errors.Join(basic, err, fmt.Errorf("during %s: %s", op, msg))
|
||||
}
|
||||
|
||||
func ToREST(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, ErrUnauthenticated):
|
||||
return http.StatusUnauthorized
|
||||
case errors.Is(err, ErrBadInput):
|
||||
return http.StatusBadRequest
|
||||
case errors.Is(err, ErrNotFound):
|
||||
return http.StatusNotFound
|
||||
case errors.Is(err, ErrInternal):
|
||||
return http.StatusInternalServerError
|
||||
case errors.Is(err, NoPermission):
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Contest struct {
|
||||
Id *int `db:"id"`
|
||||
Name *string `db:"name"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
UpdatedAt *time.Time `db:"updated_at"`
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package models
|
||||
|
||||
type Language struct {
|
||||
Name string
|
||||
CompileCmd []string //source: src;result:executable
|
||||
RunCmd []string //source: executable
|
||||
}
|
||||
|
||||
var Languages = [...]Language {
|
||||
{Name : "gcc std=c90",
|
||||
CompileCmd : []string{"gcc", "src","-std=c90","-o","executable"},
|
||||
RunCmd : []string{"executable"}},
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package models
|
||||
|
||||
type Participant struct {
|
||||
Id *int32 `db:"id"`
|
||||
UserId *int32 `db:"user_id"`
|
||||
ContestId *int32 `db:"contest_id"`
|
||||
Name *string `db:"name"`
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Problem struct {
|
||||
Id *int32 `db:"id"`
|
||||
Name *string `db:"name"`
|
||||
Description *string `db:"description"`
|
||||
TimeLimit *int32 `db:"time_limit"`
|
||||
MemoryLimit *int32 `db:"memory_limit"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
UpdatedAt *time.Time `db:"updated_at"`
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
)
|
||||
|
||||
type Result int32
|
||||
|
||||
const (
|
||||
NotTested Result = 0 // change only with schema change
|
||||
Accepted Result = 1
|
||||
CompilationError Result = 2
|
||||
MemoryLimitExceeded Result = 3
|
||||
TimeLimitExceeded Result = 4
|
||||
SystemFailDuringTesting Result = 5
|
||||
Testing Result = 6
|
||||
)
|
||||
|
||||
var ErrBadResult = errors.New("bad result")
|
||||
|
||||
func (result Result) Valid() error {
|
||||
switch result {
|
||||
case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceeded, CompilationError, SystemFailDuringTesting:
|
||||
return nil
|
||||
}
|
||||
return lib.ServiceError(ErrBadResult, lib.ErrValidationFailed, "bad result")
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
)
|
||||
|
||||
type Role int32
|
||||
|
||||
const (
|
||||
RoleSpectator Role = 0
|
||||
RoleParticipant Role = 1
|
||||
RoleModerator Role = 2
|
||||
RoleAdmin Role = 3
|
||||
)
|
||||
|
||||
func (role Role) IsAdmin() bool {
|
||||
return role == RoleAdmin
|
||||
}
|
||||
|
||||
func (role Role) IsModerator() bool {
|
||||
return role == RoleModerator
|
||||
}
|
||||
|
||||
func (role Role) IsParticipant() bool {
|
||||
return role == RoleParticipant
|
||||
}
|
||||
|
||||
func (role Role) IsSpectator() bool {
|
||||
return role == RoleSpectator
|
||||
}
|
||||
|
||||
func (role Role) AtLeast(other Role) bool {
|
||||
return role >= other
|
||||
}
|
||||
|
||||
func (role Role) AtMost(other Role) bool {
|
||||
return role <= other
|
||||
}
|
||||
|
||||
var ErrBadRole = errors.New("bad role")
|
||||
|
||||
func (role Role) Valid() error {
|
||||
switch role {
|
||||
case RoleSpectator, RoleParticipant, RoleModerator, RoleAdmin:
|
||||
return nil
|
||||
}
|
||||
return lib.ServiceError(ErrBadRole, lib.ErrValidationFailed, "bad role")
|
||||
}
|
||||
|
||||
func (role Role) AsPointer() *Role {
|
||||
return &role
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Solution struct {
|
||||
Id *int32 `db:"id"`
|
||||
ParticipantId *int32 `db:"participant_id"`
|
||||
TaskId *int32 `db:"task_id"`
|
||||
LanguageId *int32 `db:"language_id"`
|
||||
SolutionHash *string `db:"solution_hash"`
|
||||
Result *int32 `db:"result"`
|
||||
Score *int32 `db:"score"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package models
|
||||
|
||||
type SubTask struct {
|
||||
Id *int32 `db:"id"`
|
||||
ContestId *int32 `db:"contest_id"`
|
||||
TestgroupId *int32 `db:"testgroup_id"`
|
||||
TaskId *int32 `db:"task_id"`
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package models
|
||||
|
||||
type Task struct {
|
||||
Id *int32 `db:"id"`
|
||||
ContestId *int32 `db:"contest_id"`
|
||||
ProblemId *int32 `db:"problem_id"`
|
||||
Position *int32 `db:"position"`
|
||||
PositionName *string `db:"position_name"`
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
)
|
||||
|
||||
type TestingStrategy int32
|
||||
|
||||
const (
|
||||
EachTestTS TestingStrategy = 1
|
||||
CompleteGroupTS TestingStrategy = 2
|
||||
)
|
||||
|
||||
type Testgroup struct {
|
||||
Id *int32 `db:"id"`
|
||||
ProblemId *int32 `db:"problem_id"`
|
||||
TestingStrategy *TestingStrategy `db:"testing_strategy"`
|
||||
}
|
||||
|
||||
type TestGroupData struct {
|
||||
Ts TestingStrategy
|
||||
TestAmount int32
|
||||
}
|
||||
|
||||
var ErrBadTestingStrategy = errors.New("bad testing strategy")
|
||||
|
||||
func (ts TestingStrategy) Valid() error {
|
||||
switch ts {
|
||||
case EachTestTS, CompleteGroupTS:
|
||||
return nil
|
||||
}
|
||||
return lib.ServiceError(ErrBadTestingStrategy, lib.ErrValidationFailed, "bad testing strategy")
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
UserId *int32 `json:"user_id" db:"user_id"`
|
||||
Role *Role `json:"role" db:"role"`
|
||||
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
153
pkg/pandoc-client.go
Normal file
153
pkg/pandoc-client.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *http.Client
|
||||
address string
|
||||
}
|
||||
|
||||
type PandocClient interface {
|
||||
ConvertLatexToHtml5(ctx context.Context, text string) (string, error)
|
||||
BatchConvertLatexToHtml5(ctx context.Context, texts []string) ([]string, error)
|
||||
}
|
||||
|
||||
func NewPandocClient(client *http.Client, address string) *Client {
|
||||
return &Client{
|
||||
client: client,
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
|
||||
type conversation struct {
|
||||
Text string `json:"text"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Math string `json:"html-math-method"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Message string `json:"message"`
|
||||
Verbosity string `json:"verbosity"`
|
||||
}
|
||||
|
||||
type output struct {
|
||||
Error string `json:"error"`
|
||||
Output string `json:"output"`
|
||||
Base64 bool `json:"base64"`
|
||||
Messages []message `json:"messages"`
|
||||
}
|
||||
|
||||
func (client *Client) sendRaw(ctx context.Context, path string, body []byte) ([]byte, error) {
|
||||
path, err := url.JoinPath(client.address, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (client *Client) convert(ctx context.Context, text, from, to, math string) (string, error) {
|
||||
body, err := json.Marshal(conversation{
|
||||
Text: text,
|
||||
From: from,
|
||||
To: to,
|
||||
Math: math,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := client.sendRaw(ctx, "/", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(resp), nil
|
||||
}
|
||||
|
||||
func (client *Client) batchConvert(ctx context.Context, texts []string, from, to, math string) ([]string, error) {
|
||||
list := make([]conversation, len(texts))
|
||||
for i, text := range texts {
|
||||
list[i] = conversation{
|
||||
Text: text,
|
||||
From: from,
|
||||
To: to,
|
||||
Math: math,
|
||||
}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.sendRaw(ctx, "/batch", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []output
|
||||
err = json.Unmarshal(resp, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result) != len(texts) {
|
||||
return nil, fmt.Errorf("wrong number of fieilds returned: %d", len(result))
|
||||
}
|
||||
|
||||
err = nil
|
||||
for _, o := range result {
|
||||
if o.Error != "" {
|
||||
err = errors.Join(err, errors.New(o.Error))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, Wrap(ErrBadInput, err, "BatchConvertLatexToHtml5", "invalid input")
|
||||
}
|
||||
|
||||
res := make([]string, len(result))
|
||||
for i, o := range result {
|
||||
res[i] = o.Output
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (client *Client) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) {
|
||||
return client.convert(ctx, text, "latex", "html5", "katex")
|
||||
}
|
||||
|
||||
func (client *Client) BatchConvertLatexToHtml5(ctx context.Context, texts []string) ([]string, error) {
|
||||
return client.batchConvert(ctx, texts, "latex", "html5", "katex")
|
||||
}
|
31
pkg/postgres-client.go
Normal file
31
pkg/postgres-client.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxOpenConns = 60
|
||||
connMaxLifetime = 120
|
||||
maxIdleConns = 30
|
||||
connMaxIdleTime = 20
|
||||
)
|
||||
|
||||
func NewPostgresDB(dsn string) (*sqlx.DB, error) {
|
||||
db, err := sqlx.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(maxOpenConns)
|
||||
db.SetConnMaxLifetime(connMaxLifetime * time.Second)
|
||||
db.SetMaxIdleConns(maxIdleConns)
|
||||
db.SetConnMaxIdleTime(connMaxIdleTime * time.Second)
|
||||
if err = db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
7
pkg/rabbitmq-client.go
Normal file
7
pkg/rabbitmq-client.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package pkg
|
||||
|
||||
import amqp "github.com/rabbitmq/amqp091-go"
|
||||
|
||||
func NewRabbitClient(dsn string) (*amqp.Connection, error) {
|
||||
return amqp.Dial(dsn)
|
||||
}
|
12
pkg/valkey-client.go
Normal file
12
pkg/valkey-client.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package pkg
|
||||
|
||||
import "github.com/valkey-io/valkey-go"
|
||||
|
||||
func NewValkeyClient(dsn string) (valkey.Client, error) {
|
||||
opts, err := valkey.ParseURL(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return valkey.NewClient(opts)
|
||||
}
|
1
proto
1
proto
|
@ -1 +0,0 @@
|
|||
Subproject commit 74332e33051110a36082a46dbc0379b1c816406b
|
1
todo.md
1
todo.md
|
@ -8,6 +8,7 @@
|
|||
|
||||
## in future:
|
||||
|
||||
* picture & similar storage
|
||||
* think about scaling
|
||||
* add ms-auth roles integration
|
||||
* add problem notes
|
||||
|
|
Loading…
Add table
Reference in a new issue