fix(go): backfill realtime plate from binding
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,15 +15,24 @@ type SnapshotExecer interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
}
|
||||
|
||||
type PlateResolver interface {
|
||||
PlateByVIN(context.Context, string) (string, error)
|
||||
}
|
||||
|
||||
type SnapshotWriter struct {
|
||||
exec SnapshotExecer
|
||||
exec SnapshotExecer
|
||||
plateResolver PlateResolver
|
||||
}
|
||||
|
||||
func NewSnapshotWriter(exec SnapshotExecer) *SnapshotWriter {
|
||||
return NewSnapshotWriterWithPlateResolver(exec, nil)
|
||||
}
|
||||
|
||||
func NewSnapshotWriterWithPlateResolver(exec SnapshotExecer, plateResolver PlateResolver) *SnapshotWriter {
|
||||
if exec == nil {
|
||||
panic("snapshot execer must not be nil")
|
||||
}
|
||||
return &SnapshotWriter{exec: exec}
|
||||
return &SnapshotWriter{exec: exec, plateResolver: plateResolver}
|
||||
}
|
||||
|
||||
func (w *SnapshotWriter) EnsureSchema(ctx context.Context) error {
|
||||
@@ -41,6 +51,10 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
||||
if !hasRealtimePayload(env) {
|
||||
return nil
|
||||
}
|
||||
plate, err := w.plateForEnvelope(ctx, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fieldsJSON, err := marshalObject(env.Fields)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -57,7 +71,7 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
||||
strings.TrimSpace(env.VIN),
|
||||
strings.TrimSpace(env.Phone),
|
||||
strings.TrimSpace(env.DeviceID),
|
||||
strings.TrimSpace(env.Plate),
|
||||
plate,
|
||||
strings.TrimSpace(env.MessageID),
|
||||
env.Sequence,
|
||||
strings.TrimSpace(env.SourceEndpoint),
|
||||
@@ -69,7 +83,7 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
location, ok := realtimeLocationFromEnvelope(env, vehicleKey, fieldsJSON)
|
||||
location, ok := realtimeLocationFromEnvelope(env, vehicleKey, fieldsJSON, plate)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -100,6 +114,27 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *SnapshotWriter) plateForEnvelope(ctx context.Context, env envelope.FrameEnvelope) (string, error) {
|
||||
if plate := strings.TrimSpace(env.Plate); plate != "" {
|
||||
return plate, nil
|
||||
}
|
||||
if w.plateResolver == nil {
|
||||
return "", nil
|
||||
}
|
||||
vin := strings.TrimSpace(env.VIN)
|
||||
if vin == "" {
|
||||
return "", nil
|
||||
}
|
||||
plate, err := w.plateResolver.PlateByVIN(ctx, vin)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(plate), nil
|
||||
}
|
||||
|
||||
type realtimeLocationRow struct {
|
||||
Protocol string
|
||||
VehicleKey string
|
||||
@@ -125,7 +160,7 @@ type realtimeLocationRow struct {
|
||||
EventID string
|
||||
}
|
||||
|
||||
func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vehicleKey string, fieldsJSON string) (realtimeLocationRow, bool) {
|
||||
func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vehicleKey string, fieldsJSON string, plate string) (realtimeLocationRow, bool) {
|
||||
latitude, okLat := numberField(env.Fields, envelope.FieldLatitude)
|
||||
longitude, okLon := numberField(env.Fields, envelope.FieldLongitude)
|
||||
if !okLat || !okLon {
|
||||
@@ -137,7 +172,7 @@ func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vehicleKey string,
|
||||
VIN: strings.TrimSpace(env.VIN),
|
||||
Phone: strings.TrimSpace(env.Phone),
|
||||
DeviceID: strings.TrimSpace(env.DeviceID),
|
||||
Plate: strings.TrimSpace(env.Plate),
|
||||
Plate: plate,
|
||||
MessageID: strings.TrimSpace(env.MessageID),
|
||||
Sequence: env.Sequence,
|
||||
SourceEndpoint: strings.TrimSpace(env.SourceEndpoint),
|
||||
@@ -200,6 +235,53 @@ func numberField(fields map[string]any, key string) (float64, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
type BindingPlateResolver struct {
|
||||
queryer Queryer
|
||||
table string
|
||||
}
|
||||
|
||||
type Queryer interface {
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
}
|
||||
|
||||
func NewBindingPlateResolver(queryer Queryer, table string) *BindingPlateResolver {
|
||||
if queryer == nil {
|
||||
panic("plate binding queryer must not be nil")
|
||||
}
|
||||
table = strings.TrimSpace(table)
|
||||
if table == "" || !safeIdentifier(table) {
|
||||
table = "vehicle_identity_binding"
|
||||
}
|
||||
return &BindingPlateResolver{queryer: queryer, table: table}
|
||||
}
|
||||
|
||||
func (r *BindingPlateResolver) PlateByVIN(ctx context.Context, vin string) (string, error) {
|
||||
vin = strings.TrimSpace(vin)
|
||||
if vin == "" {
|
||||
return "", sql.ErrNoRows
|
||||
}
|
||||
query := "SELECT plate FROM " + r.table + " WHERE vin = ? AND plate IS NOT NULL AND plate <> '' ORDER BY updated_at DESC LIMIT 1"
|
||||
var plate string
|
||||
err := r.queryer.QueryRowContext(ctx, query, vin).Scan(&plate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(plate), nil
|
||||
}
|
||||
|
||||
func safeIdentifier(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func marshalObject(value map[string]any) (string, error) {
|
||||
if value == nil {
|
||||
value = map[string]any{}
|
||||
|
||||
Reference in New Issue
Block a user