devices: added count devices endpoint

This commit is contained in:
Tigor Hutasuhut 2024-08-08 22:51:53 +07:00
parent ea92229dcc
commit ab86ff19a5
12 changed files with 143 additions and 91 deletions

39
go/api/devices_count.go Normal file
View file

@ -0,0 +1,39 @@
package api
import (
"context"
"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"
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
)
func queryFromCountDevicesRequest(req *device.CountDevicesRequest) (expr []bob.Mod[*dialect.SelectQuery]) {
switch req.Disabled {
case device.DisabledFilter_DISABLED_FILTER_TRUE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(1))
case device.DisabledFilter_DISABLED_FILTER_FALSE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(0))
}
if req.Search != "" {
arg := sqlite.Arg("%" + req.Search + "%")
expr = append(expr,
sm.Where(
models.DeviceColumns.Name.Like(arg).Or(models.DeviceColumns.Slug.Like(arg)),
),
)
}
return expr
}
func (api *API) DevicesCount(ctx context.Context, request *device.CountDevicesRequest) (uint64, error) {
count, err := models.Devices.Query(ctx, api.DB, queryFromCountDevicesRequest(request)...).Count()
if err != nil {
return 0, errs.Wrapw(err, "failed to count devices", "request", request)
}
return uint64(count), nil
}

View file

@ -14,7 +14,21 @@ import (
) )
func queryFromListDeviceRequest(req *device.ListDevicesRequest) (expr []bob.Mod[*dialect.SelectQuery]) { func queryFromListDeviceRequest(req *device.ListDevicesRequest) (expr []bob.Mod[*dialect.SelectQuery]) {
expr = countQueryFromListDeviceRequest(req) switch req.Disabled {
case device.DisabledFilter_DISABLED_FILTER_TRUE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(1))
case device.DisabledFilter_DISABLED_FILTER_FALSE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(0))
}
if req.Search != "" {
arg := sqlite.Arg("%" + req.Search + "%")
expr = append(expr,
sm.Where(
models.DeviceColumns.Name.Like(arg).Or(models.DeviceColumns.Slug.Like(arg)),
),
)
}
if req.Limit > 0 { if req.Limit > 0 {
expr = append(expr, sm.Limit(req.Limit)) expr = append(expr, sm.Limit(req.Limit))
@ -41,46 +55,12 @@ func queryFromListDeviceRequest(req *device.ListDevicesRequest) (expr []bob.Mod[
return expr return expr
} }
func countQueryFromListDeviceRequest(req *device.ListDevicesRequest) (expr []bob.Mod[*dialect.SelectQuery]) {
switch req.Disabled {
case device.DisabledFilter_DISABLED_FILTER_TRUE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(1))
case device.DisabledFilter_DISABLED_FILTER_FALSE:
expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(0))
}
if req.Search != "" {
arg := sqlite.Arg("%" + req.Search + "%")
expr = append(expr,
sm.Where(
models.DeviceColumns.Name.Like(arg).Or(models.DeviceColumns.Slug.Like(arg)),
),
)
}
return expr
}
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) {
resp = &device.ListDevicesResponse{} resp = &device.ListDevicesResponse{}
results, err := models.Devices.Query(ctx, api.DB, queryFromListDeviceRequest(req)...).All() results, err := models.Devices.Query(ctx, api.DB, queryFromListDeviceRequest(req)...).All()
if err != nil { if err != nil {
return resp, errs.Wrapw(err, "failed to list devices", "request", req) return resp, errs.Wrapw(err, "failed to list devices", "request", req)
} }
if req.Disabled == device.DisabledFilter_DISABLED_FILTER_UNSPECIFIED && req.Search == "" {
const metricName = "devices.count"
metric, err := models.FindMetric(ctx, api.DB, metricName)
if err != nil {
return resp, errs.Wrapw(err, "failed to find devices count metric", "metric", metricName)
}
resp.Count = uint64(metric.Value)
} else {
count, err := models.Devices.Query(ctx, api.DB, countQueryFromListDeviceRequest(req)...).Count()
if err != nil {
return resp, errs.Wrapw(err, "failed to count query result")
}
resp.Count = uint64(count)
}
for _, result := range results { for _, result := range results {
resp.Devices = append(resp.Devices, convert.ModelsDeviceToGetDeviceResponse(result)) resp.Devices = append(resp.Devices, convert.ModelsDeviceToGetDeviceResponse(result))
} }

View file

@ -81,7 +81,8 @@ var Cmd = &cobra.Command{
if err != nil && !errors.Is(err, http.ErrServerClosed) { if err != nil && !errors.Is(err, http.ErrServerClosed) {
return errs.Wrap(err, "failed to serve") return errs.Wrap(err, "failed to serve")
} }
return errors.Join(sqldb.Close()) slog.Info("ConnectRPC server stopped")
return errors.Join(sqldb.Close(), prettyHandler.Flush())
}, },
SilenceUsage: true, SilenceUsage: true,
} }

View file

@ -14,7 +14,7 @@ import (
// goverter:extend PtrFloat64ToOmitFloat64 // goverter:extend PtrFloat64ToOmitFloat64
// goverter:extend PtrInt32ToOmitInt32 // goverter:extend PtrInt32ToOmitInt32
type DeviceConverter interface { type DeviceConverter interface {
// goverter:ignore CreatedAt UpdatedAt // goverter:ignore CreatedAt UpdatedAt R
// goverter:map Nsfw NSFW // goverter:map Nsfw NSFW
CreateDeviceRequestToModelsDevice(*device.CreateDeviceRequest) *models.Device CreateDeviceRequestToModelsDevice(*device.CreateDeviceRequest) *models.Device
// goverter:ignore state sizeCache unknownFields // goverter:ignore state sizeCache unknownFields

View file

@ -83,3 +83,16 @@ func (de *DeviceHandler) DeviceExists(ctx context.Context, request *connect.Requ
} }
return connect.NewResponse(resp), nil return connect.NewResponse(resp), nil
} }
// CountDevices count the number of devices.
func (de *DeviceHandler) CountDevices(ctx context.Context, request *connect.Request[device.CountDevicesRequest]) (*connect.Response[device.CountDevicesResponse], error) {
count, err := de.API.DevicesCount(ctx, request.Msg)
if err != nil {
return nil, errs.IntoConnectError(err)
}
resp := &device.CountDevicesResponse{
Count: count,
}
return connect.NewResponse(resp), nil
}

View file

@ -1,21 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE metrics (
name VARCHAR(255) NOT NULL PRIMARY KEY COLLATE NOCASE,
value BIGINT DEFAULT 0 NOT NULL,
created_at BIGINT DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at BIGINT DEFAULT (strftime('%s', 'now')) NOT NULL
);
CREATE UNIQUE INDEX idx_metrics_name ON metrics(name);
CREATE TRIGGER update_metrics_timestamp_after_update AFTER UPDATE ON metrics FOR EACH ROW
BEGIN
UPDATE metrics SET updated_at = (strftime('%s', 'now')) WHERE name = old.name;
END;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE metrics;
-- +goose StatementEnd

View file

@ -12,22 +12,6 @@ CREATE TABLE subreddits (
); );
CREATE UNIQUE INDEX idx_subreddits_name ON subreddits(name); CREATE UNIQUE INDEX idx_subreddits_name ON subreddits(name);
CREATE TRIGGER subreddits_update_timestamp AFTER UPDATE ON subreddits FOR EACH ROW
BEGIN
UPDATE subreddits SET updated_at = (strftime('%s', 'now')) WHERE name = old.name;
END;
CREATE TRIGGER subreddits_insert_create_total_image_metrics AFTER INSERT on subreddits FOR EACH ROW
BEGIN
INSERT INTO metrics (name, value)
VALUES (CONCAT('subreddits.', NEW.name, '.total_images'), 0);
END;
CREATE TRIGGER subreddits_delete_total_image_metrics AFTER DELETE on subreddits FOR EACH ROW
BEGIN
DELETE FROM metrics WHERE name = CONCAT('subreddits.', OLD.name, '.total_images');
END;
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down

View file

@ -17,8 +17,6 @@ CREATE TABLE devices (
updated_at BIGINT NOT NULL DEFAULT (strftime('%s', 'now')) updated_at BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
); );
INSERT INTO metrics(name) VALUES ('devices.count');
CREATE UNIQUE INDEX idx_devices_unique_slug ON devices(slug); CREATE UNIQUE INDEX idx_devices_unique_slug ON devices(slug);
CREATE TRIGGER devices_update_timestamp AFTER UPDATE ON devices FOR EACH ROW CREATE TRIGGER devices_update_timestamp AFTER UPDATE ON devices FOR EACH ROW
@ -26,22 +24,9 @@ BEGIN
UPDATE devices SET updated_at = strftime('%s', 'now') WHERE slug = old.slug; UPDATE devices SET updated_at = strftime('%s', 'now') WHERE slug = old.slug;
END; END;
CREATE TRIGGER devices_insert_create_total_image_metrics AFTER INSERT on devices FOR EACH ROW
BEGIN
INSERT INTO metrics (name, value)
VALUES (CONCAT('devices.', NEW.slug, '.total_images'), 0);
UPDATE metrics SET value = value + 1 WHERE name = 'devices.count';
END;
CREATE TRIGGER devices_delete_total_image_metrics AFTER DELETE on devices FOR EACH ROW
BEGIN
DELETE FROM metrics WHERE name = CONCAT('devices.', OLD.slug, '.total_images');
UPDATE metrics SET value = value - 1 WHERE name = 'devices.count';
END;
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down
-- +goose StatementBegin -- +goose StatementBegin
DROP TABLE devices; DROP TABLE devices;
DELETE FROM metrics WHERE name = 'devices.count';
-- +goose StatementEnd -- +goose StatementEnd

View file

@ -0,0 +1,42 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE images(
id INTEGER PRIMARY KEY,
subreddit VARCHAR(255) NOT NULL COLLATE NOCASE,
device VARCHAR(250) NOT NULL COLLATE NOCASE,
post_title VARCHAR(255) NOT NULL,
post_name VARCHAR(255) NOT NULL,
post_url VARCHAR(255) NOT NULL,
post_created BIGINT NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_author VARCHAR(50) NOT NULL,
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_size BIGINT NOT NULL DEFAULT 0,
thumbnail_relative_path VARCHAR(255) NOT NULL DEFAULT '',
nsfw INTEGER NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
CONSTRAINT fk_image_subreddit
FOREIGN KEY (subreddit)
REFERENCES subreddits(name)
ON DELETE CASCADE,
CONSTRAINT fk_image_devices_slug
FOREIGN KEY (device)
REFERENCES devices(slug)
ON DELETE CASCADE
);
CREATE INDEX idx_subreddit_images ON images(subreddit);
CREATE INDEX idx_subreddit_device_images ON images(device, subreddit);
CREATE INDEX idx_nsfw_images ON images(nsfw);
CREATE INDEX idx_images_created_at_nsfw ON images(created_at DESC, nsfw);
CREATE UNIQUE INDEX idx_unique_images_per_device ON images(device, post_name);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE images;
-- +goose StatementEnd

View file

@ -0,0 +1,26 @@
syntax = "proto3";
package device.v1;
import "device/v1/list.proto";
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1";
message CountDevicesRequest {
// Limits the counts to devices that have the given name.
// case insensitive.
//
// Ignored if empty.
//
// default: empty string.
string search = 1;
// disabled limit the counting to devices with the given status.
//
// If unspecified, devices with either status will be counted.
DisabledFilter disabled = 2;
}
message CountDevicesResponse {
uint64 count = 1;
}

View file

@ -2,6 +2,7 @@ syntax = "proto3";
package device.v1; package device.v1;
import "device/v1/count.proto";
import "device/v1/create.proto"; import "device/v1/create.proto";
import "device/v1/exists.proto"; import "device/v1/exists.proto";
import "device/v1/get.proto"; import "device/v1/get.proto";
@ -33,4 +34,7 @@ service DeviceService {
// DeviceExists checks if a device exists in the database. // DeviceExists checks if a device exists in the database.
rpc DeviceExists(DeviceExistsRequest) returns (DeviceExistsResponse) {} rpc DeviceExists(DeviceExistsRequest) returns (DeviceExistsResponse) {}
// CountDevices count the number of devices.
rpc CountDevices(CountDevicesRequest) returns (CountDevicesResponse) {}
} }

View file

@ -47,8 +47,7 @@ message ListDevicesRequest {
} }
message ListDevicesResponse { message ListDevicesResponse {
uint64 count = 1; repeated GetDeviceResponse devices = 1;
repeated GetDeviceResponse devices = 2;
} }
enum DisabledFilter { enum DisabledFilter {