Skip to content

Commit 97895b9

Browse files
elffjsclaude
andauthored
Add types array filter to GraphQL CloudEventFilter (#72)
* Add types array filter to GraphQL CloudEventFilter Clients can now pass types: [String!] to match events of multiple types in a single query (OR semantics). The type and types fields are unioned when both are set. The GraphQL layer now builds AdvancedSearchOptions directly, eliminating the redundant SearchOptions→AdvancedSearchOptions conversion step. A new GetCloudEventTypeSummariesAdvanced method was added to eventrepo.Service to support this, with the original GetCloudEventTypeSummaries delegating to it for backward compatibility. https://claude.ai/code/session_01SR5ptKf3grphf7mfv7PY14 * Regenerate --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2c0c4a1 commit 97895b9

8 files changed

Lines changed: 121 additions & 33 deletions

File tree

internal/graph/base.resolvers.go

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/convert.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,38 @@ import (
66
"github.com/DIMO-Network/fetch-api/pkg/eventrepo"
77
"github.com/DIMO-Network/fetch-api/pkg/grpc"
88
"google.golang.org/protobuf/types/known/timestamppb"
9-
"google.golang.org/protobuf/types/known/wrapperspb"
109
)
1110

12-
// filterToSearchOptions converts GraphQL filter and subject DID to grpc.SearchOptions.
13-
func filterToSearchOptions(filter *model.CloudEventFilter, subject string) *grpc.SearchOptions {
14-
opts := &grpc.SearchOptions{
15-
Subject: &wrapperspb.StringValue{Value: subject},
11+
// filterToAdvancedSearchOptions converts a GraphQL filter and subject DID directly to
12+
// grpc.AdvancedSearchOptions. The type and types fields are unioned: if both are set,
13+
// results match events whose type is any of the combined values.
14+
func filterToAdvancedSearchOptions(filter *model.CloudEventFilter, subject string) *grpc.AdvancedSearchOptions {
15+
opts := &grpc.AdvancedSearchOptions{
16+
Subject: &grpc.StringFilterOption{In: []string{subject}},
1617
}
1718
if filter == nil {
1819
return opts
1920
}
20-
if filter.ID != nil {
21-
opts.Id = &wrapperspb.StringValue{Value: *filter.ID}
22-
}
21+
// Merge type (single) and types (list) with OR semantics.
22+
var allTypes []string
2323
if filter.Type != nil {
24-
opts.Type = &wrapperspb.StringValue{Value: *filter.Type}
24+
allTypes = append(allTypes, *filter.Type)
25+
}
26+
allTypes = append(allTypes, filter.Types...)
27+
if len(allTypes) > 0 {
28+
opts.Type = &grpc.StringFilterOption{In: allTypes}
29+
}
30+
if filter.ID != nil {
31+
opts.Id = &grpc.StringFilterOption{In: []string{*filter.ID}}
2532
}
2633
if filter.Dataversion != nil {
27-
opts.DataVersion = &wrapperspb.StringValue{Value: *filter.Dataversion}
34+
opts.DataVersion = &grpc.StringFilterOption{In: []string{*filter.Dataversion}}
2835
}
2936
if filter.Source != nil {
30-
opts.Source = &wrapperspb.StringValue{Value: *filter.Source}
37+
opts.Source = &grpc.StringFilterOption{In: []string{*filter.Source}}
3138
}
3239
if filter.Producer != nil {
33-
opts.Producer = &wrapperspb.StringValue{Value: *filter.Producer}
40+
opts.Producer = &grpc.StringFilterOption{In: []string{*filter.Producer}}
3441
}
3542
if filter.Before != nil {
3643
opts.Before = timestamppb.New(*filter.Before)

internal/graph/generated.go

Lines changed: 47 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/model/models_gen.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/resolver.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ const (
3333
errNoAccessToSubject = "unauthorized: token does not have access to this subject"
3434
)
3535

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

5252
// requireRawDataToken returns the token if the context has claims and the token has raw-data permission.

internal/graph/resolver_test.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ func TestRequireVehicleOptsByDID(t *testing.T) {
3131
require.NoError(t, err)
3232
require.NotNil(t, opts)
3333
require.NotNil(t, opts.Subject)
34-
assert.Equal(t, didStr, opts.Subject.Value)
34+
assert.Equal(t, []string{didStr}, opts.Subject.In)
3535
})
3636

37-
t.Run("applies filter to search options", func(t *testing.T) {
37+
t.Run("applies single type filter to search options", func(t *testing.T) {
3838
ctx := contextWithToken(didStr, tokenclaims.PermissionGetRawData)
3939
filter := &model.CloudEventFilter{
4040
Type: ptr("dimo.status"),
@@ -45,8 +45,37 @@ func TestRequireVehicleOptsByDID(t *testing.T) {
4545
require.NoError(t, err)
4646
require.NotNil(t, opts)
4747
require.NotNil(t, opts.Type)
48-
assert.Equal(t, "dimo.status", opts.Type.Value)
49-
assert.Equal(t, didStr, opts.Subject.Value)
48+
assert.Equal(t, []string{"dimo.status"}, opts.Type.In)
49+
assert.Equal(t, []string{didStr}, opts.Subject.In)
50+
})
51+
52+
t.Run("applies types array filter to search options", func(t *testing.T) {
53+
ctx := contextWithToken(didStr, tokenclaims.PermissionGetRawData)
54+
filter := &model.CloudEventFilter{
55+
Types: []string{"dimo.status", "dimo.fingerprint"},
56+
}
57+
r := &Resolver{}
58+
q := &queryResolver{r}
59+
opts, err := q.requireSubjectOptsByDID(ctx, didStr, filter)
60+
require.NoError(t, err)
61+
require.NotNil(t, opts)
62+
require.NotNil(t, opts.Type)
63+
assert.Equal(t, []string{"dimo.status", "dimo.fingerprint"}, opts.Type.In)
64+
})
65+
66+
t.Run("unions type and types when both set", func(t *testing.T) {
67+
ctx := contextWithToken(didStr, tokenclaims.PermissionGetRawData)
68+
filter := &model.CloudEventFilter{
69+
Type: ptr("dimo.status"),
70+
Types: []string{"dimo.fingerprint", "dimo.attestation"},
71+
}
72+
r := &Resolver{}
73+
q := &queryResolver{r}
74+
opts, err := q.requireSubjectOptsByDID(ctx, didStr, filter)
75+
require.NoError(t, err)
76+
require.NotNil(t, opts)
77+
require.NotNil(t, opts.Type)
78+
assert.Equal(t, []string{"dimo.status", "dimo.fingerprint", "dimo.attestation"}, opts.Type.In)
5079
})
5180

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

96125
t.Run("only GetLocationHistory without GetNonLocationHistory denied", func(t *testing.T) {
@@ -156,7 +185,7 @@ func TestRequireSubjectOptsByDID_EthrDID(t *testing.T) {
156185
require.NoError(t, err)
157186
require.NotNil(t, opts)
158187
require.NotNil(t, opts.Subject)
159-
assert.Equal(t, ethrDID, opts.Subject.Value)
188+
assert.Equal(t, []string{ethrDID}, opts.Subject.In)
160189
})
161190

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

229258
t.Run("device DID with no identity client returns error", func(t *testing.T) {

pkg/eventrepo/eventrepo.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,11 @@ type CloudEventTypeSummary struct {
199199

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

205+
// GetCloudEventTypeSummariesAdvanced returns event type summaries filtered by advanced search options.
206+
func (s *Service) GetCloudEventTypeSummariesAdvanced(ctx context.Context, advancedOpts *grpc.AdvancedSearchOptions) ([]CloudEventTypeSummary, error) {
204207
mods := []qm.QueryMod{
205208
qm.Select(
206209
chindexer.TypeColumn,

schema/base.graphqls

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,13 @@ type CloudEventIndex {
9393
}
9494

9595
"""
96-
Filter for cloud event queries.
96+
Filter for cloud event queries.
9797
"""
9898
input CloudEventFilter {
9999
id: String
100100
type: String
101+
"""List of event types to match (OR semantics). Combined with `type` if both are set."""
102+
types: [String!]
101103
dataversion: String
102104
source: String
103105
producer: String

0 commit comments

Comments
 (0)