image: update image list api
This commit is contained in:
parent
e45e0c0d2b
commit
28ddbd3bee
|
@ -7,7 +7,7 @@ args_bin = ["serve"]
|
||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "go build -o ./tmp/main ./cmd/bluemage"
|
cmd = "go build -o ./tmp/main ./cmd/bluemage"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "go/gen"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "go/gen", "web"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/tigorlazuardi/bluemage/go/gen/jet/model"
|
"github.com/tigorlazuardi/bluemage/go/gen/jet/model"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
||||||
|
|
||||||
. "github.com/go-jet/jet/v2/sqlite"
|
. "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())
|
var imagesFTSBM25 = fmt.Sprintf("bm25(%s, 25, 20, 10, 5, 3, 3)", ImagesFts5.TableName())
|
||||||
|
|
||||||
func (request ImageListRequest) Statement() SelectStatement {
|
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
|
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 {
|
if request.NSFW != nil {
|
||||||
n := *request.NSFW
|
n := *request.NSFW
|
||||||
if n {
|
if n {
|
||||||
|
@ -78,32 +116,32 @@ func (request ImageListRequest) Statement() SelectStatement {
|
||||||
if request.Before > 0 {
|
if request.Before > 0 {
|
||||||
cond.AND(Images.CreatedAt.GT_EQ(Int(request.Before)))
|
cond.AND(Images.CreatedAt.GT_EQ(Int(request.Before)))
|
||||||
}
|
}
|
||||||
|
return cond
|
||||||
stmt := SELECT(Images.AllColumns).FROM(from).WHERE(cond)
|
|
||||||
if request.Limit > 0 {
|
|
||||||
stmt.LIMIT(request.Limit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(request.Search) > 0 {
|
// ImageList list images by request.
|
||||||
from.INNER_JOIN(ImagesFts5, Images.PostName.EQ(ImagesFts5.PostName))
|
func (api *API) ImageList(ctx context.Context, request ImageListRequest) (images []model.Images, err error) {
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) ImageList(ctx context.Context, request ImageListRequest) (images []*model.Images, err error) {
|
|
||||||
ctx, span := tracer.Start(ctx, "ImageList")
|
ctx, span := tracer.Start(ctx, "ImageList")
|
||||||
defer func() { telemetry.EndWithStatus(span, err) }()
|
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
|
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
|
||||||
|
}
|
||||||
|
|
102
go/converts/images.go
Normal file
102
go/converts/images.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package converts
|
package converts
|
||||||
|
|
||||||
import "github.com/aarondl/opt/omit"
|
import (
|
||||||
|
"github.com/aarondl/opt/omit"
|
||||||
|
)
|
||||||
|
|
||||||
func BoolToInt8(b bool) int8 {
|
func BoolToInt8(b bool) int8 {
|
||||||
if b {
|
if b {
|
||||||
|
|
|
@ -5,14 +5,27 @@ import (
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/tigorlazuardi/bluemage/go/api"
|
"github.com/tigorlazuardi/bluemage/go/api"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/gen/converter"
|
||||||
images "github.com/tigorlazuardi/bluemage/go/gen/proto/images/v1"
|
images "github.com/tigorlazuardi/bluemage/go/gen/proto/images/v1"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageHandler struct {
|
type ImageHandler struct {
|
||||||
API *api.API
|
API *api.API
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var imageConverter = converter.ImageConverterImpl{}
|
||||||
|
|
||||||
func (im *ImageHandler) ListImages(ctx context.Context, request *connect.Request[images.ListImagesRequest]) (*connect.Response[images.ListImagesResponse], error) {
|
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
|
panic("not implemented") // TODO: Implement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ CREATE TABLE images(
|
||||||
post_author_url VARCHAR(255) NOT NULL,
|
post_author_url VARCHAR(255) NOT NULL,
|
||||||
image_relative_path VARCHAR(255) NOT NULL,
|
image_relative_path VARCHAR(255) NOT NULL,
|
||||||
image_original_url VARCHAR(255) NOT NULL,
|
image_original_url VARCHAR(255) NOT NULL,
|
||||||
image_height INTEGER NOT NULL DEFAULT 0,
|
image_height BIGINT NOT NULL DEFAULT 0,
|
||||||
image_width INTEGER NOT NULL DEFAULT 0,
|
image_width BIGINT NOT NULL DEFAULT 0,
|
||||||
image_size BIGINT NOT NULL DEFAULT 0,
|
image_size BIGINT NOT NULL DEFAULT 0,
|
||||||
thumbnail_relative_path VARCHAR(255) NOT NULL DEFAULT '',
|
thumbnail_relative_path VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
nsfw TINYINT NOT NULL DEFAULT 0,
|
nsfw TINYINT NOT NULL DEFAULT 0,
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -27,12 +27,12 @@ message Image {
|
||||||
message GroupedByDeviceImages {
|
message GroupedByDeviceImages {
|
||||||
string slug = 1;
|
string slug = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
repeated Image images = 3;
|
repeated GroupedBySubredditImages subreddits = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupedBySubredditDevices {
|
message GroupedBySubredditImages {
|
||||||
string subreddit = 1;
|
string subreddit = 1;
|
||||||
repeated GroupedByDeviceImages devices = 2;
|
repeated Image images = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OrderBy {
|
enum OrderBy {
|
||||||
|
|
Loading…
Reference in a new issue