├── .dockerignore ├── .editorconfig ├── .env.dist ├── .envrc ├── .github ├── .editorconfig └── workflows │ └── ci.yaml ├── .gitignore ├── .golangci.yaml ├── CHANGELOG.md ├── Dockerfile ├── INSPIRATION.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── modern-go-application │ ├── config.go │ ├── config_test.go │ └── main.go └── todocli │ └── main.go ├── config.toml.dist ├── config.yaml.dist ├── doc.go ├── docker-compose.override.yml.dist ├── docker-compose.yml ├── etc ├── ansible │ ├── README.md │ └── roles │ │ ├── application-setup │ │ ├── .yamllint │ │ ├── README.md │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── molecule │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ └── default │ │ │ │ ├── Dockerfile.j2 │ │ │ │ ├── INSTALL.rst │ │ │ │ ├── molecule.yml │ │ │ │ ├── playbook.yml │ │ │ │ ├── prepare.yml │ │ │ │ └── tests │ │ │ │ └── test_default.py │ │ ├── tasks │ │ │ ├── app.yml │ │ │ ├── ingress.yml │ │ │ ├── main.yml │ │ │ └── user.yml │ │ └── templates │ │ │ ├── config.env │ │ │ ├── config.toml │ │ │ └── nginx.conf │ │ └── deployment │ │ ├── .yamllint │ │ ├── README.md │ │ ├── defaults │ │ └── main.yml │ │ ├── meta │ │ └── main.yml │ │ ├── molecule │ │ ├── .editorconfig │ │ ├── .gitignore │ │ └── default │ │ │ ├── Dockerfile.j2 │ │ │ ├── INSTALL.rst │ │ │ ├── molecule.yml │ │ │ ├── playbook.yml │ │ │ ├── prepare.yml │ │ │ ├── requirements.yml │ │ │ └── tests │ │ │ └── test_default.py │ │ └── tasks │ │ └── main.yml ├── loadgen │ ├── Dockerfile │ ├── README.md │ ├── loadgen.sh │ ├── locustfile.py │ └── requirements.txt └── local │ ├── grafana │ └── provisioning │ │ └── datasources │ │ └── datasource.yaml │ ├── opencensus │ ├── agent.yaml │ └── collector.yaml │ └── prometheus │ └── prometheus.yml ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── init.sh ├── internal ├── app │ ├── mga │ │ ├── app.go │ │ ├── common.go │ │ ├── httpbin │ │ │ ├── httpbin.go │ │ │ └── interfaces.go │ │ ├── landing │ │ │ └── landingdriver │ │ │ │ └── http_transport.go │ │ └── todo │ │ │ ├── README.md │ │ │ ├── common.go │ │ │ ├── event_handlers.go │ │ │ ├── event_handlers_test.go │ │ │ ├── middleware.go │ │ │ ├── service.go │ │ │ ├── todoadapter │ │ │ ├── ent │ │ │ │ ├── client.go │ │ │ │ ├── config.go │ │ │ │ ├── context.go │ │ │ │ ├── ent.go │ │ │ │ ├── enttest │ │ │ │ │ └── enttest.go │ │ │ │ ├── hook │ │ │ │ │ └── hook.go │ │ │ │ ├── migrate │ │ │ │ │ ├── migrate.go │ │ │ │ │ └── schema.go │ │ │ │ ├── mutation.go │ │ │ │ ├── predicate │ │ │ │ │ └── predicate.go │ │ │ │ ├── runtime.go │ │ │ │ ├── runtime │ │ │ │ │ └── runtime.go │ │ │ │ ├── schema │ │ │ │ │ └── todoitem.go │ │ │ │ ├── todoitem.go │ │ │ │ ├── todoitem │ │ │ │ │ ├── todoitem.go │ │ │ │ │ └── where.go │ │ │ │ ├── todoitem_create.go │ │ │ │ ├── todoitem_delete.go │ │ │ │ ├── todoitem_query.go │ │ │ │ ├── todoitem_update.go │ │ │ │ └── tx.go │ │ │ └── store_ent.go │ │ │ ├── tododriver │ │ │ └── middleware.go │ │ │ └── todogen │ │ │ ├── zz_generated.event_dispatcher.go │ │ │ └── zz_generated.event_handler.go │ └── todocli │ │ ├── command │ │ ├── add.go │ │ ├── cmd.go │ │ ├── complete.go │ │ └── list.go │ │ ├── configure.go │ │ └── context.go ├── common │ ├── commonadapter │ │ ├── logger.go │ │ └── logger_test.go │ ├── error_handler.go │ └── logger.go └── platform │ ├── appkit │ └── context.go │ ├── database │ ├── config.go │ ├── config_test.go │ ├── database.go │ └── logger.go │ ├── gosundheit │ └── logger.go │ ├── log │ ├── config.go │ ├── logger.go │ └── standard_logger.go │ ├── opencensus │ ├── exporter.go │ └── trace.go │ └── watermill │ ├── middleware.go │ ├── pubsub.go │ └── router.go ├── main.mk ├── pkg └── .gitkeep └── static └── templates ├── landing.html └── templates.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !.git/ 4 | !.gen/ 5 | !*.go 6 | !cmd/ 7 | !go.* 8 | !internal/ 9 | !pkg/ 10 | !static/ 11 | !Makefile 12 | !main.mk 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | 14 | [{Makefile, *.mk}] 15 | indent_style = tab 16 | 17 | [*.proto] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | LOG_FORMAT=logfmt 2 | LOG_LEVEL=debug 3 | TELEMETRY_ADDR=127.0.0.1:10000 4 | APP_HTTPADDR=127.0.0.1:8000 5 | APP_GRPCADDR=127.0.0.1:8001 6 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 1.5.1; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.5.1/direnvrc" "sha256-p4CDMJjuBmEh9pkn2aoJrZqr0DlPZHPU7eXOSDzzcuo=" 3 | fi 4 | use flake 5 | -------------------------------------------------------------------------------- /.github/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | env: 14 | VERBOSE: 1 15 | GOFLAGS: -mod=readonly 16 | 17 | steps: 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.17 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | - name: Build 27 | run: make build 28 | 29 | lint: 30 | name: Lint 31 | runs-on: ubuntu-latest 32 | env: 33 | VERBOSE: 1 34 | GOFLAGS: -mod=readonly 35 | 36 | steps: 37 | - name: Set up Go 38 | uses: actions/setup-go@v2 39 | with: 40 | go-version: 1.17 41 | 42 | - name: Checkout code 43 | uses: actions/checkout@v2 44 | 45 | - name: Lint 46 | run: make lint 47 | 48 | test: 49 | name: Test 50 | runs-on: ubuntu-latest 51 | env: 52 | VERBOSE: 1 53 | GOFLAGS: -mod=readonly 54 | 55 | steps: 56 | - name: Set up Go 57 | uses: actions/setup-go@v2 58 | with: 59 | go-version: 1.17 60 | 61 | - name: Checkout code 62 | uses: actions/checkout@v2 63 | 64 | - name: Test 65 | run: make test 66 | 67 | docker: 68 | name: Docker 69 | runs-on: ubuntu-latest 70 | steps: 71 | - name: Checkout code 72 | uses: actions/checkout@v2 73 | 74 | - name: Build image 75 | run: docker build -t docker.pkg.github.com/${GITHUB_REPOSITORY}/${{ github.event.repository.name }}:${GITHUB_SHA:0:7} . 76 | 77 | # - name: Tag image 78 | # run: docker tag docker.pkg.github.com/${GITHUB_REPOSITORY}/${{ github.event.repository.name }}:${GITHUB_SHA:0:7} docker.pkg.github.com/${GITHUB_REPOSITORY}/${{ github.event.repository.name }}:${GITHUB_REF#"refs/heads/"} 79 | # if: github.event_name == 'push' 80 | # 81 | # - name: Tag latest image 82 | # run: docker tag docker.pkg.github.com/${GITHUB_REPOSITORY}/${{ github.event.repository.name }}:${GITHUB_SHA:0:7} docker.pkg.github.com/${GITHUB_REPOSITORY}/${{ github.event.repository.name }}:latest 83 | # if: github.event_name == 'push' && github.ref == 'refs/heads/master' 84 | # 85 | # - name: Log in to registry 86 | # run: echo ${{ secrets.DOCKER_GITHUB_PASSWORD }} | docker login -u ${GITHUB_ACTOR} --password-stdin docker.pkg.github.com 87 | # if: github.event_name == 'push' 88 | # 89 | # - name: Push image 90 | # run: docker push docker.pkg.github.com/${GITHUB_REPOSITORY}/${{ github.event.repository.name }} 91 | # if: github.event_name == 'push' 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.direnv/ 2 | /bin/ 3 | /build/ 4 | /config.* 5 | !config.toml.dist 6 | !config.yaml.dist 7 | /docker-compose.override.yml 8 | /.env 9 | /.env.test 10 | /var/ 11 | /vendor/ 12 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - .gen 4 | 5 | skip-files: 6 | - ".*_gen\\.go$" 7 | 8 | linters-settings: 9 | golint: 10 | min-confidence: 0.1 11 | goimports: 12 | local-prefixes: github.com/sagikazarmark/modern-go-application 13 | 14 | linters: 15 | enable-all: true 16 | disable: 17 | - funlen 18 | - maligned 19 | - wsl 20 | - gomnd 21 | - testpackage 22 | - goerr113 23 | 24 | # Drives todos nuts 25 | - godox 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 7 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 8 | 9 | 10 | ## [Unreleased] 11 | 12 | 13 | [Unreleased]: https://github.com/sagikazarmark/modern-go-application/compare/v0.0.0...HEAD 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine3.14 AS builder 2 | 3 | ENV GOFLAGS="-mod=readonly" 4 | 5 | RUN apk add --update --no-cache ca-certificates make git curl mercurial 6 | 7 | RUN mkdir -p /workspace 8 | WORKDIR /workspace 9 | 10 | ARG GOPROXY 11 | 12 | COPY go.* ./ 13 | RUN go mod download 14 | 15 | ARG BUILD_TARGET 16 | 17 | COPY Makefile *.mk ./ 18 | 19 | RUN if [[ "${BUILD_TARGET}" == "debug" ]]; then make build-debug-deps; else make build-release-deps; fi 20 | 21 | COPY . . 22 | 23 | RUN set -xe && \ 24 | if [[ "${BUILD_TARGET}" == "debug" ]]; then \ 25 | cd /tmp; GOBIN=/workspace/build/debug go get github.com/go-delve/delve/cmd/dlv; cd -; \ 26 | make build-debug; \ 27 | mv build/debug /build; \ 28 | else \ 29 | make build-release; \ 30 | mv build/release /build; \ 31 | fi 32 | 33 | 34 | FROM alpine:3.14 35 | 36 | RUN apk add --update --no-cache ca-certificates tzdata bash curl 37 | 38 | SHELL ["/bin/bash", "-c"] 39 | 40 | # set up nsswitch.conf for Go's "netgo" implementation 41 | # https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-424546457 42 | RUN test ! -e /etc/nsswitch.conf && echo 'hosts: files dns' > /etc/nsswitch.conf 43 | 44 | ARG BUILD_TARGET 45 | 46 | RUN if [[ "${BUILD_TARGET}" == "debug" ]]; then apk add --update --no-cache libc6-compat; fi 47 | 48 | COPY --from=builder /build/* /usr/local/bin/ 49 | 50 | EXPOSE 8000 8001 10000 51 | CMD ["modern-go-application", "--telemetry-addr", ":10000", "--http-addr", ":8000", "--grpc-addr", ":8001"] 52 | -------------------------------------------------------------------------------- /INSPIRATION.md: -------------------------------------------------------------------------------- 1 | # Inspiration 2 | 3 | Some articles, projects, code examples, questions that somehow inspired me and added something to this project. 4 | 5 | 6 | ### Distributed tracing 7 | 8 | https://github.com/basvanbeek/opencensus-gokit-example 9 | https://medium.com/@rghetia/distributed-tracing-and-monitoring-using-opencensus-fe5f6e9479fb 10 | https://github.com/census-ecosystem/opencensus-microservices-demo 11 | 12 | 13 | ### Docker 14 | 15 | https://medium.com/@pierreprinetti/the-go-1-11-dockerfile-a3218319d191 16 | 17 | 18 | ### Testing 19 | 20 | https://docs.gitlab.com/ee/ci/junit_test_reports.html 21 | https://circleci.com/docs/2.0/collect-test-data/ 22 | 23 | 24 | ### Misc 25 | 26 | https://kvz.io/blog/2013/11/21/bash-best-practices/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Márk Sági-Kazár 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include main.mk 2 | 3 | # Project variables 4 | OPENAPI_DESCRIPTOR_DIR = api/openapi 5 | 6 | # Dependency versions 7 | MGA_VERSION = 0.2.0 8 | 9 | .PHONY: up 10 | up: start config.toml ## Set up the development environment 11 | 12 | .PHONY: down 13 | down: clear ## Destroy the development environment 14 | docker-compose down --volumes --remove-orphans --rmi local 15 | rm -rf var/docker/volumes/* 16 | 17 | .PHONY: reset 18 | reset: down up ## Reset the development environment 19 | 20 | docker-compose.override.yml: 21 | cp docker-compose.override.yml.dist docker-compose.override.yml 22 | 23 | .PHONY: start 24 | start: docker-compose.override.yml ## Start docker development environment 25 | @ if [ docker-compose.override.yml -ot docker-compose.override.yml.dist ]; then diff -u docker-compose.override.yml docker-compose.override.yml.dist || (echo "!!! The distributed docker-compose.override.yml example changed. Please update your file accordingly (or at least touch it). !!!" && false); fi 26 | docker-compose up -d 27 | 28 | .PHONY: stop 29 | stop: ## Stop docker development environment 30 | docker-compose stop 31 | 32 | config.toml: 33 | sed 's/production/development/g; s/debug = false/debug = true/g; s/shutdownTimeout = "15s"/shutdownTimeout = "0s"/g; s/format = "json"/format = "logfmt"/g; s/level = "info"/level = "debug"/g; s/addr = ":10000"/addr = "127.0.0.1:10000"/g; s/httpAddr = ":8000"/httpAddr = "127.0.0.1:8000"/g; s/grpcAddr = ":8001"/grpcAddr = "127.0.0.1:8001"/g' config.toml.dist > config.toml 34 | 35 | bin/entc: 36 | @mkdir -p bin 37 | go build -o bin/entc github.com/facebook/ent/cmd/entc 38 | 39 | bin/mga: bin/mga-${MGA_VERSION} 40 | @ln -sf mga-${MGA_VERSION} bin/mga 41 | bin/mga-${MGA_VERSION}: 42 | @mkdir -p bin 43 | curl -sfL https://git.io/mgatool | bash -s v${MGA_VERSION} 44 | @mv bin/mga $@ 45 | 46 | .PHONY: generate 47 | generate: bin/mga bin/entc ## Generate code 48 | go generate -x ./... 49 | mga generate kit endpoint ./internal/app/mga/todo/... 50 | mga generate event handler --output subpkg:suffix=gen ./internal/app/mga/todo/... 51 | mga generate event dispatcher --output subpkg:suffix=gen ./internal/app/mga/todo/... 52 | entc generate ./internal/app/mga/todo/todoadapter/ent/schema 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern Go Application 2 | 3 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#project-layout) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/sagikazarmark/modern-go-application?style=flat-square)](https://goreportcard.com/report/github.com/sagikazarmark/modern-go-application) 5 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/sagikazarmark/modern-go-application) 6 | 7 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/sagikazarmark/modern-go-application/CI?style=flat-square) 8 | [![CircleCI](https://circleci.com/gh/sagikazarmark/modern-go-application.svg?style=svg)](https://circleci.com/gh/sagikazarmark/modern-go-application) 9 | [![Gitlab](https://img.shields.io/badge/gitlab-sagikazarmark%2Fmodern--go--application-orange.svg?logo=gitlab&longCache=true&style=flat-square)](https://gitlab.com/sagikazarmark/modern-go-application) 10 | 11 | **Go application boilerplate and example applying modern practices** 12 | 13 | This repository tries to collect the best practices of application development using Go language. 14 | In addition to the language specific details, it also implements various language independent practices. 15 | 16 | Some of the areas Modern Go Application touches: 17 | 18 | - architecture 19 | - package structure 20 | - building the application 21 | - testing 22 | - configuration 23 | - running the application (eg. in Docker) 24 | - developer environment/experience 25 | - telemetry 26 | 27 | To help adopting these practices, this repository also serves as a boilerplate for new applications. 28 | 29 | 30 | ## Features 31 | 32 | - configuration (using [spf13/viper](https://github.com/spf13/viper)) 33 | - logging (using [logur.dev/logur](https://logur.dev/logur) and [sirupsen/logrus](https://github.com/sirupsen/logrus)) 34 | - error handling (using [emperror.dev/emperror](https://emperror.dev/emperror)) 35 | - metrics and tracing using [Prometheus](https://prometheus.io/) and [Jaeger](https://www.jaegertracing.io/) (via [OpenCensus](https://opencensus.io/)) 36 | - health checks (using [AppsFlyer/go-sundheit](https://github.com/AppsFlyer/go-sundheit)) 37 | - graceful restart (using [cloudflare/tableflip](https://github.com/cloudflare/tableflip)) and shutdown 38 | - support for multiple server/daemon instances (using [oklog/run](https://github.com/oklog/run)) 39 | - messaging (using [ThreeDotsLabs/watermill](https://github.com/ThreeDotsLabs/watermill)) 40 | - MySQL database connection (using [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)) 41 | - ~~Redis connection (using [gomodule/redigo](https://github.com/gomodule/redigo))~~ removed due to lack of usage (see [#120](../../issues/120)) 42 | 43 | 44 | ## First steps 45 | 46 | To create a new application from the boilerplate clone this repository (if you haven't done already) into your GOPATH 47 | then execute the following: 48 | 49 | ```bash 50 | chmod +x init.sh && ./init.sh 51 | ? Package name (github.com/sagikazarmark/modern-go-application) 52 | ? Project name (modern-go-application) 53 | ? Binary name (modern-go-application) 54 | ? Service name (modern-go-application) 55 | ? Friendly service name (Modern Go Application) 56 | ? Update README (Y/n) 57 | ? Remove init script (y/N) y 58 | ``` 59 | 60 | It updates every import path and name in the repository to your project's values. 61 | **Review** and commit the changes. 62 | 63 | 64 | ### Load generation 65 | 66 | To test or demonstrate the application it comes with a simple load generation tool. 67 | You can use it to test the example endpoints and generate some load (for example in order to fill dashboards with data). 68 | 69 | Follow the instructions in [etc/loadgen](etc/loadgen). 70 | 71 | 72 | ## Inspiration 73 | 74 | See [INSPIRATION.md](INSPIRATION.md) for links to articles, projects, code examples that somehow inspired 75 | me while working on this project. 76 | 77 | 78 | ## License 79 | 80 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 81 | -------------------------------------------------------------------------------- /cmd/modern-go-application/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/spf13/pflag" 10 | "github.com/spf13/viper" 11 | 12 | "github.com/sagikazarmark/modern-go-application/internal/platform/database" 13 | "github.com/sagikazarmark/modern-go-application/internal/platform/log" 14 | "github.com/sagikazarmark/modern-go-application/internal/platform/opencensus" 15 | ) 16 | 17 | // configuration holds any kind of configuration that comes from the outside world and 18 | // is necessary for running the application. 19 | type configuration struct { 20 | // Log configuration 21 | Log log.Config 22 | 23 | // Telemetry configuration 24 | Telemetry struct { 25 | // Telemetry HTTP server address 26 | Addr string 27 | } 28 | 29 | // OpenCensus configuration 30 | Opencensus struct { 31 | Exporter struct { 32 | Enabled bool 33 | 34 | opencensus.ExporterConfig `mapstructure:",squash"` 35 | } 36 | 37 | Trace opencensus.TraceConfig 38 | } 39 | 40 | // App configuration 41 | App appConfig 42 | 43 | // Database connection information 44 | Database database.Config 45 | } 46 | 47 | // Process post-processes configuration after loading it. 48 | func (configuration) Process() error { 49 | return nil 50 | } 51 | 52 | // Validate validates the configuration. 53 | func (c configuration) Validate() error { 54 | if c.Telemetry.Addr == "" { 55 | return errors.New("telemetry http server address is required") 56 | } 57 | 58 | if err := c.App.Validate(); err != nil { 59 | return err 60 | } 61 | 62 | if err := c.Database.Validate(); err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // appConfig represents the application related configuration. 70 | type appConfig struct { 71 | // HTTP server address 72 | // nolint: golint, stylecheck 73 | HttpAddr string 74 | 75 | // GRPC server address 76 | GrpcAddr string 77 | 78 | // Storage is the storage backend of the application 79 | Storage string 80 | } 81 | 82 | // Validate validates the configuration. 83 | func (c appConfig) Validate() error { 84 | if c.HttpAddr == "" { 85 | return errors.New("http app server address is required") 86 | } 87 | 88 | if c.GrpcAddr == "" { 89 | return errors.New("grpc app server address is required") 90 | } 91 | 92 | if c.Storage != "inmemory" && c.Storage != "database" { 93 | return errors.New("app storage must be inmemory or database") 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // configure configures some defaults in the Viper instance. 100 | func configure(v *viper.Viper, f *pflag.FlagSet) { 101 | // Viper settings 102 | v.AddConfigPath(".") 103 | v.AddConfigPath("$CONFIG_DIR/") 104 | 105 | // Environment variable settings 106 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 107 | v.AllowEmptyEnv(true) 108 | v.AutomaticEnv() 109 | 110 | // Global configuration 111 | v.SetDefault("shutdownTimeout", 15*time.Second) 112 | if _, ok := os.LookupEnv("NO_COLOR"); ok { 113 | v.SetDefault("no_color", true) 114 | } 115 | 116 | // Log configuration 117 | v.SetDefault("log.format", "json") 118 | v.SetDefault("log.level", "info") 119 | v.RegisterAlias("log.noColor", "no_color") 120 | 121 | // Telemetry configuration 122 | f.String("telemetry-addr", ":10000", "Telemetry HTTP server address") 123 | _ = v.BindPFlag("telemetry.addr", f.Lookup("telemetry-addr")) 124 | v.SetDefault("telemetry.addr", ":10000") 125 | 126 | // OpenCensus configuration 127 | v.SetDefault("opencensus.exporter.enabled", false) 128 | _ = v.BindEnv("opencensus.exporter.address") 129 | _ = v.BindEnv("opencensus.exporter.insecure") 130 | _ = v.BindEnv("opencensus.exporter.reconnectPeriod") 131 | v.SetDefault("opencensus.trace.sampling.sampler", "never") 132 | v.SetDefault("opencensus.prometheus.enabled", false) 133 | 134 | // App configuration 135 | f.String("http-addr", ":8000", "App HTTP server address") 136 | _ = v.BindPFlag("app.httpAddr", f.Lookup("http-addr")) 137 | v.SetDefault("app.httpAddr", ":8000") 138 | 139 | f.String("grpc-addr", ":8001", "App GRPC server address") 140 | _ = v.BindPFlag("app.grpcAddr", f.Lookup("grpc-addr")) 141 | v.SetDefault("app.grpcAddr", ":8001") 142 | 143 | v.SetDefault("app.storage", "inmemory") 144 | 145 | // Database configuration 146 | _ = v.BindEnv("database.host") 147 | v.SetDefault("database.port", 3306) 148 | _ = v.BindEnv("database.user") 149 | _ = v.BindEnv("database.pass") 150 | _ = v.BindEnv("database.name") 151 | v.SetDefault("database.params", map[string]string{ 152 | "collation": "utf8mb4_general_ci", 153 | }) 154 | } 155 | -------------------------------------------------------------------------------- /cmd/modern-go-application/config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/spf13/pflag" 8 | "github.com/spf13/viper" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestConfigure(t *testing.T) { 13 | var config configuration 14 | 15 | v := viper.New() 16 | p := pflag.NewFlagSet("test", pflag.ContinueOnError) 17 | 18 | configure(v, p) 19 | 20 | file, err := os.Open("../../config.toml.dist") 21 | require.NoError(t, err) 22 | 23 | v.SetConfigType("toml") 24 | 25 | err = v.ReadConfig(file) 26 | require.NoError(t, err) 27 | 28 | err = v.Unmarshal(&config) 29 | require.NoError(t, err) 30 | 31 | err = config.Validate() 32 | require.NoError(t, err) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/modern-go-application/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "net/http" 8 | _ "net/http/pprof" // register pprof HTTP handlers #nosec 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "contrib.go.opencensus.io/exporter/ocagent" 15 | "contrib.go.opencensus.io/exporter/prometheus" 16 | "contrib.go.opencensus.io/integrations/ocsql" 17 | "emperror.dev/emperror" 18 | "emperror.dev/errors" 19 | "emperror.dev/errors/match" 20 | logurhandler "emperror.dev/handler/logur" 21 | health "github.com/AppsFlyer/go-sundheit" 22 | "github.com/AppsFlyer/go-sundheit/checks" 23 | healthhttp "github.com/AppsFlyer/go-sundheit/http" 24 | "github.com/cloudflare/tableflip" 25 | "github.com/gorilla/handlers" 26 | "github.com/gorilla/mux" 27 | "github.com/oklog/run" 28 | "github.com/sagikazarmark/appkit/buildinfo" 29 | appkiterrors "github.com/sagikazarmark/appkit/errors" 30 | appkitrun "github.com/sagikazarmark/appkit/run" 31 | "github.com/sagikazarmark/ocmux" 32 | "github.com/spf13/pflag" 33 | "github.com/spf13/viper" 34 | "go.opencensus.io/plugin/ocgrpc" 35 | "go.opencensus.io/plugin/ochttp" 36 | "go.opencensus.io/stats/view" 37 | "go.opencensus.io/trace" 38 | "go.opencensus.io/zpages" 39 | "google.golang.org/grpc" 40 | "logur.dev/logur" 41 | 42 | "github.com/sagikazarmark/modern-go-application/internal/app/mga" 43 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/tododriver" 44 | "github.com/sagikazarmark/modern-go-application/internal/common/commonadapter" 45 | "github.com/sagikazarmark/modern-go-application/internal/platform/appkit" 46 | "github.com/sagikazarmark/modern-go-application/internal/platform/database" 47 | "github.com/sagikazarmark/modern-go-application/internal/platform/gosundheit" 48 | "github.com/sagikazarmark/modern-go-application/internal/platform/log" 49 | "github.com/sagikazarmark/modern-go-application/internal/platform/watermill" 50 | ) 51 | 52 | // Provisioned by ldflags 53 | // nolint: gochecknoglobals 54 | var ( 55 | version string 56 | commitHash string 57 | buildDate string 58 | ) 59 | 60 | const ( 61 | // appName is an identifier-like name used anywhere this app needs to be identified. 62 | // 63 | // It identifies the application itself, the actual instance needs to be identified via environment 64 | // and other details. 65 | appName = "mga" 66 | 67 | // friendlyAppName is the visible name of the application. 68 | friendlyAppName = "Modern Go Application" 69 | ) 70 | 71 | func main() { 72 | v, f := viper.New(), pflag.NewFlagSet(friendlyAppName, pflag.ExitOnError) 73 | 74 | configure(v, f) 75 | 76 | f.String("config", "", "Configuration file") 77 | f.Bool("version", false, "Show version information") 78 | 79 | _ = f.Parse(os.Args[1:]) 80 | 81 | if v, _ := f.GetBool("version"); v { 82 | fmt.Printf("%s version %s (%s) built on %s\n", friendlyAppName, version, commitHash, buildDate) 83 | 84 | os.Exit(0) 85 | } 86 | 87 | if c, _ := f.GetString("config"); c != "" { 88 | v.SetConfigFile(c) 89 | } 90 | 91 | err := v.ReadInConfig() 92 | _, configFileNotFound := err.(viper.ConfigFileNotFoundError) 93 | if !configFileNotFound { 94 | emperror.Panic(errors.Wrap(err, "failed to read configuration")) 95 | } 96 | 97 | var config configuration 98 | err = v.Unmarshal(&config) 99 | emperror.Panic(errors.Wrap(err, "failed to unmarshal configuration")) 100 | 101 | err = config.Process() 102 | emperror.Panic(errors.WithMessage(err, "failed to process configuration")) 103 | 104 | // Create logger (first thing after configuration loading) 105 | logger := log.NewLogger(config.Log) 106 | 107 | // Override the global standard library logger to make sure everything uses our logger 108 | log.SetStandardLogger(logger) 109 | 110 | if configFileNotFound { 111 | logger.Warn("configuration file not found") 112 | } 113 | 114 | err = config.Validate() 115 | if err != nil { 116 | logger.Error(err.Error()) 117 | 118 | os.Exit(3) 119 | } 120 | 121 | // Configure error handler 122 | errorHandler := logurhandler.New(logger) 123 | defer emperror.HandleRecover(errorHandler) 124 | 125 | buildInfo := buildinfo.New(version, commitHash, buildDate) 126 | 127 | logger.Info("starting application", buildInfo.Fields()) 128 | 129 | telemetryRouter := http.NewServeMux() 130 | telemetryRouter.Handle("/buildinfo", buildinfo.HTTPHandler(buildInfo)) 131 | 132 | // Register pprof endpoints 133 | telemetryRouter.Handle("/debug/pprof/", http.DefaultServeMux) 134 | 135 | // Configure health checker 136 | healthChecker := health.New() 137 | healthChecker.WithCheckListener(gosundheit.NewLogger(logur.WithField(logger, "component", "healthcheck"))) 138 | { 139 | handler := healthhttp.HandleHealthJSON(healthChecker) 140 | telemetryRouter.Handle("/healthz", handler) 141 | 142 | // Kubernetes style health checks 143 | telemetryRouter.HandleFunc("/healthz/live", func(w http.ResponseWriter, _ *http.Request) { 144 | _, _ = w.Write([]byte("ok")) 145 | }) 146 | telemetryRouter.Handle("/healthz/ready", handler) 147 | } 148 | 149 | zpages.Handle(telemetryRouter, "/debug") 150 | 151 | trace.ApplyConfig(config.Opencensus.Trace.Config()) 152 | 153 | // Configure OpenCensus exporter 154 | if config.Opencensus.Exporter.Enabled { 155 | exporter, err := ocagent.NewExporter(append( 156 | config.Opencensus.Exporter.Options(), 157 | ocagent.WithServiceName(appName), 158 | )...) 159 | emperror.Panic(err) 160 | 161 | trace.RegisterExporter(exporter) 162 | view.RegisterExporter(exporter) 163 | } 164 | 165 | // Configure Prometheus exporter 166 | exporter, err := prometheus.NewExporter(prometheus.Options{ 167 | OnError: emperror.WithDetails( 168 | errorHandler, 169 | "component", "opencensus", 170 | "exporter", "prometheus", 171 | ).Handle, 172 | }) 173 | emperror.Panic(err) 174 | 175 | view.RegisterExporter(exporter) 176 | telemetryRouter.Handle("/metrics", exporter) 177 | 178 | // configure graceful restart 179 | upg, _ := tableflip.New(tableflip.Options{}) 180 | 181 | // Do an upgrade on SIGHUP 182 | go func() { 183 | ch := make(chan os.Signal, 1) 184 | signal.Notify(ch, syscall.SIGHUP) 185 | for range ch { 186 | logger.Info("graceful reloading") 187 | 188 | _ = upg.Upgrade() 189 | } 190 | }() 191 | 192 | var group run.Group 193 | 194 | // Set up telemetry server 195 | { 196 | const name = "telemetry" 197 | logger := logur.WithField(logger, "server", name) 198 | 199 | logger.Info("listening on address", map[string]interface{}{"address": config.Telemetry.Addr}) 200 | 201 | ln, err := upg.Fds.Listen("tcp", config.Telemetry.Addr) 202 | emperror.Panic(err) 203 | 204 | server := &http.Server{ 205 | Handler: telemetryRouter, 206 | ErrorLog: log.NewErrorStandardLogger(logger), 207 | } 208 | defer server.Close() 209 | 210 | group.Add( 211 | func() error { return server.Serve(ln) }, 212 | func(err error) { _ = server.Shutdown(context.Background()) }, 213 | ) 214 | } 215 | 216 | // Register SQL stat views 217 | ocsql.RegisterAllViews() 218 | 219 | // Connect to the database 220 | logger.Info("connecting to database") 221 | dbConnector, err := database.NewConnector(config.Database) 222 | emperror.Panic(err) 223 | 224 | database.SetLogger(logger) 225 | 226 | db := sql.OpenDB(dbConnector) 227 | defer db.Close() 228 | 229 | // Record DB stats every 5 seconds until we exit 230 | defer ocsql.RecordStats(db, 5*time.Second)() 231 | 232 | // Register database health check 233 | _ = healthChecker.RegisterCheck(&health.Config{ 234 | Check: checks.Must(checks.NewPingCheck("db.check", db, time.Millisecond*100)), 235 | ExecutionPeriod: 3 * time.Second, 236 | }) 237 | 238 | publisher, subscriber := watermill.NewPubSub(logger) 239 | defer publisher.Close() 240 | defer subscriber.Close() 241 | 242 | publisher = watermill.PublisherCorrelationID(publisher) 243 | subscriber = watermill.SubscriberCorrelationID(subscriber) 244 | 245 | // Register stat views 246 | err = view.Register( 247 | // Health checks 248 | health.ViewCheckCountByNameAndStatus, 249 | health.ViewCheckStatusByName, 250 | health.ViewCheckExecutionTime, 251 | 252 | // HTTP 253 | ochttp.ServerRequestCountView, 254 | ochttp.ServerRequestBytesView, 255 | ochttp.ServerResponseBytesView, 256 | ochttp.ServerLatencyView, 257 | ochttp.ServerRequestCountByMethod, 258 | ochttp.ServerResponseCountByStatusCode, 259 | 260 | // GRPC 261 | ocgrpc.ServerReceivedBytesPerRPCView, 262 | ocgrpc.ServerSentBytesPerRPCView, 263 | ocgrpc.ServerLatencyView, 264 | ocgrpc.ServerCompletedRPCsView, 265 | 266 | // Todo 267 | tododriver.CreatedTodoItemCountView, 268 | tododriver.CompleteTodoItemCountView, 269 | ) 270 | emperror.Panic(errors.Wrap(err, "failed to register stat views")) 271 | 272 | // Set up app server 273 | { 274 | const name = "app" 275 | logger := logur.WithField(logger, "server", name) 276 | 277 | httpRouter := mux.NewRouter() 278 | httpRouter.Use(ocmux.Middleware()) 279 | 280 | cors := handlers.CORS( 281 | handlers.AllowedOrigins([]string{"*"}), 282 | handlers.AllowedMethods([]string{http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete}), 283 | handlers.AllowedHeaders([]string{"content-type"}), 284 | ) 285 | 286 | httpServer := &http.Server{ 287 | Handler: &ochttp.Handler{ 288 | // Handler: httpRouter, 289 | Handler: cors(httpRouter), 290 | StartOptions: trace.StartOptions{ 291 | Sampler: trace.AlwaysSample(), 292 | SpanKind: trace.SpanKindServer, 293 | }, 294 | IsPublicEndpoint: true, 295 | }, 296 | ErrorLog: log.NewErrorStandardLogger(logger), 297 | } 298 | defer httpServer.Close() 299 | 300 | grpcServer := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{ 301 | StartOptions: trace.StartOptions{ 302 | Sampler: trace.AlwaysSample(), 303 | SpanKind: trace.SpanKindServer, 304 | }, 305 | IsPublicEndpoint: true, 306 | })) 307 | defer grpcServer.Stop() 308 | 309 | // In larger apps, this should be split up into smaller functions 310 | { 311 | logger := commonadapter.NewContextAwareLogger(logger, appkit.ContextExtractor) 312 | errorHandler := emperror.WithFilter( 313 | emperror.WithContextExtractor(errorHandler, appkit.ContextExtractor), 314 | appkiterrors.IsServiceError, // filter out service errors 315 | ) 316 | 317 | mga.InitializeApp(httpRouter, grpcServer, publisher, config.App.Storage, db, logger, errorHandler) 318 | 319 | h, err := watermill.NewRouter(logger) 320 | emperror.Panic(err) 321 | 322 | err = mga.RegisterEventHandlers(h, subscriber, logger) 323 | emperror.Panic(err) 324 | 325 | group.Add(func() error { return h.Run(context.Background()) }, func(e error) { _ = h.Close() }) 326 | } 327 | 328 | logger.Info("listening on address", map[string]interface{}{"address": config.App.HttpAddr}) 329 | 330 | httpLn, err := upg.Fds.Listen("tcp", config.App.HttpAddr) 331 | emperror.Panic(err) 332 | 333 | logger.Info("listening on address", map[string]interface{}{"address": config.App.GrpcAddr}) 334 | 335 | grpcLn, err := upg.Fds.Listen("tcp", config.App.GrpcAddr) 336 | emperror.Panic(err) 337 | 338 | group.Add( 339 | func() error { return httpServer.Serve(httpLn) }, 340 | func(err error) { _ = httpServer.Shutdown(context.Background()) }, 341 | ) 342 | group.Add( 343 | func() error { return grpcServer.Serve(grpcLn) }, 344 | func(err error) { grpcServer.GracefulStop() }, 345 | ) 346 | } 347 | 348 | // Setup signal handler 349 | group.Add(run.SignalHandler(context.Background(), syscall.SIGINT, syscall.SIGTERM)) 350 | 351 | // Setup graceful restart 352 | group.Add(appkitrun.GracefulRestart(context.Background(), upg)) 353 | 354 | err = group.Run() 355 | emperror.WithFilter(errorHandler, match.As(&run.SignalError{}).MatchError).Handle(err) 356 | } 357 | -------------------------------------------------------------------------------- /cmd/todocli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/sagikazarmark/modern-go-application/internal/app/todocli" 10 | ) 11 | 12 | func main() { 13 | rootCmd := &cobra.Command{ 14 | Use: "todocli", 15 | Short: "TODO CLI manages TODOs.", 16 | } 17 | 18 | todocli.Configure(rootCmd) 19 | 20 | if err := rootCmd.Execute(); err != nil { 21 | fmt.Println(err) 22 | 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config.toml.dist: -------------------------------------------------------------------------------- 1 | [log] 2 | format = "json" 3 | level = "info" 4 | 5 | [telemetry] 6 | addr = ":10000" 7 | 8 | [opencensus.exporter] 9 | enabled = false 10 | address = "127.0.0.1:55678" 11 | insecure = false 12 | reconnectPeriod = "5s" 13 | 14 | [opencensus.trace] 15 | sampling = { sampler = "always" } 16 | # sampling = { sampler = "probability", fraction = 0.5 } 17 | 18 | [opencensus.prometheus] 19 | enabled = false 20 | 21 | [app] 22 | httpAddr = ":8000" 23 | grpcAddr = ":8001" 24 | 25 | storage = "inmemory" 26 | 27 | [database] 28 | host = "localhost" 29 | port = 3306 30 | user = "root" 31 | pass = "" 32 | name = "app" 33 | params = { collation = "utf8mb4_general_ci" } 34 | -------------------------------------------------------------------------------- /config.yaml.dist: -------------------------------------------------------------------------------- 1 | log: 2 | format: "json" 3 | level: "info" 4 | 5 | telemetry: 6 | addr: ":10000" 7 | 8 | opencensus: 9 | exporter: 10 | enabled: false 11 | address: "127.0.0.1:55678" 12 | insecure: false 13 | reconnectPeriod: "5s" 14 | 15 | trace: 16 | sampling: 17 | sampler: "always" # probability 18 | # fraction: 0.5 19 | 20 | prometheus: 21 | enabled: false 22 | 23 | app: 24 | httpAddr: ":8000" 25 | grpcAddr: ":8001" 26 | 27 | storage: "inmemory" 28 | 29 | database: 30 | host: "localhost" 31 | port: 3306 32 | user: "root" 33 | pass: "" 34 | name: "app" 35 | params: 36 | collation: "utf8mb4_general_ci" 37 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package modern_go_application 3 | -------------------------------------------------------------------------------- /docker-compose.override.yml.dist: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | db: 5 | ports: 6 | - 127.0.0.1:3306:3306 7 | volumes: 8 | - ./var/docker/volumes/mysql:/var/lib/mysql 9 | 10 | prometheus: 11 | ports: 12 | - 127.0.0.1:9090:9090 13 | volumes: 14 | - ./var/docker/volumes/prometheus:/prometheus 15 | 16 | grafana: 17 | ports: 18 | - 127.0.0.1:3000:3000 19 | volumes: 20 | - ./var/docker/volumes/grafana:/var/lib/grafana 21 | 22 | jaeger: 23 | ports: 24 | - 127.0.0.1:14268:14268 25 | - 127.0.0.1:16686:16686 26 | 27 | oc-collector: 28 | ports: 29 | - 127.0.0.1:55680:55679 30 | 31 | oc-agent: 32 | ports: 33 | - 127.0.0.1:55678:55678 34 | - 127.0.0.1:55679:55679 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | db: 5 | image: mysql:8.0 6 | command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 7 | environment: 8 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 9 | MYSQL_DATABASE: app 10 | 11 | dockerhost: 12 | image: qoomon/docker-host:2.3.0 13 | cap_add: [ 'NET_ADMIN', 'NET_RAW' ] 14 | 15 | prometheus: 16 | image: prom/prometheus:v2.12.0 17 | volumes: 18 | - ./etc/local/prometheus/:/etc/prometheus/ 19 | command: 20 | - '--config.file=/etc/prometheus/prometheus.yml' 21 | - '--storage.tsdb.path=/prometheus' 22 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 23 | - '--web.console.templates=/usr/share/prometheus/consoles' 24 | - '--storage.tsdb.retention=200h' 25 | - '--web.enable-lifecycle' 26 | 27 | grafana: 28 | image: grafana/grafana:6.3.3 29 | depends_on: 30 | - prometheus 31 | environment: 32 | GF_USERS_ALLOW_SIGN_UP: "false" 33 | GF_AUTH_ANONYMOUS_ENABLED: "true" 34 | GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin" 35 | volumes: 36 | - ./etc/local/grafana/provisioning:/etc/grafana/provisioning 37 | 38 | jaeger: 39 | image: jaegertracing/all-in-one:1.13.1 40 | 41 | oc-collector: 42 | image: omnition/opencensus-collector:0.1.10 43 | command: ["--config=/etc/opencensus/collector.yaml"] 44 | volumes: 45 | - ./etc/local/opencensus/:/etc/opencensus/:ro 46 | depends_on: 47 | - jaeger 48 | 49 | oc-agent: 50 | image: omnition/opencensus-agent:0.1.10 51 | command: ["--config=/etc/opencensus/agent.yaml"] 52 | volumes: 53 | - ./etc/local/opencensus/:/etc/opencensus/:ro 54 | depends_on: 55 | - oc-collector 56 | -------------------------------------------------------------------------------- /etc/ansible/README.md: -------------------------------------------------------------------------------- 1 | # Ansible setup 2 | 3 | Modern Go Application comes with a set of Ansible roles as an example for application deployment. 4 | 5 | **Note:** Since deployment is usually very environment specific, creating new setup and deployment roles is recommended 6 | (instead of customizing the existing ones). 7 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | braces: 5 | max-spaces-inside: 1 6 | level: error 7 | brackets: 8 | max-spaces-inside: 1 9 | level: error 10 | line-length: disable 11 | # NOTE(retr0h): Templates no longer fail this lint rule. 12 | # Uncomment if running old Molecule templates. 13 | # truthy: disable 14 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/README.md: -------------------------------------------------------------------------------- 1 | Application Setup 2 | ================= 3 | 4 | Prepares a host for running the application. 5 | Normally this role is part of a bigger playbook executed outside of the application. 6 | Alternatively this role can become a whole playbook as running an application might require configuration 7 | across various hosts (eg. setting up database users, firewall rules, etc) . 8 | 9 | Requirements 10 | ------------ 11 | 12 | Requires nginx to be installed. 13 | 14 | Example Playbook 15 | ---------------- 16 | 17 | - hosts: servers 18 | roles: 19 | - { role: application-setup } 20 | 21 | License 22 | ------- 23 | 24 | MIT 25 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Default application name 3 | app_name: app 4 | 5 | # Default deployment and application user 6 | app_user_name: app 7 | 8 | # Default SSH authorized keys for the application user 9 | app_user_authorized_keys: [] 10 | 11 | # Default server name for nginx proxy 12 | app_server_name: application.local 13 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: reload nginx 4 | service: 5 | name: nginx 6 | state: reloaded 7 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: sagikazarmark 4 | description: Modern Go Application 5 | license: MIT 6 | min_ansible_version: 2.4 7 | 8 | dependencies: [] 9 | # List your role dependencies here, one per line. Be sure to remove the '[]' above, 10 | # if you add dependencies to this list. 11 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/default/Dockerfile.j2: -------------------------------------------------------------------------------- 1 | # Molecule managed 2 | 3 | {% if item.registry is defined %} 4 | FROM {{ item.registry.url }}/{{ item.image }} 5 | {% else %} 6 | FROM {{ item.image }} 7 | {% endif %} 8 | 9 | RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \ 10 | elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash && dnf clean all; \ 11 | elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \ 12 | elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \ 13 | elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \ 14 | elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi 15 | 16 | {% if 'ubuntu' in item.image or 'debian' in item.image %} 17 | RUN apt-get update 18 | RUN apt-get install -y dbus dbus-user-session 19 | RUN apt-get install -y nginx 20 | {% endif %} 21 | 22 | {% if 'ubuntu1804' in item.image %} 23 | RUN apt-get install -y dirmngr 24 | {% endif %} 25 | 26 | {% if 'debian9' in item.image %} 27 | RUN apt-get install -y gnupg2 28 | {% endif %} 29 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/default/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * General molecule dependencies (see https://molecule.readthedocs.io/en/latest/installation.html) 9 | * Docker Engine 10 | * docker-py 11 | * docker 12 | 13 | Install 14 | ======= 15 | 16 | $ sudo pip install docker-py 17 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | lint: 7 | name: yamllint 8 | platforms: 9 | - name: instance 10 | image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu1804}-ansible:latest" 11 | command: ${MOLECULE_DOCKER_COMMAND:-""} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 14 | privileged: true 15 | provisioner: 16 | name: ansible 17 | lint: 18 | name: ansible-lint 19 | playbooks: 20 | prepare: prepare.yml 21 | scenario: 22 | name: default 23 | verifier: 24 | name: testinfra 25 | lint: 26 | name: flake8 27 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/default/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: application-setup 6 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | tasks: 5 | - name: Create dummy docker group 6 | group: 7 | name: docker 8 | state: present 9 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import testinfra.utils.ansible_runner 4 | 5 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 6 | os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') 7 | 8 | 9 | def test_app_user_is_created(host): 10 | user = host.user("app") 11 | 12 | assert user.shell == "/bin/bash" 13 | assert "docker" in user.groups 14 | 15 | 16 | def test_nginx_is_configured(host): 17 | f = host.file("/etc/nginx/conf.d/application.local.conf") 18 | 19 | assert f.exists 20 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/tasks/app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: Create application configuration 4 | template: 5 | src: config.toml 6 | dest: "{{ user.home }}/etc/{{ app_name }}.toml" 7 | owner: "{{ user.name }}" 8 | group: "{{ user.name }}" 9 | mode: 0600 10 | 11 | - 12 | name: Create application environment configuration 13 | template: 14 | src: config.env 15 | dest: "{{ user.home }}/etc/{{ app_name }}.env" 16 | owner: "{{ user.name }}" 17 | group: "{{ user.name }}" 18 | mode: 0600 19 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/tasks/ingress.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: Configure nginx virtualhost 4 | template: 5 | src: nginx.conf 6 | dest: "/etc/nginx/conf.d/{{ app_server_name }}.conf" 7 | owner: root 8 | group: root 9 | mode: 0644 10 | notify: reload nginx 11 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_tasks: user.yml 3 | - import_tasks: app.yml 4 | - import_tasks: ingress.yml 5 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/tasks/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: Ensure application user is present 4 | user: 5 | name: "{{ app_user_name }}" 6 | shell: /bin/bash 7 | groups: docker 8 | append: true 9 | state: present 10 | register: user 11 | 12 | - 13 | name: Add application user authorized keys 14 | authorized_key: 15 | user: "{{ user.name }}" 16 | key: "{{ item }}" 17 | state: present 18 | with_items: "{{ app_user_authorized_keys }}" 19 | no_log: true 20 | 21 | - 22 | name: Ensure required directories exist 23 | file: 24 | path: "{{ user.home }}/{{ item }}" 25 | state: directory 26 | owner: "{{ user.name }}" 27 | group: "{{ user.name }}" 28 | mode: 0700 29 | with_items: 30 | - etc 31 | 32 | - .config/systemd/user 33 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/templates/config.env: -------------------------------------------------------------------------------- 1 | # Add some configuration here 2 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/templates/config.toml: -------------------------------------------------------------------------------- 1 | # Add some configuration here 2 | -------------------------------------------------------------------------------- /etc/ansible/roles/application-setup/templates/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | server_name "{{ app_server_name }}"; 6 | 7 | location / { 8 | proxy_set_header Host $host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_pass http://127.0.0.1:8000; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | braces: 5 | max-spaces-inside: 1 6 | level: error 7 | brackets: 8 | max-spaces-inside: 1 9 | level: error 10 | line-length: disable 11 | # NOTE(retr0h): Templates no longer fail this lint rule. 12 | # Uncomment if running old Molecule templates. 13 | # truthy: disable 14 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/README.md: -------------------------------------------------------------------------------- 1 | Deploy Modern Go Application 2 | ============================ 3 | 4 | This role is used to deploy Modern Go Application from a CI/CD pipeline. 5 | 6 | Requirements 7 | ------------ 8 | 9 | `modern-go-application` role should be applied to the host (either in a separate or in the same playbook). 10 | 11 | Role Variables 12 | -------------- 13 | 14 | | Variable | Default | Description | 15 | | -------- | ------- | ----------- | 16 | | `binary_source` | *none* | Local source of the binaries to copy | 17 | | `binary_name` | `modern-go-application` | Binary to copy | 18 | | `mga_service_name` | `mga` | Service to be restarted | 19 | 20 | Dependencies 21 | ------------ 22 | 23 | - `modern-go-application` role 24 | 25 | Example Playbook 26 | ---------------- 27 | 28 | - hosts: servers 29 | roles: 30 | - { role: deploy-modern-go-application, binary_source: build/ } 31 | 32 | License 33 | ------- 34 | 35 | MIT 36 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Default application name 3 | app_name: app 4 | 5 | # Default deployment and application user 6 | app_user_name: app 7 | 8 | # Default application user home directory 9 | app_config_path: "/home/{{ app_user_name }}" 10 | 11 | # Default application image 12 | app_image: sagikazarmark/modern-go-application 13 | 14 | # Default application version 15 | app_version: latest 16 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: sagikazarmark 4 | description: your description 5 | license: MIT 6 | min_ansible_version: 2.4 7 | 8 | dependencies: [] 9 | # List your role dependencies here, one per line. Be sure to remove the '[]' above, 10 | # if you add dependencies to this list. 11 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/Dockerfile.j2: -------------------------------------------------------------------------------- 1 | # Molecule managed 2 | 3 | {% if item.registry is defined %} 4 | FROM {{ item.registry.url }}/{{ item.image }} 5 | {% else %} 6 | FROM {{ item.image }} 7 | {% endif %} 8 | 9 | RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \ 10 | elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash && dnf clean all; \ 11 | elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \ 12 | elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \ 13 | elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \ 14 | elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi 15 | 16 | {% if 'ubuntu' in item.image or 'debian' in item.image %} 17 | RUN apt-get update 18 | RUN apt-get install -y dbus dbus-user-session 19 | RUN apt-get install -y nginx 20 | {% endif %} 21 | 22 | {% if 'ubuntu1804' in item.image %} 23 | RUN apt-get install -y dirmngr 24 | {% endif %} 25 | 26 | {% if 'debian9' in item.image %} 27 | RUN apt-get install -y gnupg2 28 | {% endif %} 29 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * General molecule dependencies (see https://molecule.readthedocs.io/en/latest/installation.html) 9 | * Docker Engine 10 | * docker-py 11 | * docker 12 | 13 | Install 14 | ======= 15 | 16 | $ sudo pip install docker-py 17 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | requirements-file: requirements.yml 5 | driver: 6 | name: docker 7 | lint: 8 | name: yamllint 9 | platforms: 10 | - name: instance 11 | image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu1804}-ansible:latest" 12 | command: ${MOLECULE_DOCKER_COMMAND:-""} 13 | volumes: 14 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 15 | privileged: true 16 | provisioner: 17 | name: ansible 18 | lint: 19 | name: ansible-lint 20 | playbooks: 21 | prepare: prepare.yml 22 | scenario: 23 | name: default 24 | verifier: 25 | name: testinfra 26 | lint: 27 | name: flake8 28 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | vars: 5 | binary_source: ../../../../../../build 6 | binary_name: modern-go-application-docker 7 | roles: 8 | # - role: application-setup 9 | - role: deployment 10 | pre_tasks: 11 | - name: Create valid application configuration 12 | template: 13 | src: ../../../../../../config.toml.dist 14 | dest: "/home/app/etc/app.toml" 15 | owner: "app" 16 | group: "app" 17 | mode: 0600 18 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | vars: 5 | pip_install_packages: 6 | - name: docker 7 | roles: 8 | - geerlingguy.pip 9 | - geerlingguy.docker 10 | - application-setup 11 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - geerlingguy.pip 3 | - geerlingguy.docker 4 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import testinfra.utils.ansible_runner 4 | 5 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 6 | os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') 7 | 8 | 9 | # def test_application_is_running(host): 10 | # host.process.get(user="mga", comm="modern-go-application") 11 | -------------------------------------------------------------------------------- /etc/ansible/roles/deployment/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: Start application 4 | become: true 5 | become_user: "{{ app_user_name }}" 6 | docker_container: 7 | name: "{{ app_name }}" 8 | image: "{{ app_image }}:{{ app_version }}" 9 | state: started 10 | pull: true 11 | env_file: "{{ app_config_path }}/etc/{{ app_name }}.env" 12 | ports: 13 | - "127.0.0.1:8000:8000" 14 | - "127.0.0.1:10000:10000" 15 | volumes: 16 | - "{{ app_config_path }}/etc/{{ app_name }}.toml:/config.toml:ro" 17 | -------------------------------------------------------------------------------- /etc/loadgen/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | COPY requirements.txt . 4 | RUN pip install -r requirements.txt 5 | 6 | COPY . . 7 | ENTRYPOINT ./loadgen.sh 8 | -------------------------------------------------------------------------------- /etc/loadgen/README.md: -------------------------------------------------------------------------------- 1 | # Loadgen 2 | 3 | **A simple load generator tool for Modern Go Application** 4 | 5 | Useful to demonstrate behavior and to fill charts with data. 6 | 7 | ```bash 8 | docker build -t loadgen . 9 | docker run --rm -it -e FRONTEND_ADDR=http://host.docker.internal:8000 loadgen 10 | ``` 11 | 12 | Alternatively, you can use the prebuilt image: 13 | 14 | ```bash 15 | docker run --rm -it -e FRONTEND_ADDR=http://host.docker.internal:8000 sagikazarmark/modern-go-application:latest-loadgen 16 | ``` 17 | -------------------------------------------------------------------------------- /etc/loadgen/loadgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | trap "exit" TERM 6 | 7 | if [[ -z "${FRONTEND_ADDR}" ]]; then 8 | echo >&2 "FRONTEND_ADDR not specified" 9 | exit 1 10 | fi 11 | 12 | set -x 13 | locust --host="${FRONTEND_ADDR}" --no-web -c "${USERS:-10}" 14 | -------------------------------------------------------------------------------- /etc/loadgen/locustfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import json 4 | import random 5 | import urllib3 6 | from locust import HttpLocust, TaskSet 7 | 8 | clientErrorCodes = [400, 403, 404] 9 | serverErrorCodes = [500, 503] 10 | 11 | 12 | def index(l): 13 | l.client.get("/") 14 | 15 | def clientErrors(l): 16 | l.client.get("/httpbin/status/" + str(random.choice(clientErrorCodes))) 17 | 18 | def serverErrors(l): 19 | l.client.get("/httpbin/status/" + str(random.choice(serverErrorCodes))) 20 | 21 | def responseSize(l): 22 | l.client.get("/httpbin/bytes/" + str(random.randint(100, 500000))) 23 | 24 | class DemoBehavior(TaskSet): 25 | def on_start(self): 26 | self.client.verify = False 27 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 28 | index(self) 29 | 30 | tasks = { 31 | index: 3, 32 | clientErrors: 6, 33 | serverErrors: 4, 34 | responseSize: 2, 35 | } 36 | 37 | 38 | class WebsiteUser(HttpLocust): 39 | task_set = DemoBehavior 40 | min_wait = 1000 41 | max_wait = 10000 42 | -------------------------------------------------------------------------------- /etc/loadgen/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio==0.8.1 2 | pyzmq==17.0.0 3 | -------------------------------------------------------------------------------- /etc/local/grafana/provisioning/datasources/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - 5 | name: Prometheus 6 | type: prometheus 7 | access: proxy 8 | orgId: 1 9 | url: http://prometheus:9090 10 | basicAuth: false 11 | isDefault: true 12 | version: 1 13 | editable: false 14 | -------------------------------------------------------------------------------- /etc/local/opencensus/agent.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | opencensus: 3 | address: ":55678" 4 | 5 | exporters: 6 | opencensus: 7 | endpoint: "oc-collector:55678" 8 | -------------------------------------------------------------------------------- /etc/local/opencensus/collector.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | opencensus: 3 | port: 55678 4 | 5 | exporters: 6 | queued-exporters: 7 | jaeger-all-in-one: 8 | num-workers: 4 9 | queue-size: 100 10 | retry-on-failure: true 11 | sender-type: jaeger-thrift-http 12 | jaeger-thrift-http: 13 | collector-endpoint: http://jaeger:14268/api/traces 14 | timeout: 5s 15 | -------------------------------------------------------------------------------- /etc/local/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - 7 | job_name: 'prometheus' 8 | scrape_interval: 10s 9 | static_configs: 10 | - targets: ['localhost:9090'] 11 | 12 | - 13 | job_name: 'application' 14 | scrape_interval: 5s 15 | static_configs: 16 | - targets: ['dockerhost:10000'] 17 | 18 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1638122382, 6 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "goflake": { 19 | "inputs": { 20 | "nixpkgs": [ 21 | "nixpkgs" 22 | ] 23 | }, 24 | "locked": { 25 | "lastModified": 1637751478, 26 | "narHash": "sha256-9S2UnNsZJGQE57M6kY5UupR1NPzWxS9KLpwWzJJVDQ0=", 27 | "owner": "sagikazarmark", 28 | "repo": "go-flake", 29 | "rev": "58f9abd107f598e6b0c50f99574351c41f5251ca", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "sagikazarmark", 34 | "repo": "go-flake", 35 | "type": "github" 36 | } 37 | }, 38 | "nixpkgs": { 39 | "locked": { 40 | "lastModified": 1640233012, 41 | "narHash": "sha256-DNKMmWZ/RLoh5IVJLAa5HYOy4IW28mBBYDMgMxzzom8=", 42 | "owner": "NixOS", 43 | "repo": "nixpkgs", 44 | "rev": "611f29bedadfb2aa4c9c26c4af65f05dd35f2f3f", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "id": "nixpkgs", 49 | "ref": "nixos-unstable", 50 | "type": "indirect" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "goflake": "goflake", 57 | "nixpkgs": "nixpkgs" 58 | } 59 | } 60 | }, 61 | "root": "root", 62 | "version": 7 63 | } 64 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Modern Go Application example"; 3 | 4 | inputs = { 5 | nixpkgs.url = "nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | goflake.url = "github:sagikazarmark/go-flake"; 8 | goflake.inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | 11 | outputs = { self, nixpkgs, flake-utils, goflake, ... }: 12 | flake-utils.lib.eachDefaultSystem (system: 13 | let 14 | pkgs = import nixpkgs { 15 | inherit system; 16 | 17 | overlays = [ goflake.overlay ]; 18 | }; 19 | buildDeps = with pkgs; [ git go_1_17 gnumake ]; 20 | devDeps = with pkgs; buildDeps ++ [ 21 | golangci-lint 22 | gotestsum 23 | goreleaser 24 | protobuf 25 | protoc-gen-go 26 | protoc-gen-go-grpc 27 | protoc-gen-kit 28 | # gqlgen 29 | openapi-generator-cli 30 | ent 31 | ]; 32 | in 33 | { devShell = pkgs.mkShell { buildInputs = devDeps; }; }); 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sagikazarmark/modern-go-application 2 | 3 | go 1.17 4 | 5 | require ( 6 | contrib.go.opencensus.io/exporter/ocagent v0.7.0 7 | contrib.go.opencensus.io/exporter/prometheus v0.4.0 8 | contrib.go.opencensus.io/integrations/ocsql v0.1.6 9 | emperror.dev/emperror v0.33.0 10 | emperror.dev/errors v0.8.0 11 | emperror.dev/handler/logur v0.4.0 12 | entgo.io/ent v0.9.1 13 | github.com/99designs/gqlgen v0.14.0 14 | github.com/AppsFlyer/go-sundheit v0.2.0 15 | github.com/ThreeDotsLabs/watermill v1.1.1 16 | github.com/cloudflare/tableflip v1.2.1 17 | github.com/go-kit/kit v0.12.0 18 | github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae 19 | github.com/golang/protobuf v1.5.2 20 | github.com/goph/idgen v0.4.0 21 | github.com/gorilla/handlers v1.5.1 22 | github.com/gorilla/mux v1.8.0 23 | github.com/mccutchen/go-httpbin v0.0.0-20190116014521-c5cb2f4802fa 24 | github.com/oklog/run v1.1.0 25 | github.com/olekukonko/tablewriter v0.0.5 26 | github.com/sagikazarmark/appkit v0.13.0 27 | github.com/sagikazarmark/kitx v0.17.0 28 | github.com/sagikazarmark/ocmux v0.2.0 29 | github.com/sagikazarmark/todobackend-go-kit v0.6.0 30 | github.com/sagikazarmark/todobackend-go-kit/api v0.5.0 31 | github.com/sirupsen/logrus v1.8.1 32 | github.com/spf13/cobra v1.1.3 33 | github.com/spf13/pflag v1.0.5 34 | github.com/spf13/viper v1.7.1 35 | github.com/stretchr/testify v1.7.0 36 | go.opencensus.io v0.23.0 37 | google.golang.org/genproto v0.0.0-20211117155847-120650a500bb 38 | google.golang.org/grpc v1.42.0 39 | logur.dev/adapter/logrus v0.5.0 40 | logur.dev/integration/watermill v0.5.0 41 | logur.dev/logur v0.17.0 42 | ) 43 | 44 | require ( 45 | github.com/agnivade/levenshtein v1.1.1 // indirect 46 | github.com/beorn7/perks v1.0.1 // indirect 47 | github.com/cenkalti/backoff/v3 v3.0.0 // indirect 48 | github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect 49 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 50 | github.com/davecgh/go-spew v1.1.1 // indirect 51 | github.com/felixge/httpsnoop v1.0.2 // indirect 52 | github.com/fsnotify/fsnotify v1.4.9 // indirect 53 | github.com/go-kit/log v0.2.0 // indirect 54 | github.com/go-logfmt/logfmt v0.5.1 // indirect 55 | github.com/gogo/protobuf v1.3.2 // indirect 56 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 57 | github.com/google/uuid v1.3.0 // indirect 58 | github.com/gorilla/websocket v1.4.2 // indirect 59 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 60 | github.com/hashicorp/errwrap v1.0.0 // indirect 61 | github.com/hashicorp/go-multierror v1.1.0 // indirect 62 | github.com/hashicorp/golang-lru v0.5.4 // indirect 63 | github.com/hashicorp/hcl v1.0.0 // indirect 64 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 65 | github.com/kr/text v0.2.0 // indirect 66 | github.com/lithammer/shortuuid/v3 v3.0.4 // indirect 67 | github.com/magiconair/properties v1.8.1 // indirect 68 | github.com/mattn/go-runewidth v0.0.9 // indirect 69 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 70 | github.com/mitchellh/mapstructure v1.4.2 // indirect 71 | github.com/moogar0880/problems v0.1.1 // indirect 72 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 73 | github.com/oklog/ulid v1.3.1 // indirect 74 | github.com/pelletier/go-toml v1.2.0 // indirect 75 | github.com/pkg/errors v0.9.1 // indirect 76 | github.com/pmezard/go-difflib v1.0.0 // indirect 77 | github.com/prometheus/client_golang v1.11.1 // indirect 78 | github.com/prometheus/client_model v0.2.0 // indirect 79 | github.com/prometheus/common v0.30.0 // indirect 80 | github.com/prometheus/procfs v0.7.3 // indirect 81 | github.com/prometheus/statsd_exporter v0.21.0 // indirect 82 | github.com/spf13/afero v1.1.2 // indirect 83 | github.com/spf13/cast v1.3.0 // indirect 84 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 85 | github.com/subosito/gotenv v1.2.0 // indirect 86 | github.com/vektah/gqlparser/v2 v2.2.0 // indirect 87 | go.uber.org/atomic v1.9.0 // indirect 88 | go.uber.org/multierr v1.7.0 // indirect 89 | golang.org/x/net v0.7.0 // indirect 90 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 91 | golang.org/x/sys v0.5.0 // indirect 92 | golang.org/x/text v0.7.0 // indirect 93 | google.golang.org/api v0.30.0 // indirect 94 | google.golang.org/protobuf v1.27.1 // indirect 95 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 96 | gopkg.in/ini.v1 v1.51.0 // indirect 97 | gopkg.in/yaml.v2 v2.4.0 // indirect 98 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 99 | ) 100 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Destination directory of modifications 4 | DEST="." 5 | 6 | # Original project variables 7 | originalProjectName="project" 8 | originalPackageName="github.com/sagikazarmark/modern-go-application" 9 | originalBinaryName="modern-go-application" 10 | originalAppName="mga" 11 | originalFriendlyAppName="Modern Go Application" 12 | 13 | # Prepare testing 14 | if [[ ! -z "${TEST}" ]]; then 15 | #set -xe 16 | DEST="tmp/inittest" 17 | mkdir -p ${DEST} 18 | echo "." > tmp/.gitignore 19 | fi 20 | 21 | function prompt() { 22 | echo -n -e "\033[1;32m?\033[0m \033[1m$1\033[0m ($2) " 23 | } 24 | 25 | function replace() { 26 | if [[ ! -z "${TEST}" ]]; then 27 | dest=$(echo $2 | sed "s|^${DEST}/||") 28 | mkdir -p $(dirname "${DEST}/${dest}") 29 | if [[ "$2" == "${DEST}/${dest}" ]]; then 30 | sed -E -e "$1" $2 > ${DEST}/${dest}.new 31 | mv -f ${DEST}/${dest}.new ${DEST}/${dest} 32 | else 33 | sed -E -e "$1" $2 > ${DEST}/${dest} 34 | fi 35 | else 36 | sed -E -e "$1" $2 > $2.new 37 | mv -f $2.new $2 38 | fi 39 | } 40 | 41 | function move() { 42 | if [[ ! -z "${TEST}" ]]; then 43 | dest=$(echo $2 | sed "s|^${DEST}/||") 44 | mkdir -p $(dirname "${DEST}/${dest}") 45 | cp -r "$1" ${DEST}/${dest} 46 | else 47 | mv $@ 48 | fi 49 | } 50 | 51 | function remove() { 52 | if [[ -z "${TEST}" ]]; then 53 | rm $@ 54 | fi 55 | } 56 | 57 | defaultPackageName=${PWD##*src/} 58 | prompt "Package name" ${defaultPackageName} 59 | read packageName 60 | packageName=$(echo "${packageName:-${defaultPackageName}}" | sed 's/[[:space:]]//g') 61 | 62 | defaultProjectName=$(basename ${packageName}) 63 | prompt "Project name" ${defaultProjectName} 64 | read projectName 65 | projectName=$(echo "${projectName:-${defaultProjectName}}" | sed 's/[[:space:]]//g') 66 | 67 | prompt "Binary name" ${projectName} 68 | read binaryName 69 | binaryName=$(echo "${binaryName:-${projectName}}" | sed 's/[[:space:]]//g') 70 | 71 | prompt "Application name" ${projectName} 72 | read appName 73 | appName=$(echo "${appName:-${projectName}}" | sed 's/[[:space:]]//g') 74 | 75 | defaultFriendlyAppName=$(echo "${appName}" | sed -e 's/-/ /g;' | awk '{for(i=1;i<=NF;i++){ $i=toupper(substr($i,1,1)) substr($i,2) }}1') 76 | prompt "Friendly application name" "${defaultFriendlyAppName}" 77 | read friendlyAppName 78 | friendlyAppName=${friendlyAppName:-${defaultFriendlyAppName}} 79 | 80 | prompt "Update README" "Y/n" 81 | read updateReadme 82 | updateReadme=${updateReadme:-y} 83 | 84 | prompt "Remove init script" "y/N" 85 | read removeInit 86 | removeInit=${removeInit:-n} 87 | 88 | # IDE configuration 89 | move .idea/${originalProjectName}.iml .idea/${projectName}.iml 90 | replace "s|.idea/${originalProjectName}.iml|.idea/${projectName}.iml|g" .idea/modules.xml 91 | 92 | # Run configurations 93 | replace 's|name="project"|name="'${projectName}'"|' .idea/runConfigurations/All_tests.xml 94 | replace 's|name="project"|name="'${projectName}'"|; s|value="\$PROJECT_DIR\$\/cmd\/'${originalBinaryName}'\/"|value="$PROJECT_DIR$/cmd/'${binaryName}'/"|' .idea/runConfigurations/Debug.xml 95 | replace 's|name="project"|name="'${projectName}'"|' .idea/runConfigurations/Integration_tests.xml 96 | replace 's|name="project"|name="'${projectName}'"|' .idea/runConfigurations/Tests.xml 97 | replace "s|${originalBinaryName}|${binaryName}|" .vscode/launch.json 98 | 99 | # Binary changes: 100 | # - binary name 101 | # - source code 102 | # - variables 103 | move cmd/${originalBinaryName} cmd/${binaryName} 104 | replace "s|${originalAppName}|${appName}|; s|${originalFriendlyAppName}|${friendlyAppName}|" ${DEST}/cmd/${binaryName}/main.go 105 | find ${DEST}/cmd -type f | while read file; do replace "s|${originalPackageName}|${packageName}|" "$file"; done 106 | 107 | # Other project files 108 | declare -a files=("CHANGELOG.md" "prototool.yaml" "go.mod" ".golangci.yml" "gqlgen.yml") 109 | for file in "${files[@]}"; do 110 | if [[ -f "${file}" ]]; then 111 | replace "s|${originalPackageName}|${packageName}|" ${file} 112 | fi 113 | done 114 | declare -a files=("prototool.yaml") 115 | for file in "${files[@]}"; do 116 | if [[ -f "${file}" ]]; then 117 | replace "s|${originalProjectName}|${projectName}|" ${file} 118 | fi 119 | done 120 | declare -a files=("Dockerfile") 121 | for file in "${files[@]}"; do 122 | if [[ -f "${file}" ]]; then 123 | replace "s|${originalBinaryName}|${binaryName}|" ${file} 124 | fi 125 | done 126 | 127 | # Update source code 128 | find .gen -type f | while read file; do replace "s|${originalPackageName}|${packageName}|" "$file"; done 129 | find internal -type f | while read file; do replace "s|${originalPackageName}|${packageName}|" "$file"; done 130 | 131 | if [[ "${removeInit}" != "n" && "${removeInit}" != "N" ]]; then 132 | remove "$0" 133 | fi 134 | 135 | # Update readme 136 | if [[ "${updateReadme}" == "y" || "${updateReadme}" == "Y" ]]; then 137 | echo -e "# FRIENDLY_PROJECT_NAME\n\n**Project description.**" | sed "s/FRIENDLY_PROJECT_NAME/${friendlyAppName}/" > ${DEST}/README.md 138 | fi 139 | -------------------------------------------------------------------------------- /internal/app/mga/app.go: -------------------------------------------------------------------------------- 1 | package mga 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "net/http" 7 | 8 | entsql "entgo.io/ent/dialect/sql" 9 | "github.com/99designs/gqlgen/graphql/handler" 10 | "github.com/ThreeDotsLabs/watermill/components/cqrs" 11 | "github.com/ThreeDotsLabs/watermill/message" 12 | "github.com/go-kit/kit/endpoint" 13 | "github.com/go-kit/kit/tracing/opencensus" 14 | kitgrpc "github.com/go-kit/kit/transport/grpc" 15 | kithttp "github.com/go-kit/kit/transport/http" 16 | "github.com/goph/idgen/ulidgen" 17 | "github.com/gorilla/mux" 18 | appkitendpoint "github.com/sagikazarmark/appkit/endpoint" 19 | appkithttp "github.com/sagikazarmark/appkit/transport/http" 20 | "github.com/sagikazarmark/kitx/correlation" 21 | kitxendpoint "github.com/sagikazarmark/kitx/endpoint" 22 | kitxtransport "github.com/sagikazarmark/kitx/transport" 23 | kitxgrpc "github.com/sagikazarmark/kitx/transport/grpc" 24 | kitxhttp "github.com/sagikazarmark/kitx/transport/http" 25 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 26 | "github.com/sagikazarmark/todobackend-go-kit/todo" 27 | "github.com/sagikazarmark/todobackend-go-kit/todo/tododriver" 28 | "google.golang.org/grpc" 29 | watermilllog "logur.dev/integration/watermill" 30 | 31 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/httpbin" 32 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/landing/landingdriver" 33 | todo2 "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo" 34 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter" 35 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent" 36 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/migrate" 37 | tododriver2 "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/tododriver" 38 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todogen" 39 | "github.com/sagikazarmark/modern-go-application/static/templates" 40 | ) 41 | 42 | const todoTopic = "todo" 43 | 44 | // InitializeApp initializes a new HTTP and a new gRPC application. 45 | func InitializeApp( 46 | httpRouter *mux.Router, 47 | grpcServer *grpc.Server, 48 | publisher message.Publisher, 49 | storage string, 50 | db *sql.DB, 51 | logger Logger, 52 | errorHandler ErrorHandler, // nolint: interfacer 53 | ) { 54 | endpointMiddleware := []endpoint.Middleware{ 55 | correlation.Middleware(), 56 | opencensus.TraceEndpoint("", opencensus.WithSpanName(func(ctx context.Context, _ string) string { 57 | name, _ := kitxendpoint.OperationName(ctx) 58 | 59 | return name 60 | })), 61 | appkitendpoint.LoggingMiddleware(logger), 62 | } 63 | 64 | transportErrorHandler := kitxtransport.NewErrorHandler(errorHandler) 65 | 66 | httpServerOptions := []kithttp.ServerOption{ 67 | kithttp.ServerErrorHandler(transportErrorHandler), 68 | kithttp.ServerErrorEncoder(kitxhttp.NewJSONProblemErrorEncoder(appkithttp.NewDefaultProblemConverter())), 69 | kithttp.ServerBefore(correlation.HTTPToContext(), kithttp.PopulateRequestContext), 70 | } 71 | 72 | grpcServerOptions := []kitgrpc.ServerOption{ 73 | kitgrpc.ServerErrorHandler(transportErrorHandler), 74 | kitgrpc.ServerBefore(correlation.GRPCToContext()), 75 | } 76 | 77 | { 78 | eventBus, _ := cqrs.NewEventBus( 79 | publisher, 80 | func(eventName string) string { return todoTopic }, 81 | cqrs.JSONMarshaler{GenerateName: cqrs.StructName}, 82 | ) 83 | 84 | var store todo.Store = todo.NewInMemoryStore() 85 | if storage == "database" { 86 | client := ent.NewClient(ent.Driver(entsql.OpenDB("mysql", db))) 87 | err := client.Schema.Create( 88 | context.Background(), 89 | migrate.WithDropIndex(true), 90 | migrate.WithDropColumn(true), 91 | ) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | store = todoadapter.NewEntStore(client) 97 | } 98 | 99 | service := todo.NewService(ulidgen.NewGenerator(), store) 100 | service = todo2.EventMiddleware(todogen.NewEventDispatcher(eventBus))(service) 101 | service = tododriver2.LoggingMiddleware(logger)(service) 102 | service = tododriver2.InstrumentationMiddleware()(service) 103 | 104 | endpoints := tododriver.MakeEndpoints( 105 | service, 106 | kitxendpoint.Combine(endpointMiddleware...), 107 | ) 108 | 109 | tododriver.RegisterHTTPHandlers( 110 | endpoints, 111 | httpRouter.PathPrefix("/todos").Subrouter(), 112 | kitxhttp.ServerOptions(httpServerOptions), 113 | ) 114 | todov1.RegisterTodoListServiceServer( 115 | grpcServer, 116 | tododriver.MakeGRPCServer(endpoints, kitxgrpc.ServerOptions(grpcServerOptions)), 117 | ) 118 | httpRouter.PathPrefix("/graphql").Handler(handler.NewDefaultServer( 119 | tododriver.MakeGraphQLSchema(endpoints), 120 | )) 121 | } 122 | 123 | landingdriver.RegisterHTTPHandlers(httpRouter, templates.Files()) 124 | httpRouter.PathPrefix("/httpbin").Handler(http.StripPrefix( 125 | "/httpbin", 126 | httpbin.MakeHTTPHandler(logger.WithFields(map[string]interface{}{"module": "httpbin"})), 127 | )) 128 | } 129 | 130 | // RegisterEventHandlers registers event handlers in a message router. 131 | func RegisterEventHandlers(router *message.Router, subscriber message.Subscriber, logger Logger) error { 132 | todoEventProcessor, _ := cqrs.NewEventProcessor( 133 | []cqrs.EventHandler{ 134 | todogen.NewMarkedAsCompleteEventHandler(todo2.NewLogEventHandler(logger), "marked_as_complete"), 135 | }, 136 | func(eventName string) string { return todoTopic }, 137 | func(handlerName string) (message.Subscriber, error) { return subscriber, nil }, 138 | cqrs.JSONMarshaler{GenerateName: cqrs.StructName}, 139 | watermilllog.New(logger.WithFields(map[string]interface{}{"component": "watermill"})), 140 | ) 141 | 142 | err := todoEventProcessor.AddHandlersToRouter(router) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /internal/app/mga/common.go: -------------------------------------------------------------------------------- 1 | package mga 2 | 3 | import ( 4 | "github.com/sagikazarmark/modern-go-application/internal/common" 5 | ) 6 | 7 | // These interfaces are aliased so that the module code is separated from the rest of the application. 8 | // If the module is moved out of the app, copy the aliased interfaces here. 9 | 10 | // Logger is the fundamental interface for all log operations. 11 | type Logger = common.Logger 12 | 13 | // ErrorHandler handles an error. 14 | type ErrorHandler = common.ErrorHandler 15 | -------------------------------------------------------------------------------- /internal/app/mga/httpbin/httpbin.go: -------------------------------------------------------------------------------- 1 | package httpbin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mccutchen/go-httpbin/httpbin" 7 | ) 8 | 9 | // MakeHTTPHandler returns a new HTTP handler serving HTTPBin. 10 | func MakeHTTPHandler(logger Logger) http.Handler { 11 | return httpbin.New( 12 | httpbin.WithObserver(func(result httpbin.Result) { 13 | logger.Info( 14 | "httpbin call", 15 | map[string]interface{}{ 16 | "status": result.Status, 17 | "method": result.Method, 18 | "uri": result.URI, 19 | "size_bytes": result.Size, 20 | "duration_ms": result.Duration.Seconds() * 1e3, // https://github.com/golang/go/issues/5491#issuecomment-66079585 21 | }, 22 | ) 23 | }), 24 | ).Handler() 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/mga/httpbin/interfaces.go: -------------------------------------------------------------------------------- 1 | package httpbin 2 | 3 | import ( 4 | "github.com/sagikazarmark/modern-go-application/internal/common" 5 | ) 6 | 7 | // These interfaces are aliased so that the module code is separated from the rest of the application. 8 | // If the module is moved out of the app, copy the aliased interfaces here. 9 | 10 | // Logger is the fundamental interface for all log operations. 11 | type Logger = common.Logger 12 | -------------------------------------------------------------------------------- /internal/app/mga/landing/landingdriver/http_transport.go: -------------------------------------------------------------------------------- 1 | package landingdriver 2 | 3 | import ( 4 | "io/fs" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // RegisterHTTPHandlers mounts the HTTP handler for the landing page in a router. 12 | func RegisterHTTPHandlers(router *mux.Router, fsys fs.FS) { 13 | router.Path("/").Methods(http.MethodGet).Handler(Landing(fsys)) 14 | } 15 | 16 | // Landing is the landing page for Modern Go Application. 17 | func Landing(fsys fs.FS) http.Handler { 18 | file, err := fsys.Open("landing.html") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | body, err := ioutil.ReadAll(file) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 29 | w.Header().Add("Content-Type", "text/html") 30 | 31 | _, _ = w.Write(body) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /internal/app/mga/todo/README.md: -------------------------------------------------------------------------------- 1 | # TodoBackend example 2 | 3 | Code can be found at https://github.com/sagikazarmark/todobackend-go-kit 4 | -------------------------------------------------------------------------------- /internal/app/mga/todo/common.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "github.com/sagikazarmark/modern-go-application/internal/common" 5 | ) 6 | 7 | // These interfaces are aliased so that the module code is separated from the rest of the application. 8 | // If the module is moved out of the app, copy the aliased interfaces here. 9 | 10 | // Logger is the fundamental interface for all log operations. 11 | type Logger = common.Logger 12 | 13 | // ErrorHandler handles an error. 14 | type ErrorHandler = common.ErrorHandler 15 | -------------------------------------------------------------------------------- /internal/app/mga/todo/event_handlers.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // LogEventHandler handles todo events and logs them. 8 | type LogEventHandler struct { 9 | logger Logger 10 | } 11 | 12 | // NewLogEventHandler returns a new LogEventHandler instance. 13 | func NewLogEventHandler(logger Logger) LogEventHandler { 14 | return LogEventHandler{ 15 | logger: logger, 16 | } 17 | } 18 | 19 | // MarkedAsComplete logs a MarkedAsComplete event. 20 | func (h LogEventHandler) MarkedAsComplete(ctx context.Context, event MarkedAsComplete) error { 21 | logger := h.logger.WithContext(ctx) 22 | 23 | logger.Info("todo marked as complete", map[string]interface{}{ 24 | "event": "MarkedAsComplete", 25 | "todo_id": event.ID, 26 | }) 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/app/mga/todo/event_handlers_test.go: -------------------------------------------------------------------------------- 1 | package todo_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "logur.dev/logur" 9 | "logur.dev/logur/logtesting" 10 | 11 | . "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo" 12 | "github.com/sagikazarmark/modern-go-application/internal/common/commonadapter" 13 | ) 14 | 15 | func TestLogEventHandler_MarkedAsComplete(t *testing.T) { 16 | logger := &logur.TestLoggerFacade{} 17 | 18 | eventHandler := NewLogEventHandler(commonadapter.NewLogger(logger)) 19 | 20 | event := MarkedAsComplete{ 21 | ID: "1234", 22 | } 23 | 24 | err := eventHandler.MarkedAsComplete(context.Background(), event) 25 | require.NoError(t, err) 26 | 27 | logEvent := logur.LogEvent{ 28 | Level: logur.Info, 29 | Line: "todo marked as complete", 30 | Fields: map[string]interface{}{ 31 | "event": "MarkedAsComplete", 32 | "todo_id": "1234", 33 | }, 34 | } 35 | 36 | logtesting.AssertLogEventsEqual(t, logEvent, *(logger.LastEvent())) 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/mga/todo/middleware.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sagikazarmark/todobackend-go-kit/todo" 7 | ) 8 | 9 | // Middleware is a service middleware. 10 | type Middleware func(todo.Service) todo.Service 11 | 12 | // DefaultMiddleware helps implementing partial middleware. 13 | type DefaultMiddleware struct { 14 | Service todo.Service 15 | } 16 | 17 | func (m DefaultMiddleware) AddItem(ctx context.Context, newItem todo.NewItem) (todo.Item, error) { 18 | return m.Service.AddItem(ctx, newItem) 19 | } 20 | 21 | func (m DefaultMiddleware) ListItems(ctx context.Context) ([]todo.Item, error) { 22 | return m.Service.ListItems(ctx) 23 | } 24 | 25 | func (m DefaultMiddleware) DeleteItems(ctx context.Context) error { 26 | return m.Service.DeleteItems(ctx) 27 | } 28 | 29 | func (m DefaultMiddleware) GetItem(ctx context.Context, id string) (todo.Item, error) { 30 | return m.Service.GetItem(ctx, id) 31 | } 32 | 33 | func (m DefaultMiddleware) UpdateItem(ctx context.Context, id string, itemUpdate todo.ItemUpdate) (todo.Item, error) { 34 | return m.Service.UpdateItem(ctx, id, itemUpdate) 35 | } 36 | 37 | func (m DefaultMiddleware) DeleteItem(ctx context.Context, id string) error { 38 | return m.Service.DeleteItem(ctx, id) 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/mga/todo/service.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "context" 5 | 6 | "emperror.dev/errors" 7 | "github.com/sagikazarmark/todobackend-go-kit/todo" 8 | ) 9 | 10 | // +mga:event:dispatcher 11 | 12 | // Events dispatches todo events. 13 | type Events interface { 14 | // MarkedAsComplete dispatches a MarkedAsComplete event. 15 | MarkedAsComplete(ctx context.Context, event MarkedAsComplete) error 16 | } 17 | 18 | // +mga:event:handler 19 | 20 | // MarkedAsComplete event is triggered when an item gets marked as complete. 21 | type MarkedAsComplete struct { 22 | ID string 23 | } 24 | 25 | // EventMiddleware fires todo events. 26 | func EventMiddleware(events Events) Middleware { 27 | return func(next todo.Service) todo.Service { 28 | return eventMiddleware{ 29 | Service: DefaultMiddleware{Service: next}, 30 | next: next, 31 | 32 | events: events, 33 | } 34 | } 35 | } 36 | 37 | type eventMiddleware struct { 38 | todo.Service 39 | next todo.Service 40 | 41 | events Events 42 | } 43 | 44 | func (mw eventMiddleware) UpdateItem(ctx context.Context, id string, itemUpdate todo.ItemUpdate) (todo.Item, error) { 45 | var fireComplete bool 46 | if itemUpdate.Completed != nil && *itemUpdate.Completed { 47 | fireComplete = true 48 | } 49 | 50 | item, err := mw.next.UpdateItem(ctx, id, itemUpdate) 51 | if err != nil { 52 | return item, err 53 | } 54 | 55 | if fireComplete { 56 | event := MarkedAsComplete{ 57 | ID: item.ID, 58 | } 59 | 60 | err = mw.events.MarkedAsComplete(ctx, event) 61 | if err != nil { 62 | // TODO: rollback item store here? retry? 63 | return item, errors.WithMessage(err, "mark item as complete") 64 | } 65 | } 66 | 67 | return item, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/migrate" 11 | 12 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 13 | 14 | "entgo.io/ent/dialect" 15 | "entgo.io/ent/dialect/sql" 16 | ) 17 | 18 | // Client is the client that holds all ent builders. 19 | type Client struct { 20 | config 21 | // Schema is the client for creating, migrating and dropping schema. 22 | Schema *migrate.Schema 23 | // TodoItem is the client for interacting with the TodoItem builders. 24 | TodoItem *TodoItemClient 25 | } 26 | 27 | // NewClient creates a new client configured with the given options. 28 | func NewClient(opts ...Option) *Client { 29 | cfg := config{log: log.Println, hooks: &hooks{}} 30 | cfg.options(opts...) 31 | client := &Client{config: cfg} 32 | client.init() 33 | return client 34 | } 35 | 36 | func (c *Client) init() { 37 | c.Schema = migrate.NewSchema(c.driver) 38 | c.TodoItem = NewTodoItemClient(c.config) 39 | } 40 | 41 | // Open opens a database/sql.DB specified by the driver name and 42 | // the data source name, and returns a new client attached to it. 43 | // Optional parameters can be added for configuring the client. 44 | func Open(driverName, dataSourceName string, options ...Option) (*Client, error) { 45 | switch driverName { 46 | case dialect.MySQL, dialect.Postgres, dialect.SQLite: 47 | drv, err := sql.Open(driverName, dataSourceName) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return NewClient(append(options, Driver(drv))...), nil 52 | default: 53 | return nil, fmt.Errorf("unsupported driver: %q", driverName) 54 | } 55 | } 56 | 57 | // Tx returns a new transactional client. The provided context 58 | // is used until the transaction is committed or rolled back. 59 | func (c *Client) Tx(ctx context.Context) (*Tx, error) { 60 | if _, ok := c.driver.(*txDriver); ok { 61 | return nil, fmt.Errorf("ent: cannot start a transaction within a transaction") 62 | } 63 | tx, err := newTx(ctx, c.driver) 64 | if err != nil { 65 | return nil, fmt.Errorf("ent: starting a transaction: %w", err) 66 | } 67 | cfg := c.config 68 | cfg.driver = tx 69 | return &Tx{ 70 | ctx: ctx, 71 | config: cfg, 72 | TodoItem: NewTodoItemClient(cfg), 73 | }, nil 74 | } 75 | 76 | // BeginTx returns a transactional client with specified options. 77 | func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { 78 | if _, ok := c.driver.(*txDriver); ok { 79 | return nil, fmt.Errorf("ent: cannot start a transaction within a transaction") 80 | } 81 | tx, err := c.driver.(interface { 82 | BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error) 83 | }).BeginTx(ctx, opts) 84 | if err != nil { 85 | return nil, fmt.Errorf("ent: starting a transaction: %w", err) 86 | } 87 | cfg := c.config 88 | cfg.driver = &txDriver{tx: tx, drv: c.driver} 89 | return &Tx{ 90 | config: cfg, 91 | TodoItem: NewTodoItemClient(cfg), 92 | }, nil 93 | } 94 | 95 | // Debug returns a new debug-client. It's used to get verbose logging on specific operations. 96 | // 97 | // client.Debug(). 98 | // TodoItem. 99 | // Query(). 100 | // Count(ctx) 101 | // 102 | func (c *Client) Debug() *Client { 103 | if c.debug { 104 | return c 105 | } 106 | cfg := c.config 107 | cfg.driver = dialect.Debug(c.driver, c.log) 108 | client := &Client{config: cfg} 109 | client.init() 110 | return client 111 | } 112 | 113 | // Close closes the database connection and prevents new queries from starting. 114 | func (c *Client) Close() error { 115 | return c.driver.Close() 116 | } 117 | 118 | // Use adds the mutation hooks to all the entity clients. 119 | // In order to add hooks to a specific client, call: `client.Node.Use(...)`. 120 | func (c *Client) Use(hooks ...Hook) { 121 | c.TodoItem.Use(hooks...) 122 | } 123 | 124 | // TodoItemClient is a client for the TodoItem schema. 125 | type TodoItemClient struct { 126 | config 127 | } 128 | 129 | // NewTodoItemClient returns a client for the TodoItem from the given config. 130 | func NewTodoItemClient(c config) *TodoItemClient { 131 | return &TodoItemClient{config: c} 132 | } 133 | 134 | // Use adds a list of mutation hooks to the hooks stack. 135 | // A call to `Use(f, g, h)` equals to `todoitem.Hooks(f(g(h())))`. 136 | func (c *TodoItemClient) Use(hooks ...Hook) { 137 | c.hooks.TodoItem = append(c.hooks.TodoItem, hooks...) 138 | } 139 | 140 | // Create returns a create builder for TodoItem. 141 | func (c *TodoItemClient) Create() *TodoItemCreate { 142 | mutation := newTodoItemMutation(c.config, OpCreate) 143 | return &TodoItemCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} 144 | } 145 | 146 | // CreateBulk returns a builder for creating a bulk of TodoItem entities. 147 | func (c *TodoItemClient) CreateBulk(builders ...*TodoItemCreate) *TodoItemCreateBulk { 148 | return &TodoItemCreateBulk{config: c.config, builders: builders} 149 | } 150 | 151 | // Update returns an update builder for TodoItem. 152 | func (c *TodoItemClient) Update() *TodoItemUpdate { 153 | mutation := newTodoItemMutation(c.config, OpUpdate) 154 | return &TodoItemUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} 155 | } 156 | 157 | // UpdateOne returns an update builder for the given entity. 158 | func (c *TodoItemClient) UpdateOne(ti *TodoItem) *TodoItemUpdateOne { 159 | mutation := newTodoItemMutation(c.config, OpUpdateOne, withTodoItem(ti)) 160 | return &TodoItemUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} 161 | } 162 | 163 | // UpdateOneID returns an update builder for the given id. 164 | func (c *TodoItemClient) UpdateOneID(id int) *TodoItemUpdateOne { 165 | mutation := newTodoItemMutation(c.config, OpUpdateOne, withTodoItemID(id)) 166 | return &TodoItemUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} 167 | } 168 | 169 | // Delete returns a delete builder for TodoItem. 170 | func (c *TodoItemClient) Delete() *TodoItemDelete { 171 | mutation := newTodoItemMutation(c.config, OpDelete) 172 | return &TodoItemDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} 173 | } 174 | 175 | // DeleteOne returns a delete builder for the given entity. 176 | func (c *TodoItemClient) DeleteOne(ti *TodoItem) *TodoItemDeleteOne { 177 | return c.DeleteOneID(ti.ID) 178 | } 179 | 180 | // DeleteOneID returns a delete builder for the given id. 181 | func (c *TodoItemClient) DeleteOneID(id int) *TodoItemDeleteOne { 182 | builder := c.Delete().Where(todoitem.ID(id)) 183 | builder.mutation.id = &id 184 | builder.mutation.op = OpDeleteOne 185 | return &TodoItemDeleteOne{builder} 186 | } 187 | 188 | // Query returns a query builder for TodoItem. 189 | func (c *TodoItemClient) Query() *TodoItemQuery { 190 | return &TodoItemQuery{ 191 | config: c.config, 192 | } 193 | } 194 | 195 | // Get returns a TodoItem entity by its id. 196 | func (c *TodoItemClient) Get(ctx context.Context, id int) (*TodoItem, error) { 197 | return c.Query().Where(todoitem.ID(id)).Only(ctx) 198 | } 199 | 200 | // GetX is like Get, but panics if an error occurs. 201 | func (c *TodoItemClient) GetX(ctx context.Context, id int) *TodoItem { 202 | obj, err := c.Get(ctx, id) 203 | if err != nil { 204 | panic(err) 205 | } 206 | return obj 207 | } 208 | 209 | // Hooks returns the client hooks. 210 | func (c *TodoItemClient) Hooks() []Hook { 211 | return c.hooks.TodoItem 212 | } 213 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/config.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "entgo.io/ent" 7 | "entgo.io/ent/dialect" 8 | ) 9 | 10 | // Option function to configure the client. 11 | type Option func(*config) 12 | 13 | // Config is the configuration for the client and its builder. 14 | type config struct { 15 | // driver used for executing database requests. 16 | driver dialect.Driver 17 | // debug enable a debug logging. 18 | debug bool 19 | // log used for logging on debug mode. 20 | log func(...interface{}) 21 | // hooks to execute on mutations. 22 | hooks *hooks 23 | } 24 | 25 | // hooks per client, for fast access. 26 | type hooks struct { 27 | TodoItem []ent.Hook 28 | } 29 | 30 | // Options applies the options on the config object. 31 | func (c *config) options(opts ...Option) { 32 | for _, opt := range opts { 33 | opt(c) 34 | } 35 | if c.debug { 36 | c.driver = dialect.Debug(c.driver, c.log) 37 | } 38 | } 39 | 40 | // Debug enables debug logging on the ent.Driver. 41 | func Debug() Option { 42 | return func(c *config) { 43 | c.debug = true 44 | } 45 | } 46 | 47 | // Log sets the logging function for debug mode. 48 | func Log(fn func(...interface{})) Option { 49 | return func(c *config) { 50 | c.log = fn 51 | } 52 | } 53 | 54 | // Driver configures the client driver. 55 | func Driver(driver dialect.Driver) Option { 56 | return func(c *config) { 57 | c.driver = driver 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/context.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type clientCtxKey struct{} 10 | 11 | // FromContext returns a Client stored inside a context, or nil if there isn't one. 12 | func FromContext(ctx context.Context) *Client { 13 | c, _ := ctx.Value(clientCtxKey{}).(*Client) 14 | return c 15 | } 16 | 17 | // NewContext returns a new context with the given Client attached. 18 | func NewContext(parent context.Context, c *Client) context.Context { 19 | return context.WithValue(parent, clientCtxKey{}, c) 20 | } 21 | 22 | type txCtxKey struct{} 23 | 24 | // TxFromContext returns a Tx stored inside a context, or nil if there isn't one. 25 | func TxFromContext(ctx context.Context) *Tx { 26 | tx, _ := ctx.Value(txCtxKey{}).(*Tx) 27 | return tx 28 | } 29 | 30 | // NewTxContext returns a new context with the given Tx attached. 31 | func NewTxContext(parent context.Context, tx *Tx) context.Context { 32 | return context.WithValue(parent, txCtxKey{}, tx) 33 | } 34 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/ent.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "entgo.io/ent" 10 | "entgo.io/ent/dialect/sql" 11 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 12 | ) 13 | 14 | // ent aliases to avoid import conflicts in user's code. 15 | type ( 16 | Op = ent.Op 17 | Hook = ent.Hook 18 | Value = ent.Value 19 | Query = ent.Query 20 | Policy = ent.Policy 21 | Mutator = ent.Mutator 22 | Mutation = ent.Mutation 23 | MutateFunc = ent.MutateFunc 24 | ) 25 | 26 | // OrderFunc applies an ordering on the sql selector. 27 | type OrderFunc func(*sql.Selector) 28 | 29 | // columnChecker returns a function indicates if the column exists in the given column. 30 | func columnChecker(table string) func(string) error { 31 | checks := map[string]func(string) bool{ 32 | todoitem.Table: todoitem.ValidColumn, 33 | } 34 | check, ok := checks[table] 35 | if !ok { 36 | return func(string) error { 37 | return fmt.Errorf("unknown table %q", table) 38 | } 39 | } 40 | return func(column string) error { 41 | if !check(column) { 42 | return fmt.Errorf("unknown column %q for table %q", column, table) 43 | } 44 | return nil 45 | } 46 | } 47 | 48 | // Asc applies the given fields in ASC order. 49 | func Asc(fields ...string) OrderFunc { 50 | return func(s *sql.Selector) { 51 | check := columnChecker(s.TableName()) 52 | for _, f := range fields { 53 | if err := check(f); err != nil { 54 | s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) 55 | } 56 | s.OrderBy(sql.Asc(s.C(f))) 57 | } 58 | } 59 | } 60 | 61 | // Desc applies the given fields in DESC order. 62 | func Desc(fields ...string) OrderFunc { 63 | return func(s *sql.Selector) { 64 | check := columnChecker(s.TableName()) 65 | for _, f := range fields { 66 | if err := check(f); err != nil { 67 | s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) 68 | } 69 | s.OrderBy(sql.Desc(s.C(f))) 70 | } 71 | } 72 | } 73 | 74 | // AggregateFunc applies an aggregation step on the group-by traversal/selector. 75 | type AggregateFunc func(*sql.Selector) string 76 | 77 | // As is a pseudo aggregation function for renaming another other functions with custom names. For example: 78 | // 79 | // GroupBy(field1, field2). 80 | // Aggregate(ent.As(ent.Sum(field1), "sum_field1"), (ent.As(ent.Sum(field2), "sum_field2")). 81 | // Scan(ctx, &v) 82 | // 83 | func As(fn AggregateFunc, end string) AggregateFunc { 84 | return func(s *sql.Selector) string { 85 | return sql.As(fn(s), end) 86 | } 87 | } 88 | 89 | // Count applies the "count" aggregation function on each group. 90 | func Count() AggregateFunc { 91 | return func(s *sql.Selector) string { 92 | return sql.Count("*") 93 | } 94 | } 95 | 96 | // Max applies the "max" aggregation function on the given field of each group. 97 | func Max(field string) AggregateFunc { 98 | return func(s *sql.Selector) string { 99 | check := columnChecker(s.TableName()) 100 | if err := check(field); err != nil { 101 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) 102 | return "" 103 | } 104 | return sql.Max(s.C(field)) 105 | } 106 | } 107 | 108 | // Mean applies the "mean" aggregation function on the given field of each group. 109 | func Mean(field string) AggregateFunc { 110 | return func(s *sql.Selector) string { 111 | check := columnChecker(s.TableName()) 112 | if err := check(field); err != nil { 113 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) 114 | return "" 115 | } 116 | return sql.Avg(s.C(field)) 117 | } 118 | } 119 | 120 | // Min applies the "min" aggregation function on the given field of each group. 121 | func Min(field string) AggregateFunc { 122 | return func(s *sql.Selector) string { 123 | check := columnChecker(s.TableName()) 124 | if err := check(field); err != nil { 125 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) 126 | return "" 127 | } 128 | return sql.Min(s.C(field)) 129 | } 130 | } 131 | 132 | // Sum applies the "sum" aggregation function on the given field of each group. 133 | func Sum(field string) AggregateFunc { 134 | return func(s *sql.Selector) string { 135 | check := columnChecker(s.TableName()) 136 | if err := check(field); err != nil { 137 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) 138 | return "" 139 | } 140 | return sql.Sum(s.C(field)) 141 | } 142 | } 143 | 144 | // ValidationError returns when validating a field fails. 145 | type ValidationError struct { 146 | Name string // Field or edge name. 147 | err error 148 | } 149 | 150 | // Error implements the error interface. 151 | func (e *ValidationError) Error() string { 152 | return e.err.Error() 153 | } 154 | 155 | // Unwrap implements the errors.Wrapper interface. 156 | func (e *ValidationError) Unwrap() error { 157 | return e.err 158 | } 159 | 160 | // IsValidationError returns a boolean indicating whether the error is a validation error. 161 | func IsValidationError(err error) bool { 162 | if err == nil { 163 | return false 164 | } 165 | var e *ValidationError 166 | return errors.As(err, &e) 167 | } 168 | 169 | // NotFoundError returns when trying to fetch a specific entity and it was not found in the database. 170 | type NotFoundError struct { 171 | label string 172 | } 173 | 174 | // Error implements the error interface. 175 | func (e *NotFoundError) Error() string { 176 | return "ent: " + e.label + " not found" 177 | } 178 | 179 | // IsNotFound returns a boolean indicating whether the error is a not found error. 180 | func IsNotFound(err error) bool { 181 | if err == nil { 182 | return false 183 | } 184 | var e *NotFoundError 185 | return errors.As(err, &e) 186 | } 187 | 188 | // MaskNotFound masks not found error. 189 | func MaskNotFound(err error) error { 190 | if IsNotFound(err) { 191 | return nil 192 | } 193 | return err 194 | } 195 | 196 | // NotSingularError returns when trying to fetch a singular entity and more then one was found in the database. 197 | type NotSingularError struct { 198 | label string 199 | } 200 | 201 | // Error implements the error interface. 202 | func (e *NotSingularError) Error() string { 203 | return "ent: " + e.label + " not singular" 204 | } 205 | 206 | // IsNotSingular returns a boolean indicating whether the error is a not singular error. 207 | func IsNotSingular(err error) bool { 208 | if err == nil { 209 | return false 210 | } 211 | var e *NotSingularError 212 | return errors.As(err, &e) 213 | } 214 | 215 | // NotLoadedError returns when trying to get a node that was not loaded by the query. 216 | type NotLoadedError struct { 217 | edge string 218 | } 219 | 220 | // Error implements the error interface. 221 | func (e *NotLoadedError) Error() string { 222 | return "ent: " + e.edge + " edge was not loaded" 223 | } 224 | 225 | // IsNotLoaded returns a boolean indicating whether the error is a not loaded error. 226 | func IsNotLoaded(err error) bool { 227 | if err == nil { 228 | return false 229 | } 230 | var e *NotLoadedError 231 | return errors.As(err, &e) 232 | } 233 | 234 | // ConstraintError returns when trying to create/update one or more entities and 235 | // one or more of their constraints failed. For example, violation of edge or 236 | // field uniqueness. 237 | type ConstraintError struct { 238 | msg string 239 | wrap error 240 | } 241 | 242 | // Error implements the error interface. 243 | func (e ConstraintError) Error() string { 244 | return "ent: constraint failed: " + e.msg 245 | } 246 | 247 | // Unwrap implements the errors.Wrapper interface. 248 | func (e *ConstraintError) Unwrap() error { 249 | return e.wrap 250 | } 251 | 252 | // IsConstraintError returns a boolean indicating whether the error is a constraint failure. 253 | func IsConstraintError(err error) bool { 254 | if err == nil { 255 | return false 256 | } 257 | var e *ConstraintError 258 | return errors.As(err, &e) 259 | } 260 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/enttest/enttest.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package enttest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent" 9 | // required by schema hooks. 10 | _ "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/runtime" 11 | 12 | "entgo.io/ent/dialect/sql/schema" 13 | ) 14 | 15 | type ( 16 | // TestingT is the interface that is shared between 17 | // testing.T and testing.B and used by enttest. 18 | TestingT interface { 19 | FailNow() 20 | Error(...interface{}) 21 | } 22 | 23 | // Option configures client creation. 24 | Option func(*options) 25 | 26 | options struct { 27 | opts []ent.Option 28 | migrateOpts []schema.MigrateOption 29 | } 30 | ) 31 | 32 | // WithOptions forwards options to client creation. 33 | func WithOptions(opts ...ent.Option) Option { 34 | return func(o *options) { 35 | o.opts = append(o.opts, opts...) 36 | } 37 | } 38 | 39 | // WithMigrateOptions forwards options to auto migration. 40 | func WithMigrateOptions(opts ...schema.MigrateOption) Option { 41 | return func(o *options) { 42 | o.migrateOpts = append(o.migrateOpts, opts...) 43 | } 44 | } 45 | 46 | func newOptions(opts []Option) *options { 47 | o := &options{} 48 | for _, opt := range opts { 49 | opt(o) 50 | } 51 | return o 52 | } 53 | 54 | // Open calls ent.Open and auto-run migration. 55 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { 56 | o := newOptions(opts) 57 | c, err := ent.Open(driverName, dataSourceName, o.opts...) 58 | if err != nil { 59 | t.Error(err) 60 | t.FailNow() 61 | } 62 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil { 63 | t.Error(err) 64 | t.FailNow() 65 | } 66 | return c 67 | } 68 | 69 | // NewClient calls ent.NewClient and auto-run migration. 70 | func NewClient(t TestingT, opts ...Option) *ent.Client { 71 | o := newOptions(opts) 72 | c := ent.NewClient(o.opts...) 73 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil { 74 | t.Error(err) 75 | t.FailNow() 76 | } 77 | return c 78 | } 79 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/hook/hook.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package hook 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent" 10 | ) 11 | 12 | // The TodoItemFunc type is an adapter to allow the use of ordinary 13 | // function as TodoItem mutator. 14 | type TodoItemFunc func(context.Context, *ent.TodoItemMutation) (ent.Value, error) 15 | 16 | // Mutate calls f(ctx, m). 17 | func (f TodoItemFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { 18 | mv, ok := m.(*ent.TodoItemMutation) 19 | if !ok { 20 | return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.TodoItemMutation", m) 21 | } 22 | return f(ctx, mv) 23 | } 24 | 25 | // Condition is a hook condition function. 26 | type Condition func(context.Context, ent.Mutation) bool 27 | 28 | // And groups conditions with the AND operator. 29 | func And(first, second Condition, rest ...Condition) Condition { 30 | return func(ctx context.Context, m ent.Mutation) bool { 31 | if !first(ctx, m) || !second(ctx, m) { 32 | return false 33 | } 34 | for _, cond := range rest { 35 | if !cond(ctx, m) { 36 | return false 37 | } 38 | } 39 | return true 40 | } 41 | } 42 | 43 | // Or groups conditions with the OR operator. 44 | func Or(first, second Condition, rest ...Condition) Condition { 45 | return func(ctx context.Context, m ent.Mutation) bool { 46 | if first(ctx, m) || second(ctx, m) { 47 | return true 48 | } 49 | for _, cond := range rest { 50 | if cond(ctx, m) { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | } 57 | 58 | // Not negates a given condition. 59 | func Not(cond Condition) Condition { 60 | return func(ctx context.Context, m ent.Mutation) bool { 61 | return !cond(ctx, m) 62 | } 63 | } 64 | 65 | // HasOp is a condition testing mutation operation. 66 | func HasOp(op ent.Op) Condition { 67 | return func(_ context.Context, m ent.Mutation) bool { 68 | return m.Op().Is(op) 69 | } 70 | } 71 | 72 | // HasAddedFields is a condition validating `.AddedField` on fields. 73 | func HasAddedFields(field string, fields ...string) Condition { 74 | return func(_ context.Context, m ent.Mutation) bool { 75 | if _, exists := m.AddedField(field); !exists { 76 | return false 77 | } 78 | for _, field := range fields { 79 | if _, exists := m.AddedField(field); !exists { 80 | return false 81 | } 82 | } 83 | return true 84 | } 85 | } 86 | 87 | // HasClearedFields is a condition validating `.FieldCleared` on fields. 88 | func HasClearedFields(field string, fields ...string) Condition { 89 | return func(_ context.Context, m ent.Mutation) bool { 90 | if exists := m.FieldCleared(field); !exists { 91 | return false 92 | } 93 | for _, field := range fields { 94 | if exists := m.FieldCleared(field); !exists { 95 | return false 96 | } 97 | } 98 | return true 99 | } 100 | } 101 | 102 | // HasFields is a condition validating `.Field` on fields. 103 | func HasFields(field string, fields ...string) Condition { 104 | return func(_ context.Context, m ent.Mutation) bool { 105 | if _, exists := m.Field(field); !exists { 106 | return false 107 | } 108 | for _, field := range fields { 109 | if _, exists := m.Field(field); !exists { 110 | return false 111 | } 112 | } 113 | return true 114 | } 115 | } 116 | 117 | // If executes the given hook under condition. 118 | // 119 | // hook.If(ComputeAverage, And(HasFields(...), HasAddedFields(...))) 120 | // 121 | func If(hk ent.Hook, cond Condition) ent.Hook { 122 | return func(next ent.Mutator) ent.Mutator { 123 | return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { 124 | if cond(ctx, m) { 125 | return hk(next).Mutate(ctx, m) 126 | } 127 | return next.Mutate(ctx, m) 128 | }) 129 | } 130 | } 131 | 132 | // On executes the given hook only for the given operation. 133 | // 134 | // hook.On(Log, ent.Delete|ent.Create) 135 | // 136 | func On(hk ent.Hook, op ent.Op) ent.Hook { 137 | return If(hk, HasOp(op)) 138 | } 139 | 140 | // Unless skips the given hook only for the given operation. 141 | // 142 | // hook.Unless(Log, ent.Update|ent.UpdateOne) 143 | // 144 | func Unless(hk ent.Hook, op ent.Op) ent.Hook { 145 | return If(hk, Not(HasOp(op))) 146 | } 147 | 148 | // FixedError is a hook returning a fixed error. 149 | func FixedError(err error) ent.Hook { 150 | return func(ent.Mutator) ent.Mutator { 151 | return ent.MutateFunc(func(context.Context, ent.Mutation) (ent.Value, error) { 152 | return nil, err 153 | }) 154 | } 155 | } 156 | 157 | // Reject returns a hook that rejects all operations that match op. 158 | // 159 | // func (T) Hooks() []ent.Hook { 160 | // return []ent.Hook{ 161 | // Reject(ent.Delete|ent.Update), 162 | // } 163 | // } 164 | // 165 | func Reject(op ent.Op) ent.Hook { 166 | hk := FixedError(fmt.Errorf("%s operation is not allowed", op)) 167 | return On(hk, op) 168 | } 169 | 170 | // Chain acts as a list of hooks and is effectively immutable. 171 | // Once created, it will always hold the same set of hooks in the same order. 172 | type Chain struct { 173 | hooks []ent.Hook 174 | } 175 | 176 | // NewChain creates a new chain of hooks. 177 | func NewChain(hooks ...ent.Hook) Chain { 178 | return Chain{append([]ent.Hook(nil), hooks...)} 179 | } 180 | 181 | // Hook chains the list of hooks and returns the final hook. 182 | func (c Chain) Hook() ent.Hook { 183 | return func(mutator ent.Mutator) ent.Mutator { 184 | for i := len(c.hooks) - 1; i >= 0; i-- { 185 | mutator = c.hooks[i](mutator) 186 | } 187 | return mutator 188 | } 189 | } 190 | 191 | // Append extends a chain, adding the specified hook 192 | // as the last ones in the mutation flow. 193 | func (c Chain) Append(hooks ...ent.Hook) Chain { 194 | newHooks := make([]ent.Hook, 0, len(c.hooks)+len(hooks)) 195 | newHooks = append(newHooks, c.hooks...) 196 | newHooks = append(newHooks, hooks...) 197 | return Chain{newHooks} 198 | } 199 | 200 | // Extend extends a chain, adding the specified chain 201 | // as the last ones in the mutation flow. 202 | func (c Chain) Extend(chain Chain) Chain { 203 | return c.Append(chain.hooks...) 204 | } 205 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package migrate 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | 10 | "entgo.io/ent/dialect" 11 | "entgo.io/ent/dialect/sql/schema" 12 | ) 13 | 14 | var ( 15 | // WithGlobalUniqueID sets the universal ids options to the migration. 16 | // If this option is enabled, ent migration will allocate a 1<<32 range 17 | // for the ids of each entity (table). 18 | // Note that this option cannot be applied on tables that already exist. 19 | WithGlobalUniqueID = schema.WithGlobalUniqueID 20 | // WithDropColumn sets the drop column option to the migration. 21 | // If this option is enabled, ent migration will drop old columns 22 | // that were used for both fields and edges. This defaults to false. 23 | WithDropColumn = schema.WithDropColumn 24 | // WithDropIndex sets the drop index option to the migration. 25 | // If this option is enabled, ent migration will drop old indexes 26 | // that were defined in the schema. This defaults to false. 27 | // Note that unique constraints are defined using `UNIQUE INDEX`, 28 | // and therefore, it's recommended to enable this option to get more 29 | // flexibility in the schema changes. 30 | WithDropIndex = schema.WithDropIndex 31 | // WithFixture sets the foreign-key renaming option to the migration when upgrading 32 | // ent from v0.1.0 (issue-#285). Defaults to false. 33 | WithFixture = schema.WithFixture 34 | // WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true. 35 | WithForeignKeys = schema.WithForeignKeys 36 | ) 37 | 38 | // Schema is the API for creating, migrating and dropping a schema. 39 | type Schema struct { 40 | drv dialect.Driver 41 | universalID bool 42 | } 43 | 44 | // NewSchema creates a new schema client. 45 | func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} } 46 | 47 | // Create creates all schema resources. 48 | func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error { 49 | migrate, err := schema.NewMigrate(s.drv, opts...) 50 | if err != nil { 51 | return fmt.Errorf("ent/migrate: %w", err) 52 | } 53 | return migrate.Create(ctx, Tables...) 54 | } 55 | 56 | // WriteTo writes the schema changes to w instead of running them against the database. 57 | // 58 | // if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil { 59 | // log.Fatal(err) 60 | // } 61 | // 62 | func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error { 63 | drv := &schema.WriteDriver{ 64 | Writer: w, 65 | Driver: s.drv, 66 | } 67 | migrate, err := schema.NewMigrate(drv, opts...) 68 | if err != nil { 69 | return fmt.Errorf("ent/migrate: %w", err) 70 | } 71 | return migrate.Create(ctx, Tables...) 72 | } 73 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/migrate/schema.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package migrate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql/schema" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | var ( 11 | // TodoItemsColumns holds the columns for the "todo_items" table. 12 | TodoItemsColumns = []*schema.Column{ 13 | {Name: "id", Type: field.TypeInt, Increment: true}, 14 | {Name: "uid", Type: field.TypeString, Unique: true, Size: 26}, 15 | {Name: "title", Type: field.TypeString, Size: 2147483647}, 16 | {Name: "completed", Type: field.TypeBool}, 17 | {Name: "order", Type: field.TypeInt}, 18 | {Name: "created_at", Type: field.TypeTime}, 19 | {Name: "updated_at", Type: field.TypeTime}, 20 | } 21 | // TodoItemsTable holds the schema information for the "todo_items" table. 22 | TodoItemsTable = &schema.Table{ 23 | Name: "todo_items", 24 | Columns: TodoItemsColumns, 25 | PrimaryKey: []*schema.Column{TodoItemsColumns[0]}, 26 | } 27 | // Tables holds all the tables in the schema. 28 | Tables = []*schema.Table{ 29 | TodoItemsTable, 30 | } 31 | ) 32 | 33 | func init() { 34 | } 35 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/predicate/predicate.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package predicate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | // TodoItem is the predicate function for todoitem builders. 10 | type TodoItem func(*sql.Selector) 11 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/schema" 9 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 10 | ) 11 | 12 | // The init function reads all schema descriptors with runtime code 13 | // (default values, validators, hooks and policies) and stitches it 14 | // to their package variables. 15 | func init() { 16 | todoitemFields := schema.TodoItem{}.Fields() 17 | _ = todoitemFields 18 | // todoitemDescUID is the schema descriptor for uid field. 19 | todoitemDescUID := todoitemFields[0].Descriptor() 20 | // todoitem.UIDValidator is a validator for the "uid" field. It is called by the builders before save. 21 | todoitem.UIDValidator = func() func(string) error { 22 | validators := todoitemDescUID.Validators 23 | fns := [...]func(string) error{ 24 | validators[0].(func(string) error), 25 | validators[1].(func(string) error), 26 | } 27 | return func(uid string) error { 28 | for _, fn := range fns { 29 | if err := fn(uid); err != nil { 30 | return err 31 | } 32 | } 33 | return nil 34 | } 35 | }() 36 | // todoitemDescCreatedAt is the schema descriptor for created_at field. 37 | todoitemDescCreatedAt := todoitemFields[4].Descriptor() 38 | // todoitem.DefaultCreatedAt holds the default value on creation for the created_at field. 39 | todoitem.DefaultCreatedAt = todoitemDescCreatedAt.Default.(func() time.Time) 40 | // todoitemDescUpdatedAt is the schema descriptor for updated_at field. 41 | todoitemDescUpdatedAt := todoitemFields[5].Descriptor() 42 | // todoitem.DefaultUpdatedAt holds the default value on creation for the updated_at field. 43 | todoitem.DefaultUpdatedAt = todoitemDescUpdatedAt.Default.(func() time.Time) 44 | // todoitem.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. 45 | todoitem.UpdateDefaultUpdatedAt = todoitemDescUpdatedAt.UpdateDefault.(func() time.Time) 46 | } 47 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package runtime 4 | 5 | // The schema-stitching logic is generated in github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/runtime.go 6 | 7 | const ( 8 | Version = "(devel)" // Version of ent codegen. 9 | ) 10 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/schema/todoitem.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "entgo.io/ent" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | // TodoItem holds the schema definition for the TodoItem entity. 11 | type TodoItem struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the TodoItem. 16 | func (TodoItem) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("uid"). 19 | MaxLen(26). 20 | NotEmpty(). 21 | Unique(). 22 | Immutable(), 23 | field.Text("title"), 24 | field.Bool("completed"), 25 | field.Int("order"), 26 | field.Time("created_at"). 27 | Default(time.Now), 28 | field.Time("updated_at"). 29 | Default(time.Now). 30 | UpdateDefault(time.Now), 31 | } 32 | } 33 | 34 | // Edges of the TodoItem. 35 | func (TodoItem) Edges() []ent.Edge { 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/todoitem.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "entgo.io/ent/dialect/sql" 11 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 12 | ) 13 | 14 | // TodoItem is the model entity for the TodoItem schema. 15 | type TodoItem struct { 16 | config `json:"-"` 17 | // ID of the ent. 18 | ID int `json:"id,omitempty"` 19 | // UID holds the value of the "uid" field. 20 | UID string `json:"uid,omitempty"` 21 | // Title holds the value of the "title" field. 22 | Title string `json:"title,omitempty"` 23 | // Completed holds the value of the "completed" field. 24 | Completed bool `json:"completed,omitempty"` 25 | // Order holds the value of the "order" field. 26 | Order int `json:"order,omitempty"` 27 | // CreatedAt holds the value of the "created_at" field. 28 | CreatedAt time.Time `json:"created_at,omitempty"` 29 | // UpdatedAt holds the value of the "updated_at" field. 30 | UpdatedAt time.Time `json:"updated_at,omitempty"` 31 | } 32 | 33 | // scanValues returns the types for scanning values from sql.Rows. 34 | func (*TodoItem) scanValues(columns []string) ([]interface{}, error) { 35 | values := make([]interface{}, len(columns)) 36 | for i := range columns { 37 | switch columns[i] { 38 | case todoitem.FieldCompleted: 39 | values[i] = new(sql.NullBool) 40 | case todoitem.FieldID, todoitem.FieldOrder: 41 | values[i] = new(sql.NullInt64) 42 | case todoitem.FieldUID, todoitem.FieldTitle: 43 | values[i] = new(sql.NullString) 44 | case todoitem.FieldCreatedAt, todoitem.FieldUpdatedAt: 45 | values[i] = new(sql.NullTime) 46 | default: 47 | return nil, fmt.Errorf("unexpected column %q for type TodoItem", columns[i]) 48 | } 49 | } 50 | return values, nil 51 | } 52 | 53 | // assignValues assigns the values that were returned from sql.Rows (after scanning) 54 | // to the TodoItem fields. 55 | func (ti *TodoItem) assignValues(columns []string, values []interface{}) error { 56 | if m, n := len(values), len(columns); m < n { 57 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) 58 | } 59 | for i := range columns { 60 | switch columns[i] { 61 | case todoitem.FieldID: 62 | value, ok := values[i].(*sql.NullInt64) 63 | if !ok { 64 | return fmt.Errorf("unexpected type %T for field id", value) 65 | } 66 | ti.ID = int(value.Int64) 67 | case todoitem.FieldUID: 68 | if value, ok := values[i].(*sql.NullString); !ok { 69 | return fmt.Errorf("unexpected type %T for field uid", values[i]) 70 | } else if value.Valid { 71 | ti.UID = value.String 72 | } 73 | case todoitem.FieldTitle: 74 | if value, ok := values[i].(*sql.NullString); !ok { 75 | return fmt.Errorf("unexpected type %T for field title", values[i]) 76 | } else if value.Valid { 77 | ti.Title = value.String 78 | } 79 | case todoitem.FieldCompleted: 80 | if value, ok := values[i].(*sql.NullBool); !ok { 81 | return fmt.Errorf("unexpected type %T for field completed", values[i]) 82 | } else if value.Valid { 83 | ti.Completed = value.Bool 84 | } 85 | case todoitem.FieldOrder: 86 | if value, ok := values[i].(*sql.NullInt64); !ok { 87 | return fmt.Errorf("unexpected type %T for field order", values[i]) 88 | } else if value.Valid { 89 | ti.Order = int(value.Int64) 90 | } 91 | case todoitem.FieldCreatedAt: 92 | if value, ok := values[i].(*sql.NullTime); !ok { 93 | return fmt.Errorf("unexpected type %T for field created_at", values[i]) 94 | } else if value.Valid { 95 | ti.CreatedAt = value.Time 96 | } 97 | case todoitem.FieldUpdatedAt: 98 | if value, ok := values[i].(*sql.NullTime); !ok { 99 | return fmt.Errorf("unexpected type %T for field updated_at", values[i]) 100 | } else if value.Valid { 101 | ti.UpdatedAt = value.Time 102 | } 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | // Update returns a builder for updating this TodoItem. 109 | // Note that you need to call TodoItem.Unwrap() before calling this method if this TodoItem 110 | // was returned from a transaction, and the transaction was committed or rolled back. 111 | func (ti *TodoItem) Update() *TodoItemUpdateOne { 112 | return (&TodoItemClient{config: ti.config}).UpdateOne(ti) 113 | } 114 | 115 | // Unwrap unwraps the TodoItem entity that was returned from a transaction after it was closed, 116 | // so that all future queries will be executed through the driver which created the transaction. 117 | func (ti *TodoItem) Unwrap() *TodoItem { 118 | tx, ok := ti.config.driver.(*txDriver) 119 | if !ok { 120 | panic("ent: TodoItem is not a transactional entity") 121 | } 122 | ti.config.driver = tx.drv 123 | return ti 124 | } 125 | 126 | // String implements the fmt.Stringer. 127 | func (ti *TodoItem) String() string { 128 | var builder strings.Builder 129 | builder.WriteString("TodoItem(") 130 | builder.WriteString(fmt.Sprintf("id=%v", ti.ID)) 131 | builder.WriteString(", uid=") 132 | builder.WriteString(ti.UID) 133 | builder.WriteString(", title=") 134 | builder.WriteString(ti.Title) 135 | builder.WriteString(", completed=") 136 | builder.WriteString(fmt.Sprintf("%v", ti.Completed)) 137 | builder.WriteString(", order=") 138 | builder.WriteString(fmt.Sprintf("%v", ti.Order)) 139 | builder.WriteString(", created_at=") 140 | builder.WriteString(ti.CreatedAt.Format(time.ANSIC)) 141 | builder.WriteString(", updated_at=") 142 | builder.WriteString(ti.UpdatedAt.Format(time.ANSIC)) 143 | builder.WriteByte(')') 144 | return builder.String() 145 | } 146 | 147 | // TodoItems is a parsable slice of TodoItem. 148 | type TodoItems []*TodoItem 149 | 150 | func (ti TodoItems) config(cfg config) { 151 | for _i := range ti { 152 | ti[_i].config = cfg 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/todoitem/todoitem.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package todoitem 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | const ( 10 | // Label holds the string label denoting the todoitem type in the database. 11 | Label = "todo_item" 12 | // FieldID holds the string denoting the id field in the database. 13 | FieldID = "id" 14 | // FieldUID holds the string denoting the uid field in the database. 15 | FieldUID = "uid" 16 | // FieldTitle holds the string denoting the title field in the database. 17 | FieldTitle = "title" 18 | // FieldCompleted holds the string denoting the completed field in the database. 19 | FieldCompleted = "completed" 20 | // FieldOrder holds the string denoting the order field in the database. 21 | FieldOrder = "order" 22 | // FieldCreatedAt holds the string denoting the created_at field in the database. 23 | FieldCreatedAt = "created_at" 24 | // FieldUpdatedAt holds the string denoting the updated_at field in the database. 25 | FieldUpdatedAt = "updated_at" 26 | // Table holds the table name of the todoitem in the database. 27 | Table = "todo_items" 28 | ) 29 | 30 | // Columns holds all SQL columns for todoitem fields. 31 | var Columns = []string{ 32 | FieldID, 33 | FieldUID, 34 | FieldTitle, 35 | FieldCompleted, 36 | FieldOrder, 37 | FieldCreatedAt, 38 | FieldUpdatedAt, 39 | } 40 | 41 | // ValidColumn reports if the column name is valid (part of the table columns). 42 | func ValidColumn(column string) bool { 43 | for i := range Columns { 44 | if column == Columns[i] { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | var ( 52 | // UIDValidator is a validator for the "uid" field. It is called by the builders before save. 53 | UIDValidator func(string) error 54 | // DefaultCreatedAt holds the default value on creation for the "created_at" field. 55 | DefaultCreatedAt func() time.Time 56 | // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. 57 | DefaultUpdatedAt func() time.Time 58 | // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. 59 | UpdateDefaultUpdatedAt func() time.Time 60 | ) 61 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/todoitem_create.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "entgo.io/ent/dialect/sql/sqlgraph" 12 | "entgo.io/ent/schema/field" 13 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 14 | ) 15 | 16 | // TodoItemCreate is the builder for creating a TodoItem entity. 17 | type TodoItemCreate struct { 18 | config 19 | mutation *TodoItemMutation 20 | hooks []Hook 21 | } 22 | 23 | // SetUID sets the "uid" field. 24 | func (tic *TodoItemCreate) SetUID(s string) *TodoItemCreate { 25 | tic.mutation.SetUID(s) 26 | return tic 27 | } 28 | 29 | // SetTitle sets the "title" field. 30 | func (tic *TodoItemCreate) SetTitle(s string) *TodoItemCreate { 31 | tic.mutation.SetTitle(s) 32 | return tic 33 | } 34 | 35 | // SetCompleted sets the "completed" field. 36 | func (tic *TodoItemCreate) SetCompleted(b bool) *TodoItemCreate { 37 | tic.mutation.SetCompleted(b) 38 | return tic 39 | } 40 | 41 | // SetOrder sets the "order" field. 42 | func (tic *TodoItemCreate) SetOrder(i int) *TodoItemCreate { 43 | tic.mutation.SetOrder(i) 44 | return tic 45 | } 46 | 47 | // SetCreatedAt sets the "created_at" field. 48 | func (tic *TodoItemCreate) SetCreatedAt(t time.Time) *TodoItemCreate { 49 | tic.mutation.SetCreatedAt(t) 50 | return tic 51 | } 52 | 53 | // SetNillableCreatedAt sets the "created_at" field if the given value is not nil. 54 | func (tic *TodoItemCreate) SetNillableCreatedAt(t *time.Time) *TodoItemCreate { 55 | if t != nil { 56 | tic.SetCreatedAt(*t) 57 | } 58 | return tic 59 | } 60 | 61 | // SetUpdatedAt sets the "updated_at" field. 62 | func (tic *TodoItemCreate) SetUpdatedAt(t time.Time) *TodoItemCreate { 63 | tic.mutation.SetUpdatedAt(t) 64 | return tic 65 | } 66 | 67 | // SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. 68 | func (tic *TodoItemCreate) SetNillableUpdatedAt(t *time.Time) *TodoItemCreate { 69 | if t != nil { 70 | tic.SetUpdatedAt(*t) 71 | } 72 | return tic 73 | } 74 | 75 | // Mutation returns the TodoItemMutation object of the builder. 76 | func (tic *TodoItemCreate) Mutation() *TodoItemMutation { 77 | return tic.mutation 78 | } 79 | 80 | // Save creates the TodoItem in the database. 81 | func (tic *TodoItemCreate) Save(ctx context.Context) (*TodoItem, error) { 82 | var ( 83 | err error 84 | node *TodoItem 85 | ) 86 | tic.defaults() 87 | if len(tic.hooks) == 0 { 88 | if err = tic.check(); err != nil { 89 | return nil, err 90 | } 91 | node, err = tic.sqlSave(ctx) 92 | } else { 93 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { 94 | mutation, ok := m.(*TodoItemMutation) 95 | if !ok { 96 | return nil, fmt.Errorf("unexpected mutation type %T", m) 97 | } 98 | if err = tic.check(); err != nil { 99 | return nil, err 100 | } 101 | tic.mutation = mutation 102 | if node, err = tic.sqlSave(ctx); err != nil { 103 | return nil, err 104 | } 105 | mutation.id = &node.ID 106 | mutation.done = true 107 | return node, err 108 | }) 109 | for i := len(tic.hooks) - 1; i >= 0; i-- { 110 | if tic.hooks[i] == nil { 111 | return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") 112 | } 113 | mut = tic.hooks[i](mut) 114 | } 115 | if _, err := mut.Mutate(ctx, tic.mutation); err != nil { 116 | return nil, err 117 | } 118 | } 119 | return node, err 120 | } 121 | 122 | // SaveX calls Save and panics if Save returns an error. 123 | func (tic *TodoItemCreate) SaveX(ctx context.Context) *TodoItem { 124 | v, err := tic.Save(ctx) 125 | if err != nil { 126 | panic(err) 127 | } 128 | return v 129 | } 130 | 131 | // Exec executes the query. 132 | func (tic *TodoItemCreate) Exec(ctx context.Context) error { 133 | _, err := tic.Save(ctx) 134 | return err 135 | } 136 | 137 | // ExecX is like Exec, but panics if an error occurs. 138 | func (tic *TodoItemCreate) ExecX(ctx context.Context) { 139 | if err := tic.Exec(ctx); err != nil { 140 | panic(err) 141 | } 142 | } 143 | 144 | // defaults sets the default values of the builder before save. 145 | func (tic *TodoItemCreate) defaults() { 146 | if _, ok := tic.mutation.CreatedAt(); !ok { 147 | v := todoitem.DefaultCreatedAt() 148 | tic.mutation.SetCreatedAt(v) 149 | } 150 | if _, ok := tic.mutation.UpdatedAt(); !ok { 151 | v := todoitem.DefaultUpdatedAt() 152 | tic.mutation.SetUpdatedAt(v) 153 | } 154 | } 155 | 156 | // check runs all checks and user-defined validators on the builder. 157 | func (tic *TodoItemCreate) check() error { 158 | if _, ok := tic.mutation.UID(); !ok { 159 | return &ValidationError{Name: "uid", err: errors.New(`ent: missing required field "uid"`)} 160 | } 161 | if v, ok := tic.mutation.UID(); ok { 162 | if err := todoitem.UIDValidator(v); err != nil { 163 | return &ValidationError{Name: "uid", err: fmt.Errorf(`ent: validator failed for field "uid": %w`, err)} 164 | } 165 | } 166 | if _, ok := tic.mutation.Title(); !ok { 167 | return &ValidationError{Name: "title", err: errors.New(`ent: missing required field "title"`)} 168 | } 169 | if _, ok := tic.mutation.Completed(); !ok { 170 | return &ValidationError{Name: "completed", err: errors.New(`ent: missing required field "completed"`)} 171 | } 172 | if _, ok := tic.mutation.Order(); !ok { 173 | return &ValidationError{Name: "order", err: errors.New(`ent: missing required field "order"`)} 174 | } 175 | if _, ok := tic.mutation.CreatedAt(); !ok { 176 | return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "created_at"`)} 177 | } 178 | if _, ok := tic.mutation.UpdatedAt(); !ok { 179 | return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "updated_at"`)} 180 | } 181 | return nil 182 | } 183 | 184 | func (tic *TodoItemCreate) sqlSave(ctx context.Context) (*TodoItem, error) { 185 | _node, _spec := tic.createSpec() 186 | if err := sqlgraph.CreateNode(ctx, tic.driver, _spec); err != nil { 187 | if sqlgraph.IsConstraintError(err) { 188 | err = &ConstraintError{err.Error(), err} 189 | } 190 | return nil, err 191 | } 192 | id := _spec.ID.Value.(int64) 193 | _node.ID = int(id) 194 | return _node, nil 195 | } 196 | 197 | func (tic *TodoItemCreate) createSpec() (*TodoItem, *sqlgraph.CreateSpec) { 198 | var ( 199 | _node = &TodoItem{config: tic.config} 200 | _spec = &sqlgraph.CreateSpec{ 201 | Table: todoitem.Table, 202 | ID: &sqlgraph.FieldSpec{ 203 | Type: field.TypeInt, 204 | Column: todoitem.FieldID, 205 | }, 206 | } 207 | ) 208 | if value, ok := tic.mutation.UID(); ok { 209 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ 210 | Type: field.TypeString, 211 | Value: value, 212 | Column: todoitem.FieldUID, 213 | }) 214 | _node.UID = value 215 | } 216 | if value, ok := tic.mutation.Title(); ok { 217 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ 218 | Type: field.TypeString, 219 | Value: value, 220 | Column: todoitem.FieldTitle, 221 | }) 222 | _node.Title = value 223 | } 224 | if value, ok := tic.mutation.Completed(); ok { 225 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ 226 | Type: field.TypeBool, 227 | Value: value, 228 | Column: todoitem.FieldCompleted, 229 | }) 230 | _node.Completed = value 231 | } 232 | if value, ok := tic.mutation.Order(); ok { 233 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ 234 | Type: field.TypeInt, 235 | Value: value, 236 | Column: todoitem.FieldOrder, 237 | }) 238 | _node.Order = value 239 | } 240 | if value, ok := tic.mutation.CreatedAt(); ok { 241 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ 242 | Type: field.TypeTime, 243 | Value: value, 244 | Column: todoitem.FieldCreatedAt, 245 | }) 246 | _node.CreatedAt = value 247 | } 248 | if value, ok := tic.mutation.UpdatedAt(); ok { 249 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ 250 | Type: field.TypeTime, 251 | Value: value, 252 | Column: todoitem.FieldUpdatedAt, 253 | }) 254 | _node.UpdatedAt = value 255 | } 256 | return _node, _spec 257 | } 258 | 259 | // TodoItemCreateBulk is the builder for creating many TodoItem entities in bulk. 260 | type TodoItemCreateBulk struct { 261 | config 262 | builders []*TodoItemCreate 263 | } 264 | 265 | // Save creates the TodoItem entities in the database. 266 | func (ticb *TodoItemCreateBulk) Save(ctx context.Context) ([]*TodoItem, error) { 267 | specs := make([]*sqlgraph.CreateSpec, len(ticb.builders)) 268 | nodes := make([]*TodoItem, len(ticb.builders)) 269 | mutators := make([]Mutator, len(ticb.builders)) 270 | for i := range ticb.builders { 271 | func(i int, root context.Context) { 272 | builder := ticb.builders[i] 273 | builder.defaults() 274 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { 275 | mutation, ok := m.(*TodoItemMutation) 276 | if !ok { 277 | return nil, fmt.Errorf("unexpected mutation type %T", m) 278 | } 279 | if err := builder.check(); err != nil { 280 | return nil, err 281 | } 282 | builder.mutation = mutation 283 | nodes[i], specs[i] = builder.createSpec() 284 | var err error 285 | if i < len(mutators)-1 { 286 | _, err = mutators[i+1].Mutate(root, ticb.builders[i+1].mutation) 287 | } else { 288 | spec := &sqlgraph.BatchCreateSpec{Nodes: specs} 289 | // Invoke the actual operation on the latest mutation in the chain. 290 | if err = sqlgraph.BatchCreate(ctx, ticb.driver, spec); err != nil { 291 | if sqlgraph.IsConstraintError(err) { 292 | err = &ConstraintError{err.Error(), err} 293 | } 294 | } 295 | } 296 | if err != nil { 297 | return nil, err 298 | } 299 | mutation.id = &nodes[i].ID 300 | mutation.done = true 301 | if specs[i].ID.Value != nil { 302 | id := specs[i].ID.Value.(int64) 303 | nodes[i].ID = int(id) 304 | } 305 | return nodes[i], nil 306 | }) 307 | for i := len(builder.hooks) - 1; i >= 0; i-- { 308 | mut = builder.hooks[i](mut) 309 | } 310 | mutators[i] = mut 311 | }(i, ctx) 312 | } 313 | if len(mutators) > 0 { 314 | if _, err := mutators[0].Mutate(ctx, ticb.builders[0].mutation); err != nil { 315 | return nil, err 316 | } 317 | } 318 | return nodes, nil 319 | } 320 | 321 | // SaveX is like Save, but panics if an error occurs. 322 | func (ticb *TodoItemCreateBulk) SaveX(ctx context.Context) []*TodoItem { 323 | v, err := ticb.Save(ctx) 324 | if err != nil { 325 | panic(err) 326 | } 327 | return v 328 | } 329 | 330 | // Exec executes the query. 331 | func (ticb *TodoItemCreateBulk) Exec(ctx context.Context) error { 332 | _, err := ticb.Save(ctx) 333 | return err 334 | } 335 | 336 | // ExecX is like Exec, but panics if an error occurs. 337 | func (ticb *TodoItemCreateBulk) ExecX(ctx context.Context) { 338 | if err := ticb.Exec(ctx); err != nil { 339 | panic(err) 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/todoitem_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "entgo.io/ent/dialect/sql" 10 | "entgo.io/ent/dialect/sql/sqlgraph" 11 | "entgo.io/ent/schema/field" 12 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/predicate" 13 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 14 | ) 15 | 16 | // TodoItemDelete is the builder for deleting a TodoItem entity. 17 | type TodoItemDelete struct { 18 | config 19 | hooks []Hook 20 | mutation *TodoItemMutation 21 | } 22 | 23 | // Where appends a list predicates to the TodoItemDelete builder. 24 | func (tid *TodoItemDelete) Where(ps ...predicate.TodoItem) *TodoItemDelete { 25 | tid.mutation.Where(ps...) 26 | return tid 27 | } 28 | 29 | // Exec executes the deletion query and returns how many vertices were deleted. 30 | func (tid *TodoItemDelete) Exec(ctx context.Context) (int, error) { 31 | var ( 32 | err error 33 | affected int 34 | ) 35 | if len(tid.hooks) == 0 { 36 | affected, err = tid.sqlExec(ctx) 37 | } else { 38 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { 39 | mutation, ok := m.(*TodoItemMutation) 40 | if !ok { 41 | return nil, fmt.Errorf("unexpected mutation type %T", m) 42 | } 43 | tid.mutation = mutation 44 | affected, err = tid.sqlExec(ctx) 45 | mutation.done = true 46 | return affected, err 47 | }) 48 | for i := len(tid.hooks) - 1; i >= 0; i-- { 49 | if tid.hooks[i] == nil { 50 | return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") 51 | } 52 | mut = tid.hooks[i](mut) 53 | } 54 | if _, err := mut.Mutate(ctx, tid.mutation); err != nil { 55 | return 0, err 56 | } 57 | } 58 | return affected, err 59 | } 60 | 61 | // ExecX is like Exec, but panics if an error occurs. 62 | func (tid *TodoItemDelete) ExecX(ctx context.Context) int { 63 | n, err := tid.Exec(ctx) 64 | if err != nil { 65 | panic(err) 66 | } 67 | return n 68 | } 69 | 70 | func (tid *TodoItemDelete) sqlExec(ctx context.Context) (int, error) { 71 | _spec := &sqlgraph.DeleteSpec{ 72 | Node: &sqlgraph.NodeSpec{ 73 | Table: todoitem.Table, 74 | ID: &sqlgraph.FieldSpec{ 75 | Type: field.TypeInt, 76 | Column: todoitem.FieldID, 77 | }, 78 | }, 79 | } 80 | if ps := tid.mutation.predicates; len(ps) > 0 { 81 | _spec.Predicate = func(selector *sql.Selector) { 82 | for i := range ps { 83 | ps[i](selector) 84 | } 85 | } 86 | } 87 | return sqlgraph.DeleteNodes(ctx, tid.driver, _spec) 88 | } 89 | 90 | // TodoItemDeleteOne is the builder for deleting a single TodoItem entity. 91 | type TodoItemDeleteOne struct { 92 | tid *TodoItemDelete 93 | } 94 | 95 | // Exec executes the deletion query. 96 | func (tido *TodoItemDeleteOne) Exec(ctx context.Context) error { 97 | n, err := tido.tid.Exec(ctx) 98 | switch { 99 | case err != nil: 100 | return err 101 | case n == 0: 102 | return &NotFoundError{todoitem.Label} 103 | default: 104 | return nil 105 | } 106 | } 107 | 108 | // ExecX is like Exec, but panics if an error occurs. 109 | func (tido *TodoItemDeleteOne) ExecX(ctx context.Context) { 110 | tido.tid.ExecX(ctx) 111 | } 112 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/todoitem_update.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "time" 9 | 10 | "entgo.io/ent/dialect/sql" 11 | "entgo.io/ent/dialect/sql/sqlgraph" 12 | "entgo.io/ent/schema/field" 13 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/predicate" 14 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 15 | ) 16 | 17 | // TodoItemUpdate is the builder for updating TodoItem entities. 18 | type TodoItemUpdate struct { 19 | config 20 | hooks []Hook 21 | mutation *TodoItemMutation 22 | } 23 | 24 | // Where appends a list predicates to the TodoItemUpdate builder. 25 | func (tiu *TodoItemUpdate) Where(ps ...predicate.TodoItem) *TodoItemUpdate { 26 | tiu.mutation.Where(ps...) 27 | return tiu 28 | } 29 | 30 | // SetTitle sets the "title" field. 31 | func (tiu *TodoItemUpdate) SetTitle(s string) *TodoItemUpdate { 32 | tiu.mutation.SetTitle(s) 33 | return tiu 34 | } 35 | 36 | // SetCompleted sets the "completed" field. 37 | func (tiu *TodoItemUpdate) SetCompleted(b bool) *TodoItemUpdate { 38 | tiu.mutation.SetCompleted(b) 39 | return tiu 40 | } 41 | 42 | // SetOrder sets the "order" field. 43 | func (tiu *TodoItemUpdate) SetOrder(i int) *TodoItemUpdate { 44 | tiu.mutation.ResetOrder() 45 | tiu.mutation.SetOrder(i) 46 | return tiu 47 | } 48 | 49 | // AddOrder adds i to the "order" field. 50 | func (tiu *TodoItemUpdate) AddOrder(i int) *TodoItemUpdate { 51 | tiu.mutation.AddOrder(i) 52 | return tiu 53 | } 54 | 55 | // SetCreatedAt sets the "created_at" field. 56 | func (tiu *TodoItemUpdate) SetCreatedAt(t time.Time) *TodoItemUpdate { 57 | tiu.mutation.SetCreatedAt(t) 58 | return tiu 59 | } 60 | 61 | // SetNillableCreatedAt sets the "created_at" field if the given value is not nil. 62 | func (tiu *TodoItemUpdate) SetNillableCreatedAt(t *time.Time) *TodoItemUpdate { 63 | if t != nil { 64 | tiu.SetCreatedAt(*t) 65 | } 66 | return tiu 67 | } 68 | 69 | // SetUpdatedAt sets the "updated_at" field. 70 | func (tiu *TodoItemUpdate) SetUpdatedAt(t time.Time) *TodoItemUpdate { 71 | tiu.mutation.SetUpdatedAt(t) 72 | return tiu 73 | } 74 | 75 | // Mutation returns the TodoItemMutation object of the builder. 76 | func (tiu *TodoItemUpdate) Mutation() *TodoItemMutation { 77 | return tiu.mutation 78 | } 79 | 80 | // Save executes the query and returns the number of nodes affected by the update operation. 81 | func (tiu *TodoItemUpdate) Save(ctx context.Context) (int, error) { 82 | var ( 83 | err error 84 | affected int 85 | ) 86 | tiu.defaults() 87 | if len(tiu.hooks) == 0 { 88 | affected, err = tiu.sqlSave(ctx) 89 | } else { 90 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { 91 | mutation, ok := m.(*TodoItemMutation) 92 | if !ok { 93 | return nil, fmt.Errorf("unexpected mutation type %T", m) 94 | } 95 | tiu.mutation = mutation 96 | affected, err = tiu.sqlSave(ctx) 97 | mutation.done = true 98 | return affected, err 99 | }) 100 | for i := len(tiu.hooks) - 1; i >= 0; i-- { 101 | if tiu.hooks[i] == nil { 102 | return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") 103 | } 104 | mut = tiu.hooks[i](mut) 105 | } 106 | if _, err := mut.Mutate(ctx, tiu.mutation); err != nil { 107 | return 0, err 108 | } 109 | } 110 | return affected, err 111 | } 112 | 113 | // SaveX is like Save, but panics if an error occurs. 114 | func (tiu *TodoItemUpdate) SaveX(ctx context.Context) int { 115 | affected, err := tiu.Save(ctx) 116 | if err != nil { 117 | panic(err) 118 | } 119 | return affected 120 | } 121 | 122 | // Exec executes the query. 123 | func (tiu *TodoItemUpdate) Exec(ctx context.Context) error { 124 | _, err := tiu.Save(ctx) 125 | return err 126 | } 127 | 128 | // ExecX is like Exec, but panics if an error occurs. 129 | func (tiu *TodoItemUpdate) ExecX(ctx context.Context) { 130 | if err := tiu.Exec(ctx); err != nil { 131 | panic(err) 132 | } 133 | } 134 | 135 | // defaults sets the default values of the builder before save. 136 | func (tiu *TodoItemUpdate) defaults() { 137 | if _, ok := tiu.mutation.UpdatedAt(); !ok { 138 | v := todoitem.UpdateDefaultUpdatedAt() 139 | tiu.mutation.SetUpdatedAt(v) 140 | } 141 | } 142 | 143 | func (tiu *TodoItemUpdate) sqlSave(ctx context.Context) (n int, err error) { 144 | _spec := &sqlgraph.UpdateSpec{ 145 | Node: &sqlgraph.NodeSpec{ 146 | Table: todoitem.Table, 147 | Columns: todoitem.Columns, 148 | ID: &sqlgraph.FieldSpec{ 149 | Type: field.TypeInt, 150 | Column: todoitem.FieldID, 151 | }, 152 | }, 153 | } 154 | if ps := tiu.mutation.predicates; len(ps) > 0 { 155 | _spec.Predicate = func(selector *sql.Selector) { 156 | for i := range ps { 157 | ps[i](selector) 158 | } 159 | } 160 | } 161 | if value, ok := tiu.mutation.Title(); ok { 162 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 163 | Type: field.TypeString, 164 | Value: value, 165 | Column: todoitem.FieldTitle, 166 | }) 167 | } 168 | if value, ok := tiu.mutation.Completed(); ok { 169 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 170 | Type: field.TypeBool, 171 | Value: value, 172 | Column: todoitem.FieldCompleted, 173 | }) 174 | } 175 | if value, ok := tiu.mutation.Order(); ok { 176 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 177 | Type: field.TypeInt, 178 | Value: value, 179 | Column: todoitem.FieldOrder, 180 | }) 181 | } 182 | if value, ok := tiu.mutation.AddedOrder(); ok { 183 | _spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{ 184 | Type: field.TypeInt, 185 | Value: value, 186 | Column: todoitem.FieldOrder, 187 | }) 188 | } 189 | if value, ok := tiu.mutation.CreatedAt(); ok { 190 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 191 | Type: field.TypeTime, 192 | Value: value, 193 | Column: todoitem.FieldCreatedAt, 194 | }) 195 | } 196 | if value, ok := tiu.mutation.UpdatedAt(); ok { 197 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 198 | Type: field.TypeTime, 199 | Value: value, 200 | Column: todoitem.FieldUpdatedAt, 201 | }) 202 | } 203 | if n, err = sqlgraph.UpdateNodes(ctx, tiu.driver, _spec); err != nil { 204 | if _, ok := err.(*sqlgraph.NotFoundError); ok { 205 | err = &NotFoundError{todoitem.Label} 206 | } else if sqlgraph.IsConstraintError(err) { 207 | err = &ConstraintError{err.Error(), err} 208 | } 209 | return 0, err 210 | } 211 | return n, nil 212 | } 213 | 214 | // TodoItemUpdateOne is the builder for updating a single TodoItem entity. 215 | type TodoItemUpdateOne struct { 216 | config 217 | fields []string 218 | hooks []Hook 219 | mutation *TodoItemMutation 220 | } 221 | 222 | // SetTitle sets the "title" field. 223 | func (tiuo *TodoItemUpdateOne) SetTitle(s string) *TodoItemUpdateOne { 224 | tiuo.mutation.SetTitle(s) 225 | return tiuo 226 | } 227 | 228 | // SetCompleted sets the "completed" field. 229 | func (tiuo *TodoItemUpdateOne) SetCompleted(b bool) *TodoItemUpdateOne { 230 | tiuo.mutation.SetCompleted(b) 231 | return tiuo 232 | } 233 | 234 | // SetOrder sets the "order" field. 235 | func (tiuo *TodoItemUpdateOne) SetOrder(i int) *TodoItemUpdateOne { 236 | tiuo.mutation.ResetOrder() 237 | tiuo.mutation.SetOrder(i) 238 | return tiuo 239 | } 240 | 241 | // AddOrder adds i to the "order" field. 242 | func (tiuo *TodoItemUpdateOne) AddOrder(i int) *TodoItemUpdateOne { 243 | tiuo.mutation.AddOrder(i) 244 | return tiuo 245 | } 246 | 247 | // SetCreatedAt sets the "created_at" field. 248 | func (tiuo *TodoItemUpdateOne) SetCreatedAt(t time.Time) *TodoItemUpdateOne { 249 | tiuo.mutation.SetCreatedAt(t) 250 | return tiuo 251 | } 252 | 253 | // SetNillableCreatedAt sets the "created_at" field if the given value is not nil. 254 | func (tiuo *TodoItemUpdateOne) SetNillableCreatedAt(t *time.Time) *TodoItemUpdateOne { 255 | if t != nil { 256 | tiuo.SetCreatedAt(*t) 257 | } 258 | return tiuo 259 | } 260 | 261 | // SetUpdatedAt sets the "updated_at" field. 262 | func (tiuo *TodoItemUpdateOne) SetUpdatedAt(t time.Time) *TodoItemUpdateOne { 263 | tiuo.mutation.SetUpdatedAt(t) 264 | return tiuo 265 | } 266 | 267 | // Mutation returns the TodoItemMutation object of the builder. 268 | func (tiuo *TodoItemUpdateOne) Mutation() *TodoItemMutation { 269 | return tiuo.mutation 270 | } 271 | 272 | // Select allows selecting one or more fields (columns) of the returned entity. 273 | // The default is selecting all fields defined in the entity schema. 274 | func (tiuo *TodoItemUpdateOne) Select(field string, fields ...string) *TodoItemUpdateOne { 275 | tiuo.fields = append([]string{field}, fields...) 276 | return tiuo 277 | } 278 | 279 | // Save executes the query and returns the updated TodoItem entity. 280 | func (tiuo *TodoItemUpdateOne) Save(ctx context.Context) (*TodoItem, error) { 281 | var ( 282 | err error 283 | node *TodoItem 284 | ) 285 | tiuo.defaults() 286 | if len(tiuo.hooks) == 0 { 287 | node, err = tiuo.sqlSave(ctx) 288 | } else { 289 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { 290 | mutation, ok := m.(*TodoItemMutation) 291 | if !ok { 292 | return nil, fmt.Errorf("unexpected mutation type %T", m) 293 | } 294 | tiuo.mutation = mutation 295 | node, err = tiuo.sqlSave(ctx) 296 | mutation.done = true 297 | return node, err 298 | }) 299 | for i := len(tiuo.hooks) - 1; i >= 0; i-- { 300 | if tiuo.hooks[i] == nil { 301 | return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") 302 | } 303 | mut = tiuo.hooks[i](mut) 304 | } 305 | if _, err := mut.Mutate(ctx, tiuo.mutation); err != nil { 306 | return nil, err 307 | } 308 | } 309 | return node, err 310 | } 311 | 312 | // SaveX is like Save, but panics if an error occurs. 313 | func (tiuo *TodoItemUpdateOne) SaveX(ctx context.Context) *TodoItem { 314 | node, err := tiuo.Save(ctx) 315 | if err != nil { 316 | panic(err) 317 | } 318 | return node 319 | } 320 | 321 | // Exec executes the query on the entity. 322 | func (tiuo *TodoItemUpdateOne) Exec(ctx context.Context) error { 323 | _, err := tiuo.Save(ctx) 324 | return err 325 | } 326 | 327 | // ExecX is like Exec, but panics if an error occurs. 328 | func (tiuo *TodoItemUpdateOne) ExecX(ctx context.Context) { 329 | if err := tiuo.Exec(ctx); err != nil { 330 | panic(err) 331 | } 332 | } 333 | 334 | // defaults sets the default values of the builder before save. 335 | func (tiuo *TodoItemUpdateOne) defaults() { 336 | if _, ok := tiuo.mutation.UpdatedAt(); !ok { 337 | v := todoitem.UpdateDefaultUpdatedAt() 338 | tiuo.mutation.SetUpdatedAt(v) 339 | } 340 | } 341 | 342 | func (tiuo *TodoItemUpdateOne) sqlSave(ctx context.Context) (_node *TodoItem, err error) { 343 | _spec := &sqlgraph.UpdateSpec{ 344 | Node: &sqlgraph.NodeSpec{ 345 | Table: todoitem.Table, 346 | Columns: todoitem.Columns, 347 | ID: &sqlgraph.FieldSpec{ 348 | Type: field.TypeInt, 349 | Column: todoitem.FieldID, 350 | }, 351 | }, 352 | } 353 | id, ok := tiuo.mutation.ID() 354 | if !ok { 355 | return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing TodoItem.ID for update")} 356 | } 357 | _spec.Node.ID.Value = id 358 | if fields := tiuo.fields; len(fields) > 0 { 359 | _spec.Node.Columns = make([]string, 0, len(fields)) 360 | _spec.Node.Columns = append(_spec.Node.Columns, todoitem.FieldID) 361 | for _, f := range fields { 362 | if !todoitem.ValidColumn(f) { 363 | return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} 364 | } 365 | if f != todoitem.FieldID { 366 | _spec.Node.Columns = append(_spec.Node.Columns, f) 367 | } 368 | } 369 | } 370 | if ps := tiuo.mutation.predicates; len(ps) > 0 { 371 | _spec.Predicate = func(selector *sql.Selector) { 372 | for i := range ps { 373 | ps[i](selector) 374 | } 375 | } 376 | } 377 | if value, ok := tiuo.mutation.Title(); ok { 378 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 379 | Type: field.TypeString, 380 | Value: value, 381 | Column: todoitem.FieldTitle, 382 | }) 383 | } 384 | if value, ok := tiuo.mutation.Completed(); ok { 385 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 386 | Type: field.TypeBool, 387 | Value: value, 388 | Column: todoitem.FieldCompleted, 389 | }) 390 | } 391 | if value, ok := tiuo.mutation.Order(); ok { 392 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 393 | Type: field.TypeInt, 394 | Value: value, 395 | Column: todoitem.FieldOrder, 396 | }) 397 | } 398 | if value, ok := tiuo.mutation.AddedOrder(); ok { 399 | _spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{ 400 | Type: field.TypeInt, 401 | Value: value, 402 | Column: todoitem.FieldOrder, 403 | }) 404 | } 405 | if value, ok := tiuo.mutation.CreatedAt(); ok { 406 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 407 | Type: field.TypeTime, 408 | Value: value, 409 | Column: todoitem.FieldCreatedAt, 410 | }) 411 | } 412 | if value, ok := tiuo.mutation.UpdatedAt(); ok { 413 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ 414 | Type: field.TypeTime, 415 | Value: value, 416 | Column: todoitem.FieldUpdatedAt, 417 | }) 418 | } 419 | _node = &TodoItem{config: tiuo.config} 420 | _spec.Assign = _node.assignValues 421 | _spec.ScanValues = _node.scanValues 422 | if err = sqlgraph.UpdateNode(ctx, tiuo.driver, _spec); err != nil { 423 | if _, ok := err.(*sqlgraph.NotFoundError); ok { 424 | err = &NotFoundError{todoitem.Label} 425 | } else if sqlgraph.IsConstraintError(err) { 426 | err = &ConstraintError{err.Error(), err} 427 | } 428 | return nil, err 429 | } 430 | return _node, nil 431 | } 432 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/ent/tx.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | "sync" 8 | 9 | "entgo.io/ent/dialect" 10 | ) 11 | 12 | // Tx is a transactional client that is created by calling Client.Tx(). 13 | type Tx struct { 14 | config 15 | // TodoItem is the client for interacting with the TodoItem builders. 16 | TodoItem *TodoItemClient 17 | 18 | // lazily loaded. 19 | client *Client 20 | clientOnce sync.Once 21 | 22 | // completion callbacks. 23 | mu sync.Mutex 24 | onCommit []CommitHook 25 | onRollback []RollbackHook 26 | 27 | // ctx lives for the life of the transaction. It is 28 | // the same context used by the underlying connection. 29 | ctx context.Context 30 | } 31 | 32 | type ( 33 | // Committer is the interface that wraps the Committer method. 34 | Committer interface { 35 | Commit(context.Context, *Tx) error 36 | } 37 | 38 | // The CommitFunc type is an adapter to allow the use of ordinary 39 | // function as a Committer. If f is a function with the appropriate 40 | // signature, CommitFunc(f) is a Committer that calls f. 41 | CommitFunc func(context.Context, *Tx) error 42 | 43 | // CommitHook defines the "commit middleware". A function that gets a Committer 44 | // and returns a Committer. For example: 45 | // 46 | // hook := func(next ent.Committer) ent.Committer { 47 | // return ent.CommitFunc(func(context.Context, tx *ent.Tx) error { 48 | // // Do some stuff before. 49 | // if err := next.Commit(ctx, tx); err != nil { 50 | // return err 51 | // } 52 | // // Do some stuff after. 53 | // return nil 54 | // }) 55 | // } 56 | // 57 | CommitHook func(Committer) Committer 58 | ) 59 | 60 | // Commit calls f(ctx, m). 61 | func (f CommitFunc) Commit(ctx context.Context, tx *Tx) error { 62 | return f(ctx, tx) 63 | } 64 | 65 | // Commit commits the transaction. 66 | func (tx *Tx) Commit() error { 67 | txDriver := tx.config.driver.(*txDriver) 68 | var fn Committer = CommitFunc(func(context.Context, *Tx) error { 69 | return txDriver.tx.Commit() 70 | }) 71 | tx.mu.Lock() 72 | hooks := append([]CommitHook(nil), tx.onCommit...) 73 | tx.mu.Unlock() 74 | for i := len(hooks) - 1; i >= 0; i-- { 75 | fn = hooks[i](fn) 76 | } 77 | return fn.Commit(tx.ctx, tx) 78 | } 79 | 80 | // OnCommit adds a hook to call on commit. 81 | func (tx *Tx) OnCommit(f CommitHook) { 82 | tx.mu.Lock() 83 | defer tx.mu.Unlock() 84 | tx.onCommit = append(tx.onCommit, f) 85 | } 86 | 87 | type ( 88 | // Rollbacker is the interface that wraps the Rollbacker method. 89 | Rollbacker interface { 90 | Rollback(context.Context, *Tx) error 91 | } 92 | 93 | // The RollbackFunc type is an adapter to allow the use of ordinary 94 | // function as a Rollbacker. If f is a function with the appropriate 95 | // signature, RollbackFunc(f) is a Rollbacker that calls f. 96 | RollbackFunc func(context.Context, *Tx) error 97 | 98 | // RollbackHook defines the "rollback middleware". A function that gets a Rollbacker 99 | // and returns a Rollbacker. For example: 100 | // 101 | // hook := func(next ent.Rollbacker) ent.Rollbacker { 102 | // return ent.RollbackFunc(func(context.Context, tx *ent.Tx) error { 103 | // // Do some stuff before. 104 | // if err := next.Rollback(ctx, tx); err != nil { 105 | // return err 106 | // } 107 | // // Do some stuff after. 108 | // return nil 109 | // }) 110 | // } 111 | // 112 | RollbackHook func(Rollbacker) Rollbacker 113 | ) 114 | 115 | // Rollback calls f(ctx, m). 116 | func (f RollbackFunc) Rollback(ctx context.Context, tx *Tx) error { 117 | return f(ctx, tx) 118 | } 119 | 120 | // Rollback rollbacks the transaction. 121 | func (tx *Tx) Rollback() error { 122 | txDriver := tx.config.driver.(*txDriver) 123 | var fn Rollbacker = RollbackFunc(func(context.Context, *Tx) error { 124 | return txDriver.tx.Rollback() 125 | }) 126 | tx.mu.Lock() 127 | hooks := append([]RollbackHook(nil), tx.onRollback...) 128 | tx.mu.Unlock() 129 | for i := len(hooks) - 1; i >= 0; i-- { 130 | fn = hooks[i](fn) 131 | } 132 | return fn.Rollback(tx.ctx, tx) 133 | } 134 | 135 | // OnRollback adds a hook to call on rollback. 136 | func (tx *Tx) OnRollback(f RollbackHook) { 137 | tx.mu.Lock() 138 | defer tx.mu.Unlock() 139 | tx.onRollback = append(tx.onRollback, f) 140 | } 141 | 142 | // Client returns a Client that binds to current transaction. 143 | func (tx *Tx) Client() *Client { 144 | tx.clientOnce.Do(func() { 145 | tx.client = &Client{config: tx.config} 146 | tx.client.init() 147 | }) 148 | return tx.client 149 | } 150 | 151 | func (tx *Tx) init() { 152 | tx.TodoItem = NewTodoItemClient(tx.config) 153 | } 154 | 155 | // txDriver wraps the given dialect.Tx with a nop dialect.Driver implementation. 156 | // The idea is to support transactions without adding any extra code to the builders. 157 | // When a builder calls to driver.Tx(), it gets the same dialect.Tx instance. 158 | // Commit and Rollback are nop for the internal builders and the user must call one 159 | // of them in order to commit or rollback the transaction. 160 | // 161 | // If a closed transaction is embedded in one of the generated entities, and the entity 162 | // applies a query, for example: TodoItem.QueryXXX(), the query will be executed 163 | // through the driver which created this transaction. 164 | // 165 | // Note that txDriver is not goroutine safe. 166 | type txDriver struct { 167 | // the driver we started the transaction from. 168 | drv dialect.Driver 169 | // tx is the underlying transaction. 170 | tx dialect.Tx 171 | } 172 | 173 | // newTx creates a new transactional driver. 174 | func newTx(ctx context.Context, drv dialect.Driver) (*txDriver, error) { 175 | tx, err := drv.Tx(ctx) 176 | if err != nil { 177 | return nil, err 178 | } 179 | return &txDriver{tx: tx, drv: drv}, nil 180 | } 181 | 182 | // Tx returns the transaction wrapper (txDriver) to avoid Commit or Rollback calls 183 | // from the internal builders. Should be called only by the internal builders. 184 | func (tx *txDriver) Tx(context.Context) (dialect.Tx, error) { return tx, nil } 185 | 186 | // Dialect returns the dialect of the driver we started the transaction from. 187 | func (tx *txDriver) Dialect() string { return tx.drv.Dialect() } 188 | 189 | // Close is a nop close. 190 | func (*txDriver) Close() error { return nil } 191 | 192 | // Commit is a nop commit for the internal builders. 193 | // User must call `Tx.Commit` in order to commit the transaction. 194 | func (*txDriver) Commit() error { return nil } 195 | 196 | // Rollback is a nop rollback for the internal builders. 197 | // User must call `Tx.Rollback` in order to rollback the transaction. 198 | func (*txDriver) Rollback() error { return nil } 199 | 200 | // Exec calls tx.Exec. 201 | func (tx *txDriver) Exec(ctx context.Context, query string, args, v interface{}) error { 202 | return tx.tx.Exec(ctx, query, args, v) 203 | } 204 | 205 | // Query calls tx.Query. 206 | func (tx *txDriver) Query(ctx context.Context, query string, args, v interface{}) error { 207 | return tx.tx.Query(ctx, query, args, v) 208 | } 209 | 210 | var _ dialect.Driver = (*txDriver)(nil) 211 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todoadapter/store_ent.go: -------------------------------------------------------------------------------- 1 | package todoadapter 2 | 3 | import ( 4 | "context" 5 | 6 | "emperror.dev/errors" 7 | 8 | "github.com/sagikazarmark/todobackend-go-kit/todo" 9 | 10 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent" 11 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo/todoadapter/ent/todoitem" 12 | ) 13 | 14 | type entStore struct { 15 | client *ent.Client 16 | } 17 | 18 | // NewEntStore returns a new todo store backed by Ent ORM. 19 | func NewEntStore(client *ent.Client) todo.Store { 20 | return entStore{ 21 | client: client, 22 | } 23 | } 24 | 25 | func (s entStore) Store(ctx context.Context, todo todo.Item) error { 26 | existing, err := s.client.TodoItem.Query().Where(todoitem.UID(todo.ID)).First(ctx) 27 | if ent.IsNotFound(err) { 28 | _, err := s.client.TodoItem.Create(). 29 | SetUID(todo.ID). 30 | SetTitle(todo.Title). 31 | SetCompleted(todo.Completed). 32 | SetOrder(todo.Order). 33 | Save(ctx) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | if err != nil { 41 | return err 42 | } 43 | 44 | _, err = s.client.TodoItem.UpdateOneID(existing.ID). 45 | SetTitle(todo.Title). 46 | SetCompleted(todo.Completed). 47 | SetOrder(todo.Order). 48 | Save(ctx) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (s entStore) GetAll(ctx context.Context) ([]todo.Item, error) { 57 | todoModels, err := s.client.TodoItem.Query().All(ctx) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | todos := make([]todo.Item, 0, len(todoModels)) 63 | 64 | for _, todoModel := range todoModels { 65 | todos = append(todos, todo.Item{ 66 | ID: todoModel.UID, 67 | Title: todoModel.Title, 68 | Completed: todoModel.Completed, 69 | Order: todoModel.Order, 70 | }) 71 | } 72 | 73 | return todos, nil 74 | } 75 | 76 | func (s entStore) GetOne(ctx context.Context, id string) (todo.Item, error) { 77 | todoModel, err := s.client.TodoItem.Query().Where(todoitem.UID(id)).First(ctx) 78 | if ent.IsNotFound(err) { 79 | return todo.Item{}, errors.WithStack(todo.NotFoundError{ID: id}) 80 | } 81 | 82 | return todo.Item{ 83 | ID: todoModel.UID, 84 | Title: todoModel.Title, 85 | Completed: todoModel.Completed, 86 | Order: todoModel.Order, 87 | }, nil 88 | } 89 | 90 | func (s entStore) DeleteAll(ctx context.Context) error { 91 | _, err := s.client.TodoItem.Delete().Exec(ctx) 92 | 93 | if err != nil { 94 | return errors.WithStack(err) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (s entStore) DeleteOne(ctx context.Context, id string) error { 101 | _, err := s.client.TodoItem.Delete().Where(todoitem.UID(id)).Exec(ctx) 102 | 103 | if err != nil { 104 | return errors.WithStack(err) 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/app/mga/todo/tododriver/middleware.go: -------------------------------------------------------------------------------- 1 | package tododriver 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sagikazarmark/todobackend-go-kit/todo" 7 | "go.opencensus.io/stats" 8 | "go.opencensus.io/stats/view" 9 | "go.opencensus.io/trace" 10 | 11 | todo2 "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo" 12 | ) 13 | 14 | // LoggingMiddleware is a service level logging middleware. 15 | func LoggingMiddleware(logger todo2.Logger) todo2.Middleware { 16 | return func(next todo.Service) todo.Service { 17 | return loggingMiddleware{ 18 | next: next, 19 | logger: logger, 20 | } 21 | } 22 | } 23 | 24 | type loggingMiddleware struct { 25 | next todo.Service 26 | logger todo2.Logger 27 | } 28 | 29 | func (mw loggingMiddleware) AddItem(ctx context.Context, newItem todo.NewItem) (todo.Item, error) { 30 | logger := mw.logger.WithContext(ctx) 31 | 32 | logger.Info("adding item") 33 | 34 | id, err := mw.next.AddItem(ctx, newItem) 35 | if err != nil { 36 | return id, err 37 | } 38 | 39 | logger.Info("added item", map[string]interface{}{"item_id": id}) 40 | 41 | return id, err 42 | } 43 | 44 | func (mw loggingMiddleware) ListItems(ctx context.Context) ([]todo.Item, error) { 45 | logger := mw.logger.WithContext(ctx) 46 | 47 | logger.Info("listing item") 48 | 49 | return mw.next.ListItems(ctx) 50 | } 51 | 52 | func (mw loggingMiddleware) DeleteItems(ctx context.Context) error { 53 | logger := mw.logger.WithContext(ctx) 54 | 55 | logger.Info("deleting all items") 56 | 57 | return mw.next.DeleteItems(ctx) 58 | } 59 | 60 | func (mw loggingMiddleware) GetItem(ctx context.Context, id string) (todo.Item, error) { 61 | logger := mw.logger.WithContext(ctx) 62 | 63 | logger.Info("getting item details", map[string]interface{}{"item_id": id}) 64 | 65 | return mw.next.GetItem(ctx, id) 66 | } 67 | 68 | func (mw loggingMiddleware) UpdateItem(ctx context.Context, id string, itemUpdate todo.ItemUpdate) (todo.Item, error) { // nolint: lll 69 | logger := mw.logger.WithContext(ctx) 70 | 71 | logger.Info("updating item", map[string]interface{}{"item_id": id}) 72 | 73 | return mw.next.UpdateItem(ctx, id, itemUpdate) 74 | } 75 | 76 | func (mw loggingMiddleware) DeleteItem(ctx context.Context, id string) error { 77 | logger := mw.logger.WithContext(ctx) 78 | 79 | logger.Info("deleting item", map[string]interface{}{"item_id": id}) 80 | 81 | return mw.next.DeleteItem(ctx, id) 82 | } 83 | 84 | // Business metrics 85 | // nolint: gochecknoglobals,lll 86 | var ( 87 | CreatedTodoItemCount = stats.Int64("created_todo_item_count", "Number of todo items created", stats.UnitDimensionless) 88 | CompleteTodoItemCount = stats.Int64("complete_todo_item_count", "Number of todo items marked complete", stats.UnitDimensionless) 89 | ) 90 | 91 | // nolint: gochecknoglobals 92 | var ( 93 | CreatedTodoItemCountView = &view.View{ 94 | Name: "todo_item_created_count", 95 | Description: "Count of todo items created", 96 | Measure: CreatedTodoItemCount, 97 | Aggregation: view.Count(), 98 | } 99 | 100 | CompleteTodoItemCountView = &view.View{ 101 | Name: "todo_item_complete_count", 102 | Description: "Count of todo items complete", 103 | Measure: CompleteTodoItemCount, 104 | Aggregation: view.Count(), 105 | } 106 | ) 107 | 108 | // InstrumentationMiddleware is a service level instrumentation middleware. 109 | func InstrumentationMiddleware() todo2.Middleware { 110 | return func(next todo.Service) todo.Service { 111 | return instrumentationMiddleware{ 112 | Service: todo2.DefaultMiddleware{Service: next}, 113 | next: next, 114 | } 115 | } 116 | } 117 | 118 | type instrumentationMiddleware struct { 119 | todo.Service 120 | next todo.Service 121 | } 122 | 123 | func (mw instrumentationMiddleware) AddItem(ctx context.Context, newItem todo.NewItem) (todo.Item, error) { 124 | item, err := mw.next.AddItem(ctx, newItem) 125 | if err != nil { 126 | return item, err 127 | } 128 | 129 | if span := trace.FromContext(ctx); span != nil { 130 | span.AddAttributes(trace.StringAttribute("item_id", item.ID)) 131 | } 132 | 133 | stats.Record(ctx, CreatedTodoItemCount.M(1)) 134 | 135 | return item, nil 136 | } 137 | 138 | func (mw instrumentationMiddleware) UpdateItem(ctx context.Context, id string, itemUpdate todo.ItemUpdate) (todo.Item, error) { // nolint: lll 139 | if span := trace.FromContext(ctx); span != nil { 140 | span.AddAttributes(trace.StringAttribute("item_id", id)) 141 | } 142 | 143 | if itemUpdate.Completed != nil && *itemUpdate.Completed { 144 | stats.Record(ctx, CompleteTodoItemCount.M(1)) 145 | } 146 | 147 | return mw.next.UpdateItem(ctx, id, itemUpdate) 148 | } 149 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todogen/zz_generated.event_dispatcher.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by mga tool. DO NOT EDIT. 4 | 5 | package todogen 6 | 7 | import ( 8 | "context" 9 | "emperror.dev/errors" 10 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo" 11 | ) 12 | 13 | // EventBus is a generic event bus. 14 | type EventBus interface { 15 | // Publish sends an event to the underlying message bus. 16 | Publish(ctx context.Context, event interface{}) error 17 | } 18 | 19 | // EventDispatcher dispatches events through the underlying generic event bus. 20 | type EventDispatcher struct { 21 | bus EventBus 22 | } 23 | 24 | // NewEventDispatcher returns a new EventDispatcher instance. 25 | func NewEventDispatcher(bus EventBus) EventDispatcher { 26 | return EventDispatcher{bus: bus} 27 | } 28 | 29 | // MarkedAsComplete dispatches a(n) MarkedAsComplete event. 30 | func (d EventDispatcher) MarkedAsComplete(ctx context.Context, event todo.MarkedAsComplete) error { 31 | err := d.bus.Publish(ctx, event) 32 | if err != nil { 33 | return errors.WithDetails(errors.WithMessage(err, "failed to dispatch event"), "event", "MarkedAsComplete") 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/mga/todo/todogen/zz_generated.event_handler.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by mga tool. DO NOT EDIT. 4 | 5 | package todogen 6 | 7 | import ( 8 | "context" 9 | "emperror.dev/errors" 10 | "fmt" 11 | "github.com/sagikazarmark/modern-go-application/internal/app/mga/todo" 12 | ) 13 | 14 | // MarkedAsCompleteHandler handles MarkedAsComplete events. 15 | type MarkedAsCompleteHandler interface { 16 | // MarkedAsComplete handles a(n) MarkedAsComplete event. 17 | MarkedAsComplete(ctx context.Context, event todo.MarkedAsComplete) error 18 | } 19 | 20 | // MarkedAsCompleteEventHandler handles MarkedAsComplete events. 21 | type MarkedAsCompleteEventHandler struct { 22 | handler MarkedAsCompleteHandler 23 | name string 24 | } 25 | 26 | // NewMarkedAsCompleteEventHandler returns a new MarkedAsCompleteEventHandler instance. 27 | func NewMarkedAsCompleteEventHandler(handler MarkedAsCompleteHandler, name string) MarkedAsCompleteEventHandler { 28 | return MarkedAsCompleteEventHandler{ 29 | handler: handler, 30 | name: name, 31 | } 32 | } 33 | 34 | // HandlerName returns the name of the event handler. 35 | func (h MarkedAsCompleteEventHandler) HandlerName() string { 36 | return h.name 37 | } 38 | 39 | // NewEvent returns a new empty event used for serialization. 40 | func (h MarkedAsCompleteEventHandler) NewEvent() interface{} { 41 | return &todo.MarkedAsComplete{} 42 | } 43 | 44 | // Handle handles an event. 45 | func (h MarkedAsCompleteEventHandler) Handle(ctx context.Context, event interface{}) error { 46 | e, ok := event.(*todo.MarkedAsComplete) 47 | if !ok { 48 | return errors.NewWithDetails("unexpected event type", "type", fmt.Sprintf("%T", event)) 49 | } 50 | 51 | return h.handler.MarkedAsComplete(ctx, *e) 52 | } 53 | -------------------------------------------------------------------------------- /internal/app/todocli/command/add.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/spf13/cobra" 9 | "google.golang.org/genproto/googleapis/rpc/errdetails" 10 | "google.golang.org/grpc/status" 11 | 12 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 13 | ) 14 | 15 | type createOptions struct { 16 | title string 17 | client todov1.TodoListServiceClient 18 | } 19 | 20 | // NewAddCommand creates a new cobra.Command for adding a new item to the list. 21 | func NewAddCommand(c Context) *cobra.Command { 22 | options := createOptions{} 23 | 24 | cmd := &cobra.Command{ 25 | Use: "add", 26 | Aliases: []string{"a"}, 27 | Short: "Add an item to the list", 28 | Args: cobra.ExactArgs(1), 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | options.title = args[0] 31 | options.client = c.GetTodoClient() 32 | 33 | cmd.SilenceErrors = true 34 | cmd.SilenceUsage = true 35 | 36 | return runCreate(options) 37 | }, 38 | } 39 | 40 | return cmd 41 | } 42 | 43 | func runCreate(options createOptions) error { 44 | req := &todov1.AddItemRequest{ 45 | Title: options.title, 46 | } 47 | 48 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 49 | defer cancel() 50 | 51 | resp, err := options.client.AddItem(ctx, req) 52 | if err != nil { 53 | st := status.Convert(err) 54 | for _, detail := range st.Details() { 55 | // nolint: gocritic 56 | switch t := detail.(type) { 57 | case *errdetails.BadRequest: 58 | fmt.Println("Oops! Your request was rejected by the server.") 59 | for _, violation := range t.GetFieldViolations() { 60 | fmt.Printf("The %q field was wrong:\n", violation.GetField()) 61 | fmt.Printf("\t%s\n", violation.GetDescription()) 62 | } 63 | } 64 | } 65 | 66 | return err 67 | } 68 | 69 | fmt.Printf("Todo item %q with ID %s has been created.", options.title, resp.GetItem().GetId()) 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/app/todocli/command/cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 7 | ) 8 | 9 | // Context represents the application context. 10 | type Context interface { 11 | GetTodoClient() todov1.TodoListServiceClient 12 | } 13 | 14 | // AddCommands adds all the commands from cli/command to the root command. 15 | func AddCommands(cmd *cobra.Command, c Context) { 16 | cmd.AddCommand( 17 | NewAddCommand(c), 18 | NewListCommand(c), 19 | NewMarkAsCompleteCommand(c), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /internal/app/todocli/command/complete.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang/protobuf/ptypes/wrappers" 9 | "github.com/spf13/cobra" 10 | 11 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 12 | ) 13 | 14 | type markAsCompleteOptions struct { 15 | todoID string 16 | client todov1.TodoListServiceClient 17 | } 18 | 19 | // NewMarkAsCompleteCommand creates a new cobra.Command for marking a todo item as complete. 20 | func NewMarkAsCompleteCommand(c Context) *cobra.Command { 21 | options := markAsCompleteOptions{} 22 | 23 | cmd := &cobra.Command{ 24 | Use: "complete", 25 | Aliases: []string{"c"}, 26 | Short: "Mark a todo item as complete", 27 | Args: cobra.ExactArgs(1), 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | options.todoID = args[0] 30 | options.client = c.GetTodoClient() 31 | 32 | cmd.SilenceErrors = true 33 | cmd.SilenceUsage = true 34 | 35 | return runMarkAsComplete(options) 36 | }, 37 | } 38 | 39 | return cmd 40 | } 41 | 42 | func runMarkAsComplete(options markAsCompleteOptions) error { 43 | req := &todov1.UpdateItemRequest{ 44 | Id: options.todoID, 45 | Completed: &wrappers.BoolValue{ 46 | Value: true, 47 | }, 48 | } 49 | 50 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 51 | defer cancel() 52 | 53 | _, err := options.client.UpdateItem(ctx, req) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | fmt.Printf("Todo item with ID %s has been marked as complete.", options.todoID) 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/app/todocli/command/list.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/olekukonko/tablewriter" 10 | "github.com/spf13/cobra" 11 | 12 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 13 | ) 14 | 15 | type listOptions struct { 16 | client todov1.TodoListServiceClient 17 | } 18 | 19 | // NewListCommand creates a new cobra.Command for listing todo items. 20 | func NewListCommand(c Context) *cobra.Command { 21 | options := listOptions{} 22 | 23 | cmd := &cobra.Command{ 24 | Use: "list", 25 | Aliases: []string{"l"}, 26 | Short: "List todo items", 27 | Args: cobra.NoArgs, 28 | RunE: func(cmd *cobra.Command, _ []string) error { 29 | options.client = c.GetTodoClient() 30 | 31 | cmd.SilenceErrors = true 32 | cmd.SilenceUsage = true 33 | 34 | return runList(options) 35 | }, 36 | } 37 | cobra.OnInitialize() 38 | 39 | return cmd 40 | } 41 | 42 | func runList(options listOptions) error { 43 | req := &todov1.ListItemsRequest{} 44 | 45 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 46 | defer cancel() 47 | 48 | resp, err := options.client.ListItems(ctx, req) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | table := tablewriter.NewWriter(os.Stdout) 54 | table.SetHeader([]string{"ID", "Title", "Completed"}) 55 | 56 | for _, item := range resp.GetItems() { 57 | table.Append([]string{item.GetId(), item.GetTitle(), strconv.FormatBool(item.GetCompleted())}) 58 | } 59 | table.Render() 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/app/todocli/configure.go: -------------------------------------------------------------------------------- 1 | package todocli 2 | 3 | import ( 4 | "contrib.go.opencensus.io/exporter/ocagent" 5 | "emperror.dev/errors" 6 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 7 | "github.com/spf13/cobra" 8 | "go.opencensus.io/plugin/ocgrpc" 9 | "go.opencensus.io/trace" 10 | "google.golang.org/grpc" 11 | 12 | "github.com/sagikazarmark/modern-go-application/internal/app/todocli/command" 13 | ) 14 | 15 | // Configure configures a root command. 16 | func Configure(rootCmd *cobra.Command) { 17 | var address string 18 | 19 | flags := rootCmd.PersistentFlags() 20 | 21 | flags.StringVar(&address, "address", "127.0.0.1:8001", "Todo service address") 22 | 23 | c := &context{} 24 | 25 | var grpcConn *grpc.ClientConn 26 | var ocagentExporter *ocagent.Exporter 27 | 28 | rootCmd.PersistentPreRunE = func(_ *cobra.Command, _ []string) error { 29 | conn, err := grpc.Dial( 30 | address, 31 | grpc.WithInsecure(), 32 | grpc.WithStatsHandler(&ocgrpc.ClientHandler{ 33 | StartOptions: trace.StartOptions{ 34 | Sampler: trace.AlwaysSample(), 35 | SpanKind: trace.SpanKindClient, 36 | }, 37 | }), 38 | ) 39 | if err != nil { 40 | return errors.WrapIf(err, "failed to dial service") 41 | } 42 | 43 | // Configure OpenCensus exporter 44 | exporter, err := ocagent.NewExporter(ocagent.WithServiceName("todocli"), ocagent.WithInsecure()) 45 | if err != nil { 46 | return errors.WrapIf(err, "failed to create exporter") 47 | } 48 | 49 | ocagentExporter = exporter 50 | 51 | trace.RegisterExporter(exporter) 52 | 53 | grpcConn = conn 54 | 55 | c.client = todov1.NewTodoListServiceClient(conn) 56 | 57 | return nil 58 | } 59 | 60 | rootCmd.PersistentPostRunE = func(_ *cobra.Command, _ []string) error { 61 | ocagentExporter.Flush() 62 | 63 | return grpcConn.Close() 64 | } 65 | 66 | command.AddCommands(rootCmd, c) 67 | } 68 | -------------------------------------------------------------------------------- /internal/app/todocli/context.go: -------------------------------------------------------------------------------- 1 | package todocli 2 | 3 | import ( 4 | todov1 "github.com/sagikazarmark/todobackend-go-kit/api/todo/v1" 5 | ) 6 | 7 | type context struct { 8 | client todov1.TodoListServiceClient 9 | } 10 | 11 | func (c *context) GetTodoClient() todov1.TodoListServiceClient { 12 | return c.client 13 | } 14 | -------------------------------------------------------------------------------- /internal/common/commonadapter/logger.go: -------------------------------------------------------------------------------- 1 | package commonadapter 2 | 3 | import ( 4 | "context" 5 | 6 | "logur.dev/logur" 7 | 8 | "github.com/sagikazarmark/modern-go-application/internal/common" 9 | ) 10 | 11 | // Logger wraps a logur logger and exposes it under a custom interface. 12 | type Logger struct { 13 | logur.LoggerFacade 14 | 15 | extractor ContextExtractor 16 | } 17 | 18 | // ContextExtractor extracts log fields from a context. 19 | type ContextExtractor func(ctx context.Context) map[string]interface{} 20 | 21 | // NewLogger returns a new Logger instance. 22 | func NewLogger(logger logur.LoggerFacade) *Logger { 23 | return &Logger{ 24 | LoggerFacade: logger, 25 | } 26 | } 27 | 28 | // NewContextAwareLogger returns a new Logger instance that can extract information from a context. 29 | func NewContextAwareLogger(logger logur.LoggerFacade, extractor ContextExtractor) *Logger { 30 | return &Logger{ 31 | LoggerFacade: logur.WithContextExtractor(logger, logur.ContextExtractor(extractor)), 32 | extractor: extractor, 33 | } 34 | } 35 | 36 | // WithFields annotates a logger with key-value pairs. 37 | func (l *Logger) WithFields(fields map[string]interface{}) common.Logger { 38 | return &Logger{ 39 | LoggerFacade: logur.WithFields(l.LoggerFacade, fields), 40 | extractor: l.extractor, 41 | } 42 | } 43 | 44 | // WithContext annotates a logger with a context. 45 | func (l *Logger) WithContext(ctx context.Context) common.Logger { 46 | if l.extractor == nil { 47 | return l 48 | } 49 | 50 | return l.WithFields(l.extractor(ctx)) 51 | } 52 | -------------------------------------------------------------------------------- /internal/common/commonadapter/logger_test.go: -------------------------------------------------------------------------------- 1 | package commonadapter 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "logur.dev/logur" 8 | "logur.dev/logur/conformance" 9 | "logur.dev/logur/logtesting" 10 | ) 11 | 12 | func TestLogger(t *testing.T) { 13 | t.Run("WithFields", testLoggerWithFields) 14 | t.Run("WithContext", testLoggerWithContext) 15 | 16 | suite := conformance.TestSuite{ 17 | LoggerFactory: func(level logur.Level) (logur.Logger, conformance.TestLogger) { 18 | testLogger := &logur.TestLoggerFacade{} 19 | 20 | return NewLogger(testLogger), testLogger 21 | }, 22 | } 23 | t.Run("Conformance", suite.Run) 24 | } 25 | 26 | func TestContextAwareLogger(t *testing.T) { 27 | t.Run("WithContext", testContextAwareLoggerWithContext) 28 | 29 | suite := conformance.TestSuite{ 30 | LoggerFactory: func(level logur.Level) (logur.Logger, conformance.TestLogger) { 31 | testLogger := &logur.TestLoggerFacade{} 32 | 33 | return NewContextAwareLogger( 34 | testLogger, 35 | func(ctx context.Context) map[string]interface{} { 36 | return nil 37 | }, 38 | ), testLogger 39 | }, 40 | } 41 | t.Run("Conformance", suite.Run) 42 | } 43 | 44 | func testLoggerWithFields(t *testing.T) { 45 | testLogger := &logur.TestLoggerFacade{} 46 | 47 | fields := map[string]interface{}{ 48 | "key1": "value1", 49 | "key2": "value2", 50 | } 51 | 52 | logger := NewLogger(testLogger).WithFields(fields) 53 | 54 | logger.Debug("message", nil) 55 | 56 | event := logur.LogEvent{ 57 | Level: logur.Debug, 58 | Line: "message", 59 | Fields: fields, 60 | } 61 | 62 | logtesting.AssertLogEventsEqual(t, event, *(testLogger.LastEvent())) 63 | } 64 | 65 | func testLoggerWithContext(t *testing.T) { 66 | testLogger := &logur.TestLoggerFacade{} 67 | 68 | logger := NewLogger(testLogger).WithContext(context.Background()) 69 | 70 | logger.Debug("message", nil) 71 | 72 | event := logur.LogEvent{ 73 | Level: logur.Debug, 74 | Line: "message", 75 | } 76 | 77 | logtesting.AssertLogEventsEqual(t, event, *(testLogger.LastEvent())) 78 | } 79 | 80 | func testContextAwareLoggerWithContext(t *testing.T) { 81 | testLogger := &logur.TestLoggerFacade{} 82 | 83 | logger := NewContextAwareLogger( 84 | testLogger, 85 | func(_ context.Context) map[string]interface{} { 86 | return map[string]interface{}{ 87 | "key1": "value1", 88 | "key2": "value2", 89 | } 90 | }, 91 | ).WithContext(context.Background()) 92 | 93 | logger.Debug("message", nil) 94 | 95 | event := logur.LogEvent{ 96 | Level: logur.Debug, 97 | Line: "message", 98 | Fields: map[string]interface{}{ 99 | "key1": "value1", 100 | "key2": "value2", 101 | }, 102 | } 103 | 104 | logtesting.AssertLogEventsEqual(t, event, *(testLogger.LastEvent())) 105 | } 106 | -------------------------------------------------------------------------------- /internal/common/error_handler.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // ErrorHandler handles an error. 8 | type ErrorHandler interface { 9 | Handle(err error) 10 | HandleContext(ctx context.Context, err error) 11 | } 12 | 13 | // NoopErrorHandler is an error handler that discards every error. 14 | type NoopErrorHandler struct{} 15 | 16 | func (NoopErrorHandler) Handle(_ error) {} 17 | func (NoopErrorHandler) HandleContext(_ context.Context, _ error) {} 18 | -------------------------------------------------------------------------------- /internal/common/logger.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Logger is the fundamental interface for all log operations. 8 | type Logger interface { 9 | // Trace logs a trace event. 10 | Trace(msg string, fields ...map[string]interface{}) 11 | 12 | // Debug logs a debug event. 13 | Debug(msg string, fields ...map[string]interface{}) 14 | 15 | // Info logs an info event. 16 | Info(msg string, fields ...map[string]interface{}) 17 | 18 | // Warn logs a warning event. 19 | Warn(msg string, fields ...map[string]interface{}) 20 | 21 | // Error logs an error event. 22 | Error(msg string, fields ...map[string]interface{}) 23 | 24 | // TraceContext logs a trace event with a context. 25 | TraceContext(ctx context.Context, msg string, fields ...map[string]interface{}) 26 | 27 | // DebugContext logs a debug event with a context. 28 | DebugContext(ctx context.Context, msg string, fields ...map[string]interface{}) 29 | 30 | // InfoContext logs an info event with a context. 31 | InfoContext(ctx context.Context, msg string, fields ...map[string]interface{}) 32 | 33 | // WarnContext logs a warning event with a context. 34 | WarnContext(ctx context.Context, msg string, fields ...map[string]interface{}) 35 | 36 | // ErrorContext logs an error event with a context. 37 | ErrorContext(ctx context.Context, msg string, fields ...map[string]interface{}) 38 | 39 | // WithFields annotates a logger with key-value pairs. 40 | WithFields(fields map[string]interface{}) Logger 41 | 42 | // WithContext annotates a logger with a context. 43 | WithContext(ctx context.Context) Logger 44 | } 45 | 46 | // NoopLogger is a logger that discards every log event. 47 | type NoopLogger struct{} 48 | 49 | func (NoopLogger) Trace(_ string, _ ...map[string]interface{}) {} 50 | func (NoopLogger) Debug(_ string, _ ...map[string]interface{}) {} 51 | func (NoopLogger) Info(_ string, _ ...map[string]interface{}) {} 52 | func (NoopLogger) Warn(_ string, _ ...map[string]interface{}) {} 53 | func (NoopLogger) Error(_ string, _ ...map[string]interface{}) {} 54 | 55 | func (NoopLogger) TraceContext(_ context.Context, _ string, _ ...map[string]interface{}) {} 56 | func (NoopLogger) DebugContext(_ context.Context, _ string, _ ...map[string]interface{}) {} 57 | func (NoopLogger) InfoContext(_ context.Context, _ string, _ ...map[string]interface{}) {} 58 | func (NoopLogger) WarnContext(_ context.Context, _ string, _ ...map[string]interface{}) {} 59 | func (NoopLogger) ErrorContext(_ context.Context, _ string, _ ...map[string]interface{}) {} 60 | 61 | func (n NoopLogger) WithFields(_ map[string]interface{}) Logger { return n } 62 | func (n NoopLogger) WithContext(_ context.Context) Logger { return n } 63 | -------------------------------------------------------------------------------- /internal/platform/appkit/context.go: -------------------------------------------------------------------------------- 1 | package appkit 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sagikazarmark/kitx/correlation" 7 | kitxendpoint "github.com/sagikazarmark/kitx/endpoint" 8 | "go.opencensus.io/trace" 9 | ) 10 | 11 | // ContextExtractor extracts fields from a context. 12 | func ContextExtractor(ctx context.Context) map[string]interface{} { 13 | fields := make(map[string]interface{}) 14 | 15 | if correlationID, ok := correlation.FromContext(ctx); ok { 16 | fields["correlation_id"] = correlationID 17 | } 18 | 19 | if operationName, ok := kitxendpoint.OperationName(ctx); ok { 20 | fields["operation_name"] = operationName 21 | } 22 | 23 | if span := trace.FromContext(ctx); span != nil { 24 | spanCtx := span.SpanContext() 25 | 26 | fields["trace_id"] = spanCtx.TraceID.String() 27 | fields["span_id"] = spanCtx.SpanID.String() 28 | } 29 | 30 | return fields 31 | } 32 | -------------------------------------------------------------------------------- /internal/platform/database/config.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "emperror.dev/errors" 7 | ) 8 | 9 | // Config holds information necessary for connecting to a database. 10 | type Config struct { 11 | Host string 12 | Port int 13 | User string 14 | Pass string 15 | Name string 16 | 17 | Params map[string]string 18 | } 19 | 20 | // Validate checks that the configuration is valid. 21 | func (c Config) Validate() error { 22 | if c.Host == "" { 23 | return errors.New("database host is required") 24 | } 25 | 26 | if c.Port == 0 { 27 | return errors.New("database port is required") 28 | } 29 | 30 | if c.User == "" { 31 | return errors.New("database user is required") 32 | } 33 | 34 | if c.Name == "" { 35 | return errors.New("database name is required") 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // DSN returns a MySQL driver compatible data source name. 42 | func (c Config) DSN() string { 43 | var params string 44 | 45 | if len(c.Params) > 0 { 46 | var query string 47 | 48 | for key, value := range c.Params { 49 | if query != "" { 50 | query += "&" 51 | } 52 | 53 | query += key + "=" + value 54 | } 55 | 56 | params = "?" + query 57 | } 58 | 59 | return fmt.Sprintf( 60 | "%s:%s@tcp(%s:%d)/%s%s", 61 | c.User, 62 | c.Pass, 63 | c.Host, 64 | c.Port, 65 | c.Name, 66 | params, 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /internal/platform/database/config_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConfig_Validate(t *testing.T) { 10 | tests := map[string]Config{ 11 | "database host is required": { 12 | Port: 3306, 13 | User: "root", 14 | Pass: "", 15 | Name: "database", 16 | }, 17 | "database port is required": { 18 | Host: "localhost", 19 | User: "root", 20 | Pass: "", 21 | Name: "database", 22 | }, 23 | "database user is required": { 24 | Host: "localhost", 25 | Port: 3306, 26 | Pass: "", 27 | Name: "database", 28 | }, 29 | "database name is required": { 30 | Host: "localhost", 31 | Port: 3306, 32 | User: "root", 33 | Pass: "", 34 | }, 35 | } 36 | 37 | for name, test := range tests { 38 | name, test := name, test 39 | 40 | t.Run(name, func(t *testing.T) { 41 | err := test.Validate() 42 | 43 | assert.EqualError(t, err, name) 44 | }) 45 | } 46 | } 47 | 48 | func TestConfig_DSN(t *testing.T) { 49 | config := Config{ 50 | Host: "host", 51 | Port: 3306, 52 | User: "root", 53 | Pass: "", 54 | Name: "database", 55 | Params: map[string]string{ 56 | "parseTime": "true", 57 | }, 58 | } 59 | 60 | dsn := config.DSN() 61 | 62 | assert.Equal(t, "root:@tcp(host:3306)/database?parseTime=true", dsn) 63 | } 64 | -------------------------------------------------------------------------------- /internal/platform/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql/driver" 5 | 6 | "contrib.go.opencensus.io/integrations/ocsql" 7 | "emperror.dev/errors" 8 | "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | // NewConnector returns a new database connector for the application. 12 | func NewConnector(config Config) (driver.Connector, error) { 13 | // Set some mandatory parameters 14 | config.Params["parseTime"] = "true" 15 | config.Params["rejectReadOnly"] = "true" 16 | 17 | // TODO: fill in the config instead of playing with DSN 18 | conf, err := mysql.ParseDSN(config.DSN()) 19 | if err != nil { 20 | return nil, errors.WithStack(err) 21 | } 22 | 23 | connector, err := mysql.NewConnector(conf) 24 | if err != nil { 25 | return nil, errors.WithStack(err) 26 | } 27 | 28 | return ocsql.WrapConnector( 29 | connector, 30 | ocsql.WithOptions(ocsql.TraceOptions{ 31 | AllowRoot: false, 32 | Ping: true, 33 | RowsNext: true, 34 | RowsClose: true, 35 | RowsAffected: true, 36 | LastInsertID: true, 37 | Query: true, 38 | QueryParams: false, 39 | }), 40 | ), nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/platform/database/logger.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/go-sql-driver/mysql" 5 | "logur.dev/logur" 6 | ) 7 | 8 | // SetLogger configures the global database logger. 9 | func SetLogger(logger logur.Logger) { 10 | logger = logur.WithField(logger, "component", "mysql") 11 | 12 | _ = mysql.SetLogger(logur.NewErrorPrintLogger(logger)) 13 | } 14 | -------------------------------------------------------------------------------- /internal/platform/gosundheit/logger.go: -------------------------------------------------------------------------------- 1 | package gosundheit 2 | 3 | import ( 4 | health "github.com/AppsFlyer/go-sundheit" 5 | "logur.dev/logur" 6 | ) 7 | 8 | type checkListener struct { 9 | logger logur.Logger 10 | } 11 | 12 | func NewLogger(logger logur.Logger) health.CheckListener { 13 | return checkListener{ 14 | logger: logger, 15 | } 16 | } 17 | 18 | func (c checkListener) OnCheckStarted(name string) { 19 | c.logger.Trace("starting check", map[string]interface{}{"check": name}) 20 | } 21 | 22 | func (c checkListener) OnCheckCompleted(name string, result health.Result) { 23 | if result.Error != nil { 24 | c.logger.Trace("check failed", map[string]interface{}{"check": name, "error": result.Error.Error()}) 25 | 26 | return 27 | } 28 | 29 | c.logger.Trace("check completed", map[string]interface{}{"check": name}) 30 | } 31 | -------------------------------------------------------------------------------- /internal/platform/log/config.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | // Config holds details necessary for logging. 4 | type Config struct { 5 | // Format specifies the output log format. 6 | // Accepted values are: json, logfmt 7 | Format string 8 | 9 | // Level is the minimum log level that should appear on the output. 10 | Level string 11 | 12 | // NoColor makes sure that no log output gets colorized. 13 | NoColor bool 14 | } 15 | -------------------------------------------------------------------------------- /internal/platform/log/logger.go: -------------------------------------------------------------------------------- 1 | // Package log configures a new logger for an application. 2 | package log 3 | 4 | import ( 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | logrusadapter "logur.dev/adapter/logrus" 9 | "logur.dev/logur" 10 | ) 11 | 12 | // NewLogger creates a new logger. 13 | func NewLogger(config Config) logur.LoggerFacade { 14 | logger := logrus.New() 15 | 16 | logger.SetOutput(os.Stdout) 17 | logger.SetFormatter(&logrus.TextFormatter{ 18 | DisableColors: config.NoColor, 19 | EnvironmentOverrideColors: true, 20 | }) 21 | 22 | switch config.Format { 23 | case "logfmt": 24 | // Already the default 25 | 26 | case "json": 27 | logger.SetFormatter(&logrus.JSONFormatter{}) 28 | } 29 | 30 | if level, err := logrus.ParseLevel(config.Level); err == nil { 31 | logger.SetLevel(level) 32 | } 33 | 34 | return logrusadapter.New(logger) 35 | } 36 | -------------------------------------------------------------------------------- /internal/platform/log/standard_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "log" 5 | 6 | "logur.dev/logur" 7 | ) 8 | 9 | // NewErrorStandardLogger returns a new standard logger logging on error level. 10 | func NewErrorStandardLogger(logger logur.Logger) *log.Logger { 11 | return logur.NewErrorStandardLogger(logger, "", 0) 12 | } 13 | 14 | // SetStandardLogger sets the global logger's output to a custom logger instance. 15 | func SetStandardLogger(logger logur.Logger) { 16 | log.SetOutput(logur.NewLevelWriter(logger, logur.Info)) 17 | } 18 | -------------------------------------------------------------------------------- /internal/platform/opencensus/exporter.go: -------------------------------------------------------------------------------- 1 | package opencensus 2 | 3 | import ( 4 | "time" 5 | 6 | "contrib.go.opencensus.io/exporter/ocagent" 7 | ) 8 | 9 | // ExporterConfig configures an OpenCensus exporter. 10 | type ExporterConfig struct { 11 | Address string 12 | Insecure bool 13 | ReconnectPeriod time.Duration 14 | } 15 | 16 | // Options returns a set of OpenCensus exporter options used for configuring the exporter. 17 | func (c ExporterConfig) Options() []ocagent.ExporterOption { 18 | options := []ocagent.ExporterOption{ 19 | ocagent.WithAddress(c.Address), 20 | ocagent.WithReconnectionPeriod(c.ReconnectPeriod), 21 | } 22 | 23 | if c.Insecure { 24 | options = append(options, ocagent.WithInsecure()) 25 | } 26 | 27 | return options 28 | } 29 | -------------------------------------------------------------------------------- /internal/platform/opencensus/trace.go: -------------------------------------------------------------------------------- 1 | package opencensus 2 | 3 | import ( 4 | "strings" 5 | 6 | "go.opencensus.io/trace" 7 | ) 8 | 9 | // TraceConfig configures OpenCensus tracing. 10 | type TraceConfig struct { 11 | // Sampling describes the default sampler used when creating new spans. 12 | Sampling SamplingTraceConfig 13 | 14 | // MaxAnnotationEventsPerSpan is max number of annotation events per span. 15 | MaxAnnotationEventsPerSpan int 16 | 17 | // MaxMessageEventsPerSpan is max number of message events per span. 18 | MaxMessageEventsPerSpan int 19 | 20 | // MaxAnnotationEventsPerSpan is max number of attributes per span. 21 | MaxAttributesPerSpan int 22 | 23 | // MaxLinksPerSpan is max number of links per span. 24 | MaxLinksPerSpan int 25 | } 26 | 27 | // SamplingTraceConfig configures OpenCensus trace sampling. 28 | type SamplingTraceConfig struct { 29 | Sampler string 30 | Fraction float64 31 | } 32 | 33 | // Config returns an OpenCensus trace configuration. 34 | func (t TraceConfig) Config() trace.Config { 35 | config := trace.Config{ 36 | MaxAnnotationEventsPerSpan: t.MaxAnnotationEventsPerSpan, 37 | MaxMessageEventsPerSpan: t.MaxMessageEventsPerSpan, 38 | MaxAttributesPerSpan: t.MaxAttributesPerSpan, 39 | MaxLinksPerSpan: t.MaxLinksPerSpan, 40 | } 41 | 42 | switch strings.ToLower(strings.TrimSpace(t.Sampling.Sampler)) { 43 | case "always": 44 | config.DefaultSampler = trace.AlwaysSample() 45 | 46 | case "never": 47 | config.DefaultSampler = trace.NeverSample() 48 | 49 | case "probability": 50 | config.DefaultSampler = trace.ProbabilitySampler(t.Sampling.Fraction) 51 | } 52 | 53 | return config 54 | } 55 | -------------------------------------------------------------------------------- /internal/platform/watermill/middleware.go: -------------------------------------------------------------------------------- 1 | package watermill 2 | 3 | import ( 4 | "github.com/ThreeDotsLabs/watermill/message" 5 | "github.com/ThreeDotsLabs/watermill/message/router/middleware" 6 | "github.com/sagikazarmark/kitx/correlation" 7 | ) 8 | 9 | // PublisherCorrelationID decorates a publisher with a correlation ID middleware. 10 | func PublisherCorrelationID(publisher message.Publisher) message.Publisher { 11 | publisher, _ = message.MessageTransformPublisherDecorator(func(msg *message.Message) { 12 | if cid, ok := correlation.FromContext(msg.Context()); ok { 13 | middleware.SetCorrelationID(cid, msg) 14 | } 15 | })(publisher) 16 | 17 | return publisher 18 | } 19 | 20 | // SubscriberCorrelationID decorates a subscriber with a correlation ID middleware. 21 | func SubscriberCorrelationID(subscriber message.Subscriber) message.Subscriber { 22 | subscriber, _ = message.MessageTransformSubscriberDecorator(func(msg *message.Message) { 23 | if cid := middleware.MessageCorrelationID(msg); cid != "" { 24 | msg.SetContext(correlation.ToContext(msg.Context(), cid)) 25 | } 26 | })(subscriber) 27 | 28 | return subscriber 29 | } 30 | -------------------------------------------------------------------------------- /internal/platform/watermill/pubsub.go: -------------------------------------------------------------------------------- 1 | package watermill 2 | 3 | import ( 4 | "github.com/ThreeDotsLabs/watermill/message" 5 | "github.com/ThreeDotsLabs/watermill/pubsub/gochannel" 6 | watermilllog "logur.dev/integration/watermill" 7 | "logur.dev/logur" 8 | ) 9 | 10 | // NewPubSub returns a new PubSub. 11 | func NewPubSub(logger logur.Logger) (message.Publisher, message.Subscriber) { 12 | pubsub := gochannel.NewGoChannel( 13 | gochannel.Config{}, 14 | watermilllog.New(logur.WithField(logger, "component", "watermill")), 15 | ) 16 | 17 | return pubsub, pubsub 18 | } 19 | -------------------------------------------------------------------------------- /internal/platform/watermill/router.go: -------------------------------------------------------------------------------- 1 | package watermill 2 | 3 | import ( 4 | "time" 5 | 6 | "emperror.dev/errors" 7 | "github.com/ThreeDotsLabs/watermill/message" 8 | "github.com/ThreeDotsLabs/watermill/message/router/middleware" 9 | watermilllog "logur.dev/integration/watermill" 10 | "logur.dev/logur" 11 | ) 12 | 13 | // NewRouter returns a new message router for message subscription logic. 14 | func NewRouter(logger logur.Logger) (*message.Router, error) { 15 | h, err := message.NewRouter( 16 | message.RouterConfig{}, 17 | watermilllog.New(logur.WithField(logger, "component", "watermill")), 18 | ) 19 | if err != nil { 20 | return nil, errors.WithMessage(err, "failed to create message router") 21 | } 22 | 23 | retryMiddleware := middleware.Retry{} 24 | retryMiddleware.MaxRetries = 1 25 | retryMiddleware.MaxInterval = time.Millisecond * 10 26 | 27 | h.AddMiddleware( 28 | // if retries limit was exceeded, message is sent to poison queue (poison_queue topic) 29 | retryMiddleware.Middleware, 30 | 31 | // recovered recovers panic from handlers 32 | middleware.Recoverer, 33 | 34 | // correlation ID middleware adds to every produced message correlation id of consumed message, 35 | // useful for debugging 36 | middleware.CorrelationID, 37 | ) 38 | 39 | return h, nil 40 | } 41 | -------------------------------------------------------------------------------- /main.mk: -------------------------------------------------------------------------------- 1 | # Main targets for a Go app project 2 | # 3 | # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 4 | 5 | OS = $(shell uname | tr A-Z a-z) 6 | export PATH := $(abspath bin/):${PATH} 7 | 8 | # Build variables 9 | BUILD_DIR ?= build 10 | VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || git symbolic-ref -q --short HEAD) 11 | COMMIT_HASH ?= $(shell git rev-parse --short HEAD 2>/dev/null) 12 | DATE_FMT = +%FT%T%z 13 | ifdef SOURCE_DATE_EPOCH 14 | BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") 15 | else 16 | BUILD_DATE ?= $(shell date "$(DATE_FMT)") 17 | endif 18 | LDFLAGS += -X main.version=${VERSION} -X main.commitHash=${COMMIT_HASH} -X main.buildDate=${BUILD_DATE} 19 | export CGO_ENABLED ?= 0 20 | ifeq (${VERBOSE}, 1) 21 | ifeq ($(filter -v,${GOARGS}),) 22 | GOARGS += -v 23 | endif 24 | TEST_FORMAT = short-verbose 25 | endif 26 | 27 | # Dependency versions 28 | GOTESTSUM_VERSION ?= 0.4.2 29 | GOLANGCI_VERSION ?= 1.27.0 30 | 31 | GOLANG_VERSION ?= 1.14 32 | 33 | .PHONY: clear 34 | clear: ${CLEAR_TARGETS} ## Clear the working area and the project 35 | rm -rf bin/ 36 | 37 | .PHONY: clean 38 | clean: ${CLEAN_TARGETS} ## Clean builds 39 | rm -rf ${BUILD_DIR}/ 40 | 41 | .PHONY: run-% 42 | run-%: build-% 43 | ${BUILD_DIR}/$* 44 | 45 | .PHONY: run 46 | run: $(patsubst cmd/%,run-%,$(wildcard cmd/*)) ## Build and execute all applications 47 | 48 | .PHONY: goversion 49 | goversion: 50 | ifneq (${IGNORE_GOLANG_VERSION}, 1) 51 | @printf "${GOLANG_VERSION}\n$$(go version | awk '{sub(/^go/, "", $$3);print $$3}')" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -g | head -1 | grep -q -E "^${GOLANG_VERSION}$$" || (printf "Required Go version is ${GOLANG_VERSION}\nInstalled: `go version`" && exit 1) 52 | endif 53 | 54 | .PHONY: build-deps 55 | build-deps: ${BUILD_DEP_TARGETS} 56 | @: 57 | 58 | .PHONY: pre-build 59 | pre-build: ${PRE_BUILD_TARGETS} 60 | @: 61 | 62 | .PHONY: post-build 63 | post-build: ${POST_BUILD_TARGETS} 64 | @: 65 | 66 | .PHONY: build-% 67 | build-%: build-deps pre-build 68 | build-%: goversion 69 | ifeq (${VERBOSE}, 1) 70 | go env 71 | endif 72 | 73 | @mkdir -p ${BUILD_DIR} 74 | go build ${GOARGS} -trimpath -tags "${GOTAGS}" -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/$* ./cmd/$* 75 | 76 | @${MAKE} post-build 77 | 78 | .PHONY: build 79 | build: build-deps pre-build 80 | build: goversion ## Build binaries 81 | ifeq (${VERBOSE}, 1) 82 | go env 83 | endif 84 | 85 | @mkdir -p ${BUILD_DIR} 86 | go build ${GOARGS} -trimpath -tags "${GOTAGS}" -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/ ./cmd/... 87 | 88 | @${MAKE} post-build 89 | 90 | .PHONY: build-release-deps 91 | build-release-deps: build-deps 92 | build-release-deps: ${BUILD_RELEASE_DEP_TARGETS} 93 | @: 94 | 95 | .PHONY: pre-build-release 96 | pre-build-release: ${PRE_BUILD_RELEASE_TARGETS} 97 | @: 98 | 99 | .PHONY: post-build-release 100 | post-build-release: ${POST_BUILD_RELEASE_TARGETS} 101 | @: 102 | 103 | .PHONY: build-release 104 | build-release: build-release-deps pre-build-release 105 | build-release: ## Build binaries without debug information 106 | @${MAKE} LDFLAGS="-w ${LDFLAGS}" GOARGS="${GOARGS} -trimpath" BUILD_DIR="${BUILD_DIR}/release" build 107 | 108 | @${MAKE} post-build-release 109 | 110 | .PHONY: build-debug-deps 111 | build-debug-deps: build-deps 112 | build-debug-deps: ${BUILD_DEBUG_DEP_TARGETS} 113 | @: 114 | 115 | .PHONY: pre-build-debug 116 | pre-build-debug: ${PRE_BUILD_DEBUG_TARGETS} 117 | @: 118 | 119 | .PHONY: post-build-debug 120 | post-build-debug: ${POST_BUILD_DEBUG_TARGETS} 121 | @: 122 | 123 | .PHONY: build-debug 124 | build-debug: build-debug-deps pre-build-debug 125 | build-debug: ## Build binaries with remote debugging capabilities 126 | @${MAKE} GOARGS="${GOARGS} -gcflags \"all=-N -l\"" BUILD_DIR="${BUILD_DIR}/debug" build 127 | 128 | @${MAKE} post-build-debug 129 | 130 | .PHONY: check 131 | check: ${CHECK_TARGETS} 132 | check: test lint ## Run checks (tests and linters) 133 | 134 | bin/gotestsum: bin/gotestsum-${GOTESTSUM_VERSION} 135 | @ln -sf gotestsum-${GOTESTSUM_VERSION} bin/gotestsum 136 | bin/gotestsum-${GOTESTSUM_VERSION}: 137 | @mkdir -p bin 138 | curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_${OS}_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum-${GOTESTSUM_VERSION} && chmod +x ./bin/gotestsum-${GOTESTSUM_VERSION} 139 | 140 | TEST_PKGS ?= ./... 141 | .PHONY: test 142 | test: TEST_FORMAT ?= short 143 | test: SHELL = /bin/bash 144 | test: export CGO_ENABLED=1 145 | test: bin/gotestsum ## Run tests 146 | @mkdir -p ${BUILD_DIR} 147 | bin/gotestsum --no-summary=skipped --junitfile ${BUILD_DIR}/coverage.xml --format ${TEST_FORMAT} -- -race -coverprofile=${BUILD_DIR}/coverage.txt -covermode=atomic $(filter-out -v,${GOARGS}) $(if ${TEST_PKGS},${TEST_PKGS},./...) 148 | 149 | bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION} 150 | @ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint 151 | bin/golangci-lint-${GOLANGCI_VERSION}: 152 | @mkdir -p bin 153 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION} 154 | @mv bin/golangci-lint $@ 155 | 156 | .PHONY: lint 157 | lint: bin/golangci-lint ## Run linter 158 | bin/golangci-lint run 159 | 160 | .PHONY: fix 161 | fix: bin/golangci-lint ## Fix lint violations 162 | bin/golangci-lint run --fix 163 | 164 | release-%: TAG_PREFIX = v 165 | release-%: 166 | ifneq (${DRY}, 1) 167 | @sed -e "s/^## \[Unreleased\]$$/## [Unreleased]\\"$$'\n'"\\"$$'\n'"\\"$$'\n'"## [$*] - $$(date +%Y-%m-%d)/g; s|^\[Unreleased\]: \(.*\/compare\/\)\(.*\)...HEAD$$|[Unreleased]: \1${TAG_PREFIX}$*...HEAD\\"$$'\n'"[$*]: \1\2...${TAG_PREFIX}$*|g" CHANGELOG.md > CHANGELOG.md.new 168 | @mv CHANGELOG.md.new CHANGELOG.md 169 | 170 | ifeq (${TAG}, 1) 171 | git add CHANGELOG.md 172 | git commit -m 'Prepare release $*' 173 | git tag -m 'Release $*' ${TAG_PREFIX}$* 174 | ifeq (${PUSH}, 1) 175 | git push; git push origin ${TAG_PREFIX}$* 176 | endif 177 | endif 178 | endif 179 | 180 | @echo "Version updated to $*!" 181 | ifneq (${PUSH}, 1) 182 | @echo 183 | @echo "Review the changes made by this script then execute the following:" 184 | ifneq (${TAG}, 1) 185 | @echo 186 | @echo "git add CHANGELOG.md && git commit -m 'Prepare release $*' && git tag -m 'Release $*' ${TAG_PREFIX}$*" 187 | @echo 188 | @echo "Finally, push the changes:" 189 | endif 190 | @echo 191 | @echo "git push; git push origin ${TAG_PREFIX}$*" 192 | endif 193 | 194 | .PHONY: patch 195 | patch: ## Release a new patch version 196 | @${MAKE} release-$(shell (git describe --abbrev=0 --tags 2> /dev/null || echo "0.0.0") | sed 's/^v//' | awk -F'[ .]' '{print $$1"."$$2"."$$3+1}') 197 | 198 | .PHONY: minor 199 | minor: ## Release a new minor version 200 | @${MAKE} release-$(shell (git describe --abbrev=0 --tags 2> /dev/null || echo "0.0.0") | sed 's/^v//' | awk -F'[ .]' '{print $$1"."$$2+1".0"}') 201 | 202 | .PHONY: major 203 | major: ## Release a new major version 204 | @${MAKE} release-$(shell (git describe --abbrev=0 --tags 2> /dev/null || echo "0.0.0") | sed 's/^v//' | awk -F'[ .]' '{print $$1+1".0.0"}') 205 | 206 | .PHONY: list 207 | list: ## List all make targets 208 | @${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort 209 | 210 | .PHONY: help 211 | .DEFAULT_GOAL := help 212 | help: 213 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 214 | 215 | # Variable outputting/exporting rules 216 | var-%: ; @echo $($*) 217 | varexport-%: ; @echo $*=$($*) 218 | 219 | # Update main targets 220 | main.mk: 221 | curl https://raw.githubusercontent.com/sagikazarmark/makefiles/master/go-app/main.mk > main.mk 222 | -------------------------------------------------------------------------------- /pkg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagikazarmark/modern-go-application/468a20bc42f96ffebae693a51b006b263b4a497e/pkg/.gitkeep -------------------------------------------------------------------------------- /static/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import "embed" 4 | 5 | //go:embed landing.html 6 | var files embed.FS 7 | 8 | // Files returns a filesystem with static files. 9 | func Files() embed.FS { 10 | return files 11 | } 12 | --------------------------------------------------------------------------------