Compare commits
No commits in common. "217a9bf4167ed88466d3e169d9be78d24389b96d" and "d1ecd8f5a3a606c9bfe8603881e2dee041d4571c" have entirely different histories.
217a9bf416
...
d1ecd8f5a3
4
go.mod
4
go.mod
@ -15,7 +15,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
@ -26,13 +25,10 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
|||||||
12
go.sum
12
go.sum
@ -1,8 +1,5 @@
|
|||||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
||||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
|
||||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
|
||||||
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
|
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
|
||||||
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
||||||
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
|
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
|
||||||
@ -51,7 +48,6 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
|||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
@ -69,8 +65,6 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
|
|||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
@ -84,17 +78,12 @@ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2
|
|||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
|
||||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@ -110,7 +99,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -17,7 +18,6 @@ import (
|
|||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
@ -26,7 +26,7 @@ type Server struct {
|
|||||||
port int
|
port int
|
||||||
Router *gin.Engine
|
Router *gin.Engine
|
||||||
config cors.Config
|
config cors.Config
|
||||||
DB *sqlx.DB
|
DB *sql.DB
|
||||||
deps domain.InjectedDependencies
|
deps domain.InjectedDependencies
|
||||||
logs []logging.Logger
|
logs []logging.Logger
|
||||||
cleanupFuncs []func() error
|
cleanupFuncs []func() error
|
||||||
@ -132,7 +132,7 @@ func (s *Server) Setup() *Server {
|
|||||||
auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)
|
auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)
|
||||||
|
|
||||||
// SETUP DATABASE
|
// SETUP DATABASE
|
||||||
db, err := sqlx.Open("postgres", cfg.DatabaseUrl)
|
db, err := sql.Open("postgres", cfg.DatabaseUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Could not connect to database: " + err.Error())
|
panic("Could not connect to database: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,7 +131,7 @@ func (s *RecipeService) DeleteRecipe(userId, recipeId int) error {
|
|||||||
return fmt.Errorf("User id does not match. Do you own the target recipe?")
|
return fmt.Errorf("User id does not match. Do you own the target recipe?")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.recipeRepository.DeleteRecipe(recipeId, userId)
|
return s.recipeRepository.DeleteRecipe(recipeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package domain
|
|||||||
type RecipeRepository interface {
|
type RecipeRepository interface {
|
||||||
CreateRecipe(recipe *Recipe) error
|
CreateRecipe(recipe *Recipe) error
|
||||||
EditRecipe(recipe *Recipe, userId int) error
|
EditRecipe(recipe *Recipe, userId int) error
|
||||||
DeleteRecipe(recipeId, userId int) error
|
DeleteRecipe(recipeId int) error
|
||||||
GetRecipe(id int, userId *int) (*Recipe, error)
|
GetRecipe(id int, userId *int) (*Recipe, error)
|
||||||
GetRecipes(ids []int, userId *int) ([]Recipe, error)
|
GetRecipes(ids []int, userId *int) ([]Recipe, error)
|
||||||
SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]int, error)
|
SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]int, error)
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EngagementRepository struct {
|
type EngagementRepository struct {
|
||||||
db *sqlx.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check to ensure the EngagementRepository implements domain.EngagementRepository
|
// Compile-time check to ensure the EngagementRepository implements domain.EngagementRepository
|
||||||
@ -20,7 +19,7 @@ var _ domain.EngagementRepository = (*EngagementRepository)(nil)
|
|||||||
|
|
||||||
// NewUserRepository creates a user repository object which is used by the user service to access
|
// NewUserRepository creates a user repository object which is used by the user service to access
|
||||||
// the database. Any user related database operations will take place in this repository.
|
// the database. Any user related database operations will take place in this repository.
|
||||||
func NewEngagementRepository(db *sqlx.DB) domain.EngagementRepository {
|
func NewEngagementRepository(db *sql.DB) domain.EngagementRepository {
|
||||||
return &EngagementRepository{db: db}
|
return &EngagementRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,18 +3,17 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecipeRepository struct {
|
type RecipeRepository struct {
|
||||||
db *sqlx.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check to ensure the RecipeRepository implements domain.RecipeRepository
|
// Compile-time check to ensure the RecipeRepository implements domain.RecipeRepository
|
||||||
@ -22,7 +21,7 @@ var _ domain.RecipeRepository = (*RecipeRepository)(nil)
|
|||||||
|
|
||||||
// NewRecipeRepository creates a user repository object which is used by the user service to access
|
// NewRecipeRepository creates a user repository object which is used by the user service to access
|
||||||
// the database. Any recipe related database operations will take place in this repository.
|
// the database. Any recipe related database operations will take place in this repository.
|
||||||
func NewRecipeRepository(db *sqlx.DB) domain.RecipeRepository {
|
func NewRecipeRepository(db *sql.DB) domain.RecipeRepository {
|
||||||
return &RecipeRepository{db: db}
|
return &RecipeRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +32,26 @@ func NewRecipeRepository(db *sqlx.DB) domain.RecipeRepository {
|
|||||||
// be bubbled to the caller. The recipe parameter is passed by reference and will therefore be updated
|
// be bubbled to the caller. The recipe parameter is passed by reference and will therefore be updated
|
||||||
// directly and the new fields (ID, created) can be accessed upon success.
|
// directly and the new fields (ID, created) can be accessed upon success.
|
||||||
func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
||||||
// Convert data into a readable format
|
tx, err := r.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO recipes (
|
||||||
|
title, description, instructions, serves, difficulty,
|
||||||
|
duration, category, ingredients, userid, modified, created
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||||
|
) RETURNING id;`
|
||||||
|
|
||||||
|
// NOTE: Data steps
|
||||||
|
// cast duration to JSON
|
||||||
|
// convert ingredients to store type
|
||||||
|
// cast store type to JSON
|
||||||
|
// extract string instructions from type
|
||||||
|
// cast category to string
|
||||||
|
// use nil for the modified time
|
||||||
|
|
||||||
durationJSON, err := json.Marshal(recipe.Duration)
|
durationJSON, err := json.Marshal(recipe.Duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -54,24 +72,9 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
|||||||
instructions[i] = instruction.Content
|
instructions[i] = instruction.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
var id int
|
||||||
|
if err = tx.QueryRow(
|
||||||
query := psql.
|
query,
|
||||||
Insert("recipes").
|
|
||||||
Columns(
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"instructions",
|
|
||||||
"serves",
|
|
||||||
"difficulty",
|
|
||||||
"duration",
|
|
||||||
"category",
|
|
||||||
"ingredients",
|
|
||||||
"userid",
|
|
||||||
"modified",
|
|
||||||
"created",
|
|
||||||
).
|
|
||||||
Values(
|
|
||||||
recipe.Title,
|
recipe.Title,
|
||||||
recipe.Description,
|
recipe.Description,
|
||||||
pq.Array(instructions),
|
pq.Array(instructions),
|
||||||
@ -83,17 +86,13 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
|||||||
recipe.UserId,
|
recipe.UserId,
|
||||||
nil,
|
nil,
|
||||||
recipe.Created,
|
recipe.Created,
|
||||||
).
|
).Scan(&id); err != nil {
|
||||||
Suffix("RETURNING id")
|
tx.Rollback()
|
||||||
|
return err
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to construct query: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var id int
|
if err := tx.Commit(); err != nil {
|
||||||
if err := r.db.Get(&id, _sql, args...); err != nil {
|
return err
|
||||||
return fmt.Errorf("Failed to create recipe: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new ID
|
// Set the new ID
|
||||||
@ -106,9 +105,36 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
|||||||
// function will fail - it will not know what recipe to edit.
|
// function will fail - it will not know what recipe to edit.
|
||||||
func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error {
|
func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error {
|
||||||
if recipe.Id <= 0 {
|
if recipe.Id <= 0 {
|
||||||
return fmt.Errorf("Recipe must contain an ID. Cannot edit unknown recipe.")
|
return fmt.Errorf("[ERROR] Recipe must contain an ID. Cannot edit unknown recipe.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx, err := r.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This query will ensure the userId matches the owner.
|
||||||
|
query := `UPDATE recipes SET
|
||||||
|
title = $1,
|
||||||
|
description = $2,
|
||||||
|
instructions = $3,
|
||||||
|
serves = $4,
|
||||||
|
difficulty = $5,
|
||||||
|
duration = $6,
|
||||||
|
category = $7,
|
||||||
|
ingredients = $8,
|
||||||
|
modified = $9
|
||||||
|
WHERE id = $10
|
||||||
|
AND userid = $11;`
|
||||||
|
|
||||||
|
// NOTE: Data steps
|
||||||
|
// cast duration to JSON
|
||||||
|
// convert ingredients to store type
|
||||||
|
// cast store type to JSON
|
||||||
|
// extract string instructions from type
|
||||||
|
// cast category to string
|
||||||
|
// use nil for the modified time
|
||||||
|
|
||||||
durationJSON, err := json.Marshal(recipe.Duration)
|
durationJSON, err := json.Marshal(recipe.Duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -129,38 +155,38 @@ func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error {
|
|||||||
instructions[i] = instruction.Content
|
instructions[i] = instruction.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
result, err := tx.Exec(
|
||||||
|
query,
|
||||||
query := psql.
|
recipe.Title,
|
||||||
Update("recipes").
|
recipe.Description,
|
||||||
Set("title", recipe.Title).
|
pq.Array(instructions),
|
||||||
Set("description", recipe.Description).
|
recipe.Serves,
|
||||||
Set("instructions", pq.Array(instructions)).
|
recipe.Difficulty,
|
||||||
Set("serves", recipe.Serves).
|
durationJSON,
|
||||||
Set("difficulty", recipe.Difficulty).
|
string(recipe.Category),
|
||||||
Set("duration", durationJSON).
|
ingredientsJSON,
|
||||||
Set("category", string(recipe.Category)).
|
time.Now().UTC(),
|
||||||
Set("ingredients", ingredientsJSON).
|
recipe.Id,
|
||||||
Set("modified", time.Now().UTC()).
|
userId,
|
||||||
Where(sq.Eq{
|
)
|
||||||
"id": recipe.Id,
|
|
||||||
"userid": userId,
|
|
||||||
})
|
|
||||||
|
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to construct query: %w", err)
|
tx.Rollback()
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
result, err := r.db.Exec(_sql, args...)
|
|
||||||
if err != nil {
|
rows, err := result.RowsAffected()
|
||||||
return fmt.Errorf("Failed to update recipe: %w", err)
|
if err != nil {
|
||||||
}
|
tx.Rollback()
|
||||||
|
return err
|
||||||
if rows, err := result.RowsAffected(); err != nil {
|
}
|
||||||
|
|
||||||
|
if rows != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("[ERROR] Modified an unexpected number of rows. Expected 1, modified %d.", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if rows != 1 {
|
|
||||||
return fmt.Errorf("Modified an unexpected number of rows. Expected 1, modified %d.", rows)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -169,35 +195,17 @@ func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error {
|
|||||||
// DeleteRecipe deletes a recipe in the database. This is done by setting the deleted field to true.
|
// DeleteRecipe deletes a recipe in the database. This is done by setting the deleted field to true.
|
||||||
// This will create a "soft delete" effect. This function does not validate that the user is the owner,
|
// This will create a "soft delete" effect. This function does not validate that the user is the owner,
|
||||||
// so the caller should validate the owner. If any errors occur, they will be returned to the caller.
|
// so the caller should validate the owner. If any errors occur, they will be returned to the caller.
|
||||||
func (r *RecipeRepository) DeleteRecipe(recipeId, userId int) error {
|
func (r *RecipeRepository) DeleteRecipe(recipeId int) error {
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
query := "UPDATE recipes SET deleted = TRUE WHERE id = $1"
|
||||||
|
|
||||||
query := psql.
|
result, err := r.db.Exec(query, recipeId)
|
||||||
Update("recipes").
|
|
||||||
Set("deleted", true).
|
|
||||||
Set("modified", time.Now().UTC()).
|
|
||||||
Where(sq.Eq{
|
|
||||||
"id": recipeId,
|
|
||||||
"userid": userId,
|
|
||||||
"deleted": false,
|
|
||||||
})
|
|
||||||
|
|
||||||
sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to build delete query: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := r.db.Exec(sql, args...)
|
rows, _ := result.RowsAffected()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to delete recipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to get rows affects: %w", err)
|
|
||||||
}
|
|
||||||
if rows != 1 {
|
if rows != 1 {
|
||||||
return fmt.Errorf("Incorrect number of rows modified. Expected 1, received %d.", rows)
|
return fmt.Errorf("[ERROR] Incorrect number of rows modified. Expected 1, received %d.", rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -330,165 +338,198 @@ func isBitActive(bits, pos int) bool {
|
|||||||
// 12/28/25: This function has changed, now longer returns the recipes, but their IDs for fetching
|
// 12/28/25: This function has changed, now longer returns the recipes, but their IDs for fetching
|
||||||
// elsewhere.
|
// elsewhere.
|
||||||
//
|
//
|
||||||
// 2/3/26: Refactored this large function to use Squirrel for simpler generation. Reduced line count by 50,
|
|
||||||
// but this is still insane. We need to clean this up.
|
|
||||||
//
|
|
||||||
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
||||||
// and the standard "not-found" error will be returned.
|
// and the standard "not-found" error will be returned.
|
||||||
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]int, error) {
|
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]int, error) {
|
||||||
// Begin creating the query
|
// Compute meals type filters (there are 7 bits)
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
var mealConditions []string
|
||||||
query := psql.Select("r.id").From("recipes r")
|
|
||||||
|
|
||||||
// Only select fields where the recipe ID can be found in the favorites table (mapped to user ID)
|
|
||||||
if favorites && userId != nil {
|
|
||||||
query = query.
|
|
||||||
Join("favorites f ON f.recipeId = r.id").
|
|
||||||
Where(sq.Eq{"f.userid": *userId})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute and add meal type filters (7 bit options)
|
|
||||||
var mealCategories []string
|
|
||||||
for i := range 7 {
|
for i := range 7 {
|
||||||
if isBitActive(filters.MealType, i) {
|
if isBitActive(filters.MealType, i) {
|
||||||
mealCategories = append(mealCategories, string(domain.ParseMeal(i)))
|
mealConditions = append(mealConditions, fmt.Sprintf("category = '%s'", domain.ParseMeal(i)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mealCategories) > 0 {
|
// Compute time filters (there are 5 bits)
|
||||||
query = query.Where(sq.Eq{"category": mealCategories})
|
var timeConditions []string
|
||||||
}
|
|
||||||
|
|
||||||
// Compute and add time filters (5 bit options)
|
|
||||||
var timeOr sq.Or
|
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
|
var cond string
|
||||||
if isBitActive(filters.Time, i) {
|
if isBitActive(filters.Time, i) {
|
||||||
switch i {
|
switch i {
|
||||||
case 0:
|
case 0:
|
||||||
timeOr = append(timeOr, sq.Lt{"(duration->>'total')::int": 15})
|
cond = "(duration->>'total')::int < 15"
|
||||||
case 1:
|
case 1:
|
||||||
timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 15 AND 30"))
|
cond = "(duration->>'total')::int BETWEEN 15 AND 30"
|
||||||
case 2:
|
case 2:
|
||||||
timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 30 AND 60"))
|
cond = "(duration->>'total')::int BETWEEN 30 AND 60"
|
||||||
case 3:
|
case 3:
|
||||||
timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 60 AND 120"))
|
cond = "(duration->>'total')::int BETWEEN 60 AND 120"
|
||||||
case 4:
|
case 4:
|
||||||
timeOr = append(timeOr, sq.Gt{"(duration->>'total')::int": 120})
|
cond = "(duration->>'total')::int > 120"
|
||||||
}
|
}
|
||||||
|
timeConditions = append(timeConditions, cond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(timeOr) > 0 {
|
// Compute difficulty filters (there are 5 bits)
|
||||||
query = query.Where(timeOr)
|
var difficultyConditions []string
|
||||||
}
|
|
||||||
|
|
||||||
// Compute and add difficulty filters (5 bit options)
|
|
||||||
var difficulties []int
|
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
if isBitActive(filters.Difficulty, i) {
|
if isBitActive(filters.Difficulty, i) {
|
||||||
difficulties = append(difficulties, i+1)
|
cond := fmt.Sprintf("difficulty = '%d'", i+1)
|
||||||
|
difficultyConditions = append(difficultyConditions, cond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(difficulties) > 0 {
|
// Compute serving size filters (there are 5 bits)
|
||||||
query = query.Where(sq.Eq{"difficulty": difficulties})
|
var servingConditions []string
|
||||||
}
|
|
||||||
|
|
||||||
// Compute and add serving size filters (5 bit options)
|
|
||||||
var servingOr sq.Or
|
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
|
var cond string
|
||||||
if isBitActive(filters.ServingSize, i) {
|
if isBitActive(filters.ServingSize, i) {
|
||||||
switch i {
|
switch i {
|
||||||
case 0:
|
case 0:
|
||||||
servingOr = append(servingOr, sq.Expr("serves BETWEEN 1 AND 2"))
|
cond = "serves BETWEEN 1 AND 2"
|
||||||
case 1:
|
case 1:
|
||||||
servingOr = append(servingOr, sq.Expr("serves BETWEEN 2 AND 4"))
|
cond = "serves BETWEEN 2 AND 4"
|
||||||
case 2:
|
case 2:
|
||||||
servingOr = append(servingOr, sq.Expr("serves BETWEEN 4 AND 6"))
|
cond = "serves BETWEEN 4 AND 6"
|
||||||
case 3:
|
case 3:
|
||||||
servingOr = append(servingOr, sq.Expr("serves BETWEEN 6 AND 8"))
|
cond = "serves BETWEEN 6 AND 8"
|
||||||
case 4:
|
case 4:
|
||||||
servingOr = append(servingOr, sq.Gt{"serves": 8})
|
cond = "serves > 8"
|
||||||
}
|
}
|
||||||
|
servingConditions = append(servingConditions, cond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(servingOr) > 0 {
|
// Merge condition strings
|
||||||
query = query.Where(servingOr)
|
mealString := fmt.Sprintf("(%s)", strings.Join(mealConditions, " OR "))
|
||||||
|
timeString := fmt.Sprintf("(%s)", strings.Join(timeConditions, " OR "))
|
||||||
|
difficultyString := fmt.Sprintf("(%s)", strings.Join(difficultyConditions, " OR "))
|
||||||
|
servingString := fmt.Sprintf("(%s)", strings.Join(servingConditions, " OR "))
|
||||||
|
|
||||||
|
// Combine condition strings
|
||||||
|
var conditions []string
|
||||||
|
if len(mealConditions) > 0 {
|
||||||
|
conditions = append(conditions, mealString)
|
||||||
|
}
|
||||||
|
if len(timeConditions) > 0 {
|
||||||
|
conditions = append(conditions, timeString)
|
||||||
|
}
|
||||||
|
if len(difficultyConditions) > 0 {
|
||||||
|
conditions = append(conditions, difficultyString)
|
||||||
|
}
|
||||||
|
if len(servingConditions) > 0 {
|
||||||
|
conditions = append(conditions, servingString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle search with full-text search and ILIKE fallback
|
// Define columns to select
|
||||||
|
columns := []string{
|
||||||
|
"r.id",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create search vector query with SAFE parameterization
|
||||||
|
var orderBy string = ""
|
||||||
|
var searchQuery string = ""
|
||||||
|
|
||||||
if filters.Search != "" {
|
if filters.Search != "" {
|
||||||
spl := strings.Split(filters.Search, " ")
|
spl := strings.Split(filters.Search, " ")
|
||||||
var cleaned []string
|
var cleaned []string
|
||||||
|
|
||||||
// Sanitize search terms
|
// Use a string replacer for safety
|
||||||
replacer := strings.NewReplacer(
|
replacer := strings.NewReplacer(
|
||||||
"'", "",
|
"'", "",
|
||||||
"-", "",
|
"-", "",
|
||||||
"&", "",
|
"&", "",
|
||||||
"|", "",
|
"|", "",
|
||||||
"!", "",
|
"!", "",
|
||||||
":", "",
|
":", "", // Remove colons to prevent tsquery syntax injection
|
||||||
"(", "",
|
"(", "",
|
||||||
")", "",
|
")", "",
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, term := range spl {
|
for i := range len(spl) {
|
||||||
q := strings.TrimSpace(replacer.Replace(term))
|
q := strings.TrimSpace(replacer.Replace(spl[i]))
|
||||||
if q != "" {
|
if q != "" {
|
||||||
cleaned = append(cleaned, q+":*") // Add prefix matching
|
// Add :* suffix for prefix matching
|
||||||
|
cleaned = append(cleaned, q+":*")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cleaned) > 0 {
|
// Join with OR operator for full-text search
|
||||||
vectorQuery := strings.Join(cleaned, " | ")
|
vector_query := strings.Join(cleaned, " | ")
|
||||||
|
searchQuery = vector_query
|
||||||
|
|
||||||
// Build search condition as raw SQL expression
|
// Full-text search with prefix matching
|
||||||
// We'll use sq.Expr for the entire OR clause
|
searchCondition := fmt.Sprintf("r.search_vector @@ to_tsquery('english', '%s')", vector_query)
|
||||||
var searchConditions []string
|
|
||||||
var searchArgs []interface{}
|
|
||||||
|
|
||||||
// Full-text search
|
// Add fallback ILIKE for true substring matching
|
||||||
searchConditions = append(searchConditions, "r.search_vector @@ to_tsquery('english', ?)")
|
// This catches cases where "pan" is inside "pancake" but not at word boundaries
|
||||||
searchArgs = append(searchArgs, vectorQuery)
|
var ilikeConditions []string
|
||||||
|
|
||||||
// ILIKE fallback for substring matching
|
|
||||||
for _, term := range spl {
|
for _, term := range spl {
|
||||||
cleanTerm := strings.TrimSpace(replacer.Replace(term))
|
cleanTerm := strings.TrimSpace(replacer.Replace(term))
|
||||||
if cleanTerm != "" {
|
if cleanTerm != "" {
|
||||||
searchConditions = append(searchConditions, "r.title ILIKE ?")
|
ilikeConditions = append(ilikeConditions, fmt.Sprintf("(r.title ILIKE '%%%s%%' OR r.description ILIKE '%%%s%%')", cleanTerm, cleanTerm))
|
||||||
searchArgs = append(searchArgs, "%"+cleanTerm+"%")
|
|
||||||
|
|
||||||
searchConditions = append(searchConditions, "r.description ILIKE ?")
|
|
||||||
searchArgs = append(searchArgs, "%"+cleanTerm+"%")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine all conditions with OR
|
if len(ilikeConditions) > 0 {
|
||||||
searchExpr := fmt.Sprintf("(%s)", strings.Join(searchConditions, " OR "))
|
searchCondition = fmt.Sprintf("(%s OR %s)", searchCondition, strings.Join(ilikeConditions, " OR "))
|
||||||
query = query.Where(sq.Expr(searchExpr, searchArgs...))
|
|
||||||
|
|
||||||
// Add ordering for search results
|
|
||||||
query = query.
|
|
||||||
OrderBy(fmt.Sprintf("CASE WHEN r.search_vector @@ to_tsquery('english', '%s') THEN 1 ELSE 2 END", vectorQuery)).
|
|
||||||
OrderBy(fmt.Sprintf("ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC", vectorQuery)).
|
|
||||||
OrderBy(fmt.Sprintf("ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC", vectorQuery))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude deleted recipes
|
conditions = append(conditions, searchCondition)
|
||||||
query = query.Where(sq.Eq{"deleted": false})
|
|
||||||
|
|
||||||
sql, args, err := query.ToSql()
|
// Ranking with preference for full-text matches
|
||||||
|
orderBy = fmt.Sprintf(`
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN r.search_vector @@ to_tsquery('english', '%s') THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
|
ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC,
|
||||||
|
ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC
|
||||||
|
`, searchQuery, searchQuery, searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the query
|
||||||
|
var query string
|
||||||
|
if favorites && userId != nil {
|
||||||
|
query = fmt.Sprintf(
|
||||||
|
"SELECT %s FROM recipes r JOIN favorites f ON f.recipeId = r.id",
|
||||||
|
strings.Join(columns, ","),
|
||||||
|
)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("f.userid = %d", *userId))
|
||||||
|
} else {
|
||||||
|
query = fmt.Sprintf("SELECT %s FROM recipes r", strings.Join(columns, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert and append conditions if provided
|
||||||
|
conditions = append(conditions, "deleted = false")
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND "))
|
||||||
|
query = fmt.Sprintf("%s %s", query, conditionsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append sorting order if exists
|
||||||
|
if len(orderBy) > 0 {
|
||||||
|
query = fmt.Sprintf("%s %s", query, orderBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish it off with a semicolon!
|
||||||
|
query += ";"
|
||||||
|
|
||||||
|
// Execute the query
|
||||||
|
rows, err := r.db.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to build query: %w", err)
|
return []int{}, fmt.Errorf("failed to query recipes: %w", err)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
// Execute query using SQLX
|
|
||||||
var ids []int
|
var ids []int
|
||||||
if err = r.db.Select(&ids, sql, args...); err != nil {
|
for rows.Next() {
|
||||||
return nil, fmt.Errorf("Failed to query recipes: %w", err)
|
var id int
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return []int{}, fmt.Errorf("failed to extract ID: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids, nil
|
return ids, nil
|
||||||
@ -636,26 +677,28 @@ func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string)
|
|||||||
//
|
//
|
||||||
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
||||||
// and the standard "not-found" error will be returned.
|
// and the standard "not-found" error will be returned.
|
||||||
func (r *RecipeRepository) GetUserRecipesIds(userId int) ([]int, error) {
|
func (r *RecipeRepository) GetUserRecipesIds(user_id int) ([]int, error) {
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
query := `
|
||||||
|
SELECT id
|
||||||
|
FROM recipes
|
||||||
|
WHERE userid = $1 AND deleted = false
|
||||||
|
ORDER BY created DESC;
|
||||||
|
`
|
||||||
|
|
||||||
query := psql.
|
rows, err := r.db.Query(query, user_id)
|
||||||
Select("id").
|
|
||||||
From("recipes").
|
|
||||||
Where(sq.Eq{
|
|
||||||
"userid": userId,
|
|
||||||
"deleted": false,
|
|
||||||
}).
|
|
||||||
OrderBy("created DESC")
|
|
||||||
|
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []int{}, fmt.Errorf("Failed to construct SQL query: %w", err)
|
return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var ids []int
|
var ids []int
|
||||||
if err := r.db.Select(&ids, _sql, args...); err != nil {
|
for rows.Next() {
|
||||||
return []int{}, fmt.Errorf("Failed to get user recipes: %w", err)
|
var r_id int
|
||||||
|
if err := rows.Scan(&r_id); err != nil {
|
||||||
|
return []int{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = append(ids, r_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids, nil
|
return ids, nil
|
||||||
@ -669,29 +712,28 @@ func (r *RecipeRepository) GetUserRecipesIds(userId int) ([]int, error) {
|
|||||||
//
|
//
|
||||||
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
||||||
// and the standard "not-found" error will be returned.
|
// and the standard "not-found" error will be returned.
|
||||||
func (r *RecipeRepository) GetUserFavoriteRecipesIds(userId int) ([]int, error) {
|
func (r *RecipeRepository) GetUserFavoriteRecipesIds(id int) ([]int, error) {
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
query := `
|
||||||
|
SELECT r.id
|
||||||
query := psql.
|
FROM favorites f
|
||||||
Select("r.id").
|
JOIN recipes r ON r.id = f.recipeid
|
||||||
From("favorites f").
|
WHERE f.userid = $1 AND deleted = false
|
||||||
Join("recipes r on r.id = f.recipeid").
|
ORDER BY f.created DESC;
|
||||||
Where(sq.Eq{
|
`
|
||||||
"f.userid": userId,
|
rows, err := r.db.Query(query, id)
|
||||||
"deleted": false,
|
|
||||||
}).
|
|
||||||
OrderBy("f.created DESC")
|
|
||||||
|
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []int{}, fmt.Errorf("Failed to construct SQL query: %w", err)
|
return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
fmt.Println(_sql)
|
|
||||||
|
|
||||||
var ids []int
|
var ids []int
|
||||||
if err := r.db.Select(&ids, _sql, args...); err != nil {
|
for rows.Next() {
|
||||||
return []int{}, fmt.Errorf("Failed to get users' favorite recipes: %w", err)
|
var r_id int
|
||||||
|
if err := rows.Scan(&r_id); err != nil {
|
||||||
|
return []int{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = append(ids, r_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids, nil
|
return ids, nil
|
||||||
@ -742,24 +784,15 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
query := `
|
||||||
|
SELECT COUNT(*)
|
||||||
query := psql.
|
FROM favorites
|
||||||
Select("COUNT(*)").
|
WHERE recipeid = $1 AND userid = $2;
|
||||||
From("favorites").
|
`
|
||||||
Where(sq.Eq{
|
|
||||||
"recipeid": recipe.Id,
|
|
||||||
"userid": userId,
|
|
||||||
})
|
|
||||||
|
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to construct SQL query: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int
|
var count int
|
||||||
if err := r.db.Get(&count, _sql, args...); err != nil {
|
if err := r.db.QueryRow(query, recipe.Id, userId).Scan(&count); err != nil {
|
||||||
return fmt.Errorf("Failed to get recipe favorite status: %w", err)
|
return fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
recipe.Favorite = count > 0
|
recipe.Favorite = count > 0
|
||||||
@ -772,56 +805,41 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int)
|
|||||||
// table and return it. If there is no entry, nil will be returned. Any errors will be bubbled to
|
// table and return it. If there is no entry, nil will be returned. Any errors will be bubbled to
|
||||||
// the caller. All that is returned is the recipe ID, that way the caller can handle the fetching.
|
// the caller. All that is returned is the recipe ID, that way the caller can handle the fetching.
|
||||||
func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) {
|
func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) {
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
query := `
|
||||||
|
SELECT
|
||||||
|
r.id
|
||||||
|
FROM recipes r
|
||||||
|
JOIN recipeoftheweek rw ON rw.recipeid = r.id
|
||||||
|
WHERE r.deleted = false
|
||||||
|
ORDER BY rw.created DESC
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
query := psql.
|
var id int
|
||||||
Select("r.id").
|
if err := r.db.QueryRow(query).Scan(&id); err != nil {
|
||||||
From("recipes r").
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
Join("recipeoftheweek rw ON rw.recipeid = r.id").
|
|
||||||
Where(sq.Eq{"r.deleted": false}).
|
|
||||||
OrderBy("rw.created DESC").
|
|
||||||
Limit(1)
|
|
||||||
|
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to build SQL query: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var recipeId int
|
|
||||||
if err := r.db.Get(&recipeId, _sql, args...); err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Failed to locate recipe in database: %s", err.Error())
|
return nil, fmt.Errorf("Failed to locate recipe in database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &recipeId, nil
|
return &id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRecipeOwner takes two required arguments: a user id and a recipe id. This function queries the DB
|
// IsRecipeOwner takes two required arguments: a user id and a recipe id. This function queries the DB
|
||||||
// to check if the user is the owner of the provided recipe. Any error will be bubbled to the caller.
|
// to check if the user is the owner of the provided recipe. Any error will be bubbled to the caller.
|
||||||
func (r *RecipeRepository) IsRecipeOwner(userId, recipeId int) (bool, error) {
|
func (r *RecipeRepository) IsRecipeOwner(userId, recipeId int) (bool, error) {
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
query := `
|
||||||
|
SELECT
|
||||||
query := psql.
|
userid
|
||||||
Select("userid").
|
FROM recipes
|
||||||
From("recipes").
|
WHERE deleted = false
|
||||||
Where(sq.Eq{
|
AND id = $1;
|
||||||
"id": recipeId,
|
`
|
||||||
"deleted": false,
|
|
||||||
})
|
|
||||||
|
|
||||||
_sql, args, err := query.ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var recipeOwnerId int
|
var recipeOwnerId int
|
||||||
if err := r.db.Get(&recipeOwnerId, _sql, args...); err != nil {
|
if err := r.db.QueryRow(query, recipeId).Scan(&recipeOwnerId); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
return false, fmt.Errorf("Failed to get recipe owner id: %s", err.Error())
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("Failed to get recipe owner id: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipeOwnerId == userId, nil
|
return recipeOwnerId == userId, nil
|
||||||
|
|||||||
@ -5,12 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository struct {
|
type UserRepository struct {
|
||||||
db *sqlx.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check to ensure the UserRepository implements domain.UserRepository
|
// Compile-time check to ensure the UserRepository implements domain.UserRepository
|
||||||
@ -18,7 +17,7 @@ var _ domain.UserRepository = (*UserRepository)(nil)
|
|||||||
|
|
||||||
// NewUserRepository creates a user repository object which is used by the user service to access
|
// NewUserRepository creates a user repository object which is used by the user service to access
|
||||||
// the database. Any user related database operations will take place in this repository.
|
// the database. Any user related database operations will take place in this repository.
|
||||||
func NewUserRepository(db *sqlx.DB) domain.UserRepository {
|
func NewUserRepository(db *sql.DB) domain.UserRepository {
|
||||||
return &UserRepository{db: db}
|
return &UserRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
package loggers
|
package loggers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DatabaseLogger struct {
|
type DatabaseLogger struct {
|
||||||
db *sqlx.DB
|
db *sql.DB
|
||||||
table string
|
table string
|
||||||
filter logging.LogLevel
|
filter logging.LogLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ logging.Logger = (*DatabaseLogger)(nil)
|
var _ logging.Logger = (*DatabaseLogger)(nil)
|
||||||
|
|
||||||
func NewDatabaseLogger(conn *sqlx.DB, table string, filter logging.LogLevel) (logging.Logger, error) {
|
func NewDatabaseLogger(conn *sql.DB, table string, filter logging.LogLevel) (logging.Logger, error) {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return &DatabaseLogger{}, fmt.Errorf("Connection is nil, something is very wrong.")
|
return &DatabaseLogger{}, fmt.Errorf("Connection is nil, something is very wrong.")
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ func NewDatabaseLogger(conn *sqlx.DB, table string, filter logging.LogLevel) (lo
|
|||||||
|
|
||||||
// tableExists queries a database connection and returns whether the table name provided
|
// tableExists queries a database connection and returns whether the table name provided
|
||||||
// exists on the table.
|
// exists on the table.
|
||||||
func tableExists(conn *sqlx.DB, tableName string) (bool, error) {
|
func tableExists(conn *sql.DB, tableName string) (bool, error) {
|
||||||
var exists bool
|
var exists bool
|
||||||
err := conn.QueryRow(`
|
err := conn.QueryRow(`
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
|
|||||||
@ -127,12 +127,8 @@ export default function RecipePage() {
|
|||||||
<MadeButton id={recipe.Id} />
|
<MadeButton id={recipe.Id} />
|
||||||
<ShareButton id={recipe.Id} />
|
<ShareButton id={recipe.Id} />
|
||||||
|
|
||||||
{isAuthor && (
|
{isAuthor && <DeleteButton clickHandler={deleteHandler} />}
|
||||||
<>
|
{isAuthor && <EditButton clickHandler={editHandler} />}
|
||||||
<DeleteButton clickHandler={deleteHandler} />
|
|
||||||
<EditButton clickHandler={editHandler} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
<div className="px-4 py-8 md:px-8">
|
<div className="px-4 py-8 md:px-8">
|
||||||
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user