feat(go): add realtime kv projection
This commit is contained in:
@@ -19,6 +19,8 @@ type RealtimeTableQuery struct {
|
||||
Protocol string
|
||||
VIN string
|
||||
Plate string
|
||||
Domain string
|
||||
Field string
|
||||
IncludeTotal bool
|
||||
Limit int
|
||||
Offset int
|
||||
@@ -56,6 +58,19 @@ type LocationRow struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type KVRow struct {
|
||||
Protocol string `json:"protocol"`
|
||||
VIN string `json:"vin"`
|
||||
Domain string `json:"domain"`
|
||||
Field string `json:"field"`
|
||||
Value string `json:"value"`
|
||||
ValueType string `json:"value_type"`
|
||||
EventTime string `json:"event_time,omitempty"`
|
||||
ReceivedAt string `json:"received_at,omitempty"`
|
||||
EventID string `json:"event_id"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type SnapshotQueryRepository struct {
|
||||
db mysqlQueryer
|
||||
}
|
||||
@@ -108,6 +123,10 @@ type LocationQueryRepository struct {
|
||||
db mysqlQueryer
|
||||
}
|
||||
|
||||
type KVQueryRepository struct {
|
||||
db mysqlQueryer
|
||||
}
|
||||
|
||||
func NewLocationQueryRepository(db mysqlQueryer) *LocationQueryRepository {
|
||||
if db == nil {
|
||||
panic("location query db must not be nil")
|
||||
@@ -115,6 +134,13 @@ func NewLocationQueryRepository(db mysqlQueryer) *LocationQueryRepository {
|
||||
return &LocationQueryRepository{db: db}
|
||||
}
|
||||
|
||||
func NewKVQueryRepository(db mysqlQueryer) *KVQueryRepository {
|
||||
if db == nil {
|
||||
panic("kv query db must not be nil")
|
||||
}
|
||||
return &KVQueryRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *LocationQueryRepository) Count(ctx context.Context, query RealtimeTableQuery) (int64, error) {
|
||||
sqlText, args := buildRealtimeCountSQL("vehicle_realtime_location", normalizeRealtimeTableQuery(query))
|
||||
return queryCount(ctx, r.db, sqlText, args)
|
||||
@@ -174,6 +200,49 @@ func (r *LocationQueryRepository) Query(ctx context.Context, query RealtimeTable
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (r *KVQueryRepository) Count(ctx context.Context, query RealtimeTableQuery) (int64, error) {
|
||||
where, args := buildRealtimeKVWhere(normalizeRealtimeTableQuery(query))
|
||||
sqlText := "SELECT COUNT(*) FROM vehicle_realtime_kv"
|
||||
if len(where) > 0 {
|
||||
sqlText += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
return queryCount(ctx, r.db, sqlText, args)
|
||||
}
|
||||
|
||||
func (r *KVQueryRepository) Query(ctx context.Context, query RealtimeTableQuery) ([]KVRow, error) {
|
||||
query = normalizeRealtimeTableQuery(query)
|
||||
where, args := buildRealtimeKVWhere(query)
|
||||
sqlText := "SELECT protocol, vin, domain_name, field_name, field_value, value_type, event_time, received_at, event_id, updated_at FROM vehicle_realtime_kv"
|
||||
if len(where) > 0 {
|
||||
sqlText += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
sqlText += " ORDER BY updated_at DESC LIMIT ? OFFSET ?"
|
||||
args = append(args, query.Limit, query.Offset)
|
||||
rows, err := r.db.QueryContext(ctx, sqlText, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]KVRow, 0)
|
||||
for rows.Next() {
|
||||
var row KVRow
|
||||
var eventTime, receivedAt, updatedAt scanSQLDateTime
|
||||
var value sql.NullString
|
||||
if err := rows.Scan(&row.Protocol, &row.VIN, &row.Domain, &row.Field, &value, &row.ValueType, &eventTime, &receivedAt, &row.EventID, &updatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value.Valid {
|
||||
row.Value = value.String
|
||||
}
|
||||
row.EventTime = eventTime.String
|
||||
row.ReceivedAt = receivedAt.String
|
||||
row.UpdatedAt = updatedAt.String
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
type SnapshotQueryHandler struct {
|
||||
repository *SnapshotQueryRepository
|
||||
}
|
||||
@@ -222,6 +291,10 @@ type LocationQueryHandler struct {
|
||||
repository *LocationQueryRepository
|
||||
}
|
||||
|
||||
type KVQueryHandler struct {
|
||||
repository *KVQueryRepository
|
||||
}
|
||||
|
||||
func NewLocationQueryHandler(repository *LocationQueryRepository) *LocationQueryHandler {
|
||||
if repository == nil {
|
||||
panic("location query repository must not be nil")
|
||||
@@ -229,6 +302,13 @@ func NewLocationQueryHandler(repository *LocationQueryRepository) *LocationQuery
|
||||
return &LocationQueryHandler{repository: repository}
|
||||
}
|
||||
|
||||
func NewKVQueryHandler(repository *KVQueryRepository) *KVQueryHandler {
|
||||
if repository == nil {
|
||||
panic("kv query repository must not be nil")
|
||||
}
|
||||
return &KVQueryHandler{repository: repository}
|
||||
}
|
||||
|
||||
func (h *LocationQueryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@@ -262,6 +342,39 @@ func (h *LocationQueryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
writePage(w, rows, total, query)
|
||||
}
|
||||
|
||||
func (h *KVQueryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
if strings.Trim(r.URL.Path, "/") != "api/realtime/kv" {
|
||||
writeError(w, http.StatusNotFound, "route not found")
|
||||
return
|
||||
}
|
||||
query, err := parseRealtimeTableQuery(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
var total int64
|
||||
if query.IncludeTotal {
|
||||
total, err = h.repository.Count(r.Context(), query)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
rows, err := h.repository.Query(r.Context(), query)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !query.IncludeTotal {
|
||||
total = int64(len(rows))
|
||||
}
|
||||
writePage(w, rows, total, query)
|
||||
}
|
||||
|
||||
func parseRealtimeTableQuery(r *http.Request) (RealtimeTableQuery, error) {
|
||||
values := r.URL.Query()
|
||||
limit, err := parseRealtimeBoundedInt(values.Get("limit"), 50, 1, 1000, "limit")
|
||||
@@ -276,6 +389,8 @@ func parseRealtimeTableQuery(r *http.Request) (RealtimeTableQuery, error) {
|
||||
Protocol: values.Get("protocol"),
|
||||
VIN: values.Get("vin"),
|
||||
Plate: values.Get("plate"),
|
||||
Domain: values.Get("domain"),
|
||||
Field: values.Get("field"),
|
||||
IncludeTotal: strings.EqualFold(strings.TrimSpace(values.Get("includeTotal")), "true"),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
@@ -286,12 +401,36 @@ func normalizeRealtimeTableQuery(query RealtimeTableQuery) RealtimeTableQuery {
|
||||
query.Protocol = strings.ToUpper(strings.TrimSpace(query.Protocol))
|
||||
query.VIN = strings.TrimSpace(query.VIN)
|
||||
query.Plate = strings.TrimSpace(query.Plate)
|
||||
query.Domain = strings.TrimSpace(query.Domain)
|
||||
query.Field = strings.TrimSpace(query.Field)
|
||||
if query.Limit <= 0 {
|
||||
query.Limit = 50
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func buildRealtimeKVWhere(query RealtimeTableQuery) ([]string, []any) {
|
||||
var where []string
|
||||
var args []any
|
||||
if query.Protocol != "" {
|
||||
where = append(where, "protocol = ?")
|
||||
args = append(args, query.Protocol)
|
||||
}
|
||||
if query.VIN != "" {
|
||||
where = append(where, "vin = ?")
|
||||
args = append(args, query.VIN)
|
||||
}
|
||||
if query.Domain != "" {
|
||||
where = append(where, "domain_name = ?")
|
||||
args = append(args, query.Domain)
|
||||
}
|
||||
if query.Field != "" {
|
||||
where = append(where, "field_name = ?")
|
||||
args = append(args, query.Field)
|
||||
}
|
||||
return where, args
|
||||
}
|
||||
|
||||
func buildRealtimeCountSQL(table string, query RealtimeTableQuery) (string, []any) {
|
||||
where, args := buildRealtimeWhere(query)
|
||||
sqlText := "SELECT COUNT(*) FROM " + table
|
||||
|
||||
Reference in New Issue
Block a user