all repos — go-lift @ 2e1c54060dc6096e5772a13f5248451e86b03f6d

Lightweight workout tracker prototype..

Initial commit
Marco Andronaco andronacomarco@gmail.com
Tue, 20 May 2025 09:55:01 +0200
commit

2e1c54060dc6096e5772a13f5248451e86b03f6d

A .gitignore

@@ -0,0 +1,3 @@

+.env +data +__debug_bin*
A .vscode/launch.json

@@ -0,0 +1,15 @@

+{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "." + } + ] +}
A LICENSE

@@ -0,0 +1,21 @@

+The MIT License (MIT) + +Copyright (c) 2025 birabittoh + +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.
A go.mod

@@ -0,0 +1,46 @@

+module github.com/birabittoh/go-lift + +go 1.24 + +require ( + github.com/BurntSushi/toml v1.4.0 + github.com/glebarez/sqlite v1.11.0 + github.com/joho/godotenv v1.5.1 + github.com/nicksnyder/go-i18n/v2 v2.5.1 + golang.org/x/text v0.23.0 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect + github.com/a-h/templ v0.3.857 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cli/browser v1.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/natefinch/atomic v1.0.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) + +tool ( + github.com/a-h/templ/cmd/templ + github.com/nicksnyder/go-i18n/v2/goi18n +)
A go.sum

@@ -0,0 +1,121 @@

+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= +github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= +github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg= +github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M= +github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A= +github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= +github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.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/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= +github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= +gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= +modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
A main.go

@@ -0,0 +1,14 @@

+package main + +import ( + "log" + + "github.com/birabittoh/go-lift/src" +) + +func main() { + err := src.Run() + if err != nil { + log.Fatal(err) + } +}
A src/api/crud.go

@@ -0,0 +1,284 @@

+package api + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/birabittoh/go-lift/src/database" + "gorm.io/gorm" +) + +// Routines handlers +func getRoutinesHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var routines []database.Routine + result := db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").Find(&routines) + if result.Error != nil { + jsonError(w, http.StatusInternalServerError, "Failed to fetch routines: "+result.Error.Error()) + return + } + jsonResponse(w, http.StatusOK, routines) + } +} + +func getRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var routine database.Routine + result := db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + jsonError(w, http.StatusNotFound, "Routine not found") + return + } + jsonError(w, http.StatusInternalServerError, "Failed to fetch routine: "+result.Error.Error()) + return + } + jsonResponse(w, http.StatusOK, routine) + } +} + +func createRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var routine database.Routine + if err := json.NewDecoder(r.Body).Decode(&routine); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if err := db.Create(&routine).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to create routine: "+err.Error()) + return + } + + // Reload with associations + db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, routine.ID) + + jsonResponse(w, http.StatusCreated, routine) + } +} + +func updateRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var routine database.Routine + + // Check if exists + if err := db.First(&routine, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + jsonError(w, http.StatusNotFound, "Routine not found") + return + } + jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error()) + return + } + + // Parse update data + if err := json.NewDecoder(r.Body).Decode(&routine); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + // Save with associations + if err := db.Session(&gorm.Session{FullSaveAssociations: true}).Save(&routine).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to update routine: "+err.Error()) + return + } + + // Reload complete data + db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, id) + jsonResponse(w, http.StatusOK, routine) + } +} + +func deleteRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := db.Delete(&database.Routine{}, id).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to delete routine: "+err.Error()) + return + } + jsonResponse(w, http.StatusOK, map[string]string{"message": "Routine deleted successfully"}) + } +} + +// Exercises handlers +func getExercisesHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var exercises []database.Exercise + if err := db.Find(&exercises).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to fetch exercises: "+err.Error()) + return + } + jsonResponse(w, http.StatusOK, exercises) + } +} + +func getExerciseHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var exercise database.Exercise + if err := db.First(&exercise, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + jsonError(w, http.StatusNotFound, "Exercise not found") + return + } + jsonError(w, http.StatusInternalServerError, "Failed to fetch exercise: "+err.Error()) + return + } + jsonResponse(w, http.StatusOK, exercise) + } +} + +func createExerciseHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var exercise database.Exercise + if err := json.NewDecoder(r.Body).Decode(&exercise); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if err := db.Create(&exercise).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to create exercise: "+err.Error()) + return + } + + jsonResponse(w, http.StatusCreated, exercise) + } +} + +func updateExerciseHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + // Verify exercise exists + var exercise database.Exercise + if err := db.First(&exercise, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + jsonError(w, http.StatusNotFound, "Exercise not found") + return + } + jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error()) + return + } + + // Parse update data + if err := json.NewDecoder(r.Body).Decode(&exercise); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if err := db.Save(&exercise).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to update exercise: "+err.Error()) + return + } + + jsonResponse(w, http.StatusOK, exercise) + } +} + +func deleteExerciseHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := db.Delete(&database.Exercise{}, id).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to delete exercise: "+err.Error()) + return + } + jsonResponse(w, http.StatusOK, map[string]string{"message": "Exercise deleted successfully"}) + } +} + +// RecordRoutines handlers +func getRecordRoutinesHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var records []database.RecordRoutine + result := db.Preload("RecordExercises").Preload("Routine.Exercises").Preload("Routine.Supersets").Find(&records) + if result.Error != nil { + jsonError(w, http.StatusInternalServerError, "Failed to fetch record routines: "+result.Error.Error()) + return + } + jsonResponse(w, http.StatusOK, records) + } +} + +func getRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var record database.RecordRoutine + result := db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + jsonError(w, http.StatusNotFound, "Record routine not found") + return + } + jsonError(w, http.StatusInternalServerError, "Failed to fetch record routine: "+result.Error.Error()) + return + } + jsonResponse(w, http.StatusOK, record) + } +} + +func createRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var record database.RecordRoutine + if err := json.NewDecoder(r.Body).Decode(&record); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if err := db.Create(&record).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to create record routine: "+err.Error()) + return + } + + // Reload with associations + db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, record.ID) + + jsonResponse(w, http.StatusCreated, record) + } +} + +func updateRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var record database.RecordRoutine + + // Check if exists + if err := db.First(&record, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + jsonError(w, http.StatusNotFound, "Record routine not found") + return + } + jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error()) + return + } + + // Parse update data + if err := json.NewDecoder(r.Body).Decode(&record); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + // Save with associations + if err := db.Session(&gorm.Session{FullSaveAssociations: true}).Save(&record).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to update record routine: "+err.Error()) + return + } + + // Reload complete data + db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, id) + jsonResponse(w, http.StatusOK, record) + } +} + +func deleteRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := db.Delete(&database.RecordRoutine{}, id).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to delete record routine: "+err.Error()) + return + } + jsonResponse(w, http.StatusOK, map[string]string{"message": "Record routine deleted successfully"}) + } +}
A src/api/health.go

@@ -0,0 +1,55 @@

+package api + +import ( + "net/http" + + "gorm.io/gorm" +) + +type AutheliaUserInfo struct { + DisplayName string `json:"display_name"` + Emails []string `json:"emails"` + Method string `json:"method"` + HasTOTP bool `json:"has_totp"` + HasWebAuthn bool `json:"has_webauthn"` + HasDuo bool `json:"has_duo"` +} + +type AutheliaUserInfoResponse struct { + Status string `json:"status"` + Data AutheliaUserInfo `json:"data"` +} + +var mockAutheliaResponse = AutheliaUserInfoResponse{ + Status: "OK", + Data: AutheliaUserInfo{ + DisplayName: "Admin", + Emails: []string{"a***n@admin.com"}, + Method: "totp", + }, +} + +func pingHandler(w http.ResponseWriter, r *http.Request) { + jsonResponse(w, http.StatusOK, map[string]string{"message": "pong"}) +} +func connectionHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sqlDB, err := db.DB() + if err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to get database connection:", err.Error()) + return + } + + err = sqlDB.Ping() + if err != nil { + jsonError(w, http.StatusInternalServerError, "Database ping failed:", err.Error()) + return + } + + jsonResponse(w, http.StatusOK, map[string]string{"message": "Database connection is healthy"}) + } +} + +func mockAutheliaHandler(w http.ResponseWriter, r *http.Request) { + jsonResponse(w, http.StatusOK, mockAutheliaResponse) +}
A src/api/routes.go

@@ -0,0 +1,39 @@

+package api + +import ( + "net/http" + + "gorm.io/gorm" +) + +func GetServeMux(db *gorm.DB) *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("GET /authelia/api/user/info", mockAutheliaHandler) + + mux.HandleFunc("GET /api/ping", pingHandler) + mux.HandleFunc("GET /api/connection", connectionHandler(db)) + + // Routines routes + mux.HandleFunc("GET /api/routines", getRoutinesHandler(db)) + mux.HandleFunc("GET /api/routines/{id}", getRoutineHandler(db)) + mux.HandleFunc("POST /api/routines", createRoutineHandler(db)) + mux.HandleFunc("PUT /api/routines/{id}", updateRoutineHandler(db)) + mux.HandleFunc("DELETE /api/routines/{id}", deleteRoutineHandler(db)) + + // Exercises routes + mux.HandleFunc("GET /api/exercises", getExercisesHandler(db)) + mux.HandleFunc("GET /api/exercises/{id}", getExerciseHandler(db)) + mux.HandleFunc("POST /api/exercises", createExerciseHandler(db)) + mux.HandleFunc("PUT /api/exercises/{id}", updateExerciseHandler(db)) + mux.HandleFunc("DELETE /api/exercises/{id}", deleteExerciseHandler(db)) + + // RecordRoutines routes + mux.HandleFunc("GET /api/recordroutines", getRecordRoutinesHandler(db)) + mux.HandleFunc("GET /api/recordroutines/{id}", getRecordRoutineHandler(db)) + mux.HandleFunc("POST /api/recordroutines", createRecordRoutineHandler(db)) + mux.HandleFunc("PUT /api/recordroutines/{id}", updateRecordRoutineHandler(db)) + mux.HandleFunc("DELETE /api/recordroutines/{id}", deleteRecordRoutineHandler(db)) + + return mux +}
A src/api/utils.go

@@ -0,0 +1,17 @@

+package api + +import ( + "encoding/json" + "net/http" + "strings" +) + +func jsonResponse(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(data) +} + +func jsonError(w http.ResponseWriter, status int, messages ...string) { + jsonResponse(w, status, map[string]string{"error": strings.Join(messages, " ")}) +}
A src/database/data.go

@@ -0,0 +1,193 @@

+package database + +import ( + "log" + + "gorm.io/gorm" +) + +// CheckInitialData ensures that all necessary initial data is in the database +func CheckInitialData(db *gorm.DB) (err error) { + err = ensureEquipmentData(db) + if err != nil { + return + } + + err = ensureMuscleGroupData(db) + if err != nil { + return + } + + err = ensureExerciseData(db) + if err != nil { + return + } + + log.Println("Initial data verification complete") + return +} + +// ensureEquipmentData checks if equipment data exists and adds it if not +func ensureEquipmentData(db *gorm.DB) error { + equipmentList := []string{ + "None", + "Barbell", + "Dumbbell", + "Kettlebell", + "Machine", + "Plate", + "ResistanceBand", + "Suspension", + "Other", + } + + // Check if equipment data already exists + var count int64 + if err := db.Model(&Equipment{}).Count(&count).Error; err != nil { + return err + } + + // If no equipment data, insert the initial data + if count == 0 { + log.Println("Adding initial equipment data") + for _, name := range equipmentList { + equipment := Equipment{ + Name: name, + } + if err := db.Create(&equipment).Error; err != nil { + return err + } + } + } + + return nil +} + +// ensureMuscleGroupData checks if muscle group data exists and adds it if not +func ensureMuscleGroupData(db *gorm.DB) error { + muscleGroupList := []string{ + "Abdominals", + "Abductors", + "Adductors", + "Biceps", + "LowerBack", + "UpperBack", + "Cardio", + "Chest", + "Calves", + "Forearms", + "Glutes", + "Hamstrings", + "Lats", + "Quadriceps", + "Shoulders", + "Triceps", + "Traps", + "Neck", + "FullBody", + "Other", + } + + // Check if muscle group data already exists + var count int64 + if err := db.Model(&MuscleGroup{}).Count(&count).Error; err != nil { + return err + } + + // If no muscle group data, insert the initial data + if count == 0 { + log.Println("Adding initial muscle group data") + for _, name := range muscleGroupList { + muscleGroup := MuscleGroup{ + Name: name, + } + if err := db.Create(&muscleGroup).Error; err != nil { + return err + } + } + } + + return nil +} + +// ensureExerciseData checks if exercise data exists and adds it if not +func ensureExerciseData(db *gorm.DB) error { + exerciseList := []Exercise{ + {Name: "BenchPress", MuscleGroups: []MuscleGroup{{ID: 8}, {ID: 15}, {ID: 16}}, Equipment: []Equipment{{ID: 2}}}, + {Name: "Squat", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}, {ID: 5}}, Equipment: []Equipment{{ID: 2}}}, + {Name: "Deadlift", MuscleGroups: []MuscleGroup{{ID: 5}, {ID: 6}, {ID: 12}, {ID: 11}, {ID: 14}}, Equipment: []Equipment{{ID: 2}}}, + {Name: "OverheadPress", MuscleGroups: []MuscleGroup{{ID: 15}, {ID: 16}}, Equipment: []Equipment{{ID: 2}}}, + {Name: "PullUp", MuscleGroups: []MuscleGroup{{ID: 13}, {ID: 6}, {ID: 4}}, Equipment: []Equipment{{ID: 1}}}, + {Name: "PushUp", MuscleGroups: []MuscleGroup{{ID: 8}, {ID: 15}, {ID: 16}, {ID: 1}}, Equipment: []Equipment{{ID: 1}}}, + {Name: "Lunges", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}}, Equipment: []Equipment{{ID: 1}}}, + {Name: "Plank", MuscleGroups: []MuscleGroup{{ID: 1}, {ID: 5}}, Equipment: []Equipment{{ID: 1}}}, + {Name: "BicepCurl", MuscleGroups: []MuscleGroup{{ID: 4}, {ID: 10}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "TricepDip", MuscleGroups: []MuscleGroup{{ID: 16}, {ID: 8}, {ID: 15}}, Equipment: []Equipment{{ID: 1}}}, + {Name: "LegPress", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "LatPulldown", MuscleGroups: []MuscleGroup{{ID: 13}, {ID: 6}, {ID: 4}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "LegExtension", MuscleGroups: []MuscleGroup{{ID: 14}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "LegCurl", MuscleGroups: []MuscleGroup{{ID: 12}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "ShoulderPress", MuscleGroups: []MuscleGroup{{ID: 15}, {ID: 16}}, Equipment: []Equipment{{ID: 2}}}, + {Name: "ChestFly", MuscleGroups: []MuscleGroup{{ID: 8}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "CableRow", MuscleGroups: []MuscleGroup{{ID: 6}, {ID: 13}, {ID: 4}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "SeatedRow", MuscleGroups: []MuscleGroup{{ID: 6}, {ID: 13}, {ID: 4}}, Equipment: []Equipment{{ID: 5}}}, + {Name: "DumbbellFly", MuscleGroups: []MuscleGroup{{ID: 8}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellRow", MuscleGroups: []MuscleGroup{{ID: 6}, {ID: 13}, {ID: 4}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellShoulderPress", MuscleGroups: []MuscleGroup{{ID: 15}, {ID: 16}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellLateralRaise", MuscleGroups: []MuscleGroup{{ID: 15}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellFrontRaise", MuscleGroups: []MuscleGroup{{ID: 15}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellShrug", MuscleGroups: []MuscleGroup{{ID: 17}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellTricepExtension", MuscleGroups: []MuscleGroup{{ID: 16}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellBicepCurl", MuscleGroups: []MuscleGroup{{ID: 4}, {ID: 10}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellLunge", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellSquat", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}, {ID: 5}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellDeadlift", MuscleGroups: []MuscleGroup{{ID: 5}, {ID: 6}, {ID: 12}, {ID: 11}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellChestPress", MuscleGroups: []MuscleGroup{{ID: 8}, {ID: 15}, {ID: 16}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellChestFly", MuscleGroups: []MuscleGroup{{ID: 8}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellBentOverRow", MuscleGroups: []MuscleGroup{{ID: 6}, {ID: 13}, {ID: 4}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellLateralRaise", MuscleGroups: []MuscleGroup{{ID: 15}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellFrontRaise", MuscleGroups: []MuscleGroup{{ID: 15}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellShrug", MuscleGroups: []MuscleGroup{{ID: 17}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellTricepKickback", MuscleGroups: []MuscleGroup{{ID: 16}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellHammerCurl", MuscleGroups: []MuscleGroup{{ID: 4}, {ID: 10}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellConcentrationCurl", MuscleGroups: []MuscleGroup{{ID: 4}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellSkullCrusher", MuscleGroups: []MuscleGroup{{ID: 16}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellWristCurl", MuscleGroups: []MuscleGroup{{ID: 10}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellWristExtension", MuscleGroups: []MuscleGroup{{ID: 10}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellSideBend", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellRussianTwist", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellPlankRow", MuscleGroups: []MuscleGroup{{ID: 1}, {ID: 6}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellSidePlank", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellMountainClimber", MuscleGroups: []MuscleGroup{{ID: 1}, {ID: 14}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellBicycleCrunch", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellLegRaise", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellReverseCrunch", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellFlutterKick", MuscleGroups: []MuscleGroup{{ID: 1}, {ID: 14}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellSideCrunch", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellToeTouch", MuscleGroups: []MuscleGroup{{ID: 1}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellWoodchopper", MuscleGroups: []MuscleGroup{{ID: 1}, {ID: 5}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellSideLegRaise", MuscleGroups: []MuscleGroup{{ID: 2}, {ID: 11}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellGluteBridge", MuscleGroups: []MuscleGroup{{ID: 11}, {ID: 12}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellCalfRaise", MuscleGroups: []MuscleGroup{{ID: 9}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellStepUp", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}}, Equipment: []Equipment{{ID: 3}}}, + {Name: "DumbbellBulgarianSplitSquat", MuscleGroups: []MuscleGroup{{ID: 14}, {ID: 12}, {ID: 11}}, Equipment: []Equipment{{ID: 3}}}, + } + + // Check if exercise data already exists + var count int64 + if err := db.Model(&Exercise{}).Count(&count).Error; err != nil { + return err + } + + // If no exercise data, insert the initial data + if count == 0 { + log.Println("Adding initial exercise data") + for _, exercise := range exerciseList { + if err := db.Create(&exercise).Error; err != nil { + return err + } + } + } + + return nil +}
A src/database/models.go

@@ -0,0 +1,318 @@

+package database + +import ( + "log" + "os" + "path/filepath" + "time" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +const ( + dbDir = "data" + dbName = "fitness.db" +) + +type Equipment struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` + Description string `gorm:"size:500" json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + Exercises []Exercise `gorm:"many2many:exercise_equipment" json:"exercises,omitempty"` +} + +type MuscleGroup struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + Exercises []Exercise `gorm:"many2many:exercise_muscle_groups" json:"exercises,omitempty"` +} + +type ExerciseMuscleGroup struct { + ID uint `gorm:"primaryKey" json:"id"` + ExerciseID uint `gorm:"uniqueIndex:idx_exercise_muscle_group" json:"exercise_id"` + MuscleGroupID uint `gorm:"uniqueIndex:idx_exercise_muscle_group" json:"muscle_group_id"` + + Exercise Exercise `json:"exercise"` + MuscleGroup MuscleGroup `json:"muscle_group"` +} + +type Exercise struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Description string `gorm:"size:500" json:"description"` + //UserID uint `gorm:"index" json:"user_id"` + //IsPublic bool `gorm:"default:false" json:"is_public"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + //User User `json:"-"` + Equipment []Equipment `gorm:"many2many:exercise_equipment" json:"equipment"` + MuscleGroups []MuscleGroup `gorm:"many2many:exercise_muscle_groups" json:"muscle_groups"` + Sets []Set `json:"sets,omitempty"` +} + +type Set struct { + ID uint `gorm:"primaryKey" json:"id"` + ExerciseID uint `gorm:"index" json:"exercise_id"` + Reps int `json:"reps"` + Weight float64 `json:"weight"` + Duration int `json:"duration"` // In seconds, for timed exercises + OrderIndex int `gorm:"not null" json:"order_index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + Exercise Exercise `json:"-"` +} + +// SuperSet to handle two exercises with single rest time +type SuperSet struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100" json:"name"` + PrimaryExerciseID uint `gorm:"index" json:"primary_exercise_id"` + SecondaryExerciseID uint `gorm:"index" json:"secondary_exercise_id"` + RestTime int `gorm:"default:0" json:"rest_time"` // In seconds + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + PrimaryExercise Exercise `json:"primary_exercise"` + SecondaryExercise Exercise `json:"secondary_exercise"` +} + +// RoutineItem represents either an Exercise or a SuperSet in a Routine +type RoutineItem struct { + ID uint `gorm:"primaryKey" json:"id"` + RoutineID uint `gorm:"index" json:"routine_id"` + ExerciseID *uint `gorm:"index" json:"exercise_id"` + SuperSetID *uint `gorm:"index" json:"super_set_id"` + RestTime int `gorm:"default:0" json:"rest_time"` // In seconds + OrderIndex int `gorm:"not null" json:"order_index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + Routine Routine `json:"-"` + SuperSet *SuperSet `json:"super_set,omitempty"` + Exercise *Exercise `json:"exercise,omitempty"` +} + +type Routine struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Description string `gorm:"size:500" json:"description"` + //UserID uint `gorm:"index" json:"user_id"` + //IsPublic bool `gorm:"default:false" json:"is_public"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + //User User `json:"-"` + RoutineItems []RoutineItem `json:"routine_items,omitempty"` +} + +/* +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"size:50;not null;uniqueIndex" json:"username"` + Email string `gorm:"size:100;not null;uniqueIndex" json:"email"` + Password string `gorm:"size:100;not null" json:"-"` + Name string `gorm:"size:100" json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + Exercises []Exercise `json:"exercises,omitempty"` + Routines []Routine `json:"routines,omitempty"` + RecordRoutines []RecordRoutine `json:"record_routines,omitempty"` +} +*/ + +type RecordRoutine struct { + ID uint `gorm:"primaryKey" json:"id"` + //UserID uint `gorm:"index" json:"user_id"` + RoutineID uint `gorm:"index" json:"routine_id"` + StartedAt time.Time `gorm:"not null" json:"started_at"` + EndedAt *time.Time `json:"ended_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + //User User `json:"-"` + Routine Routine `json:"routine"` + RecordRoutineItems []RecordRoutineItem `json:"record_routine_items,omitempty"` +} + +// RecordRoutineItem represents either a RecordExercise or a RecordSuperSet in a completed routine +type RecordRoutineItem struct { + ID uint `gorm:"primaryKey" json:"id"` + RecordRoutineID uint `gorm:"index" json:"record_routine_id"` + RecordExerciseID *uint `gorm:"index" json:"record_exercise_id"` + RecordSuperSetID *uint `gorm:"index" json:"record_super_set_id"` + ActualRestTime int `json:"actual_rest_time"` // In seconds + OrderIndex int `gorm:"not null" json:"order_index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + RecordRoutine RecordRoutine `json:"-"` + RecordSuperSet *RecordSuperSet `json:"record_super_set,omitempty"` + RecordExercise *RecordExercise `json:"record_exercise,omitempty"` +} + +// RecordSuperSet records a completed superset +type RecordSuperSet struct { + ID uint `gorm:"primaryKey" json:"id"` + RecordRoutineID uint `gorm:"index" json:"record_routine_id"` + SuperSetID uint `gorm:"index" json:"super_set_id"` + StartedAt time.Time `gorm:"not null" json:"started_at"` + EndedAt time.Time `gorm:"not null" json:"ended_at"` + ActualRestTime int `json:"actual_rest_time"` // In seconds + OrderIndex int `gorm:"not null" json:"order_index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + RecordRoutine RecordRoutine `json:"-"` + SuperSet SuperSet `json:"super_set"` +} + +type RecordExercise struct { + ID uint `gorm:"primaryKey" json:"id"` + RecordRoutineID uint `gorm:"index" json:"record_routine_id"` + RecordRoutine RecordRoutine `json:"-"` + ExerciseID uint `gorm:"index" json:"exercise_id"` + Exercise Exercise `json:"exercise"` + StartedAt time.Time `gorm:"not null" json:"started_at"` + EndedAt time.Time `gorm:"not null" json:"ended_at"` + ActualRestTime int `json:"actual_rest_time"` // In seconds + RecordSets []RecordSet `json:"record_sets,omitempty"` + OrderIndex int `gorm:"not null" json:"order_index"` + RecordSuperSetID *uint `gorm:"index" json:"record_super_set_id"` + RecordSuperSet *RecordSuperSet `json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} + +type RecordSet struct { + ID uint `gorm:"primaryKey" json:"id"` + RecordExerciseID uint `gorm:"index" json:"record_exercise_id"` + SetID uint `gorm:"index" json:"set_id"` + ActualReps int `json:"actual_reps"` + ActualWeight float64 `json:"actual_weight"` + ActualDuration int `json:"actual_duration"` // In seconds + CompletedAt time.Time `gorm:"not null" json:"completed_at"` + OrderIndex int `gorm:"not null" json:"order_index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + + RecordExercise RecordExercise `json:"-"` + Set Set `json:"set"` +} + +type Localization struct { + ID uint `gorm:"primaryKey" json:"id"` + LanguageID uint `gorm:"not null;uniqueIndex:idx_lang_keyword" json:"language_id"` + Keyword string `gorm:"size:255;not null;uniqueIndex:idx_lang_keyword" json:"keyword"` + Text string `gorm:"size:1000;not null" json:"text"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} + +type Language struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` + Code string `gorm:"size:8;not null;uniqueIndex" json:"code"` + Flag string `gorm:"size:50" json:"flag"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} + +// InitializeDB creates and initializes the SQLite database with all models +func InitializeDB() (db *gorm.DB, err error) { + // Create the data directory if it doesn't exist + if _, err = os.Stat(dbDir); os.IsNotExist(err) { + err = os.MkdirAll(dbDir, 0755) + if err != nil { + return + } + } + + dbPath := filepath.Join(dbDir, dbName) + + // Set up logger for GORM + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: logger.Info, + Colorful: true, + }, + ) + + dialector := sqlite.Open(dbPath + "?_pragma=foreign_keys(1)") + config := &gorm.Config{Logger: newLogger} + + // Open connection to the database + db, err = gorm.Open(dialector, config) + if err != nil { + return + } + + // Get the underlying SQL database to set connection parameters + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // Set connection pool settings + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + // Auto migrate the models + err = db.AutoMigrate( + Equipment{}, + MuscleGroup{}, + Exercise{}, + ExerciseMuscleGroup{}, + Set{}, + SuperSet{}, + RoutineItem{}, + Routine{}, + //User{}, + RecordRoutine{}, + RecordExercise{}, + RecordSuperSet{}, + RecordSet{}, + Localization{}, + Language{}, + ) + if err != nil { + return + } + + // Ensure initial data is present + err = CheckInitialData(db) + if err != nil { + return nil, err + } + + log.Println("Database initialized successfully") + return db, nil +}
A src/run.go

@@ -0,0 +1,35 @@

+package src + +import ( + "log" + "net/http" + "os" + + "github.com/birabittoh/go-lift/src/api" + "github.com/birabittoh/go-lift/src/database" + "github.com/joho/godotenv" +) + +func getEnv(key, def string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return def +} + +func Run() (err error) { + godotenv.Load() + + db, err := database.InitializeDB() + if err != nil { + return + } + + listenAddress := getEnv("APP_LISTEN_ADDRESS", ":3000") + + mux := api.GetServeMux(db) + + log.Println("Example running at", listenAddress) + err = http.ListenAndServe(listenAddress, mux) + return +}