diff --git a/internal/graph/base.resolvers.go b/internal/graph/base.resolvers.go index 35f4e37..7ab6cac 100644 --- a/internal/graph/base.resolvers.go +++ b/internal/graph/base.resolvers.go @@ -47,7 +47,7 @@ func (r *queryResolver) LatestIndex(ctx context.Context, did string, filter *mod if err != nil { return nil, err } - idx, err := r.EventService.GetLatestIndex(ctx, opts) + idx, err := r.EventService.GetLatestIndexAdvanced(ctx, opts) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (r *queryResolver) Indexes(ctx context.Context, did string, limit *int, fil if err != nil { return nil, err } - list, err := r.EventService.ListIndexes(ctx, resolveLimit(limit), opts) + list, err := r.EventService.ListIndexesAdvanced(ctx, resolveLimit(limit), opts) if err != nil { if errors.Is(err, sql.ErrNoRows) { return emptyCloudEventIndexList, nil @@ -80,7 +80,7 @@ func (r *queryResolver) LatestCloudEvent(ctx context.Context, did string, filter if err != nil { return nil, err } - idx, err := r.EventService.GetLatestIndex(ctx, opts) + idx, err := r.EventService.GetLatestIndexAdvanced(ctx, opts) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (r *queryResolver) CloudEvents(ctx context.Context, did string, limit *int, if err != nil { return nil, err } - list, err := r.EventService.ListIndexes(ctx, resolveLimit(limit), opts) + list, err := r.EventService.ListIndexesAdvanced(ctx, resolveLimit(limit), opts) if err != nil { if errors.Is(err, sql.ErrNoRows) { return emptyCloudEventList, nil diff --git a/internal/graph/convert.go b/internal/graph/convert.go index c1ad58c..c68820e 100644 --- a/internal/graph/convert.go +++ b/internal/graph/convert.go @@ -6,31 +6,36 @@ 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 tokenID to grpc.SearchOptions. -func filterToSearchOptions(filter *model.CloudEventFilter, subject cloudevent.ERC721DID) *grpc.SearchOptions { - opts := &grpc.SearchOptions{ - Subject: &wrapperspb.StringValue{Value: subject.String()}, +// filterToAdvancedSearchOptions converts a GraphQL filter and subject DID to grpc.AdvancedSearchOptions. +func filterToAdvancedSearchOptions(filter *model.CloudEventFilter, subject cloudevent.ERC721DID) *grpc.AdvancedSearchOptions { + opts := &grpc.AdvancedSearchOptions{ + Subject: &grpc.StringFilterOption{In: []string{subject.String()}}, } if filter == nil { return opts } if filter.ID != nil { - opts.Id = &wrapperspb.StringValue{Value: *filter.ID} + opts.Id = &grpc.StringFilterOption{In: []string{*filter.ID}} } if filter.Type != nil { - opts.Type = &wrapperspb.StringValue{Value: *filter.Type} + opts.Type = &grpc.StringFilterOption{In: []string{*filter.Type}} } + // dataversion (exact) and dataversions (list) are merged into a single In filter. + var dvIn []string if filter.Dataversion != nil { - opts.DataVersion = &wrapperspb.StringValue{Value: *filter.Dataversion} + dvIn = append(dvIn, *filter.Dataversion) + } + dvIn = append(dvIn, filter.Dataversions...) + if len(dvIn) > 0 { + opts.DataVersion = &grpc.StringFilterOption{In: dvIn} } 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) @@ -41,6 +46,7 @@ func filterToSearchOptions(filter *model.CloudEventFilter, subject cloudevent.ER return opts } + const defaultLimit = 10 // Preallocated empty slices for list resolvers to avoid allocating on sql.ErrNoRows. diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 61b79ed..bc6f94a 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -428,12 +428,14 @@ type CloudEventIndex { } """ -Filter for cloud event queries. +Filter for cloud event queries. """ input CloudEventFilter { id: String type: String dataversion: String + """Filter by one or more dataversion values (OR logic).""" + dataversions: [String!] source: String producer: String before: Time @@ -2882,7 +2884,7 @@ func (ec *executionContext) unmarshalInputCloudEventFilter(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"id", "type", "dataversion", "source", "producer", "before", "after"} + fieldsInOrder := [...]string{"id", "type", "dataversion", "dataversions", "source", "producer", "before", "after"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -2910,6 +2912,13 @@ func (ec *executionContext) unmarshalInputCloudEventFilter(ctx context.Context, return it, err } it.Dataversion = data + case "dataversions": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataversions")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Dataversions = data case "source": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("source")) data, err := ec.unmarshalOString2ᚖstring(ctx, v) @@ -4250,6 +4259,13 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } +func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + if v == nil { + return nil, nil + } + return ec.unmarshalNString2ᚕstringᚄ(ctx, v) +} + func (ec *executionContext) unmarshalOTime2ᚖtimeᚐTime(ctx context.Context, v any) (*time.Time, error) { if v == nil { return nil, nil diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 82fe636..fc1e213 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -10,13 +10,14 @@ import ( // Filter for cloud event queries. type CloudEventFilter struct { - ID *string `json:"id,omitempty"` - Type *string `json:"type,omitempty"` - Dataversion *string `json:"dataversion,omitempty"` - Source *string `json:"source,omitempty"` - Producer *string `json:"producer,omitempty"` - Before *time.Time `json:"before,omitempty"` - After *time.Time `json:"after,omitempty"` + ID *string `json:"id,omitempty"` + Type *string `json:"type,omitempty"` + Dataversion *string `json:"dataversion,omitempty"` + Dataversions []string `json:"dataversions,omitempty"` + Source *string `json:"source,omitempty"` + Producer *string `json:"producer,omitempty"` + Before *time.Time `json:"before,omitempty"` + After *time.Time `json:"after,omitempty"` } // Cloud event index entry: typed header + storage key. diff --git a/internal/graph/resolver.go b/internal/graph/resolver.go index d2a317f..b076830 100644 --- a/internal/graph/resolver.go +++ b/internal/graph/resolver.go @@ -47,8 +47,8 @@ func CheckVehicleRawDataByDID(ctx context.Context, did string) error { return nil } -// requireVehicleOptsByDID validates access by DID and returns search options for the vehicle. -func (r *queryResolver) requireVehicleOptsByDID(ctx context.Context, did string, filter *model.CloudEventFilter) (*grpc.SearchOptions, error) { +// requireVehicleOptsByDID validates access by DID and returns advanced search options for the vehicle. +func (r *queryResolver) requireVehicleOptsByDID(ctx context.Context, did string, filter *model.CloudEventFilter) (*grpc.AdvancedSearchOptions, error) { if err := CheckVehicleRawDataByDID(ctx, did); err != nil { return nil, err } @@ -56,5 +56,5 @@ func (r *queryResolver) requireVehicleOptsByDID(ctx context.Context, did string, if err != nil { return nil, fmt.Errorf("invalid DID: %w", err) } - return filterToSearchOptions(filter, subject), nil + return filterToAdvancedSearchOptions(filter, subject), nil } diff --git a/internal/graph/resolver_test.go b/internal/graph/resolver_test.go index 4483c7b..e621057 100644 --- a/internal/graph/resolver_test.go +++ b/internal/graph/resolver_test.go @@ -103,7 +103,7 @@ 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) { @@ -122,8 +122,8 @@ 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("unauthorized when token does not match DID", func(t *testing.T) { diff --git a/schema/base.graphqls b/schema/base.graphqls index cfc2f90..bf5c9ab 100644 --- a/schema/base.graphqls +++ b/schema/base.graphqls @@ -72,12 +72,14 @@ type CloudEventIndex { } """ -Filter for cloud event queries. +Filter for cloud event queries. """ input CloudEventFilter { id: String type: String dataversion: String + """Filter by one or more dataversion values (OR logic).""" + dataversions: [String!] source: String producer: String before: Time