refactor: now uses jet sql as much as possible

This commit is contained in:
Tigor Hutasuhut 2024-08-15 09:36:06 +07:00
parent 4872e653ef
commit 8bb8cb30ec
9 changed files with 128 additions and 63 deletions

View file

@ -2,25 +2,43 @@ package api
import ( import (
"context" "context"
"errors"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/tigorlazuardi/bluemage/go/gen/models"
"github.com/tigorlazuardi/bluemage/go/pkg/errs" "github.com/tigorlazuardi/bluemage/go/pkg/errs"
"github.com/tigorlazuardi/bluemage/go/pkg/log" "github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
"github.com/go-jet/jet/v2/qrm"
"github.com/tigorlazuardi/bluemage/go/gen/jet/model"
. "github.com/go-jet/jet/v2/sqlite"
. "github.com/tigorlazuardi/bluemage/go/gen/jet/table"
) )
func (api *API) GetDevice(ctx context.Context, slug string) (device *models.Device, err error) { func (api *API) GetDevice(ctx context.Context, slug string) (device model.Devices, err error) {
ctx, coll := log.WithQueryCollector(ctx) ctx, span := tracer.Start(ctx, "GetDevice")
device, err = models.FindDevice(ctx, api.Executor, slug) defer func() { telemetry.EndWithStatus(span, err) }()
if err != nil {
if err.Error() == "sql: no rows in result set" {
return device, errs.Wrapw(err, "device not found",
"slug", slug,
"query", coll,
).Code(connect.CodeNotFound)
}
return device, errs.Wrapw(err, "failed to find device", "slug", slug) stmt := SELECT(Devices.AllColumns).
FROM(Devices).
WHERE(Devices.Slug.EQ(String(slug)))
err = stmt.QueryContext(ctx, api.DB, &device)
if err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return device, errs.
Wrapf(err, "device '%s' does not exist", slug).
Details(
"slug", slug,
"query", stmt.DebugSql(),
).
Code(connect.CodeNotFound)
}
return device, errs.Wrapf(err, "failed to get device '%s'", slug).
Details(
"slug", slug,
"query", stmt.DebugSql(),
)
} }
return device, nil return device, nil

View file

@ -3,66 +3,74 @@ package api
import ( import (
"strings" "strings"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
"github.com/stephenafamo/bob/dialect/sqlite/sm"
"github.com/tigorlazuardi/bluemage/go/gen/models"
device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1" device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1"
"github.com/tigorlazuardi/bluemage/go/pkg/errs" "github.com/tigorlazuardi/bluemage/go/pkg/errs"
"golang.org/x/net/context" "golang.org/x/net/context"
. "github.com/go-jet/jet/v2/sqlite"
"github.com/tigorlazuardi/bluemage/go/gen/jet/model"
. "github.com/tigorlazuardi/bluemage/go/gen/jet/table"
) )
func queryFromListDeviceRequest(req *device.ListDevicesRequest) (expr []bob.Mod[*dialect.SelectQuery]) { func listDevicesRequestSelectStatement(req *device.ListDevicesRequest) SelectStatement {
cond := Bool(true)
switch req.Disabled { switch req.Disabled {
case device.DisabledFilter_DISABLED_FILTER_TRUE: case device.DisabledFilter_DISABLED_FILTER_TRUE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(1)) cond.AND(Devices.Disabled.EQ(Int(1)))
case device.DisabledFilter_DISABLED_FILTER_FALSE: case device.DisabledFilter_DISABLED_FILTER_FALSE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(0)) cond.AND(Devices.Disabled.EQ(Int(0)))
} }
if req.Search != "" { if req.Search != "" {
arg := sqlite.Arg("%" + req.Search + "%") cond.AND(
expr = append(expr, Devices.Name.LIKE(String("%" + req.Search + "%")).
sm.Where( OR(Devices.Slug.LIKE(String("%" + req.Search + "%"))),
models.DeviceColumns.Name.Like(arg).Or(models.DeviceColumns.Slug.Like(arg)),
),
) )
} }
stmt := SELECT(Devices.AllColumns).
FROM(Devices).
WHERE(cond)
if req.Limit > 0 { if req.Limit > 0 {
expr = append(expr, sm.Limit(req.Limit)) stmt.LIMIT(int64(req.Limit))
} }
if req.Offset > 0 { if req.Offset > 0 {
expr = append(expr, sm.Offset(req.Offset)) stmt.OFFSET(int64(req.Offset))
} }
if req.OrderBy == device.OrderBy_ORDER_BY_UNSPECIFIED { if req.OrderBy == device.OrderBy_ORDER_BY_UNSPECIFIED {
return append(expr, sm.OrderBy(models.DeviceColumns.CreatedAt).Desc()) return stmt.ORDER_BY(Devices.CreatedAt.DESC())
} }
orderByField, _ := strings.CutPrefix(device.OrderBy_name[int32(req.OrderBy)], "ORDER_BY_") orderByField, _ := strings.CutPrefix(device.OrderBy_name[int32(req.OrderBy)], "ORDER_BY_")
orderByField = strings.ToLower(orderByField) orderByField = strings.ToLower(orderByField)
orderBy := sm.OrderBy(sqlite.Quote(orderByField)) orderBy := StringColumn(orderByField)
if req.Sort == device.Sort_SORT_DESCENDING {
expr = append(expr, orderBy.Desc())
} else {
expr = append(expr, orderBy.Asc())
}
return expr if req.Sort == device.Sort_SORT_DESCENDING {
return stmt.ORDER_BY(orderBy.DESC())
} else {
return stmt.ORDER_BY(orderBy.ASC())
}
} }
func (api *API) DevicesList(ctx context.Context, req *device.ListDevicesRequest) (resp *device.ListDevicesResponse, err error) { func (api *API) DevicesList(ctx context.Context, req *device.ListDevicesRequest) (resp *device.ListDevicesResponse, err error) {
ctx, span := tracer.Start(ctx, "DevicesList")
defer span.End()
resp = &device.ListDevicesResponse{} resp = &device.ListDevicesResponse{}
results, err := models.Devices.Query(ctx, api.Executor, queryFromListDeviceRequest(req)...).All() stmt := listDevicesRequestSelectStatement(req)
if err != nil { var out []model.Devices
return resp, errs.Wrapw(err, "failed to list devices", "request", req) if err := stmt.QueryContext(ctx, api.DB, &out); err != nil {
return resp, errs.Wrapw(err, "failed to list devices",
"request", req,
"query", stmt.DebugSql(),
)
} }
for _, result := range results { for _, result := range out {
resp.Devices = append(resp.Devices, convert.ModelsDeviceToGetDeviceResponse(result)) resp.Devices = append(resp.Devices, convert.JetModelDeviceToGetDeviceResponse(result))
} }
return resp, err return resp, err
} }

View file

@ -9,6 +9,7 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"time" "time"
"connectrpc.com/connect" "connectrpc.com/connect"
@ -58,18 +59,24 @@ var Cmd = &cobra.Command{
return errs.Wrapw(err, "failed to create download directory", "path", cfg.String("download.directory")) return errs.Wrapw(err, "failed to create download directory", "path", cfg.String("download.directory"))
} }
dbPath := fmt.Sprintf("file:%s", cfg.String("db.path")) dbpath := cfg.String("db.path")
sqldb, err := sql.Open(cfg.String("db.driver"), dbPath) dir := filepath.Dir(dbpath)
if err := os.MkdirAll(dir, 0755); err != nil {
return errs.Wrapw(err, "failed to create database directory", "path", dir)
}
dsn := fmt.Sprintf("file:%s", cfg.String("db.path"))
sqldb, err := sql.Open(cfg.String("db.driver"), dsn)
if err != nil { if err != nil {
return errs.Wrapw(err, "failed to open database", return errs.Wrapw(err, "failed to open database",
"driver", cfg.String("db.driver"), "driver", cfg.String("db.driver"),
"dsn", cfg.String("db.dsn"), "dsn", dsn,
) )
} }
cleanups = append(cleanups, sqldb.Close) cleanups = append(cleanups, sqldb.Close)
sqldb = sqldblogger.OpenDriver( sqldb = sqldblogger.OpenDriver(
dbPath, dsn,
sqldb.Driver(), sqldb.Driver(),
log.SQLLogger{}, log.SQLLogger{},
sqldblogger.WithSQLQueryAsMessage(true), sqldblogger.WithSQLQueryAsMessage(true),
@ -101,6 +108,7 @@ var Cmd = &cobra.Command{
mux.Handle(v1connect.NewDeviceServiceHandler(handler, connect.WithInterceptors( mux.Handle(v1connect.NewDeviceServiceHandler(handler, connect.WithInterceptors(
validationInterceptor, validationInterceptor,
otelInterceptor, otelInterceptor,
server.ErrorMessageInterceptor(),
server.LogInterceptor(), server.LogInterceptor(),
))) )))

View file

@ -2,6 +2,7 @@ package converts
import ( import (
"github.com/aarondl/opt/omit" "github.com/aarondl/opt/omit"
"github.com/tigorlazuardi/bluemage/go/gen/jet/model"
"github.com/tigorlazuardi/bluemage/go/gen/models" "github.com/tigorlazuardi/bluemage/go/gen/models"
device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1" device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1"
) )
@ -30,8 +31,8 @@ type DeviceConverter interface {
// goverter:ignore state sizeCache unknownFields // goverter:ignore state sizeCache unknownFields
ModelsDeviceToCreateDeviceResponse(*models.Device) *device.CreateDeviceResponse ModelsDeviceToCreateDeviceResponse(*models.Device) *device.CreateDeviceResponse
// goverter:ignore state sizeCache unknownFields // goverter:ignore state sizeCache unknownFields
// goverter:map NSFW Nsfw // goverter:useZeroValueOnPointerInconsistency
ModelsDeviceToGetDeviceResponse(*models.Device) *device.GetDeviceResponse JetModelDeviceToGetDeviceResponse(model.Devices) *device.GetDeviceResponse
// goverter:ignore Slug SingleFolderMode CreatedAt UpdatedAt // goverter:ignore Slug SingleFolderMode CreatedAt UpdatedAt
// goverter:map Nsfw NSFW // goverter:map Nsfw NSFW

View file

@ -21,7 +21,7 @@ type QueryCollector struct {
func (qu *QueryCollector) LogValue() slog.Value { func (qu *QueryCollector) LogValue() slog.Value {
return slog.GroupValue( return slog.GroupValue(
slog.String("statement", strings.ReplaceAll(qu.Statement, `"`, "")), slog.String("statement", qu.Statement),
slog.Any("args", qu.Args), slog.Any("args", qu.Args),
) )
} }

View file

@ -39,7 +39,7 @@ func (d *DeviceHandler) GetDevice(ctx context.Context, request *connect.Request[
return nil, errs.IntoConnectError(err) return nil, errs.IntoConnectError(err)
} }
devResp := deviceConvert.ModelsDeviceToGetDeviceResponse(dev) devResp := deviceConvert.JetModelDeviceToGetDeviceResponse(dev)
return connect.NewResponse(devResp), nil return connect.NewResponse(devResp), nil
} }

View file

@ -2,6 +2,7 @@ package server
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
@ -45,3 +46,27 @@ func LogInterceptor() connect.UnaryInterceptorFunc {
} }
return connect.UnaryInterceptorFunc(interceptor) return connect.UnaryInterceptorFunc(interceptor)
} }
func ErrorMessageInterceptor() connect.UnaryInterceptorFunc {
return func(uf connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) {
resp, err := uf(ctx, ar)
if err == nil {
return resp, err
}
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
return resp, err
}
if cerr := new(connect.Error); errors.As(err, &cerr) {
if e := errs.FindError(cerr); e != nil {
msg := e.GetMessage()
e.Message("[%s] %s", span.SpanContext().SpanID().String(), msg)
}
}
return resp, err
}
}
}

View file

@ -38,3 +38,20 @@ service DeviceService {
// CountDevices count the number of devices. // CountDevices count the number of devices.
rpc CountDevices(CountDevicesRequest) returns (CountDevicesResponse) {} rpc CountDevices(CountDevicesRequest) returns (CountDevicesResponse) {}
} }
message Device {
string slug = 1;
bool disabled = 2;
string name = 3;
double resolution_x = 4;
double resolution_y = 5;
double aspect_ratio_tolerance = 6;
int32 min_x = 7;
int32 min_y = 8;
int32 max_x = 9;
int32 max_y = 10;
bool nsfw = 11;
bool single_folder_mode = 12;
int64 created_at = 13;
int64 updated_at = 14;
}

View file

@ -3,6 +3,7 @@ syntax = "proto3";
package device.v1; package device.v1;
import "buf/validate/validate.proto"; import "buf/validate/validate.proto";
import "device/v1/device.proto";
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1"; option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1";
@ -12,18 +13,5 @@ message GetDeviceRequest {
} }
message GetDeviceResponse { message GetDeviceResponse {
string slug = 1; Device device = 1;
bool disabled = 2;
string name = 3;
double resolution_x = 4;
double resolution_y = 5;
double aspect_ratio_tolerance = 6;
int32 min_x = 7;
int32 min_y = 8;
int32 max_x = 9;
int32 max_y = 10;
bool nsfw = 11;
bool single_folder_mode = 12;
int64 created_at = 13;
int64 updated_at = 14;
} }