├── .app.config.dev.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── app │ └── main.go ├── config ├── app_config.go └── common.go ├── coverage.html ├── docker └── Dockerfile ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── embed.go ├── gen └── app │ └── db │ ├── books_query.sql.go │ ├── db.go │ └── models.go ├── go.mod ├── go.sum ├── internal ├── app │ ├── app.go │ └── migrate.go ├── controller │ └── http │ │ └── app.go ├── domain │ ├── books.go │ ├── common.go │ ├── errors.go │ └── schema.go └── usecase │ ├── books.go │ ├── interface.go │ └── repo │ └── books_postgres.go ├── migrations └── app │ ├── 000001_create_books_table.down.sql │ └── 000001_create_books_table.up.sql ├── pkg ├── migrator │ ├── migrator.go │ └── postgres_migrator.go └── postgres │ ├── postgres.go │ ├── qt.go │ └── sqlstate.go ├── queries └── app │ └── books_query.sql └── sqlc.yaml /.app.config.dev.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: debug 3 | http: 4 | host: 0.0.0.0 5 | port: 8888 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | build/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mikhail-bigun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME = app 2 | BUILD_DIR = $(PWD)/build 3 | CONFIG_FILE = .$(APP_NAME).config.dev.yaml 4 | 5 | POSTGRES_HOST = 127.0.0.1 6 | POSTGRES_PORT = 5432 7 | POSTGRES_USER = postgres 8 | POSTGRES_PASSWORD = postgres 9 | POSTGRES_DB = postgres 10 | POSTGRES_SSL_MODE = disable 11 | POSTGRES_SEARCH_PATH = public 12 | POSTGRES_URL = postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=$(POSTGRES_SSL_MODE)&search_path=$(POSTGRES_SEARCH_PATH) 13 | 14 | MIGRATIONS_FOLDER = migrations/$(APP_NAME) 15 | 16 | DOCKER_PATH = ./docker/Dockerfile 17 | DOCKER_TAG = test 18 | DOCKER_NETWORK = dev-network 19 | 20 | PRIVATE_REGISTRY_PATH = "" 21 | 22 | GOPRIVATE_USER = "__token__" 23 | GOPRIVATE_PAT = "" 24 | GOPRIVATE = "" 25 | GOPRIVATE_SCHEMA = "https" 26 | 27 | clean: 28 | rm -rf $(BUILD_DIR) 29 | critic: 30 | gocritic check -enableAll main 31 | security: 32 | gosec ./... 33 | lint: 34 | golangci-lint run ./... 35 | test: clean critic security lint 36 | go test -v -timeout 30s -coverprofile=cover.out -cover -p 1 ./... 37 | go tool cover -html=cover.out -o coverage.html 38 | build: test 39 | CGO_ENABLED=0 go build -ldflags="-w -s" -o $(BUILD_DIR)/$(APP_NAME) ./cmd/$(APP_NAME)/main.go 40 | run: build 41 | $(BUILD_DIR)/$(APP_NAME) 42 | run.go: 43 | go run ./cmd/$(APP_NAME)/main.go -c $(CONFIG_FILE) 44 | swag: 45 | swag fmt -d ./internal && swag init -d ./cmd/$(APP_NAME),./internal/$(APP_NAME),./internal/controller/http -pd fiber 46 | 47 | migrate.create: 48 | migrate create -dir $(MIGRATIONS_FOLDER) -ext .sql -seq $(NAME) -v 49 | migrate.up: 50 | migrate -path $(MIGRATIONS_FOLDER) -database "$(POSTGRES_URL)&x-migrations-table=$(POSTGRES_SEARCH_PATH)_migrations" -verbose up 51 | migrate.down: 52 | migrate -path $(MIGRATIONS_FOLDER) -database "$(POSTGRES_URL)&x-migrations-table=$(POSTGRES_SEARCH_PATH)_migrations" -verbose down 53 | migrate.force: 54 | migrate -path $(MIGRATIONS_FOLDER) -database "$(POSTGRES_URL)&x-migrations-table=$(POSTGRES_SEARCH_PATH)_migrations" force $(VERSION) -v 55 | 56 | gen.clean: 57 | rm -rf gen/* 58 | gen.sqlc: 59 | sqlc generate 60 | gen: gen.clean gen.sqlc 61 | 62 | docker.network: 63 | docker network inspect $(DOCKER_NETWORK) >/dev/null 2>&1 || \ 64 | docker network create -d bridge $(DOCKER_NETWORK) 65 | docker.run.postgres: 66 | docker run --rm -d \ 67 | --name $(APP_NAME)-postgres \ 68 | --network $(DOCKER_NETWORK) \ 69 | -e POSTGRES_USER=$(POSTGRES_USER) \ 70 | -e POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) \ 71 | -e POSTGRES_DB=$(POSTGRES_DB) \ 72 | -p $(POSTGRES_PORT):5432 \ 73 | postgres 74 | docker.stop.postgres: 75 | docker stop $(APP_NAME)-postgres 76 | docker.run.redis: 77 | docker run --rm -d \ 78 | --name $(APP_NAME)-redis \ 79 | --network $(DOCKER_NETWORK) \ 80 | -p 6379:6379 \ 81 | redis:7-alpine 82 | docker.stop.redis: 83 | docker stop $(APP_NAME)-redis 84 | docker.remote.push: 85 | docker buildx build --rm \ 86 | -t $(PRIVATE_REGISTRY_PATH)/$(APP_NAME):$(DOCKER_TAG) \ 87 | --build-arg APP_NAME=$(APP_NAME) \ 88 | --build-arg GOPRIVATE=$(GOPRIVATE) \ 89 | --build-arg GOPRIVATE_USER=$(GOPRIVATE_USER) \ 90 | --build-arg GOPRIVATE_PAT=$(GOPRIVATE_PAT) \ 91 | --build-arg GOPRIVATE_SCHEMA=$(GOPRIVATE_SCHEMA) \ 92 | -f $(DOCKER_PATH) . --push 93 | docker.build.app: 94 | docker build --rm \ 95 | -t $(APP_NAME):$(DOCKER_TAG) \ 96 | --build-arg APP_NAME=$(APP_NAME) \ 97 | --build-arg GOPRIVATE=$(GOPRIVATE) \ 98 | --build-arg GOPRIVATE_USER=$(GOPRIVATE_USER) \ 99 | --build-arg GOPRIVATE_PAT=$(GOPRIVATE_PAT) \ 100 | --build-arg GOPRIVATE_SCHEMA=$(GOPRIVATE_SCHEMA) \ 101 | -f $(DOCKER_PATH) . 102 | docker.run.app: docker.build.app 103 | docker run --rm \ 104 | --name $(APP_NAME) \ 105 | --network $(DOCKER_NETWORK) \ 106 | -e REDIS_HOST=$(APP_NAME)-redis \ 107 | -p 8000:8000 \ 108 | $(APP_NAME):$(DOCKER_TAG) 109 | docker.stop.app: 110 | docker stop $(APP_NAME) 111 | docker.stop: docker.stop.postgres docker.stop.redis 112 | docker.run: docker.network docker.run.postgres docker.run.redis 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App [![Mentioned in Awesome Fiber](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/gofiber/awesome-fiber) 2 | 3 | Golang application template. Based on clean architecture principles 4 | - `internal/domain` - business models and rules 5 | - `internal/app` - "App" applicaiton setup 6 | - `internal/usecase` - business logic implementations through usecase and repository interfaces 7 | - `internal/controller` - controllers implementations that orchestrate various usecases 8 | - `migrations` - application migrations 9 | - `config` - application configuration structs and helpers 10 | - `pkg` - additional, non-dependant infrastructure and logic that can be also used externaly, outside of current project scope 11 | - `cmd` - entrypoints to the applications 12 | - `embed.go` - files and directories that have to be embeded into the applications (migrations for ex.) 13 | - `gen` - autogenerated stubs 14 | ## List of contents 15 | - [App ](#app-) 16 | - [List of contents](#list-of-contents) 17 | - [Project requirements](#project-requirements) 18 | - [Install Go helpers](#install-go-helpers) 19 | - [gocritic](#gocritic) 20 | - [golangci-lint](#golangci-lint) 21 | - [gosec](#gosec) 22 | - [swag](#swag) 23 | - [migrate](#migrate) 24 | - [Makefile](#makefile) 25 | - [Postgres migrations](#postgres-migrations) 26 | - [Application](#application) 27 | - [Docker](#docker) 28 | - [Application](#application-1) 29 | - [Postgres](#postgres) 30 | - [Redis](#redis) 31 | - [Configuration](#configuration) 32 | - [env](#env) 33 | - [yaml](#yaml) 34 | - [json](#json) 35 | ## Project requirements 36 | - Go 1.19 37 | - Docker 38 | ## Install Go helpers 39 | ### gocritic 40 | Highly extensible Go source code linter providing checks currently missing from other linters. 41 | ```bash 42 | go install github.com/go-critic/go-critic/cmd/gocritic@latest 43 | ``` 44 | ### golangci-lint 45 | Fast Go linters runner. It runs linters in parallel, uses caching, supports yaml config, has integrations with all major IDE and has dozens of linters included. 46 | ```bash 47 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 48 | ``` 49 | ### gosec 50 | Inspects source code for security problems by scanning the Go AST. 51 | ```bash 52 | go install github.com/securego/gosec/v2/cmd/gosec@latest 53 | ``` 54 | ### swag 55 | Swag converts Go annotations to Swagger Documentation 2.0. 56 | ```bash 57 | go install github.com/swaggo/swag/cmd/swag@latest 58 | ``` 59 | ### migrate 60 | Database migrations written in Go. Use as CLI or import as library. 61 | ```bash 62 | go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest 63 | ``` 64 | ## Makefile 65 | ### Postgres migrations 66 | Default path for migrations folder is `migrations/$(APP_NAME)` 67 | 68 | To create new migration 69 | ```bash 70 | make migrate.create MIGRATIONS_FOLDER=migrations/my_app NAME=create_my_table 71 | ``` 72 | To apply migrations 73 | ```bash 74 | make migrate.up 75 | ``` 76 | To reverse migrations 77 | ```bash 78 | make migrate.down 79 | ``` 80 | To force migrations up to a certain version 81 | ```bash 82 | make migrate.force VERSION=000003 83 | ``` 84 | ### Application 85 | To test, build and run application 86 | ```bash 87 | make run 88 | ``` 89 | ### Docker 90 | To start project within docker network 91 | ```bash 92 | make docker.run 93 | ``` 94 | To stop 95 | ```bash 96 | make docker.stop 97 | ``` 98 | #### Application 99 | To build application docker image 100 | ```bash 101 | make docker.buld.app 102 | ``` 103 | You can also override build args to access GOPRIVATE repositories inside container 104 | ```bash 105 | make docker.app.build GOPRIVATE="" GOPRIVATE_USER="" GOPRIVATE_PAT="" GOPRIVATE_SCHEMA="" 106 | ``` 107 | To start application 108 | ```bash 109 | make docker.run.app 110 | ``` 111 | To stop application 112 | ```bash 113 | make docker.stop.app 114 | ``` 115 | #### Postgres 116 | To start postgres instance 117 | ```bash 118 | make docker.run.postgres 119 | ``` 120 | To stop 121 | ```bash 122 | make docker.stop.postgres 123 | ``` 124 | #### Redis 125 | To start redis instance 126 | ```bash 127 | make docker.run.redis 128 | ``` 129 | To stop 130 | ```bash 131 | make docker.stop.redis 132 | ``` 133 | ## Configuration 134 | ### env 135 | ```bash 136 | LOGGER_LEVEL="info" # standard logger level options (panic, fatal, warn/warning, info, debug, trace) 137 | 138 | HTTP_HOST="0.0.0.0" 139 | HTTP_PORT="8000" 140 | HTTP_TIMEOUT="4s" 141 | HTTP_PREFIX="" 142 | HTTP_API_PATH="/api" 143 | 144 | TLS_CERT_FILEPATH="" 145 | TLS_KEY_FILEPATH="" 146 | 147 | POSTGRES_HOST="127.0.0.1" 148 | POSTGRES_PORT="5432" 149 | POSTGRES_SSL_MODE="disable" 150 | POSTGRES_DB="postgres" 151 | POSTGRES_USER="postgres" 152 | POSTGRES_PASSWORD="postgres" 153 | POSTGRES_MAX_CONNS="10" 154 | POSTGRES_MIN_CONNS="2" 155 | POSTGRES_MAX_CONN_LIFETIME="10m" 156 | POSTGRES_MAX_CONN_IDLE_TIME="1m" 157 | POSTGRES_HEALTH_CHECK_PERIOD="10s" 158 | 159 | REDIS_HOST="127.0.0.1" 160 | REDIS_PORT="6379" 161 | REDIS_USERNAME="" 162 | REDIS_PASSWORD="" 163 | REDIS_DB="0" 164 | 165 | SWAGGER_HOST="127.0.0.1:8888" 166 | SWAGGER_BASE_PATH="/api" 167 | ``` 168 | ### yaml 169 | ```yaml 170 | logger: 171 | level: info 172 | http: 173 | host: 0.0.0.0 174 | port: 8000 175 | timeout: 4s 176 | prefix: "" 177 | apiPath: /api 178 | tls: 179 | cert: 180 | filepath: "" 181 | key: 182 | filepath: "" 183 | postgres: 184 | host: 127.0.0.1 185 | port: 5432 186 | sslMode: disable 187 | db: postgres 188 | user: postgres 189 | maxConns: 10 190 | minConns: 2 191 | maxConnLifetime: 10m 192 | maxConnIdleTime: 1m 193 | healthCheckPeriod: 10s 194 | redis: 195 | host: 127.0.0.1 196 | port: 6379 197 | username: "" 198 | password: "" 199 | db: 0 200 | swagger: 201 | host: 127.0.0.1:8888 202 | basePath: /api 203 | ``` 204 | ### json 205 | ```json 206 | { 207 | "logger": { 208 | "level": "info", 209 | "file": { 210 | "path": "", 211 | "name": "", 212 | "max_age": "24h", 213 | "rotation_time": "168h" 214 | }, 215 | "format": { 216 | "type": "text", 217 | "caller": false, 218 | "pretty": false 219 | } 220 | }, 221 | "http": { 222 | "host": "0.0.0.0", 223 | "port": "8000", 224 | "timeout": "4s", 225 | "prefix": "", 226 | "api_path": "/api" 227 | }, 228 | "tls": { 229 | "cert": { 230 | "filepath": "", 231 | }, 232 | "key": { 233 | "filepath": "", 234 | } 235 | }, 236 | "postgres": { 237 | "host": "127.0.0.1", 238 | "port": "5432", 239 | "ssl_mode":"disable", 240 | "db": "postgres", 241 | "user": "postgres", 242 | "max_conns": 10, 243 | "min_conns": 2, 244 | "max_conn_lifetime": "10m", 245 | "max_conn_idle_time": "1m", 246 | "health_check_period": "10s" 247 | }, 248 | "redis": { 249 | "host": "127.0.0.1", 250 | "port": "6379", 251 | "username": "", 252 | "password": "", 253 | "db": 0 254 | }, 255 | "swagger": { 256 | "host": "127.0.0.1:8888", 257 | "base_path": "/api" 258 | } 259 | } 260 | ``` -------------------------------------------------------------------------------- /cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "goapptemplate/config" 5 | "goapptemplate/internal/app" 6 | 7 | "log" 8 | 9 | "github.com/spf13/pflag" 10 | ) 11 | 12 | // @title Books API 13 | // @version 0.1.0 14 | // @description Go app template books API. 15 | 16 | // @contact.name John Doe 17 | // @contact.email johndoe@cia.gov 18 | 19 | // @schemes http https 20 | func main() { 21 | // ________________________________________________________________________ 22 | // Parse cli args to config 23 | filepath := pflag.StringP("config", "c", "", "configuration filepath (default: None)") 24 | pflag.Parse() 25 | // ________________________________________________________________________ 26 | // Load config 27 | cfg, err := config.NewAppCfg(*filepath) 28 | if err != nil { 29 | log.Fatalf("cannot load config: %s", err) 30 | } 31 | // ________________________________________________________________________ 32 | // Run app 33 | app.Run(cfg) 34 | } 35 | -------------------------------------------------------------------------------- /config/app_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/ilyakaznacheev/cleanenv" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type AppCfg struct { 9 | Logger Logger `json:"logger" yaml:"logger" env-prefix:"LOGGER_"` 10 | HTTP HTTP `json:"http" yaml:"http" env-prefix:"HTTP_"` 11 | TLS TLS `json:"tls" yaml:"tls" env-prefix:"TLS_"` 12 | Postgres Postgres `json:"postgres" yaml:"postgres" env-prefix:"POSTGRES_"` 13 | Redis Redis `json:"redis" yaml:"redis" env-prefix:"REDIS_"` 14 | Swagger Swagger `json:"swagger" yaml:"swagger" env-prefix:"SWAGGER_"` 15 | } 16 | 17 | func NewAppCfg(filepath string) (*AppCfg, error) { 18 | var err error 19 | var c AppCfg 20 | if filepath == "" { 21 | err = cleanenv.ReadEnv(&c) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "cannot read env") 24 | } 25 | } else { 26 | err = cleanenv.ReadConfig(filepath, &c) 27 | if err != nil { 28 | return nil, errors.Wrap(err, "cannot read config") 29 | } 30 | } 31 | return &c, err 32 | } 33 | -------------------------------------------------------------------------------- /config/common.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Logger struct { 9 | Level string `json:"level" yaml:"level" env:"LEVEL" env-default:"info"` 10 | } 11 | 12 | type HTTP struct { 13 | Host string `json:"host" yaml:"host" env:"HOST" env-default:"0.0.0.0"` 14 | Port int32 `json:"port" yaml:"port" env:"PORT" env-default:"8000"` 15 | Timeout time.Duration `json:"timeout" yaml:"timeout" env:"TIMEOUT" env-default:"4s"` 16 | Prefix string `json:"prefix" yaml:"prefix" env:"PREFIX" env-default:""` 17 | APIPath string `json:"api_path" yaml:"apiPath" env:"API_PATH" env-default:"/api"` 18 | } 19 | 20 | func (http HTTP) Addr() string { 21 | return fmt.Sprintf("%s:%v", http.Host, http.Port) 22 | } 23 | 24 | func (http HTTP) FullAPIPath() string { 25 | return fmt.Sprintf("%s%s", http.Prefix, http.APIPath) 26 | } 27 | 28 | type TLS struct { 29 | Cert struct { 30 | Filepath string `json:"filepath" yaml:"filepath" env:"FILEPATH" env-default:""` 31 | } `json:"cert" yaml:"cert" env-prefix:"CERT_"` 32 | Key struct { 33 | Filepath string `json:"filepath" yaml:"filepath" env:"FILEPATH" env-default:""` 34 | } `json:"key" yaml:"key" env-prefix:"KEY_"` 35 | } 36 | 37 | type Postgres struct { 38 | Host string `json:"host" yaml:"host" env:"HOST" env-default:"127.0.0.1"` 39 | Port int32 `json:"port" yaml:"port" env:"PORT" env-default:"5432"` 40 | SslMode string `json:"ssl_mode" yaml:"sslMode" env:"SSL_MODE" env-default:"disable"` 41 | Db string `json:"db" yaml:"db" env:"DB" env-default:"postgres"` 42 | User string `json:"user" yaml:"user" env:"USER" env-default:"postgres"` 43 | Password string `json:"password" yaml:"password" env:"PASSWORD" env-default:"postgres"` 44 | MaxConns int `json:"max_conns" yaml:"maxConns" env:"MAX_CONNS" env-default:"10"` 45 | MinConns int `json:"min_conns" yaml:"minConns" env:"MIN_CONNS" env-default:"2"` 46 | MaxConnLifetime time.Duration `json:"max_conn_lifetime" yaml:"maxConnLifetime" env:"MAX_CONN_LIFETIME" env-default:"10m"` 47 | MaxConnIdleTime time.Duration `json:"max_conn_idle_time" yaml:"maxConnIdleTime" env:"MAX_CONN_IDLE_TIME" env-default:"1m"` 48 | HealthCheckPeriod time.Duration `json:"health_check_period" yaml:"healthCheckPeriod" env:"HEALTH_CHECK_PERIOD" env-default:"10s"` 49 | } 50 | 51 | func (p Postgres) ConfigString(opts ...string) string { 52 | conf := fmt.Sprintf( 53 | "host=%s port=%v sslmode=%s user=%s password=%s dbname=%s pool_max_conns=%v pool_min_conns=%v pool_max_conn_lifetime=%s pool_max_conn_idle_time=%s pool_health_check_period=%s", 54 | p.Host, 55 | p.Port, 56 | p.SslMode, 57 | p.User, 58 | p.Password, 59 | p.Db, 60 | p.MaxConns, 61 | p.MinConns, 62 | p.MaxConnLifetime, 63 | p.MaxConnIdleTime, 64 | p.HealthCheckPeriod, 65 | ) 66 | for _, v := range opts { 67 | conf += fmt.Sprintf(" %s", v) 68 | } 69 | return conf 70 | } 71 | 72 | func (p Postgres) ConfigURL(args ...string) string { 73 | url := fmt.Sprintf( 74 | "postgres://%s:%s@%s:%v/%s?sslmode=%s", 75 | p.User, 76 | p.Password, 77 | p.Host, 78 | p.Port, 79 | p.Db, 80 | p.SslMode, 81 | ) 82 | for _, v := range args { 83 | url += fmt.Sprintf("&%s", v) 84 | } 85 | return url 86 | } 87 | 88 | type Redis struct { 89 | Host string `json:"host" yaml:"host" env:"HOST" env-default:"127.0.0.1"` 90 | Port int32 `json:"port" yaml:"port" env:"PORT" env-default:"6379"` 91 | Username string `json:"username" yaml:"username" env:"USERNAME" env-default:""` 92 | Password string `json:"password" yaml:"password" env:"PASSWORD" env-default:""` 93 | DB int `json:"db" yaml:"db" env:"DB" env-default:"0"` 94 | } 95 | 96 | func (redis Redis) Addr() string { 97 | return fmt.Sprintf("%s:%v", redis.Host, redis.Port) 98 | } 99 | 100 | type Swagger struct { 101 | Host string `json:"host" yaml:"host" env:"HOST" env-default:"127.0.0.1:8888"` 102 | BasePath string `json:"base_path" yaml:"basePath" env:"BASE_PATH" env-default:"/api"` 103 | } 104 | -------------------------------------------------------------------------------- /coverage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go Coverage Report 7 | 52 | 53 | 54 |
55 | 60 |
61 | not tracked 62 | 63 | no coverage 64 | low coverage 65 | * 66 | * 67 | * 68 | * 69 | * 70 | * 71 | * 72 | * 73 | high coverage 74 | 75 |
76 |
77 |
78 | 79 |
80 | 81 | 108 | 109 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG APP_NAME="app" 2 | 3 | # Step 1: Modules caching 4 | FROM golang:1.21.1-alpine3.18 as modules 5 | COPY go.mod go.sum /modules/ 6 | WORKDIR /modules 7 | RUN go mod download 8 | 9 | # Step 2: Builder 10 | FROM golang:1.21.1-alpine3.18 as builder 11 | 12 | ARG APP_NAME 13 | 14 | COPY --from=modules /go/pkg /go/pkg 15 | COPY . /${APP_NAME} 16 | WORKDIR /${APP_NAME} 17 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 18 | go build -tags migrate -o /bin/${APP_NAME} ./cmd/${APP_NAME} 19 | 20 | # Step 3: Final 21 | FROM scratch 22 | 23 | ARG APP_NAME 24 | 25 | COPY --from=builder /bin/${APP_NAME} /app 26 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 27 | 28 | CMD ["/app"] -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Code generated by swaggo/swag. DO NOT EDIT. 2 | 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": { 14 | "name": "John Doe", 15 | "email": "johndoe@cia.gov" 16 | }, 17 | "version": "{{.Version}}" 18 | }, 19 | "host": "{{.Host}}", 20 | "basePath": "{{.BasePath}}", 21 | "paths": { 22 | "/books": { 23 | "get": { 24 | "description": "Get books", 25 | "produces": [ 26 | "application/json" 27 | ], 28 | "tags": [ 29 | "books" 30 | ], 31 | "summary": "Get books", 32 | "parameters": [ 33 | { 34 | "type": "integer", 35 | "description": "page size limit", 36 | "name": "limit", 37 | "in": "query" 38 | }, 39 | { 40 | "type": "integer", 41 | "description": "page offset", 42 | "name": "offset", 43 | "in": "query" 44 | }, 45 | { 46 | "type": "string", 47 | "description": "name search pattern", 48 | "name": "name", 49 | "in": "query" 50 | }, 51 | { 52 | "type": "string", 53 | "description": "description search pattern", 54 | "name": "description", 55 | "in": "query" 56 | } 57 | ], 58 | "responses": { 59 | "200": { 60 | "description": "OK", 61 | "schema": { 62 | "$ref": "#/definitions/domain.BookPage" 63 | } 64 | }, 65 | "400": { 66 | "description": "Bad Request", 67 | "schema": { 68 | "$ref": "#/definitions/fiber.Error" 69 | } 70 | }, 71 | "500": { 72 | "description": "Internal Server Error", 73 | "schema": { 74 | "$ref": "#/definitions/fiber.Error" 75 | } 76 | } 77 | } 78 | }, 79 | "post": { 80 | "description": "Create book", 81 | "consumes": [ 82 | "application/json" 83 | ], 84 | "tags": [ 85 | "books" 86 | ], 87 | "summary": "Create book", 88 | "parameters": [ 89 | { 90 | "description": "book attributes", 91 | "name": "data", 92 | "in": "body", 93 | "required": true, 94 | "schema": { 95 | "$ref": "#/definitions/domain.Book" 96 | } 97 | } 98 | ], 99 | "responses": { 100 | "201": { 101 | "description": "Created", 102 | "headers": { 103 | "Location": { 104 | "type": "string", 105 | "description": "/books/:id" 106 | } 107 | } 108 | }, 109 | "400": { 110 | "description": "Bad Request", 111 | "schema": { 112 | "$ref": "#/definitions/fiber.Error" 113 | } 114 | }, 115 | "500": { 116 | "description": "Internal Server Error", 117 | "schema": { 118 | "$ref": "#/definitions/fiber.Error" 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "/books/{id}": { 125 | "get": { 126 | "description": "Get book", 127 | "produces": [ 128 | "application/json" 129 | ], 130 | "tags": [ 131 | "books" 132 | ], 133 | "summary": "Get book", 134 | "parameters": [ 135 | { 136 | "type": "string", 137 | "description": "book uuid", 138 | "name": "id", 139 | "in": "path", 140 | "required": true 141 | } 142 | ], 143 | "responses": { 144 | "200": { 145 | "description": "OK", 146 | "schema": { 147 | "$ref": "#/definitions/domain.Book" 148 | } 149 | }, 150 | "400": { 151 | "description": "Bad Request", 152 | "schema": { 153 | "$ref": "#/definitions/fiber.Error" 154 | } 155 | }, 156 | "404": { 157 | "description": "Not Found", 158 | "schema": { 159 | "$ref": "#/definitions/fiber.Error" 160 | } 161 | }, 162 | "500": { 163 | "description": "Internal Server Error", 164 | "schema": { 165 | "$ref": "#/definitions/fiber.Error" 166 | } 167 | } 168 | } 169 | }, 170 | "put": { 171 | "description": "Update book", 172 | "consumes": [ 173 | "application/json" 174 | ], 175 | "tags": [ 176 | "books" 177 | ], 178 | "summary": "Update book", 179 | "parameters": [ 180 | { 181 | "type": "string", 182 | "description": "book uuid", 183 | "name": "id", 184 | "in": "path", 185 | "required": true 186 | }, 187 | { 188 | "description": "book attributes", 189 | "name": "data", 190 | "in": "body", 191 | "required": true, 192 | "schema": { 193 | "$ref": "#/definitions/domain.Book" 194 | } 195 | } 196 | ], 197 | "responses": { 198 | "204": { 199 | "description": "No Content" 200 | }, 201 | "400": { 202 | "description": "Bad Request", 203 | "schema": { 204 | "$ref": "#/definitions/fiber.Error" 205 | } 206 | }, 207 | "500": { 208 | "description": "Internal Server Error", 209 | "schema": { 210 | "$ref": "#/definitions/fiber.Error" 211 | } 212 | } 213 | } 214 | }, 215 | "delete": { 216 | "description": "Delete book", 217 | "produces": [ 218 | "application/json" 219 | ], 220 | "tags": [ 221 | "books" 222 | ], 223 | "summary": "Delete book", 224 | "parameters": [ 225 | { 226 | "type": "string", 227 | "description": "book uuid", 228 | "name": "id", 229 | "in": "path", 230 | "required": true 231 | } 232 | ], 233 | "responses": { 234 | "204": { 235 | "description": "No Content" 236 | }, 237 | "400": { 238 | "description": "Bad Request", 239 | "schema": { 240 | "$ref": "#/definitions/fiber.Error" 241 | } 242 | }, 243 | "500": { 244 | "description": "Internal Server Error", 245 | "schema": { 246 | "$ref": "#/definitions/fiber.Error" 247 | } 248 | } 249 | } 250 | } 251 | } 252 | }, 253 | "definitions": { 254 | "domain.Book": { 255 | "type": "object", 256 | "properties": { 257 | "created_at": { 258 | "type": "string" 259 | }, 260 | "description": { 261 | "type": "string" 262 | }, 263 | "id": { 264 | "type": "string" 265 | }, 266 | "name": { 267 | "type": "string" 268 | }, 269 | "updated_at": { 270 | "type": "string" 271 | } 272 | } 273 | }, 274 | "domain.BookPage": { 275 | "type": "object", 276 | "properties": { 277 | "data": { 278 | "type": "array", 279 | "items": { 280 | "$ref": "#/definitions/domain.Book" 281 | } 282 | }, 283 | "limit": { 284 | "type": "integer" 285 | }, 286 | "metadata": { 287 | "type": "object", 288 | "additionalProperties": true 289 | }, 290 | "offset": { 291 | "type": "integer" 292 | }, 293 | "total": { 294 | "type": "integer" 295 | } 296 | } 297 | }, 298 | "fiber.Error": { 299 | "type": "object", 300 | "properties": { 301 | "code": { 302 | "type": "integer" 303 | }, 304 | "message": { 305 | "type": "string" 306 | } 307 | } 308 | } 309 | } 310 | }` 311 | 312 | // SwaggerInfo holds exported Swagger Info so clients can modify it 313 | var SwaggerInfo = &swag.Spec{ 314 | Version: "0.1.0", 315 | Host: "", 316 | BasePath: "", 317 | Schemes: []string{"http", "https"}, 318 | Title: "Books API", 319 | Description: "Go app template books API.", 320 | InfoInstanceName: "swagger", 321 | SwaggerTemplate: docTemplate, 322 | LeftDelim: "{{", 323 | RightDelim: "}}", 324 | } 325 | 326 | func init() { 327 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 328 | } 329 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemes": [ 3 | "http", 4 | "https" 5 | ], 6 | "swagger": "2.0", 7 | "info": { 8 | "description": "Go app template books API.", 9 | "title": "Books API", 10 | "contact": { 11 | "name": "John Doe", 12 | "email": "johndoe@cia.gov" 13 | }, 14 | "version": "0.1.0" 15 | }, 16 | "paths": { 17 | "/books": { 18 | "get": { 19 | "description": "Get books", 20 | "produces": [ 21 | "application/json" 22 | ], 23 | "tags": [ 24 | "books" 25 | ], 26 | "summary": "Get books", 27 | "parameters": [ 28 | { 29 | "type": "integer", 30 | "description": "page size limit", 31 | "name": "limit", 32 | "in": "query" 33 | }, 34 | { 35 | "type": "integer", 36 | "description": "page offset", 37 | "name": "offset", 38 | "in": "query" 39 | }, 40 | { 41 | "type": "string", 42 | "description": "name search pattern", 43 | "name": "name", 44 | "in": "query" 45 | }, 46 | { 47 | "type": "string", 48 | "description": "description search pattern", 49 | "name": "description", 50 | "in": "query" 51 | } 52 | ], 53 | "responses": { 54 | "200": { 55 | "description": "OK", 56 | "schema": { 57 | "$ref": "#/definitions/domain.BookPage" 58 | } 59 | }, 60 | "400": { 61 | "description": "Bad Request", 62 | "schema": { 63 | "$ref": "#/definitions/fiber.Error" 64 | } 65 | }, 66 | "500": { 67 | "description": "Internal Server Error", 68 | "schema": { 69 | "$ref": "#/definitions/fiber.Error" 70 | } 71 | } 72 | } 73 | }, 74 | "post": { 75 | "description": "Create book", 76 | "consumes": [ 77 | "application/json" 78 | ], 79 | "tags": [ 80 | "books" 81 | ], 82 | "summary": "Create book", 83 | "parameters": [ 84 | { 85 | "description": "book attributes", 86 | "name": "data", 87 | "in": "body", 88 | "required": true, 89 | "schema": { 90 | "$ref": "#/definitions/domain.Book" 91 | } 92 | } 93 | ], 94 | "responses": { 95 | "201": { 96 | "description": "Created", 97 | "headers": { 98 | "Location": { 99 | "type": "string", 100 | "description": "/books/:id" 101 | } 102 | } 103 | }, 104 | "400": { 105 | "description": "Bad Request", 106 | "schema": { 107 | "$ref": "#/definitions/fiber.Error" 108 | } 109 | }, 110 | "500": { 111 | "description": "Internal Server Error", 112 | "schema": { 113 | "$ref": "#/definitions/fiber.Error" 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "/books/{id}": { 120 | "get": { 121 | "description": "Get book", 122 | "produces": [ 123 | "application/json" 124 | ], 125 | "tags": [ 126 | "books" 127 | ], 128 | "summary": "Get book", 129 | "parameters": [ 130 | { 131 | "type": "string", 132 | "description": "book uuid", 133 | "name": "id", 134 | "in": "path", 135 | "required": true 136 | } 137 | ], 138 | "responses": { 139 | "200": { 140 | "description": "OK", 141 | "schema": { 142 | "$ref": "#/definitions/domain.Book" 143 | } 144 | }, 145 | "400": { 146 | "description": "Bad Request", 147 | "schema": { 148 | "$ref": "#/definitions/fiber.Error" 149 | } 150 | }, 151 | "404": { 152 | "description": "Not Found", 153 | "schema": { 154 | "$ref": "#/definitions/fiber.Error" 155 | } 156 | }, 157 | "500": { 158 | "description": "Internal Server Error", 159 | "schema": { 160 | "$ref": "#/definitions/fiber.Error" 161 | } 162 | } 163 | } 164 | }, 165 | "put": { 166 | "description": "Update book", 167 | "consumes": [ 168 | "application/json" 169 | ], 170 | "tags": [ 171 | "books" 172 | ], 173 | "summary": "Update book", 174 | "parameters": [ 175 | { 176 | "type": "string", 177 | "description": "book uuid", 178 | "name": "id", 179 | "in": "path", 180 | "required": true 181 | }, 182 | { 183 | "description": "book attributes", 184 | "name": "data", 185 | "in": "body", 186 | "required": true, 187 | "schema": { 188 | "$ref": "#/definitions/domain.Book" 189 | } 190 | } 191 | ], 192 | "responses": { 193 | "204": { 194 | "description": "No Content" 195 | }, 196 | "400": { 197 | "description": "Bad Request", 198 | "schema": { 199 | "$ref": "#/definitions/fiber.Error" 200 | } 201 | }, 202 | "500": { 203 | "description": "Internal Server Error", 204 | "schema": { 205 | "$ref": "#/definitions/fiber.Error" 206 | } 207 | } 208 | } 209 | }, 210 | "delete": { 211 | "description": "Delete book", 212 | "produces": [ 213 | "application/json" 214 | ], 215 | "tags": [ 216 | "books" 217 | ], 218 | "summary": "Delete book", 219 | "parameters": [ 220 | { 221 | "type": "string", 222 | "description": "book uuid", 223 | "name": "id", 224 | "in": "path", 225 | "required": true 226 | } 227 | ], 228 | "responses": { 229 | "204": { 230 | "description": "No Content" 231 | }, 232 | "400": { 233 | "description": "Bad Request", 234 | "schema": { 235 | "$ref": "#/definitions/fiber.Error" 236 | } 237 | }, 238 | "500": { 239 | "description": "Internal Server Error", 240 | "schema": { 241 | "$ref": "#/definitions/fiber.Error" 242 | } 243 | } 244 | } 245 | } 246 | } 247 | }, 248 | "definitions": { 249 | "domain.Book": { 250 | "type": "object", 251 | "properties": { 252 | "created_at": { 253 | "type": "string" 254 | }, 255 | "description": { 256 | "type": "string" 257 | }, 258 | "id": { 259 | "type": "string" 260 | }, 261 | "name": { 262 | "type": "string" 263 | }, 264 | "updated_at": { 265 | "type": "string" 266 | } 267 | } 268 | }, 269 | "domain.BookPage": { 270 | "type": "object", 271 | "properties": { 272 | "data": { 273 | "type": "array", 274 | "items": { 275 | "$ref": "#/definitions/domain.Book" 276 | } 277 | }, 278 | "limit": { 279 | "type": "integer" 280 | }, 281 | "metadata": { 282 | "type": "object", 283 | "additionalProperties": true 284 | }, 285 | "offset": { 286 | "type": "integer" 287 | }, 288 | "total": { 289 | "type": "integer" 290 | } 291 | } 292 | }, 293 | "fiber.Error": { 294 | "type": "object", 295 | "properties": { 296 | "code": { 297 | "type": "integer" 298 | }, 299 | "message": { 300 | "type": "string" 301 | } 302 | } 303 | } 304 | } 305 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | domain.Book: 3 | properties: 4 | created_at: 5 | type: string 6 | description: 7 | type: string 8 | id: 9 | type: string 10 | name: 11 | type: string 12 | updated_at: 13 | type: string 14 | type: object 15 | domain.BookPage: 16 | properties: 17 | data: 18 | items: 19 | $ref: '#/definitions/domain.Book' 20 | type: array 21 | limit: 22 | type: integer 23 | metadata: 24 | additionalProperties: true 25 | type: object 26 | offset: 27 | type: integer 28 | total: 29 | type: integer 30 | type: object 31 | fiber.Error: 32 | properties: 33 | code: 34 | type: integer 35 | message: 36 | type: string 37 | type: object 38 | info: 39 | contact: 40 | email: johndoe@cia.gov 41 | name: John Doe 42 | description: Go app template books API. 43 | title: Books API 44 | version: 0.1.0 45 | paths: 46 | /books: 47 | get: 48 | description: Get books 49 | parameters: 50 | - description: page size limit 51 | in: query 52 | name: limit 53 | type: integer 54 | - description: page offset 55 | in: query 56 | name: offset 57 | type: integer 58 | - description: name search pattern 59 | in: query 60 | name: name 61 | type: string 62 | - description: description search pattern 63 | in: query 64 | name: description 65 | type: string 66 | produces: 67 | - application/json 68 | responses: 69 | "200": 70 | description: OK 71 | schema: 72 | $ref: '#/definitions/domain.BookPage' 73 | "400": 74 | description: Bad Request 75 | schema: 76 | $ref: '#/definitions/fiber.Error' 77 | "500": 78 | description: Internal Server Error 79 | schema: 80 | $ref: '#/definitions/fiber.Error' 81 | summary: Get books 82 | tags: 83 | - books 84 | post: 85 | consumes: 86 | - application/json 87 | description: Create book 88 | parameters: 89 | - description: book attributes 90 | in: body 91 | name: data 92 | required: true 93 | schema: 94 | $ref: '#/definitions/domain.Book' 95 | responses: 96 | "201": 97 | description: Created 98 | headers: 99 | Location: 100 | description: /books/:id 101 | type: string 102 | "400": 103 | description: Bad Request 104 | schema: 105 | $ref: '#/definitions/fiber.Error' 106 | "500": 107 | description: Internal Server Error 108 | schema: 109 | $ref: '#/definitions/fiber.Error' 110 | summary: Create book 111 | tags: 112 | - books 113 | /books/{id}: 114 | delete: 115 | description: Delete book 116 | parameters: 117 | - description: book uuid 118 | in: path 119 | name: id 120 | required: true 121 | type: string 122 | produces: 123 | - application/json 124 | responses: 125 | "204": 126 | description: No Content 127 | "400": 128 | description: Bad Request 129 | schema: 130 | $ref: '#/definitions/fiber.Error' 131 | "500": 132 | description: Internal Server Error 133 | schema: 134 | $ref: '#/definitions/fiber.Error' 135 | summary: Delete book 136 | tags: 137 | - books 138 | get: 139 | description: Get book 140 | parameters: 141 | - description: book uuid 142 | in: path 143 | name: id 144 | required: true 145 | type: string 146 | produces: 147 | - application/json 148 | responses: 149 | "200": 150 | description: OK 151 | schema: 152 | $ref: '#/definitions/domain.Book' 153 | "400": 154 | description: Bad Request 155 | schema: 156 | $ref: '#/definitions/fiber.Error' 157 | "404": 158 | description: Not Found 159 | schema: 160 | $ref: '#/definitions/fiber.Error' 161 | "500": 162 | description: Internal Server Error 163 | schema: 164 | $ref: '#/definitions/fiber.Error' 165 | summary: Get book 166 | tags: 167 | - books 168 | put: 169 | consumes: 170 | - application/json 171 | description: Update book 172 | parameters: 173 | - description: book uuid 174 | in: path 175 | name: id 176 | required: true 177 | type: string 178 | - description: book attributes 179 | in: body 180 | name: data 181 | required: true 182 | schema: 183 | $ref: '#/definitions/domain.Book' 184 | responses: 185 | "204": 186 | description: No Content 187 | "400": 188 | description: Bad Request 189 | schema: 190 | $ref: '#/definitions/fiber.Error' 191 | "500": 192 | description: Internal Server Error 193 | schema: 194 | $ref: '#/definitions/fiber.Error' 195 | summary: Update book 196 | tags: 197 | - books 198 | schemes: 199 | - http 200 | - https 201 | swagger: "2.0" 202 | -------------------------------------------------------------------------------- /embed.go: -------------------------------------------------------------------------------- 1 | package goapptemplate 2 | 3 | import "embed" 4 | 5 | //go:embed migrations/app 6 | var MigrationsApp embed.FS 7 | -------------------------------------------------------------------------------- /gen/app/db/books_query.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | // source: books_query.sql 5 | 6 | package db 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/jackc/pgx/v5/pgtype" 12 | ) 13 | 14 | const deleteBookWhereID = `-- name: DeleteBookWhereID :exec 15 | DELETE FROM books 16 | WHERE id = $1 17 | ` 18 | 19 | func (q *Queries) DeleteBookWhereID(ctx context.Context, id pgtype.UUID) error { 20 | _, err := q.db.Exec(ctx, deleteBookWhereID, id) 21 | return err 22 | } 23 | 24 | const insertBook = `-- name: InsertBook :one 25 | INSERT INTO books(id, name, description) 26 | VALUES ($1, $2, $3) 27 | RETURNING id, name, description, created_at, updated_at 28 | ` 29 | 30 | type InsertBookParams struct { 31 | ID pgtype.UUID 32 | Name string 33 | Description pgtype.Text 34 | } 35 | 36 | func (q *Queries) InsertBook(ctx context.Context, arg InsertBookParams) (*Book, error) { 37 | row := q.db.QueryRow(ctx, insertBook, arg.ID, arg.Name, arg.Description) 38 | var i Book 39 | err := row.Scan( 40 | &i.ID, 41 | &i.Name, 42 | &i.Description, 43 | &i.CreatedAt, 44 | &i.UpdatedAt, 45 | ) 46 | return &i, err 47 | } 48 | 49 | const selectBookWhereID = `-- name: SelectBookWhereID :one 50 | SELECT id, name, description, created_at, updated_at 51 | FROM books 52 | WHERE id = $1 53 | ` 54 | 55 | func (q *Queries) SelectBookWhereID(ctx context.Context, id pgtype.UUID) (*Book, error) { 56 | row := q.db.QueryRow(ctx, selectBookWhereID, id) 57 | var i Book 58 | err := row.Scan( 59 | &i.ID, 60 | &i.Name, 61 | &i.Description, 62 | &i.CreatedAt, 63 | &i.UpdatedAt, 64 | ) 65 | return &i, err 66 | } 67 | 68 | const selectBooks = `-- name: SelectBooks :many 69 | SELECT id, name, description, created_at, updated_at 70 | FROM books 71 | WHERE name LIKE $1 72 | AND description LIKE $2 73 | ORDER BY created_at DESC 74 | LIMIT $4 OFFSET $3 75 | ` 76 | 77 | type SelectBooksParams struct { 78 | Name string 79 | Description pgtype.Text 80 | Ofst int32 81 | Lim int32 82 | } 83 | 84 | func (q *Queries) SelectBooks(ctx context.Context, arg SelectBooksParams) ([]*Book, error) { 85 | rows, err := q.db.Query(ctx, selectBooks, 86 | arg.Name, 87 | arg.Description, 88 | arg.Ofst, 89 | arg.Lim, 90 | ) 91 | if err != nil { 92 | return nil, err 93 | } 94 | defer rows.Close() 95 | var items []*Book 96 | for rows.Next() { 97 | var i Book 98 | if err := rows.Scan( 99 | &i.ID, 100 | &i.Name, 101 | &i.Description, 102 | &i.CreatedAt, 103 | &i.UpdatedAt, 104 | ); err != nil { 105 | return nil, err 106 | } 107 | items = append(items, &i) 108 | } 109 | if err := rows.Err(); err != nil { 110 | return nil, err 111 | } 112 | return items, nil 113 | } 114 | 115 | const selectBooksCount = `-- name: SelectBooksCount :one 116 | SELECT COUNT(*) 117 | FROM books 118 | WHERE name LIKE $1 119 | AND description LIKE $2 120 | ` 121 | 122 | type SelectBooksCountParams struct { 123 | Name string 124 | Description pgtype.Text 125 | } 126 | 127 | func (q *Queries) SelectBooksCount(ctx context.Context, arg SelectBooksCountParams) (int64, error) { 128 | row := q.db.QueryRow(ctx, selectBooksCount, arg.Name, arg.Description) 129 | var count int64 130 | err := row.Scan(&count) 131 | return count, err 132 | } 133 | 134 | const updateBookWhereID = `-- name: UpdateBookWhereID :exec 135 | UPDATE books 136 | SET name = $1, 137 | description = $2, 138 | updated_at = now() 139 | WHERE id = $3 140 | ` 141 | 142 | type UpdateBookWhereIDParams struct { 143 | Name string 144 | Description pgtype.Text 145 | ID pgtype.UUID 146 | } 147 | 148 | func (q *Queries) UpdateBookWhereID(ctx context.Context, arg UpdateBookWhereIDParams) error { 149 | _, err := q.db.Exec(ctx, updateBookWhereID, arg.Name, arg.Description, arg.ID) 150 | return err 151 | } 152 | -------------------------------------------------------------------------------- /gen/app/db/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/jackc/pgx/v5" 11 | "github.com/jackc/pgx/v5/pgconn" 12 | ) 13 | 14 | type DBTX interface { 15 | Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) 16 | Query(context.Context, string, ...interface{}) (pgx.Rows, error) 17 | QueryRow(context.Context, string, ...interface{}) pgx.Row 18 | } 19 | 20 | func New(db DBTX) *Queries { 21 | return &Queries{db: db} 22 | } 23 | 24 | type Queries struct { 25 | db DBTX 26 | } 27 | 28 | func (q *Queries) WithTx(tx pgx.Tx) *Queries { 29 | return &Queries{ 30 | db: tx, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gen/app/db/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | 5 | package db 6 | 7 | import ( 8 | "github.com/jackc/pgx/v5/pgtype" 9 | ) 10 | 11 | type Book struct { 12 | ID pgtype.UUID 13 | Name string 14 | Description pgtype.Text 15 | CreatedAt pgtype.Timestamptz 16 | UpdatedAt pgtype.Timestamptz 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module goapptemplate 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.3 7 | github.com/spf13/pflag v1.0.5 8 | ) 9 | 10 | require ( 11 | github.com/BurntSushi/toml v1.2.1 // indirect 12 | github.com/KyleBanks/depth v1.2.1 // indirect 13 | github.com/PuerkitoBio/purell v1.1.1 // indirect 14 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 15 | github.com/andybalholm/brotli v1.0.6 // indirect 16 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 19 | github.com/go-openapi/jsonreference v0.19.6 // indirect 20 | github.com/go-openapi/spec v0.20.4 // indirect 21 | github.com/go-openapi/swag v0.19.15 // indirect 22 | github.com/google/uuid v1.3.1 // indirect 23 | github.com/hashicorp/errwrap v1.1.0 // indirect 24 | github.com/hashicorp/go-multierror v1.1.1 // indirect 25 | github.com/jackc/pgpassfile v1.0.0 // indirect 26 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 27 | github.com/jackc/puddle/v2 v2.2.1 // indirect 28 | github.com/joho/godotenv v1.5.1 // indirect 29 | github.com/josharian/intern v1.0.0 // indirect 30 | github.com/klauspost/compress v1.17.2 // indirect 31 | github.com/kr/text v0.2.0 // indirect 32 | github.com/lib/pq v1.10.2 // indirect 33 | github.com/mailru/easyjson v0.7.6 // indirect 34 | github.com/mattn/go-colorable v0.1.13 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mattn/go-runewidth v0.0.15 // indirect 37 | github.com/philhofer/fwd v1.1.2 // indirect 38 | github.com/redis/go-redis/v9 v9.0.2 // indirect 39 | github.com/rivo/uniseg v0.4.4 // indirect 40 | github.com/rogpeppe/go-internal v1.11.0 // indirect 41 | github.com/swaggo/files/v2 v2.0.0 // indirect 42 | github.com/swaggo/swag v1.16.2 // indirect 43 | github.com/tinylib/msgp v1.1.8 // indirect 44 | github.com/valyala/bytebufferpool v1.0.0 // indirect 45 | github.com/valyala/fasthttp v1.50.0 // indirect 46 | github.com/valyala/tcplisten v1.0.0 // indirect 47 | go.uber.org/atomic v1.7.0 // indirect 48 | golang.org/x/crypto v0.9.0 // indirect 49 | golang.org/x/net v0.10.0 // indirect 50 | golang.org/x/sync v0.2.0 // indirect 51 | golang.org/x/text v0.9.0 // indirect 52 | golang.org/x/tools v0.9.1 // indirect 53 | gopkg.in/yaml.v2 v2.4.0 // indirect 54 | gopkg.in/yaml.v3 v3.0.1 // indirect 55 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 56 | ) 57 | 58 | require ( 59 | github.com/fatih/structs v1.1.0 60 | github.com/gofiber/fiber/v2 v2.50.0 61 | github.com/gofiber/helmet/v2 v2.2.26 62 | github.com/gofiber/storage/redis v1.3.4 63 | github.com/gofiber/swagger v0.1.14 64 | github.com/golang-migrate/migrate/v4 v4.16.2 65 | github.com/ilyakaznacheev/cleanenv v1.5.0 66 | github.com/jackc/pgx/v5 v5.5.0 67 | github.com/mikhail-bigun/fiberlogrus v0.1.3 68 | github.com/pkg/errors v0.9.1 69 | golang.org/x/sys v0.13.0 // indirect 70 | ) 71 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 5 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 6 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 7 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 8 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 9 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 10 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 11 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 12 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 13 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 14 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 15 | github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= 16 | github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 17 | github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= 18 | github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= 19 | github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= 20 | github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= 21 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 22 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 24 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 29 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 30 | github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= 31 | github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0= 32 | github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= 33 | github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 34 | github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= 35 | github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 36 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 37 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 38 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 39 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 40 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 41 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 42 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 43 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 44 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 45 | github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= 46 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 47 | github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= 48 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 49 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 50 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 51 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 52 | github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= 53 | github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= 54 | github.com/gofiber/helmet/v2 v2.2.26 h1:KreQVUpCIGppPQ6Yt8qQMaIR4fVXMnvBdsda0dJSsO8= 55 | github.com/gofiber/helmet/v2 v2.2.26/go.mod h1:XE0DF4cgf0M5xIt7qyAK5zOi8jJblhxfSDv9DAmEEQo= 56 | github.com/gofiber/storage/redis v1.3.4 h1:IUNx09vnLiI1wZ/z3Dl5lYPrFdFgtgkAqG26wyIrwNI= 57 | github.com/gofiber/storage/redis v1.3.4/go.mod h1:lidaD5cHTNzYwzudWN0LN0wGYsrwpMpXClwE795xWSo= 58 | github.com/gofiber/swagger v0.1.14 h1:o524wh4QaS4eKhUCpj7M0Qhn8hvtzcyxDsfZLXuQcRI= 59 | github.com/gofiber/swagger v0.1.14/go.mod h1:DCk1fUPsj+P07CKaZttBbV1WzTZSQcSxfub8y9/BFr8= 60 | github.com/gofiber/utils v1.0.1 h1:knct4cXwBipWQqFrOy1Pv6UcgPM+EXo9jDgc66V1Qio= 61 | github.com/gofiber/utils v1.0.1/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= 62 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 63 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 64 | github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= 65 | github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= 66 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 67 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 68 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 69 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 70 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 71 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 72 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 73 | github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= 74 | github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= 75 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 76 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 77 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 78 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 79 | github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= 80 | github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 81 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 82 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 83 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 84 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 85 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 86 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 87 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 88 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 89 | github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= 90 | github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 91 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 92 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 93 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 94 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 95 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 96 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 97 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 98 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 99 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 100 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 101 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 102 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 103 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 104 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 105 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 106 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 107 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 108 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 109 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 110 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 111 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 112 | github.com/mikhail-bigun/fiberlogrus v0.1.3 h1:2aVtFSfMr/T8J2p4228TwV6txvUEOQxKlu5LpcKyym0= 113 | github.com/mikhail-bigun/fiberlogrus v0.1.3/go.mod h1:Tt0FrmLd2maF8VSHsx1pfiWNRTrjyBrKfDA2JW5hvmY= 114 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 115 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 116 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 117 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 118 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 119 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 120 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 121 | github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 122 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 123 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 124 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 125 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 126 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 127 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 128 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 129 | github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= 130 | github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= 131 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 132 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 133 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 134 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 135 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 136 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 137 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 138 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 139 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 140 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 141 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 144 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 145 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 146 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 147 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 148 | github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= 149 | github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= 150 | github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= 151 | github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= 152 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 153 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 154 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 155 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 156 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 157 | github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= 158 | github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 159 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 160 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 161 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 162 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 163 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 164 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 165 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 166 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 167 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= 168 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 169 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 170 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 171 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 172 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 173 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 174 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 175 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 176 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 177 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 178 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 179 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 180 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 181 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 182 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 183 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 184 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 185 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 186 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 187 | golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= 188 | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 189 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 190 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 194 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 198 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 199 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 200 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 201 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 202 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 203 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 204 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 205 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 206 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 207 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 208 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 209 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 210 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 211 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 212 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 213 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 214 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 215 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 216 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 217 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 218 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 219 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 220 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 221 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 222 | golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= 223 | golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 224 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 225 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 226 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 227 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 228 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 229 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 230 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 231 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 232 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 233 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 234 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 235 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 236 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 237 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 238 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 239 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= 240 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= 241 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 242 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "goapptemplate/config" 8 | "goapptemplate/pkg/postgres" 9 | "os" 10 | "os/signal" 11 | "time" 12 | 13 | swdocs "goapptemplate/docs" 14 | httpController "goapptemplate/internal/controller/http" 15 | "goapptemplate/internal/domain" 16 | "goapptemplate/internal/usecase" 17 | "goapptemplate/internal/usecase/repo" 18 | 19 | swagger "github.com/gofiber/swagger" 20 | 21 | "github.com/gofiber/fiber/v2" 22 | "github.com/mikhail-bigun/fiberlogrus" 23 | 24 | "github.com/gofiber/fiber/v2/middleware/cache" 25 | "github.com/gofiber/fiber/v2/middleware/compress" 26 | "github.com/gofiber/fiber/v2/middleware/cors" 27 | "github.com/gofiber/fiber/v2/middleware/etag" 28 | "github.com/gofiber/fiber/v2/middleware/pprof" 29 | "github.com/gofiber/fiber/v2/middleware/recover" 30 | "github.com/gofiber/fiber/v2/middleware/requestid" 31 | "github.com/gofiber/helmet/v2" 32 | "github.com/gofiber/storage/redis" 33 | gomigrate "github.com/golang-migrate/migrate/v4" 34 | "github.com/sirupsen/logrus" 35 | ) 36 | 37 | func Run(cfg *config.AppCfg) { 38 | // ________________________________________________________________________ 39 | // Setup logger 40 | logger := newLogger(cfg) 41 | // ________________________________________________________________________ 42 | // Migrate 43 | err := migrate(cfg) 44 | if err != nil { 45 | if errors.Is(err, gomigrate.ErrNoChange) { 46 | logger.WithError(err).Warning("cannot migrate") 47 | } else { 48 | logger.WithError(err).Fatal("cannot migrate") 49 | } 50 | } else { 51 | logger.Info("Successfully applied migrations") 52 | } 53 | // ________________________________________________________________________ 54 | // Create Postgres database instance 55 | pgxTracer := postgres.NewLogrusQueryTracer(logger) 56 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 57 | defer cancel() 58 | db, err := postgres.NewPostgresDB( 59 | ctx, 60 | cfg.Postgres.ConfigString( 61 | fmt.Sprintf( 62 | "search_path=%s", 63 | domain.SchemaApp, 64 | ), 65 | ), 66 | pgxTracer, 67 | ) 68 | if err != nil { 69 | logger.WithError(err).Fatal("cannot create postgres db") 70 | } 71 | // ________________________________________________________________________ 72 | // Setup Fiber router 73 | f := fiber.New() 74 | // Add middleware 75 | f.Use( 76 | fiberlogrus.New(fiberlogrus.Config{ 77 | Logger: logger, 78 | Tags: []string{ 79 | fiberlogrus.TagLatency, 80 | fiberlogrus.TagMethod, 81 | fiberlogrus.TagURL, 82 | fiberlogrus.TagUA, 83 | fiberlogrus.TagBytesSent, 84 | fiberlogrus.TagPid, 85 | fiberlogrus.TagStatus, 86 | }, 87 | }), 88 | recover.New(), 89 | compress.New(), 90 | cors.New(), 91 | helmet.New(), 92 | requestid.New(), 93 | etag.New(), 94 | pprof.New(), 95 | cache.New(cache.Config{ 96 | CacheControl: true, 97 | Storage: redis.New(redis.Config{ 98 | Host: cfg.Redis.Host, 99 | Port: int(cfg.Redis.Port), 100 | Username: cfg.Redis.Username, 101 | Password: cfg.Redis.Password, 102 | Database: cfg.Redis.DB, 103 | }), 104 | Next: func(c *fiber.Ctx) bool { 105 | return c.IP() == "127.0.0.1" 106 | }, 107 | }), 108 | ) 109 | // ________________________________________________________________________ 110 | // Setup Swagger docs 111 | setupSwagger(f, cfg) 112 | // ________________________________________________________________________ 113 | // Create Books repository 114 | br := repo.NewBooksPostgresRepo(db, logger) 115 | // Create Books usecase 116 | bu := usecase.NewBooks(br, logger) 117 | // Create App HTTP controller 118 | _ = httpController.NewAppHTTPController( 119 | f, 120 | bu, 121 | &httpController.AppHTTPControllerConfig{ 122 | BasePath: cfg.HTTP.FullAPIPath(), 123 | Timeout: cfg.HTTP.Timeout, 124 | }, 125 | logger, 126 | ) 127 | // ________________________________________________________________________ 128 | // Not found handler last in stack 129 | f.Use( 130 | func(c *fiber.Ctx) error { 131 | return c.Status(fiber.StatusNotFound).JSON(fiber.ErrNotFound) 132 | }, 133 | ) 134 | // Run Fiber router in a separate go routine 135 | go func() { 136 | err := runHTTP(f, cfg) 137 | if err != nil { 138 | logger.WithError(err).Fatal("cannot run HTTP") 139 | } 140 | }() 141 | // Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds. 142 | // Use a buffered channel to avoid missing signals as recommended for signal.Notify 143 | quit := make(chan os.Signal, 1) 144 | signal.Notify(quit, os.Interrupt) 145 | // This blocks the main thread until an interrupt is received 146 | <-quit 147 | logger.Info("Gracefully shutting down...") 148 | err = f.Shutdown() 149 | if err != nil { 150 | logger.WithError(err).Fatal("cannot gracefully shutdown Fiber server") 151 | } 152 | logger.Info("Running cleanup tasks...") 153 | logger.Info("Service shutdown successfully") 154 | } 155 | 156 | func newLogger(cfg *config.AppCfg) *logrus.Logger { 157 | logger := logrus.New() 158 | lvl, err := logrus.ParseLevel(cfg.Logger.Level) 159 | if err != nil { 160 | logger.WithError(err).Fatalf("cannot parse logrus level [%s]", cfg.Logger.Level) 161 | } 162 | logger.SetLevel(lvl) 163 | logger.Formatter = &logrus.TextFormatter{ 164 | FullTimestamp: true, 165 | TimestampFormat: "", 166 | // DisableSorting: true, 167 | DisableLevelTruncation: true, 168 | PadLevelText: true, 169 | QuoteEmptyFields: true, 170 | } 171 | return logger 172 | } 173 | 174 | func runHTTP(f *fiber.App, cfg *config.AppCfg) error { 175 | if cfg.TLS.Cert.Filepath != "" && 176 | cfg.TLS.Key.Filepath != "" { 177 | return f.ListenTLS( 178 | cfg.HTTP.Addr(), 179 | cfg.TLS.Cert.Filepath, 180 | cfg.TLS.Key.Filepath, 181 | ) 182 | } else { 183 | return f.Listen(cfg.HTTP.Addr()) 184 | } 185 | } 186 | 187 | func setupSwagger(f *fiber.App, cfg *config.AppCfg) { 188 | swdocs.SwaggerInfo.Host = cfg.Swagger.Host 189 | swdocs.SwaggerInfo.BasePath = cfg.Swagger.BasePath 190 | sr := f.Group(cfg.HTTP.Prefix + "/swagger") 191 | sr.Get("*", swagger.HandlerDefault) 192 | } 193 | -------------------------------------------------------------------------------- /internal/app/migrate.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "goapptemplate" 6 | "goapptemplate/config" 7 | "goapptemplate/internal/domain" 8 | "goapptemplate/pkg/migrator" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func migrate(cfg *config.AppCfg) error { 14 | mu, err := migrator.NewPostgresMigrator( 15 | cfg.Postgres.ConfigURL(), 16 | domain.SchemaApp, 17 | goapptemplate.MigrationsApp, 18 | "migrations/app", 19 | ) 20 | if err != nil { 21 | return errors.Wrap(err, "cannot create postgres migrator") 22 | } 23 | ctx, cancel := context.WithTimeout(context.Background(), cfg.HTTP.Timeout) 24 | defer cancel() 25 | err = mu.Up(ctx) 26 | if err != nil { 27 | return errors.Wrap(err, "cannot migrate up") 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/controller/http/app.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "goapptemplate/internal/domain" 6 | "goapptemplate/internal/usecase" 7 | "time" 8 | 9 | "github.com/fatih/structs" 10 | "github.com/gofiber/fiber/v2" 11 | "github.com/google/uuid" 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type AppHTTPController interface { 17 | CreateBook() func(*fiber.Ctx) error 18 | GetBooks() func(*fiber.Ctx) error 19 | GetBook() func(*fiber.Ctx) error 20 | UpdateBook() func(*fiber.Ctx) error 21 | DeleteBook() func(*fiber.Ctx) error 22 | } 23 | 24 | type AppHTTPControllerConfig struct { 25 | BasePath string 26 | Timeout time.Duration 27 | } 28 | 29 | type appHTTPController struct { 30 | f *fiber.App 31 | books usecase.Books 32 | config *AppHTTPControllerConfig 33 | log *logrus.Entry 34 | } 35 | 36 | // CreateBook implements AppHTTPController. 37 | // 38 | // @Summary Create book 39 | // @Description Create book 40 | // @Tags books 41 | // @Accept json 42 | // @Param data body domain.Book true "book attributes" 43 | // @Success 201 44 | // @Header 201 {string} Location "/books/:id" 45 | // @Failure 400 {object} fiber.Error 46 | // @Failure 500 {object} fiber.Error 47 | // @Router /books [post] 48 | func (hc *appHTTPController) CreateBook() func(*fiber.Ctx) error { 49 | return func(c *fiber.Ctx) error { 50 | book := &domain.Book{} 51 | err := c.BodyParser(book) 52 | if err != nil { 53 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 54 | } 55 | ctx, cancel := context.WithTimeout(context.Background(), hc.config.Timeout) 56 | defer cancel() 57 | b, err := hc.books.New(ctx, book) 58 | if err != nil { 59 | if errors.Is(err, domain.ErrValidation) { 60 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 61 | } 62 | hc.log.WithFields(structs.Map(book)).Error("cannot add new book") 63 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) 64 | } 65 | c.Location(c.Path() + "/" + b.ID.String()) 66 | return c.SendStatus(fiber.StatusCreated) 67 | } 68 | } 69 | 70 | // DeleteBook implements AppHTTPController. 71 | // 72 | // @Summary Delete book 73 | // @Description Delete book 74 | // @Tags books 75 | // @Produce json 76 | // @Param id path string true "book uuid" 77 | // @Success 204 78 | // @Failure 400 {object} fiber.Error 79 | // @Failure 500 {object} fiber.Error 80 | // @Router /books/{id} [delete] 81 | func (hc *appHTTPController) DeleteBook() func(*fiber.Ctx) error { 82 | return func(c *fiber.Ctx) error { 83 | bookID, err := uuid.Parse(c.Params("id")) 84 | if err != nil { 85 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 86 | } 87 | ctx, cancel := context.WithTimeout(context.Background(), hc.config.Timeout) 88 | defer cancel() 89 | err = hc.books.Remove(ctx, bookID) 90 | if err != nil { 91 | hc.log.WithField("book_id", bookID).Error("cannot remove book") 92 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) 93 | } 94 | return c.SendStatus(fiber.StatusNoContent) 95 | } 96 | } 97 | 98 | // GetBook implements AppHTTPController. 99 | // 100 | // @Summary Get book 101 | // @Description Get book 102 | // @Tags books 103 | // @Produce json 104 | // @Param id path string true "book uuid" 105 | // @Success 200 {object} domain.Book 106 | // @Failure 400 {object} fiber.Error 107 | // @Failure 404 {object} fiber.Error 108 | // @Failure 500 {object} fiber.Error 109 | // @Router /books/{id} [get] 110 | func (hc *appHTTPController) GetBook() func(*fiber.Ctx) error { 111 | return func(c *fiber.Ctx) error { 112 | bookID, err := uuid.Parse(c.Params("id")) 113 | if err != nil { 114 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 115 | } 116 | ctx, cancel := context.WithTimeout(context.Background(), hc.config.Timeout) 117 | defer cancel() 118 | b, err := hc.books.View(ctx, bookID) 119 | if err != nil { 120 | if errors.Is(err, domain.ErrBookNotFound) { 121 | return c.Status(fiber.StatusNotFound).JSON(fiber.ErrNotFound) 122 | } 123 | hc.log.WithField("book_id", bookID).Error("cannot view book") 124 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) 125 | } 126 | return c.Status(fiber.StatusOK).JSON(b) 127 | } 128 | } 129 | 130 | // GetBooks implements AppHTTPController. 131 | // 132 | // @Summary Get books 133 | // @Description Get books 134 | // @Tags books 135 | // @Produce json 136 | // @Param limit query int false "page size limit" 137 | // @Param offset query int false "page offset" 138 | // @Param name query string false "name search pattern" 139 | // @Param description query string false "description search pattern" 140 | // @Success 200 {object} domain.BookPage 141 | // @Failure 400 {object} fiber.Error 142 | // @Failure 500 {object} fiber.Error 143 | // @Router /books [get] 144 | func (hc *appHTTPController) GetBooks() func(*fiber.Ctx) error { 145 | return func(c *fiber.Ctx) error { 146 | ctx, cancel := context.WithTimeout(context.Background(), hc.config.Timeout) 147 | defer cancel() 148 | filters := new(domain.BookFilters) 149 | err := c.QueryParser(filters) 150 | if err != nil { 151 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 152 | } 153 | p, err := hc.books.List(ctx, filters) 154 | if err != nil { 155 | if errors.Is(err, domain.ErrValidation) { 156 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 157 | } 158 | hc.log.WithFields(structs.Map(filters)).Error("cannot list books") 159 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) 160 | } 161 | return c.Status(fiber.StatusOK).JSON(p) 162 | } 163 | } 164 | 165 | // UpdateBook implements AppHTTPController. 166 | // 167 | // @Summary Update book 168 | // @Description Update book 169 | // @Tags books 170 | // @Accept json 171 | // @Param id path string true "book uuid" 172 | // @Param data body domain.Book true "book attributes" 173 | // @Success 204 174 | // @Failure 400 {object} fiber.Error 175 | // @Failure 500 {object} fiber.Error 176 | // @Router /books/{id} [put] 177 | func (hc *appHTTPController) UpdateBook() func(*fiber.Ctx) error { 178 | return func(c *fiber.Ctx) error { 179 | bookID, err := uuid.Parse(c.Params("id")) 180 | if err != nil { 181 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 182 | } 183 | book := &domain.Book{ID: bookID} 184 | err = c.BodyParser(book) 185 | if err != nil { 186 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 187 | } 188 | ctx, cancel := context.WithTimeout(context.Background(), hc.config.Timeout) 189 | defer cancel() 190 | _, err = hc.books.Modify(ctx, book) 191 | if err != nil { 192 | if errors.Is(err, domain.ErrValidation) { 193 | return c.Status(fiber.StatusBadRequest).JSON(fiber.NewError(fiber.StatusBadRequest, err.Error())) 194 | } 195 | if errors.Is(err, domain.ErrBookNotFound) { 196 | return c.Status(fiber.StatusNotFound).JSON(fiber.ErrNotFound) 197 | } 198 | hc.log.WithFields(structs.Map(book)).Error("cannot modify book") 199 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) 200 | } 201 | c.Location(c.Path()) 202 | return c.SendStatus(fiber.StatusNoContent) 203 | } 204 | } 205 | 206 | func NewAppHTTPController( 207 | f *fiber.App, 208 | bu usecase.Books, 209 | config *AppHTTPControllerConfig, 210 | logger *logrus.Logger, 211 | ) AppHTTPController { 212 | hc := &appHTTPController{ 213 | f: f, 214 | books: bu, 215 | config: config, 216 | log: logger.WithField("layer", "internal.controller.http.appHTTPController"), 217 | } 218 | books := hc.f.Group(hc.config.BasePath + "/books") 219 | books.Post("", hc.CreateBook()) 220 | books.Get("/:id", hc.GetBook()) 221 | books.Get("", hc.GetBooks()) 222 | books.Put("/:id", hc.UpdateBook()) 223 | books.Delete("/:id", hc.DeleteBook()) 224 | 225 | return hc 226 | } 227 | -------------------------------------------------------------------------------- /internal/domain/books.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Book struct { 10 | ID uuid.UUID `json:"id"` 11 | Name string `json:"name"` 12 | Description string `json:"description"` 13 | CreatedAt time.Time `json:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at"` 15 | } 16 | 17 | func (b Book) Validate() error { 18 | if b.Name == "" || 19 | len(b.Name) > 255 { 20 | return ErrBookName 21 | } 22 | return nil 23 | } 24 | 25 | type BookFilters struct { 26 | Filters 27 | Name string `json:"name" query:"name"` 28 | Description string `json:"description" query:"description"` 29 | } 30 | 31 | func (f *BookFilters) Validate() error { 32 | return f.Filters.Validate() 33 | } 34 | 35 | type BookPage struct { 36 | Page 37 | Data []*Book `json:"data"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/domain/common.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | const ( 4 | DefaultPageLimit = 25 5 | MaxPageLimit = 100 6 | ) 7 | 8 | type Filters struct { 9 | Limit int32 `json:"limit" query:"limit"` 10 | Offset int32 `json:"offset" query:"offset"` 11 | } 12 | 13 | func (f *Filters) Validate() error { 14 | if f.Limit > 100 { 15 | return ErrPageLimit 16 | } 17 | if f.Limit == 0 { 18 | f.Limit = DefaultPageLimit 19 | } 20 | return nil 21 | } 22 | 23 | type Page struct { 24 | Total int64 `json:"total"` 25 | Limit int32 `json:"limit"` 26 | Offset int32 `json:"offset"` 27 | Metadata map[string]interface{} `json:"metadata"` 28 | } 29 | -------------------------------------------------------------------------------- /internal/domain/errors.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | ErrValidation = errors.New("validation error") 10 | 11 | ErrPageLimit = fmt.Errorf("invalid page limit [max=%v]", MaxPageLimit) 12 | 13 | ErrBookName = errors.New("invalid name") 14 | ErrBookNotFound = errors.New("book not found") 15 | ) 16 | -------------------------------------------------------------------------------- /internal/domain/schema.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | const ( 4 | SchemaApp = "app_schema" 5 | ) 6 | -------------------------------------------------------------------------------- /internal/usecase/books.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "goapptemplate/internal/domain" 7 | 8 | "github.com/google/uuid" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type booksUsecase struct { 14 | repo BooksRepo 15 | log *logrus.Entry 16 | } 17 | 18 | // List implements Books. 19 | func (u *booksUsecase) List(ctx context.Context, filters *domain.BookFilters) (*domain.BookPage, error) { 20 | err := filters.Validate() 21 | if err != nil { 22 | return nil, fmt.Errorf("%w: %w", domain.ErrValidation, err) 23 | } 24 | p, err := u.repo.RetrievePage(ctx, filters) 25 | if err != nil { 26 | return nil, errors.Wrap(err, "cannot retrieve book page") 27 | } 28 | return p, nil 29 | } 30 | 31 | // Modify implements Books. 32 | func (u *booksUsecase) Modify(ctx context.Context, book *domain.Book) (*domain.Book, error) { 33 | err := book.Validate() 34 | if err != nil { 35 | return nil, fmt.Errorf("%w: %w", domain.ErrValidation, err) 36 | } 37 | b, err := u.repo.Update(ctx, book) 38 | if err != nil { 39 | return nil, errors.Wrap(err, "cannot update book") 40 | } 41 | return b, nil 42 | } 43 | 44 | // New implements Books. 45 | func (u *booksUsecase) New(ctx context.Context, book *domain.Book) (*domain.Book, error) { 46 | err := book.Validate() 47 | if err != nil { 48 | return nil, fmt.Errorf("%w: %w", domain.ErrValidation, err) 49 | } 50 | book.ID = uuid.New() 51 | b, err := u.repo.Store(ctx, book) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "cannot store book") 54 | } 55 | return b, nil 56 | } 57 | 58 | // Remove implements Books. 59 | func (u *booksUsecase) Remove(ctx context.Context, bookID uuid.UUID) error { 60 | err := u.repo.Remove(ctx, bookID) 61 | if err != nil { 62 | return errors.Wrapf(err, "cannot remove book with ID=%s", bookID) 63 | } 64 | return nil 65 | } 66 | 67 | // View implements Books. 68 | func (u *booksUsecase) View(ctx context.Context, bookID uuid.UUID) (*domain.Book, error) { 69 | b, err := u.repo.Retrieve(ctx, bookID) 70 | if err != nil { 71 | return nil, errors.Wrapf(err, "cannot retrieve book with ID=%s", bookID) 72 | } 73 | return b, nil 74 | } 75 | 76 | func NewBooks(repo BooksRepo, logger *logrus.Logger) Books { 77 | return &booksUsecase{ 78 | repo: repo, 79 | log: logger.WithField("layer", "internal.usecase.booksUsecase"), 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/usecase/interface.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "goapptemplate/internal/domain" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type ( 11 | Books interface { 12 | New(ctx context.Context, book *domain.Book) (*domain.Book, error) 13 | View(ctx context.Context, bookID uuid.UUID) (*domain.Book, error) 14 | List(ctx context.Context, filters *domain.BookFilters) (*domain.BookPage, error) 15 | Modify(ctx context.Context, book *domain.Book) (*domain.Book, error) 16 | Remove(ctx context.Context, bookID uuid.UUID) error 17 | } 18 | BooksRepo interface { 19 | Store(ctx context.Context, book *domain.Book) (*domain.Book, error) 20 | Retrieve(ctx context.Context, bookID uuid.UUID) (*domain.Book, error) 21 | RetrievePage(ctx context.Context, filters *domain.BookFilters) (*domain.BookPage, error) 22 | Update(ctx context.Context, book *domain.Book) (*domain.Book, error) 23 | Remove(ctx context.Context, bookID uuid.UUID) error 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /internal/usecase/repo/books_postgres.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "goapptemplate/gen/app/db" 6 | "goapptemplate/internal/domain" 7 | "goapptemplate/internal/usecase" 8 | "goapptemplate/pkg/postgres" 9 | 10 | "github.com/google/uuid" 11 | "github.com/jackc/pgx/v5" 12 | "github.com/jackc/pgx/v5/pgtype" 13 | "github.com/pkg/errors" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type booksPostgresRepo struct { 18 | postgres.DB 19 | log *logrus.Entry 20 | } 21 | 22 | // Remove implements usecase.BooksRepo. 23 | func (repo *booksPostgresRepo) Remove(ctx context.Context, bookID uuid.UUID) error { 24 | conn, tx, err := repo.BeginTx(ctx) 25 | if err != nil { 26 | return errors.Wrap(err, "cannot begin tx") 27 | } 28 | defer conn.Release() 29 | defer tx.Rollback(ctx) 30 | 31 | q := db.New(conn).WithTx(tx) 32 | 33 | err = q.DeleteBookWhereID(ctx, pgtype.UUID{ 34 | Bytes: bookID, 35 | Valid: true, 36 | }) 37 | if err != nil { 38 | return errors.Wrapf(err, "cannot delete book where ID=%s", bookID) 39 | } 40 | 41 | err = repo.EndTx(ctx, tx) 42 | if err != nil { 43 | return errors.Wrap(err, "cannot end tx") 44 | } 45 | 46 | return nil 47 | } 48 | 49 | // Retrieve implements usecase.BooksRepo. 50 | func (repo *booksPostgresRepo) Retrieve(ctx context.Context, bookID uuid.UUID) (*domain.Book, error) { 51 | conn, tx, err := repo.BeginTx(ctx) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "cannot begin tx") 54 | } 55 | defer conn.Release() 56 | defer tx.Rollback(ctx) 57 | 58 | q := db.New(conn).WithTx(tx) 59 | 60 | row, err := q.SelectBookWhereID(ctx, pgtype.UUID{ 61 | Bytes: bookID, 62 | Valid: true, 63 | }) 64 | if err != nil { 65 | if errors.Is(err, pgx.ErrNoRows) { 66 | return nil, domain.ErrBookNotFound 67 | } 68 | return nil, errors.Wrapf(err, "cannot select book where ID=%s", bookID) 69 | } 70 | book := &domain.Book{ 71 | ID: row.ID.Bytes, 72 | Name: row.Name, 73 | Description: row.Description.String, 74 | CreatedAt: row.CreatedAt.Time, 75 | UpdatedAt: row.UpdatedAt.Time, 76 | } 77 | 78 | err = repo.EndTx(ctx, tx) 79 | if err != nil { 80 | return nil, errors.Wrap(err, "cannot end tx") 81 | } 82 | 83 | return book, nil 84 | } 85 | 86 | // RetrievePage implements usecase.BooksRepo. 87 | func (repo *booksPostgresRepo) RetrievePage(ctx context.Context, filters *domain.BookFilters) (*domain.BookPage, error) { 88 | conn, tx, err := repo.BeginTx(ctx) 89 | if err != nil { 90 | return nil, errors.Wrap(err, "cannot begin tx") 91 | } 92 | defer conn.Release() 93 | defer tx.Rollback(ctx) 94 | 95 | q := db.New(conn).WithTx(tx) 96 | 97 | total, err := q.SelectBooksCount(ctx, db.SelectBooksCountParams{ 98 | Name: "%" + filters.Name + "%", 99 | Description: pgtype.Text{ 100 | String: "%" + filters.Description + "%", 101 | Valid: true, 102 | }, 103 | }) 104 | if err != nil { 105 | return nil, errors.Wrap(err, "cannot select books count") 106 | } 107 | rows, err := q.SelectBooks(ctx, db.SelectBooksParams{ 108 | Name: "%" + filters.Name + "%", 109 | Description: pgtype.Text{ 110 | String: "%" + filters.Description + "%", 111 | Valid: true, 112 | }, 113 | Ofst: filters.Offset, 114 | Lim: filters.Limit, 115 | }) 116 | if err != nil { 117 | return nil, errors.Wrap(err, "cannot select books") 118 | } 119 | var books []*domain.Book 120 | for _, b := range rows { 121 | book := &domain.Book{ 122 | ID: b.ID.Bytes, 123 | Name: b.Name, 124 | Description: b.Description.String, 125 | CreatedAt: b.CreatedAt.Time, 126 | UpdatedAt: b.UpdatedAt.Time, 127 | } 128 | books = append(books, book) 129 | } 130 | 131 | err = repo.EndTx(ctx, tx) 132 | if err != nil { 133 | return nil, errors.Wrap(err, "cannot end tx") 134 | } 135 | 136 | return &domain.BookPage{ 137 | Page: domain.Page{ 138 | Total: total, 139 | Limit: filters.Limit, 140 | Offset: filters.Offset, 141 | Metadata: map[string]interface{}{ 142 | "description": "filtered page of books", 143 | }, 144 | }, 145 | Data: books, 146 | }, nil 147 | } 148 | 149 | // Store implements usecase.BooksRepo. 150 | func (repo *booksPostgresRepo) Store(ctx context.Context, book *domain.Book) (*domain.Book, error) { 151 | conn, tx, err := repo.BeginTx(ctx) 152 | if err != nil { 153 | return nil, errors.Wrap(err, "cannot begin tx") 154 | } 155 | defer conn.Release() 156 | defer tx.Rollback(ctx) 157 | 158 | q := db.New(conn).WithTx(tx) 159 | 160 | row, err := q.InsertBook(ctx, db.InsertBookParams{ 161 | ID: pgtype.UUID{ 162 | Bytes: book.ID, 163 | Valid: true, 164 | }, 165 | Name: book.Name, 166 | Description: pgtype.Text{ 167 | String: book.Description, 168 | Valid: true, 169 | }, 170 | }) 171 | if err != nil { 172 | return nil, errors.Wrap(err, "cannot insert book") 173 | } 174 | book = &domain.Book{ 175 | ID: row.ID.Bytes, 176 | Name: row.Name, 177 | Description: row.Description.String, 178 | CreatedAt: row.CreatedAt.Time, 179 | UpdatedAt: row.UpdatedAt.Time, 180 | } 181 | 182 | err = repo.EndTx(ctx, tx) 183 | if err != nil { 184 | return nil, errors.Wrap(err, "cannot end tx") 185 | } 186 | 187 | return book, nil 188 | } 189 | 190 | // Update implements usecase.BooksRepo. 191 | func (repo *booksPostgresRepo) Update(ctx context.Context, book *domain.Book) (*domain.Book, error) { 192 | conn, tx, err := repo.BeginTx(ctx) 193 | if err != nil { 194 | return nil, errors.Wrap(err, "cannot begin tx") 195 | } 196 | defer conn.Release() 197 | defer tx.Rollback(ctx) 198 | 199 | q := db.New(conn).WithTx(tx) 200 | 201 | err = q.UpdateBookWhereID(ctx, db.UpdateBookWhereIDParams{ 202 | Name: book.Name, 203 | Description: pgtype.Text{ 204 | String: book.Description, 205 | Valid: true, 206 | }, 207 | ID: pgtype.UUID{ 208 | Bytes: book.ID, 209 | Valid: true, 210 | }, 211 | }) 212 | if err != nil { 213 | return nil, errors.Wrapf(err, "cannot update book where ID=%s", book.ID) 214 | } 215 | row, err := q.SelectBookWhereID(ctx, pgtype.UUID{ 216 | Bytes: book.ID, 217 | Valid: true, 218 | }) 219 | if err != nil { 220 | if errors.Is(err, pgx.ErrNoRows) { 221 | return nil, domain.ErrBookNotFound 222 | } 223 | return nil, errors.Wrapf(err, "cannot select book where ID=%s", book.ID) 224 | } 225 | book = &domain.Book{ 226 | ID: row.ID.Bytes, 227 | Name: row.Name, 228 | Description: row.Description.String, 229 | CreatedAt: row.CreatedAt.Time, 230 | UpdatedAt: row.UpdatedAt.Time, 231 | } 232 | 233 | err = repo.EndTx(ctx, tx) 234 | if err != nil { 235 | return nil, errors.Wrap(err, "cannot end tx") 236 | } 237 | 238 | return book, nil 239 | } 240 | 241 | func NewBooksPostgresRepo(db postgres.DB, logger *logrus.Logger) usecase.BooksRepo { 242 | return &booksPostgresRepo{ 243 | DB: db, 244 | log: logger.WithField("layer", "internal.usecase.repo.booksPostgresRepo"), 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /migrations/app/000001_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; -------------------------------------------------------------------------------- /migrations/app/000001_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS books( 2 | id UUID, 3 | name VARCHAR(255) NOT NULL, 4 | description TEXT DEFAULT NULL, 5 | created_at TIMESTAMPTZ DEFAULT NOW(), 6 | updated_at TIMESTAMPTZ DEFAULT NOW(), 7 | PRIMARY KEY(id) 8 | ); -------------------------------------------------------------------------------- /pkg/migrator/migrator.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import "context" 4 | 5 | type Migrator interface { 6 | Up(context.Context) error 7 | Down(context.Context) error 8 | } 9 | -------------------------------------------------------------------------------- /pkg/migrator/postgres_migrator.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "fmt" 7 | "goapptemplate/pkg/postgres" 8 | "time" 9 | 10 | "github.com/golang-migrate/migrate/v4" 11 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 12 | "github.com/golang-migrate/migrate/v4/source" 13 | "github.com/golang-migrate/migrate/v4/source/iofs" 14 | _ "github.com/jackc/pgx/v5/stdlib" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | const createSchema = "create schema if not exists" 19 | const dropSchema = "drop schema if exists" 20 | 21 | func NewMigrationsSource(fs embed.FS, path string) (source.Driver, error) { 22 | // Load migration source from embeded filesystem 23 | src, err := iofs.New(fs, path) 24 | if err != nil { 25 | return nil, fmt.Errorf("cannot create source driver: %w", err) 26 | } 27 | return src, nil 28 | } 29 | 30 | type PostgresMigrator struct { 31 | m *migrate.Migrate 32 | url string 33 | schema string 34 | } 35 | 36 | // Down implements migrator.Migrator. 37 | func (pm *PostgresMigrator) Down(ctx context.Context) error { 38 | err := pm.m.Down() 39 | if err != nil { 40 | return errors.Wrapf(err, "cannot migrate [%s] down", pm.schema) 41 | } 42 | return nil 43 | } 44 | 45 | // Up implements migrator.Migrator. 46 | func (pm *PostgresMigrator) Up(ctx context.Context) error { 47 | err := pm.m.Up() 48 | if err != nil { 49 | return errors.Wrapf(err, "cannot migrate [%s] up", pm.schema) 50 | } 51 | return nil 52 | } 53 | 54 | func (pm *PostgresMigrator) CreateSchema(ctx context.Context, schema string) error { 55 | db, err := postgres.NewConn(ctx, pm.url, nil) 56 | if err != nil { 57 | return errors.Wrap(err, "cannot create connection") 58 | } 59 | defer db.Close(ctx) 60 | _, err = db.Exec(ctx, fmt.Sprintf("%s %s", createSchema, schema)) 61 | if err != nil { 62 | return errors.Wrapf(err, "cannot execute create [%s] schema query", schema) 63 | } 64 | return nil 65 | } 66 | 67 | func (pm *PostgresMigrator) DropSchema(ctx context.Context, schema string) error { 68 | db, err := postgres.NewConn(ctx, pm.url, nil) 69 | if err != nil { 70 | return errors.Wrap(err, "cannot create connection") 71 | } 72 | defer db.Close(ctx) 73 | _, err = db.Exec(ctx, fmt.Sprintf("%s %s", dropSchema, schema)) 74 | if err != nil { 75 | return errors.Wrapf(err, "cannot execute drop [%s] schema query", schema) 76 | } 77 | return nil 78 | } 79 | 80 | func NewPostgresMigrator(url string, schema string, migrations embed.FS, path string) (*PostgresMigrator, error) { 81 | pm := &PostgresMigrator{ 82 | url: url, 83 | schema: schema, 84 | } 85 | src, err := NewMigrationsSource(migrations, path) 86 | if err != nil { 87 | return nil, errors.Wrap(err, "cannot create migrations source") 88 | } 89 | ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) 90 | defer cancel() 91 | err = pm.CreateSchema(ctx, pm.schema) 92 | if err != nil { 93 | return nil, errors.Wrapf(err, "cannot create [%s] schema", pm.schema) 94 | } 95 | m, err := migrate.NewWithSourceInstance("iofs", src, fmt.Sprintf("%s&x-migrations-table=%s_migrations&search_path=%s", url, schema, schema)) 96 | if err != nil { 97 | return nil, errors.Wrap(err, "cannot create migrate instance with source") 98 | } 99 | pm.m = m 100 | return pm, nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/jackc/pgx/v5" 8 | "github.com/jackc/pgx/v5/pgxpool" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type DB interface { 13 | BeginTx(ctx context.Context) (*pgxpool.Conn, pgx.Tx, error) 14 | EndTx(context.Context, pgx.Tx) error 15 | } 16 | 17 | type PostgresDB struct { 18 | *pgxpool.Pool 19 | } 20 | 21 | func (db *PostgresDB) BeginTx(ctx context.Context) (*pgxpool.Conn, pgx.Tx, error) { 22 | conn, err := db.Acquire(ctx) 23 | if err != nil { 24 | return nil, nil, errors.Wrap(err, "cannot acquire connection from dbpool") 25 | } 26 | tx, err := conn.Begin(ctx) 27 | if err != nil { 28 | return nil, nil, errors.Wrap(err, "cannot begin transaction") 29 | } 30 | return conn, tx, nil 31 | } 32 | 33 | func (db *PostgresDB) EndTx(ctx context.Context, tx pgx.Tx) error { 34 | err := tx.Commit(ctx) 35 | if err != nil { 36 | return errors.Wrap(err, "cannot commit transaction") 37 | } 38 | return nil 39 | } 40 | 41 | func NewPostgresDB(ctx context.Context, config string, tracer pgx.QueryTracer) (*PostgresDB, error) { 42 | pool, err := NewPool(ctx, config, tracer) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &PostgresDB{ 47 | Pool: pool, 48 | }, nil 49 | } 50 | 51 | func NewPool(ctx context.Context, config string, tracer pgx.QueryTracer) (*pgxpool.Pool, error) { 52 | c, err := pgxpool.ParseConfig(config) 53 | if err != nil { 54 | return nil, fmt.Errorf("cannot parse postgres pool config: %w", err) 55 | } 56 | c.ConnConfig.Tracer = tracer 57 | db, err := pgxpool.NewWithConfig(ctx, c) 58 | if err != nil { 59 | return nil, fmt.Errorf("cannot create postgres pool: %w", err) 60 | } 61 | return db, nil 62 | } 63 | 64 | func NewConn(ctx context.Context, config string, tracer pgx.QueryTracer) (*pgx.Conn, error) { 65 | c, err := pgx.ParseConfig(config) 66 | if err != nil { 67 | return nil, fmt.Errorf("cannot parse postgres config: %w", err) 68 | } 69 | c.Tracer = tracer 70 | db, err := pgx.ConnectConfig(ctx, c) 71 | if err != nil { 72 | return nil, fmt.Errorf("cannot establish a connection with a PostgreSQL server: %w", err) 73 | } 74 | return db, nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/postgres/qt.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v5" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type queryTracer struct { 11 | log *logrus.Entry 12 | } 13 | 14 | // TraceQueryEnd implements pgx.QueryTracer. 15 | func (t *queryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) { 16 | t.log.WithFields( 17 | map[string]interface{}{ 18 | "host": conn.Config().Host, 19 | "port": conn.Config().Port, 20 | "user": conn.Config().User, 21 | "database": conn.Config().Database, 22 | "err": data.Err, 23 | }, 24 | ).Debug("query execution end") 25 | } 26 | 27 | // TraceQueryStart implements pgx.QueryTracer. 28 | func (t *queryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context { 29 | t.log.WithFields( 30 | map[string]interface{}{ 31 | "host": conn.Config().Host, 32 | "port": conn.Config().Port, 33 | "user": conn.Config().User, 34 | "database": conn.Config().Database, 35 | "sql": data.SQL, 36 | "args": data.Args, 37 | }, 38 | ).Debug("query execution start") 39 | return ctx 40 | } 41 | 42 | func NewLogrusQueryTracer(logger *logrus.Logger) pgx.QueryTracer { 43 | return &queryTracer{ 44 | log: logger.WithField("layer", "infrastructure.postgres.queryTracer"), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/postgres/sqlstate.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | const ( 4 | ErrDuplicateKey = "23505" 5 | ) 6 | -------------------------------------------------------------------------------- /queries/app/books_query.sql: -------------------------------------------------------------------------------- 1 | -- name: InsertBook :one 2 | INSERT INTO books(id, name, description) 3 | VALUES (@id, @name, @description) 4 | RETURNING *; 5 | -- name: SelectBookWhereID :one 6 | SELECT * 7 | FROM books 8 | WHERE id = @id; 9 | -- name: SelectBooksCount :one 10 | SELECT COUNT(*) 11 | FROM books 12 | WHERE name LIKE @name 13 | AND description LIKE @description; 14 | -- name: SelectBooks :many 15 | SELECT * 16 | FROM books 17 | WHERE name LIKE @name 18 | AND description LIKE @description 19 | ORDER BY created_at DESC 20 | LIMIT @lim OFFSET @ofst; 21 | -- name: UpdateBookWhereID :exec 22 | UPDATE books 23 | SET name = @name, 24 | description = @description, 25 | updated_at = now() 26 | WHERE id = @id; 27 | -- name: DeleteBookWhereID :exec 28 | DELETE FROM books 29 | WHERE id = @id; -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "postgresql" 4 | queries: "queries/app" 5 | schema: "migrations/app" 6 | gen: 7 | go: 8 | package: "db" 9 | sql_package: "pgx/v5" 10 | out: "gen/app/db" 11 | emit_result_struct_pointers: true 12 | --------------------------------------------------------------------------------