Compare commits

...

87 commits
v0.1.0 ... main

Author SHA1 Message Date
nquidox
8f2b0470b1 false zero price bugfix
All checks were successful
/ Make image (push) Successful in 1m3s
2025-12-07 13:37:15 +03:00
nquidox
4c59ab3f58 return origin name instead of code
All checks were successful
/ Make image (push) Successful in 1m15s
2025-12-06 19:14:21 +03:00
nquidox
f9eac067be swagger docs update 2025-12-06 17:33:29 +03:00
nquidox
d997d8bfa4 added: delete zero prices in period 2025-12-06 17:33:18 +03:00
nquidox
a338fd03b2 added: time util 2025-12-06 17:32:53 +03:00
nquidox
aeb5cb819b сhanged ownership validation for user's merch
All checks were successful
/ Make image (push) Successful in 1m30s
2025-11-04 16:49:04 +03:00
nquidox
7fa79d770a swagger docs update
All checks were successful
/ Make image (push) Successful in 1m3s
2025-11-02 23:39:25 +03:00
nquidox
2728051fde fixes 2025-11-02 23:27:23 +03:00
nquidox
a0e21db5a0 swagger docs update 2025-11-02 21:10:59 +03:00
nquidox
93ce93770d zero prices check added 2025-11-02 21:10:49 +03:00
nquidox
88fcbfe1a5 routes + repo fix 2025-11-02 20:59:25 +03:00
nquidox
8186d8a46c getMerchLabels + fixes
All checks were successful
/ Make image (push) Successful in 1m3s
2025-10-29 20:56:26 +03:00
nquidox
b6f7875710 update 2025-10-29 20:55:51 +03:00
nquidox
8ac753f632 labels added to dto 2025-10-28 21:46:40 +03:00
nquidox
565e019a67 swagger docs update 2025-10-28 20:30:05 +03:00
nquidox
f7ec1bce1e small fixes 2025-10-28 20:29:14 +03:00
nquidox
844561ef70 update 2025-10-28 20:28:40 +03:00
nquidox
9895b86666 labels crud added 2025-10-28 20:06:32 +03:00
nquidox
475ff9919b tables added 2025-10-28 18:22:40 +03:00
nquidox
489f749ce3 minio disabled
All checks were successful
/ Make image (push) Successful in 1m8s
2025-10-26 21:59:49 +03:00
nquidox
7937d182db swagger docs update
All checks were successful
/ Make image (push) Successful in 1m9s
2025-10-26 19:55:49 +03:00
nquidox
37a1dfbf52 swagger docs update 2025-10-26 19:54:51 +03:00
nquidox
f13012b742 update 2025-10-26 19:54:43 +03:00
nquidox
212ce0a5c4 image storage added 2025-10-26 19:54:34 +03:00
nquidox
f5ca21ca68 deprecated comment 2025-10-26 19:54:10 +03:00
nquidox
fa8990ed8c new image provider 2025-10-26 19:53:49 +03:00
nquidox
a3fbd3b8e0 error handling 2025-10-26 19:53:32 +03:00
nquidox
e90852cc95 factor out tp methods 2025-10-26 19:53:14 +03:00
nquidox
dae627f4ad image storage contract added 2025-10-26 19:52:46 +03:00
nquidox
3298602a23 switch from pre-signed to public images
All checks were successful
/ Make image (push) Successful in 1m31s
2025-10-19 19:43:33 +03:00
nquidox
947220b65c endpoint env refactor
All checks were successful
/ Make image (push) Successful in 1m22s
2025-10-18 16:38:21 +03:00
nquidox
f3d123ee3b replace domain for links
All checks were successful
/ Make image (push) Successful in 1m35s
2025-10-18 16:07:56 +03:00
nquidox
bb305eab9e replace domain for links
All checks were successful
/ Make image (push) Successful in 1m21s
2025-10-18 15:32:18 +03:00
nquidox
0348dda5cd switch back to alpine
All checks were successful
/ Make image (push) Successful in 1m31s
2025-10-18 14:57:43 +03:00
nquidox
f561869b08 swagger docs update
All checks were successful
/ Make image (push) Successful in 1m29s
2025-10-18 14:11:58 +03:00
nquidox
c2304f6a7d update 2025-10-17 23:48:05 +03:00
nquidox
bc6621154b return etag 2025-10-17 23:47:48 +03:00
nquidox
d1542b274e switch to ubuntu 2025-10-17 23:46:34 +03:00
nquidox
2feda33e26 dev
All checks were successful
/ Make image (push) Successful in 1m39s
2025-10-16 21:00:23 +03:00
nquidox
f6bb647591 update
All checks were successful
/ Make image (push) Successful in 2m0s
2025-10-16 20:55:24 +03:00
nquidox
2d2afffcaf create client log + secure mode env
All checks were successful
/ Make image (push) Successful in 1m21s
2025-10-16 15:41:52 +03:00
nquidox
95b75d0067 change from creation to exists check
All checks were successful
/ Make image (push) Successful in 1m14s
2025-10-15 21:34:32 +03:00
nquidox
38193e8943 conf fix
All checks were successful
/ Make image (push) Successful in 1m14s
2025-10-15 20:48:05 +03:00
nquidox
1fc273c62f update 2025-10-15 20:47:15 +03:00
nquidox
a29d5a8766 update
Some checks failed
/ Make image (push) Failing after 1m4s
2025-10-15 20:28:15 +03:00
nquidox
b469313701 update
Some checks failed
/ Make image (push) Failing after 1m6s
2025-10-15 20:18:19 +03:00
nquidox
d23529e089 swagger docs update
Some checks failed
/ Make image (push) Has been cancelled
2025-10-15 19:46:22 +03:00
nquidox
dec89435a3 merch images crud 2025-10-15 19:46:10 +03:00
nquidox
e708c92d18 media storage config + env 2025-10-15 19:45:49 +03:00
nquidox
262d02e915 update 2025-10-15 19:44:52 +03:00
nquidox
1a67c02e00 created media storage package + interface 2025-10-15 19:44:41 +03:00
nquidox
218e7d652f update
All checks were successful
/ Make image (push) Successful in 1m6s
2025-10-12 14:52:19 +03:00
nquidox
a75d6240b3 action update 2025-10-10 21:58:50 +03:00
nquidox
1b3259a73b action update
Some checks failed
/ Make image (push) Failing after 3s
2025-10-10 20:08:29 +03:00
nquidox
aa846682c9 action update
Some checks failed
/ Make image (push) Failing after 11s
2025-10-10 19:39:21 +03:00
nquidox
a8c81f8ee1 action update
Some checks failed
/ Make image (push) Failing after 3s
2025-10-10 19:36:24 +03:00
nquidox
831734fe23 action update
Some checks failed
/ Make image (push) Failing after 9s
2025-10-10 19:14:10 +03:00
nquidox
f98357f766 action update
Some checks failed
/ Make image (push) Failing after 3s
2025-10-10 18:32:02 +03:00
nquidox
3a529fbc5c action update
Some checks failed
/ Make image (push) Failing after 3s
2025-10-10 18:30:55 +03:00
nquidox
ce45f425b6 action update
Some checks failed
/ Make image (push) Failing after 3s
2025-10-10 18:24:22 +03:00
nquidox
c668f87c96 action update
Some checks failed
/ Make image (push) Has been cancelled
2025-10-10 18:19:15 +03:00
nquidox
dcb7a4727f action update
Some checks failed
/ Make image (push) Failing after 6s
2025-10-09 19:48:37 +03:00
nquidox
5bb7ce6a45 action update
Some checks failed
/ Make image (push) Failing after 7s
2025-10-09 19:28:21 +03:00
nquidox
56d945c698 action update
Some checks failed
/ Make image (push) Failing after 6s
2025-10-09 19:12:53 +03:00
nquidox
87f4a0bdff action update
Some checks failed
/ Make image (push) Has been cancelled
2025-10-09 18:51:34 +03:00
nquidox
ec268439a1 action update
Some checks failed
/ Make image (push) Has been cancelled
2025-10-09 18:47:21 +03:00
nquidox
25a7389e63 action update
Some checks failed
/ Make image (push) Failing after 7s
2025-10-09 18:26:51 +03:00
nquidox
00e01d5c6f action update
Some checks failed
/ Make image (push) Failing after 1m11s
2025-10-09 17:44:06 +03:00
nquidox
73a7cc9d3a action update 2025-10-09 17:28:41 +03:00
nquidox
d49e4ba2d2 action update 2025-10-09 17:20:32 +03:00
nquidox
bb231b1f9d action update 2025-10-09 16:37:39 +03:00
nquidox
be5e3f6d48 action update
Some checks failed
/ Make image (push) Failing after 3s
2025-10-08 18:30:31 +03:00
nquidox
ff44577015 check for empty link removed 2025-10-07 20:40:31 +03:00
nquidox
e09a3f1d55 logs 2025-10-06 22:27:33 +03:00
nquidox
944d9482fd update 2025-10-06 20:40:10 +03:00
nquidox
b3df73d595 log added 2025-10-06 20:39:29 +03:00
nquidox
ad880b3fb4 where condition added 2025-10-05 16:24:48 +03:00
nquidox
6a21576c1c insert prices method 2025-10-05 15:31:00 +03:00
nquidox
acb8b8d4a0 update 2025-10-05 15:30:34 +03:00
nquidox
e4f55bb195 price type change 2025-10-03 19:14:13 +03:00
nquidox
6867d2d74e grpc server added 2025-10-01 19:32:56 +03:00
nquidox
5e1017df69 created 2025-10-01 19:32:27 +03:00
nquidox
850fb38e16 removed 2025-10-01 19:32:06 +03:00
nquidox
2ada5e5a9e update 2025-10-01 19:31:58 +03:00
nquidox
4759e7638c switch update to upsert 2025-10-01 12:13:43 +03:00
nquidox
a4097706de merch update method refactor 2025-10-01 11:00:39 +03:00
nquidox
7dd4a6830e release option fix 2025-09-30 18:47:23 +03:00
42 changed files with 5118 additions and 186 deletions

View file

@ -2,20 +2,29 @@ on:
push:
tags:
- 'v[0-9]+*'
workflow_dispatch:
env:
IMAGE_NAME: repo-app
IMAGE_NAME: mtv2-repo-app
jobs:
docker:
build-and-push:
name: Make image
runs-on: docker
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Docker Registry
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login repo.nqws.ru -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Forgejo
uses: docker/login-action@v3
with:
registry: repo.nqws.ru
username: ${{ secrets.MAINTAINER_USERNAME }}
password: ${{ secrets.MAINTAINER_TOKEN }}
- name: Extract version from tag
id: extract_version
@ -26,6 +35,6 @@ jobs:
- name: Make image
run: |
docker buildx build --platform linux/amd64 \
--tag repo.nqws.ru/merch-tracker/repo-app-v2:latest \
--tag repo.nqws.ru/merch-tracker/repo-app-v2:${{ env.VERSION }} \
--tag repo.nqws.ru/${{ github.repository }}:latest \
--tag repo.nqws.ru/${{ github.repository }}:${{ env.VERSION }} \
--push .

6
.gitignore vendored
View file

@ -1,3 +1,7 @@
.idea
.env
/config/devConfig.go
/config/devConfig.go
*.sh
!gen-swag.sh
.directory
*.tar

View file

@ -1,34 +1,21 @@
FROM golang:1.25.1-alpine3.22 AS builder
RUN apk add --no-cache \
bash \
curl \
git \
ca-certificates
RUN apk add --no-cache tzdata
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o main "./cmd"
FROM alpine:3.22
RUN apk add --no-cache \
bash \
curl \
ca-certificates \
tzdata
tzdata \
ca-certificates
COPY --from=builder /app/main /usr/local/bin/app
RUN chmod +x /usr/local/bin/app
RUN adduser -D -s /bin/bash appuser
USER appuser
ENTRYPOINT ["app"]

11
api.env
View file

@ -5,6 +5,17 @@ APP_API_PREFIX=/api/v2
APP_GIN_MODE=development
APP_ALLOWED_ORIGINS=http://localhost:5173,
GRPC_SERVER_PORT=9050
GRPC_CLIENT_PORT=9060
IMAGE_STORAGE_HOST=
IMAGE_STORAGE_PORT=
MEDIA_STORAGE_ENDPOINT=
MEDIA_STORAGE_USER=
MEDIA_STORAGE_PASSWORD=
MEDIA_STORAGE_SECURE=false
DB_HOST=
DB_PORT=
DB_USER=

View file

@ -8,7 +8,10 @@ import (
"merch-parser-api/internal/api/merch"
"merch-parser-api/internal/api/user"
"merch-parser-api/internal/app"
"merch-parser-api/internal/grpcService"
"merch-parser-api/internal/imagesProvider"
"merch-parser-api/internal/interfaces"
"merch-parser-api/internal/mediaStorage"
"merch-parser-api/internal/provider/auth"
"merch-parser-api/internal/provider/token"
"merch-parser-api/internal/router"
@ -26,8 +29,7 @@ import (
func main() {
log.Debug("Starting merch-parser-api")
//setup config
//c := config.NewConfig()
c := config.DevConfig()
c := config.NewConfig()
ctx := context.Background()
//log level
@ -50,6 +52,19 @@ func main() {
utilsProvider := utils.NewUtils()
log.Debug("Utils provider initialized")
mediaProvider := mediaStorage.NewHandler(mediaStorage.Deps{
Endpoint: c.MediaConf.Endpoint,
User: c.MediaConf.User,
Password: c.MediaConf.Password,
Secure: c.MediaConf.Secure,
})
log.WithFields(log.Fields{
"endpoint": c.MediaConf.Endpoint,
"provider": mediaProvider,
}).Debug("Media storage | Minio client created")
imageProvider := imagesProvider.NewClient(c.ImageConf.Host + ":" + c.ImageConf.Port)
//deps providers
routerHandler := router.NewRouter(router.Deps{
ApiPrefix: c.AppConf.ApiPrefix,
@ -66,6 +81,10 @@ func main() {
})
log.Debug("Auth provider initialized")
tasksRepo := merch.NewTaskRepository(database)
tasksProvider := merch.NewTaskProvider(tasksRepo)
grpcServer := grpcService.NewGrpcServer(tasksProvider)
//register app modules
users := user.NewHandler(user.Deps{
Auth: authProvider,
@ -77,6 +96,8 @@ func main() {
merchModule := merch.NewHandler(merch.Deps{
DB: database,
Utils: utilsProvider,
//Media: mediaProvider,
ImageStorage: imageProvider,
})
//collect modules
@ -87,11 +108,14 @@ func main() {
//keep last
appl := app.NewApp(app.Deps{
Host: c.AppConf.Host,
Port: c.AppConf.Port,
ApiPrefix: c.AppConf.ApiPrefix,
RouterHandler: routerHandler,
Modules: modules,
Host: c.AppConf.Host,
Port: c.AppConf.Port,
ApiPrefix: c.AppConf.ApiPrefix,
RouterHandler: routerHandler,
Modules: modules,
GrpcServer: grpcServer,
GrpcServerPort: c.GrpcConf.GrpcServerPort,
GrpcClientPort: c.GrpcConf.GrpcClientPort,
})
err = appl.Run(ctx)

View file

@ -3,9 +3,12 @@ package config
import "strings"
type Config struct {
AppConf AppConfig
DBConf DatabaseConfig
JWTConf JWTConfig
AppConf AppConfig
DBConf DatabaseConfig
JWTConf JWTConfig
GrpcConf GrpcConfig
MediaConf MediaConfig
ImageConf ImageStorageConfig
}
type AppConfig struct {
@ -34,6 +37,23 @@ type JWTConfig struct {
RefreshExpire string
}
type GrpcConfig struct {
GrpcServerPort string
GrpcClientPort string
}
type MediaConfig struct {
Endpoint string
User string
Password string
Secure string
}
type ImageStorageConfig struct {
Host string
Port string
}
func NewConfig() *Config {
return &Config{
AppConf: AppConfig{
@ -61,5 +81,22 @@ func NewConfig() *Config {
AccessExpire: getEnv("JWT_ACCESS_EXPIRE", ""),
RefreshExpire: getEnv("JWT_REFRESH_EXPIRE", ""),
},
GrpcConf: GrpcConfig{
GrpcServerPort: getEnv("GRPC_SERVER_PORT", ""),
GrpcClientPort: getEnv("GRPC_CLIENT_PORT", ""),
},
MediaConf: MediaConfig{
Endpoint: getEnv("MEDIA_STORAGE_ENDPOINT", ""),
User: getEnv("MEDIA_STORAGE_USER", ""),
Password: getEnv("MEDIA_STORAGE_PASSWORD", ""),
Secure: getEnv("MEDIA_STORAGE_SECURE", ""),
},
ImageConf: ImageStorageConfig{
Host: getEnv("IMAGE_STORAGE_HOST", ""),
Port: getEnv("IMAGE_STORAGE_PORT", ""),
},
}
}

View file

@ -68,6 +68,9 @@ const docTemplate = `{
}
],
"description": "Получить все записи мерча",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -95,20 +98,168 @@ const docTemplate = `{
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"consumes": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Обновить информацию про мерч",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.UpdateMerchDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/{uuid}": {
"/merch/images/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"tags": [
"Merch"
"description": "Получить картинки по merch_uuid и query параметрам",
"produces": [
"application/json"
],
"summary": "Получить всю информацию про мерч",
"tags": [
"Merch images"
],
"summary": "Получить картинки по merch_uuid и query параметрам",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "string",
"description": "image type",
"name": "type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/merch.ImageLink"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"Merch images"
],
"summary": "Загрузить картинку по merch_uuid",
"parameters": [
{
"type": "string",
"description": "Merch UUID",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "file",
"description": "Image file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/imageStorage.UploadMerchImageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить (безвозвратно) картинки по merch_uuid",
"tags": [
"Merch images"
],
"summary": "Удалить (безвозвратно) картинки по merch_uuid",
"parameters": [
{
"type": "string",
@ -118,11 +269,48 @@ const docTemplate = `{
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить все метки товаров",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить все метки товаров",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/merch.MerchDTO"
"type": "array",
"items": {
"$ref": "#/definitions/merch.LabelsList"
}
}
},
"400": {
@ -139,28 +327,425 @@ const docTemplate = `{
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Создать новую метку для товара",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Создать новую метку для товара",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/attach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Прикрепить метку к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Прикрепить метку к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/detach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить привязку метки к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Удалить привязку метки к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить метки товара по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить метки товара по его uuid",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"tags": [
"Merch"
"description": "Изменить метку",
"consumes": [
"application/json"
],
"summary": "Обновить информацию про мерч",
"tags": [
"Merch labels"
],
"summary": "Изменить метку",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.MerchDTO"
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить метку как удаленную",
"tags": [
"Merch labels"
],
"summary": "Пометить метку как удаленную",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/zeroprices": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить нулевые цены",
"produces": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Получить нулевые цены",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/merch.ZeroPrice"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные",
"consumes": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.DeleteZeroPrices"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/zeroprices/period": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные за указанный период",
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные за указанный период",
"parameters": [
{
"type": "string",
"description": "start",
"name": "start",
"in": "query",
"required": true
},
{
"type": "string",
"description": "end",
"name": "end",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Получить всю информацию про мерч",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
@ -232,6 +817,9 @@ const docTemplate = `{
}
],
"description": "Получить цены мерча за период",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -277,6 +865,9 @@ const docTemplate = `{
}
],
"description": "Получить перепады цен мерча за период по его merch_uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -620,9 +1211,90 @@ const docTemplate = `{
}
},
"definitions": {
"imageStorage.UploadMerchImageResponse": {
"type": "object",
"properties": {
"fullImage": {
"type": "string"
},
"thumbnail": {
"type": "string"
}
}
},
"merch.DeleteZeroPrices": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.ImageLink": {
"type": "object",
"properties": {
"etag": {
"type": "string"
},
"link": {
"type": "string"
}
}
},
"merch.LabelDTO": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.LabelLink": {
"type": "object",
"properties": {
"label_uuid": {
"type": "string"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.LabelsList": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"label_uuid": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.ListResponse": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -642,6 +1314,12 @@ const docTemplate = `{
"merch.MerchDTO": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -706,6 +1384,43 @@ const docTemplate = `{
}
}
},
"merch.UpdateMerchDTO": {
"type": "object",
"properties": {
"link": {
"type": "string"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"merch.ZeroPrice": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"responses.ErrorResponse400": {
"type": "object",
"properties": {

View file

@ -60,6 +60,9 @@
}
],
"description": "Получить все записи мерча",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -87,20 +90,168 @@
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"consumes": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Обновить информацию про мерч",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.UpdateMerchDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/{uuid}": {
"/merch/images/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"tags": [
"Merch"
"description": "Получить картинки по merch_uuid и query параметрам",
"produces": [
"application/json"
],
"summary": "Получить всю информацию про мерч",
"tags": [
"Merch images"
],
"summary": "Получить картинки по merch_uuid и query параметрам",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "string",
"description": "image type",
"name": "type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/merch.ImageLink"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"Merch images"
],
"summary": "Загрузить картинку по merch_uuid",
"parameters": [
{
"type": "string",
"description": "Merch UUID",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "file",
"description": "Image file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/imageStorage.UploadMerchImageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить (безвозвратно) картинки по merch_uuid",
"tags": [
"Merch images"
],
"summary": "Удалить (безвозвратно) картинки по merch_uuid",
"parameters": [
{
"type": "string",
@ -110,11 +261,48 @@
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить все метки товаров",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить все метки товаров",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/merch.MerchDTO"
"type": "array",
"items": {
"$ref": "#/definitions/merch.LabelsList"
}
}
},
"400": {
@ -131,28 +319,425 @@
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Создать новую метку для товара",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Создать новую метку для товара",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/attach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Прикрепить метку к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Прикрепить метку к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/detach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить привязку метки к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Удалить привязку метки к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить метки товара по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить метки товара по его uuid",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"tags": [
"Merch"
"description": "Изменить метку",
"consumes": [
"application/json"
],
"summary": "Обновить информацию про мерч",
"tags": [
"Merch labels"
],
"summary": "Изменить метку",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.MerchDTO"
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить метку как удаленную",
"tags": [
"Merch labels"
],
"summary": "Пометить метку как удаленную",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/zeroprices": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить нулевые цены",
"produces": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Получить нулевые цены",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/merch.ZeroPrice"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные",
"consumes": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.DeleteZeroPrices"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/zeroprices/period": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные за указанный период",
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные за указанный период",
"parameters": [
{
"type": "string",
"description": "start",
"name": "start",
"in": "query",
"required": true
},
{
"type": "string",
"description": "end",
"name": "end",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Получить всю информацию про мерч",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
@ -224,6 +809,9 @@
}
],
"description": "Получить цены мерча за период",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -269,6 +857,9 @@
}
],
"description": "Получить перепады цен мерча за период по его merch_uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -612,9 +1203,90 @@
}
},
"definitions": {
"imageStorage.UploadMerchImageResponse": {
"type": "object",
"properties": {
"fullImage": {
"type": "string"
},
"thumbnail": {
"type": "string"
}
}
},
"merch.DeleteZeroPrices": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.ImageLink": {
"type": "object",
"properties": {
"etag": {
"type": "string"
},
"link": {
"type": "string"
}
}
},
"merch.LabelDTO": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.LabelLink": {
"type": "object",
"properties": {
"label_uuid": {
"type": "string"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.LabelsList": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"label_uuid": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.ListResponse": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -634,6 +1306,12 @@
"merch.MerchDTO": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -698,6 +1376,43 @@
}
}
},
"merch.UpdateMerchDTO": {
"type": "object",
"properties": {
"link": {
"type": "string"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"merch.ZeroPrice": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"responses.ErrorResponse400": {
"type": "object",
"properties": {

View file

@ -1,7 +1,59 @@
basePath: /api/v2
definitions:
imageStorage.UploadMerchImageResponse:
properties:
fullImage:
type: string
thumbnail:
type: string
type: object
merch.DeleteZeroPrices:
properties:
id:
type: integer
merch_uuid:
type: string
type: object
merch.ImageLink:
properties:
etag:
type: string
link:
type: string
type: object
merch.LabelDTO:
properties:
bg_color:
type: string
color:
type: string
name:
type: string
type: object
merch.LabelLink:
properties:
label_uuid:
type: string
merch_uuid:
type: string
type: object
merch.LabelsList:
properties:
bg_color:
type: string
color:
type: string
label_uuid:
type: string
name:
type: string
type: object
merch.ListResponse:
properties:
labels:
items:
type: string
type: array
merch_uuid:
type: string
name:
@ -14,6 +66,10 @@ definitions:
type: object
merch.MerchDTO:
properties:
labels:
items:
type: string
type: array
merch_uuid:
type: string
name:
@ -55,6 +111,30 @@ definitions:
link:
type: string
type: object
merch.UpdateMerchDTO:
properties:
link:
type: string
merch_uuid:
type: string
name:
type: string
origin:
type: string
type: object
merch.ZeroPrice:
properties:
created_at:
type: string
id:
type: integer
merch_uuid:
type: string
name:
type: string
origin:
type: string
type: object
responses.ErrorResponse400:
properties:
error:
@ -153,6 +233,8 @@ paths:
/merch/:
get:
description: Получить все записи мерча
produces:
- application/json
responses:
"200":
description: OK
@ -173,6 +255,33 @@ paths:
summary: Получить все записи мерча
tags:
- Merch
put:
consumes:
- application/json
description: Обновить информацию про мерч по его uuid в json-е
parameters:
- description: merch_uuid
in: body
name: body
required: true
schema:
$ref: '#/definitions/merch.UpdateMerchDTO'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Обновить информацию про мерч
tags:
- Merch
/merch/{uuid}:
delete:
description: Пометить мерч как удаленный по его uuid
@ -208,6 +317,8 @@ paths:
name: uuid
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -226,20 +337,18 @@ paths:
summary: Получить всю информацию про мерч
tags:
- Merch
put:
description: Обновить информацию про мерч по его uuid в json-е
/merch/images/{uuid}:
delete:
description: Удалить (безвозвратно) картинки по merch_uuid
parameters:
- description: merch_uuid
in: body
name: body
in: path
name: uuid
required: true
schema:
$ref: '#/definitions/merch.MerchDTO'
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/merch.MerchDTO'
"400":
description: Bad Request
schema:
@ -250,9 +359,351 @@ paths:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Обновить информацию про мерч
summary: Удалить (безвозвратно) картинки по merch_uuid
tags:
- Merch
- Merch images
get:
description: Получить картинки по merch_uuid и query параметрам
parameters:
- description: merch_uuid
in: path
name: uuid
required: true
type: string
- description: image type
in: query
name: type
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/merch.ImageLink'
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить картинки по merch_uuid и query параметрам
tags:
- Merch images
post:
consumes:
- multipart/form-data
description: Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на
созданные картинки.
parameters:
- description: Merch UUID
in: path
name: uuid
required: true
type: string
- description: Image file
in: formData
name: file
required: true
type: file
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/imageStorage.UploadMerchImageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Загрузить картинку по merch_uuid
tags:
- Merch images
/merch/labels:
get:
description: Получить все метки товаров
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/merch.LabelsList'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить все метки товаров
tags:
- Merch labels
post:
consumes:
- application/json
description: Создать новую метку для товара
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelDTO'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Создать новую метку для товара
tags:
- Merch labels
/merch/labels/{uuid}:
delete:
description: Пометить метку как удаленную
parameters:
- description: label uuid
in: path
name: uuid
required: true
type: string
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Пометить метку как удаленную
tags:
- Merch labels
get:
description: Получить метки товара по его uuid
parameters:
- description: label uuid
in: path
name: uuid
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить метки товара по его uuid
tags:
- Merch labels
put:
consumes:
- application/json
description: Изменить метку
parameters:
- description: label uuid
in: path
name: uuid
required: true
type: string
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelDTO'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Изменить метку
tags:
- Merch labels
/merch/labels/attach:
post:
consumes:
- application/json
description: Прикрепить метку к товару
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelLink'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Прикрепить метку к товару
tags:
- Merch labels
/merch/labels/detach:
post:
consumes:
- application/json
description: Удалить привязку метки к товару
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelLink'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Удалить привязку метки к товару
tags:
- Merch labels
/merch/zeroprices:
delete:
consumes:
- application/json
description: Пометить нулевые цены как удаленные
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.DeleteZeroPrices'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Пометить нулевые цены как удаленные
tags:
- Merch zero prices
get:
description: Получить нулевые цены
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/merch.ZeroPrice'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить нулевые цены
tags:
- Merch zero prices
/merch/zeroprices/period:
delete:
description: Пометить нулевые цены как удаленные за указанный период
parameters:
- description: start
in: query
name: start
required: true
type: string
- description: end
in: query
name: end
required: true
type: string
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Пометить нулевые цены как удаленные за указанный период
tags:
- Merch zero prices
/prices:
get:
description: Получить цены мерча за период
@ -261,6 +712,8 @@ paths:
in: query
name: days
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -294,6 +747,8 @@ paths:
in: query
name: days
type: string
produces:
- application/json
responses:
"200":
description: OK

33
go.mod
View file

@ -3,15 +3,19 @@ module merch-parser-api
go 1.25.1
require (
github.com/disintegration/imaging v1.6.2
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/minio/minio-go/v7 v7.0.95
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.42.0
golang.org/x/crypto v0.43.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0
)
@ -22,8 +26,10 @@ require (
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
@ -36,7 +42,7 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -46,25 +52,32 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/image v0.32.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
)

88
go.sum
View file

@ -11,6 +11,10 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
@ -21,6 +25,12 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
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/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
@ -50,14 +60,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -77,6 +89,9 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -87,6 +102,12 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -94,14 +115,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
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/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -120,30 +145,47 @@ github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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/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/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
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=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
@ -156,8 +198,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -165,16 +207,22 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=

View file

@ -1,29 +1,32 @@
package merch
import (
"context"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"merch-parser-api/internal/interfaces"
"merch-parser-api/pkg/responses"
"net/http"
"strings"
"time"
)
type controller struct {
service *service
utils interfaces.Utils
expires time.Duration
}
func newController(service *service, utils interfaces.Utils) *controller {
func newController(service *service, utils interfaces.Utils, expires time.Duration) *controller {
return &controller{
service: service,
utils: utils,
expires: expires,
}
}
func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, refreshMW gin.HandlerFunc) {
merchGroup := r.Group("/merch", authMW)
merchGroup.POST("/", h.controller.addMerch)
merchGroup.GET("/:uuid", h.controller.getSingleMerch)
merchGroup.GET("/", h.controller.getAllMerch)
@ -34,6 +37,26 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, ref
chartsGroup.GET("", h.controller.getChartsPrices)
chartsGroup.GET("/:uuid", h.controller.getDistinctPrices)
imagesGroup := merchGroup.Group("/images")
imagesGroup.POST("/:uuid", h.controller.uploadMerchImage)
imagesGroup.GET("/:uuid", h.controller.getMerchImage)
imagesGroup.DELETE("/:uuid", h.controller.deleteMerchImage)
labelsGroup := merchGroup.Group("/labels", authMW)
labelsGroup.POST("", h.controller.createLabel)
labelsGroup.GET("", h.controller.getLabels)
labelsGroup.PUT("/:uuid", h.controller.updateLabel)
labelsGroup.DELETE("/:uuid", h.controller.deleteLabel)
labelsGroup.POST("/attach", h.controller.attachLabel)
labelsGroup.POST("/detach", h.controller.detachLabel)
labelsGroup.GET("/:uuid", h.controller.getMerchLabels)
zeroPricesGroup := merchGroup.Group("/zeroprices", authMW)
zeroPricesGroup.GET("", h.controller.getZeroPrices)
zeroPricesGroup.DELETE("", h.controller.deleteZeroPrices)
zeroPricesGroup.DELETE("/period", h.controller.deleteZeroPricesPeriod)
}
// @Summary Добавить новый мерч
@ -75,6 +98,7 @@ func (co *controller) addMerch(c *gin.Context) {
// @Description Получить всю информацию про мерч по его uuid
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "merch_uuid"
// @Success 200 {object} MerchDTO
// @Failure 400 {object} responses.ErrorResponse400
@ -108,6 +132,7 @@ func (co *controller) getSingleMerch(c *gin.Context) {
// @Description Получить все записи мерча
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Success 200 {array} ListResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
@ -134,16 +159,17 @@ func (co *controller) getAllMerch(c *gin.Context) {
// @Description Обновить информацию про мерч по его uuid в json-е
// @Tags Merch
// @Security BearerAuth
// @Param body body MerchDTO true "merch_uuid"
// @Success 200 {object} MerchDTO
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/{uuid} [put]
// @Accept json
// @Param body body UpdateMerchDTO true "merch_uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/ [put]
func (co *controller) updateMerch(c *gin.Context) {
var payload MerchDTO
var payload UpdateMerchDTO
if err := c.ShouldBind(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to bind JSON on add merch")
log.WithError(err).Error("Merch | Failed to bind JSON on update merch")
return
}
@ -159,6 +185,7 @@ func (co *controller) updateMerch(c *gin.Context) {
log.WithError(err).Error("Merch | Failed to get single merch")
return
}
c.Status(http.StatusOK)
}
// @Summary Пометить мерч как удаленный
@ -169,6 +196,7 @@ func (co *controller) updateMerch(c *gin.Context) {
// @Success 200 {object} MerchDTO
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
//
// @Router /merch/{uuid} [delete]
func (co *controller) deleteMerch(c *gin.Context) {
merchUuid := c.Param("uuid")
@ -197,11 +225,16 @@ func (co *controller) deleteMerch(c *gin.Context) {
// @Description Получить цены мерча за период
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Param days query string false "period in days"
// @Success 200 {array} PricesResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /prices [get]
//
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /prices [get]
func (co *controller) getChartsPrices(c *gin.Context) {
daysQuery := strings.ToLower(c.DefaultQuery("days", ""))
@ -226,11 +259,12 @@ func (co *controller) getChartsPrices(c *gin.Context) {
// @Description Получить перепады цен мерча за период по его merch_uuid
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "merch_uuid"
// @Param days query string false "period in days"
// @Success 200 {object} PricesResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Param days query string false "period in days"
// @Success 200 {object} PricesResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /prices/{uuid} [get]
func (co *controller) getDistinctPrices(c *gin.Context) {
daysQuery := strings.ToLower(c.DefaultQuery("days", ""))
@ -257,3 +291,506 @@ func (co *controller) getDistinctPrices(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// @Summary Загрузить картинку по merch_uuid
// @Description Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.
// @Tags Merch images
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param uuid path string true "Merch UUID"
// @Param file formData file true "Image file"
// @Success 200 {object} imageStorage.UploadMerchImageResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [post]
func (co *controller) uploadMerchImage(c *gin.Context) {
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to get user uuid from context")
return
}
merchUuid := c.Param("uuid")
if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
log.Error("Merch | Failed to get single merch")
return
}
//Uncomment for MinIO use
//imageType := c.PostForm("imageType")
//types := map[string]struct{}{"thumbnail": {}, "full": {}, "all": {}}
//if _, allowed := types[imageType]; !allowed {
// c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "imageType must be one of: thumbnail, full, all"})
// log.WithError(err).Error("Merch | imageType must be one of: thumbnail, full, all")
// return
//}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "file is required"})
log.WithError(err).Error("Merch | File is required")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
//Uncomment for MinIO use
//err = co.service.uploadMerchImage(ctx, userUuid, merchUuid, imageType, file)
response, err := co.service.mtUploadMerchImage(ctx, userUuid, merchUuid, file)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to upload merch image")
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Получить картинки по merch_uuid и query параметрам
// @Description Получить картинки по merch_uuid и query параметрам
// @Tags Merch images
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "merch_uuid"
// @Param type query string true "image type"
// @Success 200 {object} ImageLink
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [get]
func (co *controller) getMerchImage(c *gin.Context) {
//Uncomment for MinIO use
//typeQuery := strings.ToLower(c.Query("type"))
//if typeQuery == "" {
// c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "Image type query param is empty"})
// return
//}
//
//userUuid, err := co.utils.GetUserUuidFromContext(c)
//if err != nil {
// c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
// log.WithError(err).Error("Merch | Failed to get user uuid from context")
// return
//}
//
//merchUuid := c.Param("uuid")
//if merchUuid == "" {
// c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
// log.WithError(err).Error("Merch | Failed to get single merch")
// return
//}
//
//ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
//defer cancel()
//
//link, err := co.service.getPublicImageLink(ctx, userUuid, merchUuid, typeQuery)
//if err != nil {
// c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
// log.WithError(err).Error("Merch | Failed to get merch image")
// return
//}
//
//if link.Link == "" {
// log.Debug("Merch | No image")
// c.Status(http.StatusNoContent)
// return
//}
//
//c.JSON(http.StatusOK, link)
}
// @Summary Удалить (безвозвратно) картинки по merch_uuid
// @Description Удалить (безвозвратно) картинки по merch_uuid
// @Tags Merch images
// @Security BearerAuth
// @Param uuid path string true "merch_uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [delete]
func (co *controller) deleteMerchImage(c *gin.Context) {
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to get user uuid from context")
return
}
merchUuid := c.Param("uuid")
if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
log.WithError(err).Error("Merch | Failed to get merch uuid")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
//Uncomment for MinIO use
//if err := co.service.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
if err := co.service.mtDeleteMerchImage(ctx, userUuid, merchUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to delete merch image")
return
}
c.Status(http.StatusOK)
}
// @Summary Создать новую метку для товара
// @Description Создать новую метку для товара
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param payload body LabelDTO true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels [post]
func (co *controller) createLabel(c *gin.Context) {
const logMsg = "Merch | Create label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload LabelDTO
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.createLabel(payload, userUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Получить все метки товаров
// @Description Получить все метки товаров
// @Tags Merch labels
// @Security BearerAuth
// @Produce json
// @Success 200 {array} LabelsList
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels [get]
func (co *controller) getLabels(c *gin.Context) {
const logMsg = "Merch | Get labels"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
response, err := co.service.getLabels(userUuid)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Изменить метку
// @Description Изменить метку
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param uuid path string true "label uuid"
// @Param payload body LabelDTO true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/{uuid} [put]
func (co *controller) updateLabel(c *gin.Context) {
const logMsg = "Merch | Update label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
labelUuid := c.Param("uuid")
if labelUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"})
log.WithError(err).Error(logMsg)
return
}
var payload LabelDTO
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.updateLabel(userUuid, labelUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Пометить метку как удаленную
// @Description Пометить метку как удаленную
// @Tags Merch labels
// @Security BearerAuth
// @Param uuid path string true "label uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/{uuid} [delete]
func (co *controller) deleteLabel(c *gin.Context) {
const logMsg = "Merch | Delete label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
labelUuid := c.Param("uuid")
if labelUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.deleteLabel(userUuid, labelUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Прикрепить метку к товару
// @Description Прикрепить метку к товару
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param payload body LabelLink true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/attach [post]
func (co *controller) attachLabel(c *gin.Context) {
const logMsg = "Merch | Attach label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload LabelLink
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.attachLabel(userUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Удалить привязку метки к товару
// @Description Удалить привязку метки к товару
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param payload body LabelLink true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/detach [post]
func (co *controller) detachLabel(c *gin.Context) {
const logMsg = "Merch | Detach label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload LabelLink
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.detachLabel(userUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Получить метки товара по его uuid
// @Description Получить метки товара по его uuid
// @Tags Merch labels
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "label uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/{uuid} [get]
func (co *controller) getMerchLabels(c *gin.Context) {
const logMsg = "Merch | Get merch labels"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
merchUuid := c.Param("uuid")
if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"})
log.WithError(err).Error(logMsg)
return
}
response, err := co.service.getMerchLabels(userUuid, merchUuid)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Получить нулевые цены
// @Description Получить нулевые цены
// @Tags Merch zero prices
// @Security BearerAuth
// @Produce json
// @Success 200 {array} ZeroPrice
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/zeroprices [get]
func (co *controller) getZeroPrices(c *gin.Context) {
const logMsg = "Merch | Get zero prices"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
response, err := co.service.getZeroPrices(userUuid)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Пометить нулевые цены как удаленные
// @Description Пометить нулевые цены как удаленные
// @Tags Merch zero prices
// @Security BearerAuth
// @Accept json
// @Param payload body DeleteZeroPrices true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/zeroprices [delete]
func (co *controller) deleteZeroPrices(c *gin.Context) {
const logMsg = "Merch | Delete zero prices"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload []DeleteZeroPrices
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.deleteZeroPrices(userUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Пометить нулевые цены как удаленные за указанный период
// @Description Пометить нулевые цены как удаленные за указанный период
// @Tags Merch zero prices
// @Security BearerAuth
// @Param start query string true "start"
// @Param end query string true "end"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/zeroprices/period [delete]
func (co *controller) deleteZeroPricesPeriod(c *gin.Context) {
const logMsg = "Merch | Delete zero prices period"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
start, err := co.utils.ParseTime(c.Query("start"))
if err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
end, err := co.utils.ParseTime(c.Query("end"))
if err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.deleteZeroPricesPeriod(userUuid, start, end); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}

View file

@ -1,5 +1,7 @@
package merch
import "time"
type merchBundle struct {
Merch *Merch
Surugaya *Surugaya
@ -10,6 +12,7 @@ type MerchDTO struct {
Name string `json:"name"`
OriginSurugaya SurugayaDTO `json:"origin_surugaya"`
OriginMandarake MandarakeDTO `json:"origin_mandarake"`
Labels []string `json:"labels,omitempty" gorm:"-"`
}
type SurugayaDTO struct {
@ -27,8 +30,9 @@ type SingleMerchResponse struct {
}
type ListResponse struct {
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
Labels []string `json:"labels,omitempty" gorm:"-"`
}
type PriceEntry struct {
@ -46,3 +50,46 @@ type PricesResponse struct {
MerchUuid string `json:"merch_uuid"`
Origins []OriginWithPrices `json:"origins"`
}
type UpdateMerchDTO struct {
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
Origin string `json:"origin"`
Link string `json:"link"`
}
type ImageLink struct {
Link string `json:"link"`
ETag string `json:"etag"`
}
type LabelDTO struct {
Name string `json:"name"`
Color string `json:"color,omitempty"`
BgColor string `json:"bg_color,omitempty"`
}
type LabelsList struct {
LabelUuid string `json:"label_uuid"`
Name string `json:"name"`
Color string `json:"color,omitempty"`
BgColor string `json:"bg_color,omitempty"`
}
type LabelLink struct {
MerchUuid string `json:"merch_uuid"`
LabelUuid string `json:"label_uuid"`
}
type ZeroPrice struct {
Id int `json:"id"`
CreatedAt time.Time `json:"created_at"`
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
Origin Origin `json:"origin"`
}
type DeleteZeroPrices struct {
Id uint `json:"id"`
MerchUuid string `json:"merch_uuid"`
}

View file

@ -3,6 +3,8 @@ package merch
import (
"gorm.io/gorm"
"merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"time"
)
type Handler struct {
@ -14,12 +16,33 @@ type Handler struct {
type Deps struct {
DB *gorm.DB
Utils interfaces.Utils
//Media interfaces.MediaStorage
ImageStorage is.ImageStorageClient
}
func NewHandler(deps Deps) *Handler {
packageBucketName := "user-merch-images"
expires := time.Minute * 5
r := NewRepo(deps.DB)
s := newService(r)
c := newController(s, deps.Utils)
s := newService(serviceDeps{
repo: r,
//media: deps.Media,
bucketName: packageBucketName,
expires: expires,
imageStorage: deps.ImageStorage,
})
c := newController(s, deps.Utils, expires)
//media := deps.Media
//log.WithFields(log.Fields{
// "addr": media,
//}).Debug("Merch handler constructor | Media provider")
//
//exists, err := media.CheckBucketExists(packageBucketName)
//if err != nil || !exists {
// log.WithError(err).Fatal("Merch handler constructor | Failed to ensure bucket exists")
//}
return &Handler{
repo: r,

View file

@ -1,6 +1,7 @@
package merch
import (
"fmt"
"strconv"
"time"
)
@ -17,3 +18,14 @@ func getPeriod(days string) time.Time {
return time.Now().UTC().Add(-(time.Duration(daysInt) * time.Hour * 24))
}
func (s *service) makeObject(userUuid, merchUuid, imageType string) (string, error) {
switch imageType {
case "thumbnail":
return fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid), nil
case "full":
return fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid), nil
default:
return "", fmt.Errorf("unknown image type %s", imageType)
}
}

View file

@ -20,11 +20,10 @@ func (Merch) TableName() string {
}
type Surugaya struct {
Id uint `gorm:"primary_key" json:"-"`
DeletedAt sql.NullTime `json:"-"`
MerchUuid string `json:"-"`
Link string `json:"link"`
CookieValues string `json:"cookie_values"`
Id uint `gorm:"primary_key" json:"-"`
DeletedAt sql.NullTime `json:"-"`
MerchUuid string `json:"-"`
Link string `json:"link"`
}
func (Surugaya) TableName() string {
@ -45,9 +44,35 @@ func (Mandarake) TableName() string {
type Price struct {
Id uint `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt sql.NullTime `json:"updated_at" gorm:"column:updated_at"`
DeletedAt sql.NullTime `json:"deleted_at" gorm:"column:deleted_at"`
UpdatedAt sql.NullTime `json:"updated_at,omitempty" gorm:"column:updated_at"`
DeletedAt sql.NullTime `json:"deleted_at,omitempty" gorm:"column:deleted_at"`
MerchUuid string `json:"merch_uuid" gorm:"column:merch_uuid"`
Price int `json:"price" gorm:"column:price"`
Origin Origin `json:"origin" gorm:"column:origin"`
Origin Origin `json:"origin" gorm:"column:origin;type:integer"`
}
type Label struct {
Id uint `json:"-" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt sql.NullTime `json:"deleted_at" gorm:"column:deleted_at"`
LabelUuid string `json:"label_uuid" gorm:"column:label_uuid"`
UserUuid string `json:"user_uuid" gorm:"column:user_uuid"`
Name string `json:"name" gorm:"column:name"`
Color string `json:"color" gorm:"column:color"`
BgColor string `json:"bg_color" gorm:"column:bg_color"`
}
func (Label) TableName() string {
return "labels"
}
type CardLabel struct {
LabelUuid string `json:"label_uuid"`
UserUuid string `json:"user_uuid"`
MerchUuid string `json:"merch_uuid"`
}
func (CardLabel) TableName() string {
return "card_labels"
}

View file

@ -1,6 +1,10 @@
package merch
import "encoding/json"
import (
"database/sql/driver"
"encoding/json"
"strings"
)
type Origin int
@ -25,3 +29,16 @@ func (o Origin) String() string {
func (o Origin) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
func parseOrigin(s string) (Origin, bool) {
for i, name := range Origins {
if name == strings.ToLower(s) {
return Origin((i + 1) * 1000), true
}
}
return 0, false
}
func (o Origin) Value() (driver.Value, error) {
return int(o), nil
}

View file

@ -0,0 +1,128 @@
package merch
import (
"database/sql"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"merch-parser-api/internal/shared"
"time"
)
type Link struct {
Surugaya []Surugaya
Mandarake []Mandarake
}
type TaskProvider struct {
repo TaskRepository
}
type TaskRepository interface {
getLinks() (*Link, error)
insertPrices(prices []Price) error
}
type TaskRepo struct {
db *gorm.DB
}
func NewTaskProvider(repo TaskRepository) *TaskProvider {
return &TaskProvider{
repo: repo,
}
}
func NewTaskRepository(db *gorm.DB) TaskRepository {
return &TaskRepo{db: db}
}
func (p *TaskProvider) PrepareTasks() (map[string]shared.Task, error) {
getLinks, err := p.repo.getLinks()
if err != nil {
return nil, err
}
log.WithFields(log.Fields{
"surugaya links": len(getLinks.Surugaya),
"mandarake links": len(getLinks.Mandarake),
}).Info("gRPC Server | Prepare tasks")
taskMap := make(map[string]shared.Task)
for _, item := range getLinks.Surugaya {
if task, exists := taskMap[item.MerchUuid]; exists {
task.OriginSurugayaLink = item.Link
taskMap[item.MerchUuid] = task
} else {
taskMap[item.MerchUuid] = shared.Task{
MerchUuid: item.MerchUuid,
OriginSurugayaLink: item.Link,
}
}
}
for _, item := range getLinks.Mandarake {
if task, exists := taskMap[item.MerchUuid]; exists {
task.OriginMandarakeLink = item.Link
taskMap[item.MerchUuid] = task
} else {
taskMap[item.MerchUuid] = shared.Task{
MerchUuid: item.MerchUuid,
OriginMandarakeLink: item.Link,
}
}
}
log.WithField("data", taskMap).Debug("Prepare tasks")
return taskMap, nil
}
func (p *TaskProvider) InsertPrices(prices []shared.TaskResult) error {
if len(prices) == 0 {
log.WithField("msg", "no prices received").Debug("Merch provider | Insert prices")
return nil
}
var insertPrices []Price
for _, item := range prices {
origin, ok := parseOrigin(item.Origin)
if !ok {
continue
}
insertPrices = append(insertPrices, Price{
CreatedAt: time.Now().UTC(),
UpdatedAt: sql.NullTime{Time: time.Time{}, Valid: false},
DeletedAt: sql.NullTime{Time: time.Time{}, Valid: false},
MerchUuid: item.MerchUuid,
Price: int(item.Price),
Origin: origin,
})
}
if err := p.repo.insertPrices(insertPrices); err != nil {
return err
}
return nil
}
func (r *TaskRepo) getLinks() (*Link, error) {
var surugayaList []Surugaya
if err := r.db.Model(&Surugaya{}).Where("deleted_at IS NULL").Find(&surugayaList).Error; err != nil {
return nil, err
}
var mandarakeList []Mandarake
if err := r.db.Model(&Mandarake{}).Where("deleted_at IS NULL").Find(&mandarakeList).Error; err != nil {
return nil, err
}
return &Link{
Surugaya: surugayaList,
Mandarake: mandarakeList,
}, nil
}
func (r *TaskRepo) insertPrices(prices []Price) error {
return r.db.Model(&Price{}).Create(&prices).Error
}

View file

@ -4,6 +4,7 @@ import (
"database/sql"
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"time"
)
@ -20,21 +21,39 @@ func NewRepo(db *gorm.DB) *Repo {
type repository interface {
addMerch(bundle merchBundle) error
merchRecordExists(userUuid, merchUuid string) (bool, error)
userOwnsMerchUuids(userUuid string, merchUuids []string) ([]Merch, error)
getSingleMerch(userUuid, merchUuid string) (merchBundle, error)
getAllMerch(userUuid string) ([]ListResponse, error)
updateMerch(payload MerchDTO, userUuid string) error
updateMerch(payload UpdateMerchDTO, userUuid string) error
deleteMerch(userUuid, merchUuid string) error
getAllUserMerch(userUuid string) ([]Merch, error)
prices
labels
}
type prices interface {
getPricesWithDays(userUuid string, period time.Time) ([]Price, error)
getDistinctPrices(userUuid, merchUuid string, period time.Time) (prices []Price, err error)
getZeroPrices(userUuid string) ([]ZeroPrice, error)
deleteZeroPrices(list []DeleteZeroPrices) error
deleteZeroPricesPeriod(userUuid string, start, end time.Time) error
}
type labels interface {
createLabel(label Label) error
getLabels(userUuid string) ([]Label, error)
updateLabel(userUuid, labelUuid string, label map[string]any) error
deleteLabel(userUuid, labelUuid string) error
attachLabel(label CardLabel) error
detachLabel(label CardLabel) error
getAttachedLabelsByList(list []string) ([]CardLabel, error)
getAttachedLabelsByUuid(userUuid, merchUuid string) ([]CardLabel, error)
}
func (r *Repo) addMerch(bundle merchBundle) error {
@ -53,6 +72,36 @@ func (r *Repo) addMerch(bundle merchBundle) error {
return nil
}
func (r *Repo) merchRecordExists(userUuid, merchUuid string) (bool, error) {
var exists bool
err := r.db.Raw(`
SELECT EXISTS (
SELECT 1
FROM merch
WHERE user_uuid = ?
AND merch_uuid = ?
AND deleted_at IS NULL
);`, userUuid, merchUuid).Scan(&exists).Error
return exists, err
}
func (r *Repo) userOwnsMerchUuids(userUuid string, merchUuids []string) ([]Merch, error) {
var ownsUuids []Merch
err := r.db.Model(&Merch{}).
Select("merch_uuid").
Where("user_uuid = ?", userUuid).
Where("merch_uuid IN (?)", merchUuids).
Where("deleted_at IS NULL").
Find(&ownsUuids).Error
if err != nil {
return nil, err
}
return ownsUuids, nil
}
func (r *Repo) getSingleMerch(userUuid, merchUuid string) (merchBundle, error) {
var merch Merch
if err := r.db.
@ -99,7 +148,7 @@ func (r *Repo) getAllMerch(userUuid string) ([]ListResponse, error) {
return list, nil
}
func (r *Repo) updateMerch(payload MerchDTO, userUuid string) error {
func (r *Repo) updateMerch(payload UpdateMerchDTO, userUuid string) error {
m := make(map[string]any)
m["name"] = payload.Name
m["updated_at"] = sql.NullTime{
@ -115,27 +164,20 @@ func (r *Repo) updateMerch(payload MerchDTO, userUuid string) error {
return err
}
// surugaya
fields := make(map[string]any, 2)
if payload.OriginSurugaya.Link != "" {
fields["link"] = payload.OriginSurugaya.Link
}
if len(fields) > 0 {
if err := r.db.
Model(&Surugaya{}).
Where("merch_uuid = ?", payload.MerchUuid).
Updates(fields).Error; err != nil {
switch payload.Origin {
case "surugaya":
if err := r.upsertOrigin(&Surugaya{
MerchUuid: payload.MerchUuid,
Link: payload.Link,
}); err != nil {
return err
}
}
// mandarake
if payload.OriginMandarake.Link != "" {
if err := r.db.
Model(&Mandarake{}).
Where("merch_uuid = ?", payload.MerchUuid).
Update("link", payload.OriginMandarake.Link).Error; err != nil {
case "mandarake":
if err := r.upsertOrigin(&Mandarake{
MerchUuid: payload.MerchUuid,
Link: payload.Link,
}); err != nil {
return err
}
}
@ -223,3 +265,126 @@ func (r *Repo) getDistinctPrices(userUuid, merchUuid string, period time.Time) (
}
return prices, nil
}
func (r *Repo) upsertOrigin(model any) error {
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "merch_uuid"}},
DoUpdates: clause.AssignmentColumns([]string{"link"}),
}).Create(model).Error
}
func (r *Repo) createLabel(label Label) error {
return r.db.Model(&Label{}).Create(&label).Error
}
func (r *Repo) getLabels(userUuid string) ([]Label, error) {
var labelsList []Label
if err := r.db.
Model(&Label{}).
Where("user_uuid = ?", userUuid).
Where("deleted_at IS NULL").
Find(&labelsList).Error; err != nil {
return nil, err
}
return labelsList, nil
}
func (r *Repo) updateLabel(userUuid, labelUuid string, label map[string]any) error {
return r.db.Model(&Label{}).
Where("user_uuid =? AND label_uuid = ?", userUuid, labelUuid).
Updates(label).Error
}
func (r *Repo) deleteLabel(userUuid, labelUuid string) error {
return r.db.Model(&Label{}).
Where("user_uuid =? AND label_uuid = ?", userUuid, labelUuid).
Update("deleted_at", time.Now().UTC()).Error
}
func (r *Repo) attachLabel(label CardLabel) error {
return r.db.Model(&CardLabel{}).Create(&label).Error
}
func (r *Repo) detachLabel(label CardLabel) error {
return r.db.
Where("user_uuid = ? AND label_uuid = ? AND merch_uuid = ?", label.UserUuid, label.LabelUuid, label.MerchUuid).
Delete(&CardLabel{}).Error
}
func (r *Repo) getAttachedLabelsByList(list []string) ([]CardLabel, error) {
var labelsList []CardLabel
if err := r.db.Model(&CardLabel{}).Where("merch_uuid IN ?", list).Find(&labelsList).Error; err != nil {
return nil, err
}
return labelsList, nil
}
func (r *Repo) getAttachedLabelsByUuid(userUuid, merchUuid string) ([]CardLabel, error) {
var labelsList []CardLabel
if err := r.db.Model(&CardLabel{}).Where("user_uuid = ? AND merch_uuid = ?", userUuid, merchUuid).Find(&labelsList).Error; err != nil {
return nil, err
}
return labelsList, nil
}
func (r *Repo) getZeroPrices(userUuid string) ([]ZeroPrice, error) {
var priceList []ZeroPrice
if err := r.db.Raw(`
WITH price_with_neighbors AS (
SELECT
p.id, p.created_at, p.merch_uuid, p.price, p.origin, m.name,
LAG(price) OVER (PARTITION BY p.merch_uuid, p.origin ORDER BY p.created_at, p.id) AS prev_price,
LEAD(price) OVER (PARTITION BY p.merch_uuid, p.origin ORDER BY p.created_at, p.id) AS next_price
FROM prices AS p
JOIN merch as m ON m.merch_uuid = p.merch_uuid
WHERE p.deleted_at IS NULL
AND m.deleted_at IS NULL
AND m.user_uuid = ?)
SELECT
id, created_at, merch_uuid, origin, name
FROM price_with_neighbors
WHERE
price = 0
AND prev_price IS NOT NULL
AND prev_price > 0
AND next_price IS NOT NULL
AND next_price > 0;
`, userUuid).Scan(&priceList).Error; err != nil {
return nil, err
}
return priceList, nil
}
func (r *Repo) deleteZeroPrices(list []DeleteZeroPrices) error {
for _, item := range list {
if err := r.db.Model(&Price{}).
Where("id = ? AND merch_uuid = ?", item.Id, item.MerchUuid).
Update("deleted_at", time.Now().UTC()).Error; err != nil {
return err
}
}
return nil
}
func (r *Repo) deleteZeroPricesPeriod(userUuid string, start, end time.Time) error {
if err := r.db.Exec(`
UPDATE prices
SET deleted_at = ?
FROM merch
WHERE prices.merch_uuid = merch.merch_uuid
AND merch.user_uuid = ?
AND prices.price = 0
AND prices.deleted_at IS NULL
AND prices.created_at BETWEEN ? AND ?;
`, time.Now().UTC(), userUuid, start, end).Error; err != nil {
return err
}
return nil
}

View file

@ -1,22 +1,59 @@
package merch
import (
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"github.com/disintegration/imaging"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"image"
"image/jpeg"
"io"
"merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"mime/multipart"
"path/filepath"
"strings"
"time"
)
type service struct {
repo repository
repo repository
media interfaces.MediaStorage
bucketName string
expires time.Duration
imageStorage is.ImageStorageClient
}
func newService(repo repository) *service {
type serviceDeps struct {
repo repository
media interfaces.MediaStorage
bucketName string
expires time.Duration
imageStorage is.ImageStorageClient
}
func newService(deps serviceDeps) *service {
return &service{
repo: repo,
repo: deps.repo,
media: deps.media,
bucketName: deps.bucketName,
expires: deps.expires,
imageStorage: deps.imageStorage,
}
}
type uploadImageParams struct {
ctx context.Context
src io.Reader
imageType string
object string
quality int
}
func (s *service) addMerch(payload MerchDTO, userUuid string) error {
merchUuid := uuid.NewString()
@ -64,17 +101,56 @@ func (s *service) getSingleMerch(userUuid, merchUuid string) (MerchDTO, error) {
}
func (s *service) getAllMerch(userUuid string) ([]ListResponse, error) {
return s.repo.getAllMerch(userUuid)
const logMsg = "Merch service | Get all merch"
allMerch, err := s.repo.getAllMerch(userUuid)
if err != nil {
return nil, err
}
ids := make([]string, 0, len(allMerch))
for _, m := range allMerch {
ids = append(ids, m.MerchUuid)
}
cardLabels, err := s.repo.getAttachedLabelsByList(ids)
if err != nil {
return nil, err
}
log.WithField("content", cardLabels).Debug(logMsg)
clMap := make(map[string][]string)
for _, cl := range cardLabels {
clMap[cl.MerchUuid] = append(clMap[cl.MerchUuid], cl.LabelUuid)
}
for item := range allMerch {
allMerch[item].Labels = clMap[allMerch[item].MerchUuid]
}
return allMerch, nil
}
func (s *service) updateMerch(payload MerchDTO, userUuid string) error {
func (s *service) updateMerch(payload UpdateMerchDTO, userUuid string) error {
if payload.MerchUuid == "" {
return errors.New("no MerchUuid or empty payload")
return errors.New("no merch uuid provided")
}
if payload.Origin == "" {
return errors.New("no origin provided")
}
return s.repo.updateMerch(payload, userUuid)
}
func (s *service) deleteMerch(userUuid, merchUuid string) error {
ctx, cancel := context.WithTimeout(context.Background(), s.expires)
defer cancel()
if err := s.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
return err
}
return s.repo.deleteMerch(userUuid, merchUuid)
}
@ -171,3 +247,422 @@ func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesRes
Origins: []OriginWithPrices{originSurugaya, originMandarake},
}, nil
}
// uploadMerchImage
// Deprecated.
// Use only with MinIO storage. Use mtUploadMerchImage for merch-tracker images storage.
func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, imageType string, file *multipart.FileHeader) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
rawExt := filepath.Ext(file.Filename)
if rawExt == "" {
return errors.New("no file extension")
}
ext := strings.ToLower(rawExt[1:])
allowedTypes := map[string]struct{}{"jpeg": {}, "jpg": {}, "png": {}, "gif": {}}
if _, ok := allowedTypes[ext]; !ok {
return errors.New("invalid file type")
}
getSrc := func() (io.ReadCloser, error) {
f, err := file.Open()
if err != nil {
log.WithError(err).Error("Merch | Failed to open file")
return nil, err
}
return f, nil
}
switch imageType {
case "thumbnail":
src, err := getSrc()
if err != nil {
return err
}
return s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src,
imageType: "thumbnail",
object: fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid),
quality: 80,
})
case "full":
src, err := getSrc()
if err != nil {
return err
}
return s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src,
imageType: "full",
object: fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid),
quality: 90,
})
case "all":
src, err := getSrc()
if err != nil {
return err
}
if err = s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src,
imageType: "thumbnail",
object: fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid),
quality: 80,
}); err != nil {
log.WithError(err).Error("Merch | Upload thumbnail and full image")
return err
}
src2, err := getSrc()
if err != nil {
return err
}
if err = s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src2,
imageType: "full",
object: fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid),
quality: 90,
}); err != nil {
log.WithError(err).Error("Merch | Upload thumbnail and full image")
return err
}
default:
return errors.New("invalid file type")
}
return nil
}
// getPublicImageLink
// Deprecated.
// Use only with MinIO storage.
func (s *service) getPublicImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
object, err := s.makeObject(userUuid, merchUuid, imageType)
if err != nil {
return ImageLink{}, err
}
link, etag, err := s.media.GetPublicLink(ctx, s.bucketName, object)
if err != nil {
return ImageLink{}, err
}
return ImageLink{
Link: link,
ETag: etag,
}, nil
}
// getPresignedImageLink
// Deprecated.
// Use only with MinIO storage.
func (s *service) getPresignedImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
return ImageLink{}, err
}
if !exists {
return ImageLink{}, fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
object, err := s.makeObject(userUuid, merchUuid, imageType)
if err != nil {
return ImageLink{}, err
}
link, err := s.media.GetPresignedLink(ctx, s.bucketName, object, s.expires, nil)
if err != nil {
return ImageLink{}, err
}
etag, err := s.media.GetObjectEtag(ctx, s.bucketName, object)
if err != nil {
return ImageLink{}, err
}
return ImageLink{
Link: link,
ETag: etag,
}, nil
}
// deleteMerchImage
// Deprecated.
// Use only with MinIO storage.
func (s *service) deleteMerchImage(ctx context.Context, userUuid, merchUuid string) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
//uncomment for MinIO
//if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid)); err != nil {
// return err
//}
//
//if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid)); err != nil {
// return err
//}
return nil
}
func (s *service) _uploadToStorage(params uploadImageParams) error {
img, _, err := image.Decode(params.src)
if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}
if params.imageType == "thumbnail" {
img = imaging.Resize(img, 300, 300, imaging.Lanczos)
}
var buf bytes.Buffer
if err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: params.quality}); err != nil {
return fmt.Errorf("failed to encode full image: %w", err)
}
err = s.media.Upload(params.ctx, s.bucketName, params.object, &buf, -1)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"img type": params.imageType,
}).Error("Merch | Failed to upload file to media storage")
return err
}
return nil
}
// mtUploadMerchImage
// Upload new/rewrite existing image to merch-tracker images storage
func (s *service) mtUploadMerchImage(ctx context.Context, userUuid, merchUuid string, file *multipart.FileHeader) (*is.UploadMerchImageResponse, error) {
const uploadMerchImage = "Merch service | Upload merch image"
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
if !exists {
err = fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
f, err := file.Open()
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
response, err := s.imageStorage.UploadImage(ctx, &is.UploadMerchImageRequest{
ImageData: data,
UserUuid: userUuid,
MerchUuid: merchUuid,
})
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
return response, nil
}
// mtDeleteMerchImage
// Delete all merch images for given user and merch uuid-s from merch-tracker images storage
func (s *service) mtDeleteMerchImage(ctx context.Context, userUuid, merchUuid string) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
s.imageStorage.DeleteImage(ctx, &is.DeleteImageRequest{
UserUuid: userUuid,
MerchUuid: merchUuid,
})
return nil
}
func (s *service) createLabel(label LabelDTO, userUuid string) error {
now := time.Now().UTC()
if label.Name == "" {
return fmt.Errorf("label name is required")
}
newLabel := Label{
CreatedAt: now,
UpdatedAt: now,
DeletedAt: sql.NullTime{Time: time.Time{}, Valid: false},
LabelUuid: uuid.NewString(),
UserUuid: userUuid,
Name: label.Name,
Color: label.Color,
BgColor: label.BgColor,
}
return s.repo.createLabel(newLabel)
}
func (s *service) getLabels(userUuid string) ([]LabelsList, error) {
stored, err := s.repo.getLabels(userUuid)
if err != nil {
return nil, err
}
response := make([]LabelsList, 0, len(stored))
for _, label := range stored {
response = append(response, LabelsList{
LabelUuid: label.LabelUuid,
Name: label.Name,
Color: label.Color,
BgColor: label.BgColor,
})
}
return response, nil
}
func (s *service) updateLabel(userUuid, labelUuid string, label LabelDTO) error {
updateMap := make(map[string]any, 3)
if label.Name != "" {
updateMap["name"] = label.Name
}
if label.Color != "" {
updateMap["color"] = label.Color
}
if label.BgColor != "" {
updateMap["bg_color"] = label.BgColor
}
return s.repo.updateLabel(userUuid, labelUuid, updateMap)
}
func (s *service) deleteLabel(userUuid, labelUuid string) error {
return s.repo.deleteLabel(userUuid, labelUuid)
}
func (s *service) attachLabel(userUuid string, label LabelLink) error {
if label.LabelUuid == "" || label.MerchUuid == "" {
return fmt.Errorf("both label and merch uuid-s are required")
}
attach := CardLabel{
LabelUuid: label.LabelUuid,
UserUuid: userUuid,
MerchUuid: label.MerchUuid,
}
return s.repo.attachLabel(attach)
}
func (s *service) detachLabel(userUuid string, label LabelLink) error {
if label.LabelUuid == "" || label.MerchUuid == "" {
return fmt.Errorf("both label and merch uuid-s are required")
}
detach := CardLabel{
LabelUuid: label.LabelUuid,
UserUuid: userUuid,
MerchUuid: label.MerchUuid,
}
return s.repo.detachLabel(detach)
}
func (s *service) getMerchLabels(userUuid, merchUuid string) ([]string, error) {
getLabels, err := s.repo.getAttachedLabelsByUuid(userUuid, merchUuid)
if err != nil {
return nil, err
}
response := make([]string, 0, len(getLabels))
for _, label := range getLabels {
response = append(response, label.LabelUuid)
}
return response, nil
}
func (s *service) getZeroPrices(userUuid string) ([]ZeroPrice, error) {
return s.repo.getZeroPrices(userUuid)
}
func (s *service) deleteZeroPrices(userUuid string, list []DeleteZeroPrices) error {
const delMsg = "Merch - service | Delete zero prices"
if len(list) == 0 {
return nil
}
ids := make([]string, 0, len(list))
for _, item := range list {
ids = append(ids, item.MerchUuid)
}
uniqueMap := make(map[string]struct{}, len(list))
uniqueIds := make([]string, 0, len(list))
for _, id := range ids {
if _, ok := uniqueMap[id]; !ok {
uniqueMap[id] = struct{}{}
uniqueIds = append(uniqueIds, id)
}
}
log.WithField("uuid count", len(uniqueIds)).Debug(delMsg)
owns, err := s.repo.userOwnsMerchUuids(userUuid, uniqueIds)
if err != nil {
return err
}
if len(owns) < 1 {
return errors.New("wrong ids")
}
ownsMap := make(map[string]struct{}, len(owns))
for _, own := range owns {
ownsMap[own.MerchUuid] = struct{}{}
}
toDelete := make([]DeleteZeroPrices, 0, len(owns))
for _, item := range list {
if _, ok := ownsMap[item.MerchUuid]; ok {
toDelete = append(toDelete, item)
}
}
return s.repo.deleteZeroPrices(toDelete)
}
func (s *service) deleteZeroPricesPeriod(userUuid string, start, end time.Time) error {
return s.repo.deleteZeroPricesPeriod(userUuid, start, end)
}

View file

@ -26,10 +26,10 @@ func newController(service *service, utils interfaces.Utils) *controller {
func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, refreshMW gin.HandlerFunc) {
userGroup := r.Group("/user")
userGroup.POST("/", h.controller.register)
userGroup.GET("/", authMW, h.controller.get)
userGroup.PUT("/", authMW, h.controller.update)
userGroup.DELETE("/", authMW, h.controller.delete)
userGroup.POST("", h.controller.register)
userGroup.GET("", authMW, h.controller.get)
userGroup.PUT("", authMW, h.controller.update)
userGroup.DELETE("", authMW, h.controller.delete)
//auth
h.controller.authPath = fmt.Sprintf("%s/user/auth", h.apiPrefix)

View file

@ -40,7 +40,7 @@ func (r *repo) getByUuid(userUuid string) (user User, err error) {
}
func (r *repo) update(user map[string]any) error {
return r.db.Where("uuid = ?", user["uuid"]).Updates(&user).Error
return r.db.Model(&User{}).Where("uuid = ?", user["uuid"]).Updates(&user).Error
}
func (r *repo) delete(userUuid string) error {

View file

@ -4,33 +4,46 @@ import (
"context"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"merch-parser-api/internal/interfaces"
"net"
"net/http"
"time"
)
type App struct {
address string
apiPrefix string
modules []interfaces.Module
routerHandler interfaces.Router
router *gin.Engine
host string
address string
apiPrefix string
modules []interfaces.Module
routerHandler interfaces.Router
router *gin.Engine
grpcServer *grpc.Server
grpcServerPort string
grpcClientPort string
}
type Deps struct {
Host string
Port string
ApiPrefix string
Modules []interfaces.Module
RouterHandler interfaces.Router
Host string
Port string
ApiPrefix string
Modules []interfaces.Module
RouterHandler interfaces.Router
GrpcServer *grpc.Server
GrpcServerPort string
GrpcClientPort string
}
func NewApp(deps Deps) *App {
app := &App{
address: deps.Host + ":" + deps.Port,
apiPrefix: deps.ApiPrefix,
routerHandler: deps.RouterHandler,
modules: deps.Modules,
host: deps.Host,
address: deps.Host + ":" + deps.Port,
apiPrefix: deps.ApiPrefix,
routerHandler: deps.RouterHandler,
modules: deps.Modules,
grpcServer: deps.GrpcServer,
grpcServerPort: deps.GrpcServerPort,
grpcClientPort: deps.GrpcClientPort,
}
app.router = app.routerHandler.Set()
@ -62,6 +75,19 @@ func (a *App) Run(ctx context.Context) error {
serverErr <- server.ListenAndServe()
}()
go func() {
listener, err := net.Listen("tcp", net.JoinHostPort(a.host, a.grpcServerPort))
if err != nil {
log.WithField("err", err).Fatal("gRPC Server | Listener")
}
err = a.grpcServer.Serve(listener)
if err != nil {
log.WithField("err", err).Fatal("gRPC Server | Serve")
}
}()
log.Info("Starting gRPC server on port: ", a.grpcServerPort)
select {
case <-ctx.Done():
log.Info("Shutting down server")

View file

@ -0,0 +1,18 @@
package grpcService
import (
"google.golang.org/grpc"
"merch-parser-api/internal/interfaces"
pb "merch-parser-api/proto/taskProcessor"
)
func NewGrpcServer(taskProvider interfaces.TaskProvider) *grpc.Server {
srv := grpc.NewServer()
repoSrv := &repoServer{
taskProvider: taskProvider,
}
pb.RegisterTaskProcessorServer(srv, repoSrv)
return srv
}

View file

@ -0,0 +1,95 @@
package grpcService
import (
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"merch-parser-api/internal/interfaces"
"merch-parser-api/internal/shared"
pb "merch-parser-api/proto/taskProcessor"
"time"
)
type repoServer struct {
pb.UnimplementedTaskProcessorServer
taskProvider interfaces.TaskProvider
}
func (r *repoServer) RequestTask(_ *emptypb.Empty, stream pb.TaskProcessor_RequestTaskServer) error {
tasks, err := r.taskProvider.PrepareTasks()
if err != nil {
log.WithField("err", err).Error("gRPC Server | Request task error")
return err
}
for _, task := range tasks {
if err = stream.Send(&pb.Task{
MerchUuid: task.MerchUuid,
OriginSurugayaLink: task.OriginSurugayaLink,
OriginMandarakeLink: task.OriginMandarakeLink,
}); err != nil {
log.WithField("err", err).Error("gRPC Server | Stream send error")
return err
}
}
return nil
}
func (r *repoServer) SendResult(stream pb.TaskProcessor_SendResultServer) error {
saveInterval := time.Second * 2
batch := make([]shared.TaskResult, 0)
ticker := time.NewTicker(saveInterval)
defer ticker.Stop()
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
if len(batch) > 0 {
err := r.taskProvider.InsertPrices(batch)
if err != nil {
log.WithField("err", err).Error("gRPC Server | Batch insert")
}
}
}
}
}()
for {
response, err := stream.Recv()
if err == io.EOF {
log.Debug("gRPC EOF")
break
}
if err != nil {
log.WithField("err", err).Error("gRPC Server | Receive")
return err
}
entry := shared.TaskResult{
MerchUuid: response.MerchUuid,
Origin: response.OriginName,
Price: response.Price,
}
batch = append(batch, entry)
log.WithField("response", entry).Debug("gRPC Server | Receive success")
}
close(done)
if len(batch) > 0 {
err := r.taskProvider.InsertPrices(batch)
if err != nil {
log.WithField("err", err).Error("gRPC Server | Last data batch insert")
return err
}
}
return nil
}

View file

@ -0,0 +1,27 @@
package imagesProvider
import (
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
is "merch-parser-api/proto/imageStorage"
)
type Handler struct{}
func NewClient(address string) is.ImageStorageClient {
var opts []grpc.DialOption
insec := grpc.WithTransportCredentials(insecure.NewCredentials())
opts = append(opts, insec)
conn, err := grpc.NewClient(address, opts...)
if err != nil {
log.Fatal(err)
}
log.WithFields(log.Fields{
"address": address,
}).Debug("gRPC | API client")
return is.NewImageStorageClient(conn)
}

View file

@ -0,0 +1,19 @@
package interfaces
import (
"context"
"io"
"net/url"
"time"
)
// MinIO service replaced by imagesProvider
type MediaStorage interface {
CheckBucketExists(bucketName string) (bool, error)
Upload(ctx context.Context, bucket, object string, reader io.Reader, size int64) error
GetPublicLink(ctx context.Context, bucket, object string) (string, string, error)
GetPresignedLink(ctx context.Context, bucket, object string, expires time.Duration, params url.Values) (string, error)
Delete(ctx context.Context, bucket, object string) error
GetObjectEtag(ctx context.Context, bucketName, object string) (string, error)
}

View file

@ -0,0 +1,8 @@
package interfaces
import "merch-parser-api/internal/shared"
type TaskProvider interface {
PrepareTasks() (map[string]shared.Task, error)
InsertPrices([]shared.TaskResult) error
}

View file

@ -1,6 +1,9 @@
package interfaces
import "github.com/gin-gonic/gin"
import (
"github.com/gin-gonic/gin"
"time"
)
type Utils interface {
IsEmail(email string) bool
@ -8,4 +11,5 @@ type Utils interface {
GetRefreshUuidFromContext(c *gin.Context) (string, error)
HashPassword(password string) (string, error)
ComparePasswords(hashedPassword string, plainPassword string) error
ParseTime(t string) (time.Time, error)
}

View file

@ -0,0 +1,42 @@
package mediaStorage
import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
log "github.com/sirupsen/logrus"
)
type Handler struct {
*Service
}
type Deps struct {
Endpoint string
User string
Password string
Secure string
}
func NewHandler(deps Deps) *Handler {
secureMode := false
if deps.Secure == "true" {
secureMode = true
}
minioClient, err := minio.New(deps.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(deps.User, deps.Password, ""),
Secure: secureMode,
})
if err != nil {
log.WithError(err).Fatal("Media storage | Failed to create minio client")
}
log.WithFields(log.Fields{
"endpoint": deps.Endpoint,
"secure": secureMode,
}).Debug("Media storage | Created minio client")
return &Handler{
newService(minioClient, deps.Endpoint, secureMode),
}
}

View file

@ -0,0 +1,89 @@
package mediaStorage
import (
"context"
"fmt"
"github.com/minio/minio-go/v7"
log "github.com/sirupsen/logrus"
"io"
"net/url"
"strings"
"time"
)
type Service struct {
client *minio.Client
endpoint string
secureMode bool
}
func newService(client *minio.Client, endpoint string, secureMode bool) *Service {
return &Service{
client: client,
endpoint: endpoint,
secureMode: secureMode,
}
}
func (s *Service) CheckBucketExists(bucketName string) (bool, error) {
ctx := context.Background()
exists, err := s.client.BucketExists(ctx, bucketName)
if err != nil {
log.WithError(err).Fatal("Media storage | Failed to check bucket existence")
return exists, err
}
log.Infof("Media storage | Bucket %s exists", bucketName)
return exists, nil
}
func (s *Service) Upload(ctx context.Context, bucket, object string, reader io.Reader, size int64) error {
_, err := s.client.PutObject(ctx, bucket, object, reader, size, minio.PutObjectOptions{ContentType: "image/jpeg"})
return err
}
func (s *Service) GetPublicLink(ctx context.Context, bucket, object string) (string, string, error) {
stat, err := s.client.StatObject(ctx, bucket, object, minio.StatObjectOptions{})
if err != nil {
if err.Error() == minio.ToErrorResponse(err).Error() {
return "", "", nil
}
log.WithFields(log.Fields{
"error": err,
"key": bucket + "/" + object,
}).Error("Media storage | Failed to get public link")
return "", "", err
}
var scheme string
if s.secureMode {
scheme = "https"
} else {
scheme = "http"
}
link := fmt.Sprintf("%s://%s/%s/%s", scheme, strings.TrimRight(s.endpoint, "/"), bucket, object)
log.WithFields(log.Fields{"link": link}).Debug("Media storage | Get public link")
return link, stat.ETag, nil
}
func (s *Service) GetPresignedLink(ctx context.Context, bucket, object string, expires time.Duration, params url.Values) (string, error) {
presigned, err := s.client.PresignedGetObject(ctx, bucket, object, expires, params)
if err != nil {
return "", err
}
return presigned.String(), nil
}
func (s *Service) Delete(ctx context.Context, bucket, object string) error {
return s.client.RemoveObject(ctx, bucket, object, minio.RemoveObjectOptions{})
}
func (s *Service) GetObjectEtag(ctx context.Context, bucketName, object string) (string, error) {
info, err := s.client.StatObject(ctx, bucketName, object, minio.StatObjectOptions{})
if err != nil {
return "", err
}
return info.ETag, nil
}

View file

@ -7,17 +7,15 @@ import (
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"merch-parser-api/internal/interfaces"
"merch-parser-api/internal/shared"
"net/http"
"time"
)
type router struct {
apiPrefix string
engine *gin.Engine
ginMode string
excludeRoutes map[string]shared.ExcludeRoute
tokenProv interfaces.JWTProvider
apiPrefix string
engine *gin.Engine
ginMode string
tokenProv interfaces.JWTProvider
}
type Deps struct {
@ -28,8 +26,13 @@ type Deps struct {
}
func NewRouter(deps Deps) interfaces.Router {
engine := gin.Default()
if deps.GinMode == "release" {
gin.SetMode(gin.ReleaseMode)
} else {
gin.SetMode(gin.DebugMode)
}
engine := gin.Default()
if deps.GinMode == "release" {
gin.SetMode(gin.ReleaseMode)
err := engine.SetTrustedProxies([]string{"172.20.0.0/16"})

View file

@ -1,6 +0,0 @@
package shared
type ExcludeRoute struct {
Route string
Method string
}

13
internal/shared/task.go Normal file
View file

@ -0,0 +1,13 @@
package shared
type Task struct {
MerchUuid string
OriginSurugayaLink string
OriginMandarakeLink string
}
type TaskResult struct {
MerchUuid string
Origin string
Price int32
}

View file

@ -55,4 +55,27 @@ CREATE TABLE prices(
merch_uuid VARCHAR(36) NOT NULL,
price INT NULL,
origin INT
);
);
CREATE TABLE labels(
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NULL,
deleted_at TIMESTAMP WITH TIME ZONE NULL,
user_uuid VARCHAR(36) NOT NULL,
label_uuid VARCHAR(36) NOT NULL,
name VARCHAR(255),
color VARCHAR(32),
bg_color VARCHAR(32)
);
CREATE TABLE card_labels (
id BIGSERIAL PRIMARY KEY,
user_uuid VARCHAR(36) NOT NULL,
label_uuid VARCHAR(36) NOT NULL,
merch_uuid VARCHAR(36) NOT NULL
);
ALTER TABLE card_labels
ADD CONSTRAINT card_labels_unique_user_label_merch
UNIQUE (user_uuid, label_uuid, merch_uuid);

11
pkg/utils/time.go Normal file
View file

@ -0,0 +1,11 @@
package utils
import "time"
func (u *Utils) ParseTime(t string) (time.Time, error) {
timeStr, err := time.Parse(time.RFC3339, t)
if err != nil {
return time.Time{}, err
}
return timeStr, nil
}

27
proto/imageStorage.proto Normal file
View file

@ -0,0 +1,27 @@
syntax="proto3";
import "google/protobuf/empty.proto";
package imageStorage;
option go_package = "imageStorage/pkg/proto/imageStorage";
message UploadMerchImageRequest{
bytes imageData = 1;
string userUuid = 2;
string merchUuid = 3;
}
message UploadMerchImageResponse {
string fullImage = 1;
string thumbnail = 2;
}
message DeleteImageRequest {
string userUuid = 1;
string merchUuid = 2;
}
service ImageStorage {
rpc UploadImage(UploadMerchImageRequest) returns (UploadMerchImageResponse);
rpc DeleteImage(DeleteImageRequest) returns (google.protobuf.Empty);
}

View file

@ -0,0 +1,261 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.8
// protoc v6.32.1
// source: imageStorage.proto
package imageStorage
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type UploadMerchImageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ImageData []byte `protobuf:"bytes,1,opt,name=imageData,proto3" json:"imageData,omitempty"`
UserUuid string `protobuf:"bytes,2,opt,name=userUuid,proto3" json:"userUuid,omitempty"`
MerchUuid string `protobuf:"bytes,3,opt,name=merchUuid,proto3" json:"merchUuid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UploadMerchImageRequest) Reset() {
*x = UploadMerchImageRequest{}
mi := &file_imageStorage_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UploadMerchImageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UploadMerchImageRequest) ProtoMessage() {}
func (x *UploadMerchImageRequest) ProtoReflect() protoreflect.Message {
mi := &file_imageStorage_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UploadMerchImageRequest.ProtoReflect.Descriptor instead.
func (*UploadMerchImageRequest) Descriptor() ([]byte, []int) {
return file_imageStorage_proto_rawDescGZIP(), []int{0}
}
func (x *UploadMerchImageRequest) GetImageData() []byte {
if x != nil {
return x.ImageData
}
return nil
}
func (x *UploadMerchImageRequest) GetUserUuid() string {
if x != nil {
return x.UserUuid
}
return ""
}
func (x *UploadMerchImageRequest) GetMerchUuid() string {
if x != nil {
return x.MerchUuid
}
return ""
}
type UploadMerchImageResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
FullImage string `protobuf:"bytes,1,opt,name=fullImage,proto3" json:"fullImage,omitempty"`
Thumbnail string `protobuf:"bytes,2,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UploadMerchImageResponse) Reset() {
*x = UploadMerchImageResponse{}
mi := &file_imageStorage_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UploadMerchImageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UploadMerchImageResponse) ProtoMessage() {}
func (x *UploadMerchImageResponse) ProtoReflect() protoreflect.Message {
mi := &file_imageStorage_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UploadMerchImageResponse.ProtoReflect.Descriptor instead.
func (*UploadMerchImageResponse) Descriptor() ([]byte, []int) {
return file_imageStorage_proto_rawDescGZIP(), []int{1}
}
func (x *UploadMerchImageResponse) GetFullImage() string {
if x != nil {
return x.FullImage
}
return ""
}
func (x *UploadMerchImageResponse) GetThumbnail() string {
if x != nil {
return x.Thumbnail
}
return ""
}
type DeleteImageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserUuid string `protobuf:"bytes,1,opt,name=userUuid,proto3" json:"userUuid,omitempty"`
MerchUuid string `protobuf:"bytes,2,opt,name=merchUuid,proto3" json:"merchUuid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteImageRequest) Reset() {
*x = DeleteImageRequest{}
mi := &file_imageStorage_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteImageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteImageRequest) ProtoMessage() {}
func (x *DeleteImageRequest) ProtoReflect() protoreflect.Message {
mi := &file_imageStorage_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteImageRequest.ProtoReflect.Descriptor instead.
func (*DeleteImageRequest) Descriptor() ([]byte, []int) {
return file_imageStorage_proto_rawDescGZIP(), []int{2}
}
func (x *DeleteImageRequest) GetUserUuid() string {
if x != nil {
return x.UserUuid
}
return ""
}
func (x *DeleteImageRequest) GetMerchUuid() string {
if x != nil {
return x.MerchUuid
}
return ""
}
var File_imageStorage_proto protoreflect.FileDescriptor
const file_imageStorage_proto_rawDesc = "" +
"\n" +
"\x12imageStorage.proto\x12\fimageStorage\x1a\x1bgoogle/protobuf/empty.proto\"q\n" +
"\x17UploadMerchImageRequest\x12\x1c\n" +
"\timageData\x18\x01 \x01(\fR\timageData\x12\x1a\n" +
"\buserUuid\x18\x02 \x01(\tR\buserUuid\x12\x1c\n" +
"\tmerchUuid\x18\x03 \x01(\tR\tmerchUuid\"V\n" +
"\x18UploadMerchImageResponse\x12\x1c\n" +
"\tfullImage\x18\x01 \x01(\tR\tfullImage\x12\x1c\n" +
"\tthumbnail\x18\x02 \x01(\tR\tthumbnail\"N\n" +
"\x12DeleteImageRequest\x12\x1a\n" +
"\buserUuid\x18\x01 \x01(\tR\buserUuid\x12\x1c\n" +
"\tmerchUuid\x18\x02 \x01(\tR\tmerchUuid2\xb5\x01\n" +
"\fImageStorage\x12\\\n" +
"\vUploadImage\x12%.imageStorage.UploadMerchImageRequest\x1a&.imageStorage.UploadMerchImageResponse\x12G\n" +
"\vDeleteImage\x12 .imageStorage.DeleteImageRequest\x1a\x16.google.protobuf.EmptyB%Z#imageStorage/pkg/proto/imageStorageb\x06proto3"
var (
file_imageStorage_proto_rawDescOnce sync.Once
file_imageStorage_proto_rawDescData []byte
)
func file_imageStorage_proto_rawDescGZIP() []byte {
file_imageStorage_proto_rawDescOnce.Do(func() {
file_imageStorage_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_imageStorage_proto_rawDesc), len(file_imageStorage_proto_rawDesc)))
})
return file_imageStorage_proto_rawDescData
}
var file_imageStorage_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_imageStorage_proto_goTypes = []any{
(*UploadMerchImageRequest)(nil), // 0: imageStorage.UploadMerchImageRequest
(*UploadMerchImageResponse)(nil), // 1: imageStorage.UploadMerchImageResponse
(*DeleteImageRequest)(nil), // 2: imageStorage.DeleteImageRequest
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
}
var file_imageStorage_proto_depIdxs = []int32{
0, // 0: imageStorage.ImageStorage.UploadImage:input_type -> imageStorage.UploadMerchImageRequest
2, // 1: imageStorage.ImageStorage.DeleteImage:input_type -> imageStorage.DeleteImageRequest
1, // 2: imageStorage.ImageStorage.UploadImage:output_type -> imageStorage.UploadMerchImageResponse
3, // 3: imageStorage.ImageStorage.DeleteImage:output_type -> google.protobuf.Empty
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_imageStorage_proto_init() }
func file_imageStorage_proto_init() {
if File_imageStorage_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_imageStorage_proto_rawDesc), len(file_imageStorage_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_imageStorage_proto_goTypes,
DependencyIndexes: file_imageStorage_proto_depIdxs,
MessageInfos: file_imageStorage_proto_msgTypes,
}.Build()
File_imageStorage_proto = out.File
file_imageStorage_proto_goTypes = nil
file_imageStorage_proto_depIdxs = nil
}

View file

@ -0,0 +1,160 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.32.1
// source: imageStorage.proto
package imageStorage
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
ImageStorage_UploadImage_FullMethodName = "/imageStorage.ImageStorage/UploadImage"
ImageStorage_DeleteImage_FullMethodName = "/imageStorage.ImageStorage/DeleteImage"
)
// ImageStorageClient is the client API for ImageStorage service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ImageStorageClient interface {
UploadImage(ctx context.Context, in *UploadMerchImageRequest, opts ...grpc.CallOption) (*UploadMerchImageResponse, error)
DeleteImage(ctx context.Context, in *DeleteImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type imageStorageClient struct {
cc grpc.ClientConnInterface
}
func NewImageStorageClient(cc grpc.ClientConnInterface) ImageStorageClient {
return &imageStorageClient{cc}
}
func (c *imageStorageClient) UploadImage(ctx context.Context, in *UploadMerchImageRequest, opts ...grpc.CallOption) (*UploadMerchImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UploadMerchImageResponse)
err := c.cc.Invoke(ctx, ImageStorage_UploadImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *imageStorageClient) DeleteImage(ctx context.Context, in *DeleteImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ImageStorage_DeleteImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ImageStorageServer is the server API for ImageStorage service.
// All implementations must embed UnimplementedImageStorageServer
// for forward compatibility.
type ImageStorageServer interface {
UploadImage(context.Context, *UploadMerchImageRequest) (*UploadMerchImageResponse, error)
DeleteImage(context.Context, *DeleteImageRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedImageStorageServer()
}
// UnimplementedImageStorageServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedImageStorageServer struct{}
func (UnimplementedImageStorageServer) UploadImage(context.Context, *UploadMerchImageRequest) (*UploadMerchImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UploadImage not implemented")
}
func (UnimplementedImageStorageServer) DeleteImage(context.Context, *DeleteImageRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteImage not implemented")
}
func (UnimplementedImageStorageServer) mustEmbedUnimplementedImageStorageServer() {}
func (UnimplementedImageStorageServer) testEmbeddedByValue() {}
// UnsafeImageStorageServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ImageStorageServer will
// result in compilation errors.
type UnsafeImageStorageServer interface {
mustEmbedUnimplementedImageStorageServer()
}
func RegisterImageStorageServer(s grpc.ServiceRegistrar, srv ImageStorageServer) {
// If the following call pancis, it indicates UnimplementedImageStorageServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&ImageStorage_ServiceDesc, srv)
}
func _ImageStorage_UploadImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UploadMerchImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageStorageServer).UploadImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageStorage_UploadImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageStorageServer).UploadImage(ctx, req.(*UploadMerchImageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ImageStorage_DeleteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageStorageServer).DeleteImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageStorage_DeleteImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageStorageServer).DeleteImage(ctx, req.(*DeleteImageRequest))
}
return interceptor(ctx, in, info, handler)
}
// ImageStorage_ServiceDesc is the grpc.ServiceDesc for ImageStorage service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ImageStorage_ServiceDesc = grpc.ServiceDesc{
ServiceName: "imageStorage.ImageStorage",
HandlerType: (*ImageStorageServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "UploadImage",
Handler: _ImageStorage_UploadImage_Handler,
},
{
MethodName: "DeleteImage",
Handler: _ImageStorage_DeleteImage_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "imageStorage.proto",
}

41
proto/task.proto Normal file
View file

@ -0,0 +1,41 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package taskProcessor;
option go_package = "./taskProcessor";
message Task{
string merch_uuid = 1;
string origin_surugaya_link = 2;
string origin_mandarake_link = 3;
}
message Result{
string merch_uuid = 1;
string origin_name = 2;
int32 price = 3;
}
message ProcessorStatusRequest{}
message ProcessorStatusResponse {
int64 appStart = 1;
int64 lastCheck = 2;
int32 tasksReceived = 3;
int32 tasksInProgress = 4;
int32 tasksFirstTry = 5;
int32 tasksDoneAfterRetry = 6;
int32 tasksFailed = 7;
string workStatus = 8;
int32 numCPUs = 9;
int32 checkPeriod = 10;
int32 retriesCount = 11;
int32 retriesMinutes = 12;
}
service TaskProcessor {
rpc RequestTask(google.protobuf.Empty) returns (stream Task);
rpc SendResult(stream Result) returns (google.protobuf.Empty);
rpc ProcessorStatus(ProcessorStatusRequest) returns (ProcessorStatusResponse);
}

View file

@ -0,0 +1,409 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.8
// protoc v6.32.0
// source: task.proto
package taskProcessor
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Task struct {
state protoimpl.MessageState `protogen:"open.v1"`
MerchUuid string `protobuf:"bytes,1,opt,name=merch_uuid,json=merchUuid,proto3" json:"merch_uuid,omitempty"`
OriginSurugayaLink string `protobuf:"bytes,2,opt,name=origin_surugaya_link,json=originSurugayaLink,proto3" json:"origin_surugaya_link,omitempty"`
OriginMandarakeLink string `protobuf:"bytes,3,opt,name=origin_mandarake_link,json=originMandarakeLink,proto3" json:"origin_mandarake_link,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Task) Reset() {
*x = Task{}
mi := &file_task_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Task) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Task) ProtoMessage() {}
func (x *Task) ProtoReflect() protoreflect.Message {
mi := &file_task_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Task.ProtoReflect.Descriptor instead.
func (*Task) Descriptor() ([]byte, []int) {
return file_task_proto_rawDescGZIP(), []int{0}
}
func (x *Task) GetMerchUuid() string {
if x != nil {
return x.MerchUuid
}
return ""
}
func (x *Task) GetOriginSurugayaLink() string {
if x != nil {
return x.OriginSurugayaLink
}
return ""
}
func (x *Task) GetOriginMandarakeLink() string {
if x != nil {
return x.OriginMandarakeLink
}
return ""
}
type Result struct {
state protoimpl.MessageState `protogen:"open.v1"`
MerchUuid string `protobuf:"bytes,1,opt,name=merch_uuid,json=merchUuid,proto3" json:"merch_uuid,omitempty"`
OriginName string `protobuf:"bytes,2,opt,name=origin_name,json=originName,proto3" json:"origin_name,omitempty"`
Price int32 `protobuf:"varint,3,opt,name=price,proto3" json:"price,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Result) Reset() {
*x = Result{}
mi := &file_task_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Result) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Result) ProtoMessage() {}
func (x *Result) ProtoReflect() protoreflect.Message {
mi := &file_task_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Result.ProtoReflect.Descriptor instead.
func (*Result) Descriptor() ([]byte, []int) {
return file_task_proto_rawDescGZIP(), []int{1}
}
func (x *Result) GetMerchUuid() string {
if x != nil {
return x.MerchUuid
}
return ""
}
func (x *Result) GetOriginName() string {
if x != nil {
return x.OriginName
}
return ""
}
func (x *Result) GetPrice() int32 {
if x != nil {
return x.Price
}
return 0
}
type ProcessorStatusRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProcessorStatusRequest) Reset() {
*x = ProcessorStatusRequest{}
mi := &file_task_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProcessorStatusRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProcessorStatusRequest) ProtoMessage() {}
func (x *ProcessorStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_task_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProcessorStatusRequest.ProtoReflect.Descriptor instead.
func (*ProcessorStatusRequest) Descriptor() ([]byte, []int) {
return file_task_proto_rawDescGZIP(), []int{2}
}
type ProcessorStatusResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
AppStart int64 `protobuf:"varint,1,opt,name=appStart,proto3" json:"appStart,omitempty"`
LastCheck int64 `protobuf:"varint,2,opt,name=lastCheck,proto3" json:"lastCheck,omitempty"`
TasksReceived int32 `protobuf:"varint,3,opt,name=tasksReceived,proto3" json:"tasksReceived,omitempty"`
TasksInProgress int32 `protobuf:"varint,4,opt,name=tasksInProgress,proto3" json:"tasksInProgress,omitempty"`
TasksFirstTry int32 `protobuf:"varint,5,opt,name=tasksFirstTry,proto3" json:"tasksFirstTry,omitempty"`
TasksDoneAfterRetry int32 `protobuf:"varint,6,opt,name=tasksDoneAfterRetry,proto3" json:"tasksDoneAfterRetry,omitempty"`
TasksFailed int32 `protobuf:"varint,7,opt,name=tasksFailed,proto3" json:"tasksFailed,omitempty"`
WorkStatus string `protobuf:"bytes,8,opt,name=workStatus,proto3" json:"workStatus,omitempty"`
NumCPUs int32 `protobuf:"varint,9,opt,name=numCPUs,proto3" json:"numCPUs,omitempty"`
CheckPeriod int32 `protobuf:"varint,10,opt,name=checkPeriod,proto3" json:"checkPeriod,omitempty"`
RetriesCount int32 `protobuf:"varint,11,opt,name=retriesCount,proto3" json:"retriesCount,omitempty"`
RetriesMinutes int32 `protobuf:"varint,12,opt,name=retriesMinutes,proto3" json:"retriesMinutes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProcessorStatusResponse) Reset() {
*x = ProcessorStatusResponse{}
mi := &file_task_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProcessorStatusResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProcessorStatusResponse) ProtoMessage() {}
func (x *ProcessorStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_task_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProcessorStatusResponse.ProtoReflect.Descriptor instead.
func (*ProcessorStatusResponse) Descriptor() ([]byte, []int) {
return file_task_proto_rawDescGZIP(), []int{3}
}
func (x *ProcessorStatusResponse) GetAppStart() int64 {
if x != nil {
return x.AppStart
}
return 0
}
func (x *ProcessorStatusResponse) GetLastCheck() int64 {
if x != nil {
return x.LastCheck
}
return 0
}
func (x *ProcessorStatusResponse) GetTasksReceived() int32 {
if x != nil {
return x.TasksReceived
}
return 0
}
func (x *ProcessorStatusResponse) GetTasksInProgress() int32 {
if x != nil {
return x.TasksInProgress
}
return 0
}
func (x *ProcessorStatusResponse) GetTasksFirstTry() int32 {
if x != nil {
return x.TasksFirstTry
}
return 0
}
func (x *ProcessorStatusResponse) GetTasksDoneAfterRetry() int32 {
if x != nil {
return x.TasksDoneAfterRetry
}
return 0
}
func (x *ProcessorStatusResponse) GetTasksFailed() int32 {
if x != nil {
return x.TasksFailed
}
return 0
}
func (x *ProcessorStatusResponse) GetWorkStatus() string {
if x != nil {
return x.WorkStatus
}
return ""
}
func (x *ProcessorStatusResponse) GetNumCPUs() int32 {
if x != nil {
return x.NumCPUs
}
return 0
}
func (x *ProcessorStatusResponse) GetCheckPeriod() int32 {
if x != nil {
return x.CheckPeriod
}
return 0
}
func (x *ProcessorStatusResponse) GetRetriesCount() int32 {
if x != nil {
return x.RetriesCount
}
return 0
}
func (x *ProcessorStatusResponse) GetRetriesMinutes() int32 {
if x != nil {
return x.RetriesMinutes
}
return 0
}
var File_task_proto protoreflect.FileDescriptor
const file_task_proto_rawDesc = "" +
"\n" +
"\n" +
"task.proto\x12\rtaskProcessor\x1a\x1bgoogle/protobuf/empty.proto\"\x8b\x01\n" +
"\x04Task\x12\x1d\n" +
"\n" +
"merch_uuid\x18\x01 \x01(\tR\tmerchUuid\x120\n" +
"\x14origin_surugaya_link\x18\x02 \x01(\tR\x12originSurugayaLink\x122\n" +
"\x15origin_mandarake_link\x18\x03 \x01(\tR\x13originMandarakeLink\"^\n" +
"\x06Result\x12\x1d\n" +
"\n" +
"merch_uuid\x18\x01 \x01(\tR\tmerchUuid\x12\x1f\n" +
"\vorigin_name\x18\x02 \x01(\tR\n" +
"originName\x12\x14\n" +
"\x05price\x18\x03 \x01(\x05R\x05price\"\x18\n" +
"\x16ProcessorStatusRequest\"\xc5\x03\n" +
"\x17ProcessorStatusResponse\x12\x1a\n" +
"\bappStart\x18\x01 \x01(\x03R\bappStart\x12\x1c\n" +
"\tlastCheck\x18\x02 \x01(\x03R\tlastCheck\x12$\n" +
"\rtasksReceived\x18\x03 \x01(\x05R\rtasksReceived\x12(\n" +
"\x0ftasksInProgress\x18\x04 \x01(\x05R\x0ftasksInProgress\x12$\n" +
"\rtasksFirstTry\x18\x05 \x01(\x05R\rtasksFirstTry\x120\n" +
"\x13tasksDoneAfterRetry\x18\x06 \x01(\x05R\x13tasksDoneAfterRetry\x12 \n" +
"\vtasksFailed\x18\a \x01(\x05R\vtasksFailed\x12\x1e\n" +
"\n" +
"workStatus\x18\b \x01(\tR\n" +
"workStatus\x12\x18\n" +
"\anumCPUs\x18\t \x01(\x05R\anumCPUs\x12 \n" +
"\vcheckPeriod\x18\n" +
" \x01(\x05R\vcheckPeriod\x12\"\n" +
"\fretriesCount\x18\v \x01(\x05R\fretriesCount\x12&\n" +
"\x0eretriesMinutes\x18\f \x01(\x05R\x0eretriesMinutes2\xee\x01\n" +
"\rTaskProcessor\x12<\n" +
"\vRequestTask\x12\x16.google.protobuf.Empty\x1a\x13.taskProcessor.Task0\x01\x12=\n" +
"\n" +
"SendResult\x12\x15.taskProcessor.Result\x1a\x16.google.protobuf.Empty(\x01\x12`\n" +
"\x0fProcessorStatus\x12%.taskProcessor.ProcessorStatusRequest\x1a&.taskProcessor.ProcessorStatusResponseB\x11Z\x0f./taskProcessorb\x06proto3"
var (
file_task_proto_rawDescOnce sync.Once
file_task_proto_rawDescData []byte
)
func file_task_proto_rawDescGZIP() []byte {
file_task_proto_rawDescOnce.Do(func() {
file_task_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_task_proto_rawDesc), len(file_task_proto_rawDesc)))
})
return file_task_proto_rawDescData
}
var file_task_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_task_proto_goTypes = []any{
(*Task)(nil), // 0: taskProcessor.Task
(*Result)(nil), // 1: taskProcessor.Result
(*ProcessorStatusRequest)(nil), // 2: taskProcessor.ProcessorStatusRequest
(*ProcessorStatusResponse)(nil), // 3: taskProcessor.ProcessorStatusResponse
(*emptypb.Empty)(nil), // 4: google.protobuf.Empty
}
var file_task_proto_depIdxs = []int32{
4, // 0: taskProcessor.TaskProcessor.RequestTask:input_type -> google.protobuf.Empty
1, // 1: taskProcessor.TaskProcessor.SendResult:input_type -> taskProcessor.Result
2, // 2: taskProcessor.TaskProcessor.ProcessorStatus:input_type -> taskProcessor.ProcessorStatusRequest
0, // 3: taskProcessor.TaskProcessor.RequestTask:output_type -> taskProcessor.Task
4, // 4: taskProcessor.TaskProcessor.SendResult:output_type -> google.protobuf.Empty
3, // 5: taskProcessor.TaskProcessor.ProcessorStatus:output_type -> taskProcessor.ProcessorStatusResponse
3, // [3:6] is the sub-list for method output_type
0, // [0:3] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_task_proto_init() }
func file_task_proto_init() {
if File_task_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_task_proto_rawDesc), len(file_task_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_task_proto_goTypes,
DependencyIndexes: file_task_proto_depIdxs,
MessageInfos: file_task_proto_msgTypes,
}.Build()
File_task_proto = out.File
file_task_proto_goTypes = nil
file_task_proto_depIdxs = nil
}

View file

@ -0,0 +1,195 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.32.0
// source: task.proto
package taskProcessor
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
TaskProcessor_RequestTask_FullMethodName = "/taskProcessor.TaskProcessor/RequestTask"
TaskProcessor_SendResult_FullMethodName = "/taskProcessor.TaskProcessor/SendResult"
TaskProcessor_ProcessorStatus_FullMethodName = "/taskProcessor.TaskProcessor/ProcessorStatus"
)
// TaskProcessorClient is the client API for TaskProcessor service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TaskProcessorClient interface {
RequestTask(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Task], error)
SendResult(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[Result, emptypb.Empty], error)
ProcessorStatus(ctx context.Context, in *ProcessorStatusRequest, opts ...grpc.CallOption) (*ProcessorStatusResponse, error)
}
type taskProcessorClient struct {
cc grpc.ClientConnInterface
}
func NewTaskProcessorClient(cc grpc.ClientConnInterface) TaskProcessorClient {
return &taskProcessorClient{cc}
}
func (c *taskProcessorClient) RequestTask(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Task], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &TaskProcessor_ServiceDesc.Streams[0], TaskProcessor_RequestTask_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, Task]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type TaskProcessor_RequestTaskClient = grpc.ServerStreamingClient[Task]
func (c *taskProcessorClient) SendResult(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[Result, emptypb.Empty], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &TaskProcessor_ServiceDesc.Streams[1], TaskProcessor_SendResult_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[Result, emptypb.Empty]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type TaskProcessor_SendResultClient = grpc.ClientStreamingClient[Result, emptypb.Empty]
func (c *taskProcessorClient) ProcessorStatus(ctx context.Context, in *ProcessorStatusRequest, opts ...grpc.CallOption) (*ProcessorStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ProcessorStatusResponse)
err := c.cc.Invoke(ctx, TaskProcessor_ProcessorStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// TaskProcessorServer is the server API for TaskProcessor service.
// All implementations must embed UnimplementedTaskProcessorServer
// for forward compatibility.
type TaskProcessorServer interface {
RequestTask(*emptypb.Empty, grpc.ServerStreamingServer[Task]) error
SendResult(grpc.ClientStreamingServer[Result, emptypb.Empty]) error
ProcessorStatus(context.Context, *ProcessorStatusRequest) (*ProcessorStatusResponse, error)
mustEmbedUnimplementedTaskProcessorServer()
}
// UnimplementedTaskProcessorServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedTaskProcessorServer struct{}
func (UnimplementedTaskProcessorServer) RequestTask(*emptypb.Empty, grpc.ServerStreamingServer[Task]) error {
return status.Errorf(codes.Unimplemented, "method RequestTask not implemented")
}
func (UnimplementedTaskProcessorServer) SendResult(grpc.ClientStreamingServer[Result, emptypb.Empty]) error {
return status.Errorf(codes.Unimplemented, "method SendResult not implemented")
}
func (UnimplementedTaskProcessorServer) ProcessorStatus(context.Context, *ProcessorStatusRequest) (*ProcessorStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ProcessorStatus not implemented")
}
func (UnimplementedTaskProcessorServer) mustEmbedUnimplementedTaskProcessorServer() {}
func (UnimplementedTaskProcessorServer) testEmbeddedByValue() {}
// UnsafeTaskProcessorServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TaskProcessorServer will
// result in compilation errors.
type UnsafeTaskProcessorServer interface {
mustEmbedUnimplementedTaskProcessorServer()
}
func RegisterTaskProcessorServer(s grpc.ServiceRegistrar, srv TaskProcessorServer) {
// If the following call pancis, it indicates UnimplementedTaskProcessorServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&TaskProcessor_ServiceDesc, srv)
}
func _TaskProcessor_RequestTask_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(TaskProcessorServer).RequestTask(m, &grpc.GenericServerStream[emptypb.Empty, Task]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type TaskProcessor_RequestTaskServer = grpc.ServerStreamingServer[Task]
func _TaskProcessor_SendResult_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(TaskProcessorServer).SendResult(&grpc.GenericServerStream[Result, emptypb.Empty]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type TaskProcessor_SendResultServer = grpc.ClientStreamingServer[Result, emptypb.Empty]
func _TaskProcessor_ProcessorStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ProcessorStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskProcessorServer).ProcessorStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: TaskProcessor_ProcessorStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskProcessorServer).ProcessorStatus(ctx, req.(*ProcessorStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
// TaskProcessor_ServiceDesc is the grpc.ServiceDesc for TaskProcessor service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var TaskProcessor_ServiceDesc = grpc.ServiceDesc{
ServiceName: "taskProcessor.TaskProcessor",
HandlerType: (*TaskProcessorServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ProcessorStatus",
Handler: _TaskProcessor_ProcessorStatus_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "RequestTask",
Handler: _TaskProcessor_RequestTask_Handler,
ServerStreams: true,
},
{
StreamName: "SendResult",
Handler: _TaskProcessor_SendResult_Handler,
ClientStreams: true,
},
},
Metadata: "task.proto",
}