refactor: api now uses it's own request type and handlers
will now handles the conversion of the request to the api request type
This commit is contained in:
parent
8bb8cb30ec
commit
cf14079f1f
|
@ -5,11 +5,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/stephenafamo/bob"
|
"github.com/stephenafamo/bob"
|
||||||
"github.com/tigorlazuardi/bluemage/go/gen/converter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var convert converter.DeviceConverterImpl
|
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
Executor bob.Executor
|
Executor bob.Executor
|
||||||
|
@ -21,3 +18,10 @@ func (api *API) lockf(f func()) {
|
||||||
defer api.mu.Unlock()
|
defer api.mu.Unlock()
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Sort string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SortAsc Sort = "asc"
|
||||||
|
SortDesc Sort = "desc"
|
||||||
|
)
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
@ -12,19 +9,30 @@ import (
|
||||||
. "github.com/tigorlazuardi/bluemage/go/gen/jet/table"
|
. "github.com/tigorlazuardi/bluemage/go/gen/jet/table"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listDevicesRequestSelectStatement(req *device.ListDevicesRequest) SelectStatement {
|
type ListDevicesRequest struct {
|
||||||
|
Search string
|
||||||
|
Disabled *bool
|
||||||
|
Limit int64
|
||||||
|
Offset int64
|
||||||
|
OrderBy string
|
||||||
|
Sort Sort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (request ListDevicesRequest) Statement() SelectStatement {
|
||||||
cond := Bool(true)
|
cond := Bool(true)
|
||||||
switch req.Disabled {
|
if d := request.Disabled; d != nil {
|
||||||
case device.DisabledFilter_DISABLED_FILTER_TRUE:
|
disabled := *d
|
||||||
cond.AND(Devices.Disabled.EQ(Int(1)))
|
if disabled {
|
||||||
case device.DisabledFilter_DISABLED_FILTER_FALSE:
|
cond.AND(Devices.Disabled.EQ(Int(1)))
|
||||||
cond.AND(Devices.Disabled.EQ(Int(0)))
|
} else {
|
||||||
|
cond.AND(Devices.Disabled.EQ(Int(0)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Search != "" {
|
if request.Search != "" {
|
||||||
cond.AND(
|
cond.AND(
|
||||||
Devices.Name.LIKE(String("%" + req.Search + "%")).
|
Devices.Name.LIKE(String("%" + request.Search + "%")).
|
||||||
OR(Devices.Slug.LIKE(String("%" + req.Search + "%"))),
|
OR(Devices.Slug.LIKE(String("%" + request.Search + "%"))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,45 +40,35 @@ func listDevicesRequestSelectStatement(req *device.ListDevicesRequest) SelectSta
|
||||||
FROM(Devices).
|
FROM(Devices).
|
||||||
WHERE(cond)
|
WHERE(cond)
|
||||||
|
|
||||||
if req.Limit > 0 {
|
if request.Limit > 0 {
|
||||||
stmt.LIMIT(int64(req.Limit))
|
stmt.LIMIT(request.Limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Offset > 0 {
|
if request.Offset > 0 {
|
||||||
stmt.OFFSET(int64(req.Offset))
|
stmt.OFFSET(request.Offset)
|
||||||
}
|
}
|
||||||
|
if request.OrderBy == "" {
|
||||||
if req.OrderBy == device.OrderBy_ORDER_BY_UNSPECIFIED {
|
|
||||||
return stmt.ORDER_BY(Devices.CreatedAt.DESC())
|
return stmt.ORDER_BY(Devices.CreatedAt.DESC())
|
||||||
}
|
}
|
||||||
|
|
||||||
orderByField, _ := strings.CutPrefix(device.OrderBy_name[int32(req.OrderBy)], "ORDER_BY_")
|
orderBy := StringColumn(request.OrderBy)
|
||||||
orderByField = strings.ToLower(orderByField)
|
if request.Sort == SortDesc {
|
||||||
|
|
||||||
orderBy := StringColumn(orderByField)
|
|
||||||
|
|
||||||
if req.Sort == device.Sort_SORT_DESCENDING {
|
|
||||||
return stmt.ORDER_BY(orderBy.DESC())
|
return stmt.ORDER_BY(orderBy.DESC())
|
||||||
} else {
|
} else {
|
||||||
return stmt.ORDER_BY(orderBy.ASC())
|
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 ListDevicesRequest) (resp []model.Devices, err error) {
|
||||||
ctx, span := tracer.Start(ctx, "DevicesList")
|
ctx, span := tracer.Start(ctx, "DevicesList")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
resp = &device.ListDevicesResponse{}
|
stmt := req.Statement()
|
||||||
stmt := listDevicesRequestSelectStatement(req)
|
if err := stmt.QueryContext(ctx, api.DB, &resp); err != nil {
|
||||||
var out []model.Devices
|
|
||||||
if err := stmt.QueryContext(ctx, api.DB, &out); err != nil {
|
|
||||||
return resp, errs.Wrapw(err, "failed to list devices",
|
return resp, errs.Wrapw(err, "failed to list devices",
|
||||||
"request", req,
|
"request", req,
|
||||||
"query", stmt.DebugSql(),
|
"query", stmt.DebugSql(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, result := range out {
|
|
||||||
resp.Devices = append(resp.Devices, convert.JetModelDeviceToGetDeviceResponse(result))
|
|
||||||
}
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package converts
|
package converts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aarondl/opt/omit"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/api"
|
||||||
"github.com/tigorlazuardi/bluemage/go/gen/jet/model"
|
"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"
|
||||||
|
@ -24,6 +26,9 @@ import (
|
||||||
// goverter:extend Int64ToOmitInt64
|
// goverter:extend Int64ToOmitInt64
|
||||||
// goverter:extend Float64ToOmitFloat64
|
// goverter:extend Float64ToOmitFloat64
|
||||||
// goverter:extend StringToOmitString
|
// goverter:extend StringToOmitString
|
||||||
|
// goverter:extend DeviceDisabledFilterToPtrBool
|
||||||
|
// goverter:extend DeviceOrderByToString
|
||||||
|
// goverter:extend DeviceSortToAPISort
|
||||||
type DeviceConverter interface {
|
type DeviceConverter interface {
|
||||||
// goverter:ignore CreatedAt UpdatedAt
|
// goverter:ignore CreatedAt UpdatedAt
|
||||||
// goverter:map Nsfw NSFW
|
// goverter:map Nsfw NSFW
|
||||||
|
@ -32,86 +37,41 @@ type DeviceConverter interface {
|
||||||
ModelsDeviceToCreateDeviceResponse(*models.Device) *device.CreateDeviceResponse
|
ModelsDeviceToCreateDeviceResponse(*models.Device) *device.CreateDeviceResponse
|
||||||
// goverter:ignore state sizeCache unknownFields
|
// goverter:ignore state sizeCache unknownFields
|
||||||
// goverter:useZeroValueOnPointerInconsistency
|
// goverter:useZeroValueOnPointerInconsistency
|
||||||
JetModelDeviceToGetDeviceResponse(model.Devices) *device.GetDeviceResponse
|
JetModelDeviceToProtoDevice(model.Devices) *device.Device
|
||||||
|
|
||||||
|
// goverter:useZeroValueOnPointerInconsistency
|
||||||
|
ListDevicesRequestToAPIListDevicesRequest(*device.ListDevicesRequest) api.ListDevicesRequest
|
||||||
|
|
||||||
// goverter:ignore Slug SingleFolderMode CreatedAt UpdatedAt
|
// goverter:ignore Slug SingleFolderMode CreatedAt UpdatedAt
|
||||||
// goverter:map Nsfw NSFW
|
// goverter:map Nsfw NSFW
|
||||||
DeviceSetterProtoToModelsDeviceSetter(*device.DeviceSetter) *models.DeviceSetter
|
DeviceSetterProtoToModelsDeviceSetter(*device.DeviceSetter) *models.DeviceSetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func BoolToInt8(b bool) int8 {
|
func DeviceDisabledFilterToPtrBool(f device.DisabledFilter) *bool {
|
||||||
if b {
|
switch f {
|
||||||
return 1
|
case device.DisabledFilter_DISABLED_FILTER_TRUE:
|
||||||
|
b := true
|
||||||
|
return &b
|
||||||
|
case device.DisabledFilter_DISABLED_FILTER_FALSE:
|
||||||
|
b := false
|
||||||
|
return &b
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PtrBoolToOmitInt8(b *bool) omit.Val[int8] {
|
func DeviceOrderByToString(order device.OrderBy) string {
|
||||||
if b == nil {
|
if order == device.OrderBy_ORDER_BY_UNSPECIFIED {
|
||||||
return omit.Val[int8]{}
|
return ""
|
||||||
}
|
}
|
||||||
v := *b
|
field, _ := strings.CutPrefix(device.OrderBy_name[int32(order)], "ORDER_BY_")
|
||||||
if v {
|
field = strings.ToLower(field)
|
||||||
return omit.From(int8(1))
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeviceSortToAPISort(sort device.Sort) api.Sort {
|
||||||
|
if sort == device.Sort_SORT_DESCENDING {
|
||||||
|
return api.SortDesc
|
||||||
}
|
}
|
||||||
return omit.From(int8(0))
|
return api.SortDesc
|
||||||
}
|
|
||||||
|
|
||||||
func PtrStringToOmitString(s *string) omit.Val[string] {
|
|
||||||
return omit.FromPtr(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringToOmitString(s string) omit.Val[string] {
|
|
||||||
return omit.From(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PtrFloat64ToOmitFloat64(f *float64) omit.Val[float64] {
|
|
||||||
return omit.FromPtr(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Float64ToOmitFloat64(f float64) omit.Val[float64] {
|
|
||||||
return omit.From(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PtrIntToOmitInt(i *int) omit.Val[int] {
|
|
||||||
return omit.FromPtr(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IntToOmitInt(i int) omit.Val[int] {
|
|
||||||
return omit.From(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PtrInt8ToOmitInt8(i *int8) omit.Val[int8] {
|
|
||||||
return omit.FromPtr(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Int8ToOmitInt8(i int8) omit.Val[int8] {
|
|
||||||
return omit.From(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PtrInt32ToOmitInt32(i *int32) omit.Val[int32] {
|
|
||||||
return omit.FromPtr(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Int32ToOmitInt32(i int32) omit.Val[int32] {
|
|
||||||
return omit.From(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PtrInt64ToOmitInt64(i *int64) omit.Val[int64] {
|
|
||||||
return omit.FromPtr(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Int64ToOmitInt64(i int64) omit.Val[int64] {
|
|
||||||
return omit.From(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BoolToOmitInt8(b bool) omit.Val[int8] {
|
|
||||||
if b {
|
|
||||||
return omit.From(int8(1))
|
|
||||||
}
|
|
||||||
return omit.From(int8(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Int8ToBool(i int8) bool {
|
|
||||||
return i > 0
|
|
||||||
}
|
}
|
80
go/converts/utils.go
Normal file
80
go/converts/utils.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package converts
|
||||||
|
|
||||||
|
import "github.com/aarondl/opt/omit"
|
||||||
|
|
||||||
|
func BoolToInt8(b bool) int8 {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrBoolToOmitInt8(b *bool) omit.Val[int8] {
|
||||||
|
if b == nil {
|
||||||
|
return omit.Val[int8]{}
|
||||||
|
}
|
||||||
|
v := *b
|
||||||
|
if v {
|
||||||
|
return omit.From(int8(1))
|
||||||
|
}
|
||||||
|
return omit.From(int8(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrStringToOmitString(s *string) omit.Val[string] {
|
||||||
|
return omit.FromPtr(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToOmitString(s string) omit.Val[string] {
|
||||||
|
return omit.From(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrFloat64ToOmitFloat64(f *float64) omit.Val[float64] {
|
||||||
|
return omit.FromPtr(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float64ToOmitFloat64(f float64) omit.Val[float64] {
|
||||||
|
return omit.From(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrIntToOmitInt(i *int) omit.Val[int] {
|
||||||
|
return omit.FromPtr(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntToOmitInt(i int) omit.Val[int] {
|
||||||
|
return omit.From(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrInt8ToOmitInt8(i *int8) omit.Val[int8] {
|
||||||
|
return omit.FromPtr(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int8ToOmitInt8(i int8) omit.Val[int8] {
|
||||||
|
return omit.From(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrInt32ToOmitInt32(i *int32) omit.Val[int32] {
|
||||||
|
return omit.FromPtr(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int32ToOmitInt32(i int32) omit.Val[int32] {
|
||||||
|
return omit.From(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrInt64ToOmitInt64(i *int64) omit.Val[int64] {
|
||||||
|
return omit.FromPtr(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64ToOmitInt64(i int64) omit.Val[int64] {
|
||||||
|
return omit.From(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolToOmitInt8(b bool) omit.Val[int8] {
|
||||||
|
if b {
|
||||||
|
return omit.From(int8(1))
|
||||||
|
}
|
||||||
|
return omit.From(int8(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int8ToBool(i int8) bool {
|
||||||
|
return i > 0
|
||||||
|
}
|
|
@ -39,16 +39,24 @@ func (d *DeviceHandler) GetDevice(ctx context.Context, request *connect.Request[
|
||||||
return nil, errs.IntoConnectError(err)
|
return nil, errs.IntoConnectError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
devResp := deviceConvert.JetModelDeviceToGetDeviceResponse(dev)
|
data := deviceConvert.JetModelDeviceToProtoDevice(dev)
|
||||||
return connect.NewResponse(devResp), nil
|
resp := &device.GetDeviceResponse{
|
||||||
|
Device: data,
|
||||||
|
}
|
||||||
|
return connect.NewResponse(resp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDevices implements v1connect.DeviceServiceHandler.
|
// ListDevices implements v1connect.DeviceServiceHandler.
|
||||||
func (d *DeviceHandler) ListDevices(ctx context.Context, request *connect.Request[device.ListDevicesRequest]) (*connect.Response[device.ListDevicesResponse], error) {
|
func (d *DeviceHandler) ListDevices(ctx context.Context, request *connect.Request[device.ListDevicesRequest]) (*connect.Response[device.ListDevicesResponse], error) {
|
||||||
resp, err := d.API.DevicesList(ctx, request.Msg)
|
listRequest := deviceConvert.ListDevicesRequestToAPIListDevicesRequest(request.Msg)
|
||||||
|
devices, err := d.API.DevicesList(ctx, listRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.IntoConnectError(err)
|
return nil, errs.IntoConnectError(err)
|
||||||
}
|
}
|
||||||
|
resp := &device.ListDevicesResponse{}
|
||||||
|
for _, device := range devices {
|
||||||
|
resp.Devices = append(resp.Devices, deviceConvert.JetModelDeviceToProtoDevice(device))
|
||||||
|
}
|
||||||
return connect.NewResponse(resp), nil
|
return connect.NewResponse(resp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,20 +38,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +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";
|
import "device/v1/types.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";
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,7 @@ syntax = "proto3";
|
||||||
|
|
||||||
package device.v1;
|
package device.v1;
|
||||||
|
|
||||||
// import "buf/validate/validate.proto";
|
import "device/v1/types.proto";
|
||||||
import "device/v1/get.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";
|
||||||
|
|
||||||
|
@ -28,12 +27,12 @@ message ListDevicesRequest {
|
||||||
// if value is 0 or not given, limit is set to 25.
|
// if value is 0 or not given, limit is set to 25.
|
||||||
//
|
//
|
||||||
// if limit is higher than 100, it is clamped to 100.
|
// if limit is higher than 100, it is clamped to 100.
|
||||||
uint32 limit = 3;
|
int64 limit = 3;
|
||||||
|
|
||||||
// offset for the given data.
|
// offset for the given data.
|
||||||
//
|
//
|
||||||
// If offset is 0 or not given, the query begins from start.
|
// If offset is 0 or not given, the query begins from start.
|
||||||
uint32 offset = 4;
|
int64 offset = 4;
|
||||||
|
|
||||||
// order_by is the field to order the devices by.
|
// order_by is the field to order the devices by.
|
||||||
//
|
//
|
||||||
|
@ -47,7 +46,7 @@ message ListDevicesRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListDevicesResponse {
|
message ListDevicesResponse {
|
||||||
repeated GetDeviceResponse devices = 1;
|
repeated Device devices = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DisabledFilter {
|
enum DisabledFilter {
|
||||||
|
|
22
schemas/proto/device/v1/types.proto
Normal file
22
schemas/proto/device/v1/types.proto
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package device.v1;
|
||||||
|
|
||||||
|
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Reference in a new issue