diff --git a/go/.air.toml b/go/.air.toml index be1ffe3..e4d525a 100644 --- a/go/.air.toml +++ b/go/.air.toml @@ -7,7 +7,7 @@ args_bin = ["serve"] bin = "./tmp/main" cmd = "go build -o ./tmp/main ./cmd/bluemage" delay = 1000 -exclude_dir = ["assets", "tmp", "vendor", "testdata", "go/gen"] +exclude_dir = ["assets", "tmp", "vendor", "testdata", "go/gen", "web"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/go/api/images_list.go b/go/api/images_list.go index 3069a4b..7c86acb 100644 --- a/go/api/images_list.go +++ b/go/api/images_list.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/tigorlazuardi/bluemage/go/gen/jet/model" + "github.com/tigorlazuardi/bluemage/go/pkg/errs" "github.com/tigorlazuardi/bluemage/go/pkg/telemetry" . "github.com/go-jet/jet/v2/sqlite" @@ -50,9 +51,46 @@ type ImageListRequest struct { var imagesFTSBM25 = fmt.Sprintf("bm25(%s, 25, 20, 10, 5, 3, 3)", ImagesFts5.TableName()) func (request ImageListRequest) Statement() SelectStatement { - cond := Bool(true) + // TODO: Change top level select query to WITH query + // so the result can be sorted by Devices and Subreddits. + cond := request.WhereExpression() from := Images + if len(request.Search) > 0 { + cond.AND(ImagesFts5.ImagesFts5.EQ(String(request.Search))) + from.INNER_JOIN(ImagesFts5, Images.PostName.EQ(ImagesFts5.PostName)) + stmt := SELECT(Images.AllColumns).FROM(from).WHERE(cond) + if request.Limit > 0 { + stmt.LIMIT(request.Limit) + } + return stmt.ORDER_BY(RawString(imagesFTSBM25).DESC()) + } + stmt := SELECT(Images.AllColumns).FROM(from).WHERE(cond) + if request.Limit > 0 { + stmt.LIMIT(request.Limit) + } + + if request.OrderBy == "" { + return stmt.ORDER_BY(Images.CreatedAt.DESC()) + } + + orderBy := StringColumn(request.OrderBy) + if request.Sort == SortDesc { + return stmt.ORDER_BY(orderBy.DESC()) + } + return stmt.ORDER_BY(orderBy.ASC()) +} + +func (request ImageListRequest) WhereExpression() BoolExpression { + cond := Bool(true) + if len(request.Devices) > 0 { + cond.AND(Images.Device.IN(stringSliceExpressions(request.Devices)...)) + } + + if len(request.Subreddits) > 0 { + cond.AND(Images.Subreddit.IN(stringSliceExpressions(request.Subreddits)...)) + } + if request.NSFW != nil { n := *request.NSFW if n { @@ -78,32 +116,32 @@ func (request ImageListRequest) Statement() SelectStatement { if request.Before > 0 { cond.AND(Images.CreatedAt.GT_EQ(Int(request.Before))) } - - stmt := SELECT(Images.AllColumns).FROM(from).WHERE(cond) - if request.Limit > 0 { - stmt.LIMIT(request.Limit) - } - - if len(request.Search) > 0 { - from.INNER_JOIN(ImagesFts5, Images.PostName.EQ(ImagesFts5.PostName)) - cond.AND(ImagesFts5.ImagesFts5.EQ(String(request.Search))) - return stmt.ORDER_BY(RawString(imagesFTSBM25).ASC()) - } - - if request.OrderBy == "" { - return stmt.ORDER_BY(Images.CreatedAt.DESC()) - } - - orderBy := StringColumn(request.OrderBy) - if request.Sort == SortDesc { - return stmt.ORDER_BY(orderBy.DESC()) - } - return stmt.ORDER_BY(orderBy.ASC()) + return cond } -func (api *API) ImageList(ctx context.Context, request ImageListRequest) (images []*model.Images, err error) { +// ImageList list images by request. +func (api *API) ImageList(ctx context.Context, request ImageListRequest) (images []model.Images, err error) { ctx, span := tracer.Start(ctx, "ImageList") defer func() { telemetry.EndWithStatus(span, err) }() + stmt := request.Statement() + if err := stmt.QueryContext(ctx, api.DB, &images); err != nil { + return images, errs.Wrapw(err, "failed to list images", + "request", request, + "query", stmt.DebugSql(), + ) + } + return images, err } + +func stringSliceExpressions(s []string) []Expression { + var expr []Expression + if s != nil { + expr = make([]Expression, len(s)) + for i, str := range s { + expr[i] = String(str) + } + } + return expr +} diff --git a/go/converts/images.go b/go/converts/images.go new file mode 100644 index 0000000..5e7543a --- /dev/null +++ b/go/converts/images.go @@ -0,0 +1,102 @@ +package converts + +import ( + "strings" + + "github.com/tigorlazuardi/bluemage/go/api" + "github.com/tigorlazuardi/bluemage/go/gen/jet/model" + images "github.com/tigorlazuardi/bluemage/go/gen/proto/images/v1" +) + +// goverter:converter +// goverter:extend BoolToInt8 +// goverter:extend Int8ToBool +// goverter:extend PtrBoolToOmitInt8 +// goverter:extend BoolToOmitInt8 +// goverter:extend PtrStringToOmitString +// goverter:extend PtrFloat64ToOmitFloat64 +// goverter:extend PtrInt32ToOmitInt32 +// goverter:extend PtrInt64ToOmitInt64 +// goverter:extend PtrIntToOmitInt +// goverter:extend PtrInt8ToOmitInt8 +// goverter:extend IntToOmitInt +// goverter:extend Int8ToOmitInt8 +// goverter:extend Int32ToOmitInt32 +// goverter:extend Int64ToOmitInt64 +// goverter:extend Float64ToOmitFloat64 +// goverter:extend StringToOmitString +// +// goverter:extend ImagesOrderByToString +// goverter:extend ImagesSortToAPISort +// goverter:extend ImagesNSFWToPtrBool +// goverter:extend ImagesBlacklistToPtrBool +// goverter:extend Int8ToImagesNSFW +// goverter:extend Int8ToImagesBlacklist +type ImageConverter interface { + // goverter:useZeroValueOnPointerInconsistency + // goverter:map Nsfw NSFW + ProtoListImagesRequestToAPIImagesRequest(*images.ListImagesRequest) api.ImageListRequest + + // goverter:map ID Id + // goverter:map PostURL PostUrl + // goverter:map PostAuthorURL PostAuthorUrl + // goverter:map ImageOriginalURL ImageOriginalUrl + // goverter:ignore state sizeCache unknownFields + // goverter:useZeroValueOnPointerInconsistency + JetImageToProtoImage(model.Images) *images.Image + JetImagesToProtoImages([]model.Images) []*images.Image +} + +func ImagesOrderByToString(order images.OrderBy) string { + if order == images.OrderBy_ORDER_BY_UNSPECIFIED { + return "" + } + field := strings.TrimPrefix(images.OrderBy_name[int32(order)], "ORDER_BY_") + field = strings.ToLower(field) + return field +} + +func ImagesSortToAPISort(s images.Sort) api.Sort { + if s == images.Sort_SORT_DESCENDING { + return api.SortDesc + } + return api.SortAsc +} + +func ImagesNSFWToPtrBool(n images.NSFW) *bool { + if n == images.NSFW_NSFW_UNSPECIFIED { + return nil + } + b := (n == images.NSFW_NSFW_TRUE) + return &b +} + +func Int8ToImagesNSFW(i int8) images.NSFW { + switch i { + case 1: + return images.NSFW_NSFW_TRUE + case 0: + return images.NSFW_NSFW_FALSE + default: + return images.NSFW_NSFW_UNSPECIFIED + } +} + +func ImagesBlacklistToPtrBool(b images.Blacklist) *bool { + if b == images.Blacklist_BLACKLIST_UNSPECIFIED { + return nil + } + t := (b == images.Blacklist_BLACKLIST_TRUE) + return &t +} + +func Int8ToImagesBlacklist(i int8) images.Blacklist { + switch i { + case 1: + return images.Blacklist_BLACKLIST_TRUE + case 0: + return images.Blacklist_BLACKLIST_FALSE + default: + return images.Blacklist_BLACKLIST_UNSPECIFIED + } +} diff --git a/go/converts/utils.go b/go/converts/utils.go index b5d1d2e..dee3d0e 100644 --- a/go/converts/utils.go +++ b/go/converts/utils.go @@ -1,6 +1,8 @@ package converts -import "github.com/aarondl/opt/omit" +import ( + "github.com/aarondl/opt/omit" +) func BoolToInt8(b bool) int8 { if b { diff --git a/go/server/image_handlers.go b/go/server/image_handlers.go index 854da4a..6ef42f3 100644 --- a/go/server/image_handlers.go +++ b/go/server/image_handlers.go @@ -5,14 +5,27 @@ import ( "connectrpc.com/connect" "github.com/tigorlazuardi/bluemage/go/api" + "github.com/tigorlazuardi/bluemage/go/gen/converter" images "github.com/tigorlazuardi/bluemage/go/gen/proto/images/v1" + "github.com/tigorlazuardi/bluemage/go/pkg/errs" ) type ImageHandler struct { API *api.API } +var imageConverter = converter.ImageConverterImpl{} + func (im *ImageHandler) ListImages(ctx context.Context, request *connect.Request[images.ListImagesRequest]) (*connect.Response[images.ListImagesResponse], error) { + listRequest := imageConverter.ProtoListImagesRequestToAPIImagesRequest(request.Msg) + + list, err := im.API.ImageList(ctx, listRequest) + if err != nil { + return nil, errs.IntoConnectError(err) + } + + _ = list + panic("not implemented") // TODO: Implement } diff --git a/schemas/migrations/20240808143100_create_images_table.sql b/schemas/migrations/20240808143100_create_images_table.sql index b048a28..f519fbb 100644 --- a/schemas/migrations/20240808143100_create_images_table.sql +++ b/schemas/migrations/20240808143100_create_images_table.sql @@ -12,8 +12,8 @@ CREATE TABLE images( post_author_url VARCHAR(255) NOT NULL, image_relative_path VARCHAR(255) NOT NULL, image_original_url VARCHAR(255) NOT NULL, - image_height INTEGER NOT NULL DEFAULT 0, - image_width INTEGER NOT NULL DEFAULT 0, + image_height BIGINT NOT NULL DEFAULT 0, + image_width BIGINT NOT NULL DEFAULT 0, image_size BIGINT NOT NULL DEFAULT 0, thumbnail_relative_path VARCHAR(255) NOT NULL DEFAULT '', nsfw TINYINT NOT NULL DEFAULT 0, diff --git a/schemas/proto/images/v1/recently_added.proto b/schemas/proto/images/v1/recently_added.proto deleted file mode 100644 index 8844f74..0000000 --- a/schemas/proto/images/v1/recently_added.proto +++ /dev/null @@ -1,56 +0,0 @@ -syntax = "proto3"; - -package images.v1; - -import "buf/validate/validate.proto"; -import "images/v1/types.proto"; - -message RecentlyAddedImagesRequest { - option (buf.validate.message).cel = { - id: "RecentlyAddedImagesRequest.after_cannot_exist_with_before" - message: "after and before cannot be set at the same time" - expression: "this.after > 0 && this.before > 0" - }; - // subreddits filter the images to be fetched belonging to the given subreddits. - // - // If empty, images from all subreddits will be fetched. - repeated string subreddits = 1; - // devices is a filter the images to be fetched belonging to the given devices slug. - // - // If empty, images from all devices will be fetched. - repeated string devices = 2; - // limit limits the number of images to be fetched. - // - // if 0 (or not set), the default limit is 100. - // - // if set, the maximum limit is clamped to 300. - int64 limit = 3 [(buf.validate.field).int64.gte = 0]; - // after lists the image after the given timestamp. - // - // using after will return images that are created after the given timestamp. - // - // cannot be set (value > 0) with before (value > 0). - int64 after = 4; - // before lists the image before the given timestamp. - // - // using before will return images that are created before the given timestamp. - // - // cannot be set (value > 0) with after (value > 0). - int64 before = 5; -} - -message RecentlyAddedImagesResponse { - // groups are images grouped by devices. - // - // devices are sorted alphabetically by it's NAME not slug. - repeated GroupedBySubredditDevices groups = 1; - // after is a unix epoch timestamp given to the client to be used as after in the next request. - // - // Given if the request has after value set and there are more images to be fetched. - optional int64 after = 2; - - // before is a unix epoch timestamp given to the client to be used as before in the next request. - // - // Given if the request has before value set and there are more images to be fetched. - optional int64 before = 3; -} diff --git a/schemas/proto/images/v1/types.proto b/schemas/proto/images/v1/types.proto index 1b1a6eb..2f72ebc 100644 --- a/schemas/proto/images/v1/types.proto +++ b/schemas/proto/images/v1/types.proto @@ -27,12 +27,12 @@ message Image { message GroupedByDeviceImages { string slug = 1; string name = 2; - repeated Image images = 3; + repeated GroupedBySubredditImages subreddits = 3; } -message GroupedBySubredditDevices { +message GroupedBySubredditImages { string subreddit = 1; - repeated GroupedByDeviceImages devices = 2; + repeated Image images = 2; } enum OrderBy {