From c33bd977194ddd62057e8b673f15d5761ab6dd0e Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Wed, 29 Apr 2026 12:05:44 -0400 Subject: [PATCH 1/4] VOids --- go.mod | 3 + go.sum | 2 - internal/fetch/rpc/rpc.go | 10 ++- internal/graph/base.resolvers.go | 20 ++--- internal/graph/convert.go | 8 ++ internal/graph/generated.go | 95 ++++++++++++++------- pkg/eventrepo/db_test.go | 15 ++++ pkg/eventrepo/event_repo_test.go | 137 ++++++++++++++++++++++++++++++- pkg/eventrepo/eventrepo.go | 75 ++++++++++++++--- schema/base.graphqls | 30 ++++--- 10 files changed, 325 insertions(+), 70 deletions(-) diff --git a/go.mod b/go.mod index 54f05eb..7657aa1 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/DIMO-Network/fetch-api go 1.25.0 +// temporary for local testing of dimo.tombstone work; remove before release. +replace github.com/DIMO-Network/cloudevent => ../cloudevent + require ( github.com/99designs/gqlgen v0.17.89 github.com/ClickHouse/clickhouse-go/v2 v2.43.0 diff --git a/go.sum b/go.sum index 2e640b7..4ee1f32 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DIMO-Network/clickhouse-infra v0.0.7 h1:TAsjkFFKu3D5Xg6dwBcRBryjCVSlXsNjVbTwJ4UDlTg= github.com/DIMO-Network/clickhouse-infra v0.0.7/go.mod h1:XS80lhSJNWBWGgZ+m4j7++zFj1wAXfmtV2gJfhGlabQ= -github.com/DIMO-Network/cloudevent v0.2.8 h1:Q0xGQVPlOshF2LSX/m15Qzi2n4BI0EQDgOM71gEbNsY= -github.com/DIMO-Network/cloudevent v0.2.8/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= github.com/DIMO-Network/server-garage v0.1.1 h1:EYmyy+Fgi2BNW0Bufn04BViDtb8BCWaN7C7BbEuoI5s= github.com/DIMO-Network/server-garage v0.1.1/go.mod h1:Z3A1KDUsXey+XhrPhmw/wyCidfrQvmEdWp7nShno7ZM= github.com/DIMO-Network/shared v1.1.7 h1:5Ex8bZ6BpOjcLj4u7n5Kih1Ho6b9BVJsKpKn4iU2EaM= diff --git a/internal/fetch/rpc/rpc.go b/internal/fetch/rpc/rpc.go index e5fcc9e..99f4e6f 100644 --- a/internal/fetch/rpc/rpc.go +++ b/internal/fetch/rpc/rpc.go @@ -36,7 +36,9 @@ func (s *Server) GetLatestIndex(ctx context.Context, req *grpc.GetLatestIndexReq var err error if req.GetAdvancedOptions() != nil { - index, err = s.eventService.GetLatestIndexAdvanced(ctx, req.GetAdvancedOptions()) + // gRPC callers see no tombstone suppression by default; the GraphQL + // includeDeleted flag is the public surface for opting in. + index, err = s.eventService.GetLatestIndexAdvanced(ctx, req.GetAdvancedOptions(), true) } else { index, err = s.eventService.GetLatestIndex(ctx, req.GetOptions()) } @@ -63,7 +65,7 @@ func (s *Server) ListIndexes(ctx context.Context, req *grpc.ListIndexesRequest) var err error if req.GetAdvancedOptions() != nil { - indexObjs, err = s.eventService.ListIndexesAdvanced(ctx, int(req.GetLimit()), req.GetAdvancedOptions()) + indexObjs, err = s.eventService.ListIndexesAdvanced(ctx, int(req.GetLimit()), req.GetAdvancedOptions(), true) } else { indexObjs, err = s.eventService.ListIndexes(ctx, int(req.GetLimit()), req.GetOptions()) } @@ -92,7 +94,7 @@ func (s *Server) ListCloudEvents(ctx context.Context, req *grpc.ListCloudEventsR var err error if req.GetAdvancedOptions() != nil { - metaList, err = s.eventService.ListIndexesAdvanced(ctx, int(req.GetLimit()), req.GetAdvancedOptions()) + metaList, err = s.eventService.ListIndexesAdvanced(ctx, int(req.GetLimit()), req.GetAdvancedOptions(), true) } else { metaList, err = s.eventService.ListIndexes(ctx, int(req.GetLimit()), req.GetOptions()) } @@ -121,7 +123,7 @@ func (s *Server) GetLatestCloudEvent(ctx context.Context, req *grpc.GetLatestClo var err error if req.GetAdvancedOptions() != nil { - metadata, err = s.eventService.GetLatestIndexAdvanced(ctx, req.GetAdvancedOptions()) + metadata, err = s.eventService.GetLatestIndexAdvanced(ctx, req.GetAdvancedOptions(), true) } else { metadata, err = s.eventService.GetLatestIndex(ctx, req.GetOptions()) } diff --git a/internal/graph/base.resolvers.go b/internal/graph/base.resolvers.go index 2daf409..54a770f 100644 --- a/internal/graph/base.resolvers.go +++ b/internal/graph/base.resolvers.go @@ -42,12 +42,12 @@ func (r *cloudEventResolver) DataBase64(ctx context.Context, obj *CloudEventWrap } // LatestIndex is the resolver for the latestIndex field. -func (r *queryResolver) LatestIndex(ctx context.Context, did string, filter *model.CloudEventFilter) (*model.CloudEventIndex, error) { +func (r *queryResolver) LatestIndex(ctx context.Context, did string, filter *model.CloudEventFilter, includeDeleted *bool) (*model.CloudEventIndex, error) { opts, err := r.requireSubjectOptsByDID(ctx, did, filter) if err != nil { return nil, err } - idx, err := r.EventService.GetLatestIndexAdvanced(ctx, opts) + idx, err := r.EventService.GetLatestIndexAdvanced(ctx, opts, resolveIncludeDeleted(includeDeleted)) if err != nil { return nil, err } @@ -55,12 +55,12 @@ func (r *queryResolver) LatestIndex(ctx context.Context, did string, filter *mod } // Indexes is the resolver for the indexes field. -func (r *queryResolver) Indexes(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter) ([]*model.CloudEventIndex, error) { +func (r *queryResolver) Indexes(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) ([]*model.CloudEventIndex, error) { opts, err := r.requireSubjectOptsByDID(ctx, did, filter) if err != nil { return nil, err } - list, err := r.EventService.ListIndexesAdvanced(ctx, resolveLimit(limit), opts) + list, err := r.EventService.ListIndexesAdvanced(ctx, resolveLimit(limit), opts, resolveIncludeDeleted(includeDeleted)) if err != nil { if errors.Is(err, sql.ErrNoRows) { return emptyCloudEventIndexList, nil @@ -75,12 +75,12 @@ func (r *queryResolver) Indexes(ctx context.Context, did string, limit *int, fil } // LatestCloudEvent is the resolver for the latestCloudEvent field. -func (r *queryResolver) LatestCloudEvent(ctx context.Context, did string, filter *model.CloudEventFilter) (*CloudEventWrapper, error) { +func (r *queryResolver) LatestCloudEvent(ctx context.Context, did string, filter *model.CloudEventFilter, includeDeleted *bool) (*CloudEventWrapper, error) { opts, err := r.requireSubjectOptsByDID(ctx, did, filter) if err != nil { return nil, err } - idx, err := r.EventService.GetLatestIndexAdvanced(ctx, opts) + idx, err := r.EventService.GetLatestIndexAdvanced(ctx, opts, resolveIncludeDeleted(includeDeleted)) if err != nil { return nil, err } @@ -100,12 +100,12 @@ func (r *queryResolver) LatestCloudEvent(ctx context.Context, did string, filter } // CloudEvents is the resolver for the cloudEvents field. -func (r *queryResolver) CloudEvents(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter) ([]*CloudEventWrapper, error) { +func (r *queryResolver) CloudEvents(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) ([]*CloudEventWrapper, error) { opts, err := r.requireSubjectOptsByDID(ctx, did, filter) if err != nil { return nil, err } - list, err := r.EventService.ListIndexesAdvanced(ctx, resolveLimit(limit), opts) + list, err := r.EventService.ListIndexesAdvanced(ctx, resolveLimit(limit), opts, resolveIncludeDeleted(includeDeleted)) if err != nil { if errors.Is(err, sql.ErrNoRows) { return emptyCloudEventList, nil @@ -146,12 +146,12 @@ func (r *queryResolver) CloudEvents(ctx context.Context, did string, limit *int, } // AvailableCloudEventTypes is the resolver for the availableCloudEventTypes field. -func (r *queryResolver) AvailableCloudEventTypes(ctx context.Context, did string, filter *model.CloudEventFilter) ([]*model.CloudEventTypeSummary, error) { +func (r *queryResolver) AvailableCloudEventTypes(ctx context.Context, did string, filter *model.CloudEventFilter, includeDeleted *bool) ([]*model.CloudEventTypeSummary, error) { opts, err := r.requireSubjectOptsByDID(ctx, did, filter) if err != nil { return nil, err } - summaries, err := r.EventService.GetCloudEventTypeSummariesAdvanced(ctx, opts) + summaries, err := r.EventService.GetCloudEventTypeSummariesAdvanced(ctx, opts, resolveIncludeDeleted(includeDeleted)) if err != nil { return nil, err } diff --git a/internal/graph/convert.go b/internal/graph/convert.go index c0e9593..e313e72 100644 --- a/internal/graph/convert.go +++ b/internal/graph/convert.go @@ -63,6 +63,14 @@ func resolveLimit(limit *int) int { return defaultLimit } +// resolveIncludeDeleted maps the GraphQL `includeDeleted` argument (which has +// a default of false in the schema, but may arrive as nil when the caller did +// not supply a value at all) to the eventrepo flag. False means tombstoned +// attestations are suppressed; true means they are returned. +func resolveIncludeDeleted(p *bool) bool { + return p != nil && *p +} + // indexToModel converts a CloudEvent index entry to the GraphQL model, // using the CloudEventHeader from the library directly. func indexToModel(idx cloudevent.CloudEvent[eventrepo.ObjectInfo]) *model.CloudEventIndex { diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 5c36e23..19b583e 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -74,11 +74,11 @@ type ComplexityRoot struct { } Query struct { - AvailableCloudEventTypes func(childComplexity int, did string, filter *model.CloudEventFilter) int - CloudEvents func(childComplexity int, did string, limit *int, filter *model.CloudEventFilter) int - Indexes func(childComplexity int, did string, limit *int, filter *model.CloudEventFilter) int - LatestCloudEvent func(childComplexity int, did string, filter *model.CloudEventFilter) int - LatestIndex func(childComplexity int, did string, filter *model.CloudEventFilter) int + AvailableCloudEventTypes func(childComplexity int, did string, filter *model.CloudEventFilter, includeDeleted *bool) int + CloudEvents func(childComplexity int, did string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) int + Indexes func(childComplexity int, did string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) int + LatestCloudEvent func(childComplexity int, did string, filter *model.CloudEventFilter, includeDeleted *bool) int + LatestIndex func(childComplexity int, did string, filter *model.CloudEventFilter, includeDeleted *bool) int } } @@ -88,11 +88,11 @@ type CloudEventResolver interface { DataBase64(ctx context.Context, obj *CloudEventWrapper) (*string, error) } type QueryResolver interface { - LatestIndex(ctx context.Context, did string, filter *model.CloudEventFilter) (*model.CloudEventIndex, error) - Indexes(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter) ([]*model.CloudEventIndex, error) - LatestCloudEvent(ctx context.Context, did string, filter *model.CloudEventFilter) (*CloudEventWrapper, error) - CloudEvents(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter) ([]*CloudEventWrapper, error) - AvailableCloudEventTypes(ctx context.Context, did string, filter *model.CloudEventFilter) ([]*model.CloudEventTypeSummary, error) + LatestIndex(ctx context.Context, did string, filter *model.CloudEventFilter, includeDeleted *bool) (*model.CloudEventIndex, error) + Indexes(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) ([]*model.CloudEventIndex, error) + LatestCloudEvent(ctx context.Context, did string, filter *model.CloudEventFilter, includeDeleted *bool) (*CloudEventWrapper, error) + CloudEvents(ctx context.Context, did string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) ([]*CloudEventWrapper, error) + AvailableCloudEventTypes(ctx context.Context, did string, filter *model.CloudEventFilter, includeDeleted *bool) ([]*model.CloudEventTypeSummary, error) } type executableSchema graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot] @@ -261,7 +261,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.AvailableCloudEventTypes(childComplexity, args["did"].(string), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.AvailableCloudEventTypes(childComplexity, args["did"].(string), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.cloudEvents": if e.ComplexityRoot.Query.CloudEvents == nil { break @@ -272,7 +272,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.CloudEvents(childComplexity, args["did"].(string), args["limit"].(*int), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.CloudEvents(childComplexity, args["did"].(string), args["limit"].(*int), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.indexes": if e.ComplexityRoot.Query.Indexes == nil { break @@ -283,7 +283,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.Indexes(childComplexity, args["did"].(string), args["limit"].(*int), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.Indexes(childComplexity, args["did"].(string), args["limit"].(*int), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.latestCloudEvent": if e.ComplexityRoot.Query.LatestCloudEvent == nil { @@ -295,7 +295,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.LatestCloudEvent(childComplexity, args["did"].(string), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.LatestCloudEvent(childComplexity, args["did"].(string), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.latestIndex": if e.ComplexityRoot.Query.LatestIndex == nil { break @@ -306,7 +306,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.LatestIndex(childComplexity, args["did"].(string), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.LatestIndex(childComplexity, args["did"].(string), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true } return 0, false @@ -420,9 +420,11 @@ The root query type for the Fetch API GraphQL schema. ERC721 DID (e.g. did:eth:c """ type Query { """ - Latest cloud event index matching filters. + Latest cloud event index matching filters. Tombstoned attestations are + hidden from the result by default; pass includeDeleted: true to disable + tombstone suppression. """ - latestIndex(did: String!, filter: CloudEventFilter): CloudEventIndex! + latestIndex(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEventIndex! @mcpTool( name: "get_latest_index" description: "Get the latest CloudEvent index entry (header + storage key) for a DID-scoped subject, optionally filtered by event type, source, producer, or time range. Returns metadata only — use fetch_get_latest_cloud_event for the full payload." @@ -430,9 +432,11 @@ type Query { ) """ - List cloud event indexes matching filters. + List cloud event indexes matching filters. Tombstoned attestations are + hidden from the result by default; pass includeDeleted: true to disable + tombstone suppression. """ - indexes(did: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEventIndex!]! + indexes(did: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventIndex!]! @mcpTool( name: "list_indexes" description: "List CloudEvent index entries for a DID-scoped subject, optionally filtered and limited (default 10). Returns headers + storage keys without payloads — cheaper than fetch_list_cloud_events when exploring." @@ -440,9 +444,11 @@ type Query { ) """ - Latest full cloud event. + Latest full cloud event. Tombstoned attestations are hidden from the + result by default; pass includeDeleted: true to disable tombstone + suppression. """ - latestCloudEvent(did: String!, filter: CloudEventFilter): CloudEvent! + latestCloudEvent(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEvent! @mcpTool( name: "get_latest_cloud_event" description: "Get the latest full CloudEvent (header + JSON payload) for a DID-scoped subject, optionally filtered. Returns dataUrl (presigned S3 link) for large binary payloads instead of inlining them." @@ -450,9 +456,11 @@ type Query { ) """ - List full cloud events. + List full cloud events. Tombstoned attestations are hidden from the + result by default; pass includeDeleted: true to disable tombstone + suppression. """ - cloudEvents(did: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEvent!]! + cloudEvents(did: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEvent!]! @mcpTool( name: "list_cloud_events" description: "List full CloudEvents (headers + JSON payloads) for a DID-scoped subject, optionally filtered and limited (default 10). Large binary payloads are returned as presigned S3 URLs via dataUrl instead of inlined data." @@ -460,9 +468,11 @@ type Query { ) """ - List cloud event types available for a subject, with counts and time ranges. + List cloud event types available for a subject, with counts and time + ranges. Tombstoned attestations are excluded from counts by default; + pass includeDeleted: true to disable tombstone suppression. """ - availableCloudEventTypes(did: String!, filter: CloudEventFilter): [CloudEventTypeSummary!]! + availableCloudEventTypes(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventTypeSummary!]! @mcpTool( name: "list_available_event_types" description: "Summarize the CloudEvent types present for a DID-scoped subject. Returns each type with its count, firstSeen, and lastSeen timestamps — useful for discovering what kinds of events exist before querying them." @@ -547,6 +557,11 @@ func (ec *executionContext) field_Query_availableCloudEventTypes_args(ctx contex return nil, err } args["filter"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeleted", ec.unmarshalOBoolean2ᚖbool) + if err != nil { + return nil, err + } + args["includeDeleted"] = arg2 return args, nil } @@ -568,6 +583,11 @@ func (ec *executionContext) field_Query_cloudEvents_args(ctx context.Context, ra return nil, err } args["filter"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeleted", ec.unmarshalOBoolean2ᚖbool) + if err != nil { + return nil, err + } + args["includeDeleted"] = arg3 return args, nil } @@ -589,6 +609,11 @@ func (ec *executionContext) field_Query_indexes_args(ctx context.Context, rawArg return nil, err } args["filter"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeleted", ec.unmarshalOBoolean2ᚖbool) + if err != nil { + return nil, err + } + args["includeDeleted"] = arg3 return args, nil } @@ -605,6 +630,11 @@ func (ec *executionContext) field_Query_latestCloudEvent_args(ctx context.Contex return nil, err } args["filter"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeleted", ec.unmarshalOBoolean2ᚖbool) + if err != nil { + return nil, err + } + args["includeDeleted"] = arg2 return args, nil } @@ -621,6 +651,11 @@ func (ec *executionContext) field_Query_latestIndex_args(ctx context.Context, ra return nil, err } args["filter"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeleted", ec.unmarshalOBoolean2ᚖbool) + if err != nil { + return nil, err + } + args["includeDeleted"] = arg2 return args, nil } @@ -1407,7 +1442,7 @@ func (ec *executionContext) _Query_latestIndex(ctx context.Context, field graphq ec.fieldContext_Query_latestIndex, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().LatestIndex(ctx, fc.Args["did"].(string), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().LatestIndex(ctx, fc.Args["did"].(string), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEventIndex2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋfetchᚑapiᚋinternalᚋgraphᚋmodelᚐCloudEventIndex, @@ -1454,7 +1489,7 @@ func (ec *executionContext) _Query_indexes(ctx context.Context, field graphql.Co ec.fieldContext_Query_indexes, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().Indexes(ctx, fc.Args["did"].(string), fc.Args["limit"].(*int), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().Indexes(ctx, fc.Args["did"].(string), fc.Args["limit"].(*int), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEventIndex2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋfetchᚑapiᚋinternalᚋgraphᚋmodelᚐCloudEventIndexᚄ, @@ -1501,7 +1536,7 @@ func (ec *executionContext) _Query_latestCloudEvent(ctx context.Context, field g ec.fieldContext_Query_latestCloudEvent, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().LatestCloudEvent(ctx, fc.Args["did"].(string), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().LatestCloudEvent(ctx, fc.Args["did"].(string), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEvent2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋfetchᚑapiᚋinternalᚋgraphᚐCloudEventWrapper, @@ -1552,7 +1587,7 @@ func (ec *executionContext) _Query_cloudEvents(ctx context.Context, field graphq ec.fieldContext_Query_cloudEvents, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().CloudEvents(ctx, fc.Args["did"].(string), fc.Args["limit"].(*int), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().CloudEvents(ctx, fc.Args["did"].(string), fc.Args["limit"].(*int), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEvent2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋfetchᚑapiᚋinternalᚋgraphᚐCloudEventWrapperᚄ, @@ -1603,7 +1638,7 @@ func (ec *executionContext) _Query_availableCloudEventTypes(ctx context.Context, ec.fieldContext_Query_availableCloudEventTypes, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().AvailableCloudEventTypes(ctx, fc.Args["did"].(string), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().AvailableCloudEventTypes(ctx, fc.Args["did"].(string), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEventTypeSummary2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋfetchᚑapiᚋinternalᚋgraphᚋmodelᚐCloudEventTypeSummaryᚄ, diff --git a/pkg/eventrepo/db_test.go b/pkg/eventrepo/db_test.go index ec98869..58cd808 100644 --- a/pkg/eventrepo/db_test.go +++ b/pkg/eventrepo/db_test.go @@ -68,3 +68,18 @@ func insertTestData(t *testing.T, ctx context.Context, conn clickhouse.Conn, ind require.NoError(t, err) return chindexer.CloudEventToObjectKey(index) } + +// insertTombstoneTestData inserts a tombstone row with the given voids_id. +// Returns the index_key. +func insertTombstoneTestData(t *testing.T, ctx context.Context, conn clickhouse.Conn, index *cloudevent.CloudEventHeader, voidsID string) string { + key := chindexer.CloudEventToObjectKey(index) + stored := &cloudevent.StoredEvent{ + RawEvent: cloudevent.RawEvent{CloudEventHeader: *index}, + VoidsID: voidsID, + } + values := chindexer.StoredEventToSlice(stored, key) + + err := conn.Exec(ctx, chindexer.InsertStmt, values...) + require.NoError(t, err) + return key +} diff --git a/pkg/eventrepo/event_repo_test.go b/pkg/eventrepo/event_repo_test.go index fca1089..662fe07 100644 --- a/pkg/eventrepo/event_repo_test.go +++ b/pkg/eventrepo/event_repo_test.go @@ -999,7 +999,7 @@ func TestListIndexesAdvanced(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - results, err := indexService.ListIndexesAdvanced(t.Context(), 10, tt.advancedOpts) + results, err := indexService.ListIndexesAdvanced(t.Context(), 10, tt.advancedOpts, true) if tt.expectedError { require.Error(t, err, "Expected error but got none") } else { @@ -1159,3 +1159,138 @@ func randAddress() common.Address { } return crypto.PubkeyToAddress(privateKey.PublicKey) } + +// TestTombstoneSuppression verifies that ListIndexesAdvanced honors +// includeDeleted: when false, attestations whose (source, id) appears as +// (source, voids_id) on a dimo.tombstone row for the same subject are +// hidden; when true, they come back. Mismatched-source and orphan-target +// tombstones never suppress. +func TestTombstoneSuppression(t *testing.T) { + t.Parallel() + chContainer := setupClickHouseContainer(t) + + conn, err := chContainer.GetClickHouseAsConn() + require.NoError(t, err) + ctx := context.Background() + now := time.Now() + + subject := cloudevent.ERC721DID{ + ChainID: 153, + ContractAddress: randAddress(), + TokenID: big.NewInt(7777), + }.String() + + // One attestation that will be tombstoned. + att := &cloudevent.CloudEventHeader{ + ID: "att-target", + Subject: subject, + Type: cloudevent.TypeAttestation, + Source: "0xSrcA", + Producer: "producer-A", + Time: now.Add(-3 * time.Hour), + } + insertTestData(t, ctx, conn, att) + + // Another attestation by the same source that will NOT be tombstoned. + attKept := &cloudevent.CloudEventHeader{ + ID: "att-kept", + Subject: subject, + Type: cloudevent.TypeAttestation, + Source: "0xSrcA", + Producer: "producer-A", + Time: now.Add(-2 * time.Hour), + } + insertTestData(t, ctx, conn, attKept) + + // Matching tombstone (same source, voids_id == att.ID) → suppresses att. + tombstone := &cloudevent.CloudEventHeader{ + ID: "tombstone-1", + Subject: subject, + Type: cloudevent.TypeAttestationTombstone, + Source: "0xSrcA", + Producer: "producer-A", + Time: now.Add(-1 * time.Hour), + } + insertTombstoneTestData(t, ctx, conn, tombstone, att.ID) + + // Mismatched-source tombstone (different source claims to void att) → MUST NOT suppress. + tombstoneWrongSource := &cloudevent.CloudEventHeader{ + ID: "tombstone-wrong-source", + Subject: subject, + Type: cloudevent.TypeAttestationTombstone, + Source: "0xSrcB", + Producer: "producer-B", + Time: now.Add(-50 * time.Minute), + } + insertTombstoneTestData(t, ctx, conn, tombstoneWrongSource, attKept.ID) + + // Orphan tombstone (target id doesn't exist) → harmless, doesn't suppress anything. + tombstoneOrphan := &cloudevent.CloudEventHeader{ + ID: "tombstone-orphan", + Subject: subject, + Type: cloudevent.TypeAttestationTombstone, + Source: "0xSrcA", + Producer: "producer-A", + Time: now.Add(-40 * time.Minute), + } + insertTombstoneTestData(t, ctx, conn, tombstoneOrphan, "no-such-attestation") + + indexService := eventrepo.New(conn, nil, nil, "", "") + + subjectFilter := &grpc.AdvancedSearchOptions{ + Subject: &grpc.StringFilterOption{In: []string{subject}}, + } + + t.Run("includeDeleted=false suppresses tombstoned attestations and only those", func(t *testing.T) { + // Filter to attestations only so we don't have to reason about tombstone rows. + opts := &grpc.AdvancedSearchOptions{ + Subject: subjectFilter.Subject, + Type: &grpc.StringFilterOption{In: []string{cloudevent.TypeAttestation}}, + } + results, err := indexService.ListIndexesAdvanced(ctx, 100, opts, false) + require.NoError(t, err) + + ids := make([]string, len(results)) + for i, r := range results { + ids[i] = r.ID + } + require.ElementsMatch(t, []string{attKept.ID}, ids, + "only the non-tombstoned attestation should come back; mismatched-source tombstone must NOT suppress attKept") + }) + + t.Run("includeDeleted=true returns tombstoned attestation as well", func(t *testing.T) { + opts := &grpc.AdvancedSearchOptions{ + Subject: subjectFilter.Subject, + Type: &grpc.StringFilterOption{In: []string{cloudevent.TypeAttestation}}, + } + results, err := indexService.ListIndexesAdvanced(ctx, 100, opts, true) + require.NoError(t, err) + + ids := make([]string, len(results)) + for i, r := range results { + ids[i] = r.ID + } + require.ElementsMatch(t, []string{att.ID, attKept.ID}, ids) + }) + + t.Run("includeDeleted=true plus tombstone type returns both attestations and tombstones", func(t *testing.T) { + opts := &grpc.AdvancedSearchOptions{ + Subject: subjectFilter.Subject, + Type: &grpc.StringFilterOption{In: []string{ + cloudevent.TypeAttestation, + cloudevent.TypeAttestationTombstone, + }}, + } + results, err := indexService.ListIndexesAdvanced(ctx, 100, opts, true) + require.NoError(t, err) + + ids := make([]string, len(results)) + for i, r := range results { + ids[i] = r.ID + } + require.ElementsMatch(t, + []string{att.ID, attKept.ID, tombstone.ID, tombstoneWrongSource.ID, tombstoneOrphan.ID}, + ids, + ) + }) +} diff --git a/pkg/eventrepo/eventrepo.go b/pkg/eventrepo/eventrepo.go index d2ca617..4d77b48 100644 --- a/pkg/eventrepo/eventrepo.go +++ b/pkg/eventrepo/eventrepo.go @@ -95,20 +95,24 @@ func (s *Service) PresignBlobURL(ctx context.Context, key string) (string, error } // GetLatestIndex returns the latest cloud event index that matches the given options. +// Tombstoned attestations are not suppressed; callers that want suppression should +// use GetLatestIndexAdvanced with includeDeleted=false. func (s *Service) GetLatestIndex(ctx context.Context, opts *grpc.SearchOptions) (cloudevent.CloudEvent[ObjectInfo], error) { advancedOpts := convertSearchOptionsToAdvanced(opts) - return s.GetLatestIndexAdvanced(ctx, advancedOpts) + return s.GetLatestIndexAdvanced(ctx, advancedOpts, true) } // GetLatestIndexAdvanced returns the latest cloud event index that matches the given advanced options. -func (s *Service) GetLatestIndexAdvanced(ctx context.Context, advancedOpts *grpc.AdvancedSearchOptions) (cloudevent.CloudEvent[ObjectInfo], error) { +// When includeDeleted is false, rows whose (source, id) appears in a dimo.tombstone +// row's voids_id for the same subject are suppressed. +func (s *Service) GetLatestIndexAdvanced(ctx context.Context, advancedOpts *grpc.AdvancedSearchOptions, includeDeleted bool) (cloudevent.CloudEvent[ObjectInfo], error) { // Only clone when we actually need to change TimestampAsc. opts := advancedOpts if advancedOpts != nil && advancedOpts.GetTimestampAsc().GetValue() { opts = proto.Clone(advancedOpts).(*grpc.AdvancedSearchOptions) opts.TimestampAsc = wrapperspb.Bool(false) } - events, err := s.ListIndexesAdvanced(ctx, 1, opts) + events, err := s.ListIndexesAdvanced(ctx, 1, opts, includeDeleted) if err != nil { return cloudevent.CloudEvent[ObjectInfo]{}, err } @@ -116,9 +120,11 @@ func (s *Service) GetLatestIndexAdvanced(ctx context.Context, advancedOpts *grpc } // ListIndexes fetches and returns a list of index for cloud events that match the given options. +// Tombstoned attestations are not suppressed; callers that want suppression should +// use ListIndexesAdvanced with includeDeleted=false. func (s *Service) ListIndexes(ctx context.Context, limit int, opts *grpc.SearchOptions) ([]cloudevent.CloudEvent[ObjectInfo], error) { advancedOpts := convertSearchOptionsToAdvanced(opts) - return s.ListIndexesAdvanced(ctx, limit, advancedOpts) + return s.ListIndexesAdvanced(ctx, limit, advancedOpts, true) } // maxQueryLimit is the maximum number of rows a single query may return. @@ -126,7 +132,9 @@ func (s *Service) ListIndexes(ctx context.Context, limit int, opts *grpc.SearchO const maxQueryLimit = 1000 // ListIndexesAdvanced fetches and returns a list of index for cloud events that match the given advanced options. -func (s *Service) ListIndexesAdvanced(ctx context.Context, limit int, advancedOpts *grpc.AdvancedSearchOptions) ([]cloudevent.CloudEvent[ObjectInfo], error) { +// When includeDeleted is false, rows tombstoned by a dimo.tombstone event with a matching +// (source, target_id) pair for the same subject are suppressed. +func (s *Service) ListIndexesAdvanced(ctx context.Context, limit int, advancedOpts *grpc.AdvancedSearchOptions, includeDeleted bool) ([]cloudevent.CloudEvent[ObjectInfo], error) { if limit <= 0 { limit = 1 } @@ -160,6 +168,11 @@ func (s *Service) ListIndexesAdvanced(ctx context.Context, limit int, advancedOp advancedMods := AdvancedSearchOptionsToQueryMod(advancedOpts) mods = append(mods, advancedMods...) } + if !includeDeleted { + if mod := voidsSuppressionMod(advancedOpts); mod != nil { + mods = append(mods, mod) + } + } query, args := newQuery(mods...) rows, err := s.chConn.Query(ctx, query, args...) if err != nil { @@ -204,12 +217,16 @@ type CloudEventTypeSummary struct { } // GetCloudEventTypeSummaries returns per-type counts and time ranges for the given search options. +// Tombstoned attestations are not suppressed; callers that want suppression should +// use GetCloudEventTypeSummariesAdvanced with includeDeleted=false. func (s *Service) GetCloudEventTypeSummaries(ctx context.Context, opts *grpc.SearchOptions) ([]CloudEventTypeSummary, error) { - return s.GetCloudEventTypeSummariesAdvanced(ctx, convertSearchOptionsToAdvanced(opts)) + return s.GetCloudEventTypeSummariesAdvanced(ctx, convertSearchOptionsToAdvanced(opts), true) } // GetCloudEventTypeSummariesAdvanced returns event type summaries filtered by advanced search options. -func (s *Service) GetCloudEventTypeSummariesAdvanced(ctx context.Context, advancedOpts *grpc.AdvancedSearchOptions) ([]CloudEventTypeSummary, error) { +// When includeDeleted is false, rows tombstoned by a dimo.tombstone event with a matching +// (source, target_id) pair for the same subject are excluded from the counts. +func (s *Service) GetCloudEventTypeSummariesAdvanced(ctx context.Context, advancedOpts *grpc.AdvancedSearchOptions, includeDeleted bool) ([]CloudEventTypeSummary, error) { mods := []qm.QueryMod{ qm.Select( chindexer.TypeColumn, @@ -225,6 +242,11 @@ func (s *Service) GetCloudEventTypeSummariesAdvanced(ctx context.Context, advanc if advancedOpts != nil { mods = append(mods, AdvancedSearchOptionsToQueryMod(advancedOpts)...) } + if !includeDeleted { + if mod := voidsSuppressionMod(advancedOpts); mod != nil { + mods = append(mods, mod) + } + } query, args := newQuery(mods...) rows, err := s.chConn.Query(ctx, query, args...) @@ -252,14 +274,16 @@ func (s *Service) GetCloudEventTypeSummariesAdvanced(ctx context.Context, advanc } // ListCloudEvents fetches and returns the cloud events that match the given options. +// Tombstoned attestations are not suppressed; callers that want suppression should +// use ListCloudEventsAdvanced with includeDeleted=false. func (s *Service) ListCloudEvents(ctx context.Context, bucketName string, limit int, opts *grpc.SearchOptions) ([]cloudevent.RawEvent, error) { advancedOpts := convertSearchOptionsToAdvanced(opts) - return s.ListCloudEventsAdvanced(ctx, bucketName, limit, advancedOpts) + return s.ListCloudEventsAdvanced(ctx, bucketName, limit, advancedOpts, true) } // ListCloudEventsAdvanced fetches and returns the cloud events that match the given advanced options. -func (s *Service) ListCloudEventsAdvanced(ctx context.Context, bucketName string, limit int, advancedOpts *grpc.AdvancedSearchOptions) ([]cloudevent.RawEvent, error) { - events, err := s.ListIndexesAdvanced(ctx, limit, advancedOpts) +func (s *Service) ListCloudEventsAdvanced(ctx context.Context, bucketName string, limit int, advancedOpts *grpc.AdvancedSearchOptions, includeDeleted bool) ([]cloudevent.RawEvent, error) { + events, err := s.ListIndexesAdvanced(ctx, limit, advancedOpts, includeDeleted) if err != nil { return nil, err } @@ -272,14 +296,16 @@ func (s *Service) ListCloudEventsAdvanced(ctx context.Context, bucketName string } // GetLatestCloudEvent fetches and returns the latest cloud event that matches the given options. +// Tombstoned attestations are not suppressed; callers that want suppression should +// use GetLatestCloudEventAdvanced with includeDeleted=false. func (s *Service) GetLatestCloudEvent(ctx context.Context, bucketName string, opts *grpc.SearchOptions) (cloudevent.RawEvent, error) { advancedOpts := convertSearchOptionsToAdvanced(opts) - return s.GetLatestCloudEventAdvanced(ctx, bucketName, advancedOpts) + return s.GetLatestCloudEventAdvanced(ctx, bucketName, advancedOpts, true) } // GetLatestCloudEventAdvanced fetches and returns the latest cloud event that matches the given advanced options. -func (s *Service) GetLatestCloudEventAdvanced(ctx context.Context, bucketName string, advancedOpts *grpc.AdvancedSearchOptions) (cloudevent.RawEvent, error) { - cloudIdx, err := s.GetLatestIndexAdvanced(ctx, advancedOpts) +func (s *Service) GetLatestCloudEventAdvanced(ctx context.Context, bucketName string, advancedOpts *grpc.AdvancedSearchOptions, includeDeleted bool) (cloudevent.RawEvent, error) { + cloudIdx, err := s.GetLatestIndexAdvanced(ctx, advancedOpts, includeDeleted) if err != nil { return cloudevent.RawEvent{}, err } @@ -601,6 +627,29 @@ func convertSearchOptionsToAdvanced(opts *grpc.SearchOptions) *grpc.AdvancedSear return advanced } +// voidsSuppressionMod returns a QueryMod that excludes rows whose (source, id) +// pair appears as (source, voids_id) on a dimo.tombstone row for the same +// subject. Returns nil when no subject is constrained on opts (e.g. a global +// query); without a subject anchor the inner subquery would scan the whole +// table, which we don't want to do silently. +func voidsSuppressionMod(opts *grpc.AdvancedSearchOptions) qm.QueryMod { + if opts == nil { + return nil + } + subjects := opts.GetSubject().GetIn() + if len(subjects) == 0 { + return nil + } + clause := "(" + chindexer.SourceColumn + ", " + chindexer.IDColumn + ") NOT IN (" + + "SELECT " + chindexer.SourceColumn + ", " + chindexer.VoidsIDColumn + + " FROM " + chindexer.TableName + + " WHERE " + chindexer.SubjectColumn + " IN (?)" + + " AND " + chindexer.TypeColumn + " = ?" + + " AND " + chindexer.VoidsIDColumn + " != ''" + + ")" + return qm.Where(clause, subjects, cloudevent.TypeAttestationTombstone) +} + func AdvancedSearchOptionsToQueryMod(opts *grpc.AdvancedSearchOptions) []qm.QueryMod { if opts == nil { return nil diff --git a/schema/base.graphqls b/schema/base.graphqls index 27e0d83..9c9f973 100644 --- a/schema/base.graphqls +++ b/schema/base.graphqls @@ -41,9 +41,11 @@ The root query type for the Fetch API GraphQL schema. ERC721 DID (e.g. did:eth:c """ type Query { """ - Latest cloud event index matching filters. + Latest cloud event index matching filters. Tombstoned attestations are + hidden from the result by default; pass includeDeleted: true to disable + tombstone suppression. """ - latestIndex(did: String!, filter: CloudEventFilter): CloudEventIndex! + latestIndex(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEventIndex! @mcpTool( name: "get_latest_index" description: "Get the latest CloudEvent index entry (header + storage key) for a DID-scoped subject, optionally filtered by event type, source, producer, or time range. Returns metadata only — use fetch_get_latest_cloud_event for the full payload." @@ -51,9 +53,11 @@ type Query { ) """ - List cloud event indexes matching filters. + List cloud event indexes matching filters. Tombstoned attestations are + hidden from the result by default; pass includeDeleted: true to disable + tombstone suppression. """ - indexes(did: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEventIndex!]! + indexes(did: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventIndex!]! @mcpTool( name: "list_indexes" description: "List CloudEvent index entries for a DID-scoped subject, optionally filtered and limited (default 10). Returns headers + storage keys without payloads — cheaper than fetch_list_cloud_events when exploring." @@ -61,9 +65,11 @@ type Query { ) """ - Latest full cloud event. + Latest full cloud event. Tombstoned attestations are hidden from the + result by default; pass includeDeleted: true to disable tombstone + suppression. """ - latestCloudEvent(did: String!, filter: CloudEventFilter): CloudEvent! + latestCloudEvent(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEvent! @mcpTool( name: "get_latest_cloud_event" description: "Get the latest full CloudEvent (header + JSON payload) for a DID-scoped subject, optionally filtered. Returns dataUrl (presigned S3 link) for large binary payloads instead of inlining them." @@ -71,9 +77,11 @@ type Query { ) """ - List full cloud events. + List full cloud events. Tombstoned attestations are hidden from the + result by default; pass includeDeleted: true to disable tombstone + suppression. """ - cloudEvents(did: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEvent!]! + cloudEvents(did: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEvent!]! @mcpTool( name: "list_cloud_events" description: "List full CloudEvents (headers + JSON payloads) for a DID-scoped subject, optionally filtered and limited (default 10). Large binary payloads are returned as presigned S3 URLs via dataUrl instead of inlined data." @@ -81,9 +89,11 @@ type Query { ) """ - List cloud event types available for a subject, with counts and time ranges. + List cloud event types available for a subject, with counts and time + ranges. Tombstoned attestations are excluded from counts by default; + pass includeDeleted: true to disable tombstone suppression. """ - availableCloudEventTypes(did: String!, filter: CloudEventFilter): [CloudEventTypeSummary!]! + availableCloudEventTypes(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventTypeSummary!]! @mcpTool( name: "list_available_event_types" description: "Summarize the CloudEvent types present for a DID-scoped subject. Returns each type with its count, firstSeen, and lastSeen timestamps — useful for discovering what kinds of events exist before querying them." From 5bd3c753cd6873706464ee34cd4e8ba7af37e280 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Wed, 29 Apr 2026 12:06:46 -0400 Subject: [PATCH 2/4] Generate --- internal/graph/mcp_tools_gen.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/graph/mcp_tools_gen.go b/internal/graph/mcp_tools_gen.go index d01e01d..4c75ec4 100644 --- a/internal/graph/mcp_tools_gen.go +++ b/internal/graph/mcp_tools_gen.go @@ -15,8 +15,9 @@ var MCPTools = []mcpserver.ToolDefinition{ Args: []mcpserver.ArgDefinition{ {Name: "did", Type: "string", Description: "did (String!, required)", Required: true, ItemsType: ""}, {Name: "filter", Type: "object", Description: "filter (CloudEventFilter, optional)", Required: false, ItemsType: ""}, + {Name: "includeDeleted", Type: "boolean", Description: "includeDeleted (Boolean, optional)", Required: false, ItemsType: ""}, }, - Query: "query($did: String!, $filter: CloudEventFilter) { latestIndex(did: $did, filter: $filter) { header { type source subject id time producer dataversion } indexKey } }", + Query: "query($did: String!, $filter: CloudEventFilter, $includeDeleted: Boolean) { latestIndex(did: $did, filter: $filter, includeDeleted: $includeDeleted) { header { type source subject id time producer dataversion } indexKey } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -31,8 +32,9 @@ var MCPTools = []mcpserver.ToolDefinition{ {Name: "did", Type: "string", Description: "did (String!, required)", Required: true, ItemsType: ""}, {Name: "limit", Type: "integer", Description: "limit (Int, optional)", Required: false, ItemsType: ""}, {Name: "filter", Type: "object", Description: "filter (CloudEventFilter, optional)", Required: false, ItemsType: ""}, + {Name: "includeDeleted", Type: "boolean", Description: "includeDeleted (Boolean, optional)", Required: false, ItemsType: ""}, }, - Query: "query($did: String!, $limit: Int, $filter: CloudEventFilter) { indexes(did: $did, limit: $limit, filter: $filter) { header { type source subject id time producer } indexKey } }", + Query: "query($did: String!, $limit: Int, $filter: CloudEventFilter, $includeDeleted: Boolean) { indexes(did: $did, limit: $limit, filter: $filter, includeDeleted: $includeDeleted) { header { type source subject id time producer } indexKey } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -46,8 +48,9 @@ var MCPTools = []mcpserver.ToolDefinition{ Args: []mcpserver.ArgDefinition{ {Name: "did", Type: "string", Description: "did (String!, required)", Required: true, ItemsType: ""}, {Name: "filter", Type: "object", Description: "filter (CloudEventFilter, optional)", Required: false, ItemsType: ""}, + {Name: "includeDeleted", Type: "boolean", Description: "includeDeleted (Boolean, optional)", Required: false, ItemsType: ""}, }, - Query: "query($did: String!, $filter: CloudEventFilter) { latestCloudEvent(did: $did, filter: $filter) { header { type source subject id time producer } data dataUrl } }", + Query: "query($did: String!, $filter: CloudEventFilter, $includeDeleted: Boolean) { latestCloudEvent(did: $did, filter: $filter, includeDeleted: $includeDeleted) { header { type source subject id time producer } data dataUrl } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -62,8 +65,9 @@ var MCPTools = []mcpserver.ToolDefinition{ {Name: "did", Type: "string", Description: "did (String!, required)", Required: true, ItemsType: ""}, {Name: "limit", Type: "integer", Description: "limit (Int, optional)", Required: false, ItemsType: ""}, {Name: "filter", Type: "object", Description: "filter (CloudEventFilter, optional)", Required: false, ItemsType: ""}, + {Name: "includeDeleted", Type: "boolean", Description: "includeDeleted (Boolean, optional)", Required: false, ItemsType: ""}, }, - Query: "query($did: String!, $limit: Int, $filter: CloudEventFilter) { cloudEvents(did: $did, limit: $limit, filter: $filter) { header { type source subject id time producer } data dataUrl } }", + Query: "query($did: String!, $limit: Int, $filter: CloudEventFilter, $includeDeleted: Boolean) { cloudEvents(did: $did, limit: $limit, filter: $filter, includeDeleted: $includeDeleted) { header { type source subject id time producer } data dataUrl } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -77,8 +81,9 @@ var MCPTools = []mcpserver.ToolDefinition{ Args: []mcpserver.ArgDefinition{ {Name: "did", Type: "string", Description: "did (String!, required)", Required: true, ItemsType: ""}, {Name: "filter", Type: "object", Description: "filter (CloudEventFilter, optional)", Required: false, ItemsType: ""}, + {Name: "includeDeleted", Type: "boolean", Description: "includeDeleted (Boolean, optional)", Required: false, ItemsType: ""}, }, - Query: "query($did: String!, $filter: CloudEventFilter) { availableCloudEventTypes(did: $did, filter: $filter) { type count firstSeen lastSeen } }", + Query: "query($did: String!, $filter: CloudEventFilter, $includeDeleted: Boolean) { availableCloudEventTypes(did: $did, filter: $filter, includeDeleted: $includeDeleted) { type count firstSeen lastSeen } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -88,4 +93,4 @@ var MCPTools = []mcpserver.ToolDefinition{ }, } -var CondensedSchema = "scalar JSON # Arbitrary JSON value; serialized as raw JSON (object/array), not an escaped string.\nscalar Time # A point in time, encoded per RFC-3339.\n\ntype Query {\n \"Latest cloud event index matching filters.\"\n latestIndex(did: String!, filter: CloudEventFilter): CloudEventIndex!\n\n indexes(did: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEventIndex!]!\n \"Latest full cloud event.\"\n latestCloudEvent(did: String!, filter: CloudEventFilter): CloudEvent!\n\n cloudEvents(did: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEvent!]!\n availableCloudEventTypes(did: String!, filter: CloudEventFilter): [CloudEventTypeSummary!]!\n}\n\ntype CloudEvent { header: CloudEventHeader!, data: JSON, dataBase64: String, dataUrl: String }\n\ninput CloudEventFilter { id: String, type: String, types: [String!], dataversion: String, source: String, producer: String, before: Time, after: Time }\n\ntype CloudEventHeader { specversion: String!, type: String!, source: String!, subject: String!, id: String!, time: Time!, datacontenttype: String, dataschema: String, dataversion: String, producer: String!, signature: String, raweventid: String, tags: [String!]! }\n\ntype CloudEventIndex { header: CloudEventHeader!, indexKey: String! }\n\ntype CloudEventTypeSummary { type: String!, count: Int!, firstSeen: Time!, lastSeen: Time! }\n" +var CondensedSchema = "scalar JSON # Arbitrary JSON value; serialized as raw JSON (object/array), not an escaped string.\nscalar Time # A point in time, encoded per RFC-3339.\n\ntype Query {\n \"Latest cloud event index matching filters. Tombstoned attestations are hidden from the result by default; pass includeDeleted: true to disable tombstone suppression.\"\n latestIndex(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEventIndex!\n\n indexes(did: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventIndex!]!\n \"Latest full cloud event. Tombstoned attestations are hidden from the result by default; pass includeDeleted: true to disable tombstone suppression.\"\n latestCloudEvent(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEvent!\n\n cloudEvents(did: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEvent!]!\n availableCloudEventTypes(did: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventTypeSummary!]!\n}\n\ntype CloudEvent { header: CloudEventHeader!, data: JSON, dataBase64: String, dataUrl: String }\n\ninput CloudEventFilter { id: String, type: String, types: [String!], dataversion: String, source: String, producer: String, before: Time, after: Time }\n\ntype CloudEventHeader { specversion: String!, type: String!, source: String!, subject: String!, id: String!, time: Time!, datacontenttype: String, dataschema: String, dataversion: String, producer: String!, signature: String, raweventid: String, tags: [String!]! }\n\ntype CloudEventIndex { header: CloudEventHeader!, indexKey: String! }\n\ntype CloudEventTypeSummary { type: String!, count: Int!, firstSeen: Time!, lastSeen: Time! }\n" From fb377a09e4a69a9d3ad470e4ec3603568970e067 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Wed, 29 Apr 2026 13:29:43 -0400 Subject: [PATCH 3/4] Use a real cloudevent commit --- go.mod | 5 +---- go.sum | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 7657aa1..31220ce 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,11 @@ module github.com/DIMO-Network/fetch-api go 1.25.0 -// temporary for local testing of dimo.tombstone work; remove before release. -replace github.com/DIMO-Network/cloudevent => ../cloudevent - require ( github.com/99designs/gqlgen v0.17.89 github.com/ClickHouse/clickhouse-go/v2 v2.43.0 github.com/DIMO-Network/clickhouse-infra v0.0.7 - github.com/DIMO-Network/cloudevent v0.2.8 + github.com/DIMO-Network/cloudevent v0.2.9-0.20260429154543-207c0df87d44 github.com/DIMO-Network/server-garage v0.1.1 github.com/DIMO-Network/shared v1.1.7 github.com/DIMO-Network/token-exchange-api v0.4.0 diff --git a/go.sum b/go.sum index 4ee1f32..28d8817 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DIMO-Network/clickhouse-infra v0.0.7 h1:TAsjkFFKu3D5Xg6dwBcRBryjCVSlXsNjVbTwJ4UDlTg= github.com/DIMO-Network/clickhouse-infra v0.0.7/go.mod h1:XS80lhSJNWBWGgZ+m4j7++zFj1wAXfmtV2gJfhGlabQ= +github.com/DIMO-Network/cloudevent v0.2.9-0.20260429154543-207c0df87d44 h1:lzpO98VaF/0zIXW0nk4+PWShPTXCmxJlvGUBUo1gnhQ= +github.com/DIMO-Network/cloudevent v0.2.9-0.20260429154543-207c0df87d44/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= github.com/DIMO-Network/server-garage v0.1.1 h1:EYmyy+Fgi2BNW0Bufn04BViDtb8BCWaN7C7BbEuoI5s= github.com/DIMO-Network/server-garage v0.1.1/go.mod h1:Z3A1KDUsXey+XhrPhmw/wyCidfrQvmEdWp7nShno7ZM= github.com/DIMO-Network/shared v1.1.7 h1:5Ex8bZ6BpOjcLj4u7n5Kih1Ho6b9BVJsKpKn4iU2EaM= From e4585eaa3226539545d19376220f04c518a2b568 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Wed, 29 Apr 2026 14:12:11 -0400 Subject: [PATCH 4/4] Use a proper cloudevent tag --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 31220ce..dcb1925 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/99designs/gqlgen v0.17.89 github.com/ClickHouse/clickhouse-go/v2 v2.43.0 github.com/DIMO-Network/clickhouse-infra v0.0.7 - github.com/DIMO-Network/cloudevent v0.2.9-0.20260429154543-207c0df87d44 + github.com/DIMO-Network/cloudevent v0.2.9 github.com/DIMO-Network/server-garage v0.1.1 github.com/DIMO-Network/shared v1.1.7 github.com/DIMO-Network/token-exchange-api v0.4.0 diff --git a/go.sum b/go.sum index 28d8817..fa01263 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DIMO-Network/clickhouse-infra v0.0.7 h1:TAsjkFFKu3D5Xg6dwBcRBryjCVSlXsNjVbTwJ4UDlTg= github.com/DIMO-Network/clickhouse-infra v0.0.7/go.mod h1:XS80lhSJNWBWGgZ+m4j7++zFj1wAXfmtV2gJfhGlabQ= -github.com/DIMO-Network/cloudevent v0.2.9-0.20260429154543-207c0df87d44 h1:lzpO98VaF/0zIXW0nk4+PWShPTXCmxJlvGUBUo1gnhQ= -github.com/DIMO-Network/cloudevent v0.2.9-0.20260429154543-207c0df87d44/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= +github.com/DIMO-Network/cloudevent v0.2.9 h1:8MxbZtNReMHbwPUGQc2+G5THf08dFxju+npKZFZygJc= +github.com/DIMO-Network/cloudevent v0.2.9/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= github.com/DIMO-Network/server-garage v0.1.1 h1:EYmyy+Fgi2BNW0Bufn04BViDtb8BCWaN7C7BbEuoI5s= github.com/DIMO-Network/server-garage v0.1.1/go.mod h1:Z3A1KDUsXey+XhrPhmw/wyCidfrQvmEdWp7nShno7ZM= github.com/DIMO-Network/shared v1.1.7 h1:5Ex8bZ6BpOjcLj4u7n5Kih1Ho6b9BVJsKpKn4iU2EaM=