├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── config.yml ├── stale.yml └── workflows │ └── testing_build.yml ├── .gitignore ├── .idea └── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app ├── BUSINESS_LOGIC.md ├── controllers │ ├── book_controller.go │ └── token_controller.go ├── models │ └── book_model.go └── queries │ └── book_query.go ├── docs ├── API_DOCS.md ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── main.go ├── pkg ├── PROJECT_SPECIFIC.md ├── configs │ └── fiber_config.go ├── middleware │ ├── fiber_middleware.go │ └── jwt_middleware.go ├── routes │ ├── not_found_route.go │ ├── private_routes.go │ ├── private_routes_test.go │ ├── public_routes.go │ ├── public_routes_test.go │ └── swagger_route.go └── utils │ ├── jwt_generator.go │ ├── jwt_parser.go │ ├── start_server.go │ └── validator.go └── platform ├── PLATFORM_LEVEL.md ├── database ├── open_db_connection.go └── postgres.go └── migrations ├── 000001_create_init_tables.down.sql └── 000001_create_init_tables.up.sql /.dockerignore: -------------------------------------------------------------------------------- 1 | # Files 2 | .dockerignore 3 | .editorconfig 4 | .gitignore 5 | .env.example 6 | Dockerfile 7 | Makefile 8 | LICENSE 9 | **/*.md 10 | **/*_test.go 11 | 12 | # Folders 13 | .git/ 14 | .github/ 15 | build/ 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{go.mod,go.sum,*.go}] 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | [{Makefile,Dockerfile,*.yml,*.yaml}] 15 | indent_style = tab 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server settings: 2 | SERVER_URL="0.0.0.0:5000" 3 | SERVER_READ_TIMEOUT=60 4 | 5 | # JWT settings: 6 | JWT_SECRET_KEY="secret" 7 | JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT=15 8 | 9 | # Database settings: 10 | DB_SERVER_URL="host=localhost port=5432 user=postgres password=password dbname=postgres sslmode=disable" 11 | DB_MAX_CONNECTIONS=100 12 | DB_MAX_IDLE_CONNECTIONS=10 13 | DB_MAX_LIFETIME_CONNECTIONS=2 -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # Server settings: 2 | SERVER_URL="0.0.0.0:5000" 3 | SERVER_READ_TIMEOUT=60 4 | 5 | # JWT settings: 6 | JWT_SECRET_KEY="secret" 7 | JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT=15 8 | 9 | # Database settings: 10 | DB_SERVER_URL="host=localhost port=5432 user=postgres password=password dbname=postgres sslmode=disable" 11 | DB_MAX_CONNECTIONS=100 12 | DB_MAX_IDLE_CONNECTIONS=10 13 | DB_MAX_LIFETIME_CONNECTIONS=2 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | go.sum merge=union 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: koddr 4 | custom: https://paypal.me/koddr 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve. 4 | title: "" 5 | labels: "bug" 6 | assignees: "koddr" 7 | --- 8 | 9 | **Required check list:** 10 | 11 | - [x] I'm gonna mark the checkboxes like this. 12 | - [ ] I didn't find in the repository's issues section similar bug. 13 | - [ ] I understand, this is Open Source and not-for-profit product. 14 | - [ ] This is not about third-party project, framework, package or technology. 15 | 16 | **My environment:** 17 | 18 | - OS (`uname -a`): 19 | - Golang (`go version`): 20 | 21 | **Describe the bug:** 22 | 23 | 24 | 25 | **Steps to reproduce the behavior:** 26 | 27 | 1. Go to ... 28 | 2. Click on ... 29 | 3. ... 30 | 4. See error 31 | 32 | **Expected behavior:** 33 | 34 | 35 | 36 | **Screenshots:** 37 | 38 | 39 | 40 | **Additional context:** 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project. 4 | title: "" 5 | labels: "feature" 6 | assignees: "koddr" 7 | --- 8 | 9 | **Required check list:** 10 | 11 | - [x] I'm gonna mark the checkboxes like this. 12 | - [ ] I didn't find in the repository's issues section similar feature request. 13 | - [ ] I understand, this is Open Source and not-for-profit product. 14 | - [ ] This is not about third-party project, framework, package or technology. 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | 18 | 19 | 20 | **Describe the solution you'd like:** 21 | 22 | 23 | 24 | **Describe alternatives you've considered:** 25 | 26 | 27 | 28 | **Screenshots:** 29 | 30 | 31 | 32 | **Additional context:** 33 | 34 | 35 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 2 | # Comment to be posted to on first time issues 3 | newIssueWelcomeComment: > 4 | Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! 5 | 6 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 7 | # Comment to be posted to on PRs from first time contributors in your repository 8 | newPRWelcomeComment: > 9 | Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. 10 | 11 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 12 | # Comment to be posted to on pull requests merged by a first time user 13 | firstPRMergeComment: > 14 | Congrats on merging your first pull request! 🎉 We here at `Create Go App` are proud of you! 15 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | 12 | # Label to use when marking an issue as stale 13 | staleLabel: wontfix 14 | 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | 👋 Hello. Is this still relevant? If so, what is blocking it? Is there 18 | anything you can do to help move it forward? 19 | 20 | > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. 21 | 22 | Thank you for your contributions. 23 | 24 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 25 | closeComment: > 26 | ⚡️ This issue has been automatically closed because it has not had recent activity. -------------------------------------------------------------------------------- /.github/workflows/testing_build.yml: -------------------------------------------------------------------------------- 1 | name: Testing CLI 2 | 3 | on: 4 | push: 5 | branches: [master, main] 6 | paths: 7 | - "**.go" 8 | 9 | jobs: 10 | build: 11 | name: Testing build 12 | 13 | strategy: 14 | matrix: 15 | go-version: [1.16.x] 16 | platform: [ubuntu-latest, windows-latest, macos-latest] 17 | 18 | runs-on: ${{ matrix.platform }} 19 | 20 | steps: 21 | - name: Set up Go 1.x 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | id: go 26 | 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v2 29 | 30 | - name: Test 31 | run: go test -v -cover -race -timeout 60s ./... 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | **/.DS_Store 3 | 4 | # IDEs 5 | .vscode/ 6 | .idea/ 7 | 8 | # Dev build 9 | build/ 10 | tmp/ 11 | 12 | # Test 13 | *.out 14 | 15 | # Environment variables 16 | .env -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS builder 2 | 3 | # Move to working directory (/build). 4 | WORKDIR /build 5 | 6 | # Copy and download dependency using go mod. 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | # Copy the code into the container. 11 | COPY . . 12 | 13 | # Set necessary environmet variables needed for our image and build the API server. 14 | ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 15 | RUN go build -ldflags="-s -w" -o apiserver . 16 | 17 | FROM scratch 18 | 19 | # Copy binary and config files from /build to root folder of scratch container. 20 | COPY --from=builder ["/build/apiserver", "/build/.env", "/"] 21 | 22 | # Export necessary port. 23 | EXPOSE 5000 24 | 25 | # Command to run when starting the container. 26 | ENTRYPOINT ["/apiserver"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vic Shóstak (https://1wa.co) 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean test security build run 2 | 3 | APP_NAME = apiserver 4 | BUILD_DIR = $(PWD)/build 5 | MIGRATIONS_FOLDER = $(PWD)/platform/migrations 6 | DATABASE_URL = postgres://postgres:password@localhost/postgres?sslmode=disable 7 | 8 | clean: 9 | rm -rf ./build 10 | 11 | security: 12 | gosec -quiet ./... 13 | 14 | test: security 15 | go test -v -timeout 30s -coverprofile=cover.out -cover ./... 16 | go tool cover -func=cover.out 17 | 18 | build: clean test 19 | CGO_ENABLED=0 go build -ldflags="-w -s" -o $(BUILD_DIR)/$(APP_NAME) main.go 20 | 21 | run: swag build 22 | $(BUILD_DIR)/$(APP_NAME) 23 | 24 | migrate.up: 25 | migrate -path $(MIGRATIONS_FOLDER) -database "$(DATABASE_URL)" up 26 | 27 | migrate.down: 28 | migrate -path $(MIGRATIONS_FOLDER) -database "$(DATABASE_URL)" down 29 | 30 | migrate.force: 31 | migrate -path $(MIGRATIONS_FOLDER) -database "$(DATABASE_URL)" force $(version) 32 | 33 | docker.run: docker.network docker.postgres swag docker.fiber migrate.up 34 | 35 | docker.network: 36 | docker network inspect dev-network >/dev/null 2>&1 || \ 37 | docker network create -d bridge dev-network 38 | 39 | docker.fiber.build: 40 | docker build -t fiber . 41 | 42 | docker.fiber: docker.fiber.build 43 | docker run --rm -d \ 44 | --name dev-fiber \ 45 | --network dev-network \ 46 | -p 5000:5000 \ 47 | fiber 48 | 49 | docker.postgres: 50 | docker run --rm -d \ 51 | --name dev-postgres \ 52 | --network dev-network \ 53 | -e POSTGRES_USER=postgres \ 54 | -e POSTGRES_PASSWORD=password \ 55 | -e POSTGRES_DB=postgres \ 56 | -v ${HOME}/dev-postgres/data/:/var/lib/postgresql/data \ 57 | -p 5432:5432 \ 58 | postgres 59 | 60 | docker.stop: docker.stop.fiber docker.stop.postgres 61 | 62 | docker.stop.fiber: 63 | docker stop dev-fiber 64 | 65 | docker.stop.postgres: 66 | docker stop dev-postgres 67 | 68 | swag: 69 | swag init -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📖 Tutorial: Build a RESTful API on Go 2 | 3 | Fiber, PostgreSQL, JWT and Swagger docs in isolated Docker containers. 4 | 5 | 👉 The full article is published on **March 22, 2021**, on Dev.to: https://dev.to/koddr/build-a-restful-api-on-go-fiber-postgresql-jwt-and-swagger-docs-in-isolated-docker-containers-475j 6 | 7 | ![fiber_cover_gh](https://user-images.githubusercontent.com/11155743/112001218-cf258b00-8b2f-11eb-9c6d-d6c38a09af86.jpg) 8 | 9 | ## Quick start 10 | 11 | 1. Rename `.env.example` to `.env` and fill it with your environment values. 12 | 2. Install [Docker](https://www.docker.com/get-started) and [migrate](https://github.com/golang-migrate/migrate) tool for applying migrations. 13 | 3. Run project by this command: 14 | 15 | ```bash 16 | make docker.run 17 | 18 | # Process: 19 | # - Generate API docs by Swagger 20 | # - Create a new Docker network for containers 21 | # - Build and run Docker containers (Fiber, PostgreSQL) 22 | # - Apply database migrations (using github.com/golang-migrate/migrate) 23 | ``` 24 | 25 | 4. Go to your API Docs page: [127.0.0.1:5000/swagger/index.html](http://127.0.0.1:5000/swagger/index.html) 26 | 27 | ![Screenshot](https://user-images.githubusercontent.com/11155743/111976684-f15ce000-8b12-11eb-871a-8d32465900fe.png) 28 | 29 | ## P.S. 30 | 31 | If you want more articles like this on this blog, then post a comment below and subscribe to me. Thanks! 😘 32 | 33 | And, of course, you can support me by donating at [LiberaPay](https://liberapay.com/koddr/donate). _Each donation will be used to write new articles and develop non-profit open-source projects for the community._ 34 | 35 | [![Support author at LiberaPay](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq8442cqyjq2i1jdeay8.png)](https://liberapay.com/koddr/donate) 36 | 37 | ## ⚠️ License 38 | 39 | MIT © [Vic Shóstak](https://github.com/koddr) & [True web artisans](https://1wa.co/). 40 | 41 | 42 | ## Building: 43 | 44 | https://levelup.gitconnected.com/a-better-way-than-ldflags-to-add-a-build-version-to-your-go-binaries-2258ce419d2d 45 | 46 | 47 | 48 | Windows: 49 | ``` 50 | $ set CGO_ENABLED=0 && go build -ldflags="-w -s -X 'main.version=1.0.1'" . 51 | ``` 52 | -------------------------------------------------------------------------------- /app/BUSINESS_LOGIC.md: -------------------------------------------------------------------------------- 1 | # ./app 2 | 3 | **Folder with business logic only**. This directory doesn't care about _what database driver you're using_ or _which caching solution your choose_ or any third-party things. 4 | 5 | - `./app/controllers` folder for functional controllers (used in routes) 6 | - `./app/models` folder for describe business models of your project 7 | - `./app/queries` folder for describe queries for models of your project 8 | -------------------------------------------------------------------------------- /app/controllers/book_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/google/uuid" 8 | "github.com/koddr/tutorial-go-fiber-rest-api/app/models" 9 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils" 10 | "github.com/koddr/tutorial-go-fiber-rest-api/platform/database" 11 | ) 12 | 13 | // GetBooks func gets all exists books. 14 | // @Description Get all exists books. 15 | // @Summary get all exists books 16 | // @Tags Books 17 | // @Accept json 18 | // @Produce json 19 | // @Success 200 {array} models.Book 20 | // @Router /v1/books [get] 21 | func GetBooks(c *fiber.Ctx) error { 22 | // Create database connection. 23 | db, err := database.OpenDBConnection() 24 | if err != nil { 25 | // Return status 500 and database connection error. 26 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 27 | "error": true, 28 | "msg": err.Error(), 29 | }) 30 | } 31 | 32 | // Get all books. 33 | books, err := db.GetBooks() 34 | if err != nil { 35 | // Return, if books not found. 36 | return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ 37 | "error": true, 38 | "msg": "books were not found", 39 | "count": 0, 40 | "books": nil, 41 | }) 42 | } 43 | 44 | // Return status 200 OK. 45 | return c.JSON(fiber.Map{ 46 | "error": false, 47 | "msg": nil, 48 | "count": len(books), 49 | "books": books, 50 | }) 51 | } 52 | 53 | // GetBook func gets book by given ID or 404 error. 54 | // @Description Get book by given ID. 55 | // @Summary get book by given ID 56 | // @Tags Book 57 | // @Accept json 58 | // @Produce json 59 | // @Param id path string true "Book ID" 60 | // @Success 200 {object} models.Book 61 | // @Router /v1/book/{id} [get] 62 | func GetBook(c *fiber.Ctx) error { 63 | // Catch book ID from URL. 64 | id, err := uuid.Parse(c.Params("id")) 65 | if err != nil { 66 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 67 | "error": true, 68 | "msg": err.Error(), 69 | }) 70 | } 71 | 72 | // Create database connection. 73 | db, err := database.OpenDBConnection() 74 | if err != nil { 75 | // Return status 500 and database connection error. 76 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 77 | "error": true, 78 | "msg": err.Error(), 79 | }) 80 | } 81 | 82 | // Get book by ID. 83 | book, err := db.GetBook(id) 84 | if err != nil { 85 | // Return, if book not found. 86 | return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ 87 | "error": true, 88 | "msg": "book with the given ID is not found", 89 | "book": nil, 90 | }) 91 | } 92 | 93 | // Return status 200 OK. 94 | return c.JSON(fiber.Map{ 95 | "error": false, 96 | "msg": nil, 97 | "book": book, 98 | }) 99 | } 100 | 101 | // CreateBook func for creates a new book. 102 | // @Description Create a new book. 103 | // @Summary create a new book 104 | // @Tags Book 105 | // @Accept json 106 | // @Produce json 107 | // @Param title body string true "Title" 108 | // @Param author body string true "Author" 109 | // @Param book_attrs body models.BookAttrs true "Book attributes" 110 | // @Success 200 {object} models.Book 111 | // @Security ApiKeyAuth 112 | // @Router /v1/book [post] 113 | func CreateBook(c *fiber.Ctx) error { 114 | // Get now time. 115 | now := time.Now().Unix() 116 | 117 | // Get claims from JWT. 118 | claims, err := utils.ExtractTokenMetadata(c) 119 | if err != nil { 120 | // Return status 500 and JWT parse error. 121 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 122 | "error": true, 123 | "msg": err.Error(), 124 | }) 125 | } 126 | 127 | // Set expiration time from JWT data of current book. 128 | expires := claims.Expires 129 | 130 | // Checking, if now time greater than expiration from JWT. 131 | if now > expires { 132 | // Return status 401 and unauthorized error message. 133 | return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ 134 | "error": true, 135 | "msg": "unauthorized, check expiration time of your token", 136 | }) 137 | } 138 | 139 | // Create new Book struct 140 | book := &models.Book{} 141 | 142 | // Check, if received JSON data is valid. 143 | if err := c.BodyParser(book); err != nil { 144 | // Return status 400 and error message. 145 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 146 | "error": true, 147 | "msg": err.Error(), 148 | }) 149 | } 150 | 151 | // Create database connection. 152 | db, err := database.OpenDBConnection() 153 | if err != nil { 154 | // Return status 500 and database connection error. 155 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 156 | "error": true, 157 | "msg": err.Error(), 158 | }) 159 | } 160 | 161 | // Create a new validator for a Book model. 162 | validate := utils.NewValidator() 163 | 164 | // Set initialized default data for book: 165 | book.ID = uuid.New() 166 | book.CreatedAt = time.Now() 167 | book.BookStatus = 1 // 0 == draft, 1 == active 168 | 169 | // Validate book fields. 170 | if err := validate.Struct(book); err != nil { 171 | // Return, if some fields are not valid. 172 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 173 | "error": true, 174 | "msg": utils.ValidatorErrors(err), 175 | }) 176 | } 177 | 178 | // Delete book by given ID. 179 | if err := db.CreateBook(book); err != nil { 180 | // Return status 500 and error message. 181 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 182 | "error": true, 183 | "msg": err.Error(), 184 | }) 185 | } 186 | 187 | // Return status 200 OK. 188 | return c.JSON(fiber.Map{ 189 | "error": false, 190 | "msg": nil, 191 | "book": book, 192 | }) 193 | } 194 | 195 | // UpdateBook func for updates book by given ID. 196 | // @Description Update book. 197 | // @Summary update book 198 | // @Tags Book 199 | // @Accept json 200 | // @Produce json 201 | // @Param id body string true "Book ID" 202 | // @Param title body string true "Title" 203 | // @Param author body string true "Author" 204 | // @Param book_status body integer true "Book status" 205 | // @Param book_attrs body models.BookAttrs true "Book attributes" 206 | // @Success 201 {string} status "ok" 207 | // @Security ApiKeyAuth 208 | // @Router /v1/book [put] 209 | func UpdateBook(c *fiber.Ctx) error { 210 | // Get now time. 211 | now := time.Now().Unix() 212 | 213 | // Get claims from JWT. 214 | claims, err := utils.ExtractTokenMetadata(c) 215 | if err != nil { 216 | // Return status 500 and JWT parse error. 217 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 218 | "error": true, 219 | "msg": err.Error(), 220 | }) 221 | } 222 | 223 | // Set expiration time from JWT data of current book. 224 | expires := claims.Expires 225 | 226 | // Checking, if now time greater than expiration from JWT. 227 | if now > expires { 228 | // Return status 401 and unauthorized error message. 229 | return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ 230 | "error": true, 231 | "msg": "unauthorized, check expiration time of your token", 232 | }) 233 | } 234 | 235 | // Create new Book struct 236 | book := &models.Book{} 237 | 238 | // Check, if received JSON data is valid. 239 | if err := c.BodyParser(book); err != nil { 240 | // Return status 400 and error message. 241 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 242 | "error": true, 243 | "msg": err.Error(), 244 | }) 245 | } 246 | 247 | // Create database connection. 248 | db, err := database.OpenDBConnection() 249 | if err != nil { 250 | // Return status 500 and database connection error. 251 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 252 | "error": true, 253 | "msg": err.Error(), 254 | }) 255 | } 256 | 257 | // Checking, if book with given ID does exist. 258 | foundedBook, err := db.GetBook(book.ID) 259 | if err != nil { 260 | // Return status 404 and book not found error. 261 | return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ 262 | "error": true, 263 | "msg": "book with this ID not found", 264 | }) 265 | } 266 | 267 | // Set initialized default data for book: 268 | book.UpdatedAt = time.Now() 269 | 270 | // Create a new validator for a Book model. 271 | validate := utils.NewValidator() 272 | 273 | // Validate book fields. 274 | if err := validate.Struct(book); err != nil { 275 | // Return, if some fields are not valid. 276 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 277 | "error": true, 278 | "msg": utils.ValidatorErrors(err), 279 | }) 280 | } 281 | 282 | // Update book by given ID. 283 | if err := db.UpdateBook(foundedBook.ID, book); err != nil { 284 | // Return status 500 and error message. 285 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 286 | "error": true, 287 | "msg": err.Error(), 288 | }) 289 | } 290 | 291 | // Return status 201. 292 | return c.SendStatus(fiber.StatusCreated) 293 | } 294 | 295 | // DeleteBook func for deletes book by given ID. 296 | // @Description Delete book by given ID. 297 | // @Summary delete book by given ID 298 | // @Tags Book 299 | // @Accept json 300 | // @Produce json 301 | // @Param id body string true "Book ID" 302 | // @Success 204 {string} status "ok" 303 | // @Security ApiKeyAuth 304 | // @Router /v1/book [delete] 305 | func DeleteBook(c *fiber.Ctx) error { 306 | // Get now time. 307 | now := time.Now().Unix() 308 | 309 | // Get claims from JWT. 310 | claims, err := utils.ExtractTokenMetadata(c) 311 | if err != nil { 312 | // Return status 500 and JWT parse error. 313 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 314 | "error": true, 315 | "msg": err.Error(), 316 | }) 317 | } 318 | 319 | // Set expiration time from JWT data of current book. 320 | expires := claims.Expires 321 | 322 | // Checking, if now time greater than expiration from JWT. 323 | if now > expires { 324 | // Return status 401 and unauthorized error message. 325 | return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ 326 | "error": true, 327 | "msg": "unauthorized, check expiration time of your token", 328 | }) 329 | } 330 | 331 | // Create new Book struct 332 | book := &models.Book{} 333 | 334 | // Check, if received JSON data is valid. 335 | if err := c.BodyParser(book); err != nil { 336 | // Return status 400 and error message. 337 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 338 | "error": true, 339 | "msg": err.Error(), 340 | }) 341 | } 342 | 343 | // Create a new validator for a Book model. 344 | validate := utils.NewValidator() 345 | 346 | // Validate only one book field ID. 347 | if err := validate.StructPartial(book, "id"); err != nil { 348 | // Return, if some fields are not valid. 349 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 350 | "error": true, 351 | "msg": utils.ValidatorErrors(err), 352 | }) 353 | } 354 | 355 | // Create database connection. 356 | db, err := database.OpenDBConnection() 357 | if err != nil { 358 | // Return status 500 and database connection error. 359 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 360 | "error": true, 361 | "msg": err.Error(), 362 | }) 363 | } 364 | 365 | // Checking, if book with given ID does exist. 366 | foundedBook, err := db.GetBook(book.ID) 367 | if err != nil { 368 | // Return status 404 and book not found error. 369 | return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ 370 | "error": true, 371 | "msg": "book with this ID not found", 372 | }) 373 | } 374 | 375 | // Delete book by given ID. 376 | if err := db.DeleteBook(foundedBook.ID); err != nil { 377 | // Return status 500 and error message. 378 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 379 | "error": true, 380 | "msg": err.Error(), 381 | }) 382 | } 383 | 384 | // Return status 204 no content. 385 | return c.SendStatus(fiber.StatusNoContent) 386 | } 387 | -------------------------------------------------------------------------------- /app/controllers/token_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils" 6 | ) 7 | 8 | // GetNewAccessToken method for create a new access token. 9 | // @Description Create a new access token. 10 | // @Summary create a new access token 11 | // @Tags Token 12 | // @Accept json 13 | // @Produce json 14 | // @Success 200 {string} status "ok" 15 | // @Router /v1/token/new [get] 16 | func GetNewAccessToken(c *fiber.Ctx) error { 17 | // Generate a new Access token. 18 | token, err := utils.GenerateNewAccessToken() 19 | if err != nil { 20 | // Return status 500 and token generation error. 21 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 22 | "error": true, 23 | "msg": err.Error(), 24 | }) 25 | } 26 | 27 | return c.JSON(fiber.Map{ 28 | "error": false, 29 | "msg": nil, 30 | "access_token": token, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /app/models/book_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // Book struct to describe book object. 13 | type Book struct { 14 | ID uuid.UUID `db:"id" json:"id" validate:"required,uuid"` 15 | CreatedAt time.Time `db:"created_at" json:"created_at"` 16 | UpdatedAt time.Time `db:"updated_at" json:"updated_at"` 17 | UserID uuid.UUID `db:"user_id" json:"user_id" validate:"required,uuid"` 18 | Title string `db:"title" json:"title" validate:"required,lte=255"` 19 | Author string `db:"author" json:"author" validate:"required,lte=255"` 20 | BookStatus int `db:"book_status" json:"book_status" validate:"required,len=1"` 21 | BookAttrs BookAttrs `db:"book_attrs" json:"book_attrs" validate:"required,dive"` 22 | } 23 | 24 | // BookAttrs struct to describe book attributes. 25 | type BookAttrs struct { 26 | Picture string `json:"picture"` 27 | Description string `json:"description"` 28 | Rating int `json:"rating" validate:"min=1,max=10"` 29 | } 30 | 31 | // Value make the BookAttrs struct implement the driver.Valuer interface. 32 | // This method simply returns the JSON-encoded representation of the struct. 33 | func (b *BookAttrs) Value() (driver.Value, error) { 34 | return json.Marshal(b) 35 | } 36 | 37 | // Scan make the BookAttrs struct implement the sql.Scanner interface. 38 | // This method simply decodes a JSON-encoded value into the struct fields. 39 | func (b *BookAttrs) Scan(value interface{}) error { 40 | j, ok := value.([]byte) 41 | if !ok { 42 | return errors.New("type assertion to []byte failed") 43 | } 44 | 45 | return json.Unmarshal(j, &b) 46 | } 47 | -------------------------------------------------------------------------------- /app/queries/book_query.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jmoiron/sqlx" 6 | "github.com/koddr/tutorial-go-fiber-rest-api/app/models" 7 | ) 8 | 9 | // BookQueries struct for queries from Book model. 10 | type BookQueries struct { 11 | *sqlx.DB 12 | } 13 | 14 | // GetBooks method for getting all books. 15 | func (q *BookQueries) GetBooks() ([]models.Book, error) { 16 | // Define books variable. 17 | books := []models.Book{} 18 | 19 | // Define query string. 20 | query := `SELECT * FROM books` 21 | 22 | // Send query to database. 23 | err := q.Select(&books, query) 24 | if err != nil { 25 | // Return empty object and error. 26 | return books, err 27 | } 28 | 29 | // Return query result. 30 | return books, nil 31 | } 32 | 33 | // GetBook method for getting one book by given ID. 34 | func (q *BookQueries) GetBook(id uuid.UUID) (models.Book, error) { 35 | // Define book variable. 36 | book := models.Book{} 37 | 38 | // Define query string. 39 | query := `SELECT * FROM books WHERE id = $1` 40 | 41 | // Send query to database. 42 | err := q.Get(&book, query, id) 43 | if err != nil { 44 | // Return empty object and error. 45 | return book, err 46 | } 47 | 48 | // Return query result. 49 | return book, nil 50 | } 51 | 52 | // CreateBook method for creating book by given Book object. 53 | func (q *BookQueries) CreateBook(b *models.Book) error { 54 | // Define query string. 55 | query := `INSERT INTO books VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` 56 | 57 | // Send query to database. 58 | _, err := q.Exec(query, b.ID, b.CreatedAt, b.UpdatedAt, b.UserID, b.Title, b.Author, b.BookStatus, b.BookAttrs) 59 | if err != nil { 60 | // Return only error. 61 | return err 62 | } 63 | 64 | // This query returns nothing. 65 | return nil 66 | } 67 | 68 | // UpdateBook method for updating book by given Book object. 69 | func (q *BookQueries) UpdateBook(id uuid.UUID, b *models.Book) error { 70 | // Define query string. 71 | query := `UPDATE books SET updated_at = $2, title = $3, author = $4, book_status = $5, book_attrs = $6 WHERE id = $1` 72 | 73 | // Send query to database. 74 | _, err := q.Exec(query, id, b.UpdatedAt, b.Title, b.Author, b.BookStatus, b.BookAttrs) 75 | if err != nil { 76 | // Return only error. 77 | return err 78 | } 79 | 80 | // This query returns nothing. 81 | return nil 82 | } 83 | 84 | // DeleteBook method for delete book by given ID. 85 | func (q *BookQueries) DeleteBook(id uuid.UUID) error { 86 | // Define query string. 87 | query := `DELETE FROM books WHERE id = $1` 88 | 89 | // Send query to database. 90 | _, err := q.Exec(query, id) 91 | if err != nil { 92 | // Return only error. 93 | return err 94 | } 95 | 96 | // This query returns nothing. 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /docs/API_DOCS.md: -------------------------------------------------------------------------------- 1 | # ./docs 2 | 3 | **Folder with API Documentation**. This directory contains config files for auto-generated API Docs by Swagger. 4 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY SWAG; DO NOT EDIT 2 | // This file was generated by swaggo/swag 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 | "termsOfService": "http://swagger.io/terms/", 14 | "contact": { 15 | "name": "API Support", 16 | "email": "your@mail.com" 17 | }, 18 | "license": { 19 | "name": "Apache 2.0", 20 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 21 | }, 22 | "version": "{{.Version}}" 23 | }, 24 | "host": "{{.Host}}", 25 | "basePath": "{{.BasePath}}", 26 | "paths": { 27 | "/v1/book": { 28 | "put": { 29 | "security": [ 30 | { 31 | "ApiKeyAuth": [] 32 | } 33 | ], 34 | "description": "Update book.", 35 | "consumes": [ 36 | "application/json" 37 | ], 38 | "produces": [ 39 | "application/json" 40 | ], 41 | "tags": [ 42 | "Book" 43 | ], 44 | "summary": "update book", 45 | "parameters": [ 46 | { 47 | "description": "Book ID", 48 | "name": "id", 49 | "in": "body", 50 | "required": true, 51 | "schema": { 52 | "type": "string" 53 | } 54 | }, 55 | { 56 | "description": "Title", 57 | "name": "title", 58 | "in": "body", 59 | "required": true, 60 | "schema": { 61 | "type": "string" 62 | } 63 | }, 64 | { 65 | "description": "Author", 66 | "name": "author", 67 | "in": "body", 68 | "required": true, 69 | "schema": { 70 | "type": "string" 71 | } 72 | }, 73 | { 74 | "description": "Book status", 75 | "name": "book_status", 76 | "in": "body", 77 | "required": true, 78 | "schema": { 79 | "type": "integer" 80 | } 81 | }, 82 | { 83 | "description": "Book attributes", 84 | "name": "book_attrs", 85 | "in": "body", 86 | "required": true, 87 | "schema": { 88 | "$ref": "#/definitions/models.BookAttrs" 89 | } 90 | } 91 | ], 92 | "responses": { 93 | "201": { 94 | "description": "ok", 95 | "schema": { 96 | "type": "string" 97 | } 98 | } 99 | } 100 | }, 101 | "post": { 102 | "security": [ 103 | { 104 | "ApiKeyAuth": [] 105 | } 106 | ], 107 | "description": "Create a new book.", 108 | "consumes": [ 109 | "application/json" 110 | ], 111 | "produces": [ 112 | "application/json" 113 | ], 114 | "tags": [ 115 | "Book" 116 | ], 117 | "summary": "create a new book", 118 | "parameters": [ 119 | { 120 | "description": "Title", 121 | "name": "title", 122 | "in": "body", 123 | "required": true, 124 | "schema": { 125 | "type": "string" 126 | } 127 | }, 128 | { 129 | "description": "Author", 130 | "name": "author", 131 | "in": "body", 132 | "required": true, 133 | "schema": { 134 | "type": "string" 135 | } 136 | }, 137 | { 138 | "description": "Book attributes", 139 | "name": "book_attrs", 140 | "in": "body", 141 | "required": true, 142 | "schema": { 143 | "$ref": "#/definitions/models.BookAttrs" 144 | } 145 | } 146 | ], 147 | "responses": { 148 | "200": { 149 | "description": "OK", 150 | "schema": { 151 | "$ref": "#/definitions/models.Book" 152 | } 153 | } 154 | } 155 | }, 156 | "delete": { 157 | "security": [ 158 | { 159 | "ApiKeyAuth": [] 160 | } 161 | ], 162 | "description": "Delete book by given ID.", 163 | "consumes": [ 164 | "application/json" 165 | ], 166 | "produces": [ 167 | "application/json" 168 | ], 169 | "tags": [ 170 | "Book" 171 | ], 172 | "summary": "delete book by given ID", 173 | "parameters": [ 174 | { 175 | "description": "Book ID", 176 | "name": "id", 177 | "in": "body", 178 | "required": true, 179 | "schema": { 180 | "type": "string" 181 | } 182 | } 183 | ], 184 | "responses": { 185 | "204": { 186 | "description": "ok", 187 | "schema": { 188 | "type": "string" 189 | } 190 | } 191 | } 192 | } 193 | }, 194 | "/v1/book/{id}": { 195 | "get": { 196 | "description": "Get book by given ID.", 197 | "consumes": [ 198 | "application/json" 199 | ], 200 | "produces": [ 201 | "application/json" 202 | ], 203 | "tags": [ 204 | "Book" 205 | ], 206 | "summary": "get book by given ID", 207 | "parameters": [ 208 | { 209 | "type": "string", 210 | "description": "Book ID", 211 | "name": "id", 212 | "in": "path", 213 | "required": true 214 | } 215 | ], 216 | "responses": { 217 | "200": { 218 | "description": "OK", 219 | "schema": { 220 | "$ref": "#/definitions/models.Book" 221 | } 222 | } 223 | } 224 | } 225 | }, 226 | "/v1/books": { 227 | "get": { 228 | "description": "Get all exists books.", 229 | "consumes": [ 230 | "application/json" 231 | ], 232 | "produces": [ 233 | "application/json" 234 | ], 235 | "tags": [ 236 | "Books" 237 | ], 238 | "summary": "get all exists books", 239 | "responses": { 240 | "200": { 241 | "description": "OK", 242 | "schema": { 243 | "type": "array", 244 | "items": { 245 | "$ref": "#/definitions/models.Book" 246 | } 247 | } 248 | } 249 | } 250 | } 251 | }, 252 | "/v1/token/new": { 253 | "get": { 254 | "description": "Create a new access token.", 255 | "consumes": [ 256 | "application/json" 257 | ], 258 | "produces": [ 259 | "application/json" 260 | ], 261 | "tags": [ 262 | "Token" 263 | ], 264 | "summary": "create a new access token", 265 | "responses": { 266 | "200": { 267 | "description": "ok", 268 | "schema": { 269 | "type": "string" 270 | } 271 | } 272 | } 273 | } 274 | } 275 | }, 276 | "definitions": { 277 | "models.Book": { 278 | "type": "object", 279 | "required": [ 280 | "author", 281 | "book_attrs", 282 | "book_status", 283 | "id", 284 | "title", 285 | "user_id" 286 | ], 287 | "properties": { 288 | "author": { 289 | "type": "string", 290 | "maxLength": 255 291 | }, 292 | "book_attrs": { 293 | "$ref": "#/definitions/models.BookAttrs" 294 | }, 295 | "book_status": { 296 | "type": "integer" 297 | }, 298 | "created_at": { 299 | "type": "string" 300 | }, 301 | "id": { 302 | "type": "string" 303 | }, 304 | "title": { 305 | "type": "string", 306 | "maxLength": 255 307 | }, 308 | "updated_at": { 309 | "type": "string" 310 | }, 311 | "user_id": { 312 | "type": "string" 313 | } 314 | } 315 | }, 316 | "models.BookAttrs": { 317 | "type": "object", 318 | "properties": { 319 | "description": { 320 | "type": "string" 321 | }, 322 | "picture": { 323 | "type": "string" 324 | }, 325 | "rating": { 326 | "type": "integer", 327 | "maximum": 10, 328 | "minimum": 1 329 | } 330 | } 331 | } 332 | }, 333 | "securityDefinitions": { 334 | "ApiKeyAuth": { 335 | "type": "apiKey", 336 | "name": "Authorization", 337 | "in": "header" 338 | } 339 | } 340 | }` 341 | 342 | // SwaggerInfo holds exported Swagger Info so clients can modify it 343 | var SwaggerInfo = &swag.Spec{ 344 | Version: "1.0", 345 | Host: "", 346 | BasePath: "/api", 347 | Schemes: []string{}, 348 | Title: "API", 349 | Description: "This is an auto-generated API Docs.", 350 | InfoInstanceName: "swagger", 351 | SwaggerTemplate: docTemplate, 352 | } 353 | 354 | func init() { 355 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 356 | } 357 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is an auto-generated API Docs.", 5 | "title": "API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "API Support", 9 | "email": "your@mail.com" 10 | }, 11 | "license": { 12 | "name": "Apache 2.0", 13 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 14 | }, 15 | "version": "1.0" 16 | }, 17 | "basePath": "/api", 18 | "paths": { 19 | "/v1/book": { 20 | "put": { 21 | "security": [ 22 | { 23 | "ApiKeyAuth": [] 24 | } 25 | ], 26 | "description": "Update book.", 27 | "consumes": [ 28 | "application/json" 29 | ], 30 | "produces": [ 31 | "application/json" 32 | ], 33 | "tags": [ 34 | "Book" 35 | ], 36 | "summary": "update book", 37 | "parameters": [ 38 | { 39 | "description": "Book ID", 40 | "name": "id", 41 | "in": "body", 42 | "required": true, 43 | "schema": { 44 | "type": "string" 45 | } 46 | }, 47 | { 48 | "description": "Title", 49 | "name": "title", 50 | "in": "body", 51 | "required": true, 52 | "schema": { 53 | "type": "string" 54 | } 55 | }, 56 | { 57 | "description": "Author", 58 | "name": "author", 59 | "in": "body", 60 | "required": true, 61 | "schema": { 62 | "type": "string" 63 | } 64 | }, 65 | { 66 | "description": "Book status", 67 | "name": "book_status", 68 | "in": "body", 69 | "required": true, 70 | "schema": { 71 | "type": "integer" 72 | } 73 | }, 74 | { 75 | "description": "Book attributes", 76 | "name": "book_attrs", 77 | "in": "body", 78 | "required": true, 79 | "schema": { 80 | "$ref": "#/definitions/models.BookAttrs" 81 | } 82 | } 83 | ], 84 | "responses": { 85 | "201": { 86 | "description": "ok", 87 | "schema": { 88 | "type": "string" 89 | } 90 | } 91 | } 92 | }, 93 | "post": { 94 | "security": [ 95 | { 96 | "ApiKeyAuth": [] 97 | } 98 | ], 99 | "description": "Create a new book.", 100 | "consumes": [ 101 | "application/json" 102 | ], 103 | "produces": [ 104 | "application/json" 105 | ], 106 | "tags": [ 107 | "Book" 108 | ], 109 | "summary": "create a new book", 110 | "parameters": [ 111 | { 112 | "description": "Title", 113 | "name": "title", 114 | "in": "body", 115 | "required": true, 116 | "schema": { 117 | "type": "string" 118 | } 119 | }, 120 | { 121 | "description": "Author", 122 | "name": "author", 123 | "in": "body", 124 | "required": true, 125 | "schema": { 126 | "type": "string" 127 | } 128 | }, 129 | { 130 | "description": "Book attributes", 131 | "name": "book_attrs", 132 | "in": "body", 133 | "required": true, 134 | "schema": { 135 | "$ref": "#/definitions/models.BookAttrs" 136 | } 137 | } 138 | ], 139 | "responses": { 140 | "200": { 141 | "description": "OK", 142 | "schema": { 143 | "$ref": "#/definitions/models.Book" 144 | } 145 | } 146 | } 147 | }, 148 | "delete": { 149 | "security": [ 150 | { 151 | "ApiKeyAuth": [] 152 | } 153 | ], 154 | "description": "Delete book by given ID.", 155 | "consumes": [ 156 | "application/json" 157 | ], 158 | "produces": [ 159 | "application/json" 160 | ], 161 | "tags": [ 162 | "Book" 163 | ], 164 | "summary": "delete book by given ID", 165 | "parameters": [ 166 | { 167 | "description": "Book ID", 168 | "name": "id", 169 | "in": "body", 170 | "required": true, 171 | "schema": { 172 | "type": "string" 173 | } 174 | } 175 | ], 176 | "responses": { 177 | "204": { 178 | "description": "ok", 179 | "schema": { 180 | "type": "string" 181 | } 182 | } 183 | } 184 | } 185 | }, 186 | "/v1/book/{id}": { 187 | "get": { 188 | "description": "Get book by given ID.", 189 | "consumes": [ 190 | "application/json" 191 | ], 192 | "produces": [ 193 | "application/json" 194 | ], 195 | "tags": [ 196 | "Book" 197 | ], 198 | "summary": "get book by given ID", 199 | "parameters": [ 200 | { 201 | "type": "string", 202 | "description": "Book ID", 203 | "name": "id", 204 | "in": "path", 205 | "required": true 206 | } 207 | ], 208 | "responses": { 209 | "200": { 210 | "description": "OK", 211 | "schema": { 212 | "$ref": "#/definitions/models.Book" 213 | } 214 | } 215 | } 216 | } 217 | }, 218 | "/v1/books": { 219 | "get": { 220 | "description": "Get all exists books.", 221 | "consumes": [ 222 | "application/json" 223 | ], 224 | "produces": [ 225 | "application/json" 226 | ], 227 | "tags": [ 228 | "Books" 229 | ], 230 | "summary": "get all exists books", 231 | "responses": { 232 | "200": { 233 | "description": "OK", 234 | "schema": { 235 | "type": "array", 236 | "items": { 237 | "$ref": "#/definitions/models.Book" 238 | } 239 | } 240 | } 241 | } 242 | } 243 | }, 244 | "/v1/token/new": { 245 | "get": { 246 | "description": "Create a new access token.", 247 | "consumes": [ 248 | "application/json" 249 | ], 250 | "produces": [ 251 | "application/json" 252 | ], 253 | "tags": [ 254 | "Token" 255 | ], 256 | "summary": "create a new access token", 257 | "responses": { 258 | "200": { 259 | "description": "ok", 260 | "schema": { 261 | "type": "string" 262 | } 263 | } 264 | } 265 | } 266 | } 267 | }, 268 | "definitions": { 269 | "models.Book": { 270 | "type": "object", 271 | "required": [ 272 | "author", 273 | "book_attrs", 274 | "book_status", 275 | "id", 276 | "title", 277 | "user_id" 278 | ], 279 | "properties": { 280 | "author": { 281 | "type": "string", 282 | "maxLength": 255 283 | }, 284 | "book_attrs": { 285 | "$ref": "#/definitions/models.BookAttrs" 286 | }, 287 | "book_status": { 288 | "type": "integer" 289 | }, 290 | "created_at": { 291 | "type": "string" 292 | }, 293 | "id": { 294 | "type": "string" 295 | }, 296 | "title": { 297 | "type": "string", 298 | "maxLength": 255 299 | }, 300 | "updated_at": { 301 | "type": "string" 302 | }, 303 | "user_id": { 304 | "type": "string" 305 | } 306 | } 307 | }, 308 | "models.BookAttrs": { 309 | "type": "object", 310 | "properties": { 311 | "description": { 312 | "type": "string" 313 | }, 314 | "picture": { 315 | "type": "string" 316 | }, 317 | "rating": { 318 | "type": "integer", 319 | "maximum": 10, 320 | "minimum": 1 321 | } 322 | } 323 | } 324 | }, 325 | "securityDefinitions": { 326 | "ApiKeyAuth": { 327 | "type": "apiKey", 328 | "name": "Authorization", 329 | "in": "header" 330 | } 331 | } 332 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /api 2 | definitions: 3 | models.Book: 4 | properties: 5 | author: 6 | maxLength: 255 7 | type: string 8 | book_attrs: 9 | $ref: '#/definitions/models.BookAttrs' 10 | book_status: 11 | type: integer 12 | created_at: 13 | type: string 14 | id: 15 | type: string 16 | title: 17 | maxLength: 255 18 | type: string 19 | updated_at: 20 | type: string 21 | user_id: 22 | type: string 23 | required: 24 | - author 25 | - book_attrs 26 | - book_status 27 | - id 28 | - title 29 | - user_id 30 | type: object 31 | models.BookAttrs: 32 | properties: 33 | description: 34 | type: string 35 | picture: 36 | type: string 37 | rating: 38 | maximum: 10 39 | minimum: 1 40 | type: integer 41 | type: object 42 | info: 43 | contact: 44 | email: your@mail.com 45 | name: API Support 46 | description: This is an auto-generated API Docs. 47 | license: 48 | name: Apache 2.0 49 | url: http://www.apache.org/licenses/LICENSE-2.0.html 50 | termsOfService: http://swagger.io/terms/ 51 | title: API 52 | version: "1.0" 53 | paths: 54 | /v1/book: 55 | delete: 56 | consumes: 57 | - application/json 58 | description: Delete book by given ID. 59 | parameters: 60 | - description: Book ID 61 | in: body 62 | name: id 63 | required: true 64 | schema: 65 | type: string 66 | produces: 67 | - application/json 68 | responses: 69 | "204": 70 | description: ok 71 | schema: 72 | type: string 73 | security: 74 | - ApiKeyAuth: [] 75 | summary: delete book by given ID 76 | tags: 77 | - Book 78 | post: 79 | consumes: 80 | - application/json 81 | description: Create a new book. 82 | parameters: 83 | - description: Title 84 | in: body 85 | name: title 86 | required: true 87 | schema: 88 | type: string 89 | - description: Author 90 | in: body 91 | name: author 92 | required: true 93 | schema: 94 | type: string 95 | - description: Book attributes 96 | in: body 97 | name: book_attrs 98 | required: true 99 | schema: 100 | $ref: '#/definitions/models.BookAttrs' 101 | produces: 102 | - application/json 103 | responses: 104 | "200": 105 | description: OK 106 | schema: 107 | $ref: '#/definitions/models.Book' 108 | security: 109 | - ApiKeyAuth: [] 110 | summary: create a new book 111 | tags: 112 | - Book 113 | put: 114 | consumes: 115 | - application/json 116 | description: Update book. 117 | parameters: 118 | - description: Book ID 119 | in: body 120 | name: id 121 | required: true 122 | schema: 123 | type: string 124 | - description: Title 125 | in: body 126 | name: title 127 | required: true 128 | schema: 129 | type: string 130 | - description: Author 131 | in: body 132 | name: author 133 | required: true 134 | schema: 135 | type: string 136 | - description: Book status 137 | in: body 138 | name: book_status 139 | required: true 140 | schema: 141 | type: integer 142 | - description: Book attributes 143 | in: body 144 | name: book_attrs 145 | required: true 146 | schema: 147 | $ref: '#/definitions/models.BookAttrs' 148 | produces: 149 | - application/json 150 | responses: 151 | "201": 152 | description: ok 153 | schema: 154 | type: string 155 | security: 156 | - ApiKeyAuth: [] 157 | summary: update book 158 | tags: 159 | - Book 160 | /v1/book/{id}: 161 | get: 162 | consumes: 163 | - application/json 164 | description: Get book by given ID. 165 | parameters: 166 | - description: Book ID 167 | in: path 168 | name: id 169 | required: true 170 | type: string 171 | produces: 172 | - application/json 173 | responses: 174 | "200": 175 | description: OK 176 | schema: 177 | $ref: '#/definitions/models.Book' 178 | summary: get book by given ID 179 | tags: 180 | - Book 181 | /v1/books: 182 | get: 183 | consumes: 184 | - application/json 185 | description: Get all exists books. 186 | produces: 187 | - application/json 188 | responses: 189 | "200": 190 | description: OK 191 | schema: 192 | items: 193 | $ref: '#/definitions/models.Book' 194 | type: array 195 | summary: get all exists books 196 | tags: 197 | - Books 198 | /v1/token/new: 199 | get: 200 | consumes: 201 | - application/json 202 | description: Create a new access token. 203 | produces: 204 | - application/json 205 | responses: 206 | "200": 207 | description: ok 208 | schema: 209 | type: string 210 | summary: create a new access token 211 | tags: 212 | - Token 213 | securityDefinitions: 214 | ApiKeyAuth: 215 | in: header 216 | name: Authorization 217 | type: apiKey 218 | swagger: "2.0" 219 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/koddr/tutorial-go-fiber-rest-api 2 | 3 | go 1.21 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/arsmn/fiber-swagger/v2 v2.31.1 8 | github.com/go-playground/validator/v10 v10.15.5 9 | github.com/gofiber/fiber/v2 v2.52.5 10 | github.com/gofiber/jwt/v2 v2.2.7 11 | github.com/golang-jwt/jwt v3.2.2+incompatible 12 | github.com/google/uuid v1.5.0 13 | github.com/jackc/pgx/v4 v4.18.2 14 | github.com/jmoiron/sqlx v1.3.5 15 | github.com/joho/godotenv v1.5.1 16 | github.com/stretchr/testify v1.8.4 17 | github.com/swaggo/swag v1.16.2 18 | ) 19 | 20 | require ( 21 | github.com/KyleBanks/depth v1.2.1 // indirect 22 | github.com/andybalholm/brotli v1.0.5 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 25 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 26 | github.com/go-openapi/jsonreference v0.20.2 // indirect 27 | github.com/go-openapi/spec v0.20.9 // indirect 28 | github.com/go-openapi/swag v0.22.4 // indirect 29 | github.com/go-playground/locales v0.14.1 // indirect 30 | github.com/go-playground/universal-translator v0.18.1 // indirect 31 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 32 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 33 | github.com/jackc/pgconn v1.14.3 // indirect 34 | github.com/jackc/pgio v1.0.0 // indirect 35 | github.com/jackc/pgpassfile v1.0.0 // indirect 36 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 37 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 38 | github.com/jackc/pgtype v1.14.0 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/klauspost/compress v1.17.0 // indirect 41 | github.com/leodido/go-urn v1.2.4 // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/mattn/go-colorable v0.1.13 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/mattn/go-runewidth v0.0.15 // indirect 46 | github.com/pmezard/go-difflib v1.0.0 // indirect 47 | github.com/rivo/uniseg v0.4.4 // indirect 48 | github.com/rogpeppe/go-internal v1.11.0 // indirect 49 | github.com/swaggo/files v1.0.1 // indirect 50 | github.com/valyala/bytebufferpool v1.0.0 // indirect 51 | github.com/valyala/fasthttp v1.51.0 // indirect 52 | github.com/valyala/tcplisten v1.0.0 // indirect 53 | golang.org/x/crypto v0.36.0 // indirect 54 | golang.org/x/net v0.38.0 // indirect 55 | golang.org/x/sys v0.31.0 // indirect 56 | golang.org/x/text v0.23.0 // indirect 57 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 58 | gopkg.in/yaml.v3 v3.0.1 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 3 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 4 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 5 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 6 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 8 | github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= 9 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 10 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 11 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 12 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 13 | github.com/arsmn/fiber-swagger/v2 v2.31.1 h1:VmX+flXiGGNqLX3loMEEzL3BMOZFSPwBEWR04GA6Mco= 14 | github.com/arsmn/fiber-swagger/v2 v2.31.1/go.mod h1:ZHhMprtB3M6jd2mleG03lPGhHH0lk9u3PtfWS1cBhMA= 15 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 16 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 17 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 18 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 20 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 21 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 26 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 27 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 28 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 29 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 30 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 31 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 32 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 33 | github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= 34 | github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= 35 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 36 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 37 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 38 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 39 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 40 | github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= 41 | github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 42 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 43 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 44 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 45 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 46 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 47 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 48 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 49 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 50 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 51 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 52 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 53 | github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= 54 | github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 55 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 56 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 57 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 58 | github.com/gofiber/fiber/v2 v2.17.0/go.mod h1:iftruuHGkRYGEXVISmdD7HTYWyfS2Bh+Dkfq4n/1Owg= 59 | github.com/gofiber/fiber/v2 v2.31.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4= 60 | github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= 61 | github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 62 | github.com/gofiber/jwt/v2 v2.2.7 h1:MgXZV+ak+FiRVepD3btHBxWcyxlFzTDGXJv78dU1sIE= 63 | github.com/gofiber/jwt/v2 v2.2.7/go.mod h1:yaOHLccYXJidk1HX/EiIdIL+Z1xmY2wnIv6hgViw384= 64 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 65 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 66 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 67 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 68 | github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 69 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 70 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 71 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 72 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 73 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 74 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 75 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 76 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 77 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 78 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 79 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 80 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 81 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 82 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 83 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 84 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 85 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 86 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 87 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 88 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 89 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 90 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 91 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 92 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 93 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 94 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 95 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 96 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 97 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 98 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 99 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 100 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 101 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 102 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 103 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 104 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 105 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 106 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 107 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 108 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 109 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 110 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 111 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 112 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= 113 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 114 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 115 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 116 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 117 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 118 | github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= 119 | github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 120 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 121 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 122 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 123 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 124 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 125 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 126 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 127 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 128 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 129 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 130 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 131 | github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 132 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 133 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 134 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 135 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 136 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 137 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 138 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 139 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 140 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 141 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 142 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 143 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 144 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 145 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 146 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 147 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 148 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 149 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 150 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 151 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 152 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 153 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 154 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 155 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 156 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 157 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 158 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 159 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 160 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 161 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 162 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 163 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 164 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 165 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 166 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 167 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 168 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 169 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 170 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 171 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 172 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 173 | github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= 174 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 175 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 176 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 177 | github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 178 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 179 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 180 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 181 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 182 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 183 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 184 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 185 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 186 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 187 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 188 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 189 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 190 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 191 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 192 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 193 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 194 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 195 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 196 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 197 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 198 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 199 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 200 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 201 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 202 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 203 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 204 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 205 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 206 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 207 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 208 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 209 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 210 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 211 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 212 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 213 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 214 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 215 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 216 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 217 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 218 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= 219 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 220 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 221 | github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= 222 | github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= 223 | github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= 224 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 225 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 226 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 227 | github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= 228 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 229 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 230 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 231 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 232 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 233 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 234 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 235 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 236 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 237 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 238 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 239 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 240 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 241 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 242 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 243 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 244 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 245 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 246 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 247 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 248 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 249 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 250 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 251 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 252 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 253 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 254 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 255 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 256 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 257 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 258 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 259 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 260 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 261 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 262 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 263 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 264 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 265 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 266 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 267 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 268 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 269 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 270 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 271 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 272 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 273 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 274 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 275 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 276 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 277 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 278 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 279 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 280 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 281 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 282 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 283 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 284 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 285 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 286 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 287 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 288 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 289 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 290 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 301 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 302 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 303 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 304 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 305 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 306 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 307 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 308 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 309 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 310 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 311 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 312 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 313 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 314 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 315 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 316 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 317 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 318 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 319 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 320 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 321 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 322 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 323 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 324 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 325 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 326 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 327 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 328 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 329 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 330 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 331 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 332 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 333 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 334 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 335 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 336 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 337 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 338 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 339 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 340 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 341 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 342 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 343 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 344 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 345 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 346 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 347 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 348 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 349 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 350 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 351 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 352 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 353 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 354 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 355 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 356 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 357 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 358 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 359 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 360 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/configs" 6 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/middleware" 7 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/routes" 8 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils" 9 | 10 | _ "github.com/joho/godotenv/autoload" // load .env file automatically 11 | _ "github.com/koddr/tutorial-go-fiber-rest-api/docs" // load API Docs files (Swagger) 12 | ) 13 | 14 | // @title API 15 | // @version 1.0 16 | // @description This is an auto-generated API Docs. 17 | // @termsOfService http://swagger.io/terms/ 18 | // @contact.name API Support 19 | // @contact.email your@mail.com 20 | // @license.name Apache 2.0 21 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 22 | // @securityDefinitions.apikey ApiKeyAuth 23 | // @in header 24 | // @name Authorization 25 | // @BasePath /api 26 | func main() { 27 | // Define Fiber config. 28 | config := configs.FiberConfig() 29 | 30 | // Define a new Fiber app with config. 31 | app := fiber.New(config) 32 | 33 | // Middlewares. 34 | middleware.FiberMiddleware(app) // Register Fiber's middleware for app. 35 | 36 | // Routes. 37 | routes.SwaggerRoute(app) // Register a route for API Docs (Swagger). 38 | routes.PublicRoutes(app) // Register a public routes for app. 39 | routes.PrivateRoutes(app) // Register a private routes for app. 40 | routes.NotFoundRoute(app) // Register route for 404 Error. 41 | 42 | // Start server (with graceful shutdown). 43 | utils.StartServer(app) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/PROJECT_SPECIFIC.md: -------------------------------------------------------------------------------- 1 | # ./pkg 2 | 3 | **Folder with project specific functionality**. This directory contains all the project-specific code tailored only for your business use case, like _configs_, _middleware_, _routes_, _utils_ or else. 4 | 5 | - `./pkg/configs` folder for configuration functions 6 | - `./pkg/middleware` folder for add middleware (Fiber and yours) 7 | - `./pkg/routes` folder for describe routes of your project 8 | - `./pkg/repository` folder for describe `const` of your project 9 | - `./pkg/utils` folder with utility functions (server starter, error checker, etc) 10 | -------------------------------------------------------------------------------- /pkg/configs/fiber_config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | // FiberConfig func for configuration Fiber app. 12 | // See: https://docs.gofiber.io/api/fiber#config 13 | func FiberConfig() fiber.Config { 14 | // Define server settings. 15 | readTimeoutSecondsCount, _ := strconv.Atoi(os.Getenv("SERVER_READ_TIMEOUT")) 16 | 17 | // Return Fiber configuration. 18 | return fiber.Config{ 19 | ReadTimeout: time.Second * time.Duration(readTimeoutSecondsCount), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/middleware/fiber_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/fiber/v2/middleware/cors" 6 | "github.com/gofiber/fiber/v2/middleware/logger" 7 | ) 8 | 9 | // FiberMiddleware provide Fiber's built-in middlewares. 10 | // See: https://docs.gofiber.io/api/middleware 11 | func FiberMiddleware(a *fiber.App) { 12 | a.Use( 13 | // Add CORS to each route. 14 | cors.New(), 15 | // Add simple logger. 16 | logger.New(), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/middleware/jwt_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | 8 | jwtMiddleware "github.com/gofiber/jwt/v2" 9 | ) 10 | 11 | // JWTProtected func for specify routes group with JWT authentication. 12 | // See: https://github.com/gofiber/jwt 13 | func JWTProtected() func(*fiber.Ctx) error { 14 | // Create config for JWT authentication middleware. 15 | config := jwtMiddleware.Config{ 16 | SigningKey: []byte(os.Getenv("JWT_SECRET_KEY")), 17 | ContextKey: "jwt", // used in private routes 18 | ErrorHandler: jwtError, 19 | } 20 | 21 | return jwtMiddleware.New(config) 22 | } 23 | 24 | func jwtError(c *fiber.Ctx, err error) error { 25 | // Return status 401 and failed authentication error. 26 | if err.Error() == "Missing or malformed JWT" { 27 | return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 28 | "error": true, 29 | "msg": err.Error(), 30 | }) 31 | } 32 | 33 | // Return status 401 and failed authentication error. 34 | return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ 35 | "error": true, 36 | "msg": err.Error(), 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/routes/not_found_route.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import "github.com/gofiber/fiber/v2" 4 | 5 | // NotFoundRoute func for describe 404 Error route. 6 | func NotFoundRoute(a *fiber.App) { 7 | // Register new special route. 8 | a.Use( 9 | // Anonymous function. 10 | func(c *fiber.Ctx) error { 11 | // Return HTTP 404 status and JSON response. 12 | return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ 13 | "error": true, 14 | "msg": "sorry, endpoint is not found", 15 | }) 16 | }, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/routes/private_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/koddr/tutorial-go-fiber-rest-api/app/controllers" 6 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/middleware" 7 | ) 8 | 9 | // PrivateRoutes func for describe group of private routes. 10 | func PrivateRoutes(a *fiber.App) { 11 | // Create routes group. 12 | route := a.Group("/api/v1") 13 | 14 | // Routes for POST method: 15 | route.Post("/book", middleware.JWTProtected(), controllers.CreateBook) // create a new book 16 | 17 | // Routes for PUT method: 18 | route.Put("/book", middleware.JWTProtected(), controllers.UpdateBook) // update one book by ID 19 | 20 | // Routes for DELETE method: 21 | route.Delete("/book", middleware.JWTProtected(), controllers.DeleteBook) // delete one book by ID 22 | } 23 | -------------------------------------------------------------------------------- /pkg/routes/private_routes_test.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "io" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gofiber/fiber/v2" 10 | "github.com/joho/godotenv" 11 | "github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestPrivateRoutes(t *testing.T) { 16 | // Load .env.test file from the root folder. 17 | if err := godotenv.Load("../../.env.test"); err != nil { 18 | panic(err) 19 | } 20 | 21 | // Create a sample data string. 22 | dataString := `{"id": "00000000-0000-0000-0000-000000000000"}` 23 | 24 | // Create access token. 25 | token, err := utils.GenerateNewAccessToken() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | // Define a structure for specifying input and output data of a single test case. 31 | tests := []struct { 32 | description string 33 | route string // input route 34 | method string // input method 35 | tokenString string // input token 36 | body io.Reader 37 | expectedError bool 38 | expectedCode int 39 | }{ 40 | { 41 | description: "delete book without JWT and body", 42 | route: "/api/v1/book", 43 | method: "DELETE", 44 | tokenString: "", 45 | body: nil, 46 | expectedError: false, 47 | expectedCode: 400, 48 | }, 49 | { 50 | description: "delete book without right credentials", 51 | route: "/api/v1/book", 52 | method: "DELETE", 53 | tokenString: "Bearer " + token, 54 | body: strings.NewReader(dataString), 55 | expectedError: false, 56 | expectedCode: 403, 57 | }, 58 | { 59 | description: "delete book with credentials", 60 | route: "/api/v1/book", 61 | method: "DELETE", 62 | tokenString: "Bearer " + token, 63 | body: strings.NewReader(dataString), 64 | expectedError: false, 65 | expectedCode: 404, 66 | }, 67 | } 68 | 69 | // Define a new Fiber app. 70 | app := fiber.New() 71 | 72 | // Define routes. 73 | PrivateRoutes(app) 74 | 75 | // Iterate through test single test cases 76 | for _, test := range tests { 77 | // Create a new http request with the route from the test case. 78 | req := httptest.NewRequest(test.method, test.route, test.body) 79 | req.Header.Set("Authorization", test.tokenString) 80 | req.Header.Set("Content-Type", "application/json") 81 | 82 | // Perform the request plain with the app. 83 | resp, err := app.Test(req, -1) // the -1 disables request latency 84 | 85 | // Verify, that no error occurred, that is not expected 86 | assert.Equalf(t, test.expectedError, err != nil, test.description) 87 | 88 | // As expected errors lead to broken responses, 89 | // the next test case needs to be processed. 90 | if test.expectedError { 91 | continue 92 | } 93 | 94 | // Verify, if the status code is as expected. 95 | assert.Equalf(t, test.expectedCode, resp.StatusCode, test.description) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/routes/public_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/koddr/tutorial-go-fiber-rest-api/app/controllers" 6 | ) 7 | 8 | // PublicRoutes func for describe group of public routes. 9 | func PublicRoutes(a *fiber.App) { 10 | // Create routes group. 11 | route := a.Group("/api/v1") 12 | 13 | // Routes for GET method: 14 | route.Get("/books", controllers.GetBooks) // get list of all books 15 | route.Get("/book/:id", controllers.GetBook) // get one book by ID 16 | route.Get("/token/new", controllers.GetNewAccessToken) // create a new access tokens 17 | } 18 | -------------------------------------------------------------------------------- /pkg/routes/public_routes_test.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/google/uuid" 9 | "github.com/joho/godotenv" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestPublicRoutes(t *testing.T) { 14 | // Load .env.test file from the root folder 15 | if err := godotenv.Load("../../.env.test"); err != nil { 16 | panic(err) 17 | } 18 | 19 | // Define a structure for specifying input and output data of a single test case. 20 | tests := []struct { 21 | description string 22 | route string // input route 23 | expectedError bool 24 | expectedCode int 25 | }{ 26 | { 27 | description: "get book by ID", 28 | route: "/api/v1/book/" + uuid.New().String(), 29 | expectedError: false, 30 | expectedCode: 404, 31 | }, 32 | { 33 | description: "get book by invalid ID (non UUID)", 34 | route: "/api/v1/book/123456", 35 | expectedError: false, 36 | expectedCode: 500, 37 | }, 38 | } 39 | 40 | // Define Fiber app. 41 | app := fiber.New() 42 | 43 | // Define routes. 44 | PublicRoutes(app) 45 | 46 | // Iterate through test single test cases 47 | for _, test := range tests { 48 | // Create a new http request with the route from the test case. 49 | req := httptest.NewRequest("GET", test.route, nil) 50 | req.Header.Set("Content-Type", "application/json") 51 | 52 | // Perform the request plain with the app. 53 | resp, err := app.Test(req, -1) // the -1 disables request latency 54 | 55 | // Verify, that no error occurred, that is not expected 56 | assert.Equalf(t, test.expectedError, err != nil, test.description) 57 | 58 | // As expected errors lead to broken responses, 59 | // the next test case needs to be processed. 60 | if test.expectedError { 61 | continue 62 | } 63 | 64 | // Verify, if the status code is as expected. 65 | assert.Equalf(t, test.expectedCode, resp.StatusCode, test.description) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/routes/swagger_route.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | 6 | swagger "github.com/arsmn/fiber-swagger/v2" 7 | ) 8 | 9 | // SwaggerRoute func for describe group of API Docs routes. 10 | func SwaggerRoute(a *fiber.App) { 11 | // Create routes group. 12 | route := a.Group("/swagger") 13 | 14 | // Routes for GET method: 15 | route.Get("*", swagger.HandlerDefault) // get one user by ID 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/jwt_generator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/golang-jwt/jwt" 9 | ) 10 | 11 | // GenerateNewAccessToken func for generate a new Access token. 12 | func GenerateNewAccessToken() (string, error) { 13 | // Set secret key from .env file. 14 | secret := os.Getenv("JWT_SECRET_KEY") 15 | 16 | // Set expires minutes count for secret key from .env file. 17 | minutesCount, _ := strconv.Atoi(os.Getenv("JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT")) 18 | 19 | // Create a new claims. 20 | claims := jwt.MapClaims{} 21 | 22 | // Set public claims: 23 | claims["exp"] = time.Now().Add(time.Minute * time.Duration(minutesCount)).Unix() 24 | 25 | // Create a new JWT access token with claims. 26 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 27 | 28 | // Generate token. 29 | t, err := token.SignedString([]byte(secret)) 30 | if err != nil { 31 | // Return error, it JWT token generation failed. 32 | return "", err 33 | } 34 | 35 | return t, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/utils/jwt_parser.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/golang-jwt/jwt" 9 | ) 10 | 11 | // TokenMetadata struct to describe metadata in JWT. 12 | type TokenMetadata struct { 13 | Expires int64 14 | } 15 | 16 | // ExtractTokenMetadata func to extract metadata from JWT. 17 | func ExtractTokenMetadata(c *fiber.Ctx) (*TokenMetadata, error) { 18 | token, err := verifyToken(c) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | // Setting and checking token and credentials. 24 | claims, ok := token.Claims.(jwt.MapClaims) 25 | if ok && token.Valid { 26 | // Expires time. 27 | expires := int64(claims["exp"].(float64)) 28 | 29 | return &TokenMetadata{ 30 | Expires: expires, 31 | }, nil 32 | } 33 | 34 | return nil, err 35 | } 36 | 37 | func extractToken(c *fiber.Ctx) string { 38 | bearToken := c.Get("Authorization") 39 | 40 | // Normally Authorization HTTP header. 41 | onlyToken := strings.Split(bearToken, " ") 42 | if len(onlyToken) == 2 { 43 | return onlyToken[1] 44 | } 45 | 46 | return "" 47 | } 48 | 49 | func verifyToken(c *fiber.Ctx) (*jwt.Token, error) { 50 | tokenString := extractToken(c) 51 | 52 | token, err := jwt.Parse(tokenString, jwtKeyFunc) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return token, nil 58 | } 59 | 60 | func jwtKeyFunc(token *jwt.Token) (interface{}, error) { 61 | return []byte(os.Getenv("JWT_SECRET_KEY")), nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/utils/start_server.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | // StartServerWithGracefulShutdown function for starting server with a graceful shutdown. 12 | func StartServerWithGracefulShutdown(a *fiber.App) { 13 | // Create channel for idle connections. 14 | idleConnsClosed := make(chan struct{}) 15 | 16 | go func() { 17 | sigint := make(chan os.Signal, 1) 18 | signal.Notify(sigint, os.Interrupt) // Catch OS signals. 19 | <-sigint 20 | 21 | // Received an interrupt signal, shutdown. 22 | if err := a.Shutdown(); err != nil { 23 | // Error from closing listeners, or context timeout: 24 | log.Printf("Oops... Server is not shutting down! Reason: %v", err) 25 | } 26 | 27 | close(idleConnsClosed) 28 | }() 29 | 30 | // Run server. 31 | if err := a.Listen(os.Getenv("SERVER_URL")); err != nil { 32 | log.Printf("Oops... Server is not running! Reason: %v", err) 33 | } 34 | 35 | <-idleConnsClosed 36 | } 37 | 38 | // StartServer func for starting a simple server. 39 | func StartServer(a *fiber.App) { 40 | // Run server. 41 | if err := a.Listen(os.Getenv("SERVER_URL")); err != nil { 42 | log.Printf("Oops... Server is not running! Reason: %v", err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/utils/validator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | // NewValidator func for create a new validator for model fields. 9 | func NewValidator() *validator.Validate { 10 | // Create a new validator for a Book model. 11 | validate := validator.New() 12 | 13 | // Custom validation for uuid.UUID fields. 14 | _ = validate.RegisterValidation("uuid", func(fl validator.FieldLevel) bool { 15 | field := fl.Field().String() 16 | if _, err := uuid.Parse(field); err != nil { 17 | return false // if there is an error, validation should return false 18 | } 19 | return true // if no error, validation should return true 20 | }) 21 | 22 | return validate 23 | } 24 | 25 | // ValidatorErrors func for show validation errors for each invalid fields. 26 | func ValidatorErrors(err error) map[string]string { 27 | // Define fields map. 28 | fields := map[string]string{} 29 | 30 | // Make error message for each invalid field. 31 | for _, err := range err.(validator.ValidationErrors) { 32 | fields[err.Field()] = err.Error() 33 | } 34 | 35 | return fields 36 | } 37 | -------------------------------------------------------------------------------- /platform/PLATFORM_LEVEL.md: -------------------------------------------------------------------------------- 1 | # ./platform 2 | 3 | **Folder with platform-level logic**. This directory contains all the platform-level logic that will build up the actual project, like _setting up the database_ or _cache server instance_ and _storing migrations_. 4 | 5 | - `./platform/database` folder with database configuration (by default, PostgreSQL) 6 | - `./platform/migrations` folder with migration files (used with [golang-migrate/migrate](https://github.com/golang-migrate/migrate) tool) 7 | -------------------------------------------------------------------------------- /platform/database/open_db_connection.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "github.com/koddr/tutorial-go-fiber-rest-api/app/queries" 4 | 5 | // Queries struct for collect all app queries. 6 | type Queries struct { 7 | *queries.BookQueries // load queries from Book model 8 | } 9 | 10 | // OpenDBConnection func for opening database connection. 11 | func OpenDBConnection() (*Queries, error) { 12 | // Define a new PostgreSQL connection. 13 | db, err := PostgreSQLConnection() 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return &Queries{ 19 | // Set queries from models: 20 | BookQueries: &queries.BookQueries{DB: db}, // from Book model 21 | }, nil 22 | } 23 | -------------------------------------------------------------------------------- /platform/database/postgres.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/jmoiron/sqlx" 10 | 11 | _ "github.com/jackc/pgx/v4/stdlib" // load pgx driver for PostgreSQL 12 | ) 13 | 14 | // PostgreSQLConnection func for connection to PostgreSQL database. 15 | func PostgreSQLConnection() (*sqlx.DB, error) { 16 | // Define database connection settings. 17 | maxConn, _ := strconv.Atoi(os.Getenv("DB_MAX_CONNECTIONS")) 18 | maxIdleConn, _ := strconv.Atoi(os.Getenv("DB_MAX_IDLE_CONNECTIONS")) 19 | maxLifetimeConn, _ := strconv.Atoi(os.Getenv("DB_MAX_LIFETIME_CONNECTIONS")) 20 | 21 | // Define database connection for PostgreSQL. 22 | db, err := sqlx.Connect("pgx", os.Getenv("DB_SERVER_URL")) 23 | if err != nil { 24 | return nil, fmt.Errorf("error, not connected to database, %w", err) 25 | } 26 | 27 | // Set database connection settings. 28 | db.SetMaxOpenConns(maxConn) // the default is 0 (unlimited) 29 | db.SetMaxIdleConns(maxIdleConn) // defaultMaxIdleConns = 2 30 | db.SetConnMaxLifetime(time.Duration(maxLifetimeConn)) // 0, connections are reused forever 31 | 32 | // Try to ping database. 33 | if err := db.Ping(); err != nil { 34 | defer db.Close() // close database connection 35 | return nil, fmt.Errorf("error, not sent ping to database, %w", err) 36 | } 37 | 38 | return db, nil 39 | } 40 | -------------------------------------------------------------------------------- /platform/migrations/000001_create_init_tables.down.sql: -------------------------------------------------------------------------------- 1 | -- Delete tables 2 | DROP TABLE IF EXISTS books; -------------------------------------------------------------------------------- /platform/migrations/000001_create_init_tables.up.sql: -------------------------------------------------------------------------------- 1 | -- Add UUID extension 2 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 3 | 4 | -- Set timezone 5 | -- For more information, please visit: 6 | -- https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 7 | SET TIMEZONE="Europe/Moscow"; 8 | 9 | -- Create books table 10 | CREATE TABLE books ( 11 | id UUID DEFAULT uuid_generate_v4 () PRIMARY KEY, 12 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW (), 13 | updated_at TIMESTAMP NULL, 14 | title VARCHAR (255) NOT NULL, 15 | author VARCHAR (255) NOT NULL, 16 | book_status INT NOT NULL, 17 | book_attrs JSONB NOT NULL 18 | ); 19 | 20 | -- Add indexes 21 | CREATE INDEX active_books ON books (title) WHERE book_status = 1; --------------------------------------------------------------------------------