devices: added count devices endpoint
This commit is contained in:
parent
ea92229dcc
commit
ab86ff19a5
39
go/api/devices_count.go
Normal file
39
go/api/devices_count.go
Normal 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
|
||||||
|
}
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
42
schemas/migrations/20240808143100_create_images_table.sql
Normal file
42
schemas/migrations/20240808143100_create_images_table.sql
Normal 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
|
26
schemas/proto/device/v1/count.proto
Normal file
26
schemas/proto/device/v1/count.proto
Normal 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;
|
||||||
|
}
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue