├── .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 [](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 |
56 |
59 |
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 |
--------------------------------------------------------------------------------