[
  {
    "path": ".editorconfig",
    "content": "; http://editorconfig.org/\n\nroot = true\n\n[*]\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\nindent_style = space\nindent_size = 2\n\n[{Makefile,go.mod,go.sum,*.go}]\nindent_style = tab\nindent_size = 8\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    paths-ignore:\n      - \"**.md\"\n      - \"docs/**\"\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Set up Go 1.14\n        uses: actions/setup-go@v1\n        with:\n          go-version: 1.14\n        id: go\n\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v2\n\n      - name: Install tools\n        run: make setup\n\n      - name: Test\n        run: make ci\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "content": "name: Release with goreleaser\non:\n  push:\n    branches:\n      - \"!*\"\n    tags:\n      - \"v*.*.*\"\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Unshallow\n        run: git fetch --prune --unshallow\n      - name: Set up Go\n        uses: actions/setup-go@v1\n        with:\n          go-version: 1.13.x\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v1\n        with:\n          version: latest\n          args: release --rm-dist\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\nvendor\n.netrc\n.vscode\n.vs\n.tern-project\n.DS_Store\n.idea\n.cgo_ldflags\ntmp\n\n.eslintcache\n\n# dependencies\nweb/node_modules\nweb/.pnp\nweb/.pnp.js\n\n# testing\nweb/coverage\n\n# misc\n.DS_Store\n.env*\n/.sass-cache\n/connect.lock\n/coverage/*\n/test-results/*\n/libpeerconnection.log\nnpm-debug.log\ntestem.log\n/typings\n/vendor\n\n# dist\ndist\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# make artifacts\n/bin\n\n# vendored files\n/vendor\n\n# test outputs\n/test-results.xml\njunit-results\ncypress/screenshots\ncypress/videos\ncoverage.txt\n\n# gcloud utils\ncloud_sql_proxy\n.now\n\n# tools\nair\nair.log\n\n# test data\ncockroach-data\nbin\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Setup your machine\n\n`go-web` is written in [Go](https://golang.org/).\n\nPrerequisites:\n\n- `make`\n- [Go 1.14+](https://golang.org/doc/install)\n- [Docker](https://www.docker.com/)\n- `gpg` (probably already installed on your system)\n\nClone `goweb` anywhere:\n\n```sh\n$ git clone git@github.com:StarpTech/go-web.git\n```\n\nInstall the build and lint dependencies:\n\n```sh\n$ make setup\n```\n\nA good way of making sure everything is all right is running the test suite:\n\n```sh\n$ make test\n```\n\n## Test your change\n\nYou can create a branch for your changes and try to build from the source as you go:\n\n```sh\n$ make\n```\n\nWhen you are satisfied with the changes, we suggest you run:\n\n```sh\n$ make ci\n```\n\nWhich runs all the linters and tests.\n\n## Create a commit\n\nCommit messages should be well formatted, and to make that \"standardized\", we\nare using Conventional Commits.\n\nYou can follow the documentation on\n[their website](https://www.conventionalcommits.org).\n\n## Submit a pull request\n\nPush your branch to your `go-web` fork and open a pull request against the\nmaster branch.\n\n## Deployment\n\nTag a new release and push it to origin. This will trigger the Github CI to deploy the commit.\n```\ngit tag v1.0.0\ngit push origin v1.0.0\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Dustin Deus\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "MODULE   = $(shell env GO111MODULE=on $(GO) list -m)\nDATE    ?= $(shell date +%FT%T%z)\nVERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \\\n\t\t\tcat $(CURDIR)/.version 2> /dev/null || echo v0)\nPKGS     = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...))\nTESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \\\n\t\t\t'{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \\\n\t\t\t$(PKGS))\nBIN      = $(CURDIR)/bin\n\nGO      = go\nTIMEOUT = 15\nV = 0\nQ = $(if $(filter 1,$V),,@)\nM = $(shell printf \"\\033[34;1m▶\\033[0m\")\n\nexport GO111MODULE=on\nexport GOPROXY=https://proxy.golang.org,direct\n\n.PHONY: all\nall: fmt lint | $(BIN) ; $(info $(M) building executable…) @ ## Build program binary\n\t$Q $(GO) build \\\n\t\t-tags release \\\n\t\t-ldflags '-X $(MODULE)/cmd.Version=$(VERSION) -X $(MODULE)/cmd.BuildDate=$(DATE)' \\\n\t\t-o $(BIN)/$(basename $(MODULE)) main.go\n\n# Tools\n\n# Install all the build and lint dependencies\nsetup:\n\t# Install by default to .bin\n\tcurl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- v1.24.0\n\tgo mod tidy\n.PHONY: setup\n\n$(BIN):\n\t@mkdir -p $@\n$(BIN)/%: | $(BIN) ; $(info $(M) building $(PACKAGE)…)\n\t$Q tmp=$$(mktemp -d); \\\n\t   env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(PACKAGE) \\\n\t\t|| ret=$$?; \\\n\t   rm -rf $$tmp ; exit $$ret\n\n# Tests\n\n# Run all the tests\ntest:\n\tLC_ALL=C go test $(TEST_OPTIONS) -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m\n.PHONY: test\n\n.PHONY: cover\ncover: ; $(info $(M) running coverage…) @  # Run all the tests and opens the coverage report\n\tgo tool cover -html=coverage.txt\n.PHONY: cover\n\n.PHONY: fmt\nfmt: ; $(info $(M) running gofmt…) @ ## Run gofmt on all source files\n\t$Q $(GO) fmt $(PKGS)\n\n.PHONY: lint\nlint: ; $(info $(M) running lint…) @ ## Run gofmt on all source files\n\t./bin/golangci-lint run ./...\n\n.PHONY: ci\nci: all test; $(info $(M) running all the tests and code checks…) @ ## Run all the tests and code checks\n\n# UI\n\n.PHONY: ui\nui: ; @ ## Run frontend development server\n\tcd ui && yarn run dev\n\n.PHONY: build-ui\nbuild-ui: ; @ ## Build frontend production build\n\tcd ui && yarn run build\n\n# API\n\n.PHONY: start\nstart-api: ; @ ## Start api\n\tgo run main.go\n\n# Misc\n\n.PHONY: clean\nclean: ; $(info $(M) cleaning…)\t@ ## Cleanup everything\n\t@rm -rf $(BIN)\n\t@rm -rf test/tests.* test/coverage.*\n\n.PHONY: help\nhelp:\n\t@grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \\\n\t\tawk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-15s\\033[0m %s\\n\", $$1, $$2}'\n\n.PHONY: version\nversion:\n\t@echo $(VERSION)\n"
  },
  {
    "path": "README.md",
    "content": "![big-gopher](big-gopher.png)\n\n[![License MIT](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT)\n[![Build Status](https://github.com/StarpTech/go-web/workflows/Go/badge.svg)](https://github.com/StarpTech/go-web/actions)\n[![Go Report Card](https://goreportcard.com/badge/github.com/StarpTech/go-web)](https://goreportcard.com/report/github.com/StarpTech/go-web)\n\n# Go-Web\n\nModern Web Application with Golang \"Keep it simple, stupid\"\n\n# Stack\n\n## Backend\n\n- HTTP Middleware [Echo](https://echo.labstack.com/)\n- ORM library [gorm](https://github.com/jinzhu/gorm)\n- Configuration [env](https://github.com/caarlos0/env)\n- Load ENV variables from .env file [godotenv](https://github.com/joho/godotenv)\n- Payload validation [validator](https://github.com/go-playground/validator)\n- Cache [Redis](https://github.com/go-redis/redis)\n- Localization [gotext](https://github.com/leonelquinteros/gotext)\n- Database [CockroachDB](https://github.com/cockroachdb/cockroach)\n- Releasing [goreleaser](https://github.com/goreleaser/goreleaser)\n\n## Frontend\n\n- Server side templating [Go Templates](https://golang.org/pkg/text/template/)\n- Module Bundler [Parcel bundler](https://github.com/parcel-bundler/parcel)\n- Javascript UI library [React](https://github.com/facebook/react)\n\n# Getting Started\n\n## Project structure\n\nFollows https://github.com/golang-standards/project-layout\n\n## Building From Source\n\nThis project requires Go +1.13 and Go module support.\n\nTo build the project run:\n\n```\nmake\n```\n\n## Bootstrap infrastructure and run application\n\nThis project requires docker and docker compose to run the required services.\n\n1. To run the services:\n\n```\ndocker-compose up\n```\n\n2. To create database\n\n```\ndocker run --network=\"host\" -it cockroachdb/cockroach:v19.2.1 sql --insecure -e \"$(cat ./scripts/create.db.sql)\"\n```\n\n3. Build [web application](ui/README.md)\n\n4. Start server\n\n```\ngo run main.go\n```\n\n5. Navigate to users list [page](http://127.0.0.1/users)\n\n## CI and Static Analysis\n\n### CI\n\nAll pull requests will run through CI, which is currently hosted by Github-CI.\nCommunity contributors should be able to see the outcome of this process by looking at the checks on their PR.\nPlease fix any issues to ensure a prompt review from members of the team.\n\n### Static Analysis\n\nThis project uses the following static analysis tools.\nFailure during the running of any of these tools results in a failed build.\nGenerally, code must be adjusted to satisfy these tools, though there are exceptions.\n\n- [go vet](https://golang.org/cmd/vet/) checks for Go code that should be considered incorrect.\n- [go fmt](https://golang.org/cmd/gofmt/) checks that Go code is correctly formatted.\n- [golangci-lint](https://github.com/golangci/golangci-lintt) checks for things like: unused code, code that can be simplified, code that is incorrect and code that will have performance issues.\n- [go mod tidy](https://tip.golang.org/cmd/go/#hdr-Add_missing_and_remove_unused_modules) ensures that the source code and go.mod agree.\n\n# Releasing\n\nWhen a new tag is pushed, the version is released with [goreleaser](https://github.com/goreleaser/goreleaser).\n\n```\n$ git tag -a v0.1.0 -m \"First release\"\n$ git push origin v0.1.0 # => want to release v0.1.0\n```\n\n# Tooling\n\n- IDE plugin [vscode-go](https://github.com/Microsoft/vscode-go)\n- Administration of cockroachdb [DBeaver](https://dbeaver.io/)\n- REST client [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en)\n- Go testing in the browser [go-convey](https://github.com/smartystreets/goconvey)\n- Benchmarking [bombardier](http://github.com/codesenberg/bombardier)\n\n# Documentation\n\n```\n$ godoc github.com/starptech/go-web/pkg/controller\n$ godoc -http=:6060\n```\n\nVisit localhost:6060 and search for `go-web`\n\n# Benchmarking\n\n```\n$ bombardier -c 10 -n 10000 http://localhost:8080/users\n```\n\n# Cockroachdb Cluster overview\n\nhttp://localhost:8111/\n\n## Deploy on Heroku\n\n[![Heroku Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/StarpTech/go-web)\n\n# Further reading\n\n- http://www.alexedwards.net/blog/organising-database-access\n- https://12factor.net/\n- https://dev.otto.de/2015/09/30/on-monoliths-and-microservices/\n"
  },
  {
    "path": "app.json",
    "content": "{\n  \"name\": \"go-web\",\n  \"description\": \"Modern Web Application with Golang\",\n  \"website\": \"https://github.com/StarpTech/go-web\",\n  \"repository\": \"https://github.com/StarpTech/go-web\",\n  \"logo\": \"https://github.com/StarpTech/go-web/raw/master/big-gopher.png\",\n  \"success_url\": \"/\",\n  \"keywords\": [\n    \"starter-kit\",\n    \"golang\",\n    \"frontend\",\n    \"api\"\n  ],\n  \"addons\": [\n    \"heroku-postgresql\",\n    \"heroku-redis\"\n  ],\n  \"env\": {}\n}"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"log\"\n\n\t\"github.com/caarlos0/env\"\n\t\"github.com/joho/godotenv\"\n)\n\ntype Configuration struct {\n\tAddress          string `env:\"ADDRESS\" envDefault:\":8080\"`\n\tDialect          string `env:\"DIALECT,required\" envDefault:\"postgres\"`\n\tAssetsBuildDir   string `env:\"ASSETS_BUILD_DIR\"`\n\tTemplateDir      string `env:\"TPL_DIR\"`\n\tLayoutDir        string `env:\"LAYOUT_DIR\"`\n\tRedisAddr        string `env:\"REDIS_ADDR\" envDefault:\":6379\"`\n\tRedisPwd         string `env:\"REDIS_PWD\"`\n\tConnectionString string `env:\"CONNECTION_STRING,required\"`\n\tIsProduction     bool   `env:\"PRODUCTION\"`\n\tGrayLogAddr      string `env:\"GRAYLOG_ADDR\"`\n\tRequestLogger    bool   `env:\"REQUEST_LOGGER\"`\n\tLocaleDir        string `env:\"LOCALE_DIR\" envDefault:\"locales\"`\n\tLang             string `env:\"LANG\" envDefault:\"en_US\"`\n\tLangDomain       string `env:\"LANG_DOMAIN\" envDefault:\"default\"`\n\tJwtSecret        string `env:\"JWT_SECRET,required\"`\n}\n\nfunc NewConfig(files ...string) (*Configuration, error) {\n\terr := godotenv.Load(files...)\n\n\tif err != nil {\n\t\tlog.Printf(\"No .env file could be found %q\\n\", files)\n\t}\n\n\tcfg := Configuration{}\n\terr = env.Parse(&cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &cfg, nil\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  roach1:\n    container_name: roach1\n    image: cockroachdb/cockroach:v19.2.1\n    command: start --insecure\n    ports:\n      - \"26257:26257\"\n      - \"8111:8080\"\n    volumes:\n      - ./cockroach-data/roach1:/cockroach/cockroach-data\n    networks:\n      roachnet:\n        aliases:\n          - roach1\n\n  roach2:\n    container_name: roach2\n    image: cockroachdb/cockroach:v19.2.1\n    command: start --insecure --join=roach1\n    volumes:\n      - ./cockroach-data/roach2:/cockroach/cockroach-data\n    depends_on:\n      - roach1\n    networks:\n      roachnet:\n        aliases:\n          - roach2\n\n  roach3:\n    container_name: roach3\n    image: cockroachdb/cockroach:v19.2.1\n    command: start --insecure --join=roach1\n    volumes:\n      - ./cockroach-data/roach3:/cockroach/cockroach-data\n    depends_on:\n      - roach1\n    networks:\n      roachnet:\n        aliases:\n          - roach3\n\n  redis:\n    ports:\n      - \"6379:6379\"\n    image: \"redis:alpine\"\n\nnetworks:\n  roachnet:\n    driver: bridge\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/starptech/go-web\n\ngo 1.13\n\nrequire (\n\tgithub.com/caarlos0/env v3.5.0+incompatible\n\tgithub.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect\n\tgithub.com/go-playground/locales v0.12.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.16.0 // indirect\n\tgithub.com/go-redis/redis v6.15.5+incompatible\n\tgithub.com/go-sql-driver/mysql v1.5.0 // indirect\n\tgithub.com/golang/protobuf v1.3.1 // indirect\n\tgithub.com/jinzhu/gorm v1.9.12\n\tgithub.com/jinzhu/now v1.1.1 // indirect\n\tgithub.com/joho/godotenv v1.3.0\n\tgithub.com/labstack/echo/v4 v4.1.14\n\tgithub.com/labstack/gommon v0.3.0\n\tgithub.com/leodido/go-urn v1.1.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.12 // indirect\n\tgithub.com/mattn/go-sqlite3 v2.0.2+incompatible // indirect\n\tgithub.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect\n\tgithub.com/onsi/ginkgo v1.11.0 // indirect\n\tgithub.com/onsi/gomega v1.8.1 // indirect\n\tgithub.com/prometheus/client_golang v0.9.1\n\tgithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect\n\tgithub.com/prometheus/common v0.2.0 // indirect\n\tgithub.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573 // indirect\n\tgithub.com/stretchr/testify v1.4.0\n\tgolang.org/x/crypto v0.0.0-20200117160349-530e935923ad // indirect\n\tgolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect\n\tgolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect\n\tgopkg.in/go-playground/assert.v1 v1.2.1 // indirect\n\tgopkg.in/go-playground/validator.v9 v9.31.0\n\tgopkg.in/leonelquinteros/gotext.v1 v1.3.1\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=\ngithub.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=\ngithub.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=\ngithub.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=\ngithub.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=\ngithub.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=\ngithub.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk=\ngithub.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=\ngithub.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=\ngithub.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=\ngithub.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/labstack/echo/v4 v4.1.14 h1:h8XP66UfB3tUm+L3QPw7tmwAu3pJaA/nyfHPCcz46ic=\ngithub.com/labstack/echo/v4 v4.1.14/go.mod h1:Q5KZ1vD3V5FEzjM79hjwVrC3ABr7F5IdM23bXQMRDGg=\ngithub.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=\ngithub.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=\ngithub.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=\ngithub.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=\ngithub.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=\ngithub.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U=\ngithub.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=\ngithub.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=\ngithub.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=\ngithub.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU=\ngithub.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573 h1:gAuD3LIrjkoOOPLlhGlZWZXztrQII9a9kT6HS5jFtSY=\ngithub.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=\ngithub.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=\ngithub.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=\ngolang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=\ngopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=\ngopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=\ngopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=\ngopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=\ngopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\n"
  },
  {
    "path": "internal/cache/cache.go",
    "content": "package cache\n\nimport (\n\t\"log\"\n\n\t\"github.com/go-redis/redis\"\n\t\"github.com/starptech/go-web/config\"\n)\n\nfunc NewCache(config *config.Configuration) *redis.Client {\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr:     config.RedisAddr,\n\t\tPassword: config.RedisPwd,\n\t\tDB:       0, // use default DB\n\t})\n\n\tpong, err := client.Ping().Result()\n\n\tif err != nil || pong == \"\" {\n\t\tlog.Fatalf(\"redis cache: got no PONG back %q\", err)\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "internal/context/app_context.go",
    "content": "package context\n\nimport (\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/config\"\n\t\"github.com/starptech/go-web/internal/i18n\"\n\t\"github.com/starptech/go-web/internal/store\"\n)\n\n// AppContext is the new context in the request / response cycle\n// We can use the db store, cache and central configuration\ntype AppContext struct {\n\techo.Context\n\tUserStore store.User\n\tCache     store.Cache\n\tConfig    *config.Configuration\n\tLoc       i18n.I18ner\n}\n"
  },
  {
    "path": "internal/controller/healthcheck.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/context\"\n)\n\ntype Healthcheck struct{}\n\ntype healthcheckReport struct {\n\tHealth  string          `json:\"health\"`\n\tDetails map[string]bool `json:\"details\"`\n}\n\n// GetHealthcheck returns the current functional state of the application\nfunc (ctrl Healthcheck) GetHealthcheck(c echo.Context) error {\n\tcc := c.(*context.AppContext)\n\tm := healthcheckReport{Health: \"OK\"}\n\n\tdbCheck := cc.UserStore.Ping()\n\tcacheCheck := cc.Cache.Ping()\n\n\tif dbCheck != nil {\n\t\tm.Health = \"NOT\"\n\t\tm.Details[\"db\"] = false\n\t}\n\n\tif cacheCheck != nil {\n\t\tm.Health = \"NOT\"\n\t\tm.Details[\"cache\"] = false\n\t}\n\n\treturn c.JSON(http.StatusOK, m)\n}\n"
  },
  {
    "path": "internal/controller/healthcheck_test.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHealthcheck(t *testing.T) {\n\treq := httptest.NewRequest(echo.GET, \"/.well-known/health-check\", nil)\n\trec := httptest.NewRecorder()\n\te.server.Echo.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n"
  },
  {
    "path": "internal/controller/init_test.go",
    "content": "package controller\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/gommon/log\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/starptech/go-web/config\"\n\t\"github.com/starptech/go-web/internal/core\"\n\t\"github.com/starptech/go-web/internal/models\"\n)\n\nvar e struct {\n\tconfig   *config.Configuration\n\tlogger   *log.Logger\n\tserver   *core.Server\n\ttestUser *models.User\n}\n\nfunc TestMain(m *testing.M) {\n\te.config = &config.Configuration{\n\t\tConnectionString: \"host=localhost user=gorm dbname=gorm sslmode=disable password=mypassword\",\n\t\tTemplateDir:      \"../templates/*.html\",\n\t\tLayoutDir:        \"../templates/layouts/*.html\",\n\t\tDialect:          \"postgres\",\n\t\tRedisAddr:        \":6379\",\n\t}\n\n\te.server = core.NewServer(e.config)\n\n\tsetup()\n\tcode := m.Run()\n\ttearDown()\n\n\tos.Exit(code)\n}\n\nfunc setup() {\n\tuserCtrl := &User{}\n\thealthCtrl := &Healthcheck{}\n\n\tg := e.server.Echo.Group(\"/api\")\n\tg.GET(\"/users/:id\", userCtrl.GetUserJSON)\n\n\tu := e.server.Echo.Group(\"/users\")\n\tu.GET(\"/:id\", userCtrl.GetUser)\n\n\te.server.Echo.GET(\"/.well-known/health-check\", healthCtrl.GetHealthcheck)\n\te.server.Echo.GET(\"/.well-known/metrics\", echo.WrapHandler(promhttp.Handler()))\n\n\t// test data\n\tuser := models.User{Name: \"Peter\"}\n\tmr := e.server.GetModelRegistry()\n\terr := mr.Register(user)\n\n\tif err != nil {\n\t\te.server.Echo.Logger.Fatal(err)\n\t}\n\n\tmr.AutoMigrateAll()\n\tmr.Save(&user)\n\n\te.testUser = &user\n}\n\nfunc tearDown() {\n\te.server.GetModelRegistry().AutoDropAll()\n}\n"
  },
  {
    "path": "internal/controller/metric_test.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMetric(t *testing.T) {\n\treq := httptest.NewRequest(echo.GET, \"/.well-known/metrics\", nil)\n\trec := httptest.NewRecorder()\n\te.server.Echo.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n"
  },
  {
    "path": "internal/controller/user-list.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/context\"\n\t\"github.com/starptech/go-web/internal/core/errors\"\n\t\"github.com/starptech/go-web/internal/models\"\n)\n\ntype (\n\tUserList          struct{}\n\tUserListViewModel struct {\n\t\tUsers []UserViewModel\n\t}\n)\n\nfunc (ctrl UserList) GetUsers(c echo.Context) error {\n\tcc := c.(*context.AppContext)\n\n\tusers := []models.User{}\n\n\terr := cc.UserStore.Find(&users)\n\n\tif err != nil {\n\t\tb := errors.NewBoom(errors.UserNotFound, errors.ErrorText(errors.UserNotFound), err)\n\t\tc.Logger().Error(err)\n\t\treturn c.JSON(http.StatusNotFound, b)\n\t}\n\n\tviewModel := UserListViewModel{\n\t\tUsers: make([]UserViewModel, len(users)),\n\t}\n\n\tfor index, user := range users {\n\t\tviewModel.Users[index] = UserViewModel{\n\t\t\tName: user.Name,\n\t\t\tID:   user.ID,\n\t\t}\n\t}\n\n\treturn c.Render(http.StatusOK, \"user-list.html\", viewModel)\n\n}\n"
  },
  {
    "path": "internal/controller/user.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/context\"\n\t\"github.com/starptech/go-web/internal/core/errors\"\n\t\"github.com/starptech/go-web/internal/models\"\n)\n\ntype (\n\tUser          struct{}\n\tUserViewModel struct {\n\t\tName string\n\t\tID   string\n\t}\n)\n\nfunc (ctrl User) GetUser(c echo.Context) error {\n\tcc := c.(*context.AppContext)\n\tuserID := c.Param(\"id\")\n\n\tuser := models.User{ID: userID}\n\n\terr := cc.UserStore.First(&user)\n\n\tif err != nil {\n\t\tb := errors.NewBoom(errors.UserNotFound, errors.ErrorText(errors.UserNotFound), err)\n\t\tc.Logger().Error(err)\n\t\treturn c.JSON(http.StatusNotFound, b)\n\t}\n\n\tvm := UserViewModel{\n\t\tName: user.Name,\n\t\tID:   user.ID,\n\t}\n\n\treturn c.Render(http.StatusOK, \"user.html\", vm)\n\n}\n\nfunc (ctrl User) GetUserJSON(c echo.Context) error {\n\tcc := c.(*context.AppContext)\n\tuserID := c.Param(\"id\")\n\n\tuser := models.User{ID: userID}\n\n\terr := cc.UserStore.First(&user)\n\n\tif err != nil {\n\t\tb := errors.NewBoom(errors.UserNotFound, errors.ErrorText(errors.UserNotFound), err)\n\t\tc.Logger().Error(err)\n\t\treturn c.JSON(http.StatusNotFound, b)\n\t}\n\n\tvm := UserViewModel{\n\t\tName: user.Name,\n\t\tID:   user.ID,\n\t}\n\n\treturn c.JSON(http.StatusOK, vm)\n}\n"
  },
  {
    "path": "internal/controller/user_test.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/context\"\n\t\"github.com/starptech/go-web/internal/core/middleware\"\n\t\"github.com/starptech/go-web/internal/models\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype UserFakeStore struct{}\n\nfunc (s *UserFakeStore) First(m *models.User) error {\n\treturn nil\n}\nfunc (s *UserFakeStore) Find(m *[]models.User) error {\n\treturn nil\n}\nfunc (s *UserFakeStore) Create(m *models.User) error {\n\treturn nil\n}\nfunc (s *UserFakeStore) Ping() error {\n\treturn nil\n}\n\nfunc TestUserPage(t *testing.T) {\n\treq := httptest.NewRequest(echo.GET, \"/users/\"+e.testUser.ID, nil)\n\trec := httptest.NewRecorder()\n\te.server.Echo.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n\nfunc TestUnitGetUserJson(t *testing.T) {\n\ts := echo.New()\n\tg := s.Group(\"/api\")\n\n\treq := httptest.NewRequest(echo.GET, \"/api/users/\"+e.testUser.ID, nil)\n\trec := httptest.NewRecorder()\n\n\tuserCtrl := &User{}\n\n\tcc := &context.AppContext{\n\t\tConfig:    e.config,\n\t\tUserStore: &UserFakeStore{},\n\t}\n\n\ts.Use(middleware.AppContext(cc))\n\n\tg.GET(\"/users/:id\", userCtrl.GetUserJSON)\n\ts.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n"
  },
  {
    "path": "internal/core/cache_store.go",
    "content": "package core\n\nimport (\n\t\"time\"\n\n\t\"github.com/go-redis/redis\"\n)\n\n// CacheStore simple redis implementation\ntype CacheStore struct {\n\tCache *redis.Client\n}\n\nfunc (s *CacheStore) Ping() error {\n\treturn s.Cache.Ping().Err()\n}\n\nfunc (s *CacheStore) Get(key string) (string, error) {\n\treturn s.Cache.Get(key).Result()\n}\n\nfunc (s *CacheStore) Set(key string, value interface{}, exp time.Duration) (string, error) {\n\treturn s.Cache.Set(key, value, exp).Result()\n}\n"
  },
  {
    "path": "internal/core/error_handler.go",
    "content": "package core\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/core/errors\"\n)\n\nfunc HTTPErrorHandler(err error, c echo.Context) {\n\tc.Logger().Error(err)\n\tcode := http.StatusInternalServerError\n\n\tswitch v := err.(type) {\n\tcase *echo.HTTPError:\n\t\terr := c.JSON(v.Code, v)\n\t\tif err != nil {\n\t\t\tc.Logger().Error(\"error handler: json encoding\", err)\n\t\t}\n\tdefault:\n\t\te := errors.NewBoom(errors.InternalError, \"Bad implementation\", nil)\n\t\terr := c.JSON(code, e)\n\t\tif err != nil {\n\t\t\tc.Logger().Error(\"error handler: json encoding\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/core/errors/boom.go",
    "content": "package errors\n\nconst (\n\tInternalError       = \"internalError\"\n\tUserNotFound        = \"userNotFound\"\n\tInvalidBindingModel = \"invalidBindingModel\"\n\tEntityCreationError = \"entityCreationError\"\n)\n\nvar errorMessage = map[string]string{\n\t\"internalError\":       \"an internal error occured\",\n\t\"userNotFound\":        \"user could not be found\",\n\t\"invalidBindingModel\": \"model could not be bound\",\n\t\"EntityCreationError\": \"could not create entity\",\n}\n\n// Booms can contain multiple boom errors\ntype Booms struct {\n\tErrors []Boom `json:\"errors\"`\n}\n\nfunc (b *Booms) Add(e Boom) {\n\tb.Errors = append(b.Errors, e)\n}\n\nfunc NewBooms() Booms {\n\treturn Booms{}\n}\n\n// boom represent the basic structure of an json error\ntype Boom struct {\n\tCode    string      `json:\"code\"`\n\tMessage string      `json:\"message\"`\n\tDetails interface{} `json:\"details\"`\n}\n\nfunc NewBoom(code, msg string, details interface{}) Boom {\n\treturn Boom{Code: code, Message: msg, Details: details}\n}\n\nfunc ErrorText(code string) string {\n\treturn errorMessage[code]\n}\n"
  },
  {
    "path": "internal/core/middleware/app_context.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/context\"\n)\n\nfunc AppContext(cc *context.AppContext) echo.MiddlewareFunc {\n\treturn func(h echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c echo.Context) error {\n\t\t\tcc.Context = c\n\t\t\treturn h(cc)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/core/router.go",
    "content": "package core\n\nimport (\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/echo/v4/middleware\"\n\t\"github.com/starptech/go-web/internal/context\"\n\tmid \"github.com/starptech/go-web/internal/core/middleware\"\n\t\"github.com/starptech/go-web/internal/i18n\"\n\n\tv \"gopkg.in/go-playground/validator.v9\"\n)\n\nfunc NewRouter(server *Server) *echo.Echo {\n\tconfig := server.config\n\te := echo.New()\n\te.Validator = &Validator{validator: v.New()}\n\n\tcc := context.AppContext{\n\t\tCache:     &CacheStore{Cache: server.cache},\n\t\tConfig:    config,\n\t\tUserStore: &UserStore{DB: server.db},\n\t\tLoc:       i18n.New(),\n\t}\n\n\te.Use(mid.AppContext(&cc))\n\n\tif config.RequestLogger {\n\t\te.Use(middleware.Logger()) // request logger\n\t}\n\n\te.Use(middleware.Recover())       // panic errors are thrown\n\te.Use(middleware.BodyLimit(\"5M\")) // limit body payload to 5MB\n\te.Use(middleware.Secure())        // provide protection against injection attacks\n\te.Use(middleware.RequestID())     // generate unique requestId\n\n\te.Use(middleware.CORSWithConfig(middleware.CORSConfig{\n\t\tAllowOrigins: []string{\"*\"},\n\t\tAllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE},\n\t}))\n\n\t// add custom error formating\n\te.HTTPErrorHandler = HTTPErrorHandler\n\n\t// Add html templates with go template syntax\n\trenderer := newTemplateRenderer(config.LayoutDir, config.TemplateDir)\n\te.Renderer = renderer\n\n\treturn e\n}\n"
  },
  {
    "path": "internal/core/server.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis\"\n\t\"github.com/jinzhu/gorm\"\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/config\"\n\t\"github.com/starptech/go-web/internal/cache\"\n\t\"github.com/starptech/go-web/internal/i18n\"\n\t\"github.com/starptech/go-web/internal/models\"\n)\n\ntype Server struct {\n\tEcho          *echo.Echo            // HTTP middleware\n\tconfig        *config.Configuration // Configuration\n\tdb            *gorm.DB              // Database connection\n\tcache         *redis.Client         // Redis cache connection\n\tmodelRegistry *models.Model         // Model registry for migration\n}\n\n// NewServer will create a new instance of the application\nfunc NewServer(config *config.Configuration) *Server {\n\tserver := &Server{}\n\tserver.config = config\n\ti18n.Configure(config.LocaleDir, config.Lang, config.LangDomain)\n\tserver.modelRegistry = models.NewModel()\n\terr := server.modelRegistry.OpenWithConfig(config)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"gorm: could not connect to db %q\", err)\n\t}\n\n\tserver.cache = cache.NewCache(config)\n\tserver.db = server.modelRegistry.DB\n\tserver.Echo = NewRouter(server)\n\n\treturn server\n}\n\n// GetDB returns gorm (ORM)\nfunc (s *Server) GetDB() *gorm.DB {\n\treturn s.db\n}\n\n// GetCache returns the current redis client\nfunc (s *Server) GetCache() *redis.Client {\n\treturn s.cache\n}\n\n// GetConfig return the current app configuration\nfunc (s *Server) GetConfig() *config.Configuration {\n\treturn s.config\n}\n\n// GetModelRegistry returns the model registry\nfunc (s *Server) GetModelRegistry() *models.Model {\n\treturn s.modelRegistry\n}\n\n// Start the http server\nfunc (s *Server) Start(addr string) error {\n\treturn s.Echo.Start(addr)\n}\n\n// ServeStaticFiles serve static files for development purpose\nfunc (s *Server) ServeStaticFiles() {\n\ts.Echo.Static(\"/assets\", s.config.AssetsBuildDir)\n}\n\n// GracefulShutdown Wait for interrupt signal\n// to gracefully shutdown the server with a timeout of 5 seconds.\nfunc (s *Server) GracefulShutdown() {\n\tquit := make(chan os.Signal, 1)\n\n\tsignal.Notify(quit, os.Interrupt)\n\t<-quit\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\t// close cache\n\tif s.cache != nil {\n\t\tcErr := s.cache.Close()\n\t\tif cErr != nil {\n\t\t\ts.Echo.Logger.Fatal(cErr)\n\t\t}\n\t}\n\n\t// close database connection\n\tif s.db != nil {\n\t\tdErr := s.db.Close()\n\t\tif dErr != nil {\n\t\t\ts.Echo.Logger.Fatal(dErr)\n\t\t}\n\t}\n\n\t// shutdown http server\n\tif err := s.Echo.Shutdown(ctx); err != nil {\n\t\ts.Echo.Logger.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "internal/core/template.go",
    "content": "package core\n\nimport (\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"log\"\n\t\"path/filepath\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/starptech/go-web/internal/i18n\"\n)\n\nvar mainTmpl = `{{define \"main\" }} {{ template \"base\" . }} {{ end }}`\n\ntype templateRenderer struct {\n\ttemplates map[string]*template.Template\n}\n\n// NewTemplateRenderer creates a new setup to render layout based go templates\nfunc newTemplateRenderer(layoutsDir, templatesDir string) *templateRenderer {\n\tr := &templateRenderer{}\n\tr.templates = make(map[string]*template.Template)\n\tr.Load(layoutsDir, templatesDir)\n\treturn r\n}\n\nfunc (t *templateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {\n\ttmpl, ok := t.templates[name]\n\tif !ok {\n\t\tc.Logger().Fatalf(\"the template %s does not exist\", name)\n\t\treturn fmt.Errorf(\"the template %s does not exist\", name)\n\t}\n\n\treturn tmpl.ExecuteTemplate(w, \"base\", data)\n}\n\nfunc (t *templateRenderer) Load(layoutsDir, templatesDir string) {\n\tlayouts, err := filepath.Glob(layoutsDir)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tincludes, err := filepath.Glob(templatesDir)\n\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfuncMap := template.FuncMap{\n\t\t\"Loc\": i18n.Get,\n\t}\n\n\tmainTemplate := template.New(\"main\")\n\tmainTemplate.Funcs(funcMap)\n\n\tmainTemplate, err = mainTemplate.Parse(mainTmpl)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfor _, file := range includes {\n\t\tfileName := filepath.Base(file)\n\t\tfiles := append(layouts, file)\n\t\tt.templates[fileName], err = mainTemplate.Clone()\n\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tt.templates[fileName] = template.Must(t.templates[fileName].ParseFiles(files...))\n\t}\n}\n"
  },
  {
    "path": "internal/core/user_store.go",
    "content": "package core\n\nimport (\n\t\"github.com/jinzhu/gorm\"\n\t\"github.com/starptech/go-web/internal/models\"\n)\n\n// UserStore implements the UserStore interface\ntype UserStore struct {\n\tDB *gorm.DB\n}\n\nfunc (s *UserStore) First(m *models.User) error {\n\treturn s.DB.First(m).Error\n}\n\nfunc (s *UserStore) Create(m *models.User) error {\n\treturn s.DB.Create(m).Error\n}\n\nfunc (s *UserStore) Find(m *[]models.User) error {\n\treturn s.DB.Find(m).Error\n}\n\nfunc (s *UserStore) Ping() error {\n\treturn s.DB.DB().Ping()\n}\n"
  },
  {
    "path": "internal/core/validator.go",
    "content": "package core\n\nimport (\n\tvalidator \"gopkg.in/go-playground/validator.v9\"\n)\n\ntype Validator struct {\n\tvalidator *validator.Validate\n}\n\nfunc (v *Validator) Validate(i interface{}) error {\n\treturn v.validator.Struct(i)\n}\n"
  },
  {
    "path": "internal/i18n/i18n.go",
    "content": "package i18n\n\nimport gotext \"gopkg.in/leonelquinteros/gotext.v1\"\n\ntype I18ner interface {\n\tGet(string, ...interface{}) string\n}\n\ntype I18n struct{}\n\nfunc New() *I18n {\n\treturn &I18n{}\n}\n\nfunc Configure(lib, lang, dom string) {\n\tgotext.Configure(lib, lang, dom)\n}\n\nfunc (i *I18n) Get(str string, vars ...interface{}) string {\n\treturn gotext.Get(str, vars...)\n}\n\nfunc Get(str string, vars ...interface{}) string {\n\treturn gotext.Get(str, vars...)\n}\n"
  },
  {
    "path": "internal/models/models.go",
    "content": "package models\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/jinzhu/gorm\"\n\t_ \"github.com/jinzhu/gorm/dialects/postgres\"\n\t\"github.com/starptech/go-web/config\"\n)\n\n// Model facilitate database interactions\ntype Model struct {\n\tmodels map[string]reflect.Value\n\tisOpen bool\n\t*gorm.DB\n}\n\n// NewModel returns a new Model without opening database connection\nfunc NewModel() *Model {\n\treturn &Model{\n\t\tmodels: make(map[string]reflect.Value),\n\t}\n}\n\n// IsOpen returns true if the Model has already established connection\n// to the database\nfunc (m *Model) IsOpen() bool {\n\treturn m.isOpen\n}\n\n// OpenWithConfig opens database connection with the settings found in cfg\nfunc (m *Model) OpenWithConfig(cfg *config.Configuration) error {\n\tdb, err := gorm.Open(cfg.Dialect, cfg.ConnectionString)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// https://github.com/go-sql-driver/mysql/issues/461\n\tdb.DB().SetConnMaxLifetime(time.Minute * 5)\n\tdb.DB().SetMaxIdleConns(0)\n\tdb.DB().SetMaxOpenConns(20)\n\n\tm.DB = db\n\tm.isOpen = true\n\treturn nil\n}\n\n// Register adds the values to the models registry\nfunc (m *Model) Register(values ...interface{}) error {\n\n\t// do not work on them.models first, this is like an insurance policy\n\t// whenever we encounter any error in the values nothing goes into the registry\n\tmodels := make(map[string]reflect.Value)\n\tif len(values) > 0 {\n\t\tfor _, val := range values {\n\t\t\trVal := reflect.ValueOf(val)\n\t\t\tif rVal.Kind() == reflect.Ptr {\n\t\t\t\trVal = rVal.Elem()\n\t\t\t}\n\t\t\tswitch rVal.Kind() {\n\t\t\tcase reflect.Struct:\n\t\t\t\tmodels[getTypeName(rVal.Type())] = reflect.New(rVal.Type())\n\t\t\tdefault:\n\t\t\t\treturn errors.New(\"models must be structs\")\n\t\t\t}\n\t\t}\n\t}\n\tfor k, v := range models {\n\t\tm.models[k] = v\n\t}\n\treturn nil\n}\n\n// AutoMigrateAll runs migrations for all the registered models\nfunc (m *Model) AutoMigrateAll() {\n\tfor _, v := range m.models {\n\t\tm.AutoMigrate(v.Interface())\n\t}\n}\n\n// AutoDropAll drops all tables of all registered models\nfunc (m *Model) AutoDropAll() {\n\tfor _, v := range m.models {\n\t\tm.DropTableIfExists(v.Interface())\n\t}\n}\n\nfunc getTypeName(typ reflect.Type) string {\n\tif typ.Name() != \"\" {\n\t\treturn typ.Name()\n\t}\n\tsplit := strings.Split(typ.String(), \".\")\n\treturn split[len(split)-1]\n}\n"
  },
  {
    "path": "internal/models/user.go",
    "content": "package models\n\nimport \"time\"\n\ntype User struct {\n\tID        string `gorm:\"type:uuid;primary_key;default:gen_random_uuid()\"`\n\tName      string `sql:\"type:varchar(30)\"`\n\tCreatedAt time.Time\n\tUpdatedAt time.Time\n\tDeletedAt *time.Time\n}\n"
  },
  {
    "path": "internal/store/cache.go",
    "content": "package store\n\nimport \"time\"\n\ntype Cache interface {\n\tPing() error\n\tGet(string) (string, error)\n\tSet(string, interface{}, time.Duration) (string, error)\n}\n"
  },
  {
    "path": "internal/store/user.go",
    "content": "package store\n\nimport \"github.com/starptech/go-web/internal/models\"\n\ntype User interface {\n\tFirst(m *models.User) error\n\tFind(m *[]models.User) error\n\tCreate(m *models.User) error\n\tPing() error\n}\n"
  },
  {
    "path": "locales/en/default.po",
    "content": "# msgid \"\"\n# msgstr \"\"\n# Initial comment\n# Headers below\n\"Language: en\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n# Some comment\nmsgid \"My text\"\nmsgstr \"Translated text\"\n# More comments\nmsgid \"Another string\"\nmsgstr \"\""
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/starptech/go-web/config\"\n\t\"github.com/starptech/go-web/internal/controller\"\n\t\"github.com/starptech/go-web/internal/core\"\n\t\"github.com/starptech/go-web/internal/models\"\n)\n\nfunc main() {\n\tconfig, err := config.NewConfig()\n\tif err != nil {\n\t\tlog.Fatalf(\"%+v\\n\", err)\n\t}\n\t// create server\n\tserver := core.NewServer(config)\n\t// serve files for dev\n\tserver.ServeStaticFiles()\n\n\tuserCtrl := &controller.User{}\n\tuserListCtrl := &controller.UserList{}\n\thealthCtrl := &controller.Healthcheck{}\n\n\t// api endpoints\n\tg := server.Echo.Group(\"/api\")\n\tg.GET(\"/users/:id\", userCtrl.GetUserJSON)\n\n\t// pages\n\tu := server.Echo.Group(\"/users\")\n\tu.GET(\"\", userListCtrl.GetUsers)\n\tu.GET(\"/:id\", userCtrl.GetUser)\n\n\t// metric / health endpoint according to RFC 5785\n\tserver.Echo.GET(\"/.well-known/health-check\", healthCtrl.GetHealthcheck)\n\tserver.Echo.GET(\"/.well-known/metrics\", echo.WrapHandler(promhttp.Handler()))\n\n\t// migration for dev\n\tuser := models.User{Name: \"Peter\"}\n\tmr := server.GetModelRegistry()\n\terr = mr.Register(user)\n\n\tif err != nil {\n\t\tserver.Echo.Logger.Fatal(err)\n\t}\n\n\tmr.AutoMigrateAll()\n\tmr.Create(&user)\n\t// Start server\n\tgo func() {\n\t\tif err := server.Start(config.Address); err != nil {\n\t\t\tserver.Echo.Logger.Info(\"shutting down the server\")\n\t\t}\n\t}()\n\n\tserver.GracefulShutdown()\n}\n"
  },
  {
    "path": "scripts/create.db.sql",
    "content": "CREATE USER IF NOT EXISTS goweb;\nCREATE DATABASE goweb;\nGRANT ALL ON DATABASE goweb TO goweb;"
  },
  {
    "path": "web/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"modules\": false\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n}\n"
  },
  {
    "path": "web/.eslintrc.json",
    "content": "{\n  \"rules\": {},\n  \"env\": {\n    \"es6\": true,\n    \"browser\": true\n  },\n  \"parserOptions\": {\n    \"ecmaVersion\": 2018,\n    \"sourceType\": \"module\",\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    }\n  },\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:prettier/recommended\"\n  ],\n  \"globals\": {\n    \"Atomics\": \"readonly\",\n    \"SharedArrayBuffer\": \"readonly\"\n  },\n  \"plugins\": [\n    \"react\"\n  ]\n}"
  },
  {
    "path": "web/.gitignore",
    "content": "\n.cache/\ncoverage/\ndist/*\n!dist/index.html\nnode_modules/\n*.log\n\n# OS generated files\n.DS_Store\n.DS_Store?\n._*\n.Spotlight-V100\n.Trashes\nehthumbs.db\nThumbs.db\n"
  },
  {
    "path": "web/README.md",
    "content": "# Go Web\n\nWeb application for the api\n\n## Building and running on localhost\n\nFirst install dependencies:\n\n```sh\nyarn install\n```\n\nTo run in hot module reloading mode:\n\n```sh\nyarn start\n```\n\nTo create a production build:\n\n```sh\nyarn run build-prod\n```\n\n## Development\n\nRun the server and start the bundler in watch mode\n\n```sh\nyarn start\ngo run main.go\n```\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"empty-project\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"scripts\": {\n    \"clean\": \"rm -rf dist\",\n    \"start\": \"parcel watch src/app/index.js --public-url /assets/dist\",\n    \"build-prod\": \"yarn clean && parcel build src/app/index.js --public-url /assets/dist\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.12.0\",\n    \"react-dom\": \"^16.12.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.8.3\",\n    \"@babel/preset-env\": \"^7.8.3\",\n    \"@babel/preset-react\": \"^7.8.3\",\n    \"bootstrap\": \"^4.4.1\",\n    \"eslint\": \"^6.8.0\",\n    \"eslint-config-prettier\": \"^6.9.0\",\n    \"eslint-plugin-prettier\": \"^3.1.2\",\n    \"parcel-bundler\": \"^1.12.4\",\n    \"prettier\": \"^1.19.1\",\n    \"sass\": \"^1.25.1-test.1\"\n  }\n}\n"
  },
  {
    "path": "web/src/app/app.js",
    "content": "import React from \"react\";\n\nexport default function App({ message }) {\n  return (\n    <div>\n      <h1>{message}</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/components/header.js",
    "content": "import React from \"react\";\n\nexport default function Header({ message }) {\n  return (\n    <header className=\"bg-white text-white\">\n      <div className=\"container text-center text-dark\">\n        <img src=\"http://127.0.0.1:8080/assets/images/go-web.png\" />\n        <h1>{message}</h1>\n        <p className=\"lead\">Welcome to modern web development with Go</p>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "web/src/app/components/like-button.js",
    "content": "import React, { useState } from \"react\";\n\nexport default function LikeButton({ id }) {\n  const [likes, setLikes] = useState(() => parseInt(id));\n  return (\n    <button className=\"btn btn-secondary\" onClick={() => setLikes(draft => setLikes(++draft))}>\n      Like ({likes})\n    </button>\n  );\n}\n"
  },
  {
    "path": "web/src/app/index.js",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport App from \"./app\";\nimport LikeButton from \"./components/like-button\";\nimport Header from \"./components/header\";\nimport \"./styles.scss\";\n\nvar mountNode = document.getElementById(\"app\");\nReactDOM.render(<App />, mountNode);\n\ndocument.querySelectorAll(\".like-button-component\").forEach(domContainer => {\n  ReactDOM.render(<LikeButton {...domContainer.dataset} />, domContainer);\n});\ndocument.querySelectorAll(\".header-component\").forEach(domContainer => {\n  ReactDOM.render(<Header {...domContainer.dataset} />, domContainer);\n});\n"
  },
  {
    "path": "web/src/app/styles.scss",
    "content": "@import \"~bootstrap/scss/bootstrap\";\n\nheader {\n  padding: 156px 0 100px;\n}\n\nsection {\n  padding: 50px 0;\n}\n\n.bd-highlight {\n  background-color: rgb(237, 253, 255);\n  border: 1px solid rgb(106, 214, 227);\n}\n"
  },
  {
    "path": "web/src/global/app.js",
    "content": "import React from \"react\";\n\nexport default function App({ message }) {\n  return (\n    <div className=\"container\">\n      <ul className=\"nav\">\n        <li className=\"nav-item\">\n          <a className=\"nav-link active\" href=\"/\">\n            Home\n          </a>\n        </li>\n        <li className=\"nav-item\">\n          <a className=\"nav-link\" href=\"/users\">\n            Users\n          </a>\n        </li>\n      </ul>\n      <h1>{message}</h1>\n      <img src=\"http://127.0.0.1:8080/assets/images/go-web.png\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/global/global.html",
    "content": "<script src=\"index.js\"></script>\n"
  },
  {
    "path": "web/src/global/index.js",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport App from \"./app\";\nimport \"./styles.scss\";\n\nvar mountNode = document.getElementById(\"app\");\nReactDOM.render(\n  <App message=\"Welcome to modern web development with Go\" />,\n  mountNode\n);\n"
  },
  {
    "path": "web/src/global/styles.scss",
    "content": "@import \"~bootstrap/scss/bootstrap\";\n"
  },
  {
    "path": "web/templates/layouts/base.html",
    "content": "{{ define \"base\" }}\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <meta name=\"description\" content=\"\">\n  <meta name=\"author\" content=\"\">\n  <link href=\"/assets/dist/index.css\" rel=\"stylesheet\">\n  <title>{{ block \"title\" . }} {{end}}</title>\n</head>\n\n<body id=\"page-top\">\n\n  <!-- Navigation -->\n  <nav class=\"navbar navbar-expand-lg navbar-dark bg-dark fixed-top\" id=\"mainNav\">\n    <div class=\"container\">\n      <a class=\"navbar-brand js-scroll-trigger\" href=\"/users\">Go Web</a>\n      <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarResponsive\" aria-controls=\"navbarResponsive\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n        <span class=\"navbar-toggler-icon\"></span>\n      </button>\n      <div class=\"collapse navbar-collapse\" id=\"navbarResponsive\">\n        <ul class=\"navbar-nav ml-auto\">\n          <li class=\"nav-item\">\n            <a class=\"nav-link js-scroll-trigger\" href=\"#about\">About</a>\n          </li>\n          <li class=\"nav-item\">\n            <a class=\"nav-link js-scroll-trigger\" href=\"#services\">Services</a>\n          </li>\n          <li class=\"nav-item\">\n            <a class=\"nav-link js-scroll-trigger\" href=\"#contact\">Contact</a>\n          </li>\n        </ul>\n      </div>\n    </div>\n  </nav>\n\n  {{ template \"header\" . }}\n\n  <section id=\"about\">\n    <div class=\"container\">\n      <div class=\"row\">\n        <div id=\"app\"></div>\n      </div>\n      {{ template \"content\" . }}\n    </div>\n  </section>\n\n  <!-- Footer -->\n  <footer class=\"py-5 bg-dark\">\n    <div class=\"container\">\n      <p class=\"m-0 text-center text-white\">Copyright &copy; Your Website 2019</p>\n    </div>\n    <!-- /.container -->\n  </footer>\n\n  <script src=\"/assets/dist/index.js\"></script>\n\n</body>\n\n</html>\n{{ end }}\n"
  },
  {
    "path": "web/templates/pages/user-list.html",
    "content": "{{define \"title\"}}User list{{end}}\n\n{{define \"header\"}}\n<div class=\"header-component\" data-message=\"User list\"></div>\n{{end}}\n\n{{define \"content\"}}\n\n<div class=\"d-flex flex-column bd-highlight mb-3\">\n  {{range .Users}}\n  <div class=\"p-2 bd-highlight\">{{.Name}}: <a href=\"/users/{{.ID}}\">Details</a></div>\n  {{end}}\n</div>\n\n{{end}}\n"
  },
  {
    "path": "web/templates/pages/user.html",
    "content": "{{define \"title\"}}User {{.Name}}{{end}}\n\n{{define \"header\"}}\n<div class=\"header-component\" data-message=\"{{.Name}}\"></div>\n{{end}}\n\n{{define \"content\"}}\n\n<div class=\"d-flex flex-column bd-highlight mb-3\">\n  <div class=\"p-2 bd-highlight\">ID: {{.ID}}</div>\n  <div class=\"p-2 bd-highlight\">Name: {{.Name}}</div>\n  <div class=\"p-2 bd-highlight\">Translated content: {{ Loc \"My text\" }}</div>\n  <div class=\"p-2 bd-highlight\">\n    Mount server rendered html as react component:\n    <div class=\"like-button-component d-inline-block\" data-id=\"101\"></div>\n  </div>\n</div>\n\n{{end}}\n"
  }
]