Alter 17 files
Add `.gitignore` Add `LICENSE` Add `README.md` Add `go.mod` Add `go.sum` Add `config.go` Add `file.go` Add `db.go` Add `migration.go` Add `20221002195230_initialise.go` Add `migrations.go` Add `models.go` Add `endpoints.go` Add `index.go` Add `main.go` Add `paths.go` Add `richError.go`
This commit is contained in:
commit
ab92d0566e
17 changed files with 652 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 codemicro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
5
README.md
Normal file
5
README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Workboat
|
||||
|
||||
*A simple CI system for Gitea*
|
||||
|
||||
---
|
31
go.mod
Normal file
31
go.mod
Normal file
|
@ -0,0 +1,31 @@
|
|||
module github.com/codemicro/workboat
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.35.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/uptrace/bun v1.1.8
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.1.8
|
||||
github.com/uptrace/bun/extra/bundebug v1.1.8
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.15.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.38.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||
)
|
79
go.sum
Normal file
79
go.sum
Normal file
|
@ -0,0 +1,79 @@
|
|||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.35.0 h1:ct+jKw8Qb24WEIZx3VV3zz9VXyBZL7mcEjNaqj3g0h0=
|
||||
github.com/gofiber/fiber/v2 v2.35.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.1.8 h1:slxuaP4LYWFbPRUmTtQhfJN+6eX/6ar2HDKYTcI50SA=
|
||||
github.com/uptrace/bun v1.1.8/go.mod h1:iT89ESdV3uMupD9ixt6Khidht+BK0STabK/LeZE+B84=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.1.8 h1:IJ6qBLjeON21tpgmZF/V/k/oHdzAql5UrnaqMCksTlY=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.1.8/go.mod h1:IZF76cHEf8eeGA29OpkYyPYDs4l/iSMTYRyuFRqeXdY=
|
||||
github.com/uptrace/bun/extra/bundebug v1.1.8 h1:RrZNOYYFb690k14nCN0t/hokfpsgoppT55/Xk/ijvBA=
|
||||
github.com/uptrace/bun/extra/bundebug v1.1.8/go.mod h1:AXl9cPt1j3Yyu+a681xTlDyWoIBL1iSjTjr2SAU5oUY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco=
|
||||
github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
34
workboat/config/config.go
Normal file
34
workboat/config/config.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
func InitLogging() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
log.Logger = log.Logger.With().Stack().Logger()
|
||||
}
|
||||
|
||||
var Debug = struct {
|
||||
Enabled bool
|
||||
}{
|
||||
Enabled: asBool(get("debug.enable")),
|
||||
}
|
||||
|
||||
var HTTP = struct {
|
||||
Host string
|
||||
Port int
|
||||
}{
|
||||
Host: asString(withDefault("http.host", "0.0.0.0")),
|
||||
Port: asInt(withDefault("http.port", 8080)),
|
||||
}
|
||||
|
||||
var Database = struct {
|
||||
Filename string
|
||||
}{
|
||||
Filename: asString(withDefault("db.filename", "database.db")),
|
||||
}
|
120
workboat/config/file.go
Normal file
120
workboat/config/file.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const configFileName = "config.yml"
|
||||
|
||||
var (
|
||||
rawConfigFileContents map[string]any
|
||||
lastKey string
|
||||
)
|
||||
|
||||
func mustLoadConfigFile() {
|
||||
if err := loadConfigFile(); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigFile() error {
|
||||
if rawConfigFileContents != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fcont, err := os.ReadFile(configFileName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load config file")
|
||||
}
|
||||
rawConfigFileContents = make(map[string]any)
|
||||
if err := yaml.Unmarshal(fcont, &rawConfigFileContents); err != nil {
|
||||
return errors.Wrap(err, "could not unmarshal config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Reload() error {
|
||||
return loadConfigFile()
|
||||
}
|
||||
|
||||
type optionalItem struct {
|
||||
item any
|
||||
found bool
|
||||
}
|
||||
|
||||
var indexedPartRegexp = regexp.MustCompile(`(?m)([a-zA-Z]+)(?:\[(\d+)\])?`)
|
||||
|
||||
func get(key string) optionalItem {
|
||||
// http[2].bananas
|
||||
mustLoadConfigFile()
|
||||
lastKey = key
|
||||
|
||||
parts := strings.Split(key, ".")
|
||||
var cursor any = rawConfigFileContents
|
||||
for _, part := range parts {
|
||||
components := indexedPartRegexp.FindStringSubmatch(part)
|
||||
key := components[1]
|
||||
index, _ := strconv.ParseInt(components[2], 10, 32)
|
||||
isIndexed := components[2] != ""
|
||||
|
||||
item, found := cursor.(map[string]any)[key]
|
||||
if !found {
|
||||
return optionalItem{nil, false}
|
||||
}
|
||||
|
||||
if isIndexed {
|
||||
arr, conversionOk := item.([]any)
|
||||
if !conversionOk {
|
||||
log.Fatal().Msgf("attempted to index non-indexable config item %s", key)
|
||||
}
|
||||
cursor = arr[index]
|
||||
} else {
|
||||
cursor = item
|
||||
}
|
||||
}
|
||||
return optionalItem{cursor, true}
|
||||
}
|
||||
|
||||
func required(key string) optionalItem {
|
||||
opt := get(key)
|
||||
if !opt.found {
|
||||
log.Fatal().Msgf("required key %s not found in config file", lastKey)
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
func withDefault(key string, defaultValue any) optionalItem {
|
||||
opt := get(key)
|
||||
if !opt.found {
|
||||
return optionalItem{item: defaultValue, found: true}
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
func asInt(x optionalItem) int {
|
||||
if !x.found {
|
||||
return 0
|
||||
}
|
||||
return x.item.(int)
|
||||
}
|
||||
|
||||
func asString(x optionalItem) string {
|
||||
if !x.found {
|
||||
return ""
|
||||
}
|
||||
return x.item.(string)
|
||||
}
|
||||
|
||||
func asBool(x optionalItem) bool {
|
||||
if !x.found {
|
||||
return false
|
||||
}
|
||||
return x.item.(bool)
|
||||
}
|
48
workboat/db/db.go
Normal file
48
workboat/db/db.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"github.com/codemicro/workboat/workboat/config"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/extra/bundebug"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
pool *sql.DB
|
||||
bun *bun.DB
|
||||
ContextTimeout time.Duration
|
||||
}
|
||||
|
||||
func New() (*DB, error) {
|
||||
dsn := config.Database.Filename
|
||||
log.Info().Msg("connecting to database")
|
||||
db, err := sql.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not open SQL connection")
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(1) // https://github.com/mattn/go-sqlite3/issues/274#issuecomment-191597862
|
||||
|
||||
bundb := bun.NewDB(db, sqlitedialect.New())
|
||||
bundb.AddQueryHook(bundebug.NewQueryHook(
|
||||
bundebug.WithEnabled(config.Debug.Enabled),
|
||||
))
|
||||
|
||||
rtn := &DB{
|
||||
pool: db,
|
||||
bun: bundb,
|
||||
ContextTimeout: time.Second,
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (db *DB) newContext() (context.Context, func()) {
|
||||
return context.WithTimeout(context.Background(), db.ContextTimeout)
|
||||
}
|
42
workboat/db/migration.go
Normal file
42
workboat/db/migration.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"github.com/codemicro/workboat/workboat/db/migrations"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
log.Info().Msg("running migrations")
|
||||
|
||||
migs, err := migrations.GetMigrations()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
mig := migrate.NewMigrator(db.bun, migs)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := mig.Init(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
group, err := mig.Migrate(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if group.IsZero() {
|
||||
log.Info().Msg("database up to date")
|
||||
} else {
|
||||
log.Info().Msg("migrations applied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
37
workboat/db/migrations/20221002195230_initialise.go
Normal file
37
workboat/db/migrations/20221002195230_initialise.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/codemicro/workboat/workboat/db/models"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tps := []any{
|
||||
(*models.User)(nil),
|
||||
}
|
||||
|
||||
mig.MustRegister(func(ctx context.Context, db *bun.DB) error {
|
||||
logger.Debug().Msg("1 up")
|
||||
|
||||
for _, t := range tps {
|
||||
if _, err := db.NewCreateTable().Model(t).Exec(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(ctx context.Context, db *bun.DB) error {
|
||||
logger.Debug().Msg("1 down")
|
||||
|
||||
for _, t := range tps {
|
||||
if _, err := db.NewDropTable().Model(t).Exec(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
20
workboat/db/migrations/migrations.go
Normal file
20
workboat/db/migrations/migrations.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
// //go:embed *.sql
|
||||
// var files embed.FS
|
||||
|
||||
var mig = migrate.NewMigrations()
|
||||
|
||||
var logger = log.Logger.With().Str("location", "migrations").Logger()
|
||||
|
||||
func GetMigrations() (*migrate.Migrations, error) {
|
||||
// if err := mig.Discover(files); err != nil {
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
return mig, nil
|
||||
}
|
13
workboat/db/models/models.go
Normal file
13
workboat/db/models/models.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
bun.BaseModel `bun:"table:users"`
|
||||
|
||||
ID uuid.UUID `bun:"id,pk,type:varchar"`
|
||||
EmailAddress string `bun:"email_address,notnull"`
|
||||
}
|
34
workboat/endpoints/endpoints.go
Normal file
34
workboat/endpoints/endpoints.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"github.com/codemicro/workboat/workboat/config"
|
||||
"github.com/codemicro/workboat/workboat/db"
|
||||
"github.com/codemicro/workboat/workboat/paths"
|
||||
"github.com/codemicro/workboat/workboat/util"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func New(dbi *db.DB) *Endpoints {
|
||||
return &Endpoints{
|
||||
db: dbi,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Endpoints) SetupApp() *fiber.App {
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: util.JSONErrorHandler,
|
||||
DisableStartupMessage: !config.Debug.Enabled,
|
||||
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
})
|
||||
|
||||
app.Get(paths.Index, e.Index)
|
||||
|
||||
return app
|
||||
}
|
7
workboat/endpoints/index.go
Normal file
7
workboat/endpoints/index.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package endpoints
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
func (e *Endpoints) Index(ctx *fiber.Ctx) error {
|
||||
return ctx.SendString("Hello world!")
|
||||
}
|
57
workboat/main.go
Normal file
57
workboat/main.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codemicro/workboat/workboat/config"
|
||||
"github.com/codemicro/workboat/workboat/db"
|
||||
"github.com/codemicro/workboat/workboat/endpoints"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func run() error {
|
||||
database, err := db.New()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := database.Migrate(); err != nil {
|
||||
return errors.Wrap(err, "failed migration")
|
||||
}
|
||||
|
||||
e := endpoints.New(database)
|
||||
app := e.SetupApp()
|
||||
|
||||
serveAddr := config.HTTP.Host + ":" + strconv.Itoa(config.HTTP.Port)
|
||||
|
||||
go func() {
|
||||
shutdownNotifier := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdownNotifier, syscall.SIGINT)
|
||||
<-shutdownNotifier
|
||||
if err := app.Shutdown(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to shutdown server on SIGINT")
|
||||
log.Fatal().Msg("terminating")
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info().Msgf("starting server on %s", serveAddr)
|
||||
|
||||
if err := app.Listen(serveAddr); err != nil {
|
||||
return errors.Wrap(err, "fiber server run failed")
|
||||
}
|
||||
|
||||
log.Info().Msg("shutting down...")
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.InitLogging()
|
||||
if err := run(); err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
log.Error().Stack().Err(err).Msg("failed to run")
|
||||
}
|
||||
}
|
20
workboat/paths/paths.go
Normal file
20
workboat/paths/paths.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Index = "/"
|
||||
)
|
||||
|
||||
func Make(path string, replacements ...any) string {
|
||||
x := strings.Split(path, "/")
|
||||
for i, p := range x {
|
||||
if strings.HasPrefix(p, ":") {
|
||||
x[i] = "%s"
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(strings.Join(x, "/"), replacements...)
|
||||
}
|
63
workboat/util/richError.go
Normal file
63
workboat/util/richError.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type RichError struct {
|
||||
Status int
|
||||
Reason string
|
||||
Detail any
|
||||
}
|
||||
|
||||
func NewRichError(status int, reason string, detail any) error {
|
||||
return &RichError{
|
||||
Status: status,
|
||||
Reason: reason,
|
||||
Detail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRichErrorFromFiberError(err *fiber.Error, detail any) error {
|
||||
return NewRichError(err.Code, err.Message, detail)
|
||||
}
|
||||
|
||||
func (r *RichError) Error() string {
|
||||
return fmt.Sprintf("handler error, %d: %s", r.Status, r.Reason)
|
||||
}
|
||||
|
||||
func (r *RichError) AsJSON() ([]byte, error) {
|
||||
info := map[string]any{
|
||||
"status": "error",
|
||||
"message": r.Reason,
|
||||
}
|
||||
|
||||
if r.Detail != nil {
|
||||
info["detail"] = r.Detail
|
||||
}
|
||||
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
func JSONErrorHandler(ctx *fiber.Ctx, err error) error {
|
||||
var re *RichError
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
re = NewRichErrorFromFiberError(e, nil).(*RichError)
|
||||
} else if e, ok := err.(*RichError); ok {
|
||||
re = e
|
||||
} else {
|
||||
log.Error().Stack().Err(err).Str("location", "fiber error handler").Str("route", ctx.OriginalURL()).Send()
|
||||
re = NewRichErrorFromFiberError(fiber.ErrInternalServerError, nil).(*RichError)
|
||||
}
|
||||
jsonBytes, err := re.AsJSON()
|
||||
if err != nil {
|
||||
jsonBytes = []byte(`{"status":"error","message":"Internal Server Error","detail":"unable to produce detailed description"}`)
|
||||
log.Error().Err(err).Str("location", "fiber error handler").Msg("unable to produce error response")
|
||||
}
|
||||
ctx.Status(re.Status)
|
||||
ctx.Type("json")
|
||||
return ctx.Send(jsonBytes)
|
||||
}
|
Reference in a new issue