Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions internal/graph/base.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 19 additions & 12 deletions internal/graph/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,38 @@ import (
"github.com/DIMO-Network/fetch-api/pkg/eventrepo"
"github.com/DIMO-Network/fetch-api/pkg/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)

// filterToSearchOptions converts GraphQL filter and subject DID to grpc.SearchOptions.
func filterToSearchOptions(filter *model.CloudEventFilter, subject string) *grpc.SearchOptions {
opts := &grpc.SearchOptions{
Subject: &wrapperspb.StringValue{Value: subject},
// filterToAdvancedSearchOptions converts a GraphQL filter and subject DID directly to
// grpc.AdvancedSearchOptions. The type and types fields are unioned: if both are set,
// results match events whose type is any of the combined values.
func filterToAdvancedSearchOptions(filter *model.CloudEventFilter, subject string) *grpc.AdvancedSearchOptions {
opts := &grpc.AdvancedSearchOptions{
Subject: &grpc.StringFilterOption{In: []string{subject}},
}
if filter == nil {
return opts
}
if filter.ID != nil {
opts.Id = &wrapperspb.StringValue{Value: *filter.ID}
}
// Merge type (single) and types (list) with OR semantics.
var allTypes []string
if filter.Type != nil {
opts.Type = &wrapperspb.StringValue{Value: *filter.Type}
allTypes = append(allTypes, *filter.Type)
}
allTypes = append(allTypes, filter.Types...)
if len(allTypes) > 0 {
opts.Type = &grpc.StringFilterOption{In: allTypes}
}
if filter.ID != nil {
opts.Id = &grpc.StringFilterOption{In: []string{*filter.ID}}
}
if filter.Dataversion != nil {
opts.DataVersion = &wrapperspb.StringValue{Value: *filter.Dataversion}
opts.DataVersion = &grpc.StringFilterOption{In: []string{*filter.Dataversion}}
}
if filter.Source != nil {
opts.Source = &wrapperspb.StringValue{Value: *filter.Source}
opts.Source = &grpc.StringFilterOption{In: []string{*filter.Source}}
}
if filter.Producer != nil {
opts.Producer = &wrapperspb.StringValue{Value: *filter.Producer}
opts.Producer = &grpc.StringFilterOption{In: []string{*filter.Producer}}
}
if filter.Before != nil {
opts.Before = timestamppb.New(*filter.Before)
Expand Down
49 changes: 47 additions & 2 deletions internal/graph/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions internal/graph/model/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions internal/graph/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ const (
errNoAccessToSubject = "unauthorized: token does not have access to this subject"
)

// requireSubjectOptsByDID validates raw-data access and returns search options for the DID.
// requireSubjectOptsByDID validates raw-data access and returns advanced search options for the DID.
// requestedDID: the DID from the client (e.g. cloudEvents(did: "...")).
// tokenSubjectDID: the DID the JWT grants access to (tok.Asset).
func (r *queryResolver) requireSubjectOptsByDID(ctx context.Context, requestedDID string, filter *model.CloudEventFilter) (*grpc.SearchOptions, error) {
func (r *queryResolver) requireSubjectOptsByDID(ctx context.Context, requestedDID string, filter *model.CloudEventFilter) (*grpc.AdvancedSearchOptions, error) {
token, err := requireRawDataToken(ctx)
if err != nil {
return nil, err
Expand All @@ -46,7 +46,7 @@ func (r *queryResolver) requireSubjectOptsByDID(ctx context.Context, requestedDI
if err != nil {
return nil, err
}
return filterToSearchOptions(filter, searchSubject), nil
return filterToAdvancedSearchOptions(filter, searchSubject), nil
}

// requireRawDataToken returns the token if the context has claims and the token has raw-data permission.
Expand Down
43 changes: 36 additions & 7 deletions internal/graph/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ func TestRequireVehicleOptsByDID(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, opts)
require.NotNil(t, opts.Subject)
assert.Equal(t, didStr, opts.Subject.Value)
assert.Equal(t, []string{didStr}, opts.Subject.In)
})

t.Run("applies filter to search options", func(t *testing.T) {
t.Run("applies single type filter to search options", func(t *testing.T) {
ctx := contextWithToken(didStr, tokenclaims.PermissionGetRawData)
filter := &model.CloudEventFilter{
Type: ptr("dimo.status"),
Expand All @@ -45,8 +45,37 @@ func TestRequireVehicleOptsByDID(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, opts)
require.NotNil(t, opts.Type)
assert.Equal(t, "dimo.status", opts.Type.Value)
assert.Equal(t, didStr, opts.Subject.Value)
assert.Equal(t, []string{"dimo.status"}, opts.Type.In)
assert.Equal(t, []string{didStr}, opts.Subject.In)
})

t.Run("applies types array filter to search options", func(t *testing.T) {
ctx := contextWithToken(didStr, tokenclaims.PermissionGetRawData)
filter := &model.CloudEventFilter{
Types: []string{"dimo.status", "dimo.fingerprint"},
}
r := &Resolver{}
q := &queryResolver{r}
opts, err := q.requireSubjectOptsByDID(ctx, didStr, filter)
require.NoError(t, err)
require.NotNil(t, opts)
require.NotNil(t, opts.Type)
assert.Equal(t, []string{"dimo.status", "dimo.fingerprint"}, opts.Type.In)
})

t.Run("unions type and types when both set", func(t *testing.T) {
ctx := contextWithToken(didStr, tokenclaims.PermissionGetRawData)
filter := &model.CloudEventFilter{
Type: ptr("dimo.status"),
Types: []string{"dimo.fingerprint", "dimo.attestation"},
}
r := &Resolver{}
q := &queryResolver{r}
opts, err := q.requireSubjectOptsByDID(ctx, didStr, filter)
require.NoError(t, err)
require.NotNil(t, opts)
require.NotNil(t, opts.Type)
assert.Equal(t, []string{"dimo.status", "dimo.fingerprint", "dimo.attestation"}, opts.Type.In)
})

t.Run("unauthorized when token does not match DID", func(t *testing.T) {
Expand Down Expand Up @@ -90,7 +119,7 @@ func TestRequireVehicleOptsByDID(t *testing.T) {
opts, err := q.requireSubjectOptsByDID(ctx, didStr, nil)
require.NoError(t, err)
require.NotNil(t, opts)
assert.Equal(t, didStr, opts.Subject.Value)
assert.Equal(t, []string{didStr}, opts.Subject.In)
})

t.Run("only GetLocationHistory without GetNonLocationHistory denied", func(t *testing.T) {
Expand Down Expand Up @@ -156,7 +185,7 @@ func TestRequireSubjectOptsByDID_EthrDID(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, opts)
require.NotNil(t, opts.Subject)
assert.Equal(t, ethrDID, opts.Subject.Value)
assert.Equal(t, []string{ethrDID}, opts.Subject.In)
})

t.Run("ethr token + different ethr query DID denied", func(t *testing.T) {
Expand Down Expand Up @@ -223,7 +252,7 @@ func TestRequireVehicleOptsByDID_DeviceDID(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, opts)
// Subject must be the device DID, not the vehicle DID.
assert.Equal(t, deviceDID, opts.Subject.Value)
assert.Equal(t, []string{deviceDID}, opts.Subject.In)
})

t.Run("device DID with no identity client returns error", func(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion pkg/eventrepo/eventrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ type CloudEventTypeSummary struct {

// GetCloudEventTypeSummaries returns per-type counts and time ranges for the given search options.
func (s *Service) GetCloudEventTypeSummaries(ctx context.Context, opts *grpc.SearchOptions) ([]CloudEventTypeSummary, error) {
advancedOpts := convertSearchOptionsToAdvanced(opts)
return s.GetCloudEventTypeSummariesAdvanced(ctx, convertSearchOptionsToAdvanced(opts))
}

// GetCloudEventTypeSummariesAdvanced returns event type summaries filtered by advanced search options.
func (s *Service) GetCloudEventTypeSummariesAdvanced(ctx context.Context, advancedOpts *grpc.AdvancedSearchOptions) ([]CloudEventTypeSummary, error) {
mods := []qm.QueryMod{
qm.Select(
chindexer.TypeColumn,
Expand Down
4 changes: 3 additions & 1 deletion schema/base.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@ type CloudEventIndex {
}

"""
Filter for cloud event queries.
Filter for cloud event queries.
"""
input CloudEventFilter {
id: String
type: String
"""List of event types to match (OR semantics). Combined with `type` if both are set."""
types: [String!]
dataversion: String
source: String
producer: String
Expand Down
Loading