From 1b1132ea38e2f90c73ea0dec01feb2c597a32297 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sun, 1 Mar 2026 16:31:55 -0500 Subject: [PATCH 01/10] Slopped up some test fixes --- e2e/approximate_location_test.go | 4 +- e2e/clickhouse_container_test.go | 3 +- e2e/segments_test.go | 54 ++++++++--------- e2e/signals_dataSummary_test.go | 19 +++--- e2e/signals_latest_test.go | 10 ++-- go.mod | 2 +- go.sum | 6 ++ internal/repositories/repositories.go | 38 +++++++----- .../repositories/repositories_mocks_test.go | 48 +++++++-------- internal/repositories/repositories_test.go | 59 ++++++++++++------- internal/repositories/segments.go | 19 +++--- internal/repositories/validate_test.go | 6 +- internal/service/ch/ch.go | 22 +++---- internal/service/ch/ch_test.go | 26 ++++---- internal/service/ch/changepoint_detector.go | 4 +- internal/service/ch/detector.go | 2 +- internal/service/ch/frequency_detector.go | 4 +- internal/service/ch/idling_detector.go | 4 +- internal/service/ch/ignition_detector.go | 18 +++--- internal/service/ch/ignition_detector_test.go | 19 +++--- internal/service/ch/queries.go | 38 ++++++------ internal/service/ch/recharge_detector.go | 10 ++-- internal/service/ch/refuel_detector.go | 4 +- internal/service/ch/segments.go | 4 +- internal/service/ch/segments_utils.go | 14 ++--- 25 files changed, 236 insertions(+), 201 deletions(-) diff --git a/e2e/approximate_location_test.go b/e2e/approximate_location_test.go index ffeff4c3..16e10a07 100644 --- a/e2e/approximate_location_test.go +++ b/e2e/approximate_location_test.go @@ -32,7 +32,7 @@ func TestApproximateLocation(t *testing.T) { Longitude: startLoc.Lng, HDOP: startHDOP, }, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, { Source: ch.SourceTranslations["smartcar"][0], @@ -43,7 +43,7 @@ func TestApproximateLocation(t *testing.T) { Longitude: endLoc.Lng, HDOP: endHDOP, }, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, } diff --git a/e2e/clickhouse_container_test.go b/e2e/clickhouse_container_test.go index e5f87876..176cf8f7 100644 --- a/e2e/clickhouse_container_test.go +++ b/e2e/clickhouse_container_test.go @@ -99,7 +99,6 @@ func LoadSampleDataInto(t *testing.T, ch *container.Container, signalPath, event if len(row) < 9 { continue } - tokenID, _ := strconv.ParseUint(row[0], 10, 32) ts, _ := time.Parse("2006-01-02 15:04:05.000000", row[1]) var loc vss.Location if len(row[8]) > 0 { @@ -107,7 +106,7 @@ func LoadSampleDataInto(t *testing.T, ch *container.Container, signalPath, event } valNum, _ := strconv.ParseFloat(row[6], 64) signals = append(signals, vss.Signal{ - TokenID: uint32(tokenID), + Subject: row[0], Timestamp: ts, Name: row[2], Source: row[3], diff --git a/e2e/segments_test.go b/e2e/segments_test.go index 65c52e81..aa05e07f 100644 --- a/e2e/segments_test.go +++ b/e2e/segments_test.go @@ -17,7 +17,7 @@ import ( // testSignal represents minimal signal data without location fields type testSignal struct { - TokenID uint32 + Subject string Timestamp time.Time Name string ValueNumber float64 @@ -29,7 +29,7 @@ type testSignal struct { // testStateChange represents ignition state change data type testStateChange struct { - TokenID uint32 + Subject string SignalName string Timestamp time.Time NewState float64 @@ -42,7 +42,7 @@ type testStateChange struct { } const ( - testTokenID = uint32(12345) + testSubject = "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:12345" testSource = "test-source" testProducer = "test-producer" // tripDuration must be >= 240s (defaultMinSegmentDurationSeconds in detectors) @@ -66,7 +66,7 @@ func generateTripSignals(startTime time.Time, durationSeconds int, eventIDPrefix ts := startTime.Add(time.Duration(offset) * time.Second) for i, name := range signalNames { signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: name, ValueNumber: float64(offset + i), // Varying values @@ -103,7 +103,7 @@ func generateTestData() ([]testSignal, []testStateChange) { stateChanges = []testStateChange{ // Trip 1: Ignition ON { - TokenID: testTokenID, + Subject: testSubject, SignalName: "isIgnitionOn", Timestamp: trip1Start, NewState: 1, @@ -116,7 +116,7 @@ func generateTestData() ([]testSignal, []testStateChange) { }, // Trip 1: Ignition OFF { - TokenID: testTokenID, + Subject: testSubject, SignalName: "isIgnitionOn", Timestamp: trip1End, NewState: 0, @@ -129,7 +129,7 @@ func generateTestData() ([]testSignal, []testStateChange) { }, // Trip 2: Ignition ON { - TokenID: testTokenID, + Subject: testSubject, SignalName: "isIgnitionOn", Timestamp: trip2Start, NewState: 1, @@ -142,7 +142,7 @@ func generateTestData() ([]testSignal, []testStateChange) { }, // Trip 2: Ignition OFF { - TokenID: testTokenID, + Subject: testSubject, SignalName: "isIgnitionOn", Timestamp: trip2End, NewState: 0, @@ -165,7 +165,7 @@ func insertTestSignals(t *testing.T, conn clickhouse.Conn, signals []testSignal) for _, sig := range signals { err := batch.Append( - sig.TokenID, + sig.Subject, sig.Timestamp, sig.Name, sig.Source, @@ -189,7 +189,7 @@ func insertTestStateChanges(t *testing.T, conn clickhouse.Conn, stateChanges []t for _, sc := range stateChanges { err := batch.Append( - sc.TokenID, + sc.Subject, sc.SignalName, sc.Timestamp, sc.NewState, @@ -248,7 +248,7 @@ func TestSegmentDetectors(t *testing.T) { t.Run("IgnitionDetector", func(t *testing.T) { detector := ch.NewIgnitionDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, from, to, nil) + segments, err := detector.DetectSegments(ctx, testSubject, from, to, nil) require.NoError(t, err) assert.Len(t, segments, 2, "Expected 2 trips from IgnitionDetector") @@ -269,7 +269,7 @@ func TestSegmentDetectors(t *testing.T) { t.Run("FrequencyDetector", func(t *testing.T) { detector := ch.NewFrequencyDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, from, to, nil) + segments, err := detector.DetectSegments(ctx, testSubject, from, to, nil) require.NoError(t, err) assert.Len(t, segments, 2, "Expected 2 trips from FrequencyDetector") @@ -281,7 +281,7 @@ func TestSegmentDetectors(t *testing.T) { t.Run("ChangePointDetector", func(t *testing.T) { detector := ch.NewChangePointDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, from, to, nil) + segments, err := detector.DetectSegments(ctx, testSubject, from, to, nil) require.NoError(t, err) assert.Len(t, segments, 2, "Expected 2 trips from ChangePointDetector") @@ -296,7 +296,7 @@ func TestSegmentDetectors(t *testing.T) { t.Run("IgnitionDetector_StartedBeforeRange", func(t *testing.T) { detector := ch.NewIgnitionDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, fromMidTrip1, to, nil) + segments, err := detector.DetectSegments(ctx, testSubject, fromMidTrip1, to, nil) require.NoError(t, err) require.Len(t, segments, 2, "Expected 2 trips") assert.True(t, segments[0].StartedBeforeRange, "Trip 1 should have StartedBeforeRange=true") @@ -305,7 +305,7 @@ func TestSegmentDetectors(t *testing.T) { t.Run("FrequencyDetector_StartedBeforeRange", func(t *testing.T) { detector := ch.NewFrequencyDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, fromMidTrip1, to, nil) + segments, err := detector.DetectSegments(ctx, testSubject, fromMidTrip1, to, nil) require.NoError(t, err) require.Len(t, segments, 2, "Expected 2 trips") assert.True(t, segments[0].StartedBeforeRange, "Trip 1 should have StartedBeforeRange=true") @@ -314,7 +314,7 @@ func TestSegmentDetectors(t *testing.T) { t.Run("ChangePointDetector_StartedBeforeRange", func(t *testing.T) { detector := ch.NewChangePointDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, fromMidTrip1, to, nil) + segments, err := detector.DetectSegments(ctx, testSubject, fromMidTrip1, to, nil) require.NoError(t, err) require.Len(t, segments, 2, "Expected 2 trips") assert.True(t, segments[0].StartedBeforeRange, "Trip 1 should have StartedBeforeRange=true") @@ -332,7 +332,7 @@ func TestSegmentDetectors(t *testing.T) { toIdle := idleStart.Add(time.Duration(idleDurationSec)*time.Second + 1*time.Hour) detector := ch.NewIdlingDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, fromIdle, toIdle, nil) + segments, err := detector.DetectSegments(ctx, testSubject, fromIdle, toIdle, nil) require.NoError(t, err) require.Len(t, segments, 1, "Expected 1 idling segment") @@ -353,7 +353,7 @@ func TestSegmentDetectors(t *testing.T) { toRefuel := refuelStart.Add(2 * time.Hour) detector := ch.NewRefuelDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, fromRefuel, toRefuel, nil) + segments, err := detector.DetectSegments(ctx, testSubject, fromRefuel, toRefuel, nil) require.NoError(t, err) require.GreaterOrEqual(t, len(segments), 1, "Expected at least 1 refuel segment") @@ -374,7 +374,7 @@ func TestSegmentDetectors(t *testing.T) { toRecharge := rechargeStart.Add(4 * time.Hour) detector := ch.NewRechargeDetector(conn) - segments, err := detector.DetectSegments(ctx, testTokenID, fromRecharge, toRecharge, nil) + segments, err := detector.DetectSegments(ctx, testSubject, fromRecharge, toRecharge, nil) require.NoError(t, err) require.GreaterOrEqual(t, len(segments), 1, "Expected at least 1 recharge segment") @@ -395,7 +395,7 @@ func generateIdleRpmSignals(startTime time.Time, durationSeconds int) []testSign for offset := 0; offset < durationSeconds; offset += 10 { ts := startTime.Add(time.Duration(offset) * time.Second) signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: engineSpeedName, ValueNumber: idleRpm, @@ -420,7 +420,7 @@ func generateRefuelSignals(startTime time.Time) []testSignal { for offset := 0; offset < 30*60; offset += 60 { ts := startTime.Add(time.Duration(offset) * time.Second) signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: fuelName, ValueNumber: 20.0, @@ -433,7 +433,7 @@ func generateRefuelSignals(startTime time.Time) []testSignal { // Refuel event: jump to 80% (within a 5-minute window, rise is 300% relative) refuelTime := startTime.Add(30 * time.Minute) signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: refuelTime, Name: fuelName, ValueNumber: 80.0, @@ -446,7 +446,7 @@ func generateRefuelSignals(startTime time.Time) []testSignal { for offset := 1; offset <= 30; offset++ { ts := refuelTime.Add(time.Duration(offset) * time.Minute) signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: fuelName, ValueNumber: 80.0, @@ -473,7 +473,7 @@ func generateRechargeSignals(startTime time.Time) []testSignal { ts := startTime.Add(time.Duration(offset) * time.Minute) soc := 35.0 - float64(offset)*0.5 // 35% down to 20% signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: socName, ValueNumber: soc, @@ -482,7 +482,7 @@ func generateRechargeSignals(startTime time.Time) []testSignal { CloudEventID: fmt.Sprintf("recharge-pre-%d", offset), }) signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: odoName, ValueNumber: 50000.0 + float64(offset)*0.1, // moving @@ -501,7 +501,7 @@ func generateRechargeSignals(startTime time.Time) []testSignal { soc = 90.0 } signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: socName, ValueNumber: soc, @@ -510,7 +510,7 @@ func generateRechargeSignals(startTime time.Time) []testSignal { CloudEventID: fmt.Sprintf("recharge-charge-%d", offset), }) signals = append(signals, testSignal{ - TokenID: testTokenID, + Subject: testSubject, Timestamp: ts, Name: odoName, ValueNumber: 50003.0, // stationary during charge diff --git a/e2e/signals_dataSummary_test.go b/e2e/signals_dataSummary_test.go index 42ca91f3..4fb81da4 100644 --- a/e2e/signals_dataSummary_test.go +++ b/e2e/signals_dataSummary_test.go @@ -16,6 +16,7 @@ import ( func TestSignalsMetadata(t *testing.T) { services := GetTestServices(t) tokenID := uint32(rand.Intn(1000000)) + subject := fmt.Sprintf("did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:%d", tokenID) // Define test timestamps smartCarTime1 := time.Date(2024, 11, 20, 22, 28, 17, 0, time.UTC) smartCarTime2 := time.Date(2024, 11, 21, 10, 15, 30, 0, time.UTC) @@ -32,14 +33,14 @@ func TestSignalsMetadata(t *testing.T) { Timestamp: smartCarTime1, Name: vss.FieldSpeed, ValueNumber: 65.5, - TokenID: tokenID, + Subject: subject, }, { Source: ch.SourceTranslations["smartcar"][0], Timestamp: smartCarTime2, Name: vss.FieldSpeed, ValueNumber: 70.2, - TokenID: tokenID, + Subject: subject, }, { Source: ch.SourceTranslations["smartcar"][0], @@ -49,7 +50,7 @@ func TestSignalsMetadata(t *testing.T) { Latitude: 40.73899538333504, Longitude: 73.99386110247163, }, - TokenID: tokenID, + Subject: subject, }, // AutoPi signals - speed and battery { @@ -57,21 +58,21 @@ func TestSignalsMetadata(t *testing.T) { Timestamp: autopiTime1, Name: vss.FieldSpeed, ValueNumber: 14, - TokenID: tokenID, + Subject: subject, }, { Source: ch.SourceTranslations["autopi"][0], Timestamp: autopiTime2, Name: vss.FieldSpeed, ValueNumber: 25.8, - TokenID: tokenID, + Subject: subject, }, { Source: ch.SourceTranslations["autopi"][0], Timestamp: autopiTime1, Name: vss.FieldPowertrainTractionBatteryStateOfChargeCurrent, ValueNumber: 75.5, - TokenID: tokenID, + Subject: subject, }, // Macaron signals - just speed @@ -80,7 +81,7 @@ func TestSignalsMetadata(t *testing.T) { Timestamp: macaronTime, Name: vss.FieldSpeed, ValueNumber: 3, - TokenID: tokenID, + Subject: subject, }, // Tesla signals - speed and different battery field @@ -89,14 +90,14 @@ func TestSignalsMetadata(t *testing.T) { Timestamp: teslaTime, Name: vss.FieldSpeed, ValueNumber: 88.5, - TokenID: tokenID, + Subject: subject, }, { Source: ch.SourceTranslations["tesla"][0], Timestamp: teslaTime, Name: vss.FieldPowertrainTractionBatteryChargingChargeCurrentAC, ValueNumber: 82.3, - TokenID: tokenID, + Subject: subject, }, } diff --git a/e2e/signals_latest_test.go b/e2e/signals_latest_test.go index 3777204a..f22fa241 100644 --- a/e2e/signals_latest_test.go +++ b/e2e/signals_latest_test.go @@ -24,7 +24,7 @@ func TestSignalsLatest(t *testing.T) { Timestamp: smartCarTime, Name: vss.FieldSpeed, ValueNumber: 65.5, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, { Source: ch.SourceTranslations["smartcar"][0], @@ -34,7 +34,7 @@ func TestSignalsLatest(t *testing.T) { Latitude: 40.73899538333504, Longitude: 73.99386110247163, }, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, { Source: ch.SourceTranslations["smartcar"][0], @@ -43,21 +43,21 @@ func TestSignalsLatest(t *testing.T) { ValueLocation: vss.Location{ HDOP: 7, }, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, { Source: ch.SourceTranslations["autopi"][0], Timestamp: autopiTime, Name: vss.FieldSpeed, ValueNumber: 14, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, { Source: ch.SourceTranslations["macaron"][0], Timestamp: macaronTime, Name: vss.FieldSpeed, ValueNumber: 3, - TokenID: 39718, + Subject: "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:39718", }, } diff --git a/go.mod b/go.mod index 4f82a674..4fa730c2 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/DIMO-Network/cloudevent v0.1.6 github.com/DIMO-Network/credit-tracker v0.0.6 github.com/DIMO-Network/fetch-api v0.0.16 - github.com/DIMO-Network/model-garage v0.8.17 + github.com/DIMO-Network/model-garage v0.8.18-0.20260301212110-8cb393aaa1c9 github.com/DIMO-Network/server-garage v0.0.7 github.com/DIMO-Network/shared v1.0.7 github.com/DIMO-Network/token-exchange-api v0.3.7 diff --git a/go.sum b/go.sum index 54abd297..17c3b13e 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,12 @@ github.com/DIMO-Network/fetch-api v0.0.16 h1:jOIM9AHOCTN9Omh5QoRrdVsy0UCPHwB+08/ github.com/DIMO-Network/fetch-api v0.0.16/go.mod h1:pD9+5wPYjz3ORH9FBsXLlomsZJRPTyOzKy19urT6xew= github.com/DIMO-Network/model-garage v0.8.17 h1:6F29k0OUMRsKbZU3E73Cg8Dadh/GpgrUtJruaSBgalU= github.com/DIMO-Network/model-garage v0.8.17/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= +github.com/DIMO-Network/model-garage v0.8.18-0.20260227195128-b6fe603d5a94 h1:e+Z0wXbIaKVVfNlVRi4KfDn2GrZcY5TUsdjXCOyV4HU= +github.com/DIMO-Network/model-garage v0.8.18-0.20260227195128-b6fe603d5a94/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= +github.com/DIMO-Network/model-garage v0.8.18-0.20260228213518-5572b215dc07 h1:L2wtmEwMxHhki7U6CNLle5fLX88amMieIZ+eQy+dObU= +github.com/DIMO-Network/model-garage v0.8.18-0.20260228213518-5572b215dc07/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= +github.com/DIMO-Network/model-garage v0.8.18-0.20260301212110-8cb393aaa1c9 h1:s/FIk7WNp0fyNnOv169zIUKOPP1JurU3JxwRw8i4hGk= +github.com/DIMO-Network/model-garage v0.8.18-0.20260301212110-8cb393aaa1c9/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= github.com/DIMO-Network/server-garage v0.0.7 h1:kOBVyOtIbxa1x9pAf1epABTb9l/U3khf0PwUaHeHiKs= github.com/DIMO-Network/server-garage v0.0.7/go.mod h1:7DFor8MMJ8fLv9EB16Z5LrN+ftW3qeIk+swpkT7F2cU= github.com/DIMO-Network/shared v1.0.7 h1:LfSgsqJ6R7EUyfo2GTfuhrCpoDcweJqe7eVOa4j7Xbo= diff --git a/internal/repositories/repositories.go b/internal/repositories/repositories.go index 4e83905e..b00d886a 100644 --- a/internal/repositories/repositories.go +++ b/internal/repositories/repositories.go @@ -33,16 +33,16 @@ var ManufacturerSourceTranslations = map[string]string{ // CHService is the interface for the ClickHouse service. type CHService interface { - GetAggregatedSignals(ctx context.Context, aggArgs *model.AggregatedSignalArgs) ([]*ch.AggSignal, error) - GetAggregatedSignalsForRanges(ctx context.Context, tokenID uint32, ranges []ch.TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) ([]*ch.AggSignalForRange, error) - GetLatestSignals(ctx context.Context, latestArgs *model.LatestSignalsArgs) ([]*vss.Signal, error) - GetAvailableSignals(ctx context.Context, tokenID uint32, filter *model.SignalFilter) ([]string, error) - GetSignalSummaries(ctx context.Context, tokenID uint32, filter *model.SignalFilter) ([]*model.SignalDataSummary, error) + GetAggregatedSignals(ctx context.Context, subject string, aggArgs *model.AggregatedSignalArgs) ([]*ch.AggSignal, error) + GetAggregatedSignalsForRanges(ctx context.Context, subject string, ranges []ch.TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) ([]*ch.AggSignalForRange, error) + GetLatestSignals(ctx context.Context, subject string, latestArgs *model.LatestSignalsArgs) ([]*vss.Signal, error) + GetAvailableSignals(ctx context.Context, subject string, filter *model.SignalFilter) ([]string, error) + GetSignalSummaries(ctx context.Context, subject string, filter *model.SignalFilter) ([]*model.SignalDataSummary, error) GetEvents(ctx context.Context, subject string, from, to time.Time, filter *model.EventFilter) ([]*vss.Event, error) GetEventCounts(ctx context.Context, subject string, from, to time.Time, eventNames []string) ([]*ch.EventCount, error) GetEventCountsForRanges(ctx context.Context, subject string, ranges []ch.TimeRange, eventNames []string) ([]*ch.EventCountForRange, error) GetEventSummaries(ctx context.Context, subject string) ([]*ch.EventSummary, error) - GetSegments(ctx context.Context, tokenID uint32, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig) ([]*model.Segment, error) + GetSegments(ctx context.Context, subject string, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig) ([]*model.Segment, error) } // Repository is the base repository for all repositories. @@ -76,13 +76,23 @@ func NewRepository(chService CHService, settings config.Settings) (*Repository, } +// toSubject converts a tokenID to a W3C DID subject string using the repository's chain ID and vehicle address. +func (r *Repository) toSubject(tokenID uint32) string { + return cloudevent.ERC721DID{ + ChainID: r.chainID, + ContractAddress: r.vehicleAddress, + TokenID: big.NewInt(int64(tokenID)), + }.String() +} + // GetSignal returns the aggregated signals for the given tokenID, interval, from, to and filter. func (r *Repository) GetSignal(ctx context.Context, aggArgs *model.AggregatedSignalArgs) ([]*model.SignalAggregations, error) { if err := validateAggSigArgs(aggArgs); err != nil { return nil, errorhandler.NewBadRequestError(ctx, err) } - signals, err := r.chService.GetAggregatedSignals(ctx, aggArgs) + subject := r.toSubject(aggArgs.TokenID) + signals, err := r.chService.GetAggregatedSignals(ctx, subject, aggArgs) if err != nil { return nil, handleDBError(ctx, err) } @@ -134,7 +144,8 @@ func (r *Repository) GetSignalLatest(ctx context.Context, latestArgs *model.Late if err := validateLatestSigArgs(latestArgs); err != nil { return nil, errorhandler.NewBadRequestError(ctx, err) } - signals, err := r.chService.GetLatestSignals(ctx, latestArgs) + subject := r.toSubject(latestArgs.SignalArgs.TokenID) + signals, err := r.chService.GetLatestSignals(ctx, subject, latestArgs) if err != nil { return nil, handleDBError(ctx, err) } @@ -186,7 +197,8 @@ func (r *Repository) GetDeviceActivity(ctx context.Context, vehicleTokenID int, // GetAvailableSignals returns the available signals for the given tokenID and filter. // If no signals are found, a nil slice is returned. func (r *Repository) GetAvailableSignals(ctx context.Context, tokenID uint32, filter *model.SignalFilter) ([]string, error) { - allSignals, err := r.chService.GetAvailableSignals(ctx, tokenID, filter) + subject := r.toSubject(tokenID) + allSignals, err := r.chService.GetAvailableSignals(ctx, subject, filter) if err != nil { return nil, handleDBError(ctx, err) } @@ -201,15 +213,11 @@ func (r *Repository) GetAvailableSignals(ctx context.Context, tokenID uint32, fi // GetDataSummary returns the signal and event metadata for the given tokenID and filter. func (r *Repository) GetDataSummary(ctx context.Context, tokenID uint32, filter *model.SignalFilter) (*model.DataSummary, error) { - signalDataSummary, err := r.chService.GetSignalSummaries(ctx, tokenID, filter) + subject := r.toSubject(tokenID) + signalDataSummary, err := r.chService.GetSignalSummaries(ctx, subject, filter) if err != nil { return nil, handleDBError(ctx, err) } - subject := cloudevent.ERC721DID{ - ChainID: r.chainID, - ContractAddress: r.vehicleAddress, - TokenID: big.NewInt(int64(tokenID)), - }.String() eventSummaries, err := r.chService.GetEventSummaries(ctx, subject) if err != nil { return nil, handleDBError(ctx, err) diff --git a/internal/repositories/repositories_mocks_test.go b/internal/repositories/repositories_mocks_test.go index b62f6120..4c415f41 100644 --- a/internal/repositories/repositories_mocks_test.go +++ b/internal/repositories/repositories_mocks_test.go @@ -45,48 +45,48 @@ func (m *MockCHService) EXPECT() *MockCHServiceMockRecorder { } // GetAggregatedSignals mocks base method. -func (m *MockCHService) GetAggregatedSignals(ctx context.Context, aggArgs *model.AggregatedSignalArgs) ([]*ch.AggSignal, error) { +func (m *MockCHService) GetAggregatedSignals(ctx context.Context, subject string, aggArgs *model.AggregatedSignalArgs) ([]*ch.AggSignal, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAggregatedSignals", ctx, aggArgs) + ret := m.ctrl.Call(m, "GetAggregatedSignals", ctx, subject, aggArgs) ret0, _ := ret[0].([]*ch.AggSignal) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAggregatedSignals indicates an expected call of GetAggregatedSignals. -func (mr *MockCHServiceMockRecorder) GetAggregatedSignals(ctx, aggArgs any) *gomock.Call { +func (mr *MockCHServiceMockRecorder) GetAggregatedSignals(ctx, subject, aggArgs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedSignals", reflect.TypeOf((*MockCHService)(nil).GetAggregatedSignals), ctx, aggArgs) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedSignals", reflect.TypeOf((*MockCHService)(nil).GetAggregatedSignals), ctx, subject, aggArgs) } // GetAggregatedSignalsForRanges mocks base method. -func (m *MockCHService) GetAggregatedSignalsForRanges(ctx context.Context, tokenID uint32, ranges []ch.TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) ([]*ch.AggSignalForRange, error) { +func (m *MockCHService) GetAggregatedSignalsForRanges(ctx context.Context, subject string, ranges []ch.TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) ([]*ch.AggSignalForRange, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAggregatedSignalsForRanges", ctx, tokenID, ranges, globalFrom, globalTo, floatArgs, locationArgs) + ret := m.ctrl.Call(m, "GetAggregatedSignalsForRanges", ctx, subject, ranges, globalFrom, globalTo, floatArgs, locationArgs) ret0, _ := ret[0].([]*ch.AggSignalForRange) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAggregatedSignalsForRanges indicates an expected call of GetAggregatedSignalsForRanges. -func (mr *MockCHServiceMockRecorder) GetAggregatedSignalsForRanges(ctx, tokenID, ranges, globalFrom, globalTo, floatArgs, locationArgs any) *gomock.Call { +func (mr *MockCHServiceMockRecorder) GetAggregatedSignalsForRanges(ctx, subject, ranges, globalFrom, globalTo, floatArgs, locationArgs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedSignalsForRanges", reflect.TypeOf((*MockCHService)(nil).GetAggregatedSignalsForRanges), ctx, tokenID, ranges, globalFrom, globalTo, floatArgs, locationArgs) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedSignalsForRanges", reflect.TypeOf((*MockCHService)(nil).GetAggregatedSignalsForRanges), ctx, subject, ranges, globalFrom, globalTo, floatArgs, locationArgs) } // GetAvailableSignals mocks base method. -func (m *MockCHService) GetAvailableSignals(ctx context.Context, tokenID uint32, filter *model.SignalFilter) ([]string, error) { +func (m *MockCHService) GetAvailableSignals(ctx context.Context, subject string, filter *model.SignalFilter) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAvailableSignals", ctx, tokenID, filter) + ret := m.ctrl.Call(m, "GetAvailableSignals", ctx, subject, filter) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAvailableSignals indicates an expected call of GetAvailableSignals. -func (mr *MockCHServiceMockRecorder) GetAvailableSignals(ctx, tokenID, filter any) *gomock.Call { +func (mr *MockCHServiceMockRecorder) GetAvailableSignals(ctx, subject, filter any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAvailableSignals", reflect.TypeOf((*MockCHService)(nil).GetAvailableSignals), ctx, tokenID, filter) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAvailableSignals", reflect.TypeOf((*MockCHService)(nil).GetAvailableSignals), ctx, subject, filter) } // GetEventCounts mocks base method. @@ -150,46 +150,46 @@ func (mr *MockCHServiceMockRecorder) GetEvents(ctx, subject, from, to, filter an } // GetLatestSignals mocks base method. -func (m *MockCHService) GetLatestSignals(ctx context.Context, latestArgs *model.LatestSignalsArgs) ([]*vss.Signal, error) { +func (m *MockCHService) GetLatestSignals(ctx context.Context, subject string, latestArgs *model.LatestSignalsArgs) ([]*vss.Signal, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLatestSignals", ctx, latestArgs) + ret := m.ctrl.Call(m, "GetLatestSignals", ctx, subject, latestArgs) ret0, _ := ret[0].([]*vss.Signal) ret1, _ := ret[1].(error) return ret0, ret1 } // GetLatestSignals indicates an expected call of GetLatestSignals. -func (mr *MockCHServiceMockRecorder) GetLatestSignals(ctx, latestArgs any) *gomock.Call { +func (mr *MockCHServiceMockRecorder) GetLatestSignals(ctx, subject, latestArgs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestSignals", reflect.TypeOf((*MockCHService)(nil).GetLatestSignals), ctx, latestArgs) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestSignals", reflect.TypeOf((*MockCHService)(nil).GetLatestSignals), ctx, subject, latestArgs) } // GetSegments mocks base method. -func (m *MockCHService) GetSegments(ctx context.Context, tokenID uint32, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig) ([]*model.Segment, error) { +func (m *MockCHService) GetSegments(ctx context.Context, subject string, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig) ([]*model.Segment, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSegments", ctx, tokenID, from, to, mechanism, config) + ret := m.ctrl.Call(m, "GetSegments", ctx, subject, from, to, mechanism, config) ret0, _ := ret[0].([]*model.Segment) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSegments indicates an expected call of GetSegments. -func (mr *MockCHServiceMockRecorder) GetSegments(ctx, tokenID, from, to, mechanism, config any) *gomock.Call { +func (mr *MockCHServiceMockRecorder) GetSegments(ctx, subject, from, to, mechanism, config any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSegments", reflect.TypeOf((*MockCHService)(nil).GetSegments), ctx, tokenID, from, to, mechanism, config) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSegments", reflect.TypeOf((*MockCHService)(nil).GetSegments), ctx, subject, from, to, mechanism, config) } // GetSignalSummaries mocks base method. -func (m *MockCHService) GetSignalSummaries(ctx context.Context, tokenID uint32, filter *model.SignalFilter) ([]*model.SignalDataSummary, error) { +func (m *MockCHService) GetSignalSummaries(ctx context.Context, subject string, filter *model.SignalFilter) ([]*model.SignalDataSummary, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSignalSummaries", ctx, tokenID, filter) + ret := m.ctrl.Call(m, "GetSignalSummaries", ctx, subject, filter) ret0, _ := ret[0].([]*model.SignalDataSummary) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSignalSummaries indicates an expected call of GetSignalSummaries. -func (mr *MockCHServiceMockRecorder) GetSignalSummaries(ctx, tokenID, filter any) *gomock.Call { +func (mr *MockCHServiceMockRecorder) GetSignalSummaries(ctx, subject, filter any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignalSummaries", reflect.TypeOf((*MockCHService)(nil).GetSignalSummaries), ctx, tokenID, filter) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignalSummaries", reflect.TypeOf((*MockCHService)(nil).GetSignalSummaries), ctx, subject, filter) } diff --git a/internal/repositories/repositories_test.go b/internal/repositories/repositories_test.go index 37d299e4..3d9dd813 100644 --- a/internal/repositories/repositories_test.go +++ b/internal/repositories/repositories_test.go @@ -37,6 +37,11 @@ func setupMocks(t *testing.T) *Mocks { } func TestGetSignal(t *testing.T) { + testSubject := cloudevent.ERC721DID{ + ChainID: baseSettings.ChainID, + ContractAddress: baseSettings.VehicleNFTAddress, + TokenID: big.NewInt(1), + }.String() defaultArgs := &model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ TokenID: 1, @@ -65,7 +70,7 @@ func TestGetSignal(t *testing.T) { aggArgs: defaultArgs, mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAggregatedSignals(gomock.Any(), defaultArgs). + GetAggregatedSignals(gomock.Any(), testSubject, defaultArgs). Return([]*ch.AggSignal{}, nil) }, expectedResult: []*model.SignalAggregations{}, @@ -79,7 +84,7 @@ func TestGetSignal(t *testing.T) { {SignalType: ch.FloatType, SignalIndex: 0, Timestamp: time.Date(2024, 6, 11, 0, 0, 0, 0, time.UTC), ValueNumber: 1.0}, } m.CHService.EXPECT(). - GetAggregatedSignals(gomock.Any(), defaultArgs). + GetAggregatedSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: []*model.SignalAggregations{ @@ -99,7 +104,7 @@ func TestGetSignal(t *testing.T) { {SignalType: ch.FloatType, SignalIndex: 0, Timestamp: time.Date(2024, 6, 11, 1, 0, 0, 0, time.UTC), ValueNumber: 3.0}, } m.CHService.EXPECT(). - GetAggregatedSignals(gomock.Any(), defaultArgs). + GetAggregatedSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: []*model.SignalAggregations{ @@ -122,7 +127,7 @@ func TestGetSignal(t *testing.T) { {SignalType: ch.FloatType, SignalIndex: 0, Timestamp: time.Date(2024, 6, 11, 0, 0, 0, 0, time.UTC), ValueNumber: 3.0}, } m.CHService.EXPECT(). - GetAggregatedSignals(gomock.Any(), defaultArgs). + GetAggregatedSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: []*model.SignalAggregations{ @@ -142,7 +147,7 @@ func TestGetSignal(t *testing.T) { {SignalType: ch.FloatType, SignalIndex: 0, Timestamp: time.Date(2024, 6, 11, 1, 0, 0, 0, time.UTC), ValueNumber: 3.0}, } m.CHService.EXPECT(). - GetAggregatedSignals(gomock.Any(), defaultArgs). + GetAggregatedSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: []*model.SignalAggregations{ @@ -169,7 +174,7 @@ func TestGetSignal(t *testing.T) { aggArgs: defaultArgs, mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAggregatedSignals(gomock.Any(), defaultArgs). + GetAggregatedSignals(gomock.Any(), testSubject, defaultArgs). Return(nil, errors.New("service error")) }, expectedResult: nil, @@ -205,6 +210,11 @@ func TestGetSignal(t *testing.T) { } func TestGetSignalLatest(t *testing.T) { + testSubject := cloudevent.ERC721DID{ + ChainID: baseSettings.ChainID, + ContractAddress: baseSettings.VehicleNFTAddress, + TokenID: big.NewInt(1), + }.String() defaultArgs := &model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ TokenID: 1, @@ -226,7 +236,7 @@ func TestGetSignalLatest(t *testing.T) { {Timestamp: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), Name: model.LastSeenField}, } m.CHService.EXPECT(). - GetLatestSignals(gomock.Any(), defaultArgs). + GetLatestSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: &model.SignalCollection{ @@ -243,7 +253,7 @@ func TestGetSignalLatest(t *testing.T) { {Timestamp: time.Date(2024, 6, 11, 0, 0, 0, 0, time.UTC), Name: model.LastSeenField}, } m.CHService.EXPECT(). - GetLatestSignals(gomock.Any(), defaultArgs). + GetLatestSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: &model.SignalCollection{ @@ -266,7 +276,7 @@ func TestGetSignalLatest(t *testing.T) { {Timestamp: time.Date(2024, 6, 11, 2, 0, 0, 0, time.UTC), Name: model.LastSeenField}, } m.CHService.EXPECT(). - GetLatestSignals(gomock.Any(), defaultArgs). + GetLatestSignals(gomock.Any(), testSubject, defaultArgs). Return(signals, nil) }, expectedResult: &model.SignalCollection{ @@ -292,7 +302,7 @@ func TestGetSignalLatest(t *testing.T) { latestArgs: defaultArgs, mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetLatestSignals(gomock.Any(), defaultArgs). + GetLatestSignals(gomock.Any(), testSubject, defaultArgs). Return(nil, errors.New("service error")) }, expectedResult: nil, @@ -324,6 +334,11 @@ func TestGetSignalLatest(t *testing.T) { func TestDeviceActivity(t *testing.T) { vehicleTokenID := int64(1) + testSubject := cloudevent.ERC721DID{ + ChainID: baseSettings.ChainID, + ContractAddress: baseSettings.VehicleNFTAddress, + TokenID: big.NewInt(vehicleTokenID), + }.String() hashdog := "Hashdog" source := "macaron" lastSeen := time.Date(2024, 6, 11, 1, 2, 3, 3, time.UTC) @@ -350,7 +365,7 @@ func TestDeviceActivity(t *testing.T) { name: "Success case - valid last seen", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetLatestSignals(gomock.Any(), latestArgs). + GetLatestSignals(gomock.Any(), testSubject, latestArgs). Return([]*vss.Signal{ {Timestamp: lastSeen, Name: model.LastSeenField}, }, nil) @@ -365,7 +380,7 @@ func TestDeviceActivity(t *testing.T) { name: "vehicle has not transmitted any signals", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetLatestSignals(gomock.Any(), latestArgs). + GetLatestSignals(gomock.Any(), testSubject, latestArgs). Return([]*vss.Signal{ {Timestamp: time.Unix(0, 0).UTC(), Name: model.LastSeenField}, }, nil) @@ -404,7 +419,11 @@ func TestDeviceActivity(t *testing.T) { } func TestGetAvailableSignals(t *testing.T) { - testTokenId := uint32(1) + testSubject := cloudevent.ERC721DID{ + ChainID: baseSettings.ChainID, + ContractAddress: baseSettings.VehicleNFTAddress, + TokenID: big.NewInt(1), + }.String() tests := []struct { name string mockSetup func(m *Mocks) @@ -415,7 +434,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "No signals", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return(nil, nil) }, expectedResult: nil, @@ -425,7 +444,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "One signal", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return([]string{"speed"}, nil) }, expectedResult: []string{"speed"}, @@ -435,7 +454,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "Multiple signals", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return([]string{"speed", "powertrainTractionBatteryStateOfChargeCurrent"}, nil) }, expectedResult: []string{"speed", "powertrainTractionBatteryStateOfChargeCurrent"}, @@ -445,7 +464,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "Mix Unknown signals", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return([]string{"speed", "newSignalName"}, nil) }, expectedResult: []string{"speed"}, @@ -455,7 +474,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "one unknown signals", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return([]string{"newSignalName"}, nil) }, expectedResult: nil, @@ -465,7 +484,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "multiple unknown signals", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return([]string{"newSignalName", "newSignalName2"}, nil) }, expectedResult: nil, @@ -475,7 +494,7 @@ func TestGetAvailableSignals(t *testing.T) { name: "CHService error", mockSetup: func(m *Mocks) { m.CHService.EXPECT(). - GetAvailableSignals(gomock.Any(), testTokenId, nil). + GetAvailableSignals(gomock.Any(), testSubject, nil). Return(nil, errors.New("service error")) }, expectedResult: nil, diff --git a/internal/repositories/segments.go b/internal/repositories/segments.go index 3093b260..f4f13c66 100644 --- a/internal/repositories/segments.go +++ b/internal/repositories/segments.go @@ -220,7 +220,12 @@ func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time } } - chSegments, err := r.chService.GetSegments(ctx, uint32(tokenID), from, to, mechanism, config) + subject := cloudevent.ERC721DID{ + ChainID: r.chainID, + ContractAddress: r.vehicleAddress, + TokenID: big.NewInt(int64(tokenID)), + }.String() + chSegments, err := r.chService.GetSegments(ctx, subject, from, to, mechanism, config) if err != nil { return nil, handleDBError(ctx, err) } @@ -241,12 +246,6 @@ func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time } } - subject := cloudevent.ERC721DID{ - ChainID: r.chainID, - ContractAddress: r.vehicleAddress, - TokenID: big.NewInt(int64(tokenID)), - }.String() - // For refuel/recharge, segment end is when the event is "done"; telemetry often has // the first fuel/charge/odometer reading shortly after. Use an extended summary // window for signal aggregation so those readings are included. @@ -292,7 +291,7 @@ func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time }) g.Go(func() error { var err error - batchAggs, err = r.chService.GetAggregatedSignalsForRanges(gctx, uint32(tokenID), aggRanges, globalFrom, globalTo, floatArgs, locationArgs) + batchAggs, err = r.chService.GetAggregatedSignalsForRanges(gctx, subject, aggRanges, globalFrom, globalTo, floatArgs, locationArgs) return err }) if err := g.Wait(); err != nil { @@ -475,7 +474,7 @@ func (r *Repository) segmentSummary(ctx context.Context, tokenID uint32, subject LocationArgs: locationArgs, } var err error - aggs, err = r.chService.GetAggregatedSignals(ctx, aggArgs) + aggs, err = r.chService.GetAggregatedSignals(ctx, subject, aggArgs) if err != nil { return nil, handleDBError(ctx, err) } @@ -634,7 +633,7 @@ func (r *Repository) daySummary(ctx context.Context, tokenID uint32, subject str FloatArgs: floatArgs, LocationArgs: locationArgs, } - aggs, err := r.chService.GetAggregatedSignals(ctx, aggArgs) + aggs, err := r.chService.GetAggregatedSignals(ctx, subject, aggArgs) if err != nil { return nil, nil, nil, nil, handleDBError(ctx, err) } diff --git a/internal/repositories/validate_test.go b/internal/repositories/validate_test.go index 5fb29d10..4a131904 100644 --- a/internal/repositories/validate_test.go +++ b/internal/repositories/validate_test.go @@ -86,7 +86,7 @@ func TestValidateSegmentArgs(t *testing.T) { }) t.Run("date range exceeded", func(t *testing.T) { - from := validTo.Add(-32 * 24 * time.Hour) // max is 31 days + from := validTo.Add(-32*24*time.Hour - 2*time.Second) // max is 32 days + 1s err := validateSegmentArgs(1, from, validTo) require.Error(t, err) }) @@ -111,8 +111,8 @@ func TestValidateSegmentDateRange(t *testing.T) { require.NoError(t, validateSegmentDateRange(from, to)) }) - t.Run("31 days plus 2 seconds fails", func(t *testing.T) { - from := to.Add(-31*24*time.Hour - 2*time.Second) + t.Run("32 days plus 2 seconds fails", func(t *testing.T) { + from := to.Add(-32*24*time.Hour - 2*time.Second) require.Error(t, validateSegmentDateRange(from, to)) }) } diff --git a/internal/service/ch/ch.go b/internal/service/ch/ch.go index 12eb5014..f71f3113 100644 --- a/internal/service/ch/ch.go +++ b/internal/service/ch/ch.go @@ -75,10 +75,10 @@ func getMaxExecutionTime(maxRequestDuration string) (int, error) { } // GetLatestSignals returns the latest signals based on the provided arguments from the ClickHouse database. -func (s *Service) GetLatestSignals(ctx context.Context, latestArgs *model.LatestSignalsArgs) ([]*vss.Signal, error) { - stmt, args := getLatestQuery(latestArgs) +func (s *Service) GetLatestSignals(ctx context.Context, subject string, latestArgs *model.LatestSignalsArgs) ([]*vss.Signal, error) { + stmt, args := getLatestQuery(subject, latestArgs) if latestArgs.IncludeLastSeen { - lastSeenStmt, lastSeenArgs := getLastSeenQuery(&latestArgs.SignalArgs) + lastSeenStmt, lastSeenArgs := getLastSeenQuery(subject, &latestArgs.SignalArgs) stmt, args = unionAll([]string{stmt, lastSeenStmt}, [][]any{args, lastSeenArgs}) } @@ -92,12 +92,12 @@ func (s *Service) GetLatestSignals(ctx context.Context, latestArgs *model.Latest // GetAggregatedSignals returns a slice of aggregated signals based on the provided arguments from the ClickHouse database. // The signals are sorted by timestamp in ascending order. // The timestamp on each signal is for the start of the interval. -func (s *Service) GetAggregatedSignals(ctx context.Context, aggArgs *model.AggregatedSignalArgs) ([]*AggSignal, error) { +func (s *Service) GetAggregatedSignals(ctx context.Context, subject string, aggArgs *model.AggregatedSignalArgs) ([]*AggSignal, error) { if len(aggArgs.FloatArgs) == 0 && len(aggArgs.StringArgs) == 0 && len(aggArgs.LocationArgs) == 0 { return []*AggSignal{}, nil } - stmt, args, err := getAggQuery(aggArgs) + stmt, args, err := getAggQuery(subject, aggArgs) if err != nil { return nil, err } @@ -112,14 +112,14 @@ func (s *Service) GetAggregatedSignals(ctx context.Context, aggArgs *model.Aggre // GetAggregatedSignalsForRanges returns aggregated signals for multiple time ranges (one per segment) in one query. // Only FloatArgs and LocationArgs are used; StringArgs and ApproxLocArgs are ignored. -func (s *Service) GetAggregatedSignalsForRanges(ctx context.Context, tokenID uint32, ranges []TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) ([]*AggSignalForRange, error) { +func (s *Service) GetAggregatedSignalsForRanges(ctx context.Context, subject string, ranges []TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) ([]*AggSignalForRange, error) { if len(ranges) == 0 { return nil, nil } if len(floatArgs) == 0 && len(locationArgs) == 0 { return []*AggSignalForRange{}, nil } - stmt, args, err := getBatchAggQuery(tokenID, ranges, globalFrom, globalTo, floatArgs, locationArgs) + stmt, args, err := getBatchAggQuery(subject, ranges, globalFrom, globalTo, floatArgs, locationArgs) if err != nil { return nil, err } @@ -227,8 +227,8 @@ func (s *Service) getAggSignals(ctx context.Context, stmt string, args []any) ([ // GetAvailableSignals returns a slice of available signals from the ClickHouse database. // if no signals are available, a nil slice is returned. -func (s *Service) GetAvailableSignals(ctx context.Context, tokenId uint32, filter *model.SignalFilter) ([]string, error) { - stmt, args := getDistinctQuery(tokenId, filter) +func (s *Service) GetAvailableSignals(ctx context.Context, subject string, filter *model.SignalFilter) ([]string, error) { + stmt, args := getDistinctQuery(subject, filter) rows, err := s.conn.Query(ctx, stmt, args...) if err != nil { return nil, fmt.Errorf("failed querying clickhouse: %w", err) @@ -251,8 +251,8 @@ func (s *Service) GetAvailableSignals(ctx context.Context, tokenId uint32, filte return signals, nil } -func (s *Service) GetSignalSummaries(ctx context.Context, tokenId uint32, filter *model.SignalFilter) ([]*model.SignalDataSummary, error) { - stmt, args := getSignalSummariesQuery(tokenId, filter) +func (s *Service) GetSignalSummaries(ctx context.Context, subject string, filter *model.SignalFilter) ([]*model.SignalDataSummary, error) { + stmt, args := getSignalSummariesQuery(subject, filter) rows, err := s.conn.Query(ctx, stmt, args...) if err != nil { return nil, fmt.Errorf("failed querying clickhouse: %w", err) diff --git a/internal/service/ch/ch_test.go b/internal/service/ch/ch_test.go index e62ad9f7..ba855164 100644 --- a/internal/service/ch/ch_test.go +++ b/internal/service/ch/ch_test.go @@ -20,6 +20,10 @@ import ( const ( day = time.Hour * 24 dataPoints = 10 + + testSubject1 = "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:1" + testSubject2 = "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:2" + testSubject100 = "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:100" ) type CHServiceTestSuite struct { @@ -802,7 +806,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { } for _, tc := range testCases { c.Run(tc.name, func() { - result, err := c.chService.GetAggregatedSignals(ctx, &tc.aggArgs) + result, err := c.chService.GetAggregatedSignals(ctx, testSubject1, &tc.aggArgs) c.Require().NoError(err) c.Require().Len(result, len(tc.expected)) @@ -902,7 +906,7 @@ func (c *CHServiceTestSuite) TestGetLatestSignal() { } for _, tc := range testCases { c.Run(tc.name, func() { - result, err := c.chService.GetLatestSignals(ctx, &tc.latestArgs) + result, err := c.chService.GetLatestSignals(ctx, testSubject1, &tc.latestArgs) c.Require().NoError(err) for i, sig := range result { c.Require().Equal(tc.expected[i], *sig) @@ -914,20 +918,20 @@ func (c *CHServiceTestSuite) TestGetLatestSignal() { func (c *CHServiceTestSuite) TestGetAvailableSignals() { ctx := context.Background() c.Run("has signals", func() { - result, err := c.chService.GetAvailableSignals(ctx, 1, nil) + result, err := c.chService.GetAvailableSignals(ctx, testSubject1, nil) c.Require().NoError(err) c.Require().Len(result, 3) c.Require().Equal([]string{vss.FieldCurrentLocationCoordinates, vss.FieldPowertrainType, vss.FieldSpeed}, result) }) c.Run("no signals", func() { - result, err := c.chService.GetAvailableSignals(ctx, 2, nil) + result, err := c.chService.GetAvailableSignals(ctx, testSubject2, nil) c.Require().NoError(err) c.Require().Nil(result) }) c.Run("filter signals", func() { - result, err := c.chService.GetAvailableSignals(ctx, 1, &model.SignalFilter{Source: ref("Unknown")}) + result, err := c.chService.GetAvailableSignals(ctx, testSubject1, &model.SignalFilter{Source: ref("Unknown")}) c.Require().NoError(err) c.Require().Nil(result) }) @@ -950,7 +954,7 @@ func (c *CHServiceTestSuite) TestOriginGrouping() { Name: vss.FieldSpeed, Timestamp: currentTime, Source: "test/origin", - TokenID: 100, + Subject: testSubject100, ValueNumber: 100.0, } signals = append(signals, signal) @@ -986,7 +990,7 @@ func (c *CHServiceTestSuite) TestOriginGrouping() { } // Query signals - result, err := c.chService.GetAggregatedSignals(ctx, aggArgs) + result, err := c.chService.GetAggregatedSignals(ctx, testSubject100, aggArgs) c.Require().NoError(err, "Failed to get aggregated signals") // We expect exactly one group since we're using a 30-day interval @@ -1323,7 +1327,7 @@ func (c *CHServiceTestSuite) insertTestData() { Name: vss.FieldSpeed, Timestamp: c.dataStartTime.Add(time.Second * time.Duration(30*i)), Source: sources[i%3], - TokenID: 1, + Subject: testSubject1, ValueNumber: float64(i), } @@ -1331,7 +1335,7 @@ func (c *CHServiceTestSuite) insertTestData() { Name: vss.FieldPowertrainType, Timestamp: c.dataStartTime.Add(time.Second * time.Duration(30*i)), Source: sources[i%3], - TokenID: 1, + Subject: testSubject1, ValueString: fmt.Sprintf("value%d", i+1), } @@ -1339,7 +1343,7 @@ func (c *CHServiceTestSuite) insertTestData() { Name: vss.FieldCurrentLocationCoordinates, Timestamp: c.dataStartTime.Add(time.Second * time.Duration(30*i)), Source: sources[i%3], - TokenID: 1, + Subject: testSubject1, ValueLocation: vss.Location{Latitude: 3 * float64(i+1), Longitude: 5 * float64(i+1), HDOP: 7 * float64(i+1)}, } testSignal = append(testSignal, numSig, strSig, locSig) @@ -1349,7 +1353,7 @@ func (c *CHServiceTestSuite) insertTestData() { Name: vss.FieldCurrentLocationCoordinates, Timestamp: c.dataStartTime.Add(time.Second * time.Duration(299)), Source: sources[0], - TokenID: 1, + Subject: testSubject1, ValueLocation: vss.Location{Latitude: 0, Longitude: 0, HDOP: 111}, }) diff --git a/internal/service/ch/changepoint_detector.go b/internal/service/ch/changepoint_detector.go index 1ee3fa8f..e71a0464 100644 --- a/internal/service/ch/changepoint_detector.go +++ b/internal/service/ch/changepoint_detector.go @@ -33,7 +33,7 @@ func NewChangePointDetector(conn clickhouse.Conn) *ChangePointDetector { // DetectSegments implements CUSUM-based change point detection func (d *ChangePointDetector) DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) ([]*model.Segment, error) { @@ -43,7 +43,7 @@ func (d *ChangePointDetector) DetectSegments( // Look back maxGap seconds before 'from' to detect segments that started before the query range. lookbackFrom := from.Add(-time.Duration(maxGap) * time.Second) - windows, err := getWindowedSignalCounts(ctx, d.conn, tokenID, lookbackFrom, to, defaultCUSUMWindowSeconds, defaultCUSUMSignalCountThreshold, defaultCUSUMDistinctSignalCountThreshold) + windows, err := getWindowedSignalCounts(ctx, d.conn, subject, lookbackFrom, to, defaultCUSUMWindowSeconds, defaultCUSUMSignalCountThreshold, defaultCUSUMDistinctSignalCountThreshold) if err != nil { return nil, fmt.Errorf("failed to query window signal counts: %w", err) } diff --git a/internal/service/ch/detector.go b/internal/service/ch/detector.go index 603f09c9..d52dc37d 100644 --- a/internal/service/ch/detector.go +++ b/internal/service/ch/detector.go @@ -56,7 +56,7 @@ type SegmentDetector interface { // DetectSegments identifies vehicle usage segments using mechanism-specific logic DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) ([]*model.Segment, error) diff --git a/internal/service/ch/frequency_detector.go b/internal/service/ch/frequency_detector.go index 0128a1c6..d382c8f3 100644 --- a/internal/service/ch/frequency_detector.go +++ b/internal/service/ch/frequency_detector.go @@ -29,7 +29,7 @@ func NewFrequencyDetector(conn clickhouse.Conn) *FrequencyDetector { // DetectSegments implements frequency-based segment detection func (d *FrequencyDetector) DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) ([]*model.Segment, error) { @@ -43,7 +43,7 @@ func (d *FrequencyDetector) DetectSegments( // Look back maxGap seconds before 'from' to detect segments that started before the query range. lookbackFrom := from.Add(-time.Duration(maxGap) * time.Second) - windows, err := getWindowedSignalCounts(ctx, d.conn, tokenID, lookbackFrom, to, defaultWindowSizeSeconds, signalThreshold, defaultDistinctSignalCountThreshold) + windows, err := getWindowedSignalCounts(ctx, d.conn, subject, lookbackFrom, to, defaultWindowSizeSeconds, signalThreshold, defaultDistinctSignalCountThreshold) if err != nil { return nil, fmt.Errorf("failed to query active windows: %w", err) } diff --git a/internal/service/ch/idling_detector.go b/internal/service/ch/idling_detector.go index e1e58cf5..543c1f9f 100644 --- a/internal/service/ch/idling_detector.go +++ b/internal/service/ch/idling_detector.go @@ -30,7 +30,7 @@ func NewIdlingDetector(conn clickhouse.Conn) *IdlingDetector { // DetectSegments fetches RPM samples (1 CH query) and finds contiguous runs of idle RPM in-memory. func (d *IdlingDetector) DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) ([]*model.Segment, error) { @@ -44,7 +44,7 @@ func (d *IdlingDetector) DetectSegments( lookbackFrom := from.Add(-time.Duration(maxGap) * time.Second) // Single CH query: RPM samples (returned sorted by CH) - samples, err := getLevelSamples(ctx, d.conn, tokenID, vss.FieldPowertrainCombustionEngineSpeed, lookbackFrom, to) + samples, err := getLevelSamples(ctx, d.conn, subject, vss.FieldPowertrainCombustionEngineSpeed, lookbackFrom, to) if err != nil { return nil, fmt.Errorf("failed to query RPM samples: %w", err) } diff --git a/internal/service/ch/ignition_detector.go b/internal/service/ch/ignition_detector.go index 5a484c9f..025f8654 100644 --- a/internal/service/ch/ignition_detector.go +++ b/internal/service/ch/ignition_detector.go @@ -30,7 +30,7 @@ type StateChange struct { // DetectSegments implements ignition-based segment detection func (d *IgnitionDetector) DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) (_ []*model.Segment, retErr error) { @@ -39,7 +39,7 @@ func (d *IgnitionDetector) DetectSegments( minDuration := rc.minDuration // Fetch all state changes with a single query - stmt, args := d.getStateChangesQueryWithLookback(tokenID, from, to) + stmt, args := d.getStateChangesQueryWithLookback(subject, from, to) rows, err := d.conn.Query(ctx, stmt, args...) if err != nil { @@ -62,7 +62,7 @@ func (d *IgnitionDetector) DetectSegments( } // Process state changes in Go to build segments with debouncing - segments := d.buildSegmentsWithDebouncing(tokenID, stateChanges, from, to, minIdle, minDuration) + segments := d.buildSegmentsWithDebouncing(stateChanges, from, to, minIdle, minDuration) return segments, nil } @@ -76,7 +76,7 @@ const maxLookbackDays = 30 // Performance notes: // - PREWHERE filters on primary key columns before FINAL merge (much faster) // - Lookback is bounded to maxLookbackDays to prevent unbounded scans -func (d *IgnitionDetector) getStateChangesQueryWithLookback(tokenID uint32, from, to time.Time) (string, []any) { +func (d *IgnitionDetector) getStateChangesQueryWithLookback(subject string, from, to time.Time) (string, []any) { // Bound the lookback to prevent scanning unlimited history lookbackLimit := from.AddDate(0, 0, -maxLookbackDays) @@ -84,12 +84,12 @@ func (d *IgnitionDetector) getStateChangesQueryWithLookback(tokenID uint32, from // - Last state change before 'from' (LIMIT 1, ordered DESC then re-ordered) // - All state changes in range [from, to) // - // PREWHERE on token_id filters before FINAL merge, significantly reducing work + // PREWHERE on subject filters before FINAL merge, significantly reducing work query := ` SELECT timestamp, new_state, prev_state FROM ( SELECT timestamp, new_state, prev_state FROM signal_state_changes FINAL - PREWHERE token_id = ? + PREWHERE subject = ? WHERE signal_name = 'isIgnitionOn' AND timestamp >= ? AND timestamp < ? @@ -102,7 +102,7 @@ SELECT timestamp, new_state, prev_state FROM ( -- All state changes within the query range SELECT timestamp, new_state, prev_state FROM signal_state_changes FINAL - PREWHERE token_id = ? + PREWHERE subject = ? WHERE signal_name = 'isIgnitionOn' AND timestamp >= ? AND timestamp < ? @@ -110,14 +110,14 @@ SELECT timestamp, new_state, prev_state FROM ( ) ORDER BY timestamp` - args := []any{tokenID, lookbackLimit, from, tokenID, from, to} + args := []any{subject, lookbackLimit, from, subject, from, to} return query, args } // buildSegmentsWithDebouncing processes state changes and applies debouncing logic // to merge consecutive short segments separated by less than minIdle seconds -func (d *IgnitionDetector) buildSegmentsWithDebouncing(tokenID uint32, stateChanges []StateChange, from, to time.Time, minIdle, minDuration int) []*model.Segment { +func (d *IgnitionDetector) buildSegmentsWithDebouncing(stateChanges []StateChange, from, to time.Time, minIdle, minDuration int) []*model.Segment { if len(stateChanges) == 0 { return []*model.Segment{} } diff --git a/internal/service/ch/ignition_detector_test.go b/internal/service/ch/ignition_detector_test.go index 82a524cf..d127ec27 100644 --- a/internal/service/ch/ignition_detector_test.go +++ b/internal/service/ch/ignition_detector_test.go @@ -72,7 +72,6 @@ func TestFilterNoise(t *testing.T) { func TestBuildSegmentsWithDebouncing(t *testing.T) { detector := &IgnitionDetector{} now := time.Now() - tokenID := uint32(1) minIdle := 300 // 5 minutes minDuration := 60 // 1 minute @@ -80,11 +79,11 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { from := now.Add(-time.Hour) to := now - result := detector.buildSegmentsWithDebouncing(tokenID, nil, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(nil, from, to, minIdle, minDuration) require.NotNil(t, result) require.Empty(t, result) - result = detector.buildSegmentsWithDebouncing(tokenID, []StateChange{}, from, to, minIdle, minDuration) + result = detector.buildSegmentsWithDebouncing([]StateChange{}, from, to, minIdle, minDuration) require.NotNil(t, result) require.Empty(t, result) }) @@ -98,7 +97,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { {Timestamp: from.Add(20 * time.Minute), State: 0, PrevState: 1}, } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Len(t, result, 1) require.Equal(t, from.Add(10*time.Minute), result[0].Start.Timestamp) require.NotNil(t, result[0].End) @@ -119,7 +118,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { {Timestamp: from.Add(50 * time.Minute), State: 0, PrevState: 1}, } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Len(t, result, 2) }) @@ -132,7 +131,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { // No OFF signal } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Len(t, result, 1) require.True(t, result[0].IsOngoing) require.Nil(t, result[0].End) @@ -148,7 +147,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { {Timestamp: from.Add(10*time.Minute + 30*time.Second), State: 0, PrevState: 1}, } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Empty(t, result) }) @@ -162,7 +161,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { // This should not create an ongoing segment } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Empty(t, result) }) @@ -176,7 +175,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { {Timestamp: from.Add(10 * time.Minute), State: 0, PrevState: 1}, } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Len(t, result, 1) require.True(t, result[0].StartedBeforeRange) }) @@ -192,7 +191,7 @@ func TestBuildSegmentsWithDebouncing(t *testing.T) { {Timestamp: end, State: 0, PrevState: 1}, } - result := detector.buildSegmentsWithDebouncing(tokenID, changes, from, to, minIdle, minDuration) + result := detector.buildSegmentsWithDebouncing(changes, from, to, minIdle, minDuration) require.Len(t, result, 1) require.Equal(t, 600, result[0].Duration) // 10 minutes = 600 seconds }) diff --git a/internal/service/ch/queries.go b/internal/service/ch/queries.go index 6c67bd05..67af2873 100644 --- a/internal/service/ch/queries.go +++ b/internal/service/ch/queries.go @@ -21,7 +21,7 @@ const ( AggStringCol = "agg_string" AggLocationCol = "agg_location" aggTableName = "agg_table" - tokenIDWhere = vss.TokenIDCol + " = ?" + subjectWhere = vss.SubjectCol + " = ?" eventSubjectWhere = vss.EventSubjectCol + " = ?" nameIn = vss.NameCol + " IN ?" timestampFrom = vss.TimestampCol + " >= ?" @@ -40,7 +40,7 @@ const ( lastSeenName = "'" + model.LastSeenField + "' AS name" numValAsNull = "NULL AS " + vss.ValueNumberCol strValAsNull = "NULL AS " + vss.ValueStringCol - locValAsZero = "CAST(tuple(0, 0, 0), 'Tuple(latitude Float64, longitude Float64, hdop Float64)') AS " + vss.ValueLocationCol + locValAsZero = "CAST(tuple(0, 0, 0, 0), 'Tuple(latitude Float64, longitude Float64, hdop Float64, heading Float64)') AS " + vss.ValueLocationCol lastSeenTS = "max(" + vss.TimestampCol + ") AS ts" ) @@ -63,8 +63,8 @@ const ( ) const ( - locationTupleType = "Tuple(latitude Float64, longitude Float64, hdop Float64)" - locationZeroTuple = "CAST(tuple(0, 0, 0), '" + locationTupleType + "')" + locationTupleType = "Tuple(latitude Float64, longitude Float64, hdop Float64, heading Float64)" + locationZeroTuple = "CAST(tuple(0, 0, 0, 0), '" + locationTupleType + "')" ) var SourceTranslations = map[string][]string{ @@ -237,7 +237,7 @@ func floatAggExpr(valueNumberExpr, timestampExpr string, aggType model.FloatAggr func locationAggExpr(valueLocationExpr, timestampExpr string, aggType model.LocationAggregation) string { switch aggType { case model.LocationAggregationAvg: - return "CAST(tuple(avg(" + valueLocationExpr + ".latitude), avg(" + valueLocationExpr + ".longitude), avg(" + valueLocationExpr + ".hdop)), '" + locationTupleType + "')" + return "CAST(tuple(avg(" + valueLocationExpr + ".latitude), avg(" + valueLocationExpr + ".longitude), avg(" + valueLocationExpr + ".hdop), avg(" + valueLocationExpr + ".heading)), '" + locationTupleType + "')" case model.LocationAggregationRand: return fmt.Sprintf("groupArraySample(1, %d)("+valueLocationExpr+")[1]", time.Now().UnixMilli()) case model.LocationAggregationFirst: @@ -312,7 +312,7 @@ WHERE GROUP BY name */ -func getLatestQuery(latestArgs *model.LatestSignalsArgs) (string, []any) { +func getLatestQuery(subject string, latestArgs *model.LatestSignalsArgs) (string, []any) { signalNames := make([]string, 0, len(latestArgs.SignalNames)) for name := range latestArgs.SignalNames { signalNames = append(signalNames, name) @@ -330,7 +330,7 @@ func getLatestQuery(latestArgs *model.LatestSignalsArgs) (string, []any) { qm.Select(latestString), qm.Select(latestLocation), qm.From(vss.TableName), - qm.Where(tokenIDWhere, latestArgs.TokenID), + qm.Where(subjectWhere, subject), qm.Expr( qm.WhereIn(nameIn, signalNames), qm.Or2( @@ -362,7 +362,7 @@ FROM WHERE token_id = 15 */ -func getLastSeenQuery(sigArgs *model.SignalArgs) (string, []any) { +func getLastSeenQuery(subject string, sigArgs *model.SignalArgs) (string, []any) { if sigArgs == nil { return "", nil } @@ -373,7 +373,7 @@ func getLastSeenQuery(sigArgs *model.SignalArgs) (string, []any) { qm.Select(strValAsNull), qm.Select(locValAsZero), qm.From(vss.TableName), - qm.Where(tokenIDWhere, sigArgs.TokenID), + qm.Where(subjectWhere, subject), } mods = append(mods, getFilterMods(sigArgs.Filter)...) return newQuery(mods...) @@ -434,7 +434,7 @@ ORDER BY signal_type ASC, signal_index ASC; */ -func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { +func getAggQuery(subject string, aggArgs *model.AggregatedSignalArgs) (string, []any, error) { if aggArgs == nil { return "", nil, nil } @@ -518,7 +518,7 @@ func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { selectNumberAggs(aggArgs.FloatArgs), selectStringAggs(aggArgs.StringArgs), selectLocationAggs(aggArgs.LocationArgs), - qm.Where(tokenIDWhere, aggArgs.TokenID), + qm.Where(subjectWhere, subject), qm.Where(timestampFrom, aggArgs.FromTS), qm.Where(timestampTo, aggArgs.ToTS), qm.From(vss.TableName), @@ -538,7 +538,7 @@ func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { // getBatchAggQuery returns a query that computes the same aggregations as getAggQuery for multiple // time ranges (segments) in one round-trip. Only FloatArgs and LocationArgs are supported. // Result columns: seg_idx (Int32), signal_type, signal_index, value_number, value_string, value_location. -func getBatchAggQuery(tokenID uint32, ranges []TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) (string, []any, error) { +func getBatchAggQuery(subject string, ranges []TimeRange, globalFrom, globalTo time.Time, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) (string, []any, error) { if len(ranges) == 0 { return "", nil, errors.New("no ranges for batch agg") } @@ -551,7 +551,7 @@ func getBatchAggQuery(tokenID uint32, ranges []TimeRange, globalFrom, globalTo t for _, r := range ranges { args = append(args, r.From, r.To) } - args = append(args, tokenID, globalFrom, globalTo) + args = append(args, subject, globalFrom, globalTo) inner := buildBatchAggInner(valueTable, multiIf) outer := buildBatchAggOuter(inner, floatArgs, locationArgs) return outer, args, nil @@ -571,7 +571,7 @@ func buildBatchAggValueTable(floatArgs []model.FloatSignalArgs, locationArgs []m func buildBatchAggInner(valueTable, multiIf string) string { selectList := multiIf + ", " + signalTypeCol + ", " + signalIndexCol + ", " + vss.TimestampCol + ", " + vss.ValueNumberCol + ", " + vss.ValueStringCol + ", " + vss.ValueLocationCol return "SELECT " + selectList + " FROM " + vss.TableName + " INNER JOIN " + valueTable + - " WHERE " + tokenIDWhere + " AND " + vss.TimestampCol + " >= ? AND " + vss.TimestampCol + " < ?" + " WHERE " + subjectWhere + " AND " + vss.TimestampCol + " >= ? AND " + vss.TimestampCol + " < ?" } func buildBatchAggOuter(inner string, floatArgs []model.FloatSignalArgs, locationArgs []model.LocationSignalArgs) string { @@ -700,11 +700,11 @@ func buildSegmentIndexMultiIf(timestampCol string, nRanges int) string { return "multiIf(" + strings.Join(parts, ", ") + ", -1) AS seg_idx" } -func getDistinctQuery(tokenId uint32, filter *model.SignalFilter) (string, []any) { +func getDistinctQuery(subject string, filter *model.SignalFilter) (string, []any) { mods := []qm.QueryMod{ qm.Distinct(vss.NameCol), qm.From(vss.TableName), - qm.Where(tokenIDWhere, tokenId), + qm.Where(subjectWhere, subject), qm.OrderBy(vss.NameCol), } mods = append(mods, getFilterMods(filter)...) @@ -712,15 +712,15 @@ func getDistinctQuery(tokenId uint32, filter *model.SignalFilter) (string, []any return stmt, args } -// select Count(*), max(timestamp), min(timestamp), name, from signal where token_id = '39718' GROUP BY name -func getSignalSummariesQuery(tokenId uint32, filter *model.SignalFilter) (string, []any) { +// select Count(*), max(timestamp), min(timestamp), name, from signal where subject = '...' GROUP BY name +func getSignalSummariesQuery(subject string, filter *model.SignalFilter) (string, []any) { mods := []qm.QueryMod{ qm.Select(vss.NameCol), qm.Select("COUNT(*)"), qm.Select("MIN(" + vss.TimestampCol + ")"), qm.Select("MAX(" + vss.TimestampCol + ")"), qm.From(vss.TableName), - qm.Where(tokenIDWhere, tokenId), + qm.Where(subjectWhere, subject), qm.GroupBy(vss.NameCol), qm.OrderBy(vss.NameCol), } diff --git a/internal/service/ch/recharge_detector.go b/internal/service/ch/recharge_detector.go index 23bad6f3..7fa56af7 100644 --- a/internal/service/ch/recharge_detector.go +++ b/internal/service/ch/recharge_detector.go @@ -31,7 +31,7 @@ func NewRechargeDetector(conn clickhouse.Conn) *RechargeDetector { // DetectSegments finds periods where state of charge rises (trough to peak), filters by odometer, and merges nearby sessions. func (d *RechargeDetector) DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) ([]*model.Segment, error) { @@ -44,7 +44,7 @@ func (d *RechargeDetector) DetectSegments( if config != nil && config.MinIncreasePercent != nil && *config.MinIncreasePercent > 0 { minRisePct = float64(*config.MinIncreasePercent) } - return detectRechargeSegments(ctx, d.conn, tokenID, from, to, rc.minDuration, minRisePct) + return detectRechargeSegments(ctx, d.conn, subject, from, to, rc.minDuration, minRisePct) } // GetMechanismName returns the name of this detection mechanism. @@ -53,9 +53,9 @@ func (d *RechargeDetector) GetMechanismName() string { } // detectRechargeSegments: 2 CH queries (SoC + odometer), then all processing in-memory. -func detectRechargeSegments(ctx context.Context, conn clickhouse.Conn, tokenID uint32, from, to time.Time, minDuration int, minRisePct float64) ([]*model.Segment, error) { +func detectRechargeSegments(ctx context.Context, conn clickhouse.Conn, subject string, from, to time.Time, minDuration int, minRisePct float64) ([]*model.Segment, error) { // Query 1: SoC samples (returned sorted by CH) - socSamples, err := getLevelSamples(ctx, conn, tokenID, vss.FieldPowertrainTractionBatteryStateOfChargeCurrent, from, to) + socSamples, err := getLevelSamples(ctx, conn, subject, vss.FieldPowertrainTractionBatteryStateOfChargeCurrent, from, to) if err != nil { return nil, fmt.Errorf("failed to query SoC samples: %w", err) } @@ -64,7 +64,7 @@ func detectRechargeSegments(ctx context.Context, conn clickhouse.Conn, tokenID u } // Query 2: Odometer samples (returned sorted by CH) - odoSamples, err := getLevelSamples(ctx, conn, tokenID, vss.FieldPowertrainTransmissionTravelledDistance, from, to) + odoSamples, err := getLevelSamples(ctx, conn, subject, vss.FieldPowertrainTransmissionTravelledDistance, from, to) if err != nil { return nil, fmt.Errorf("failed to query odometer samples: %w", err) } diff --git a/internal/service/ch/refuel_detector.go b/internal/service/ch/refuel_detector.go index 9f1d0915..370ce595 100644 --- a/internal/service/ch/refuel_detector.go +++ b/internal/service/ch/refuel_detector.go @@ -36,7 +36,7 @@ func NewRefuelDetector(conn clickhouse.Conn) *RefuelDetector { // 1 CH query (fuel only). func (d *RefuelDetector) DetectSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, config *model.SegmentConfig, ) ([]*model.Segment, error) { @@ -51,7 +51,7 @@ func (d *RefuelDetector) DetectSegments( fuelTo := to.Add(windowDur) // Single CH query: fuel samples (returned sorted by CH) - samples, err := getLevelSamples(ctx, d.conn, tokenID, vss.FieldPowertrainFuelSystemRelativeLevel, fuelFrom, fuelTo) + samples, err := getLevelSamples(ctx, d.conn, subject, vss.FieldPowertrainFuelSystemRelativeLevel, fuelFrom, fuelTo) if err != nil { return nil, fmt.Errorf("failed to query fuel samples: %w", err) } diff --git a/internal/service/ch/segments.go b/internal/service/ch/segments.go index 12d80e49..44d0a6bc 100644 --- a/internal/service/ch/segments.go +++ b/internal/service/ch/segments.go @@ -34,7 +34,7 @@ func newDetector(conn clickhouse.Conn, mechanism model.DetectionMechanism) (Segm // GetSegments returns segments detected using the specified mechanism. func (s *Service) GetSegments( ctx context.Context, - tokenID uint32, + subject string, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, @@ -45,7 +45,7 @@ func (s *Service) GetSegments( } timer := prometheus.NewTimer(GetSegmentsLatency.WithLabelValues(mechanism.String())) - segments, err := detector.DetectSegments(ctx, tokenID, from, to, config) + segments, err := detector.DetectSegments(ctx, subject, from, to, config) timer.ObserveDuration() if err != nil { return nil, err diff --git a/internal/service/ch/segments_utils.go b/internal/service/ch/segments_utils.go index c8559c1b..73e5b540 100644 --- a/internal/service/ch/segments_utils.go +++ b/internal/service/ch/segments_utils.go @@ -30,7 +30,7 @@ type ActiveWindow struct { func getWindowedSignalCounts( ctx context.Context, conn clickhouse.Conn, - tokenID uint32, + subject string, from, to time.Time, windowSizeSeconds int, signalThreshold int, @@ -43,14 +43,14 @@ SELECT count() AS signal_count, uniq(name) AS distinct_signal_count FROM signal FINAL -PREWHERE token_id = ? +PREWHERE subject = ? WHERE timestamp >= ? AND timestamp < ? GROUP BY window_start HAVING signal_count >= ? AND distinct_signal_count >= ? ORDER BY window_start` - rows, err := conn.Query(ctx, query, windowSizeSeconds, windowSizeSeconds, windowSizeSeconds, tokenID, from, to, signalThreshold, distinctSignalThreshold) + rows, err := conn.Query(ctx, query, windowSizeSeconds, windowSizeSeconds, windowSizeSeconds, subject, from, to, signalThreshold, distinctSignalThreshold) if err != nil { return nil, fmt.Errorf("failed to query windowed signal counts: %w", err) } @@ -122,14 +122,14 @@ func levelFirstLastInRange(samples []levelSample, segStart, segEnd time.Time) (f // getLevelSamples fetches timestamped level samples for a signal. // Results are returned in timestamp order (ORDER BY in the query). -// Uses PREWHERE on token_id for efficient primary-key filtering before FINAL merge. -func getLevelSamples(ctx context.Context, conn clickhouse.Conn, tokenID uint32, name string, from, to time.Time) (_ []levelSample, retErr error) { +// Uses PREWHERE on subject for efficient primary-key filtering before FINAL merge. +func getLevelSamples(ctx context.Context, conn clickhouse.Conn, subject string, name string, from, to time.Time) (_ []levelSample, retErr error) { query := "SELECT " + vss.TimestampCol + ", " + vss.ValueNumberCol + " FROM " + vss.TableName + " FINAL" + - " PREWHERE " + vss.TokenIDCol + " = ?" + + " PREWHERE " + vss.SubjectCol + " = ?" + " WHERE " + vss.NameCol + " = ? AND " + vss.TimestampCol + " >= ? AND " + vss.TimestampCol + " < ?" + " ORDER BY " + vss.TimestampCol - rows, err := conn.Query(ctx, query, tokenID, name, from, to) + rows, err := conn.Query(ctx, query, subject, name, from, to) if err != nil { return nil, err } From 863e7d65c4fcff1f08988a29a18452072dfea744 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sun, 1 Mar 2026 16:55:13 -0500 Subject: [PATCH 02/10] Tighten up this comment --- internal/repositories/repositories.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repositories/repositories.go b/internal/repositories/repositories.go index b00d886a..64dbf123 100644 --- a/internal/repositories/repositories.go +++ b/internal/repositories/repositories.go @@ -76,7 +76,7 @@ func NewRepository(chService CHService, settings config.Settings) (*Repository, } -// toSubject converts a tokenID to a W3C DID subject string using the repository's chain ID and vehicle address. +// toSubject converts a vehicle token id to a DID subject string. func (r *Repository) toSubject(tokenID uint32) string { return cloudevent.ERC721DID{ ChainID: r.chainID, From 9975b7d36e8ccce425e5e6bad4f67b60f1b6be51 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sun, 1 Mar 2026 17:07:25 -0500 Subject: [PATCH 03/10] Fix up a location test and use the subject helper one more time --- e2e/segments_test.go | 2 +- internal/repositories/segments.go | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/e2e/segments_test.go b/e2e/segments_test.go index aa05e07f..fca8d318 100644 --- a/e2e/segments_test.go +++ b/e2e/segments_test.go @@ -173,7 +173,7 @@ func insertTestSignals(t *testing.T, conn clickhouse.Conn, signals []testSignal) sig.CloudEventID, sig.ValueNumber, sig.ValueString, - []interface{}{float64(0), float64(0), float64(0)}, // Empty location tuple + []interface{}{float64(0), float64(0), float64(0), float64(0)}, // Empty location tuple ) require.NoError(t, err, "Failed to append signal") } diff --git a/internal/repositories/segments.go b/internal/repositories/segments.go index f4f13c66..3c8c88f7 100644 --- a/internal/repositories/segments.go +++ b/internal/repositories/segments.go @@ -220,11 +220,7 @@ func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time } } - subject := cloudevent.ERC721DID{ - ChainID: r.chainID, - ContractAddress: r.vehicleAddress, - TokenID: big.NewInt(int64(tokenID)), - }.String() + subject := r.toSubject(uint32(tokenID)) chSegments, err := r.chService.GetSegments(ctx, subject, from, to, mechanism, config) if err != nil { return nil, handleDBError(ctx, err) From 7644615e8608b0a17d7626f89fdb67979a70a4dd Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sun, 1 Mar 2026 18:23:49 -0500 Subject: [PATCH 04/10] go mod tidy --- go.sum | 6 ------ 1 file changed, 6 deletions(-) diff --git a/go.sum b/go.sum index 17c3b13e..d368c175 100644 --- a/go.sum +++ b/go.sum @@ -22,12 +22,6 @@ github.com/DIMO-Network/credit-tracker v0.0.6 h1:9C9AXah2uCQKCr7Kqzu2H9aXWj7wEOj github.com/DIMO-Network/credit-tracker v0.0.6/go.mod h1:k037ZQMuBcyNj0Czg/jd6GCBC/HihkSsA7c1FRrF2yI= github.com/DIMO-Network/fetch-api v0.0.16 h1:jOIM9AHOCTN9Omh5QoRrdVsy0UCPHwB+08/DEI0qnqs= github.com/DIMO-Network/fetch-api v0.0.16/go.mod h1:pD9+5wPYjz3ORH9FBsXLlomsZJRPTyOzKy19urT6xew= -github.com/DIMO-Network/model-garage v0.8.17 h1:6F29k0OUMRsKbZU3E73Cg8Dadh/GpgrUtJruaSBgalU= -github.com/DIMO-Network/model-garage v0.8.17/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= -github.com/DIMO-Network/model-garage v0.8.18-0.20260227195128-b6fe603d5a94 h1:e+Z0wXbIaKVVfNlVRi4KfDn2GrZcY5TUsdjXCOyV4HU= -github.com/DIMO-Network/model-garage v0.8.18-0.20260227195128-b6fe603d5a94/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= -github.com/DIMO-Network/model-garage v0.8.18-0.20260228213518-5572b215dc07 h1:L2wtmEwMxHhki7U6CNLle5fLX88amMieIZ+eQy+dObU= -github.com/DIMO-Network/model-garage v0.8.18-0.20260228213518-5572b215dc07/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= github.com/DIMO-Network/model-garage v0.8.18-0.20260301212110-8cb393aaa1c9 h1:s/FIk7WNp0fyNnOv169zIUKOPP1JurU3JxwRw8i4hGk= github.com/DIMO-Network/model-garage v0.8.18-0.20260301212110-8cb393aaa1c9/go.mod h1:goE7y2N58G8CDUym/RqaAEH0E0w6+gzdUSprWItqmyU= github.com/DIMO-Network/server-garage v0.0.7 h1:kOBVyOtIbxa1x9pAf1epABTb9l/U3khf0PwUaHeHiKs= From 665413c8ca1b8aa1715f40a29e0cadc7f733f234 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sun, 1 Mar 2026 18:25:41 -0500 Subject: [PATCH 05/10] Remove embedded field name from access --- internal/repositories/repositories.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repositories/repositories.go b/internal/repositories/repositories.go index 64dbf123..1663270c 100644 --- a/internal/repositories/repositories.go +++ b/internal/repositories/repositories.go @@ -144,7 +144,7 @@ func (r *Repository) GetSignalLatest(ctx context.Context, latestArgs *model.Late if err := validateLatestSigArgs(latestArgs); err != nil { return nil, errorhandler.NewBadRequestError(ctx, err) } - subject := r.toSubject(latestArgs.SignalArgs.TokenID) + subject := r.toSubject(latestArgs.TokenID) signals, err := r.chService.GetLatestSignals(ctx, subject, latestArgs) if err != nil { return nil, handleDBError(ctx, err) From c90917f7a29e4e58c4df936b518cb0e67033202e Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sun, 1 Mar 2026 18:30:47 -0500 Subject: [PATCH 06/10] Fix up SQL examples in the comments --- internal/service/ch/queries.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/service/ch/queries.go b/internal/service/ch/queries.go index 67af2873..d33992aa 100644 --- a/internal/service/ch/queries.go +++ b/internal/service/ch/queries.go @@ -303,11 +303,11 @@ SELECT name, max(timestamp), argMax(value_string, timestamp) as value_string, - argMax(value_number, timestamp) as value_float + argMax(value_number, timestamp) as value_number FROM signal WHERE - token_id = 15 AND + subject = '...' AND (name = 'speed' OR name = 'currentLocationLatitude' OR name = 'currentLocationLongitude' OR name = 'powertrainFuelSystemSupportedFuelTypes' OR name = 'none') GROUP BY name @@ -355,12 +355,13 @@ func getLatestQuery(subject string, latestArgs *model.LatestSignalsArgs) (string SELECT 'lastSeen' AS name, max(timestamp) AS ts, + NULL AS value_number, NULL AS value_string, - NULL AS value_float + CAST(tuple(0, 0, 0, 0), 'Tuple(latitude Float64, longitude Float64, hdop Float64, heading Float64)') AS value_location FROM signal WHERE - token_id = 15 + subject = '...' */ func getLastSeenQuery(subject string, sigArgs *model.SignalArgs) (string, []any) { if sigArgs == nil { @@ -397,7 +398,7 @@ func unionAll(allStatements []string, allArgs [][]any) (string, []any) { /* SELECT signal_type, - signal_id, + signal_index, toStartOfInterval(timestamp, toIntervalMicrosecond(60000000), fromUnixTimestamp64Micro(1751274600000000)) AS group_timestamp, CASE WHEN signal_type = 1 AND signal_index = 0 THEN max(value_number) @@ -413,7 +414,7 @@ FROM signal JOIN VALUES( - 'signal_type UInt8, signal_index UInt8, name String', + 'signal_type UInt8, signal_index UInt16, name String', (1, 0, 'speed'), (1, 1, 'obdRunTime'), (2, 0, 'powertrainType'), @@ -422,7 +423,7 @@ JOIN ON signal.name = agg_table.name WHERE - token_id = 15 + subject = '...' AND timestamp > toDateTime('2024-04-15 09:21:19') AND timestamp < toDateTime('2024-04-27 09:21:19') GROUP BY From 1e90ebb2b4b0c6db50f9229f4e70d319a1b8edcd Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Mon, 2 Mar 2026 16:59:11 -0500 Subject: [PATCH 07/10] Support both token_id and subject in queries --- internal/auth/auth.go | 35 ++- internal/auth/auth_test.go | 78 ++++++- internal/graph/arguments.go | 26 ++- internal/graph/base.resolvers.go | 32 ++- internal/graph/events.resolvers.go | 8 +- internal/graph/generated.go | 251 +++++++++++++-------- internal/graph/model/signalArgs.go | 4 +- internal/graph/segments.resolvers.go | 16 +- internal/graph/vc.resolvers.go | 8 +- internal/repositories/repositories.go | 29 +-- internal/repositories/repositories_test.go | 12 +- internal/repositories/segments.go | 34 +-- internal/repositories/validate.go | 9 +- internal/repositories/validate_test.go | 36 ++- internal/repositories/vc/vc.go | 10 +- internal/repositories/vc/vc_test.go | 8 +- internal/service/ch/ch_test.go | 60 ++--- schema/base.graphqls | 13 +- schema/events.graphqls | 6 +- schema/segments.graphqls | 6 +- schema/vc.graphqls | 6 +- 21 files changed, 425 insertions(+), 262 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 09a55e9c..8dc35cd1 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -14,6 +14,7 @@ import ( const ( tokenIdArg = "tokenId" + subjectArg = "subject" byArg = "by" ) @@ -54,19 +55,39 @@ func newError(msg string, args ...any) error { func NewVehicleTokenCheck(requiredAddr common.Address) func(context.Context, any, graphql.Resolver) (any, error) { return func(ctx context.Context, _ any, next graphql.Resolver) (any, error) { - vehicleTokenID, err := getArg[int](ctx, tokenIdArg) - if err != nil { - return nil, UnauthorizedError{err: err} - } - - if err := validateHeader(ctx, requiredAddr, vehicleTokenID); err != nil { - return nil, UnauthorizedError{err: err} + tokenID, _ := getArg[*int](ctx, tokenIdArg) + subject, _ := getArg[*string](ctx, subjectArg) + + switch { + case tokenID != nil && subject != nil: + return nil, UnauthorizedError{message: "provide either tokenId or subject, not both"} + case tokenID != nil: + if err := validateHeader(ctx, requiredAddr, *tokenID); err != nil { + return nil, UnauthorizedError{err: err} + } + case subject != nil: + if err := validateSubject(ctx, *subject); err != nil { + return nil, UnauthorizedError{err: err} + } + default: + return nil, UnauthorizedError{message: "tokenId or subject is required"} } return next(ctx) } } +func validateSubject(ctx context.Context, subject string) error { + claim, err := getTelemetryClaim(ctx) + if err != nil { + return err + } + if subject != claim.Asset { + return newError("subject does not match token claim") + } + return nil +} + func NewManufacturerTokenCheck(requiredAddr common.Address, identitySvc IdentityService) func(context.Context, any, graphql.Resolver) (any, error) { return func(ctx context.Context, _ any, next graphql.Resolver) (any, error) { adFilter, err := getArg[model.AftermarketDeviceBy](ctx, byArg) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index e6f8745c..97dd3c3f 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -28,6 +28,11 @@ func TestRequiresVehicleTokenCheck(t *testing.T) { vehicleNFTAddr := common.HexToAddress("0x1") + tokenID123 := 123 + tokenID456 := 456 + validSubject := "did:erc721:1:0x0000000000000000000000000000000000000001:123" + wrongSubject := "did:erc721:1:0x0000000000000000000000000000000000000001:456" + testCases := []struct { name string args map[string]any @@ -37,7 +42,7 @@ func TestRequiresVehicleTokenCheck(t *testing.T) { { name: "valid_token", args: map[string]any{ - "tokenId": 123, + "tokenId": &tokenID123, }, telemetryClaim: &TelemetryClaim{ AssetDID: cloudevent.ERC721DID{ @@ -50,7 +55,7 @@ func TestRequiresVehicleTokenCheck(t *testing.T) { { name: "invalid_token", args: map[string]any{ - "tokenId": 456, + "tokenId": &tokenID456, }, telemetryClaim: &TelemetryClaim{ AssetDID: cloudevent.ERC721DID{ @@ -62,7 +67,7 @@ func TestRequiresVehicleTokenCheck(t *testing.T) { expectedError: true, }, { - name: "missing_tokenId", + name: "missing_both", args: map[string]any{}, expectedError: true, telemetryClaim: &TelemetryClaim{ @@ -74,8 +79,10 @@ func TestRequiresVehicleTokenCheck(t *testing.T) { }, }, { - name: "wrong_contract", - args: map[string]any{}, + name: "wrong_contract", + args: map[string]any{ + "tokenId": &tokenID123, + }, expectedError: true, telemetryClaim: &TelemetryClaim{ AssetDID: cloudevent.ERC721DID{ @@ -88,7 +95,66 @@ func TestRequiresVehicleTokenCheck(t *testing.T) { { name: "missing claim", args: map[string]any{ - "tokenId": 123, + "tokenId": &tokenID123, + }, + expectedError: true, + telemetryClaim: nil, + }, + { + name: "valid_subject", + args: map[string]any{ + "subject": &validSubject, + }, + telemetryClaim: &TelemetryClaim{ + CustomClaims: tokenclaims.CustomClaims{ + Asset: validSubject, + }, + AssetDID: cloudevent.ERC721DID{ + ChainID: 1, + ContractAddress: vehicleNFTAddr, + TokenID: big.NewInt(123), + }, + }, + }, + { + name: "wrong_subject", + args: map[string]any{ + "subject": &wrongSubject, + }, + telemetryClaim: &TelemetryClaim{ + CustomClaims: tokenclaims.CustomClaims{ + Asset: validSubject, + }, + AssetDID: cloudevent.ERC721DID{ + ChainID: 1, + ContractAddress: vehicleNFTAddr, + TokenID: big.NewInt(123), + }, + }, + expectedError: true, + }, + { + name: "both_tokenId_and_subject", + args: map[string]any{ + "tokenId": &tokenID123, + "subject": &validSubject, + }, + telemetryClaim: &TelemetryClaim{ + CustomClaims: tokenclaims.CustomClaims{ + Asset: validSubject, + }, + AssetDID: cloudevent.ERC721DID{ + ChainID: 1, + ContractAddress: vehicleNFTAddr, + TokenID: big.NewInt(123), + }, + }, + expectedError: true, + }, + { + name: "subject_missing_claim", + args: map[string]any{ + "subject": &validSubject, }, expectedError: true, telemetryClaim: nil, diff --git a/internal/graph/arguments.go b/internal/graph/arguments.go index d2569de8..656352b0 100644 --- a/internal/graph/arguments.go +++ b/internal/graph/arguments.go @@ -2,24 +2,40 @@ package graph import ( "context" + "errors" "fmt" "time" "github.com/99designs/gqlgen/graphql" "github.com/DIMO-Network/model-garage/pkg/vss" + "github.com/DIMO-Network/server-garage/pkg/gql/errorhandler" "github.com/DIMO-Network/telemetry-api/internal/graph/model" ) +// resolveSubject resolves the subject from the provided tokenID or subject arguments. +// Exactly one of tokenID or subject must be provided. +func (r *queryResolver) resolveSubject(ctx context.Context, tokenID *int, subject *string) (string, error) { + if subject != nil && tokenID != nil { + return "", errorhandler.NewBadRequestError(ctx, errors.New("provide either tokenId or subject, not both")) + } + if subject != nil { + return *subject, nil + } + if tokenID != nil { + return r.BaseRepo.ToSubject(uint32(*tokenID)), nil + } + return "", errorhandler.NewBadRequestError(ctx, errors.New("tokenId or subject is required")) +} + // aggregationArgsFromContext creates an aggregated signals arguments from the context and the provided arguments. -func aggregationArgsFromContext(ctx context.Context, tokenID int, interval string, from time.Time, to time.Time, filter *model.SignalFilter) (*model.AggregatedSignalArgs, error) { - // 1h 1s +func aggregationArgsFromContext(ctx context.Context, subject string, interval string, from time.Time, to time.Time, filter *model.SignalFilter) (*model.AggregatedSignalArgs, error) { intervalInt, err := getIntervalMicroseconds(interval) if err != nil { return nil, err } aggArgs := model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: uint32(tokenID), + Subject: subject, Filter: filter, }, FromTS: from, @@ -86,11 +102,11 @@ func addSignalAggregation(aggArgs *model.AggregatedSignalArgs, child *graphql.Fi } // latestArgsFromContext creates a latest signals arguments from the context and the provided arguments. -func latestArgsFromContext(ctx context.Context, tokenID int, filter *model.SignalFilter) (*model.LatestSignalsArgs, error) { +func latestArgsFromContext(ctx context.Context, subject string, filter *model.SignalFilter) (*model.LatestSignalsArgs, error) { fields := graphql.CollectFieldsCtx(ctx, nil) latestArgs := model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ - TokenID: uint32(tokenID), + Subject: subject, Filter: filter, }, SignalNames: make(map[string]struct{}), diff --git a/internal/graph/base.resolvers.go b/internal/graph/base.resolvers.go index 2bf62cf4..8707385e 100644 --- a/internal/graph/base.resolvers.go +++ b/internal/graph/base.resolvers.go @@ -14,8 +14,12 @@ import ( ) // Signals is the resolver for the Signals field. -func (r *queryResolver) Signals(ctx context.Context, tokenID int, interval string, from time.Time, to time.Time, filter *model.SignalFilter) ([]*model.SignalAggregations, error) { - aggArgs, err := aggregationArgsFromContext(ctx, tokenID, interval, from, to, filter) +func (r *queryResolver) Signals(ctx context.Context, tokenID *int, subject *string, interval string, from time.Time, to time.Time, filter *model.SignalFilter) ([]*model.SignalAggregations, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + aggArgs, err := aggregationArgsFromContext(ctx, sub, interval, from, to, filter) if err != nil { return nil, err } @@ -23,8 +27,12 @@ func (r *queryResolver) Signals(ctx context.Context, tokenID int, interval strin } // SignalsLatest is the resolver for the SignalsLatest field. -func (r *queryResolver) SignalsLatest(ctx context.Context, tokenID int, filter *model.SignalFilter) (*model.SignalCollection, error) { - latestArgs, err := latestArgsFromContext(ctx, tokenID, filter) +func (r *queryResolver) SignalsLatest(ctx context.Context, tokenID *int, subject *string, filter *model.SignalFilter) (*model.SignalCollection, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + latestArgs, err := latestArgsFromContext(ctx, sub, filter) if err != nil { return nil, err } @@ -32,13 +40,21 @@ func (r *queryResolver) SignalsLatest(ctx context.Context, tokenID int, filter * } // AvailableSignals is the resolver for the AvailableSignals field. -func (r *queryResolver) AvailableSignals(ctx context.Context, tokenID int, filter *model.SignalFilter) ([]string, error) { - return r.BaseRepo.GetAvailableSignals(ctx, uint32(tokenID), filter) +func (r *queryResolver) AvailableSignals(ctx context.Context, tokenID *int, subject *string, filter *model.SignalFilter) ([]string, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + return r.BaseRepo.GetAvailableSignals(ctx, sub, filter) } // DataSummary is the resolver for the dataSummary field. -func (r *queryResolver) DataSummary(ctx context.Context, tokenID int, filter *model.SignalFilter) (*model.DataSummary, error) { - return r.BaseRepo.GetDataSummary(ctx, uint32(tokenID), filter) +func (r *queryResolver) DataSummary(ctx context.Context, tokenID *int, subject *string, filter *model.SignalFilter) (*model.DataSummary, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + return r.BaseRepo.GetDataSummary(ctx, sub, filter) } // CurrentLocationApproximateCoordinates is the resolver for the currentLocationApproximateCoordinates field on SignalAggregations. diff --git a/internal/graph/events.resolvers.go b/internal/graph/events.resolvers.go index db194a45..8fb3fb81 100644 --- a/internal/graph/events.resolvers.go +++ b/internal/graph/events.resolvers.go @@ -12,6 +12,10 @@ import ( ) // Events is the resolver for the events field. -func (r *queryResolver) Events(ctx context.Context, tokenID int, from time.Time, to time.Time, filter *model.EventFilter) ([]*model.Event, error) { - return r.BaseRepo.GetEvents(ctx, tokenID, from, to, filter) +func (r *queryResolver) Events(ctx context.Context, tokenID *int, subject *string, from time.Time, to time.Time, filter *model.EventFilter) ([]*model.Event, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + return r.BaseRepo.GetEvents(ctx, sub, from, to, filter) } diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 45d90935..6c52011a 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -111,15 +111,15 @@ type ComplexityRoot struct { Query struct { Attestations func(childComplexity int, tokenID *int, subject *string, filter *model.AttestationFilter) int - AvailableSignals func(childComplexity int, tokenID int, filter *model.SignalFilter) int - DailyActivity func(childComplexity int, tokenID int, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) int - DataSummary func(childComplexity int, tokenID int, filter *model.SignalFilter) int + AvailableSignals func(childComplexity int, tokenID *int, subject *string, filter *model.SignalFilter) int + DailyActivity func(childComplexity int, tokenID *int, subject *string, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) int + DataSummary func(childComplexity int, tokenID *int, subject *string, filter *model.SignalFilter) int DeviceActivity func(childComplexity int, by model.AftermarketDeviceBy) int - Events func(childComplexity int, tokenID int, from time.Time, to time.Time, filter *model.EventFilter) int - Segments func(childComplexity int, tokenID int, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) int - Signals func(childComplexity int, tokenID int, interval string, from time.Time, to time.Time, filter *model.SignalFilter) int - SignalsLatest func(childComplexity int, tokenID int, filter *model.SignalFilter) int - VinVCLatest func(childComplexity int, tokenID int) int + Events func(childComplexity int, tokenID *int, subject *string, from time.Time, to time.Time, filter *model.EventFilter) int + Segments func(childComplexity int, tokenID *int, subject *string, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) int + Signals func(childComplexity int, tokenID *int, subject *string, interval string, from time.Time, to time.Time, filter *model.SignalFilter) int + SignalsLatest func(childComplexity int, tokenID *int, subject *string, filter *model.SignalFilter) int + VinVCLatest func(childComplexity int, tokenID *int, subject *string) int } Segment struct { @@ -409,16 +409,16 @@ type ComplexityRoot struct { } type QueryResolver interface { - Signals(ctx context.Context, tokenID int, interval string, from time.Time, to time.Time, filter *model.SignalFilter) ([]*model.SignalAggregations, error) - SignalsLatest(ctx context.Context, tokenID int, filter *model.SignalFilter) (*model.SignalCollection, error) - AvailableSignals(ctx context.Context, tokenID int, filter *model.SignalFilter) ([]string, error) - DataSummary(ctx context.Context, tokenID int, filter *model.SignalFilter) (*model.DataSummary, error) + Signals(ctx context.Context, tokenID *int, subject *string, interval string, from time.Time, to time.Time, filter *model.SignalFilter) ([]*model.SignalAggregations, error) + SignalsLatest(ctx context.Context, tokenID *int, subject *string, filter *model.SignalFilter) (*model.SignalCollection, error) + AvailableSignals(ctx context.Context, tokenID *int, subject *string, filter *model.SignalFilter) ([]string, error) + DataSummary(ctx context.Context, tokenID *int, subject *string, filter *model.SignalFilter) (*model.DataSummary, error) Attestations(ctx context.Context, tokenID *int, subject *string, filter *model.AttestationFilter) ([]*model.Attestation, error) DeviceActivity(ctx context.Context, by model.AftermarketDeviceBy) (*model.DeviceActivity, error) - Events(ctx context.Context, tokenID int, from time.Time, to time.Time, filter *model.EventFilter) ([]*model.Event, error) - Segments(ctx context.Context, tokenID int, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) ([]*model.Segment, error) - DailyActivity(ctx context.Context, tokenID int, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) ([]*model.DailyActivity, error) - VinVCLatest(ctx context.Context, tokenID int) (*model.Vinvc, error) + Events(ctx context.Context, tokenID *int, subject *string, from time.Time, to time.Time, filter *model.EventFilter) ([]*model.Event, error) + Segments(ctx context.Context, tokenID *int, subject *string, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) ([]*model.Segment, error) + DailyActivity(ctx context.Context, tokenID *int, subject *string, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) ([]*model.DailyActivity, error) + VinVCLatest(ctx context.Context, tokenID *int, subject *string) (*model.Vinvc, error) } type SignalAggregationsResolver interface { CurrentLocationApproximateCoordinates(ctx context.Context, obj *model.SignalAggregations, agg model.LocationAggregation) (*model.Location, error) @@ -778,7 +778,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.AvailableSignals(childComplexity, args["tokenId"].(int), args["filter"].(*model.SignalFilter)), true + return e.complexity.Query.AvailableSignals(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["filter"].(*model.SignalFilter)), true case "Query.dailyActivity": if e.complexity.Query.DailyActivity == nil { break @@ -789,7 +789,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.DailyActivity(childComplexity, args["tokenId"].(int), args["from"].(time.Time), args["to"].(time.Time), args["mechanism"].(model.DetectionMechanism), args["config"].(*model.SegmentConfig), args["signalRequests"].([]*model.SegmentSignalRequest), args["eventRequests"].([]*model.SegmentEventRequest), args["timezone"].(*string)), true + return e.complexity.Query.DailyActivity(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["from"].(time.Time), args["to"].(time.Time), args["mechanism"].(model.DetectionMechanism), args["config"].(*model.SegmentConfig), args["signalRequests"].([]*model.SegmentSignalRequest), args["eventRequests"].([]*model.SegmentEventRequest), args["timezone"].(*string)), true case "Query.dataSummary": if e.complexity.Query.DataSummary == nil { break @@ -800,7 +800,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.DataSummary(childComplexity, args["tokenId"].(int), args["filter"].(*model.SignalFilter)), true + return e.complexity.Query.DataSummary(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["filter"].(*model.SignalFilter)), true case "Query.deviceActivity": if e.complexity.Query.DeviceActivity == nil { break @@ -822,7 +822,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.Events(childComplexity, args["tokenId"].(int), args["from"].(time.Time), args["to"].(time.Time), args["filter"].(*model.EventFilter)), true + return e.complexity.Query.Events(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["from"].(time.Time), args["to"].(time.Time), args["filter"].(*model.EventFilter)), true case "Query.segments": if e.complexity.Query.Segments == nil { break @@ -833,7 +833,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.Segments(childComplexity, args["tokenId"].(int), args["from"].(time.Time), args["to"].(time.Time), args["mechanism"].(model.DetectionMechanism), args["config"].(*model.SegmentConfig), args["signalRequests"].([]*model.SegmentSignalRequest), args["eventRequests"].([]*model.SegmentEventRequest), args["limit"].(*int), args["after"].(*time.Time)), true + return e.complexity.Query.Segments(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["from"].(time.Time), args["to"].(time.Time), args["mechanism"].(model.DetectionMechanism), args["config"].(*model.SegmentConfig), args["signalRequests"].([]*model.SegmentSignalRequest), args["eventRequests"].([]*model.SegmentEventRequest), args["limit"].(*int), args["after"].(*time.Time)), true case "Query.signals": if e.complexity.Query.Signals == nil { break @@ -844,7 +844,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.Signals(childComplexity, args["tokenId"].(int), args["interval"].(string), args["from"].(time.Time), args["to"].(time.Time), args["filter"].(*model.SignalFilter)), true + return e.complexity.Query.Signals(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["interval"].(string), args["from"].(time.Time), args["to"].(time.Time), args["filter"].(*model.SignalFilter)), true case "Query.signalsLatest": if e.complexity.Query.SignalsLatest == nil { break @@ -855,7 +855,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.SignalsLatest(childComplexity, args["tokenId"].(int), args["filter"].(*model.SignalFilter)), true + return e.complexity.Query.SignalsLatest(childComplexity, args["tokenId"].(*int), args["subject"].(*string), args["filter"].(*model.SignalFilter)), true case "Query.vinVCLatest": if e.complexity.Query.VinVCLatest == nil { break @@ -866,7 +866,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.VinVCLatest(childComplexity, args["tokenId"].(int)), true + return e.complexity.Query.VinVCLatest(childComplexity, args["tokenId"].(*int), args["subject"].(*string)), true case "Segment.duration": if e.complexity.Segment.Duration == nil { @@ -3240,7 +3240,8 @@ type Query { signals returns a collection of signals for a given token in a given time range. """ signals( - tokenId: Int! + tokenId: Int + subject: String """ interval is a time span that used for aggregatting the data with. A duration string is a sequence of decimal numbers, each with optional fraction and a unit suffix, @@ -3254,18 +3255,18 @@ type Query { """ SignalsLatest returns the latest signals for a given token. """ - signalsLatest(tokenId: Int!, filter: SignalFilter): SignalCollection + signalsLatest(tokenId: Int, subject: String, filter: SignalFilter): SignalCollection @requiresVehicleToken """ - availableSignals returns a list of queryable signal names that have stored data for a given tokenId. + availableSignals returns a list of queryable signal names that have stored data for the given vehicle. """ - availableSignals(tokenId: Int!, filter: SignalFilter): [String!] + availableSignals(tokenId: Int, subject: String, filter: SignalFilter): [String!] @requiresVehicleToken """ - data summary of all signals for a given tokenId + data summary of all signals for the given vehicle """ - dataSummary(tokenId: Int!, filter: SignalFilter): DataSummary + dataSummary(tokenId: Int, subject: String, filter: SignalFilter): DataSummary @requiresVehicleToken } type SignalAggregations { @@ -3627,7 +3628,11 @@ input AftermarketDeviceBy @oneOf { """ tokenId is the id of the token to get events for. """ - tokenId: Int! + tokenId: Int + """ + subject is the DID subject of the vehicle. + """ + subject: String """ from is the start time of the event. """ @@ -3740,7 +3745,8 @@ extend type Query { When signalRequests is provided, those requests are added on top of the default set; duplicates (same name and agg) are omitted. """ segments( - tokenId: Int! + tokenId: Int + subject: String from: Time! to: Time! mechanism: DetectionMechanism! @@ -3764,7 +3770,8 @@ extend type Query { Maximum date range: 31 days. """ dailyActivity( - tokenId: Int! + tokenId: Int + subject: String from: Time! to: Time! mechanism: DetectionMechanism! @@ -5662,7 +5669,11 @@ extend input EventFilter { """ The token ID of the vehicle. """ - tokenId: Int! + tokenId: Int + """ + The DID subject of the vehicle. + """ + subject: String ): VINVC @requiresVehicleToken @requiresAllOfPrivileges(privileges: [VEHICLE_VIN_CREDENTIAL]) @@ -5781,78 +5792,93 @@ func (ec *executionContext) field_Query_attestations_args(ctx context.Context, r func (ec *executionContext) field_Query_availableSignals_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) if err != nil { return nil, err } - args["filter"] = arg1 + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + if err != nil { + return nil, err + } + args["filter"] = arg2 return args, nil } func (ec *executionContext) field_Query_dailyActivity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) if err != nil { return nil, err } - args["from"] = arg1 - arg2, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["to"] = arg2 - arg3, err := graphql.ProcessArgField(ctx, rawArgs, "mechanism", ec.unmarshalNDetectionMechanism2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐDetectionMechanism) + args["from"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["mechanism"] = arg3 - arg4, err := graphql.ProcessArgField(ctx, rawArgs, "config", ec.unmarshalOSegmentConfig2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentConfig) + args["to"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "mechanism", ec.unmarshalNDetectionMechanism2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐDetectionMechanism) if err != nil { return nil, err } - args["config"] = arg4 - arg5, err := graphql.ProcessArgField(ctx, rawArgs, "signalRequests", ec.unmarshalOSegmentSignalRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentSignalRequestᚄ) + args["mechanism"] = arg4 + arg5, err := graphql.ProcessArgField(ctx, rawArgs, "config", ec.unmarshalOSegmentConfig2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentConfig) if err != nil { return nil, err } - args["signalRequests"] = arg5 - arg6, err := graphql.ProcessArgField(ctx, rawArgs, "eventRequests", ec.unmarshalOSegmentEventRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentEventRequestᚄ) + args["config"] = arg5 + arg6, err := graphql.ProcessArgField(ctx, rawArgs, "signalRequests", ec.unmarshalOSegmentSignalRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentSignalRequestᚄ) if err != nil { return nil, err } - args["eventRequests"] = arg6 - arg7, err := graphql.ProcessArgField(ctx, rawArgs, "timezone", ec.unmarshalOString2ᚖstring) + args["signalRequests"] = arg6 + arg7, err := graphql.ProcessArgField(ctx, rawArgs, "eventRequests", ec.unmarshalOSegmentEventRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentEventRequestᚄ) if err != nil { return nil, err } - args["timezone"] = arg7 + args["eventRequests"] = arg7 + arg8, err := graphql.ProcessArgField(ctx, rawArgs, "timezone", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["timezone"] = arg8 return args, nil } func (ec *executionContext) field_Query_dataSummary_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) if err != nil { return nil, err } - args["filter"] = arg1 + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + if err != nil { + return nil, err + } + args["filter"] = arg2 return args, nil } @@ -5870,135 +5896,160 @@ func (ec *executionContext) field_Query_deviceActivity_args(ctx context.Context, func (ec *executionContext) field_Query_events_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["from"] = arg1 - arg2, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) + args["from"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["to"] = arg2 - arg3, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOEventFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐEventFilter) + args["to"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOEventFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐEventFilter) if err != nil { return nil, err } - args["filter"] = arg3 + args["filter"] = arg4 return args, nil } func (ec *executionContext) field_Query_segments_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["from"] = arg1 - arg2, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) + args["from"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["to"] = arg2 - arg3, err := graphql.ProcessArgField(ctx, rawArgs, "mechanism", ec.unmarshalNDetectionMechanism2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐDetectionMechanism) + args["to"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "mechanism", ec.unmarshalNDetectionMechanism2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐDetectionMechanism) if err != nil { return nil, err } - args["mechanism"] = arg3 - arg4, err := graphql.ProcessArgField(ctx, rawArgs, "config", ec.unmarshalOSegmentConfig2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentConfig) + args["mechanism"] = arg4 + arg5, err := graphql.ProcessArgField(ctx, rawArgs, "config", ec.unmarshalOSegmentConfig2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentConfig) if err != nil { return nil, err } - args["config"] = arg4 - arg5, err := graphql.ProcessArgField(ctx, rawArgs, "signalRequests", ec.unmarshalOSegmentSignalRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentSignalRequestᚄ) + args["config"] = arg5 + arg6, err := graphql.ProcessArgField(ctx, rawArgs, "signalRequests", ec.unmarshalOSegmentSignalRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentSignalRequestᚄ) if err != nil { return nil, err } - args["signalRequests"] = arg5 - arg6, err := graphql.ProcessArgField(ctx, rawArgs, "eventRequests", ec.unmarshalOSegmentEventRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentEventRequestᚄ) + args["signalRequests"] = arg6 + arg7, err := graphql.ProcessArgField(ctx, rawArgs, "eventRequests", ec.unmarshalOSegmentEventRequest2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSegmentEventRequestᚄ) if err != nil { return nil, err } - args["eventRequests"] = arg6 - arg7, err := graphql.ProcessArgField(ctx, rawArgs, "limit", ec.unmarshalOInt2ᚖint) + args["eventRequests"] = arg7 + arg8, err := graphql.ProcessArgField(ctx, rawArgs, "limit", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } - args["limit"] = arg7 - arg8, err := graphql.ProcessArgField(ctx, rawArgs, "after", ec.unmarshalOTime2ᚖtimeᚐTime) + args["limit"] = arg8 + arg9, err := graphql.ProcessArgField(ctx, rawArgs, "after", ec.unmarshalOTime2ᚖtimeᚐTime) if err != nil { return nil, err } - args["after"] = arg8 + args["after"] = arg9 return args, nil } func (ec *executionContext) field_Query_signalsLatest_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) if err != nil { return nil, err } - args["filter"] = arg1 + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + if err != nil { + return nil, err + } + args["filter"] = arg2 return args, nil } func (ec *executionContext) field_Query_signals_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "interval", ec.unmarshalNString2string) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) if err != nil { return nil, err } - args["interval"] = arg1 - arg2, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) + args["subject"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "interval", ec.unmarshalNString2string) if err != nil { return nil, err } - args["from"] = arg2 - arg3, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) + args["interval"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["to"] = arg3 - arg4, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + args["from"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["filter"] = arg4 + args["to"] = arg4 + arg5, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOSignalFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFilter) + if err != nil { + return nil, err + } + args["filter"] = arg5 return args, nil } func (ec *executionContext) field_Query_vinVCLatest_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalNInt2int) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "tokenId", ec.unmarshalOInt2ᚖint) if err != nil { return nil, err } args["tokenId"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "subject", ec.unmarshalOString2ᚖstring) + if err != nil { + return nil, err + } + args["subject"] = arg1 return args, nil } @@ -8790,7 +8841,7 @@ func (ec *executionContext) _Query_signals(ctx context.Context, field graphql.Co ec.fieldContext_Query_signals, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().Signals(ctx, fc.Args["tokenId"].(int), fc.Args["interval"].(string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["filter"].(*model.SignalFilter)) + return ec.resolvers.Query().Signals(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["interval"].(string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["filter"].(*model.SignalFilter)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9068,7 +9119,7 @@ func (ec *executionContext) _Query_signalsLatest(ctx context.Context, field grap ec.fieldContext_Query_signalsLatest, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().SignalsLatest(ctx, fc.Args["tokenId"].(int), fc.Args["filter"].(*model.SignalFilter)) + return ec.resolvers.Query().SignalsLatest(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["filter"].(*model.SignalFilter)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9346,7 +9397,7 @@ func (ec *executionContext) _Query_availableSignals(ctx context.Context, field g ec.fieldContext_Query_availableSignals, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().AvailableSignals(ctx, fc.Args["tokenId"].(int), fc.Args["filter"].(*model.SignalFilter)) + return ec.resolvers.Query().AvailableSignals(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["filter"].(*model.SignalFilter)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9400,7 +9451,7 @@ func (ec *executionContext) _Query_dataSummary(ctx context.Context, field graphq ec.fieldContext_Query_dataSummary, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().DataSummary(ctx, fc.Args["tokenId"].(int), fc.Args["filter"].(*model.SignalFilter)) + return ec.resolvers.Query().DataSummary(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["filter"].(*model.SignalFilter)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9601,7 +9652,7 @@ func (ec *executionContext) _Query_events(ctx context.Context, field graphql.Col ec.fieldContext_Query_events, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().Events(ctx, fc.Args["tokenId"].(int), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["filter"].(*model.EventFilter)) + return ec.resolvers.Query().Events(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["filter"].(*model.EventFilter)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9679,7 +9730,7 @@ func (ec *executionContext) _Query_segments(ctx context.Context, field graphql.C ec.fieldContext_Query_segments, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().Segments(ctx, fc.Args["tokenId"].(int), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["mechanism"].(model.DetectionMechanism), fc.Args["config"].(*model.SegmentConfig), fc.Args["signalRequests"].([]*model.SegmentSignalRequest), fc.Args["eventRequests"].([]*model.SegmentEventRequest), fc.Args["limit"].(*int), fc.Args["after"].(*time.Time)) + return ec.resolvers.Query().Segments(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["mechanism"].(model.DetectionMechanism), fc.Args["config"].(*model.SegmentConfig), fc.Args["signalRequests"].([]*model.SegmentSignalRequest), fc.Args["eventRequests"].([]*model.SegmentEventRequest), fc.Args["limit"].(*int), fc.Args["after"].(*time.Time)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9761,7 +9812,7 @@ func (ec *executionContext) _Query_dailyActivity(ctx context.Context, field grap ec.fieldContext_Query_dailyActivity, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().DailyActivity(ctx, fc.Args["tokenId"].(int), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["mechanism"].(model.DetectionMechanism), fc.Args["config"].(*model.SegmentConfig), fc.Args["signalRequests"].([]*model.SegmentSignalRequest), fc.Args["eventRequests"].([]*model.SegmentEventRequest), fc.Args["timezone"].(*string)) + return ec.resolvers.Query().DailyActivity(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["mechanism"].(model.DetectionMechanism), fc.Args["config"].(*model.SegmentConfig), fc.Args["signalRequests"].([]*model.SegmentSignalRequest), fc.Args["eventRequests"].([]*model.SegmentEventRequest), fc.Args["timezone"].(*string)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -9841,7 +9892,7 @@ func (ec *executionContext) _Query_vinVCLatest(ctx context.Context, field graphq ec.fieldContext_Query_vinVCLatest, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Query().VinVCLatest(ctx, fc.Args["tokenId"].(int)) + return ec.resolvers.Query().VinVCLatest(ctx, fc.Args["tokenId"].(*int), fc.Args["subject"].(*string)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next diff --git a/internal/graph/model/signalArgs.go b/internal/graph/model/signalArgs.go index d3f383be..6675877a 100644 --- a/internal/graph/model/signalArgs.go +++ b/internal/graph/model/signalArgs.go @@ -17,8 +17,8 @@ const ( type SignalArgs struct { // Filter is an optional filter for the signals. Filter *SignalFilter - // TokenID is the vehicle's NFT token ID. - TokenID uint32 + // Subject is the DID subject string for the vehicle. + Subject string } // LatestSignalsArgs is the arguments for querying the latest signals. diff --git a/internal/graph/segments.resolvers.go b/internal/graph/segments.resolvers.go index 42b327f8..b39b163a 100644 --- a/internal/graph/segments.resolvers.go +++ b/internal/graph/segments.resolvers.go @@ -12,11 +12,19 @@ import ( ) // Segments is the resolver for the segments field. -func (r *queryResolver) Segments(ctx context.Context, tokenID int, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) ([]*model.Segment, error) { - return r.BaseRepo.GetSegments(ctx, tokenID, from, to, mechanism, config, signalRequests, eventRequests, limit, after) +func (r *queryResolver) Segments(ctx context.Context, tokenID *int, subject *string, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) ([]*model.Segment, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + return r.BaseRepo.GetSegments(ctx, sub, from, to, mechanism, config, signalRequests, eventRequests, limit, after) } // DailyActivity is the resolver for the dailyActivity field. -func (r *queryResolver) DailyActivity(ctx context.Context, tokenID int, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) ([]*model.DailyActivity, error) { - return r.BaseRepo.GetDailyActivity(ctx, tokenID, from, to, mechanism, config, signalRequests, eventRequests, timezone) +func (r *queryResolver) DailyActivity(ctx context.Context, tokenID *int, subject *string, from time.Time, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) ([]*model.DailyActivity, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + return r.BaseRepo.GetDailyActivity(ctx, sub, from, to, mechanism, config, signalRequests, eventRequests, timezone) } diff --git a/internal/graph/vc.resolvers.go b/internal/graph/vc.resolvers.go index d0937dd8..3dfd763c 100644 --- a/internal/graph/vc.resolvers.go +++ b/internal/graph/vc.resolvers.go @@ -11,6 +11,10 @@ import ( ) // VinVCLatest is the resolver for the vinVCLatest field. -func (r *queryResolver) VinVCLatest(ctx context.Context, tokenID int) (*model.Vinvc, error) { - return r.VCRepo.GetLatestVINVC(ctx, uint32(tokenID)) +func (r *queryResolver) VinVCLatest(ctx context.Context, tokenID *int, subject *string) (*model.Vinvc, error) { + sub, err := r.resolveSubject(ctx, tokenID, subject) + if err != nil { + return nil, err + } + return r.VCRepo.GetLatestVINVC(ctx, sub) } diff --git a/internal/repositories/repositories.go b/internal/repositories/repositories.go index 1663270c..faf6bafe 100644 --- a/internal/repositories/repositories.go +++ b/internal/repositories/repositories.go @@ -76,8 +76,8 @@ func NewRepository(chService CHService, settings config.Settings) (*Repository, } -// toSubject converts a vehicle token id to a DID subject string. -func (r *Repository) toSubject(tokenID uint32) string { +// ToSubject converts a vehicle token id to a DID subject string. +func (r *Repository) ToSubject(tokenID uint32) string { return cloudevent.ERC721DID{ ChainID: r.chainID, ContractAddress: r.vehicleAddress, @@ -91,8 +91,7 @@ func (r *Repository) GetSignal(ctx context.Context, aggArgs *model.AggregatedSig return nil, errorhandler.NewBadRequestError(ctx, err) } - subject := r.toSubject(aggArgs.TokenID) - signals, err := r.chService.GetAggregatedSignals(ctx, subject, aggArgs) + signals, err := r.chService.GetAggregatedSignals(ctx, aggArgs.Subject, aggArgs) if err != nil { return nil, handleDBError(ctx, err) } @@ -144,8 +143,7 @@ func (r *Repository) GetSignalLatest(ctx context.Context, latestArgs *model.Late if err := validateLatestSigArgs(latestArgs); err != nil { return nil, errorhandler.NewBadRequestError(ctx, err) } - subject := r.toSubject(latestArgs.TokenID) - signals, err := r.chService.GetLatestSignals(ctx, subject, latestArgs) + signals, err := r.chService.GetLatestSignals(ctx, latestArgs.Subject, latestArgs) if err != nil { return nil, handleDBError(ctx, err) } @@ -172,7 +170,7 @@ func (r *Repository) GetDeviceActivity(ctx context.Context, vehicleTokenID int, args := &model.LatestSignalsArgs{ IncludeLastSeen: true, SignalArgs: model.SignalArgs{ - TokenID: uint32(vehicleTokenID), + Subject: r.ToSubject(uint32(vehicleTokenID)), Filter: &model.SignalFilter{ Source: &source, }, @@ -196,8 +194,7 @@ func (r *Repository) GetDeviceActivity(ctx context.Context, vehicleTokenID int, // GetAvailableSignals returns the available signals for the given tokenID and filter. // If no signals are found, a nil slice is returned. -func (r *Repository) GetAvailableSignals(ctx context.Context, tokenID uint32, filter *model.SignalFilter) ([]string, error) { - subject := r.toSubject(tokenID) +func (r *Repository) GetAvailableSignals(ctx context.Context, subject string, filter *model.SignalFilter) ([]string, error) { allSignals, err := r.chService.GetAvailableSignals(ctx, subject, filter) if err != nil { return nil, handleDBError(ctx, err) @@ -212,8 +209,7 @@ func (r *Repository) GetAvailableSignals(ctx context.Context, tokenID uint32, fi } // GetDataSummary returns the signal and event metadata for the given tokenID and filter. -func (r *Repository) GetDataSummary(ctx context.Context, tokenID uint32, filter *model.SignalFilter) (*model.DataSummary, error) { - subject := r.toSubject(tokenID) +func (r *Repository) GetDataSummary(ctx context.Context, subject string, filter *model.SignalFilter) (*model.DataSummary, error) { signalDataSummary, err := r.chService.GetSignalSummaries(ctx, subject, filter) if err != nil { return nil, handleDBError(ctx, err) @@ -263,16 +259,11 @@ func (r *Repository) GetDataSummary(ctx context.Context, tokenID uint32, filter }, nil } -// GetEvents returns the events for the given tokenID, from, to and filter. -func (r *Repository) GetEvents(ctx context.Context, tokenID int, from, to time.Time, filter *model.EventFilter) ([]*model.Event, error) { - if err := validateEventArgs(tokenID, from, to, filter); err != nil { +// GetEvents returns the events for the given subject, from, to and filter. +func (r *Repository) GetEvents(ctx context.Context, subject string, from, to time.Time, filter *model.EventFilter) ([]*model.Event, error) { + if err := validateEventArgs(from, to, filter); err != nil { return nil, errorhandler.NewBadRequestError(ctx, err) } - subject := cloudevent.ERC721DID{ - ChainID: r.chainID, - ContractAddress: r.vehicleAddress, - TokenID: big.NewInt(int64(tokenID)), - }.String() allEvents, err := r.chService.GetEvents(ctx, subject, from, to, filter) if err != nil { return nil, handleDBError(ctx, err) diff --git a/internal/repositories/repositories_test.go b/internal/repositories/repositories_test.go index 3d9dd813..8945bd83 100644 --- a/internal/repositories/repositories_test.go +++ b/internal/repositories/repositories_test.go @@ -44,7 +44,7 @@ func TestGetSignal(t *testing.T) { }.String() defaultArgs := &model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject, }, FromTS: time.Now(), ToTS: time.Now().Add(time.Hour), @@ -217,7 +217,7 @@ func TestGetSignalLatest(t *testing.T) { }.String() defaultArgs := &model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject, }, IncludeLastSeen: true, } @@ -347,7 +347,7 @@ func TestDeviceActivity(t *testing.T) { latestArgs := &model.LatestSignalsArgs{ IncludeLastSeen: true, SignalArgs: model.SignalArgs{ - TokenID: uint32(vehicleTokenID), + Subject: testSubject, Filter: &model.SignalFilter{ Source: &source, }, @@ -512,7 +512,7 @@ func TestGetAvailableSignals(t *testing.T) { repo, err := repositories.NewRepository(mocks.CHService, baseSettings) require.NoError(t, err) - result, err := repo.GetAvailableSignals(context.Background(), 1, nil) + result, err := repo.GetAvailableSignals(context.Background(), testSubject, nil) if tt.expectError { require.Error(t, err) require.Nil(t, result) @@ -564,7 +564,7 @@ func TestGetEvents(t *testing.T) { mocks.CHService.EXPECT(). GetEvents(gomock.Any(), subject, from, to, filter). Return(vssEvents, nil) - result, err := repo.GetEvents(context.Background(), tokenID, from, to, filter) + result, err := repo.GetEvents(context.Background(), subject, from, to, filter) require.NoError(t, err) require.Len(t, result, 2) require.Equal(t, vssEvents[0].Name, result[0].Name) @@ -583,7 +583,7 @@ func TestGetEvents(t *testing.T) { mocks.CHService.EXPECT(). GetEvents(gomock.Any(), subject, from, to, filter). Return(nil, errors.New("service error")) - result, err := repo.GetEvents(context.Background(), tokenID, from, to, filter) + result, err := repo.GetEvents(context.Background(), subject, from, to, filter) require.Error(t, err) require.Nil(t, result) }) diff --git a/internal/repositories/segments.go b/internal/repositories/segments.go index 3c8c88f7..60144306 100644 --- a/internal/repositories/segments.go +++ b/internal/repositories/segments.go @@ -3,11 +3,9 @@ package repositories import ( "context" "fmt" - "math/big" "sort" "time" - "github.com/DIMO-Network/cloudevent" "github.com/DIMO-Network/model-garage/pkg/vss" "github.com/DIMO-Network/server-garage/pkg/gql/errorhandler" "github.com/DIMO-Network/telemetry-api/internal/graph/model" @@ -32,11 +30,7 @@ func validateSegmentDateRange(from, to time.Time) error { } // validateSegmentArgs validates the arguments for segment queries. -func validateSegmentArgs(tokenID int, from, to time.Time) error { - if tokenID <= 0 { - return fmt.Errorf("invalid tokenID: %d", tokenID) - } - +func validateSegmentArgs(from, to time.Time) error { if from.After(to) { return fmt.Errorf("from time must be before to time") } @@ -199,11 +193,11 @@ func sortSegmentSignals(signals []*model.SignalAggregationValue) { // Pagination: pass after (exclusive cursor = startTime of last segment from previous page) and limit (default 100, max 200). // Segments are ordered by startTime ascending. When after is set, only segments with startTime > after are requested from CH. // If to is in the future (e.g. client sent end-of-day in user TZ), it is capped to now so the query succeeds. -func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) ([]*model.Segment, error) { +func (r *Repository) GetSegments(ctx context.Context, subject string, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, limit *int, after *time.Time) ([]*model.Segment, error) { if now := time.Now(); to.After(now) { to = now } - if err := validateSegmentArgs(tokenID, from, to); err != nil { + if err := validateSegmentArgs(from, to); err != nil { return nil, errorhandler.NewBadRequestError(ctx, err) } if err := validateSegmentConfig(config, mechanism); err != nil { @@ -220,7 +214,6 @@ func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time } } - subject := r.toSubject(uint32(tokenID)) chSegments, err := r.chService.GetSegments(ctx, subject, from, to, mechanism, config) if err != nil { return nil, handleDBError(ctx, err) @@ -330,7 +323,7 @@ func (r *Repository) GetSegments(ctx context.Context, tokenID int, from, to time preFetchedAggs = []*ch.AggSignal{} } } - summary, err := r.segmentSummary(ctx, uint32(tokenID), subject, seg, to, signalReqs, eventNames, eventCounts, preFetchedAggs) + summary, err := r.segmentSummary(ctx, subject, seg, to, signalReqs, eventNames, eventCounts, preFetchedAggs) if err != nil { return nil, err } @@ -445,7 +438,7 @@ func eventCountsToMap(counts []*ch.EventCount) map[string]int { return m } -func (r *Repository) segmentSummary(ctx context.Context, tokenID uint32, subject string, seg *model.Segment, queryTo time.Time, signalReqs []*model.SegmentSignalRequest, eventNames []string, preFetchedEventCounts []*ch.EventCount, preFetchedAggs []*ch.AggSignal) (*segmentSummaryResult, error) { +func (r *Repository) segmentSummary(ctx context.Context, subject string, seg *model.Segment, queryTo time.Time, signalReqs []*model.SegmentSignalRequest, eventNames []string, preFetchedEventCounts []*ch.EventCount, preFetchedAggs []*ch.AggSignal) (*segmentSummaryResult, error) { segFrom := seg.Start.Timestamp segTo := queryTo if seg.End != nil { @@ -462,7 +455,7 @@ func (r *Repository) segmentSummary(ctx context.Context, tokenID uint32, subject aggs = preFetchedAggs } else { aggArgs := &model.AggregatedSignalArgs{ - SignalArgs: model.SignalArgs{TokenID: tokenID}, + SignalArgs: model.SignalArgs{Subject: subject}, FromTS: segFrom, ToTS: segTo, Interval: intervalMicro, @@ -499,7 +492,7 @@ func (r *Repository) segmentSummary(ctx context.Context, tokenID uint32, subject // GetDailyActivity returns one record per calendar day in the requested date range, including days with zero segments. // mechanism must be ignitionDetection, frequencyAnalysis, or changePointDetection; idling, refuel, recharge return 400. -func (r *Repository) GetDailyActivity(ctx context.Context, tokenID int, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) ([]*model.DailyActivity, error) { +func (r *Repository) GetDailyActivity(ctx context.Context, subject string, from, to time.Time, mechanism model.DetectionMechanism, config *model.SegmentConfig, signalRequests []*model.SegmentSignalRequest, eventRequests []*model.SegmentEventRequest, timezone *string) ([]*model.DailyActivity, error) { if mechanism == model.DetectionMechanismIdling || mechanism == model.DetectionMechanismRefuel || mechanism == model.DetectionMechanismRecharge { return nil, errorhandler.NewBadRequestError(ctx, fmt.Errorf("dailyActivity does not accept mechanism %s; use ignitionDetection, frequencyAnalysis, or changePointDetection", mechanism)) } @@ -537,15 +530,10 @@ func (r *Repository) GetDailyActivity(ctx context.Context, tokenID int, from, to } } - segments, err := r.GetSegments(ctx, tokenID, rangeStart, rangeEnd, mechanism, config, signalReqs, eventRequests, nil, nil) + segments, err := r.GetSegments(ctx, subject, rangeStart, rangeEnd, mechanism, config, signalReqs, eventRequests, nil, nil) if err != nil { return nil, err } - subject := cloudevent.ERC721DID{ - ChainID: r.chainID, - ContractAddress: r.vehicleAddress, - TokenID: big.NewInt(int64(tokenID)), - }.String() var out []*model.DailyActivity for d := fromDate; !d.After(toDate); d = d.Add(24 * time.Hour) { @@ -581,7 +569,7 @@ func (r *Repository) GetDailyActivity(ctx context.Context, tokenID int, from, to lastSeg = seg } - signalSummary, startLoc, endLoc, eventSummary, err := r.daySummary(ctx, uint32(tokenID), subject, dayStartUTC, dayEndUTC, signalReqs, eventNames) + signalSummary, startLoc, endLoc, eventSummary, err := r.daySummary(ctx, subject, dayStartUTC, dayEndUTC, signalReqs, eventNames) if err != nil { return nil, err } @@ -615,14 +603,14 @@ func (r *Repository) GetDailyActivity(ctx context.Context, tokenID int, from, to return out, nil } -func (r *Repository) daySummary(ctx context.Context, tokenID uint32, subject string, dayStart, dayEnd time.Time, signalReqs []*model.SegmentSignalRequest, eventNames []string) ([]*model.SignalAggregationValue, *model.Location, *model.Location, []*model.EventCount, error) { +func (r *Repository) daySummary(ctx context.Context, subject string, dayStart, dayEnd time.Time, signalReqs []*model.SegmentSignalRequest, eventNames []string) ([]*model.SignalAggregationValue, *model.Location, *model.Location, []*model.EventCount, error) { intervalMicro := dayEnd.Sub(dayStart).Microseconds() if intervalMicro <= 0 { intervalMicro = 1 } floatArgs, locationArgs := buildAggArgs(signalReqs) aggArgs := &model.AggregatedSignalArgs{ - SignalArgs: model.SignalArgs{TokenID: tokenID}, + SignalArgs: model.SignalArgs{Subject: subject}, FromTS: dayStart, ToTS: dayEnd, Interval: intervalMicro, diff --git a/internal/repositories/validate.go b/internal/repositories/validate.go index b1da6f20..a8bf9063 100644 --- a/internal/repositories/validate.go +++ b/internal/repositories/validate.go @@ -97,8 +97,8 @@ func validateSignalArgs(args *model.SignalArgs) error { return ValidationError("signal args not provided") } - if args.TokenID < 1 { - return ValidationError("tokenID is not a positive integer") + if args.Subject == "" { + return ValidationError("subject is empty") } return validateFilter(args.Filter) @@ -116,10 +116,7 @@ func validateFilter(filter *model.SignalFilter) error { return nil } -func validateEventArgs(tokenID int, from, to time.Time, filter *model.EventFilter) error { - if tokenID < 1 { - return ValidationError("tokenID is not a positive integer") - } +func validateEventArgs(from, to time.Time, filter *model.EventFilter) error { if from.IsZero() { return ValidationError("from timestamp is zero") } diff --git a/internal/repositories/validate_test.go b/internal/repositories/validate_test.go index 1a7b6a9b..7cf924e6 100644 --- a/internal/repositories/validate_test.go +++ b/internal/repositories/validate_test.go @@ -15,41 +15,36 @@ func TestValidateEventArgs(t *testing.T) { validFilter := &model.EventFilter{} t.Run("valid args", func(t *testing.T) { - err := validateEventArgs(1, validFrom, validTo, validFilter) + err := validateEventArgs(validFrom, validTo, validFilter) require.NoError(t, err) }) - t.Run("tokenID < 1", func(t *testing.T) { - err := validateEventArgs(0, validFrom, validTo, validFilter) - require.Error(t, err) - }) - t.Run("from is zero", func(t *testing.T) { - err := validateEventArgs(1, time.Time{}, validTo, validFilter) + err := validateEventArgs(time.Time{}, validTo, validFilter) require.Error(t, err) }) t.Run("to is zero", func(t *testing.T) { - err := validateEventArgs(1, validFrom, time.Time{}, validFilter) + err := validateEventArgs(validFrom, time.Time{}, validFilter) require.Error(t, err) }) t.Run("from after to", func(t *testing.T) { from := validTo.Add(time.Second) - err := validateEventArgs(1, from, validTo, validFilter) + err := validateEventArgs(from, validTo, validFilter) require.Error(t, err) }) t.Run("valid tags", func(t *testing.T) { - err := validateEventArgs(1, validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAny: []string{vss.TagBehaviorHarshAcceleration, vss.TagSafetyCollision}}}) + err := validateEventArgs(validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAny: []string{vss.TagBehaviorHarshAcceleration, vss.TagSafetyCollision}}}) require.NoError(t, err) - err = validateEventArgs(1, validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAll: []string{vss.TagBehaviorHarshAcceleration}}}) + err = validateEventArgs(validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAll: []string{vss.TagBehaviorHarshAcceleration}}}) require.NoError(t, err) }) t.Run("invalid tags", func(t *testing.T) { - err := validateEventArgs(1, validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAny: []string{"invalid"}}}) + err := validateEventArgs(validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAny: []string{"invalid"}}}) require.Error(t, err) - err = validateEventArgs(1, validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAll: []string{vss.TagBehaviorHarshAcceleration, "invalid"}}}) + err = validateEventArgs(validFrom, validTo, &model.EventFilter{Tags: &model.StringArrayFilter{ContainsAll: []string{vss.TagBehaviorHarshAcceleration, "invalid"}}}) require.Error(t, err) }) @@ -60,34 +55,29 @@ func TestValidateSegmentArgs(t *testing.T) { validTo := time.Now() t.Run("valid args", func(t *testing.T) { - err := validateSegmentArgs(1, validFrom, validTo) + err := validateSegmentArgs(validFrom, validTo) require.NoError(t, err) }) t.Run("exactly 31 days passes", func(t *testing.T) { from := validTo.Add(-31 * 24 * time.Hour) - err := validateSegmentArgs(1, from, validTo) + err := validateSegmentArgs(from, validTo) require.NoError(t, err) }) - t.Run("tokenID <= 0", func(t *testing.T) { - err := validateSegmentArgs(0, validFrom, validTo) - require.Error(t, err) - }) - t.Run("from after to", func(t *testing.T) { - err := validateSegmentArgs(1, validTo.Add(time.Minute), validTo) + err := validateSegmentArgs(validTo.Add(time.Minute), validTo) require.Error(t, err) }) t.Run("from equal to", func(t *testing.T) { - err := validateSegmentArgs(1, validFrom, validFrom) + err := validateSegmentArgs(validFrom, validFrom) require.Error(t, err) }) t.Run("date range exceeded", func(t *testing.T) { from := validTo.Add(-33 * 24 * time.Hour) // max is 32 days - err := validateSegmentArgs(1, from, validTo) + err := validateSegmentArgs(from, validTo) require.Error(t, err) }) } diff --git a/internal/repositories/vc/vc.go b/internal/repositories/vc/vc.go index 54b38f42..f4bb0b52 100644 --- a/internal/repositories/vc/vc.go +++ b/internal/repositories/vc/vc.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/big" "time" "github.com/DIMO-Network/attestation-api/pkg/types" @@ -44,13 +43,8 @@ func New(indexService indexRepoService, settings config.Settings) *Repository { } // GetLatestVINVC fetches the latest VIN VC for the given vehicle. -func (r *Repository) GetLatestVINVC(ctx context.Context, vehicleTokenID uint32) (*model.Vinvc, error) { - vehicleDID := cloudevent.ERC721DID{ - ChainID: r.chainID, - ContractAddress: r.vehicleAddress, - TokenID: new(big.Int).SetUint64(uint64(vehicleTokenID)), - }.String() - dataObj, err := r.getVINVC(ctx, vehicleDID) +func (r *Repository) GetLatestVINVC(ctx context.Context, subject string) (*model.Vinvc, error) { + dataObj, err := r.getVINVC(ctx, subject) if err != nil { if status.Code(err) == codes.NotFound { return nil, nil //nolint // we nil is a valid response diff --git a/internal/repositories/vc/vc_test.go b/internal/repositories/vc/vc_test.go index d5ffc0ab..413a2052 100644 --- a/internal/repositories/vc/vc_test.go +++ b/internal/repositories/vc/vc_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "slices" "testing" "time" @@ -52,6 +53,11 @@ func TestGetLatestVC(t *testing.T) { // Initialize variables ctx := context.Background() vehicleTokenID := uint32(123) + vehicleSubject := cloudevent.ERC721DID{ + ChainID: 3, + ContractAddress: common.HexToAddress("0xfEDCBA0987654321FeDcbA0987654321fedCBA09"), + TokenID: new(big.Int).SetUint64(uint64(vehicleTokenID)), + }.String() // Create mock controller ctrl := gomock.NewController(t) @@ -148,7 +154,7 @@ func TestGetLatestVC(t *testing.T) { tt.mockSetup() // Call the method - vc, err := svc.GetLatestVINVC(ctx, vehicleTokenID) + vc, err := svc.GetLatestVINVC(ctx, vehicleSubject) // Assert the results if tt.expectedErr { diff --git a/internal/service/ch/ch_test.go b/internal/service/ch/ch_test.go index ba855164..16072b87 100644 --- a/internal/service/ch/ch_test.go +++ b/internal/service/ch/ch_test.go @@ -78,7 +78,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "no aggs", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -90,7 +90,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "average", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -116,7 +116,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "max and min", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -153,7 +153,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "max smartcar", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, Filter: &model.SignalFilter{ Source: ref("smartcar"), }, @@ -182,7 +182,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "unique", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -208,7 +208,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "Top autopi", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, Filter: &model.SignalFilter{ Source: ref("autopi"), }, @@ -237,7 +237,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "first float", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -263,7 +263,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "last float", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -289,7 +289,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "lt filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -318,7 +318,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "gt filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -347,7 +347,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "gte filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -376,7 +376,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "lte filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -405,7 +405,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "filter for numeric values in set", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -434,7 +434,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "float neq filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -463,7 +463,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "float neq filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -492,7 +492,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "float not in filter", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -521,7 +521,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "float filters and-ed", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -551,7 +551,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "float filters or-ed", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -583,7 +583,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "first string", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -609,7 +609,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "last string", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -635,7 +635,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "multiple", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -695,7 +695,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "first location", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -721,7 +721,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "last location", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -747,7 +747,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "average location", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -773,7 +773,7 @@ func (c *CHServiceTestSuite) TestGetAggSignal() { name: "first location in fence", aggArgs: model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, FromTS: c.dataStartTime, ToTS: endTs, @@ -833,7 +833,7 @@ func (c *CHServiceTestSuite) TestGetLatestSignal() { name: "latest", latestArgs: model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, SignalNames: map[string]struct{}{ vss.FieldSpeed: {}, @@ -851,7 +851,7 @@ func (c *CHServiceTestSuite) TestGetLatestSignal() { name: "latest smartcar", latestArgs: model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, Filter: &model.SignalFilter{ Source: ref("smartcar"), }, @@ -872,7 +872,7 @@ func (c *CHServiceTestSuite) TestGetLatestSignal() { name: "lastSeen", latestArgs: model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, IncludeLastSeen: true, SignalNames: map[string]struct{}{}, @@ -888,7 +888,7 @@ func (c *CHServiceTestSuite) TestGetLatestSignal() { name: "latest location", latestArgs: model.LatestSignalsArgs{ SignalArgs: model.SignalArgs{ - TokenID: 1, + Subject: testSubject1, }, SignalNames: map[string]struct{}{}, LocationSignalNames: map[string]struct{}{ @@ -975,7 +975,7 @@ func (c *CHServiceTestSuite) TestOriginGrouping() { // Create aggregation query args aggArgs := &model.AggregatedSignalArgs{ SignalArgs: model.SignalArgs{ - TokenID: 100, + Subject: testSubject100, }, FromTS: startTime, ToTS: endTime, diff --git a/schema/base.graphqls b/schema/base.graphqls index a8cfcbd0..9eeb82e9 100644 --- a/schema/base.graphqls +++ b/schema/base.graphqls @@ -29,7 +29,8 @@ type Query { signals returns a collection of signals for a given token in a given time range. """ signals( - tokenId: Int! + tokenId: Int + subject: String """ interval is a time span that used for aggregatting the data with. A duration string is a sequence of decimal numbers, each with optional fraction and a unit suffix, @@ -43,18 +44,18 @@ type Query { """ SignalsLatest returns the latest signals for a given token. """ - signalsLatest(tokenId: Int!, filter: SignalFilter): SignalCollection + signalsLatest(tokenId: Int, subject: String, filter: SignalFilter): SignalCollection @requiresVehicleToken """ - availableSignals returns a list of queryable signal names that have stored data for a given tokenId. + availableSignals returns a list of queryable signal names that have stored data for the given vehicle. """ - availableSignals(tokenId: Int!, filter: SignalFilter): [String!] + availableSignals(tokenId: Int, subject: String, filter: SignalFilter): [String!] @requiresVehicleToken """ - data summary of all signals for a given tokenId + data summary of all signals for the given vehicle """ - dataSummary(tokenId: Int!, filter: SignalFilter): DataSummary + dataSummary(tokenId: Int, subject: String, filter: SignalFilter): DataSummary @requiresVehicleToken } type SignalAggregations { diff --git a/schema/events.graphqls b/schema/events.graphqls index f9b79feb..9e9d8a64 100644 --- a/schema/events.graphqls +++ b/schema/events.graphqls @@ -6,7 +6,11 @@ extend type Query { """ tokenId is the id of the token to get events for. """ - tokenId: Int! + tokenId: Int + """ + subject is the DID subject of the vehicle. + """ + subject: String """ from is the start time of the event. """ diff --git a/schema/segments.graphqls b/schema/segments.graphqls index 46d9534c..3a459231 100644 --- a/schema/segments.graphqls +++ b/schema/segments.graphqls @@ -57,7 +57,8 @@ extend type Query { When signalRequests is provided, those requests are added on top of the default set; duplicates (same name and agg) are omitted. """ segments( - tokenId: Int! + tokenId: Int + subject: String from: Time! to: Time! mechanism: DetectionMechanism! @@ -81,7 +82,8 @@ extend type Query { Maximum date range: 31 days. """ dailyActivity( - tokenId: Int! + tokenId: Int + subject: String from: Time! to: Time! mechanism: DetectionMechanism! diff --git a/schema/vc.graphqls b/schema/vc.graphqls index f980bc4e..148884eb 100644 --- a/schema/vc.graphqls +++ b/schema/vc.graphqls @@ -8,7 +8,11 @@ extend type Query { """ The token ID of the vehicle. """ - tokenId: Int! + tokenId: Int + """ + The DID subject of the vehicle. + """ + subject: String ): VINVC @requiresVehicleToken @requiresAllOfPrivileges(privileges: [VEHICLE_VIN_CREDENTIAL]) From 3ac18cb66b1fcf187c6572b040467f9dcce7b9f4 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Mon, 2 Mar 2026 17:20:30 -0500 Subject: [PATCH 08/10] Add GraphQL deprecation warnings for tokenId Also get rid of some vehicle-specific language in the descriptions --- internal/graph/generated.go | 92 +++++++++++++++++++++++++++++-------- schema/base.graphqls | 56 ++++++++++++++++++---- schema/events.graphqls | 9 ++-- schema/segments.graphqls | 18 +++++++- schema/vc.graphqls | 9 ++-- 5 files changed, 148 insertions(+), 36 deletions(-) diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 6c52011a..280ba0be 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -3237,10 +3237,17 @@ The root query type for the GraphQL schema. """ type Query { """ - signals returns a collection of signals for a given token in a given time range. + Compute aggregations of signals for a given subject. """ signals( - tokenId: Int + """ + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ subject: String """ interval is a time span that used for aggregatting the data with. @@ -3253,20 +3260,53 @@ type Query { filter: SignalFilter ): [SignalAggregations!] @requiresVehicleToken """ - SignalsLatest returns the latest signals for a given token. + Query the latest values of signals for a given subject. """ - signalsLatest(tokenId: Int, subject: String, filter: SignalFilter): SignalCollection + signalsLatest( + """ + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ + subject: String + filter: SignalFilter + ): SignalCollection @requiresVehicleToken """ - availableSignals returns a list of queryable signal names that have stored data for the given vehicle. + List queryable signal names that have stored data for a given subject. """ - availableSignals(tokenId: Int, subject: String, filter: SignalFilter): [String!] + availableSignals( + """ + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ + subject: String + filter: SignalFilter + ): [String!] @requiresVehicleToken """ - data summary of all signals for the given vehicle + Data summary of all signals for a given subject. """ - dataSummary(tokenId: Int, subject: String, filter: SignalFilter): DataSummary + dataSummary( + """ + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ + subject: String + filter: SignalFilter + ): DataSummary @requiresVehicleToken } type SignalAggregations { @@ -3622,15 +3662,16 @@ input AftermarketDeviceBy @oneOf { `, BuiltIn: false}, {Name: "../../schema/events.graphqls", Input: `extend type Query { """ - events returns a list of events for a given token in a given time range. + Returns a list of events for a given subject in a given time range. """ events( """ - tokenId is the id of the token to get events for. + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. """ - tokenId: Int + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") """ - subject is the DID subject of the vehicle. + The subject of the requested events. Typically a W3C DID. """ subject: String """ @@ -3745,7 +3786,14 @@ extend type Query { When signalRequests is provided, those requests are added on top of the default set; duplicates (same name and agg) are omitted. """ segments( - tokenId: Int + """ + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") + """ + The subject of the requested data. Typically a W3C DID. + """ subject: String from: Time! to: Time! @@ -3770,7 +3818,14 @@ extend type Query { Maximum date range: 31 days. """ dailyActivity( - tokenId: Int + """ + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") + """ + The subject of the requested data. Typically a W3C DID. + """ subject: String from: Time! to: Time! @@ -5661,17 +5716,18 @@ extend input EventFilter { `, BuiltIn: false}, {Name: "../../schema/vc.graphqls", Input: `extend type Query { """ - vinVCLatest returns the latest VINVC data for a given token. + vinVCLatest returns the latest VIN VC data for a given subject. Required Privileges: [VEHICLE_VIN_CREDENTIAL] """ vinVCLatest( """ - The token ID of the vehicle. + A vehicle token id. This is translated into a ERC-721 DID ` + "`" + `subject` + "`" + ` string, and you + should use that argument instead. """ - tokenId: Int + tokenId: Int @deprecated(reason: "Use ` + "`" + `subject` + "`" + ` instead.") """ - The DID subject of the vehicle. + The subject of the requested data. Typically a W3C DID. """ subject: String ): VINVC diff --git a/schema/base.graphqls b/schema/base.graphqls index 9eeb82e9..5444fe25 100644 --- a/schema/base.graphqls +++ b/schema/base.graphqls @@ -26,10 +26,17 @@ The root query type for the GraphQL schema. """ type Query { """ - signals returns a collection of signals for a given token in a given time range. + Compute aggregations of signals for a given subject. """ signals( - tokenId: Int + """ + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use `subject` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ subject: String """ interval is a time span that used for aggregatting the data with. @@ -42,20 +49,53 @@ type Query { filter: SignalFilter ): [SignalAggregations!] @requiresVehicleToken """ - SignalsLatest returns the latest signals for a given token. + Query the latest values of signals for a given subject. """ - signalsLatest(tokenId: Int, subject: String, filter: SignalFilter): SignalCollection + signalsLatest( + """ + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use `subject` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ + subject: String + filter: SignalFilter + ): SignalCollection @requiresVehicleToken """ - availableSignals returns a list of queryable signal names that have stored data for the given vehicle. + List queryable signal names that have stored data for a given subject. """ - availableSignals(tokenId: Int, subject: String, filter: SignalFilter): [String!] + availableSignals( + """ + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use `subject` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ + subject: String + filter: SignalFilter + ): [String!] @requiresVehicleToken """ - data summary of all signals for the given vehicle + Data summary of all signals for a given subject. """ - dataSummary(tokenId: Int, subject: String, filter: SignalFilter): DataSummary + dataSummary( + """ + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use `subject` instead.") + """ + The subject of the requested signals. Typically a W3C DID. + """ + subject: String + filter: SignalFilter + ): DataSummary @requiresVehicleToken } type SignalAggregations { diff --git a/schema/events.graphqls b/schema/events.graphqls index 9e9d8a64..44737fe4 100644 --- a/schema/events.graphqls +++ b/schema/events.graphqls @@ -1,14 +1,15 @@ extend type Query { """ - events returns a list of events for a given token in a given time range. + Returns a list of events for a given subject in a given time range. """ events( """ - tokenId is the id of the token to get events for. + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. """ - tokenId: Int + tokenId: Int @deprecated(reason: "Use `subject` instead.") """ - subject is the DID subject of the vehicle. + The subject of the requested events. Typically a W3C DID. """ subject: String """ diff --git a/schema/segments.graphqls b/schema/segments.graphqls index 3a459231..d1bd93c9 100644 --- a/schema/segments.graphqls +++ b/schema/segments.graphqls @@ -57,7 +57,14 @@ extend type Query { When signalRequests is provided, those requests are added on top of the default set; duplicates (same name and agg) are omitted. """ segments( - tokenId: Int + """ + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use `subject` instead.") + """ + The subject of the requested data. Typically a W3C DID. + """ subject: String from: Time! to: Time! @@ -82,7 +89,14 @@ extend type Query { Maximum date range: 31 days. """ dailyActivity( - tokenId: Int + """ + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. + """ + tokenId: Int @deprecated(reason: "Use `subject` instead.") + """ + The subject of the requested data. Typically a W3C DID. + """ subject: String from: Time! to: Time! diff --git a/schema/vc.graphqls b/schema/vc.graphqls index 148884eb..d32ddc73 100644 --- a/schema/vc.graphqls +++ b/schema/vc.graphqls @@ -1,16 +1,17 @@ extend type Query { """ - vinVCLatest returns the latest VINVC data for a given token. + vinVCLatest returns the latest VIN VC data for a given subject. Required Privileges: [VEHICLE_VIN_CREDENTIAL] """ vinVCLatest( """ - The token ID of the vehicle. + A vehicle token id. This is translated into a ERC-721 DID `subject` string, and you + should use that argument instead. """ - tokenId: Int + tokenId: Int @deprecated(reason: "Use `subject` instead.") """ - The DID subject of the vehicle. + The subject of the requested data. Typically a W3C DID. """ subject: String ): VINVC From bf1e2c683cbdbd08802c0e06380a21a84c04900b Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Mon, 9 Mar 2026 14:42:01 -0400 Subject: [PATCH 09/10] Bring back some error handling in auth.go --- internal/auth/auth.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 942b781c..7773a94b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -43,8 +43,14 @@ func newError(msg string, args ...any) error { func NewVehicleTokenCheck(requiredAddr common.Address) func(context.Context, any, graphql.Resolver) (any, error) { return func(ctx context.Context, _ any, next graphql.Resolver) (any, error) { - tokenID, _ := getArg[*int](ctx, tokenIdArg) - subject, _ := getArg[*string](ctx, subjectArg) + tokenID, err := getArg[*int](ctx, tokenIdArg) + if err != nil && !errors.Is(err, errArgNotFound) { + return nil, fmt.Errorf("failed to get %s arg: %w", tokenIdArg, err) + } + subject, err := getArg[*string](ctx, subjectArg) + if err != nil && !errors.Is(err, errArgNotFound) { + return nil, fmt.Errorf("failed to get %s arg: %w", subjectArg, err) + } switch { case tokenID != nil && subject != nil: @@ -125,6 +131,8 @@ func OneOfPrivilegeCheck(ctx context.Context, _ any, next graphql.Resolver, requ return nil, newError("requires at least one of the following privileges %v", requiredPrivs) } +var errArgNotFound = errors.New("arg not found") + func getArg[T any](ctx context.Context, name string) (T, error) { var resp T fCtx := graphql.GetFieldContext(ctx) @@ -134,7 +142,7 @@ func getArg[T any](ctx context.Context, name string) (T, error) { val, ok := fCtx.Args[name] if !ok { - return resp, fmt.Errorf("no argument named %s", name) + return resp, errArgNotFound } resp, ok = val.(T) From 3c086f20e07844cb9503a112ad46b4a67dd9a528 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Mon, 9 Mar 2026 14:48:58 -0400 Subject: [PATCH 10/10] Return UnauthorizedError --- internal/auth/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 7773a94b..c63fbb87 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -45,11 +45,11 @@ func NewVehicleTokenCheck(requiredAddr common.Address) func(context.Context, any return func(ctx context.Context, _ any, next graphql.Resolver) (any, error) { tokenID, err := getArg[*int](ctx, tokenIdArg) if err != nil && !errors.Is(err, errArgNotFound) { - return nil, fmt.Errorf("failed to get %s arg: %w", tokenIdArg, err) + return nil, UnauthorizedError{err: fmt.Errorf("failed to get %s arg: %w", tokenIdArg, err)} } subject, err := getArg[*string](ctx, subjectArg) if err != nil && !errors.Is(err, errArgNotFound) { - return nil, fmt.Errorf("failed to get %s arg: %w", subjectArg, err) + return nil, UnauthorizedError{err: fmt.Errorf("failed to get %s arg: %w", subjectArg, err)} } switch {