Initial commit;
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
9
cmd/main.go
Normal file
9
cmd/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"steam_analyzer/internal/app"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Run()
|
||||
}
|
||||
20
config/config.dev.yaml
Normal file
20
config/config.dev.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
host: localhost
|
||||
port: 3000
|
||||
origins:
|
||||
- localhost:3000
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 32768
|
||||
name: app-database
|
||||
user: admin-programmer
|
||||
password: oK79k(Q#E<#|YuBL6)|TMre9£DE}F2B,t}vYt5D6e<m.7vu<pp
|
||||
migration_path: ./db/migrations
|
||||
|
||||
steam_requester:
|
||||
request_per_time: 4s
|
||||
count_per_time: 1
|
||||
|
||||
steam_worker:
|
||||
request_per_time: 10m
|
||||
20
config/config.prod.yaml
Normal file
20
config/config.prod.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 3000
|
||||
origins:
|
||||
- localhost:3000
|
||||
|
||||
database:
|
||||
host: steam_db
|
||||
port: 5432
|
||||
name: app-database
|
||||
user: admin-programmer
|
||||
password: oK79k(Q#E<#|YuBL6)|TMre9£DE}F2B,t}vYt5D6e<m.7vu<pp
|
||||
migration_path: ./db/migrations
|
||||
|
||||
steam_requester:
|
||||
request_per_time: 4s
|
||||
count_per_time: 1
|
||||
|
||||
steam_worker:
|
||||
request_per_time: 10m
|
||||
20
deploy/Dockerfile
Normal file
20
deploy/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
COPY . .
|
||||
RUN GOOS=linux go build -o steam-parser ./cmd/main.go
|
||||
|
||||
# --------------------------------- #
|
||||
FROM alpine:latest AS configure
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
COPY --from=builder /app/steam-parser .
|
||||
|
||||
CMD ["./steam-parser"]
|
||||
37
docker-compose.local.yaml
Normal file
37
docker-compose.local.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: steam_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=${DATABASE_NAME}
|
||||
- POSTGRES_USER=${DATABASE_USER}
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
ports:
|
||||
- ${DATABASE_PORT}:5432
|
||||
# volumes:
|
||||
# - ./volumes/steam/db-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- overlay
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: steam_pgadmin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
|
||||
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
ports:
|
||||
- ${PG_ADMIN_PORT}:80
|
||||
networks:
|
||||
- overlay
|
||||
|
||||
networks:
|
||||
overlay:
|
||||
|
||||
volumes:
|
||||
pgadmin_data:
|
||||
59
docker-compose.yaml
Normal file
59
docker-compose.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
version: "3.9"
|
||||
|
||||
networks:
|
||||
overlay:
|
||||
|
||||
volumes:
|
||||
pgadmin_data:
|
||||
|
||||
services:
|
||||
steam_parser:
|
||||
image: dm1roshnikov/steam-parser:latest
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ./config/config.prod.yaml:/app/config/config.yaml:ro
|
||||
networks:
|
||||
- overlay
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: steam_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DATABASE_USER}
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
- POSTGRES_DB=${DATABASE_NAME}
|
||||
ports:
|
||||
- ${DATABASE_PORT}:5432
|
||||
# volumes:
|
||||
# - ./volumes/steam/db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
networks:
|
||||
- overlay
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: steam_pgadmin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
|
||||
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
ports:
|
||||
- ${PGADMIN_PORT}:80
|
||||
networks:
|
||||
- overlay
|
||||
30
go.mod
Normal file
30
go.mod
Normal file
@@ -0,0 +1,30 @@
|
||||
module steam_analyzer
|
||||
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/rs/cors v1.11.1
|
||||
golang.org/x/time v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||
)
|
||||
110
go.sum
Normal file
110
go.sum
Normal file
@@ -0,0 +1,110 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/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/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||
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/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
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-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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.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=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
68
internal/app/app.go
Normal file
68
internal/app/app.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"steam_analyzer/internal/config"
|
||||
"steam_analyzer/internal/handler"
|
||||
"steam_analyzer/internal/repository"
|
||||
"steam_analyzer/internal/service"
|
||||
"steam_analyzer/internal/steamreq"
|
||||
"steam_analyzer/internal/utils"
|
||||
"steam_analyzer/internal/utils/slogutils"
|
||||
"steam_analyzer/pkg/logger"
|
||||
"steam_analyzer/pkg/postgresql"
|
||||
"steam_analyzer/pkg/server"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var steamItemURL = `https://steamcommunity.com/market/search/render/`
|
||||
|
||||
func Run() {
|
||||
cfg := config.MustLoadYaml("./config/config.dev.yaml")
|
||||
logger := logger.New(cfg.Enviroment)
|
||||
|
||||
// db := repository.ConnectOrCreateDatabase()
|
||||
// repo := repository.NewSQLLiteDatabase(db)
|
||||
ctx := context.Background()
|
||||
|
||||
pool, err := postgresql.NewPGXPool(ctx, postgresql.CreateDBConnectionString(cfg.Database))
|
||||
if err != nil {
|
||||
logger.Error("Error while creating pool", slogutils.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
repo := repository.NewPostgresDB(pool)
|
||||
err = repo.InitDatabaseTables(ctx)
|
||||
if err != nil {
|
||||
logger.Error("Error while init tables", slogutils.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
itemService := service.NewItemService(logger, repo)
|
||||
requester := steamreq.NewRequester(steamItemURL, cfg.SteamRequester.RequestPerTime, cfg.SteamRequester.CountPerTime)
|
||||
|
||||
steamWorker := steamreq.NewSteamWorker(logger, requester, itemService, cfg.SteamWorker.RequestPerTime)
|
||||
steamItems := []steamreq.SteamItems{
|
||||
{
|
||||
Query: "case",
|
||||
ItemType: "case",
|
||||
},
|
||||
}
|
||||
go steamWorker.Start(ctx, steamItems)
|
||||
|
||||
router := mux.NewRouter()
|
||||
steamHandler := handler.NewSteamHandler(itemService)
|
||||
steamHandler.RegisterRoutes(router, "/steam")
|
||||
|
||||
server := server.New(cfg.Server, logger, router)
|
||||
server.Run()
|
||||
}
|
||||
|
||||
func PrintItems(items []steamreq.SteamMarketItem) {
|
||||
for _, item := range items {
|
||||
item.SellPrice = float64(item.SellPrice) / 100.0 * utils.CURRENCY_RUB
|
||||
fmt.Println(item.Name, item.HashName, item.SellPrice, item.SellListings, item.AssetDescription.Classid)
|
||||
}
|
||||
}
|
||||
76
internal/config/config.go
Normal file
76
internal/config/config.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Enviroment string `yaml:"enviroment" env:"ENVIROMENT" env-default:"local"`
|
||||
Server Server `yaml:"server"`
|
||||
Database Database `yaml:"database"`
|
||||
SteamRequester SteamRequester `yaml:"steam_requester"`
|
||||
SteamWorker SteamWorker `yaml:"steam_worker"`
|
||||
}
|
||||
|
||||
type (
|
||||
Server struct {
|
||||
Host string `yaml:"host" env-default:"localhost"`
|
||||
Port string `yaml:"port" env-default:"8080"`
|
||||
IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"30s"`
|
||||
WriteTimeout time.Duration `yaml:"write_timeout" env-default:"1s"`
|
||||
ReadTimeout time.Duration `yaml:"read_timeout" env-default:"1s"`
|
||||
Origins []string `yaml:"origins" env-separator:"," env-default:"http://localhost:3000,"`
|
||||
}
|
||||
Database struct {
|
||||
Host string `yaml:"host" env:"DATABASE_HOST" env-description:"Database host" env-default:"localhost"`
|
||||
Port string `yaml:"port" env:"DATABASE_PORT" env-description:"Database port" env-default:"5432"`
|
||||
Sslmode string `yaml:"sslmode" env-description:"Database ssl mode" env-default:"disable"`
|
||||
Name string `yaml:"name" env-description:"Database name"`
|
||||
User string `yaml:"user" env-description:"Database user"`
|
||||
Password string `yaml:"password" env-description:"Database user password"`
|
||||
|
||||
MaxConnAttempts int `yaml:"max_conn_attempt" env-default:"5"`
|
||||
ConnTimeout time.Duration `yaml:"conn_timeout" env-default:"10s"`
|
||||
|
||||
MigrationPath string `yaml:"migration_path" env:"DATABASE_MIGRATION_PATH" env-description:"Migration path"`
|
||||
}
|
||||
SteamRequester struct {
|
||||
RequestPerTime time.Duration `yaml:"request_per_time" env-description:"request limit per time" env-default:"5s"`
|
||||
CountPerTime int `yaml:"count_per_time" env-description:"count limit per time" env-default:"1"`
|
||||
}
|
||||
SteamWorker struct {
|
||||
RequestPerTime time.Duration `yaml:"request_per_time" env-description:"update prices time" env-default:"5m"`
|
||||
}
|
||||
)
|
||||
|
||||
func MustLoadYaml(configPath string) *Config {
|
||||
if configPath == "" {
|
||||
log.Fatal("config path is not set")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
log.Fatalf("config path doesn't exist: %s", configPath)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
if err := cleanenv.ReadConfig(configPath, &cfg); err != nil {
|
||||
log.Fatalf("cannot read config: %s", err)
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func MustLoadEnv() *Config {
|
||||
var cfg Config
|
||||
|
||||
if err := cleanenv.ReadEnv(&cfg); err != nil {
|
||||
log.Fatalf("cannot read config: %s", err)
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
55
internal/domain/item.go
Normal file
55
internal/domain/item.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SteamItem struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
ClassID string
|
||||
GameID int
|
||||
Price float64
|
||||
Count int
|
||||
History []ItemPriceHistory
|
||||
}
|
||||
|
||||
func (si SteamItem) ToDB() DBItem {
|
||||
return DBItem{
|
||||
ID: si.ID,
|
||||
Name: si.Name,
|
||||
ClassID: si.ClassID,
|
||||
GameID: si.GameID,
|
||||
}
|
||||
}
|
||||
|
||||
func (si SteamItem) ToDBHistory() DBItemHistory {
|
||||
return DBItemHistory{
|
||||
ItemID: si.ID,
|
||||
Price: si.Price,
|
||||
Count: si.Count,
|
||||
Date: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type ItemPriceHistory struct {
|
||||
Price float64
|
||||
Count int
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
type DBItem struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
ClassID string
|
||||
GameID int
|
||||
}
|
||||
|
||||
type DBItemHistory struct {
|
||||
ItemID uuid.UUID
|
||||
Price float64
|
||||
Count int
|
||||
Date time.Time
|
||||
}
|
||||
58
internal/handler/handler.go
Normal file
58
internal/handler/handler.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"steam_analyzer/internal/repository"
|
||||
"steam_analyzer/internal/utils/jsonutils"
|
||||
"steam_analyzer/pkg/utils/httputils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type ItemService interface {
|
||||
GetAllItemsWithLastPrice(ctx context.Context, itemName string) ([]repository.ItemWithLastPrice, error)
|
||||
// GetAllItemsWithLastPrice(itemName string) ([]*domain.SteamItem, error)
|
||||
}
|
||||
|
||||
type SteamHandler struct {
|
||||
itemService ItemService
|
||||
}
|
||||
|
||||
func NewSteamHandler(itemService ItemService) SteamHandler {
|
||||
return SteamHandler{
|
||||
itemService: itemService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h SteamHandler) RegisterRoutes(router *mux.Router, prefix string) {
|
||||
steamRouter := router.PathPrefix(prefix)
|
||||
|
||||
getRouter := steamRouter.Methods(http.MethodGet).Subrouter()
|
||||
|
||||
getRouter.HandleFunc("/items", h.GetAllItemsWithLastPrice)
|
||||
}
|
||||
|
||||
type GetAllItemsWithLastPriceResponse struct {
|
||||
Result []repository.ItemWithLastPrice `json:"result"`
|
||||
}
|
||||
|
||||
func (h SteamHandler) GetAllItemsWithLastPrice(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
jsonutils.ToJSON(w, httputils.HTTPError{Message: "Name query param couldn't be empty"})
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.itemService.GetAllItemsWithLastPrice(r.Context(), name)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
jsonutils.ToJSON(w, GetAllItemsWithLastPriceResponse{Result: items})
|
||||
}
|
||||
202
internal/repository/postgres.go
Normal file
202
internal/repository/postgres.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"steam_analyzer/internal/domain"
|
||||
"steam_analyzer/pkg/postgresql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PostgresDB struct {
|
||||
conn postgresql.PGXPool
|
||||
}
|
||||
|
||||
func NewPostgresDB(conn postgresql.PGXPool) *PostgresDB {
|
||||
return &PostgresDB{
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (db PostgresDB) InitDatabaseTables(ctx context.Context) error {
|
||||
createTable := `
|
||||
CREATE TABLE IF NOT EXISTS item (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT,
|
||||
class_id TEXT UNIQUE NOT NULL,
|
||||
game_id INTEGER NOT NULL,
|
||||
type_id INTEGER
|
||||
);`
|
||||
_, err := db.conn.Exec(ctx, createTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createTable = `
|
||||
CREATE TABLE IF NOT EXISTS item_history (
|
||||
item_id UUID REFERENCES item(id),
|
||||
price REAL,
|
||||
count INTEGER,
|
||||
date TIMESTAMPTZ NOT NULL,
|
||||
PRIMARY KEY (item_id, date)
|
||||
);`
|
||||
_, err = db.conn.Exec(ctx, createTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db PostgresDB) AddItem(ctx context.Context, item domain.DBItem) error {
|
||||
query := `
|
||||
INSERT INTO item (id, name, class_id, game_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`
|
||||
|
||||
_, err := db.conn.Exec(ctx, query,
|
||||
item.ID,
|
||||
item.Name,
|
||||
item.ClassID,
|
||||
item.GameID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db PostgresDB) AddItemHistory(ctx context.Context, dbItemHistory domain.DBItemHistory) error {
|
||||
query := `
|
||||
INSERT INTO item_history (item_id, price, count, date)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`
|
||||
|
||||
_, err := db.conn.Exec(ctx, query,
|
||||
dbItemHistory.ItemID,
|
||||
dbItemHistory.Price,
|
||||
dbItemHistory.Count,
|
||||
dbItemHistory.Date,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db PostgresDB) GetItemByClassID(ctx context.Context, classID string) (*domain.SteamItem, error) {
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
class_id
|
||||
FROM item
|
||||
WHERE class_id = $1
|
||||
`
|
||||
|
||||
row := db.conn.QueryRow(ctx, query, classID)
|
||||
var steamItem domain.SteamItem
|
||||
err := row.Scan(
|
||||
&steamItem.ID,
|
||||
&steamItem.Name,
|
||||
&steamItem.ClassID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &steamItem, nil
|
||||
}
|
||||
|
||||
func (db PostgresDB) GetItemPriceHistoryByID(ctx context.Context, itemID uuid.UUID, limit int, offset int) ([]domain.ItemPriceHistory, error) {
|
||||
query := `
|
||||
SELECT
|
||||
price,
|
||||
count,
|
||||
date
|
||||
FROM item_history
|
||||
WHERE item_id = $1
|
||||
ORDER BY date DESC
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
`
|
||||
|
||||
var itemPriceHistory []domain.ItemPriceHistory
|
||||
rows, err := db.conn.Query(ctx, query,
|
||||
itemID,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var steamItemPriceHistory domain.ItemPriceHistory
|
||||
|
||||
err := rows.Scan(
|
||||
&steamItemPriceHistory.Price,
|
||||
&steamItemPriceHistory.Count,
|
||||
&steamItemPriceHistory.Date,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println("error while iterating rows", err)
|
||||
|
||||
continue
|
||||
}
|
||||
itemPriceHistory = append(itemPriceHistory, steamItemPriceHistory)
|
||||
}
|
||||
|
||||
return itemPriceHistory, nil
|
||||
}
|
||||
|
||||
func (db PostgresDB) GetItemsWithLastPriceByName(ctx context.Context, name string) ([]ItemWithLastPrice, error) {
|
||||
query := `
|
||||
SELECT
|
||||
class_id,
|
||||
name,
|
||||
price,
|
||||
last_date
|
||||
FROM item
|
||||
JOIN (
|
||||
SELECT
|
||||
item_id,
|
||||
MAX(price) AS price,
|
||||
MAX(date) AS last_date
|
||||
FROM item_history
|
||||
GROUP BY item_id
|
||||
) as last_prices
|
||||
ON item.id = last_prices.item_id
|
||||
WHERE LOWER(name) like $1
|
||||
ORDER BY last_date DESC;
|
||||
`
|
||||
|
||||
rows, err := db.conn.Query(ctx, query,
|
||||
"%"+name+"%",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var itemsWithLastPrice []ItemWithLastPrice
|
||||
var itemWithLastPrice ItemWithLastPrice
|
||||
for rows.Next() {
|
||||
err := rows.Scan(
|
||||
&itemWithLastPrice.ClassID,
|
||||
&itemWithLastPrice.Name,
|
||||
&itemWithLastPrice.Price,
|
||||
&itemWithLastPrice.Date,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println("error while scaning row", err)
|
||||
continue
|
||||
}
|
||||
|
||||
itemsWithLastPrice = append(itemsWithLastPrice, itemWithLastPrice)
|
||||
}
|
||||
|
||||
return itemsWithLastPrice, nil
|
||||
}
|
||||
221
internal/repository/sqlite.go
Normal file
221
internal/repository/sqlite.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Package repository for repositorie's
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"steam_analyzer/internal/domain"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SQLLiteDatabase struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSQLLiteDatabase(db *sql.DB) *SQLLiteDatabase {
|
||||
return &SQLLiteDatabase{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectOrCreateDatabase() *sql.DB {
|
||||
// Connect to (or create) the SQLite database
|
||||
db, err := sql.Open("sqlite3", "./steam_items.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (sqldb SQLLiteDatabase) InitDatabaseTables() error {
|
||||
createTable := `
|
||||
CREATE TABLE IF NOT EXISTS item (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT,
|
||||
class_id TEXT UNIQUE NOT NULL,
|
||||
game_id INTEGER NOT NULL,
|
||||
type_id INTEGER
|
||||
);`
|
||||
_, err := sqldb.db.Exec(createTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createTable = `
|
||||
CREATE TABLE IF NOT EXISTS item_history (
|
||||
item_id UUID REFERENCES item(id),
|
||||
price REAL,
|
||||
count INTEGER,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (item_id, date)
|
||||
);`
|
||||
_, err = sqldb.db.Exec(createTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sqldb SQLLiteDatabase) AddItem(item domain.DBItem) error {
|
||||
query := `
|
||||
INSERT INTO item (id, name, class_id, game_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
|
||||
_, err := sqldb.db.Exec(query,
|
||||
item.ID,
|
||||
item.Name,
|
||||
item.ClassID,
|
||||
item.GameID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sqldb SQLLiteDatabase) AddItemHistory(dbItemHistory domain.DBItemHistory) error {
|
||||
query := `
|
||||
INSERT INTO item_history (item_id, price, count, date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
|
||||
_, err := sqldb.db.Exec(query,
|
||||
dbItemHistory.ItemID,
|
||||
dbItemHistory.Price,
|
||||
dbItemHistory.Count,
|
||||
dbItemHistory.Date,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sqldb SQLLiteDatabase) GetItemByClassID(classID string) (*domain.SteamItem, error) {
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
class_id
|
||||
FROM item
|
||||
WHERE class_id = ?
|
||||
`
|
||||
|
||||
row := sqldb.db.QueryRow(query, classID)
|
||||
var steamItem domain.SteamItem
|
||||
err := row.Scan(
|
||||
&steamItem.ID,
|
||||
&steamItem.Name,
|
||||
&steamItem.ClassID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &steamItem, nil
|
||||
}
|
||||
|
||||
func (sqldb SQLLiteDatabase) GetItemPriceHistoryByID(itemID uuid.UUID, limit int, offset int) ([]domain.ItemPriceHistory, error) {
|
||||
query := `
|
||||
SELECT
|
||||
price,
|
||||
count,
|
||||
date
|
||||
FROM item_history
|
||||
WHERE item_id = ?
|
||||
ORDER BY date DESC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
`
|
||||
|
||||
var itemPriceHistory []domain.ItemPriceHistory
|
||||
rows, err := sqldb.db.Query(query,
|
||||
itemID,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var steamItemPriceHistory domain.ItemPriceHistory
|
||||
|
||||
err := rows.Scan(
|
||||
&steamItemPriceHistory.Price,
|
||||
&steamItemPriceHistory.Count,
|
||||
&steamItemPriceHistory.Date,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println("error while iterating rows", err)
|
||||
|
||||
continue
|
||||
}
|
||||
itemPriceHistory = append(itemPriceHistory, steamItemPriceHistory)
|
||||
}
|
||||
|
||||
return itemPriceHistory, nil
|
||||
}
|
||||
|
||||
func (sqldb SQLLiteDatabase) GetItemsWithLastPriceByName(name string) ([]ItemWithLastPrice, error) {
|
||||
query := `
|
||||
SELECT
|
||||
item.name,
|
||||
price,
|
||||
last_date
|
||||
FROM item
|
||||
JOIN (
|
||||
SELECT
|
||||
item_id,
|
||||
price,
|
||||
max(date) as last_date
|
||||
FROM item_history
|
||||
GROUP BY item_id
|
||||
) as last_prices
|
||||
ON item.id = last_prices.item_id
|
||||
WHERE item.name like ?
|
||||
ORDER BY last_date DESC
|
||||
`
|
||||
|
||||
rows, err := sqldb.db.Query(query,
|
||||
"%"+name+"%",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var itemsWithLastPrice []ItemWithLastPrice
|
||||
var itemWithLastPrice ItemWithLastPrice
|
||||
var lastDateStr string
|
||||
layout := "2006-01-02 15:04:05.999999999-07:00"
|
||||
for rows.Next() {
|
||||
err := rows.Scan(
|
||||
&itemWithLastPrice.Name,
|
||||
&itemWithLastPrice.Price,
|
||||
&lastDateStr,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println("error while scaning row", err)
|
||||
continue
|
||||
}
|
||||
|
||||
lastDate, err := time.Parse(layout, lastDateStr)
|
||||
if err != nil {
|
||||
fmt.Println("error while parsing date", err)
|
||||
continue
|
||||
}
|
||||
itemWithLastPrice.Date = lastDate
|
||||
|
||||
itemsWithLastPrice = append(itemsWithLastPrice, itemWithLastPrice)
|
||||
}
|
||||
|
||||
return itemsWithLastPrice, nil
|
||||
}
|
||||
10
internal/repository/type.go
Normal file
10
internal/repository/type.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package repository
|
||||
|
||||
import "time"
|
||||
|
||||
type ItemWithLastPrice struct {
|
||||
ClassID string
|
||||
Name string
|
||||
Price float64
|
||||
Date time.Time
|
||||
}
|
||||
129
internal/service/item.go
Normal file
129
internal/service/item.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Package service for service's
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"math"
|
||||
"steam_analyzer/internal/domain"
|
||||
"steam_analyzer/internal/repository"
|
||||
"steam_analyzer/internal/utils"
|
||||
"steam_analyzer/internal/utils/slogutils"
|
||||
"steam_analyzer/pkg/postgresql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type ItemRepository interface {
|
||||
AddItem(ctx context.Context, item domain.DBItem) error
|
||||
AddItemHistory(ctx context.Context, dbItemHistory domain.DBItemHistory) error
|
||||
GetItemByClassID(ctx context.Context, classID string) (*domain.SteamItem, error)
|
||||
GetItemPriceHistoryByID(ctx context.Context, itemID uuid.UUID, limit int, offset int) ([]domain.ItemPriceHistory, error)
|
||||
GetItemsWithLastPriceByName(ctx context.Context, name string) ([]repository.ItemWithLastPrice, error)
|
||||
}
|
||||
|
||||
type ItemService struct {
|
||||
logger *slog.Logger
|
||||
|
||||
itemRepo ItemRepository
|
||||
}
|
||||
|
||||
func NewItemService(logger *slog.Logger, itemRepo ItemRepository) *ItemService {
|
||||
return &ItemService{
|
||||
logger: logger,
|
||||
itemRepo: itemRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ItemService) AddItemWithHistory(ctx context.Context, item domain.SteamItem) error {
|
||||
searchedItem, err := s.itemRepo.GetItemByClassID(ctx, item.ClassID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
s.logger.Info("Item is new, creating it")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if searchedItem != nil {
|
||||
item.ID = searchedItem.ID
|
||||
} else {
|
||||
item.ID = uuid.New()
|
||||
|
||||
err = s.itemRepo.AddItem(ctx, item.ToDB())
|
||||
if err != nil {
|
||||
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == postgresql.ErrConstraintUnique {
|
||||
s.logger.Error("Error while adding item", slog.String("error", "Item already exists"))
|
||||
} else {
|
||||
s.logger.Error("Error while adding item", slogutils.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = s.itemRepo.AddItemHistory(ctx, item.ToDBHistory())
|
||||
if err != nil {
|
||||
s.logger.Error("Error while adding item history", slogutils.Error(sql.ErrNoRows))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ItemService) GetItemWithPriceHistoryByClassID(ctx context.Context, classID string, limit int, offset int) (*domain.SteamItem, error) {
|
||||
steamItem, err := s.itemRepo.GetItemByClassID(ctx, classID)
|
||||
if err != nil {
|
||||
s.logger.Error("Error while getting item by class id", slogutils.Error(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
itemPriceHistory, err := s.itemRepo.GetItemPriceHistoryByID(ctx, steamItem.ID, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("Error while getting item price history by id", slogutils.Error(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
steamItem.History = itemPriceHistory
|
||||
|
||||
return steamItem, nil
|
||||
}
|
||||
|
||||
func (s ItemService) GetAllItemsWithLastPrice(ctx context.Context, itemName string) ([]repository.ItemWithLastPrice, error) {
|
||||
s.logger.Info("ItemService.GetAllItemsWithLastPrice", slog.String("item name is", itemName))
|
||||
|
||||
items, err := s.itemRepo.GetItemsWithLastPriceByName(ctx, itemName)
|
||||
if err != nil {
|
||||
s.logger.Error("Error while getting item with last price", slogutils.Error(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
items[i].Price = formatPrice(item.Price)
|
||||
items[i].Date = formatDate(item.Date)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func formatPrice(price float64) float64 {
|
||||
formattedPrice := price / 100 * utils.CURRENCY_RUB
|
||||
formattedPrice = math.Round(formattedPrice*100) / 100
|
||||
|
||||
return formattedPrice
|
||||
}
|
||||
|
||||
func formatDate(date time.Time) time.Time {
|
||||
location, _ := time.LoadLocation("Europe/Moscow")
|
||||
foramettedDate := date.In(location)
|
||||
|
||||
return foramettedDate
|
||||
}
|
||||
107
internal/steamreq/requester.go
Normal file
107
internal/steamreq/requester.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package steamreq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"steam_analyzer/internal/utils/jsonutils"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type Categories struct {
|
||||
caseCat string
|
||||
knife string
|
||||
}
|
||||
|
||||
type Requester struct {
|
||||
baseMarketURL string
|
||||
categories Categories
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func NewRequester(baseURL string, perTime time.Duration, count int) *Requester {
|
||||
return &Requester{
|
||||
baseMarketURL: baseURL,
|
||||
categories: Categories{
|
||||
caseCat: "category_730_Type[]=tag_CSGO_Type_WeaponCase",
|
||||
knife: "category_730_Type[]=tag_CSGO_Type_Knife",
|
||||
},
|
||||
limiter: rate.NewLimiter(rate.Every(perTime), count),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Requester) GetItem(query string, itemType string) ([]SteamMarketItem, error) {
|
||||
var steamMarketItems []SteamMarketItem
|
||||
|
||||
var pageCount int = 1
|
||||
for i := 0; i < pageCount; i++ {
|
||||
//if err := r.limiter.Wait(context.Background()); err != nil {
|
||||
// fmt.Printf("Limiter error: %v\n", err)
|
||||
// continue
|
||||
//}
|
||||
|
||||
response, err := r.doRequest(query, itemType, i)
|
||||
if err != nil {
|
||||
fmt.Printf("Error while doing request. Query: %v. ItemType: %v. Start %v", query, itemType, i)
|
||||
continue
|
||||
}
|
||||
|
||||
steamMarketItems = append(steamMarketItems, response.Results...)
|
||||
if i == 0 {
|
||||
pageCount = int(math.Ceil(float64(response.TotalCount) / float64(response.Pagesize)))
|
||||
}
|
||||
}
|
||||
|
||||
return steamMarketItems, nil
|
||||
}
|
||||
|
||||
func (r *Requester) doRequest(query string, itemType string, start int) (*Response, error) {
|
||||
itemURL := r.createItemURL(query, itemType, start*10)
|
||||
|
||||
// fmt.Println(itemURL)
|
||||
|
||||
req, err := http.NewRequest("GET", itemURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while creating request %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while making http request %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var steamResponse Response
|
||||
err = jsonutils.FromJSON(resp.Body, &steamResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while getting item %v", err)
|
||||
}
|
||||
|
||||
return &steamResponse, nil
|
||||
}
|
||||
|
||||
func (r *Requester) createItemURL(item string, itemType string, start int) string {
|
||||
marketItemsURL := r.baseMarketURL + `?query=%v&start=%v&count=%v&search_descriptions=0&sort_column=%v&sort_dir=%v&appid=730&norender=1`
|
||||
count := 10
|
||||
sortColumn := "price"
|
||||
sortDir := "asc"
|
||||
|
||||
url := fmt.Sprintf(marketItemsURL,
|
||||
item,
|
||||
start,
|
||||
count,
|
||||
sortColumn,
|
||||
sortDir,
|
||||
)
|
||||
|
||||
switch itemType {
|
||||
case "case":
|
||||
url = url + "&" + r.categories.caseCat
|
||||
case "knife":
|
||||
url = url + "&" + r.categories.knife
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
33
internal/steamreq/type.go
Normal file
33
internal/steamreq/type.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package steamreq
|
||||
|
||||
type Response struct {
|
||||
Start int `json:"start"`
|
||||
Pagesize int `json:"pagesize"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Results []SteamMarketItem `json:"results"`
|
||||
}
|
||||
|
||||
type SteamMarketItem struct {
|
||||
Name string `json:"name"`
|
||||
HashName string `json:"hash_name"`
|
||||
SellListings int `json:"sell_listings"`
|
||||
SellPrice float64 `json:"sell_price"`
|
||||
SellPriceText string `json:"sell_price_text"`
|
||||
AppIcon string `json:"app_icon"`
|
||||
AppName string `json:"app_name"`
|
||||
AssetDescription struct {
|
||||
Appid int `json:"appid"`
|
||||
Classid string `json:"classid"`
|
||||
Instanceid string `json:"instanceid"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Tradable int `json:"tradable"`
|
||||
Name string `json:"name"`
|
||||
NameColor string `json:"name_color"`
|
||||
Type string `json:"type"`
|
||||
MarketName string `json:"market_name"`
|
||||
MarketHashName string `json:"market_hash_name"`
|
||||
Commodity int `json:"commodity"`
|
||||
} `json:"asset_description"`
|
||||
SalePriceText string `json:"sale_price_text"`
|
||||
}
|
||||
80
internal/steamreq/worker.go
Normal file
80
internal/steamreq/worker.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package steamreq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"steam_analyzer/internal/domain"
|
||||
"steam_analyzer/internal/utils/slogutils"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IRequester interface {
|
||||
GetItem(query string, itemType string) ([]SteamMarketItem, error)
|
||||
}
|
||||
|
||||
type ItemService interface {
|
||||
AddItemWithHistory(ctx context.Context, item domain.SteamItem) error
|
||||
}
|
||||
|
||||
type SteamReqWorker struct {
|
||||
logger *slog.Logger
|
||||
requester IRequester
|
||||
itemService ItemService
|
||||
reqPer time.Duration
|
||||
}
|
||||
|
||||
func NewSteamWorker(logger *slog.Logger, requester IRequester, itemService ItemService, reqPer time.Duration) *SteamReqWorker {
|
||||
return &SteamReqWorker{
|
||||
logger: logger,
|
||||
requester: requester,
|
||||
itemService: itemService,
|
||||
reqPer: reqPer,
|
||||
}
|
||||
}
|
||||
|
||||
type SteamItems struct {
|
||||
Query string
|
||||
ItemType string
|
||||
}
|
||||
|
||||
func (w SteamReqWorker) Start(ctx context.Context, items []SteamItems) {
|
||||
for {
|
||||
for _, item := range items {
|
||||
items, err := w.requester.GetItem(item.Query, item.ItemType)
|
||||
if err != nil {
|
||||
w.logger.Error("Error while request item from steam", slogutils.Error(err))
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
w.logger.Info("Succesfully get items", slog.String("items count is", strconv.Itoa(len(items))))
|
||||
|
||||
domainItems := CreateItemsFromRequest(items)
|
||||
for _, domainItem := range domainItems {
|
||||
err := w.itemService.AddItemWithHistory(ctx, domainItem)
|
||||
if err != nil {
|
||||
w.logger.Error("Error while adding item with history", slogutils.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(w.reqPer)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateItemsFromRequest(smi []SteamMarketItem) []domain.SteamItem {
|
||||
steamItem := make([]domain.SteamItem, 0, len(smi))
|
||||
|
||||
for _, item := range smi {
|
||||
steamItem = append(steamItem, domain.SteamItem{
|
||||
Name: item.Name,
|
||||
ClassID: item.AssetDescription.Classid,
|
||||
GameID: item.AssetDescription.Appid,
|
||||
Price: item.SellPrice,
|
||||
Count: item.SellListings,
|
||||
})
|
||||
}
|
||||
|
||||
return steamItem
|
||||
}
|
||||
16
internal/utils/jsonutils/jsonutils.go
Normal file
16
internal/utils/jsonutils/jsonutils.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package jsonutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ToJSON(w io.Writer, v interface{}) error {
|
||||
e := json.NewEncoder(w)
|
||||
return e.Encode(v)
|
||||
}
|
||||
|
||||
func FromJSON(r io.Reader, v interface{}) error {
|
||||
d := json.NewDecoder(r)
|
||||
return d.Decode(v)
|
||||
}
|
||||
10
internal/utils/slogutils/slogutils.go
Normal file
10
internal/utils/slogutils/slogutils.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package slogutils
|
||||
|
||||
import "log/slog"
|
||||
|
||||
func Error(err error) slog.Attr {
|
||||
return slog.Attr{
|
||||
Key: "error",
|
||||
Value: slog.StringValue(err.Error()),
|
||||
}
|
||||
}
|
||||
3
internal/utils/utils.go
Normal file
3
internal/utils/utils.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package utils
|
||||
|
||||
var CURRENCY_RUB = 78.41
|
||||
33
pkg/logger/logger.go
Normal file
33
pkg/logger/logger.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
envLocal = "local"
|
||||
envDevelopment = "development"
|
||||
envProduction = "production"
|
||||
)
|
||||
|
||||
func New(env string) *slog.Logger {
|
||||
var logger *slog.Logger
|
||||
|
||||
switch env {
|
||||
case envLocal:
|
||||
logger = slog.New(
|
||||
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}),
|
||||
)
|
||||
case envDevelopment:
|
||||
logger = slog.New(
|
||||
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}),
|
||||
)
|
||||
case envProduction:
|
||||
logger = slog.New(
|
||||
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}),
|
||||
)
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
11
pkg/postgresql/errors.go
Normal file
11
pkg/postgresql/errors.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package postgresql
|
||||
|
||||
import "github.com/jackc/pgx/v5"
|
||||
|
||||
var (
|
||||
ErrNoRows = pgx.ErrNoRows
|
||||
)
|
||||
|
||||
const (
|
||||
ErrConstraintUnique = "23505"
|
||||
)
|
||||
82
pkg/postgresql/migrations.go
Normal file
82
pkg/postgresql/migrations.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func RunMigrationsWithString(connectionString string, migrationDir string) error {
|
||||
m, err := migrate.New(
|
||||
"file://"+migrationDir,
|
||||
connectionString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
if err := m.Up(); err != nil {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Migrations applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
type MigrationHandler struct {
|
||||
migrate *migrate.Migrate
|
||||
}
|
||||
|
||||
func (migration *MigrationHandler) Up() error {
|
||||
if err := migration.migrate.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Migrations applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *MigrationHandler) Down() error {
|
||||
if err := migration.migrate.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Migrations applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMigrationWithInstance(pool *pgxpool.Pool, migrationDir string) (*MigrationHandler, error) {
|
||||
db := stdlib.OpenDBFromPool(pool)
|
||||
|
||||
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create migration driver: %w", err)
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://"+migrationDir,
|
||||
"postgres", driver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create migrator: %w", err)
|
||||
}
|
||||
|
||||
//m.Down()
|
||||
|
||||
/*if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Migrations applied")*/
|
||||
|
||||
return &MigrationHandler{
|
||||
migrate: m,
|
||||
}, nil
|
||||
}
|
||||
33
pkg/postgresql/pgx.go
Normal file
33
pkg/postgresql/pgx.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type PGXClient interface {
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
ExecContext(ctx context.Context, query string, arguments ...interface{}) (sql.Result, error)
|
||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
||||
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
|
||||
}
|
||||
|
||||
func NewPGXClient(psqlInfo string) (*sql.DB, error) {
|
||||
const op = "repository.postgresql.pq.NewConnection"
|
||||
|
||||
db, err := sql.Open("pgx", psqlInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
26
pkg/postgresql/pgx_pool.go
Normal file
26
pkg/postgresql/pgx_pool.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type PGXPool interface {
|
||||
Exec(ctx context.Context, query string, args ...any) (pgconn.CommandTag, error)
|
||||
Query(ctx context.Context, query string, args ...any) (pgx.Rows, error)
|
||||
QueryRow(ctx context.Context, query string, args ...any) pgx.Row
|
||||
SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults
|
||||
BeginTx(ctx context.Context, opts pgx.TxOptions) (pgx.Tx, error)
|
||||
}
|
||||
|
||||
func NewPGXPool(ctx context.Context, connectionString string) (*pgxpool.Pool, error) {
|
||||
pool, err := pgxpool.New(ctx, connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
41
pkg/postgresql/pq.go
Normal file
41
pkg/postgresql/pq.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type PQClient interface {
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
Exec(query string, arguments ...interface{}) (sql.Result, error)
|
||||
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||
QueryRow(query string, args ...interface{}) *sql.Row
|
||||
// Begin(ctx context.Context) (*sql.Tx, error)
|
||||
}
|
||||
|
||||
type PQClientContext interface {
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
ExecContext(ctx context.Context, query string, arguments ...interface{}) (sql.Result, error)
|
||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
||||
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
|
||||
}
|
||||
|
||||
func NewPQClient(psqlInfo string) (*sql.DB, error) {
|
||||
const op = "repository.postgresql.pq.NewConnection"
|
||||
|
||||
db, err := sql.Open("postgres", psqlInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
19
pkg/postgresql/utils.go
Normal file
19
pkg/postgresql/utils.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"steam_analyzer/internal/config"
|
||||
)
|
||||
|
||||
func CreateDBConnectionString(cfg config.Database) string {
|
||||
return fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
|
||||
cfg.User,
|
||||
url.QueryEscape(cfg.Password),
|
||||
cfg.Host,
|
||||
cfg.Port,
|
||||
cfg.Name,
|
||||
cfg.Sslmode,
|
||||
)
|
||||
}
|
||||
58
pkg/server/server.go
Normal file
58
pkg/server/server.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Package server
|
||||
// Contain struct and interface for project server
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"steam_analyzer/internal/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ILogger interface {
|
||||
Info(msg string, args ...any)
|
||||
Debug(msg string, args ...any)
|
||||
Error(msg string, args ...any)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
logger ILogger
|
||||
}
|
||||
|
||||
type ServerParam struct {
|
||||
Host string
|
||||
Port string
|
||||
Origins []string
|
||||
IdleTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
}
|
||||
|
||||
func New(cfg config.Server, log ILogger, h http.Handler) *Server {
|
||||
return &Server{
|
||||
httpServer: &http.Server{
|
||||
Addr: cfg.Host + ":" + cfg.Port,
|
||||
Handler: configureCORSFor(h, cfg.Origins),
|
||||
// ErrorLog: log,
|
||||
IdleTimeout: cfg.IdleTimeout,
|
||||
WriteTimeout: cfg.WriteTimeout,
|
||||
ReadTimeout: cfg.ReadTimeout,
|
||||
},
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run() {
|
||||
s.logger.Info(fmt.Sprintf("Server started at %s", s.httpServer.Addr))
|
||||
|
||||
if err := s.httpServer.ListenAndServe(); err != nil {
|
||||
s.logger.Error("Cannot start server", slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
27
pkg/server/utils.go
Normal file
27
pkg/server/utils.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
func configureCORSFor(handler http.Handler, origins []string) http.Handler {
|
||||
ch := cors.New(cors.Options{
|
||||
// # http://mywebsite-domain.com/ is configured in hosts (localhost:80 alias)
|
||||
AllowedOrigins: origins,
|
||||
AllowedMethods: []string{
|
||||
http.MethodPost,
|
||||
http.MethodGet,
|
||||
http.MethodPut,
|
||||
http.MethodDelete,
|
||||
// http.MethodOptions,
|
||||
},
|
||||
OptionsPassthrough: false,
|
||||
AllowCredentials: true,
|
||||
|
||||
// Debug: true,
|
||||
})
|
||||
|
||||
return ch.Handler(handler)
|
||||
}
|
||||
5
pkg/utils/httputils/httputils.go
Normal file
5
pkg/utils/httputils/httputils.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package httputils
|
||||
|
||||
type HTTPError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
19
pkg/utils/jsonutils/jsonutils.go
Normal file
19
pkg/utils/jsonutils/jsonutils.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package jsonutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ToJSON serializes the given interface into a string based JSON format
|
||||
func ToJSON(i interface{}, w io.Writer) error {
|
||||
e := json.NewEncoder(w)
|
||||
return e.Encode(i)
|
||||
}
|
||||
|
||||
// FromJSON deserializes the object from JSON string
|
||||
// in an io.Reader to the given interface
|
||||
func FromJSON(i interface{}, r io.Reader) error {
|
||||
d := json.NewDecoder(r)
|
||||
return d.Decode(i)
|
||||
}
|
||||
Reference in New Issue
Block a user