├── .github └── workflows │ ├── deploy.yaml │ └── test.yaml ├── .gitignore ├── Dockerfile ├── adapter ├── http │ ├── graphql │ │ ├── productgraphqlservice │ │ │ ├── fetch.go │ │ │ └── new.go │ │ └── schema │ │ │ ├── config.go │ │ │ ├── pagination.go │ │ │ └── product.go │ ├── main.go │ └── rest │ │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ │ ├── middleware │ │ └── cors.go │ │ └── productservice │ │ ├── create.go │ │ ├── create_test.go │ │ ├── fetch.go │ │ ├── fetch_test.go │ │ └── new.go └── postgres │ ├── connector.go │ └── productrepository │ ├── create.go │ ├── create_test.go │ ├── fetch.go │ ├── fetch_test.go │ └── new.go ├── config.json ├── core ├── domain │ ├── mocks │ │ └── fakeproduct.go │ ├── pagination.go │ └── product.go ├── dto │ ├── pagination.go │ ├── pagination_test.go │ ├── product.go │ └── product_test.go └── usecase │ └── productusecase │ ├── create.go │ ├── create_test.go │ ├── fetch.go │ ├── fetch_test.go │ └── new.go ├── database └── migrations │ ├── 000001_create_product_table.down.sql │ └── 000001_create_product_table.up.sql ├── di └── product.go ├── fly.toml └── go.mod /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to fly.io app 2 | 3 | on: 4 | create: 5 | tags: 6 | - v* 7 | 8 | env: 9 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Config file access 18 | run: | 19 | rm -rf config.json 20 | touch config.json 21 | json='{"database": {"url": "://$DB_USER:$DB_PASS@$DB_HOST:$DB_PORT/"},"server": {"port": ""}}' 22 | echo "$json" > config.json 23 | sed -i -e 's/$DB_PORT/'${{ secrets.DB_PORT }}'/g' config.json 24 | sed -i -e 's/$DB_USER/'${{ secrets.DB_USER }}'/g' config.json 25 | sed -i -e 's/$DB_PASS/'${{ secrets.DB_PASS }}'/g' config.json 26 | sed -i -e 's/$DB_HOST/'${{ secrets.DB_HOST }}'/g' config.json 27 | cat config.json 28 | 29 | - uses: superfly/flyctl-actions/setup-flyctl@master 30 | - run: flyctl deploy --remote-only --detach -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Run test suite 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.18.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Download swaggo 18 | run: go install github.com/swaggo/swag/cmd/swag@v1.8.4 19 | - name: Install swaggo 20 | run: swag init -d adapter/http --parseDependency --parseInternal --parseDepth 3 -o adapter/http/rest/docs 21 | - name: Test 22 | run: go mod tidy && go test ./... -coverprofile=coverage.txt -covermode=atomic 23 | 24 | - name: Upload coverage report 25 | uses: codecov/codecov-action@v1.0.2 26 | with: 27 | token: 276e28b3-208f-4012-ac00-594ff09c65a3 28 | file: ./coverage.txt 29 | flags: unittests 30 | name: codecov-umbrella -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cover.html 2 | cover.out 3 | go.sum -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS builder 2 | ADD . /go/api 3 | WORKDIR /go/api 4 | 5 | RUN go install github.com/swaggo/swag/cmd/swag@v1.8.4 6 | RUN rm -rf deploy 7 | RUN mkdir deploy 8 | RUN swag init -d adapter/http --parseDependency --parseInternal --parseDepth 3 -o adapter/http/rest/docs 9 | RUN go mod tidy 10 | RUN CGO_ENABLED=0 go build -o goapp adapter/http/main.go 11 | RUN mv goapp ./deploy/goapp 12 | RUN mv adapter/http/rest/docs ./deploy/docs 13 | RUN mv config.json ./deploy/config.json 14 | RUN mv database ./deploy/database 15 | 16 | 17 | FROM alpine:3.7 AS production 18 | COPY --from=builder /go/api/deploy /api/ 19 | 20 | WORKDIR /api 21 | ENTRYPOINT ./goapp -------------------------------------------------------------------------------- /adapter/http/graphql/productgraphqlservice/fetch.go: -------------------------------------------------------------------------------- 1 | package productgraphqlservice 2 | 3 | import ( 4 | "github.com/boooscaaa/clean-go/core/dto" 5 | "github.com/graphql-go/graphql" 6 | ) 7 | 8 | func (service service) Fetch(params graphql.ResolveParams) (interface{}, error) { 9 | paginationRequest, _ := dto.FromValuePaginationGraphRequestParams(params) 10 | 11 | products, err := service.usecase.Fetch(paginationRequest) 12 | 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return products, nil 18 | } 19 | -------------------------------------------------------------------------------- /adapter/http/graphql/productgraphqlservice/new.go: -------------------------------------------------------------------------------- 1 | package productgraphqlservice 2 | 3 | import "github.com/boooscaaa/clean-go/core/domain" 4 | 5 | type service struct { 6 | usecase domain.ProductUseCase 7 | } 8 | 9 | // New returns contract implementation of ProductService 10 | func New(usecase domain.ProductUseCase) domain.PoductGraphQLService { 11 | return &service{ 12 | usecase: usecase, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /adapter/http/graphql/schema/config.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/boooscaaa/clean-go/core/domain" 7 | "github.com/graphql-go/graphql" 8 | ) 9 | 10 | func Config(productService domain.PoductGraphQLService) graphql.Schema { 11 | rootQuery := graphql.NewObject(graphql.ObjectConfig{ 12 | Name: "RootQuery", 13 | Fields: graphql.Fields{ 14 | "products": &graphql.Field{ 15 | Type: paginationProducts, 16 | Description: "Get all products with server pagination", 17 | Args: paginationRequestParams, 18 | Resolve: productService.Fetch, 19 | }, 20 | }, 21 | }) 22 | 23 | schema, _ := graphql.NewSchema(graphql.SchemaConfig{ 24 | Query: rootQuery, 25 | }) 26 | 27 | return schema 28 | } 29 | 30 | func ExecuteQuery(query string, schema graphql.Schema) *graphql.Result { 31 | result := graphql.Do(graphql.Params{ 32 | Schema: schema, 33 | RequestString: query, 34 | }) 35 | if len(result.Errors) > 0 { 36 | fmt.Printf("wrong result, unexpected errors: %v", result.Errors) 37 | } 38 | return result 39 | } 40 | -------------------------------------------------------------------------------- /adapter/http/graphql/schema/pagination.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "github.com/graphql-go/graphql" 4 | 5 | var paginationRequestParams = graphql.FieldConfigArgument{ 6 | "page": &graphql.ArgumentConfig{ 7 | Type: graphql.Int, 8 | }, 9 | "itemsPerPage": &graphql.ArgumentConfig{ 10 | Type: graphql.Int, 11 | }, 12 | "search": &graphql.ArgumentConfig{ 13 | Type: graphql.String, 14 | }, 15 | "descending": &graphql.ArgumentConfig{ 16 | Type: graphql.String, 17 | }, 18 | "sort": &graphql.ArgumentConfig{ 19 | Type: graphql.String, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /adapter/http/graphql/schema/product.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/graphql-go/graphql" 5 | ) 6 | 7 | var product = graphql.NewObject(graphql.ObjectConfig{ 8 | Name: "Product", 9 | Fields: graphql.Fields{ 10 | "id": &graphql.Field{ 11 | Type: graphql.Int, 12 | }, 13 | "name": &graphql.Field{ 14 | Type: graphql.String, 15 | }, 16 | "price": &graphql.Field{ 17 | Type: graphql.Float, 18 | }, 19 | "description": &graphql.Field{ 20 | Type: graphql.String, 21 | }, 22 | }, 23 | }) 24 | 25 | var paginationProducts = graphql.NewObject(graphql.ObjectConfig{ 26 | Name: "Pagination", 27 | Fields: graphql.Fields{ 28 | "items": &graphql.Field{ 29 | Type: graphql.NewList(product), 30 | }, 31 | "total": &graphql.Field{ 32 | Type: graphql.Int, 33 | }, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /adapter/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/boooscaaa/clean-go/adapter/http/graphql/schema" 12 | "github.com/boooscaaa/clean-go/adapter/postgres" 13 | "github.com/boooscaaa/clean-go/di" 14 | "github.com/gorilla/mux" 15 | "github.com/spf13/viper" 16 | httpSwagger "github.com/swaggo/http-swagger" 17 | 18 | _ "github.com/boooscaaa/clean-go/adapter/http/rest/docs" 19 | "github.com/boooscaaa/clean-go/adapter/http/rest/middleware" 20 | ) 21 | 22 | func init() { 23 | viper.SetConfigFile(`config.json`) 24 | err := viper.ReadInConfig() 25 | if err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | // @title Clean GO API Docs 31 | // @version 1.0.0 32 | // @contact.name Vinícius Boscardin 33 | // @license.name Apache 2.0 34 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 35 | // @host localhost:port 36 | // @BasePath / 37 | func main() { 38 | ctx := context.Background() 39 | conn := postgres.GetConnection(ctx) 40 | defer conn.Close() 41 | 42 | postgres.RunMigrations() 43 | productService := di.ConfigProductDI(conn) 44 | productGraphQLService := di.ConfigProductGraphQLDI(conn) 45 | 46 | router := mux.NewRouter() 47 | graphQLRouter := schema.Config(productGraphQLService) 48 | router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) 49 | 50 | jsonApiRouter := router.PathPrefix("/").Subrouter() 51 | jsonApiRouter.Use(middleware.Cors) 52 | 53 | jsonApiRouter.Handle("/product", http.HandlerFunc(productService.Create)).Methods("POST", "OPTIONS") 54 | jsonApiRouter.Handle("/product", http.HandlerFunc(productService.Fetch)).Queries( 55 | "page", "{page}", 56 | "itemsPerPage", "{itemsPerPage}", 57 | "descending", "{descending}", 58 | "sort", "{sort}", 59 | "search", "{search}", 60 | ).Methods("GET", "OPTIONS") 61 | 62 | router.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) { 63 | result := schema.ExecuteQuery(r.URL.Query().Get("query"), graphQLRouter) 64 | json.NewEncoder(w).Encode(result) 65 | }) 66 | 67 | port := viper.GetString("server.port") 68 | 69 | if port == "" { 70 | port = os.Getenv("PORT") 71 | } 72 | log.Printf("LISTEN ON PORT: %v", port) 73 | http.ListenAndServe(fmt.Sprintf(":%v", port), router) 74 | } 75 | -------------------------------------------------------------------------------- /adapter/http/rest/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY THE COMMAND ABOVE; 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 | "contact": { 14 | "name": "Vinícius Boscardin" 15 | }, 16 | "license": { 17 | "name": "Apache 2.0", 18 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 19 | }, 20 | "version": "{{.Version}}" 21 | }, 22 | "host": "{{.Host}}", 23 | "basePath": "{{.BasePath}}", 24 | "paths": { 25 | "/product": { 26 | "get": { 27 | "description": "Fetch products with server pagination", 28 | "consumes": [ 29 | "application/json" 30 | ], 31 | "produces": [ 32 | "application/json" 33 | ], 34 | "tags": [ 35 | "product" 36 | ], 37 | "summary": "Fetch products with server pagination", 38 | "parameters": [ 39 | { 40 | "type": "string", 41 | "description": "1,2", 42 | "name": "sort", 43 | "in": "query", 44 | "required": true 45 | }, 46 | { 47 | "type": "string", 48 | "description": "true,false", 49 | "name": "descending", 50 | "in": "query", 51 | "required": true 52 | }, 53 | { 54 | "type": "integer", 55 | "description": "1", 56 | "name": "page", 57 | "in": "query", 58 | "required": true 59 | }, 60 | { 61 | "type": "integer", 62 | "description": "10", 63 | "name": "itemsPerPage", 64 | "in": "query", 65 | "required": true 66 | }, 67 | { 68 | "type": "string", 69 | "description": "product name or any column", 70 | "name": "search", 71 | "in": "query" 72 | } 73 | ], 74 | "responses": { 75 | "200": { 76 | "description": "OK", 77 | "schema": { 78 | "$ref": "#/definitions/domain.Pagination" 79 | } 80 | } 81 | } 82 | }, 83 | "post": { 84 | "description": "Create new product", 85 | "consumes": [ 86 | "application/json" 87 | ], 88 | "produces": [ 89 | "application/json" 90 | ], 91 | "tags": [ 92 | "product" 93 | ], 94 | "summary": "Create new product", 95 | "parameters": [ 96 | { 97 | "description": "product", 98 | "name": "product", 99 | "in": "body", 100 | "required": true, 101 | "schema": { 102 | "$ref": "#/definitions/dto.CreateProductRequest" 103 | } 104 | } 105 | ], 106 | "responses": { 107 | "200": { 108 | "description": "OK", 109 | "schema": { 110 | "$ref": "#/definitions/domain.Product" 111 | } 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "definitions": { 118 | "domain.Pagination": { 119 | "type": "object", 120 | "properties": { 121 | "items": {}, 122 | "total": { 123 | "type": "integer" 124 | } 125 | } 126 | }, 127 | "domain.Product": { 128 | "type": "object", 129 | "properties": { 130 | "description": { 131 | "type": "string" 132 | }, 133 | "id": { 134 | "type": "integer" 135 | }, 136 | "name": { 137 | "type": "string" 138 | }, 139 | "price": { 140 | "type": "number" 141 | } 142 | } 143 | }, 144 | "dto.CreateProductRequest": { 145 | "type": "object", 146 | "properties": { 147 | "description": { 148 | "type": "string" 149 | }, 150 | "name": { 151 | "type": "string" 152 | }, 153 | "price": { 154 | "type": "number" 155 | } 156 | } 157 | } 158 | } 159 | }` 160 | 161 | // SwaggerInfo holds exported Swagger Info so clients can modify it 162 | var SwaggerInfo = &swag.Spec{ 163 | Version: "1.0.0", 164 | Host: "localhost:port", 165 | BasePath: "/", 166 | Schemes: []string{}, 167 | Title: "Clean GO API Docs", 168 | Description: "", 169 | InfoInstanceName: "swagger", 170 | SwaggerTemplate: docTemplate, 171 | } 172 | 173 | func init() { 174 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 175 | } 176 | -------------------------------------------------------------------------------- /adapter/http/rest/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Clean GO API Docs", 5 | "contact": { 6 | "name": "Vinícius Boscardin" 7 | }, 8 | "license": { 9 | "name": "Apache 2.0", 10 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 11 | }, 12 | "version": "1.0.0" 13 | }, 14 | "host": "localhost:port", 15 | "basePath": "/", 16 | "paths": { 17 | "/product": { 18 | "get": { 19 | "description": "Fetch products with server pagination", 20 | "consumes": [ 21 | "application/json" 22 | ], 23 | "produces": [ 24 | "application/json" 25 | ], 26 | "tags": [ 27 | "product" 28 | ], 29 | "summary": "Fetch products with server pagination", 30 | "parameters": [ 31 | { 32 | "type": "string", 33 | "description": "1,2", 34 | "name": "sort", 35 | "in": "query", 36 | "required": true 37 | }, 38 | { 39 | "type": "string", 40 | "description": "true,false", 41 | "name": "descending", 42 | "in": "query", 43 | "required": true 44 | }, 45 | { 46 | "type": "integer", 47 | "description": "1", 48 | "name": "page", 49 | "in": "query", 50 | "required": true 51 | }, 52 | { 53 | "type": "integer", 54 | "description": "10", 55 | "name": "itemsPerPage", 56 | "in": "query", 57 | "required": true 58 | }, 59 | { 60 | "type": "string", 61 | "description": "product name or any column", 62 | "name": "search", 63 | "in": "query" 64 | } 65 | ], 66 | "responses": { 67 | "200": { 68 | "description": "OK", 69 | "schema": { 70 | "$ref": "#/definitions/domain.Pagination" 71 | } 72 | } 73 | } 74 | }, 75 | "post": { 76 | "description": "Create new product", 77 | "consumes": [ 78 | "application/json" 79 | ], 80 | "produces": [ 81 | "application/json" 82 | ], 83 | "tags": [ 84 | "product" 85 | ], 86 | "summary": "Create new product", 87 | "parameters": [ 88 | { 89 | "description": "product", 90 | "name": "product", 91 | "in": "body", 92 | "required": true, 93 | "schema": { 94 | "$ref": "#/definitions/dto.CreateProductRequest" 95 | } 96 | } 97 | ], 98 | "responses": { 99 | "200": { 100 | "description": "OK", 101 | "schema": { 102 | "$ref": "#/definitions/domain.Product" 103 | } 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "definitions": { 110 | "domain.Pagination": { 111 | "type": "object", 112 | "properties": { 113 | "items": {}, 114 | "total": { 115 | "type": "integer" 116 | } 117 | } 118 | }, 119 | "domain.Product": { 120 | "type": "object", 121 | "properties": { 122 | "description": { 123 | "type": "string" 124 | }, 125 | "id": { 126 | "type": "integer" 127 | }, 128 | "name": { 129 | "type": "string" 130 | }, 131 | "price": { 132 | "type": "number" 133 | } 134 | } 135 | }, 136 | "dto.CreateProductRequest": { 137 | "type": "object", 138 | "properties": { 139 | "description": { 140 | "type": "string" 141 | }, 142 | "name": { 143 | "type": "string" 144 | }, 145 | "price": { 146 | "type": "number" 147 | } 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /adapter/http/rest/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | domain.Pagination: 4 | properties: 5 | items: {} 6 | total: 7 | type: integer 8 | type: object 9 | domain.Product: 10 | properties: 11 | description: 12 | type: string 13 | id: 14 | type: integer 15 | name: 16 | type: string 17 | price: 18 | type: number 19 | type: object 20 | dto.CreateProductRequest: 21 | properties: 22 | description: 23 | type: string 24 | name: 25 | type: string 26 | price: 27 | type: number 28 | type: object 29 | host: localhost:port 30 | info: 31 | contact: 32 | name: Vinícius Boscardin 33 | license: 34 | name: Apache 2.0 35 | url: http://www.apache.org/licenses/LICENSE-2.0.html 36 | title: Clean GO API Docs 37 | version: 1.0.0 38 | paths: 39 | /product: 40 | get: 41 | consumes: 42 | - application/json 43 | description: Fetch products with server pagination 44 | parameters: 45 | - description: 1,2 46 | in: query 47 | name: sort 48 | required: true 49 | type: string 50 | - description: true,false 51 | in: query 52 | name: descending 53 | required: true 54 | type: string 55 | - description: "1" 56 | in: query 57 | name: page 58 | required: true 59 | type: integer 60 | - description: "10" 61 | in: query 62 | name: itemsPerPage 63 | required: true 64 | type: integer 65 | - description: product name or any column 66 | in: query 67 | name: search 68 | type: string 69 | produces: 70 | - application/json 71 | responses: 72 | "200": 73 | description: OK 74 | schema: 75 | $ref: '#/definitions/domain.Pagination' 76 | summary: Fetch products with server pagination 77 | tags: 78 | - product 79 | post: 80 | consumes: 81 | - application/json 82 | description: Create new product 83 | parameters: 84 | - description: product 85 | in: body 86 | name: product 87 | required: true 88 | schema: 89 | $ref: '#/definitions/dto.CreateProductRequest' 90 | produces: 91 | - application/json 92 | responses: 93 | "200": 94 | description: OK 95 | schema: 96 | $ref: '#/definitions/domain.Product' 97 | summary: Create new product 98 | tags: 99 | - product 100 | swagger: "2.0" 101 | -------------------------------------------------------------------------------- /adapter/http/rest/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "net/http" 4 | 5 | //Cors config enabled to all origin 6 | func Cors(next http.Handler) http.Handler { 7 | return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { 8 | response.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, DELETE, PUT, GET") 9 | response.Header().Set("Content-Type", "application/json") 10 | response.Header().Set("Access-Control-Allow-Origin", "*") 11 | response.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 12 | if request.Method == "OPTIONS" { 13 | response.WriteHeader(http.StatusOK) 14 | } else { 15 | next.ServeHTTP(response, request) 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /adapter/http/rest/productservice/create.go: -------------------------------------------------------------------------------- 1 | package productservice 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/boooscaaa/clean-go/core/dto" 8 | ) 9 | 10 | // @Summary Create new product 11 | // @Description Create new product 12 | // @Tags product 13 | // @Accept json 14 | // @Produce json 15 | // @Param product body dto.CreateProductRequest true "product" 16 | // @Success 200 {object} domain.Product 17 | // @Router /product [post] 18 | func (service service) Create(response http.ResponseWriter, request *http.Request) { 19 | productRequest, err := dto.FromJSONCreateProductRequest(request.Body) 20 | 21 | if err != nil { 22 | response.WriteHeader(250) 23 | response.Write([]byte(err.Error())) 24 | return 25 | } 26 | 27 | product, err := service.usecase.Create(productRequest) 28 | 29 | if err != nil { 30 | response.WriteHeader(500) 31 | response.Write([]byte(err.Error())) 32 | return 33 | } 34 | 35 | json.NewEncoder(response).Encode(product) 36 | } 37 | -------------------------------------------------------------------------------- /adapter/http/rest/productservice/create_test.go: -------------------------------------------------------------------------------- 1 | package productservice_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/boooscaaa/clean-go/adapter/http/rest/productservice" 12 | "github.com/boooscaaa/clean-go/core/domain" 13 | "github.com/boooscaaa/clean-go/core/domain/mocks" 14 | "github.com/boooscaaa/clean-go/core/dto" 15 | "github.com/bxcodec/faker/v3" 16 | "github.com/golang/mock/gomock" 17 | ) 18 | 19 | func setupCreate(t *testing.T) (dto.CreateProductRequest, domain.Product, *gomock.Controller) { 20 | fakeProductRequest := dto.CreateProductRequest{} 21 | fakeProduct := domain.Product{} 22 | faker.FakeData(&fakeProductRequest) 23 | faker.FakeData(&fakeProduct) 24 | 25 | mockCtrl := gomock.NewController(t) 26 | 27 | return fakeProductRequest, fakeProduct, mockCtrl 28 | } 29 | 30 | func TestCreate(t *testing.T) { 31 | fakeProductRequest, fakeProduct, mock := setupCreate(t) 32 | defer mock.Finish() 33 | mockProductUseCase := mocks.NewMockProductUseCase(mock) 34 | mockProductUseCase.EXPECT().Create(&fakeProductRequest).Return(&fakeProduct, nil) 35 | 36 | sut := productservice.New(mockProductUseCase) 37 | 38 | payload, _ := json.Marshal(fakeProductRequest) 39 | w := httptest.NewRecorder() 40 | r := httptest.NewRequest(http.MethodPost, "/product", strings.NewReader(string(payload))) 41 | r.Header.Set("Content-Type", "application/json") 42 | sut.Create(w, r) 43 | 44 | res := w.Result() 45 | defer res.Body.Close() 46 | 47 | if res.StatusCode != 200 { 48 | t.Errorf("status code is not correct") 49 | } 50 | } 51 | 52 | func TestCreate_JsonErrorFormater(t *testing.T) { 53 | _, _, mock := setupCreate(t) 54 | defer mock.Finish() 55 | mockProductUseCase := mocks.NewMockProductUseCase(mock) 56 | 57 | sut := productservice.New(mockProductUseCase) 58 | 59 | w := httptest.NewRecorder() 60 | r := httptest.NewRequest(http.MethodPost, "/product", strings.NewReader("{")) 61 | r.Header.Set("Content-Type", "application/json") 62 | sut.Create(w, r) 63 | 64 | res := w.Result() 65 | defer res.Body.Close() 66 | 67 | if res.StatusCode == 200 { 68 | t.Errorf("status code is not correct") 69 | } 70 | } 71 | 72 | func TestCreate_PorductError(t *testing.T) { 73 | fakeProductRequest, _, mock := setupCreate(t) 74 | defer mock.Finish() 75 | mockProductUseCase := mocks.NewMockProductUseCase(mock) 76 | mockProductUseCase.EXPECT().Create(&fakeProductRequest).Return(nil, fmt.Errorf("ANY ERROR")) 77 | 78 | sut := productservice.New(mockProductUseCase) 79 | 80 | payload, _ := json.Marshal(fakeProductRequest) 81 | w := httptest.NewRecorder() 82 | r := httptest.NewRequest(http.MethodPost, "/product", strings.NewReader(string(payload))) 83 | r.Header.Set("Content-Type", "application/json") 84 | sut.Create(w, r) 85 | 86 | res := w.Result() 87 | defer res.Body.Close() 88 | 89 | if res.StatusCode == 200 { 90 | t.Errorf("status code is not correct") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /adapter/http/rest/productservice/fetch.go: -------------------------------------------------------------------------------- 1 | package productservice 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/boooscaaa/clean-go/core/dto" 8 | ) 9 | 10 | // @Summary Fetch products with server pagination 11 | // @Description Fetch products with server pagination 12 | // @Tags product 13 | // @Accept json 14 | // @Produce json 15 | // @Param sort query string true "1,2" 16 | // @Param descending query string true "true,false" 17 | // @Param page query integer true "1" 18 | // @Param itemsPerPage query integer true "10" 19 | // @Param search query string false "product name or any column" 20 | // @Success 200 {object} domain.Pagination 21 | // @Router /product [get] 22 | func (service service) Fetch(response http.ResponseWriter, request *http.Request) { 23 | paginationRequest, _ := dto.FromValuePaginationRequestParams(request) 24 | 25 | products, err := service.usecase.Fetch(paginationRequest) 26 | 27 | if err != nil { 28 | response.WriteHeader(500) 29 | response.Write([]byte(err.Error())) 30 | return 31 | } 32 | 33 | json.NewEncoder(response).Encode(products) 34 | } 35 | -------------------------------------------------------------------------------- /adapter/http/rest/productservice/fetch_test.go: -------------------------------------------------------------------------------- 1 | package productservice_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/boooscaaa/clean-go/adapter/http/rest/productservice" 10 | "github.com/boooscaaa/clean-go/core/domain" 11 | "github.com/boooscaaa/clean-go/core/domain/mocks" 12 | "github.com/boooscaaa/clean-go/core/dto" 13 | "github.com/bxcodec/faker/v3" 14 | "github.com/golang/mock/gomock" 15 | ) 16 | 17 | func setupFetch(t *testing.T) (dto.PaginationRequestParms, domain.Product, *gomock.Controller) { 18 | fakePaginationRequestParams := dto.PaginationRequestParms{ 19 | Page: 1, 20 | ItemsPerPage: 10, 21 | Sort: []string{""}, 22 | Descending: []string{""}, 23 | Search: "", 24 | } 25 | fakeProduct := domain.Product{} 26 | faker.FakeData(&fakeProduct) 27 | 28 | mockCtrl := gomock.NewController(t) 29 | 30 | return fakePaginationRequestParams, fakeProduct, mockCtrl 31 | } 32 | 33 | func TestFetch(t *testing.T) { 34 | fakePaginationRequestParams, fakeProduct, mock := setupFetch(t) 35 | defer mock.Finish() 36 | mockProductUseCase := mocks.NewMockProductUseCase(mock) 37 | mockProductUseCase.EXPECT().Fetch(&fakePaginationRequestParams).Return(&domain.Pagination{ 38 | Items: []domain.Product{fakeProduct}, 39 | Total: 1, 40 | }, nil) 41 | 42 | sut := productservice.New(mockProductUseCase) 43 | 44 | w := httptest.NewRecorder() 45 | r := httptest.NewRequest(http.MethodGet, "/product", nil) 46 | r.Header.Set("Content-Type", "application/json") 47 | queryStringParams := r.URL.Query() 48 | queryStringParams.Add("page", "1") 49 | queryStringParams.Add("itemsPerPage", "10") 50 | queryStringParams.Add("sort", "") 51 | queryStringParams.Add("descending", "") 52 | queryStringParams.Add("search", "") 53 | r.URL.RawQuery = queryStringParams.Encode() 54 | sut.Fetch(w, r) 55 | 56 | res := w.Result() 57 | defer res.Body.Close() 58 | 59 | if res.StatusCode != 200 { 60 | t.Errorf("status code is not correct") 61 | } 62 | } 63 | 64 | func TestFetch_PorductError(t *testing.T) { 65 | fakePaginationRequestParams, _, mock := setupFetch(t) 66 | defer mock.Finish() 67 | mockProductUseCase := mocks.NewMockProductUseCase(mock) 68 | mockProductUseCase.EXPECT().Fetch(&fakePaginationRequestParams).Return(nil, fmt.Errorf("ANY ERROR")) 69 | 70 | sut := productservice.New(mockProductUseCase) 71 | 72 | w := httptest.NewRecorder() 73 | r := httptest.NewRequest(http.MethodGet, "/product", nil) 74 | r.Header.Set("Content-Type", "application/json") 75 | queryStringParams := r.URL.Query() 76 | queryStringParams.Add("page", "1") 77 | queryStringParams.Add("itemsPerPage", "10") 78 | queryStringParams.Add("sort", "") 79 | queryStringParams.Add("descending", "") 80 | queryStringParams.Add("search", "") 81 | r.URL.RawQuery = queryStringParams.Encode() 82 | sut.Fetch(w, r) 83 | 84 | res := w.Result() 85 | defer res.Body.Close() 86 | 87 | if res.StatusCode == 200 { 88 | t.Errorf("status code is not correct") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /adapter/http/rest/productservice/new.go: -------------------------------------------------------------------------------- 1 | package productservice 2 | 3 | import "github.com/boooscaaa/clean-go/core/domain" 4 | 5 | type service struct { 6 | usecase domain.ProductUseCase 7 | } 8 | 9 | // New returns contract implementation of ProductService 10 | func New(usecase domain.ProductUseCase) domain.ProductService { 11 | return &service{ 12 | usecase: usecase, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /adapter/postgres/connector.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/golang-migrate/migrate/v4" 10 | "github.com/jackc/pgconn" 11 | "github.com/jackc/pgx/v4" 12 | "github.com/jackc/pgx/v4/pgxpool" 13 | "github.com/spf13/viper" 14 | 15 | _ "github.com/golang-migrate/migrate/v4/database/pgx" //driver pgx used to run migrations 16 | _ "github.com/golang-migrate/migrate/v4/source/file" 17 | ) 18 | 19 | // PoolInterface is an wraping to PgxPool to create test mocks 20 | type PoolInterface interface { 21 | Close() 22 | Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) 23 | Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) 24 | QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row 25 | QueryFunc( 26 | ctx context.Context, 27 | sql string, 28 | args []interface{}, 29 | scans []interface{}, 30 | f func(pgx.QueryFuncRow) error, 31 | ) (pgconn.CommandTag, error) 32 | SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults 33 | Begin(ctx context.Context) (pgx.Tx, error) 34 | BeginFunc(ctx context.Context, f func(pgx.Tx) error) error 35 | BeginTxFunc(ctx context.Context, txOptions pgx.TxOptions, f func(pgx.Tx) error) error 36 | } 37 | 38 | // GetConnection return connection pool from postgres drive PGX 39 | func GetConnection(context context.Context) *pgxpool.Pool { 40 | databaseURL := viper.GetString("database.url") 41 | 42 | conn, err := pgxpool.Connect(context, "postgres"+databaseURL) 43 | 44 | if err != nil { 45 | fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) 46 | os.Exit(1) 47 | } 48 | 49 | return conn 50 | } 51 | 52 | // RunMigrations run scripts on path database/migrations 53 | func RunMigrations() { 54 | databaseURL := viper.GetString("database.url") 55 | m, err := migrate.New("file://database/migrations", "pgx"+databaseURL) 56 | if err != nil { 57 | log.Println(err) 58 | } 59 | 60 | if err := m.Up(); err != nil { 61 | log.Println(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /adapter/postgres/productrepository/create.go: -------------------------------------------------------------------------------- 1 | package productrepository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/boooscaaa/clean-go/core/domain" 7 | "github.com/boooscaaa/clean-go/core/dto" 8 | ) 9 | 10 | func (repository repository) Create( 11 | productRequest *dto.CreateProductRequest, 12 | ) (*domain.Product, error) { 13 | ctx := context.Background() 14 | product := domain.Product{} 15 | 16 | err := repository.db.QueryRow( 17 | ctx, 18 | "INSERT INTO product (name, price, description) VALUES ($1, $2, $3) returning *", 19 | productRequest.Name, 20 | productRequest.Price, 21 | productRequest.Description, 22 | ).Scan( 23 | &product.ID, 24 | &product.Name, 25 | &product.Price, 26 | &product.Description, 27 | ) 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &product, nil 34 | } 35 | -------------------------------------------------------------------------------- /adapter/postgres/productrepository/create_test.go: -------------------------------------------------------------------------------- 1 | package productrepository_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/boooscaaa/clean-go/adapter/postgres/productrepository" 8 | "github.com/boooscaaa/clean-go/core/domain" 9 | "github.com/boooscaaa/clean-go/core/dto" 10 | "github.com/bxcodec/faker/v3" 11 | "github.com/pashagolub/pgxmock" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func setupCreate() ([]string, dto.CreateProductRequest, domain.Product, pgxmock.PgxPoolIface) { 16 | cols := []string{"id", "name", "price", "description"} 17 | fakeProductRequest := dto.CreateProductRequest{} 18 | fakeProductDBResponse := domain.Product{} 19 | faker.FakeData(&fakeProductRequest) 20 | faker.FakeData(&fakeProductDBResponse) 21 | 22 | mock, _ := pgxmock.NewPool() 23 | 24 | return cols, fakeProductRequest, fakeProductDBResponse, mock 25 | } 26 | 27 | func TestCreate(t *testing.T) { 28 | cols, fakeProductRequest, fakeProductDBResponse, mock := setupCreate() 29 | defer mock.Close() 30 | 31 | mock.ExpectQuery("INSERT INTO product (.+)").WithArgs( 32 | fakeProductRequest.Name, 33 | fakeProductRequest.Price, 34 | fakeProductRequest.Description, 35 | ).WillReturnRows(pgxmock.NewRows(cols).AddRow( 36 | fakeProductDBResponse.ID, 37 | fakeProductDBResponse.Name, 38 | fakeProductDBResponse.Price, 39 | fakeProductDBResponse.Description, 40 | )) 41 | 42 | sut := productrepository.New(mock) 43 | product, err := sut.Create(&fakeProductRequest) 44 | 45 | if err := mock.ExpectationsWereMet(); err != nil { 46 | t.Errorf("there were unfulfilled expectations: %s", err) 47 | } 48 | 49 | require.Nil(t, err) 50 | require.NotEmpty(t, product.ID) 51 | require.Equal(t, product.Name, fakeProductDBResponse.Name) 52 | require.Equal(t, product.Price, fakeProductDBResponse.Price) 53 | require.Equal(t, product.Description, fakeProductDBResponse.Description) 54 | } 55 | 56 | func TestCreate_DBError(t *testing.T) { 57 | _, fakeProductRequest, _, mock := setupCreate() 58 | defer mock.Close() 59 | 60 | mock.ExpectQuery("INSERT INTO product (.+)").WithArgs( 61 | fakeProductRequest.Name, 62 | fakeProductRequest.Price, 63 | fakeProductRequest.Description, 64 | ).WillReturnError(fmt.Errorf("ANY DATABASE ERROR")) 65 | 66 | sut := productrepository.New(mock) 67 | product, err := sut.Create(&fakeProductRequest) 68 | 69 | if err := mock.ExpectationsWereMet(); err != nil { 70 | t.Errorf("there were unfulfilled expectations: %s", err) 71 | } 72 | 73 | require.NotNil(t, err) 74 | require.Nil(t, product) 75 | } 76 | -------------------------------------------------------------------------------- /adapter/postgres/productrepository/fetch.go: -------------------------------------------------------------------------------- 1 | package productrepository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/boooscaaa/clean-go/core/domain" 7 | "github.com/boooscaaa/clean-go/core/dto" 8 | "github.com/booscaaa/go-paginate/paginate" 9 | ) 10 | 11 | func (repository repository) Fetch(pagination *dto.PaginationRequestParms) (*domain.Pagination, error) { 12 | ctx := context.Background() 13 | products := []domain.Product{} 14 | total := int32(0) 15 | 16 | query, queryCount, _ := paginate.Paginate("SELECT * FROM product"). 17 | Page(pagination.Page). 18 | Desc(pagination.Descending). 19 | Sort(pagination.Sort). 20 | RowsPerPage(pagination.ItemsPerPage). 21 | SearchBy(pagination.Search, "name", "description"). 22 | Query() 23 | 24 | { 25 | rows, err := repository.db.Query( 26 | ctx, 27 | *query, 28 | ) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | for rows.Next() { 35 | product := domain.Product{} 36 | 37 | rows.Scan( 38 | &product.ID, 39 | &product.Name, 40 | &product.Price, 41 | &product.Description, 42 | ) 43 | 44 | products = append(products, product) 45 | } 46 | } 47 | 48 | { 49 | err := repository.db.QueryRow(ctx, *queryCount).Scan(&total) 50 | 51 | if err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | return &domain.Pagination{ 57 | Items: products, 58 | Total: total, 59 | }, nil 60 | } 61 | -------------------------------------------------------------------------------- /adapter/postgres/productrepository/fetch_test.go: -------------------------------------------------------------------------------- 1 | package productrepository_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/boooscaaa/clean-go/adapter/postgres/productrepository" 8 | "github.com/boooscaaa/clean-go/core/domain" 9 | "github.com/boooscaaa/clean-go/core/dto" 10 | "github.com/bxcodec/faker/v3" 11 | "github.com/pashagolub/pgxmock" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func setupFetch() ([]string, dto.PaginationRequestParms, domain.Product, pgxmock.PgxPoolIface) { 16 | cols := []string{"id", "name", "price", "description"} 17 | fakePaginationRequestParams := dto.PaginationRequestParms{ 18 | Page: 1, 19 | ItemsPerPage: 10, 20 | Sort: nil, 21 | Descending: nil, 22 | Search: "", 23 | } 24 | fakeProductDBResponse := domain.Product{} 25 | faker.FakeData(&fakeProductDBResponse) 26 | 27 | mock, _ := pgxmock.NewPool() 28 | 29 | return cols, fakePaginationRequestParams, fakeProductDBResponse, mock 30 | } 31 | 32 | func TestFetch(t *testing.T) { 33 | cols, fakePaginationRequestParams, fakeProductDBResponse, mock := setupFetch() 34 | defer mock.Close() 35 | 36 | mock.ExpectQuery("SELECT (.+) FROM product"). 37 | WillReturnRows(pgxmock.NewRows(cols).AddRow( 38 | fakeProductDBResponse.ID, 39 | fakeProductDBResponse.Name, 40 | fakeProductDBResponse.Price, 41 | fakeProductDBResponse.Description, 42 | )) 43 | 44 | mock.ExpectQuery("SELECT COUNT(.+) FROM product"). 45 | WillReturnRows(pgxmock.NewRows([]string{"count"}).AddRow(int32(1))) 46 | 47 | sut := productrepository.New(mock) 48 | products, err := sut.Fetch(&fakePaginationRequestParams) 49 | 50 | require.Nil(t, err) 51 | 52 | if err := mock.ExpectationsWereMet(); err != nil { 53 | t.Errorf("there were unfulfilled expectations: %s", err) 54 | } 55 | 56 | for _, product := range products.Items.([]domain.Product) { 57 | require.Nil(t, err) 58 | require.NotEmpty(t, product.ID) 59 | require.Equal(t, product.Name, fakeProductDBResponse.Name) 60 | require.Equal(t, product.Price, fakeProductDBResponse.Price) 61 | require.Equal(t, product.Description, fakeProductDBResponse.Description) 62 | } 63 | } 64 | 65 | func TestFetch_QueryError(t *testing.T) { 66 | _, fakePaginationRequestParams, _, mock := setupFetch() 67 | defer mock.Close() 68 | 69 | mock.ExpectQuery("SELECT (.+) FROM product"). 70 | WillReturnError(fmt.Errorf("ANY QUERY ERROR")) 71 | 72 | sut := productrepository.New(mock) 73 | products, err := sut.Fetch(&fakePaginationRequestParams) 74 | 75 | require.NotNil(t, err) 76 | 77 | if err := mock.ExpectationsWereMet(); err != nil { 78 | t.Errorf("there were unfulfilled expectations: %s", err) 79 | } 80 | 81 | require.Nil(t, products) 82 | } 83 | 84 | func TestFetch_QueryCountError(t *testing.T) { 85 | cols, fakePaginationRequestParams, fakeProductDBResponse, mock := setupFetch() 86 | defer mock.Close() 87 | 88 | mock.ExpectQuery("SELECT (.+) FROM product"). 89 | WillReturnRows(pgxmock.NewRows(cols).AddRow( 90 | fakeProductDBResponse.ID, 91 | fakeProductDBResponse.Name, 92 | fakeProductDBResponse.Price, 93 | fakeProductDBResponse.Description, 94 | )) 95 | 96 | mock.ExpectQuery("SELECT COUNT(.+) FROM product"). 97 | WillReturnError(fmt.Errorf("ANY QUERY COUNT ERROR")) 98 | 99 | sut := productrepository.New(mock) 100 | products, err := sut.Fetch(&fakePaginationRequestParams) 101 | 102 | require.NotNil(t, err) 103 | 104 | if err := mock.ExpectationsWereMet(); err != nil { 105 | t.Errorf("there were unfulfilled expectations: %s", err) 106 | } 107 | 108 | require.Nil(t, products) 109 | } 110 | -------------------------------------------------------------------------------- /adapter/postgres/productrepository/new.go: -------------------------------------------------------------------------------- 1 | package productrepository 2 | 3 | import ( 4 | "github.com/boooscaaa/clean-go/adapter/postgres" 5 | "github.com/boooscaaa/clean-go/core/domain" 6 | ) 7 | 8 | type repository struct { 9 | db postgres.PoolInterface 10 | } 11 | 12 | // New returns contract implementation of ProductRepository 13 | func New(db postgres.PoolInterface) domain.ProductRepository { 14 | return &repository{ 15 | db: db, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "url": "://postgres:4ba1b3aeb9fccc96e0c9f7e6d229f9a7a26b86578c9e3918@cleango-db.internal:5432?sslmode=disable" 4 | }, 5 | "server": { 6 | "port": "3000" 7 | } 8 | } -------------------------------------------------------------------------------- /core/domain/mocks/fakeproduct.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: core/domain/product.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | http "net/http" 9 | reflect "reflect" 10 | 11 | domain "github.com/boooscaaa/clean-go/core/domain" 12 | dto "github.com/boooscaaa/clean-go/core/dto" 13 | gomock "github.com/golang/mock/gomock" 14 | ) 15 | 16 | // MockProductService is a mock of ProductService interface. 17 | type MockProductService struct { 18 | ctrl *gomock.Controller 19 | recorder *MockProductServiceMockRecorder 20 | } 21 | 22 | // MockProductServiceMockRecorder is the mock recorder for MockProductService. 23 | type MockProductServiceMockRecorder struct { 24 | mock *MockProductService 25 | } 26 | 27 | // NewMockProductService creates a new mock instance. 28 | func NewMockProductService(ctrl *gomock.Controller) *MockProductService { 29 | mock := &MockProductService{ctrl: ctrl} 30 | mock.recorder = &MockProductServiceMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockProductService) EXPECT() *MockProductServiceMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Create mocks base method. 40 | func (m *MockProductService) Create(response http.ResponseWriter, request *http.Request) { 41 | m.ctrl.T.Helper() 42 | m.ctrl.Call(m, "Create", response, request) 43 | } 44 | 45 | // Create indicates an expected call of Create. 46 | func (mr *MockProductServiceMockRecorder) Create(response, request interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProductService)(nil).Create), response, request) 49 | } 50 | 51 | // Fetch mocks base method. 52 | func (m *MockProductService) Fetch(response http.ResponseWriter, request *http.Request) { 53 | m.ctrl.T.Helper() 54 | m.ctrl.Call(m, "Fetch", response, request) 55 | } 56 | 57 | // Fetch indicates an expected call of Fetch. 58 | func (mr *MockProductServiceMockRecorder) Fetch(response, request interface{}) *gomock.Call { 59 | mr.mock.ctrl.T.Helper() 60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockProductService)(nil).Fetch), response, request) 61 | } 62 | 63 | // MockProductUseCase is a mock of ProductUseCase interface. 64 | type MockProductUseCase struct { 65 | ctrl *gomock.Controller 66 | recorder *MockProductUseCaseMockRecorder 67 | } 68 | 69 | // MockProductUseCaseMockRecorder is the mock recorder for MockProductUseCase. 70 | type MockProductUseCaseMockRecorder struct { 71 | mock *MockProductUseCase 72 | } 73 | 74 | // NewMockProductUseCase creates a new mock instance. 75 | func NewMockProductUseCase(ctrl *gomock.Controller) *MockProductUseCase { 76 | mock := &MockProductUseCase{ctrl: ctrl} 77 | mock.recorder = &MockProductUseCaseMockRecorder{mock} 78 | return mock 79 | } 80 | 81 | // EXPECT returns an object that allows the caller to indicate expected use. 82 | func (m *MockProductUseCase) EXPECT() *MockProductUseCaseMockRecorder { 83 | return m.recorder 84 | } 85 | 86 | // Create mocks base method. 87 | func (m *MockProductUseCase) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) { 88 | m.ctrl.T.Helper() 89 | ret := m.ctrl.Call(m, "Create", productRequest) 90 | ret0, _ := ret[0].(*domain.Product) 91 | ret1, _ := ret[1].(error) 92 | return ret0, ret1 93 | } 94 | 95 | // Create indicates an expected call of Create. 96 | func (mr *MockProductUseCaseMockRecorder) Create(productRequest interface{}) *gomock.Call { 97 | mr.mock.ctrl.T.Helper() 98 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProductUseCase)(nil).Create), productRequest) 99 | } 100 | 101 | // Fetch mocks base method. 102 | func (m *MockProductUseCase) Fetch(paginationRequest *dto.PaginationRequestParms) (*domain.Pagination, error) { 103 | m.ctrl.T.Helper() 104 | ret := m.ctrl.Call(m, "Fetch", paginationRequest) 105 | ret0, _ := ret[0].(*domain.Pagination) 106 | ret1, _ := ret[1].(error) 107 | return ret0, ret1 108 | } 109 | 110 | // Fetch indicates an expected call of Fetch. 111 | func (mr *MockProductUseCaseMockRecorder) Fetch(paginationRequest interface{}) *gomock.Call { 112 | mr.mock.ctrl.T.Helper() 113 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockProductUseCase)(nil).Fetch), paginationRequest) 114 | } 115 | 116 | // MockProductRepository is a mock of ProductRepository interface. 117 | type MockProductRepository struct { 118 | ctrl *gomock.Controller 119 | recorder *MockProductRepositoryMockRecorder 120 | } 121 | 122 | // MockProductRepositoryMockRecorder is the mock recorder for MockProductRepository. 123 | type MockProductRepositoryMockRecorder struct { 124 | mock *MockProductRepository 125 | } 126 | 127 | // NewMockProductRepository creates a new mock instance. 128 | func NewMockProductRepository(ctrl *gomock.Controller) *MockProductRepository { 129 | mock := &MockProductRepository{ctrl: ctrl} 130 | mock.recorder = &MockProductRepositoryMockRecorder{mock} 131 | return mock 132 | } 133 | 134 | // EXPECT returns an object that allows the caller to indicate expected use. 135 | func (m *MockProductRepository) EXPECT() *MockProductRepositoryMockRecorder { 136 | return m.recorder 137 | } 138 | 139 | // Create mocks base method. 140 | func (m *MockProductRepository) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) { 141 | m.ctrl.T.Helper() 142 | ret := m.ctrl.Call(m, "Create", productRequest) 143 | ret0, _ := ret[0].(*domain.Product) 144 | ret1, _ := ret[1].(error) 145 | return ret0, ret1 146 | } 147 | 148 | // Create indicates an expected call of Create. 149 | func (mr *MockProductRepositoryMockRecorder) Create(productRequest interface{}) *gomock.Call { 150 | mr.mock.ctrl.T.Helper() 151 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProductRepository)(nil).Create), productRequest) 152 | } 153 | 154 | // Fetch mocks base method. 155 | func (m *MockProductRepository) Fetch(paginationRequest *dto.PaginationRequestParms) (*domain.Pagination, error) { 156 | m.ctrl.T.Helper() 157 | ret := m.ctrl.Call(m, "Fetch", paginationRequest) 158 | ret0, _ := ret[0].(*domain.Pagination) 159 | ret1, _ := ret[1].(error) 160 | return ret0, ret1 161 | } 162 | 163 | // Fetch indicates an expected call of Fetch. 164 | func (mr *MockProductRepositoryMockRecorder) Fetch(paginationRequest interface{}) *gomock.Call { 165 | mr.mock.ctrl.T.Helper() 166 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockProductRepository)(nil).Fetch), paginationRequest) 167 | } 168 | -------------------------------------------------------------------------------- /core/domain/pagination.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | // Pagination is representation of Fetch methods returns 4 | type Pagination struct { 5 | Items interface{} `json:"items"` 6 | Total int32 `json:"total"` 7 | } 8 | -------------------------------------------------------------------------------- /core/domain/product.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/boooscaaa/clean-go/core/dto" 7 | "github.com/graphql-go/graphql" 8 | ) 9 | 10 | // Product is entity of table product database column 11 | type Product struct { 12 | ID int32 `json:"id"` 13 | Name string `json:"name"` 14 | Price float32 `json:"price"` 15 | Description string `json:"description"` 16 | } 17 | 18 | type PoductGraphQLService interface { 19 | Fetch(params graphql.ResolveParams) (interface{}, error) 20 | } 21 | 22 | // ProductService is a contract of http adapter layer 23 | type ProductService interface { 24 | Create(response http.ResponseWriter, request *http.Request) 25 | Fetch(response http.ResponseWriter, request *http.Request) 26 | } 27 | 28 | // ProductUseCase is a contract of business rule layer 29 | type ProductUseCase interface { 30 | Create(productRequest *dto.CreateProductRequest) (*Product, error) 31 | Fetch(paginationRequest *dto.PaginationRequestParms) (*Pagination, error) 32 | } 33 | 34 | // ProductRepository is a contract of database connection adapter layer 35 | type ProductRepository interface { 36 | Create(productRequest *dto.CreateProductRequest) (*Product, error) 37 | Fetch(paginationRequest *dto.PaginationRequestParms) (*Pagination, error) 38 | } 39 | -------------------------------------------------------------------------------- /core/dto/pagination.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/graphql-go/graphql" 9 | ) 10 | 11 | // PaginationRequestParms is an representation query string params to filter and paginate products 12 | type PaginationRequestParms struct { 13 | Search string `json:"search"` 14 | Descending []string `json:"descending"` 15 | Page int `json:"page"` 16 | ItemsPerPage int `json:"itemsPerPage"` 17 | Sort []string `json:"sort"` 18 | } 19 | 20 | // FromValuePaginationRequestParams converts query string params to a PaginationRequestParms struct 21 | func FromValuePaginationRequestParams(request *http.Request) (*PaginationRequestParms, error) { 22 | page, _ := strconv.Atoi(request.FormValue("page")) 23 | itemsPerPage, _ := strconv.Atoi(request.FormValue("itemsPerPage")) 24 | 25 | paginationRequestParms := PaginationRequestParms{ 26 | Search: request.FormValue("search"), 27 | Descending: strings.Split(request.FormValue("descending"), ","), 28 | Sort: strings.Split(request.FormValue("sort"), ","), 29 | Page: page, 30 | ItemsPerPage: itemsPerPage, 31 | } 32 | 33 | return &paginationRequestParms, nil 34 | } 35 | 36 | // FromValuePaginationRequestParams converts query string params to a PaginationRequestParms struct 37 | func FromValuePaginationGraphRequestParams(params graphql.ResolveParams) (*PaginationRequestParms, error) { 38 | page := params.Args["page"].(int) 39 | itemsPerPage := params.Args["itemsPerPage"].(int) 40 | 41 | paginationRequestParms := PaginationRequestParms{ 42 | Search: params.Args["search"].(string), 43 | Descending: strings.Split(params.Args["descending"].(string), ","), 44 | Sort: strings.Split(params.Args["sort"].(string), ","), 45 | Page: page, 46 | ItemsPerPage: itemsPerPage, 47 | } 48 | 49 | return &paginationRequestParms, nil 50 | } 51 | -------------------------------------------------------------------------------- /core/dto/pagination_test.go: -------------------------------------------------------------------------------- 1 | package dto_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/boooscaaa/clean-go/core/dto" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestFromValuePaginationRequestParams(t *testing.T) { 14 | fakeRequest := httptest.NewRequest(http.MethodGet, "/product", nil) 15 | queryStringParams := fakeRequest.URL.Query() 16 | queryStringParams.Add("page", "1") 17 | queryStringParams.Add("itemsPerPage", "10") 18 | queryStringParams.Add("sort", "") 19 | queryStringParams.Add("descending", "") 20 | queryStringParams.Add("search", "") 21 | fakeRequest.URL.RawQuery = queryStringParams.Encode() 22 | 23 | paginationRequest, err := dto.FromValuePaginationRequestParams(fakeRequest) 24 | 25 | require.Nil(t, err) 26 | require.Equal(t, paginationRequest.Page, 1) 27 | require.Equal(t, paginationRequest.ItemsPerPage, 10) 28 | require.Equal(t, paginationRequest.Sort, []string{""}) 29 | require.Equal(t, paginationRequest.Descending, []string{""}) 30 | require.Equal(t, paginationRequest.Search, "") 31 | } 32 | -------------------------------------------------------------------------------- /core/dto/product.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | // CreateProductRequest is an representation request body to create a new Product 9 | type CreateProductRequest struct { 10 | Name string `json:"name"` 11 | Price float32 `json:"price"` 12 | Description string `json:"description"` 13 | } 14 | 15 | // FromJSONCreateProductRequest converts json body request to a CreateProductRequest struct 16 | func FromJSONCreateProductRequest(body io.Reader) (*CreateProductRequest, error) { 17 | createProductRequest := CreateProductRequest{} 18 | if err := json.NewDecoder(body).Decode(&createProductRequest); err != nil { 19 | return nil, err 20 | } 21 | 22 | return &createProductRequest, nil 23 | } 24 | -------------------------------------------------------------------------------- /core/dto/product_test.go: -------------------------------------------------------------------------------- 1 | package dto_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/boooscaaa/clean-go/core/dto" 9 | "github.com/bxcodec/faker/v3" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestFromJSONCreateProductRequest(t *testing.T) { 15 | fakeItem := dto.CreateProductRequest{} 16 | faker.FakeData(&fakeItem) 17 | 18 | json, err := json.Marshal(fakeItem) 19 | require.Nil(t, err) 20 | 21 | itemRequest, err := dto.FromJSONCreateProductRequest(strings.NewReader(string(json))) 22 | 23 | require.Nil(t, err) 24 | require.Equal(t, itemRequest.Name, fakeItem.Name) 25 | require.Equal(t, itemRequest.Price, fakeItem.Price) 26 | require.Equal(t, itemRequest.Description, fakeItem.Description) 27 | } 28 | 29 | func TestFromJSONCreateProductRequest_JSONDecodeError(t *testing.T) { 30 | itemRequest, err := dto.FromJSONCreateProductRequest(strings.NewReader("{")) 31 | 32 | require.NotNil(t, err) 33 | require.Nil(t, itemRequest) 34 | } 35 | -------------------------------------------------------------------------------- /core/usecase/productusecase/create.go: -------------------------------------------------------------------------------- 1 | package productusecase 2 | 3 | import ( 4 | "github.com/boooscaaa/clean-go/core/domain" 5 | "github.com/boooscaaa/clean-go/core/dto" 6 | ) 7 | 8 | func (usecase usecase) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) { 9 | product, err := usecase.repository.Create(productRequest) 10 | 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return product, nil 16 | } 17 | -------------------------------------------------------------------------------- /core/usecase/productusecase/create_test.go: -------------------------------------------------------------------------------- 1 | package productusecase_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/boooscaaa/clean-go/core/domain" 8 | "github.com/boooscaaa/clean-go/core/domain/mocks" 9 | "github.com/boooscaaa/clean-go/core/dto" 10 | "github.com/boooscaaa/clean-go/core/usecase/productusecase" 11 | "github.com/bxcodec/faker/v3" 12 | "github.com/golang/mock/gomock" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestCreate(t *testing.T) { 17 | fakeRequestProduct := dto.CreateProductRequest{} 18 | fakeDBProduct := domain.Product{} 19 | faker.FakeData(&fakeRequestProduct) 20 | faker.FakeData(&fakeDBProduct) 21 | 22 | mockCtrl := gomock.NewController(t) 23 | defer mockCtrl.Finish() 24 | mockProductRepository := mocks.NewMockProductRepository(mockCtrl) 25 | mockProductRepository.EXPECT().Create(&fakeRequestProduct).Return(&fakeDBProduct, nil) 26 | 27 | sut := productusecase.New(mockProductRepository) 28 | product, err := sut.Create(&fakeRequestProduct) 29 | 30 | require.Nil(t, err) 31 | require.NotEmpty(t, product.ID) 32 | require.Equal(t, product.Name, fakeDBProduct.Name) 33 | require.Equal(t, product.Price, fakeDBProduct.Price) 34 | require.Equal(t, product.Description, fakeDBProduct.Description) 35 | } 36 | 37 | func TestCreate_Error(t *testing.T) { 38 | fakeRequestProduct := dto.CreateProductRequest{} 39 | faker.FakeData(&fakeRequestProduct) 40 | 41 | mockCtrl := gomock.NewController(t) 42 | defer mockCtrl.Finish() 43 | mockProductRepository := mocks.NewMockProductRepository(mockCtrl) 44 | mockProductRepository.EXPECT().Create(&fakeRequestProduct).Return(nil, fmt.Errorf("ANY ERROR")) 45 | 46 | sut := productusecase.New(mockProductRepository) 47 | product, err := sut.Create(&fakeRequestProduct) 48 | 49 | require.NotNil(t, err) 50 | require.Nil(t, product) 51 | } 52 | -------------------------------------------------------------------------------- /core/usecase/productusecase/fetch.go: -------------------------------------------------------------------------------- 1 | package productusecase 2 | 3 | import ( 4 | "github.com/boooscaaa/clean-go/core/domain" 5 | "github.com/boooscaaa/clean-go/core/dto" 6 | ) 7 | 8 | func (usecase usecase) Fetch(paginationRequest *dto.PaginationRequestParms) (*domain.Pagination, error) { 9 | products, err := usecase.repository.Fetch(paginationRequest) 10 | 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return products, nil 16 | } 17 | -------------------------------------------------------------------------------- /core/usecase/productusecase/fetch_test.go: -------------------------------------------------------------------------------- 1 | package productusecase_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/boooscaaa/clean-go/core/domain" 8 | "github.com/boooscaaa/clean-go/core/domain/mocks" 9 | "github.com/boooscaaa/clean-go/core/dto" 10 | "github.com/boooscaaa/clean-go/core/usecase/productusecase" 11 | "github.com/bxcodec/faker/v3" 12 | "github.com/golang/mock/gomock" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestFetch(t *testing.T) { 17 | fakePaginationRequestParams := dto.PaginationRequestParms{ 18 | Page: 1, 19 | ItemsPerPage: 10, 20 | Sort: nil, 21 | Descending: nil, 22 | Search: "", 23 | } 24 | fakeDBProduct := domain.Product{} 25 | 26 | faker.FakeData(&fakeDBProduct) 27 | 28 | mockCtrl := gomock.NewController(t) 29 | defer mockCtrl.Finish() 30 | mockProductRepository := mocks.NewMockProductRepository(mockCtrl) 31 | mockProductRepository.EXPECT().Fetch(&fakePaginationRequestParams).Return(&domain.Pagination{ 32 | Items: []domain.Product{fakeDBProduct}, 33 | Total: 1, 34 | }, nil) 35 | 36 | sut := productusecase.New(mockProductRepository) 37 | products, err := sut.Fetch(&fakePaginationRequestParams) 38 | 39 | require.Nil(t, err) 40 | 41 | for _, product := range products.Items.([]domain.Product) { 42 | require.Nil(t, err) 43 | require.NotEmpty(t, product.ID) 44 | require.Equal(t, product.Name, fakeDBProduct.Name) 45 | require.Equal(t, product.Price, fakeDBProduct.Price) 46 | require.Equal(t, product.Description, fakeDBProduct.Description) 47 | } 48 | } 49 | 50 | func TestFetch_Error(t *testing.T) { 51 | fakePaginationRequestParams := dto.PaginationRequestParms{ 52 | Page: 1, 53 | ItemsPerPage: 10, 54 | Sort: nil, 55 | Descending: nil, 56 | Search: "", 57 | } 58 | 59 | mockCtrl := gomock.NewController(t) 60 | defer mockCtrl.Finish() 61 | mockProductRepository := mocks.NewMockProductRepository(mockCtrl) 62 | mockProductRepository.EXPECT().Fetch(&fakePaginationRequestParams).Return(nil, fmt.Errorf("ANY ERROR")) 63 | 64 | sut := productusecase.New(mockProductRepository) 65 | product, err := sut.Fetch(&fakePaginationRequestParams) 66 | 67 | require.NotNil(t, err) 68 | require.Nil(t, product) 69 | } 70 | -------------------------------------------------------------------------------- /core/usecase/productusecase/new.go: -------------------------------------------------------------------------------- 1 | package productusecase 2 | 3 | import "github.com/boooscaaa/clean-go/core/domain" 4 | 5 | type usecase struct { 6 | repository domain.ProductRepository 7 | } 8 | 9 | // New returns contract implementation of ProductUseCase 10 | func New(repository domain.ProductRepository) domain.ProductUseCase { 11 | return &usecase{ 12 | repository: repository, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/migrations/000001_create_product_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS product; -------------------------------------------------------------------------------- /database/migrations/000001_create_product_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE product ( 2 | id SERIAL PRIMARY KEY NOT NULL, 3 | name VARCHAR(50) NOT NULL, 4 | price FLOAT NOT NULL, 5 | description VARCHAR(500) NOT NULL 6 | ); -------------------------------------------------------------------------------- /di/product.go: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import ( 4 | "github.com/boooscaaa/clean-go/adapter/http/graphql/productgraphqlservice" 5 | "github.com/boooscaaa/clean-go/adapter/http/rest/productservice" 6 | "github.com/boooscaaa/clean-go/adapter/postgres" 7 | "github.com/boooscaaa/clean-go/adapter/postgres/productrepository" 8 | "github.com/boooscaaa/clean-go/core/domain" 9 | "github.com/boooscaaa/clean-go/core/usecase/productusecase" 10 | ) 11 | 12 | // ConfigProductDI return a ProductService abstraction with dependency injection configuration 13 | func ConfigProductDI(conn postgres.PoolInterface) domain.ProductService { 14 | productRepository := productrepository.New(conn) 15 | productUseCase := productusecase.New(productRepository) 16 | productService := productservice.New(productUseCase) 17 | 18 | return productService 19 | } 20 | 21 | // ConfigProductGraphQLDI return a PoductGraphQLService abstraction with dependency injection configuration 22 | func ConfigProductGraphQLDI(conn postgres.PoolInterface) domain.PoductGraphQLService { 23 | productRepository := productrepository.New(conn) 24 | productUseCase := productusecase.New(productRepository) 25 | productService := productgraphqlservice.New(productUseCase) 26 | 27 | return productService 28 | } 29 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for cleango on 2022-08-27T11:09:30-03:00 2 | 3 | app = "cleango" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [env] 9 | 10 | [experimental] 11 | allowed_public_ports = [] 12 | auto_rollback = true 13 | 14 | [[services]] 15 | http_checks = [] 16 | internal_port = 3000 17 | processes = ["app"] 18 | protocol = "tcp" 19 | script_checks = [] 20 | [services.concurrency] 21 | hard_limit = 25 22 | soft_limit = 20 23 | type = "connections" 24 | 25 | [[services.ports]] 26 | force_https = true 27 | handlers = ["http"] 28 | port = 80 29 | 30 | [[services.ports]] 31 | handlers = ["tls", "http"] 32 | port = 443 33 | 34 | [[services.tcp_checks]] 35 | grace_period = "1s" 36 | interval = "15s" 37 | restart_limit = 0 38 | timeout = "2s" 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/boooscaaa/clean-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/booscaaa/go-paginate v0.0.6 7 | github.com/bxcodec/faker/v3 v3.8.0 8 | github.com/golang-migrate/migrate/v4 v4.15.1 9 | github.com/golang/mock v1.6.0 10 | github.com/gorilla/mux v1.8.0 11 | github.com/jackc/pgconn v1.11.0 12 | github.com/jackc/pgx/v4 v4.15.0 13 | github.com/pashagolub/pgxmock v1.4.4 14 | github.com/spf13/viper v1.10.1 15 | github.com/stretchr/testify v1.7.0 16 | github.com/swaggo/http-swagger v1.2.5 17 | github.com/swaggo/swag v1.8.0 18 | ) 19 | 20 | require ( 21 | github.com/KyleBanks/depth v1.2.1 // indirect 22 | github.com/PuerkitoBio/purell v1.1.1 // indirect 23 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fsnotify/fsnotify v1.5.1 // indirect 26 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 27 | github.com/go-openapi/jsonreference v0.19.6 // indirect 28 | github.com/go-openapi/spec v0.20.4 // indirect 29 | github.com/go-openapi/swag v0.19.15 // indirect 30 | github.com/graphql-go/graphql v0.8.0 31 | github.com/hashicorp/errwrap v1.0.0 // indirect 32 | github.com/hashicorp/go-multierror v1.1.0 // indirect 33 | github.com/hashicorp/hcl v1.0.0 // indirect 34 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 35 | github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 // indirect 36 | github.com/jackc/pgio v1.0.0 // indirect 37 | github.com/jackc/pgpassfile v1.0.0 // indirect 38 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect 39 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 40 | github.com/jackc/pgtype v1.10.0 // indirect 41 | github.com/jackc/puddle v1.2.1 // indirect 42 | github.com/josharian/intern v1.0.0 // indirect 43 | github.com/magiconair/properties v1.8.5 // indirect 44 | github.com/mailru/easyjson v0.7.6 // indirect 45 | github.com/mitchellh/mapstructure v1.4.3 // indirect 46 | github.com/pelletier/go-toml v1.9.4 // indirect 47 | github.com/pmezard/go-difflib v1.0.0 // indirect 48 | github.com/spf13/afero v1.6.0 // indirect 49 | github.com/spf13/cast v1.4.1 // indirect 50 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/subosito/gotenv v1.2.0 // indirect 53 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect 54 | go.uber.org/atomic v1.6.0 // indirect 55 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect 56 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 57 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect 58 | golang.org/x/text v0.3.7 // indirect 59 | golang.org/x/tools v0.1.7 // indirect 60 | gopkg.in/ini.v1 v1.66.2 // indirect 61 | gopkg.in/yaml.v2 v2.4.0 // indirect 62 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 63 | ) 64 | --------------------------------------------------------------------------------