diff --git a/go.mod b/go.mod index 085ef559..6c96e19f 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgtype v1.14.3 // indirect github.com/jackc/pgx/v5 v5.5.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect diff --git a/go.sum b/go.sum index 7b51191b..6e3b3adf 100644 --- a/go.sum +++ b/go.sum @@ -309,11 +309,14 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus= +github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -553,6 +556,8 @@ golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -591,6 +596,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -628,8 +634,10 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -654,6 +662,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -706,14 +715,18 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -724,6 +737,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -785,6 +799,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/src/database/jet/postgres/public/model/product.go b/src/database/jet/postgres/public/model/product.go index df23607a..b87cfff9 100644 --- a/src/database/jet/postgres/public/model/product.go +++ b/src/database/jet/postgres/public/model/product.go @@ -27,4 +27,5 @@ type Product struct { UpdatedAt time.Time ProductTs *string Origin string + Ranking int64 } diff --git a/src/database/jet/postgres/public/table/product.go b/src/database/jet/postgres/public/table/product.go index dfe463ff..723794cc 100644 --- a/src/database/jet/postgres/public/table/product.go +++ b/src/database/jet/postgres/public/table/product.go @@ -32,6 +32,7 @@ type productTable struct { UpdatedAt postgres.ColumnTimestampz ProductTs postgres.ColumnString Origin postgres.ColumnString + Ranking postgres.ColumnInteger AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -87,7 +88,8 @@ func newProductTableImpl(schemaName, tableName, alias string) productTable { UpdatedAtColumn = postgres.TimestampzColumn("updated_at") ProductTsColumn = postgres.StringColumn("product_ts") OriginColumn = postgres.StringColumn("origin") - allColumns = postgres.ColumnList{IDColumn, TitleColumn, DescriptionColumn, ProductKeyColumn, ImageURLColumn, TotalReviewsColumn, RatingColumn, PriceColumn, CurrencyColumn, URLColumn, CategoryIDColumn, CreatedAtColumn, UpdatedAtColumn, ProductTsColumn, OriginColumn} + RankingColumn = postgres.IntegerColumn("ranking") + allColumns = postgres.ColumnList{IDColumn, TitleColumn, DescriptionColumn, ProductKeyColumn, ImageURLColumn, TotalReviewsColumn, RatingColumn, PriceColumn, CurrencyColumn, URLColumn, CategoryIDColumn, CreatedAtColumn, UpdatedAtColumn, ProductTsColumn, OriginColumn, RankingColumn} mutableColumns = postgres.ColumnList{TitleColumn, DescriptionColumn, ProductKeyColumn, ImageURLColumn, TotalReviewsColumn, RatingColumn, PriceColumn, CurrencyColumn, URLColumn, CategoryIDColumn, CreatedAtColumn, UpdatedAtColumn, OriginColumn} ) @@ -110,6 +112,7 @@ func newProductTableImpl(schemaName, tableName, alias string) productTable { UpdatedAt: UpdatedAtColumn, ProductTs: ProductTsColumn, Origin: OriginColumn, + Ranking: RankingColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/src/database/migrations/1730505143139101_product_ranking.sql b/src/database/migrations/1730505143139101_product_ranking.sql new file mode 100644 index 00000000..4157cdab --- /dev/null +++ b/src/database/migrations/1730505143139101_product_ranking.sql @@ -0,0 +1,3 @@ +ALTER TABLE "product" +ADD COLUMN "ranking" BIGINT NOT NULL + GENERATED ALWAYS AS (CEIL("total_reviews" * "rating")) STORED; diff --git a/src/database/models.go b/src/database/models.go index 518811d1..97831157 100644 --- a/src/database/models.go +++ b/src/database/models.go @@ -170,6 +170,7 @@ type Product struct { UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` ProductTs interface{} `db:"product_ts" json:"productTs"` Origin string `db:"origin" json:"origin"` + Ranking int64 `db:"ranking" json:"ranking"` } type User struct { diff --git a/src/database/product.sql.go b/src/database/product.sql.go index cd3bcec4..87fa3617 100644 --- a/src/database/product.sql.go +++ b/src/database/product.sql.go @@ -25,7 +25,7 @@ INSERT INTO "product" ( "category_id" ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $11, $8, $9, $10 -) RETURNING id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin +) RETURNING id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin, ranking ` type CreateProductParams struct { @@ -73,13 +73,14 @@ func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) (P &i.UpdatedAt, &i.ProductTs, &i.Origin, + &i.Ranking, ) return i, err } const filterProducts = `-- name: FilterProducts :many SELECT - product.id, product.title, product.description, product.product_key, product.image_url, product.total_reviews, product.rating, product.price, product.currency, product.url, product.category_id, product.created_at, product.updated_at, product.product_ts, product.origin, + product.id, product.title, product.description, product.product_key, product.image_url, product.total_reviews, product.rating, product.price, product.currency, product.url, product.category_id, product.created_at, product.updated_at, product.product_ts, product.origin, product.ranking, category.id, category.name, category.description, category.category_url, category.created_at, category.updated_at, CEIL("product"."total_reviews" * "product"."rating") AS "weight" FROM "product" @@ -150,6 +151,7 @@ func (q *Queries) FilterProducts(ctx context.Context, arg FilterProductsParams) &i.Product.UpdatedAt, &i.Product.ProductTs, &i.Product.Origin, + &i.Product.Ranking, &i.Category.ID, &i.Category.Name, &i.Category.Description, @@ -172,7 +174,7 @@ func (q *Queries) FilterProducts(ctx context.Context, arg FilterProductsParams) } const findProductById = `-- name: FindProductById :one -SELECT id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin FROM "product" +SELECT id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin, ranking FROM "product" WHERE "id" = $1 ` @@ -195,12 +197,13 @@ func (q *Queries) FindProductById(ctx context.Context, id int64) (Product, error &i.UpdatedAt, &i.ProductTs, &i.Origin, + &i.Ranking, ) return i, err } const findProductByProductKey = `-- name: FindProductByProductKey :one -SELECT id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin FROM "product" +SELECT id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin, ranking FROM "product" WHERE "product_key" = $1 ` @@ -223,6 +226,7 @@ func (q *Queries) FindProductByProductKey(ctx context.Context, productKey string &i.UpdatedAt, &i.ProductTs, &i.Origin, + &i.Ranking, ) return i, err } @@ -238,7 +242,7 @@ SET "description" = coalesce($7, "description"), "updated_at" = now() WHERE "product_key" = $1 -RETURNING id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin +RETURNING id, title, description, product_key, image_url, total_reviews, rating, price, currency, url, category_id, created_at, updated_at, product_ts, origin, ranking ` type UpdateProductParams struct { @@ -278,6 +282,7 @@ func (q *Queries) UpdateProduct(ctx context.Context, arg UpdateProductParams) (P &i.UpdatedAt, &i.ProductTs, &i.Origin, + &i.Ranking, ) return i, err } diff --git a/src/database/wish.sql.go b/src/database/wish.sql.go index 149308e7..a7fc20d9 100644 --- a/src/database/wish.sql.go +++ b/src/database/wish.sql.go @@ -69,7 +69,7 @@ func (q *Queries) DeleteWish(ctx context.Context, id int64) (int64, error) { const getAllWishesForUser = `-- name: GetAllWishesForUser :many SELECT wish.id, wish.user_id, wish.participant_id, wish.product_id, wish.event_id, wish.created_at, wish.updated_at, wish.quantity, - product.id, product.title, product.description, product.product_key, product.image_url, product.total_reviews, product.rating, product.price, product.currency, product.url, product.category_id, product.created_at, product.updated_at, product.product_ts, product.origin + product.id, product.title, product.description, product.product_key, product.image_url, product.total_reviews, product.rating, product.price, product.currency, product.url, product.category_id, product.created_at, product.updated_at, product.product_ts, product.origin, product.ranking FROM wish INNER JOIN product ON product.id = wish.product_id WHERE @@ -123,6 +123,7 @@ func (q *Queries) GetAllWishesForUser(ctx context.Context, arg GetAllWishesForUs &i.Product.UpdatedAt, &i.Product.ProductTs, &i.Product.Origin, + &i.Product.Ranking, ); err != nil { return nil, err } diff --git a/src/services/product_service.go b/src/services/product_service.go index 9328411f..96b918ff 100644 --- a/src/services/product_service.go +++ b/src/services/product_service.go @@ -3,8 +3,10 @@ package services import ( "context" "database/sql" + "encoding/base64" "fmt" "net/url" + "strconv" "github.com/giftxtrade/api/src/database" "github.com/giftxtrade/api/src/database/jet/postgres/public/table" @@ -27,13 +29,6 @@ func (service *ProductService) Search(ctx context.Context, filter types.ProductF table.Product.AllColumns, table.Category.ID, table.Category.Name, - postgres.CEIL(postgres.RawFloat(fmt.Sprintf( - "%s.%s * %s.%s", - table.Product.TableName(), - table.Product.TotalReviews.Name(), - table.Product.TableName(), - table.Product.Rating.Name(), - ))).AS("weight"), ). FROM(table.Product. INNER_JOIN(table.Category, table.Category.ID.EQ(table.Product.CategoryID), @@ -64,7 +59,7 @@ func (service *ProductService) Search(ctx context.Context, filter types.ProductF WHEN(postgres.Bool(*filter.Sort == "price")). THEN(table.Product.Price). ASC(), - postgres.FloatColumn("weight").DESC(), + table.Product.Ranking.DESC(), ). LIMIT(int64(filter.Limit)). OFFSET(int64(filter.Limit * (filter.Page - 1))) @@ -72,6 +67,76 @@ func (service *ProductService) Search(ctx context.Context, filter types.ProductF return products, err } +func (service *ProductService) SearchWithCursor(ctx context.Context, filter types.ProductFilterWithCursor) (res types.ProductsResultWithNextCursor, err error) { + res.Products = []types.Product{} + search := "" + if filter.Search != nil { + search = *filter.Search + } + var cursor_id int64 = 0 + if filter.Cursor != nil && *filter.Cursor != "" { + decoded_cursor_str, err := base64.URLEncoding.DecodeString(*filter.Cursor) + if err != nil { + return res, fmt.Errorf("invalid cursor") + } + cursor_id, err = strconv.ParseInt(string(decoded_cursor_str), 10, 64) + if err != nil { + return res, fmt.Errorf("cursor could not be parsed") + } + } + // TODO: Add condition to handle sort value + qb := table.Product. + SELECT( + table.Product.AllColumns, + table.Category.ID, + table.Category.Name, + ). + FROM(table.Product. + INNER_JOIN(table.Category, table.Category.ID.EQ(table.Product.CategoryID), + )). + WHERE( + postgres.AND( + postgres.String(search).EQ(postgres.String("")). // skips the ts_query expression if search is empty + OR( + postgres.RawBool( + fmt.Sprintf( + "%s.%s @@ to_tsquery('english', $search::TEXT)", + table.Product.ProductTs.TableName(), + table.Product.ProductTs.Name(), + ), + postgres.RawArgs{"$search": search}, + ), + ), + postgres.RawBool(fmt.Sprintf( + "%s.%s BETWEEN '$%.2f'::MONEY AND '$%.2f'::MONEY", + table.Product.TableName(), table.Product.Price.Name(), + filter.MinPrice, + filter.MaxPrice, + )), + table.Product.ID.LT_EQ(postgres.Int(cursor_id)), // handles cursor-based pagination + ), + ). + ORDER_BY( + postgres.CASE(). + WHEN(postgres.Bool(*filter.Sort == "price")). + THEN(table.Product.Price). + ASC(), + table.Product.Ranking.DESC(), + ). + LIMIT(int64(filter.Limit) + 1) // use the last element to display the next cursor + + fmt.Println(qb.DebugSql()) + err = qb.QueryContext(ctx, service.DB, &res.Products) + if err != nil && len(res.Products) > 0 { + next_cursor_id := res.Products[len(res.Products) - 1].ID + next_cursor := base64.URLEncoding.EncodeToString([]byte(fmt.Sprint(next_cursor_id))) + res.NextCursor = &next_cursor + res.Products = res.Products[:len(res.Products) - 1] + fmt.Println(res.NextCursor) + } + return res, err +} + func (service *ProductService) UpdateOrCreate(ctx context.Context, input types.CreateProduct) (database.Product, bool, error) { validation_err := service.Validator.Struct(input) if validation_err != nil { diff --git a/src/tests/product_service_test.go b/src/tests/product_service_test.go index 5f0ab908..df4afb89 100644 --- a/src/tests/product_service_test.go +++ b/src/tests/product_service_test.go @@ -183,5 +183,22 @@ func TestProductService(t *testing.T) { t.Fatal("products length is incorrect", len(products)) } }) + + t.Run("cursor", func(t *testing.T) { + // search := "tees" + sort := "rating" + res, err := product_service.SearchWithCursor(context.Background(), types.ProductFilterWithCursor{ + Limit: 10, + MinPrice: 1, + MaxPrice: 200, + Sort: &sort, + }) + if err != nil { + t.Fatal(err) + } + if len(res.Products) != 10 { + t.Fatal("products length is incorrect", len(res.Products)) + } + }) }) } \ No newline at end of file diff --git a/src/types/dto.go b/src/types/dto.go index 6fc35069..7ca5d672 100644 --- a/src/types/dto.go +++ b/src/types/dto.go @@ -93,6 +93,20 @@ type ProductFilter struct { Sort *string `json:"sort,omitempty" validate:"omitempty"` } +type ProductFilterWithCursor struct { + Search *string `json:"search,omitempty" validate:"omitempty"` + Limit int32 `json:"limit" validate:"required,min=1,max=200"` + Cursor *string `json:"cursor" validate:"omitempty"` + MinPrice float32 `json:"minPrice,omitempty" validate:"omitempty,gte=1,ltefield=MaxPrice"` + MaxPrice float32 `json:"maxPrice,omitempty" validate:"omitempty,gtefield=MinPrice"` + Sort *string `json:"sort,omitempty" validate:"omitempty"` +} + +type ProductsResultWithNextCursor struct { + Products []Product `json:"products"` + NextCursor *string `json:"next_cursor"` +} + type CreateWish struct { ProductID *int64 `json:"productId,omitempty"` }