From 4fb161b3c6c661d188ea3cb1c1940c1fb1b35b76 Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Sun, 21 Mar 2021 15:38:37 -0500 Subject: [PATCH] Rework DB query stuff, use for projects --- .gitignore | 1 + go.mod | 8 +-- go.sum | 27 +++++++--- src/ansicolor/ansicolor.go | 2 +- src/config/types.go | 7 ++- src/db/db.go | 98 ++++++++++++++++++++++++++++++++-- src/logging/logging.go | 26 ++++++--- src/models/project.go | 19 +++++++ src/website/requesthandling.go | 57 ++++++++++++++++---- src/website/routes.go | 75 ++++++++++++++++++++++---- src/website/website.go | 22 ++------ 11 files changed, 280 insertions(+), 62 deletions(-) create mode 100644 src/models/project.go diff --git a/.gitignore b/.gitignore index 8d5afc8..82023d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ src/config/config.go .vscode +vendor/ diff --git a/go.mod b/go.mod index 134e3a7..8212fd5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/Masterminds/sprig v2.22.0+incompatible github.com/go-stack/stack v1.8.0 github.com/google/uuid v1.2.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect @@ -15,7 +15,9 @@ require ( github.com/mitchellh/copystructure v1.1.1 // indirect github.com/rs/zerolog v1.20.0 github.com/spf13/cobra v1.1.3 - github.com/stretchr/testify v1.7.0 // indirect - github.com/teacat/noire v1.1.0 // indirect + github.com/stretchr/testify v1.7.0 + github.com/teacat/noire v1.1.0 github.com/wellington/go-libsass v0.9.2 ) + +replace github.com/rs/zerolog v1.20.0 => github.com/bvisness/zerolog v1.20.1-0.20210321191248-05f63bf0e9e0 diff --git a/go.sum b/go.sum index 1b92dda..c7e9a32 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bvisness/zerolog v1.20.1-0.20210321191248-05f63bf0e9e0 h1:2FrBN+MEMyGgts3Sm9M+6sMR6cMYEZ8+4fvotGFtRNE= +github.com/bvisness/zerolog v1.20.1-0.20210321191248-05f63bf0e9e0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= @@ -175,9 +177,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -216,8 +220,9 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -235,8 +240,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= -github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -267,7 +270,6 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= @@ -279,6 +281,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/wellington/go-libsass v0.9.2 h1:6Ims04UDdBs6/CGSVK5JC8FNikR5ssrsMMKE/uaO5Q8= github.com/wellington/go-libsass v0.9.2/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -299,6 +302,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -320,6 +324,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -334,8 +339,9 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -344,6 +350,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -364,6 +371,8 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -387,17 +396,20 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -421,6 +433,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= diff --git a/src/ansicolor/ansicolor.go b/src/ansicolor/ansicolor.go index f2d23d6..b0e218d 100644 --- a/src/ansicolor/ansicolor.go +++ b/src/ansicolor/ansicolor.go @@ -1,4 +1,4 @@ -package color +package ansicolor import "runtime" diff --git a/src/config/types.go b/src/config/types.go index b935cad..84c3047 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -1,6 +1,10 @@ package config -import "fmt" +import ( + "fmt" + + "github.com/jackc/pgx/v4" +) type Environment string @@ -23,6 +27,7 @@ type PostgresConfig struct { Hostname string Port int DbName string + LogLevel pgx.LogLevel } func (info PostgresConfig) DSN() string { diff --git a/src/db/db.go b/src/db/db.go index 09d8756..ad8fef7 100644 --- a/src/db/db.go +++ b/src/db/db.go @@ -2,11 +2,17 @@ package db import ( "context" + "errors" + "reflect" + "strings" "git.handmade.network/hmn/hmn/src/config" + "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/oops" "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/log/zerologadapter" "github.com/jackc/pgx/v4/pgxpool" + "github.com/rs/zerolog/log" ) func NewConn() *pgx.Conn { @@ -19,15 +25,99 @@ func NewConn() *pgx.Conn { } func NewConnPool(minConns, maxConns int32) *pgxpool.Pool { - config, err := pgxpool.ParseConfig(config.Config.Postgres.DSN()) + cfg, err := pgxpool.ParseConfig(config.Config.Postgres.DSN()) - config.MinConns = minConns - config.MaxConns = maxConns + cfg.MinConns = minConns + cfg.MaxConns = maxConns + cfg.ConnConfig.Logger = zerologadapter.NewLogger(log.Logger) + cfg.ConnConfig.LogLevel = config.Config.Postgres.LogLevel - conn, err := pgxpool.ConnectConfig(context.Background(), config) + conn, err := pgxpool.ConnectConfig(context.Background(), cfg) if err != nil { panic(oops.New(err, "failed to create database connection pool")) } return conn } + +type StructQueryIterator struct { + fieldIndices []int + rows pgx.Rows +} + +func (it *StructQueryIterator) Next(dest interface{}) bool { + hasNext := it.rows.Next() + if !hasNext { + return false + } + + v := reflect.ValueOf(dest) + if v.Kind() != reflect.Ptr { + panic(oops.New(nil, "Next requires a pointer type; got %v", v.Kind())) + } + + vals, err := it.rows.Values() + if err != nil { + panic(err) + } + + for i, val := range vals { + field := v.Elem().Field(it.fieldIndices[i]) + switch field.Kind() { + case reflect.Int: + field.SetInt(reflect.ValueOf(val).Int()) + default: + field.Set(reflect.ValueOf(val)) + } + } + + return true +} + +func (it *StructQueryIterator) Close() { + it.rows.Close() +} + +func QueryToStructs(ctx context.Context, conn *pgxpool.Pool, destType interface{}, query string, args ...interface{}) (StructQueryIterator, error) { + var fieldIndices []int + var columnNames []string + + t := reflect.TypeOf(models.Project{}) + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if columnName := f.Tag.Get("db"); columnName != "" { + fieldIndices = append(fieldIndices, i) + columnNames = append(columnNames, columnName) + } + } + + columnNamesString := strings.Join(columnNames, ", ") + query = strings.Replace(query, "$columns", columnNamesString, -1) + + rows, err := conn.Query(ctx, query, args...) + if err != nil { + return StructQueryIterator{}, err + } + + return StructQueryIterator{ + fieldIndices: fieldIndices, + rows: rows, + }, nil +} + +var ErrNoMatchingRows = errors.New("no matching rows") + +func QueryOneToStruct(ctx context.Context, conn *pgxpool.Pool, dest interface{}, query string, args ...interface{}) error { + rows, err := QueryToStructs(ctx, conn, dest, query, args...) + if err != nil { + return err + } + defer rows.Close() + + hasRow := rows.Next(dest) + if !hasRow { + return ErrNoMatchingRows + } + + return nil +} diff --git a/src/logging/logging.go b/src/logging/logging.go index b022e8b..42b9adc 100644 --- a/src/logging/logging.go +++ b/src/logging/logging.go @@ -18,6 +18,10 @@ func init() { log.Logger = log.Output(NewPrettyZerologWriter()) } +func GlobalLogger() *zerolog.Logger { + return &log.Logger +} + func Trace() *zerolog.Event { return log.Trace().Timestamp().Stack() } @@ -47,7 +51,7 @@ func Fatal() *zerolog.Event { } func With() zerolog.Context { - return log.With() + return log.With().Stack() } type PrettyZerologWriter struct { @@ -169,12 +173,20 @@ func (w *PrettyZerologWriter) Write(p []byte) (int, error) { return os.Stderr.Write([]byte(b.String())) } -func LogPanics() { +func LogPanics(logger *zerolog.Logger) { if r := recover(); r != nil { - if err, ok := r.(error); ok { - Error().Err(err).Msg("recovered from panic") - } else { - Error().Interface("recovered", r).Msg("recovered from panic") - } + LogPanicValue(logger, r, "recovered from panic") + } +} + +func LogPanicValue(logger *zerolog.Logger, val interface{}, msg string) { + if logger == nil { + logger = GlobalLogger() + } + + if err, ok := val.(error); ok { + logger.Error().Err(err).Msg(msg) + } else { + logger.Error().Interface("recovered", val).Msg(msg) } } diff --git a/src/models/project.go b/src/models/project.go new file mode 100644 index 0000000..0cf88d1 --- /dev/null +++ b/src/models/project.go @@ -0,0 +1,19 @@ +package models + +const HMNProjectID = 1 + +type Project struct { + ID int `db:"id"` + + Slug string `db:"slug"` + Name string `db:"name"` + Blurb string `db:"blurb"` + Description string `db:"description"` + + Color1 string `db:"color_1"` + Color2 string `db:"color_2"` +} + +func (p *Project) IsHMN() bool { + return p.ID == HMNProjectID +} diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go index 30a3036..9097c0e 100644 --- a/src/website/requesthandling.go +++ b/src/website/requesthandling.go @@ -8,6 +8,7 @@ import ( "net/url" "git.handmade.network/hmn/hmn/src/logging" + "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/templates" "github.com/julienschmidt/httprouter" "github.com/rs/zerolog" @@ -15,6 +16,7 @@ import ( type HMNRouter struct { HttpRouter *httprouter.Router + Wrappers []HMNHandlerWrapper } func (r *HMNRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -22,6 +24,9 @@ func (r *HMNRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } func (r *HMNRouter) Handle(method, route string, handler HMNHandler) { + for i := len(r.Wrappers) - 1; i >= 0; i-- { + handler = r.Wrappers[i](handler) + } r.HttpRouter.Handle(method, route, handleHmnHandler(route, handler)) } @@ -29,38 +34,52 @@ func (r *HMNRouter) GET(route string, handler HMNHandler) { r.Handle(http.MethodGet, route, handler) } +// TODO: POST, etc. + func (r *HMNRouter) ServeFiles(path string, root http.FileSystem) { r.HttpRouter.ServeFiles(path, root) } +func (r *HMNRouter) WithWrappers(wrappers ...HMNHandlerWrapper) *HMNRouter { + result := *r + result.Wrappers = append(result.Wrappers, wrappers...) + return &result +} + type HMNHandler func(c *RequestContext, p httprouter.Params) +type HMNHandlerWrapper func(h HMNHandler) HMNHandler type RequestContext struct { StatusCode int - Body io.ReadWriter - Logger zerolog.Context + Body *bytes.Buffer + Logger *zerolog.Logger + Req *http.Request + Errors []error - rw http.ResponseWriter - req *http.Request + rw http.ResponseWriter + + currentProject *models.Project } func newRequestContext(rw http.ResponseWriter, req *http.Request, route string) *RequestContext { + logger := logging.With().Str("route", route).Logger() + return &RequestContext{ StatusCode: http.StatusOK, Body: new(bytes.Buffer), - Logger: logging.With().Str("route", route), + Logger: &logger, + Req: req, - rw: rw, - req: req, + rw: rw, } } func (c *RequestContext) Context() context.Context { - return c.req.Context() + return c.Req.Context() } func (c *RequestContext) URL() *url.URL { - return c.req.URL + return c.Req.URL } func (c *RequestContext) Headers() http.Header { @@ -71,9 +90,29 @@ func (c *RequestContext) WriteTemplate(name string, data interface{}) error { return templates.Templates[name].Execute(c.Body, data) } +func (c *RequestContext) AddErrors(errs ...error) { + c.Errors = append(c.Errors, errs...) +} + +func (c *RequestContext) AbortWithErrors(status int, errs ...error) { + c.StatusCode = status + c.AddErrors(errs...) +} + func handleHmnHandler(route string, h HMNHandler) httprouter.Handle { return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { c := newRequestContext(rw, r, route) + defer func() { + /* + This panic recovery is the last resort. If you want to render + an error page or something, make it a request wrapper. + */ + if recovered := recover(); recovered != nil { + rw.WriteHeader(http.StatusInternalServerError) + logging.LogPanicValue(c.Logger, recovered, "request panicked and was not handled") + } + }() + h(c, p) rw.WriteHeader(c.StatusCode) diff --git a/src/website/routes.go b/src/website/routes.go index f887624..19366e3 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -2,10 +2,14 @@ package website import ( "context" + "errors" "fmt" "net/http" + "strings" - "git.handmade.network/hmn/hmn/src/logging" + "git.handmade.network/hmn/hmn/src/db" + "git.handmade.network/hmn/hmn/src/models" + "git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/templates" "github.com/jackc/pgx/v4/pgxpool" "github.com/julienschmidt/httprouter" @@ -23,21 +27,24 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler { conn: conn, } - routes.GET("/", routes.Index) - routes.GET("/project/:id", routes.Project) - routes.GET("/assets/project.css", routes.ProjectCSS) + mainRoutes := routes.WithWrappers(routes.CommonWebsiteDataWrapper) + mainRoutes.GET("/", routes.Index) + mainRoutes.GET("/project/:id", routes.Project) + mainRoutes.GET("/assets/project.css", routes.ProjectCSS) + routes.ServeFiles("/public/*filepath", http.Dir("public")) return routes } -func (s *websiteRoutes) Index(c *RequestContext, p httprouter.Params) { - err := c.WriteTemplate("index.html", templates.BaseData{ +func (s *websiteRoutes) getBaseData(c *RequestContext) templates.BaseData { + return templates.BaseData{ Project: templates.Project{ - Name: "Handmade Network", - Color: "cd4e31", + Name: c.currentProject.Name, + Subdomain: c.currentProject.Slug, + Color: c.currentProject.Color1, - IsHMN: true, + IsHMN: c.currentProject.IsHMN(), HasBlog: true, HasForum: true, @@ -45,7 +52,33 @@ func (s *websiteRoutes) Index(c *RequestContext, p httprouter.Params) { HasLibrary: true, }, Theme: "dark", - }) + } +} + +func FetchProjectBySlug(ctx context.Context, conn *pgxpool.Pool, slug string) (*models.Project, error) { + var subdomainProject models.Project + err := db.QueryOneToStruct(ctx, conn, &subdomainProject, "SELECT $columns FROM handmade_project WHERE slug = $1", slug) + if err == nil { + return &subdomainProject, nil + } else if !errors.Is(err, db.ErrNoMatchingRows) { + return nil, oops.New(err, "failed to get projects by slug") + } + + var defaultProject models.Project + err = db.QueryOneToStruct(ctx, conn, &defaultProject, "SELECT $columns FROM handmade_project WHERE id = $1", models.HMNProjectID) + if err != nil { + if errors.Is(err, db.ErrNoMatchingRows) { + return nil, oops.New(nil, "default project didn't exist in the database") + } else { + return nil, oops.New(err, "failed to get default project") + } + } + + return &defaultProject, nil +} + +func (s *websiteRoutes) Index(c *RequestContext, p httprouter.Params) { + err := c.WriteTemplate("index.html", s.getBaseData(c)) if err != nil { panic(err) } @@ -82,7 +115,27 @@ func (s *websiteRoutes) ProjectCSS(c *RequestContext, p httprouter.Params) { c.Headers().Add("Content-Type", "text/css") err := c.WriteTemplate("project.css", templateData) if err != nil { - logging.Error().Err(err).Msg("failed to generate project CSS") + c.Logger.Error().Err(err).Msg("failed to generate project CSS") return } } + +func (s *websiteRoutes) CommonWebsiteDataWrapper(h HMNHandler) HMNHandler { + return func(c *RequestContext, p httprouter.Params) { + slug := "" + hostParts := strings.SplitN(c.Req.Host, ".", 3) + if len(hostParts) >= 3 { + slug = hostParts[0] + } + + dbProject, err := FetchProjectBySlug(c.Context(), s.conn, slug) + if err != nil { + c.AbortWithErrors(http.StatusInternalServerError, oops.New(err, "failed to fetch current project")) + return + } + + c.currentProject = dbProject + + h(c, p) + } +} diff --git a/src/website/website.go b/src/website/website.go index c091d85..ea041bd 100644 --- a/src/website/website.go +++ b/src/website/website.go @@ -12,7 +12,6 @@ import ( "git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/logging" "git.handmade.network/hmn/hmn/src/templates" - "github.com/julienschmidt/httprouter" "github.com/spf13/cobra" ) @@ -21,7 +20,7 @@ var WebsiteCommand = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { templates.Init() - defer logging.LogPanics() + defer logging.LogPanics(nil) logging.Info().Msg("Hello, HMN!") @@ -37,7 +36,8 @@ var WebsiteCommand = &cobra.Command{ go func() { <-signals logging.Info().Msg("Shutting down the website") - timeout, _ := context.WithTimeout(context.Background(), 30*time.Second) + timeout, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() server.Shutdown(timeout) <-signals @@ -52,19 +52,3 @@ var WebsiteCommand = &cobra.Command{ } }, } - -func withRequestLogger(h httprouter.Handle) httprouter.Handle { - return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { - defer logging.LogPanics() - - start := time.Now() - defer func() { - end := time.Now() - logging.Info().Dur("duration", end.Sub(start)).Msg("Completed request") - }() - - h(rw, r, p) - } -} - -// func handleRequestResults