From 964dd8854a1faa6faaafae147627b6f52d00dc55 Mon Sep 17 00:00:00 2001 From: Dylan Moreland Date: Sat, 2 May 2026 11:52:07 -0400 Subject: [PATCH] Hide events based on new tombstone rows These events of type dimo.tombstone refer to files with the new voids_id column. By default, we hide voided files in the results. --- go.mod | 2 +- go.sum | 4 +- internal/fetch/rpc/rpc.go | 10 ++-- internal/graph/cloud_events.resolvers.go | 12 ++-- internal/graph/convert.go | 8 +++ internal/graph/generated.go | 53 ++++++++++++----- internal/graph/mcp_tools_gen.go | 11 ++-- pkg/eventrepo/event_repo_test.go | 2 +- pkg/eventrepo/eventrepo.go | 75 ++++++++++++++++++++---- schema/cloud_events.graphqls | 14 +++-- 10 files changed, 140 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 0663c8b..cbab6e7 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.45.0 github.com/DIMO-Network/clickhouse-infra v0.0.8 - github.com/DIMO-Network/cloudevent v0.2.8 + github.com/DIMO-Network/cloudevent v0.2.9 github.com/DIMO-Network/model-garage v1.0.11 github.com/DIMO-Network/server-garage v0.1.1 github.com/DIMO-Network/shared v1.1.7 diff --git a/go.sum b/go.sum index 76fb9c7..1b4128b 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,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.8 h1:54HPXKvNjmn9T0d9ZLYgUm4DnwTavE+Z8admrf39mJI= github.com/DIMO-Network/clickhouse-infra v0.0.8/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/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/model-garage v1.0.11 h1:aLvIyeo58p9pVgz+d3DnU5k5Fxvxh6mq/jE2s3LxXoc= github.com/DIMO-Network/model-garage v1.0.11/go.mod h1:oi7EGKQVxFVpXRsu2H+YbizbKcx06aQg2N1Yu4GqOp8= github.com/DIMO-Network/server-garage v0.1.1 h1:EYmyy+Fgi2BNW0Bufn04BViDtb8BCWaN7C7BbEuoI5s= diff --git a/internal/fetch/rpc/rpc.go b/internal/fetch/rpc/rpc.go index ae257fa..703105b 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/cloud_events.resolvers.go b/internal/graph/cloud_events.resolvers.go index d636b1a..7a02e4e 100644 --- a/internal/graph/cloud_events.resolvers.go +++ b/internal/graph/cloud_events.resolvers.go @@ -41,12 +41,12 @@ func (r *cloudEventResolver) DataBase64(ctx context.Context, obj *CloudEventWrap } // LatestCloudEvent is the resolver for the latestCloudEvent field. -func (r *queryResolver) LatestCloudEvent(ctx context.Context, subject string, filter *model.CloudEventFilter) (*CloudEventWrapper, error) { +func (r *queryResolver) LatestCloudEvent(ctx context.Context, subject string, filter *model.CloudEventFilter, includeDeleted *bool) (*CloudEventWrapper, error) { opts, err := r.requireSubjectOptsByDID(ctx, subject, 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 } @@ -69,12 +69,12 @@ func (r *queryResolver) LatestCloudEvent(ctx context.Context, subject string, fi } // CloudEvents is the resolver for the cloudEvents field. -func (r *queryResolver) CloudEvents(ctx context.Context, subject string, limit *int, filter *model.CloudEventFilter) ([]*CloudEventWrapper, error) { +func (r *queryResolver) CloudEvents(ctx context.Context, subject string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) ([]*CloudEventWrapper, error) { opts, err := r.requireSubjectOptsByDID(ctx, subject, 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 @@ -118,12 +118,12 @@ func (r *queryResolver) CloudEvents(ctx context.Context, subject string, limit * } // AvailableCloudEventTypes is the resolver for the availableCloudEventTypes field. -func (r *queryResolver) AvailableCloudEventTypes(ctx context.Context, subject string, filter *model.CloudEventFilter) ([]*model.CloudEventTypeSummary, error) { +func (r *queryResolver) AvailableCloudEventTypes(ctx context.Context, subject string, filter *model.CloudEventFilter, includeDeleted *bool) ([]*model.CloudEventTypeSummary, error) { opts, err := r.requireSubjectOptsByDID(ctx, subject, 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 6b1d4b7..f6b27cc 100644 --- a/internal/graph/convert.go +++ b/internal/graph/convert.go @@ -58,3 +58,11 @@ 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 +} diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 749f727..c0520d6 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -128,13 +128,13 @@ type ComplexityRoot struct { } Query struct { - AvailableCloudEventTypes func(childComplexity int, subject string, filter *model.CloudEventFilter) int + AvailableCloudEventTypes func(childComplexity int, subject string, filter *model.CloudEventFilter, includeDeleted *bool) int AvailableSignals func(childComplexity int, subject string, filter *model.SignalFilter) int - CloudEvents func(childComplexity int, subject string, limit *int, filter *model.CloudEventFilter) int + CloudEvents func(childComplexity int, subject string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) int DailyActivity func(childComplexity 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, subject string, filter *model.SignalFilter) int Events func(childComplexity int, subject string, from time.Time, to time.Time, filter *model.EventFilter) int - LatestCloudEvent func(childComplexity int, subject string, filter *model.CloudEventFilter) int + LatestCloudEvent func(childComplexity int, subject string, filter *model.CloudEventFilter, includeDeleted *bool) int Segments func(childComplexity 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, subject string, interval string, from time.Time, to time.Time, filter *model.SignalFilter) int SignalsLatest func(childComplexity int, subject string, filter *model.SignalFilter) int @@ -438,9 +438,9 @@ type QueryResolver interface { AvailableSignals(ctx context.Context, subject string, filter *model.SignalFilter) ([]string, error) DataSummary(ctx context.Context, subject string, filter *model.SignalFilter) (*model.DataSummary, error) SignalsSnapshot(ctx context.Context, subject string, filter *model.SignalFilter) (*model.SignalsSnapshotResponse, error) - LatestCloudEvent(ctx context.Context, subject string, filter *model.CloudEventFilter) (*CloudEventWrapper, error) - CloudEvents(ctx context.Context, subject string, limit *int, filter *model.CloudEventFilter) ([]*CloudEventWrapper, error) - AvailableCloudEventTypes(ctx context.Context, subject string, filter *model.CloudEventFilter) ([]*model.CloudEventTypeSummary, error) + LatestCloudEvent(ctx context.Context, subject string, filter *model.CloudEventFilter, includeDeleted *bool) (*CloudEventWrapper, error) + CloudEvents(ctx context.Context, subject string, limit *int, filter *model.CloudEventFilter, includeDeleted *bool) ([]*CloudEventWrapper, error) + AvailableCloudEventTypes(ctx context.Context, subject string, filter *model.CloudEventFilter, includeDeleted *bool) ([]*model.CloudEventTypeSummary, error) Events(ctx context.Context, subject string, from time.Time, to time.Time, filter *model.EventFilter) ([]*model.Event, error) Segments(ctx context.Context, 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, 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) @@ -911,7 +911,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.AvailableCloudEventTypes(childComplexity, args["subject"].(string), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.AvailableCloudEventTypes(childComplexity, args["subject"].(string), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.availableSignals": if e.ComplexityRoot.Query.AvailableSignals == nil { break @@ -933,7 +933,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.CloudEvents(childComplexity, args["subject"].(string), args["limit"].(*int), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.CloudEvents(childComplexity, args["subject"].(string), args["limit"].(*int), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.dailyActivity": if e.ComplexityRoot.Query.DailyActivity == nil { break @@ -978,7 +978,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.LatestCloudEvent(childComplexity, args["subject"].(string), args["filter"].(*model.CloudEventFilter)), true + return e.ComplexityRoot.Query.LatestCloudEvent(childComplexity, args["subject"].(string), args["filter"].(*model.CloudEventFilter), args["includeDeleted"].(*bool)), true case "Query.segments": if e.ComplexityRoot.Query.Segments == nil { break @@ -3752,8 +3752,10 @@ input CloudEventFilter { extend type Query { """ Latest full cloud event matching the given subject DID and optional filter. + Tombstoned attestations are hidden from the result by default; pass + includeDeleted: true to disable tombstone suppression. """ - latestCloudEvent(subject: String!, filter: CloudEventFilter): CloudEvent! + latestCloudEvent(subject: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEvent! @mcpTool( name: "get_latest_cloud_event" description: "Get the latest full CloudEvent (header + JSON payload) for a subject DID, optionally filtered. Returns dataUrl (presigned S3 link) for large binary payloads instead of inlining them." @@ -3762,8 +3764,10 @@ extend type Query { """ List full cloud events matching the given subject DID and optional filter. + Tombstoned attestations are hidden from the result by default; pass + includeDeleted: true to disable tombstone suppression. """ - cloudEvents(subject: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEvent!]! + cloudEvents(subject: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEvent!]! @mcpTool( name: "list_cloud_events" description: "List full CloudEvents (headers + JSON payloads) for a subject DID, optionally filtered and limited (default 10). Large binary payloads are returned as presigned S3 URLs via dataUrl instead of inlined data." @@ -3771,9 +3775,11 @@ extend 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(subject: String!, filter: CloudEventFilter): [CloudEventTypeSummary!]! + availableCloudEventTypes(subject: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventTypeSummary!]! @mcpTool( name: "list_available_event_types" description: "Summarize the CloudEvent types present for a subject DID. Returns each type with its count, firstSeen, and lastSeen timestamps — useful for discovering what kinds of events exist before querying them." @@ -5990,6 +5996,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 } @@ -6027,6 +6038,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 } @@ -6131,6 +6147,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 } @@ -10501,7 +10522,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["subject"].(string), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().LatestCloudEvent(ctx, fc.Args["subject"].(string), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEvent2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋdqᚋinternalᚋgraphᚐCloudEventWrapper, @@ -10552,7 +10573,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["subject"].(string), fc.Args["limit"].(*int), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().CloudEvents(ctx, fc.Args["subject"].(string), fc.Args["limit"].(*int), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEvent2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋdqᚋinternalᚋgraphᚐCloudEventWrapperᚄ, @@ -10603,7 +10624,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["subject"].(string), fc.Args["filter"].(*model.CloudEventFilter)) + return ec.Resolvers.Query().AvailableCloudEventTypes(ctx, fc.Args["subject"].(string), fc.Args["filter"].(*model.CloudEventFilter), fc.Args["includeDeleted"].(*bool)) }, nil, ec.marshalNCloudEventTypeSummary2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋdqᚋinternalᚋgraphᚋmodelᚐCloudEventTypeSummaryᚄ, diff --git a/internal/graph/mcp_tools_gen.go b/internal/graph/mcp_tools_gen.go index 392cc68..7eb5fb6 100644 --- a/internal/graph/mcp_tools_gen.go +++ b/internal/graph/mcp_tools_gen.go @@ -93,8 +93,9 @@ var MCPTools = []mcpserver.ToolDefinition{ Args: []mcpserver.ArgDefinition{ {Name: "subject", Type: "string", Description: "subject (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($subject: String!, $filter: CloudEventFilter) { latestCloudEvent(subject: $subject, filter: $filter) { header { type source subject id time producer } data dataUrl } }", + Query: "query($subject: String!, $filter: CloudEventFilter, $includeDeleted: Boolean) { latestCloudEvent(subject: $subject, filter: $filter, includeDeleted: $includeDeleted) { header { type source subject id time producer } data dataUrl } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -109,8 +110,9 @@ var MCPTools = []mcpserver.ToolDefinition{ {Name: "subject", Type: "string", Description: "subject (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($subject: String!, $limit: Int, $filter: CloudEventFilter) { cloudEvents(subject: $subject, limit: $limit, filter: $filter) { header { type source subject id time producer } data dataUrl } }", + Query: "query($subject: String!, $limit: Int, $filter: CloudEventFilter, $includeDeleted: Boolean) { cloudEvents(subject: $subject, limit: $limit, filter: $filter, includeDeleted: $includeDeleted) { header { type source subject id time producer } data dataUrl } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -124,8 +126,9 @@ var MCPTools = []mcpserver.ToolDefinition{ Args: []mcpserver.ArgDefinition{ {Name: "subject", Type: "string", Description: "subject (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($subject: String!, $filter: CloudEventFilter) { availableCloudEventTypes(subject: $subject, filter: $filter) { type count firstSeen lastSeen } }", + Query: "query($subject: String!, $filter: CloudEventFilter, $includeDeleted: Boolean) { availableCloudEventTypes(subject: $subject, filter: $filter, includeDeleted: $includeDeleted) { type count firstSeen lastSeen } }", Annotations: &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: boolPtr(false), @@ -195,4 +198,4 @@ var MCPTools = []mcpserver.ToolDefinition{ }, } -var CondensedSchema = "scalar Address # A 20-byte Ethereum address, encoded as a checksummed hex string with 0x prefix.\nscalar JSON # Arbitrary JSON value; serialized as raw JSON (object/array), not an escaped string.\nscalar Map\nscalar Time # A point in time, encoded per RFC-3999.\nscalar Uint64 # A 64-bit unsigned integer.\n\n# ═══ SIGNAL FIELDS (117 total) ═══\n# All signals below exist on every signal type. Calling convention per type:\n# SignalAggregations:\n# fieldName(agg: LocationAggregation!): Location\n# fieldName(agg: FloatAggregation!, filter: SignalFloatFilter): Float\n# fieldName(agg: LocationAggregation!, filter: SignalLocationFilter): Location\n# fieldName(agg: StringAggregation!): String\n# SignalCollection:\n# fieldName(): SignalLocation\n# fieldName(): SignalFloat\n# fieldName(): SignalString\n# Float is the default type. Location: currentLocationApproximateCoordinates, currentLocationCoordinates. String: obdDTCList, obdFuelTypeName, powertrainCombustionEngineEngineOilLevel, powertrainFuelSystemSupportedFuelTypes, powertrainTransmissionRetarderTorqueMode, powertrainType.\n# | Signal | Unit | Description |\n# |--------|------|-------------|\n# Shared descriptions (blank rows below use these):\n# - Is item open or closed? True = Fully or partially open\n# - Is the belt engaged\n# - Measured Load on axle row 3\n# ── CURRENT (privilege: VEHICLE_ALL_TIME_LOCATION) ──\n# | currentLocationApproximateCoordinates | | Approximate location of the vehicle in WGS 84 coordinates (privilege: VEHICLE_APPROXIMATE_LOCATION VEHICLE_ALL_TIME_LOCATION) |\n# | currentLocationAltitude | m | Current altitude relative to WGS 84 reference ellipsoid, as measured at the position of GNSS receiver antenna |\n# | currentLocationCoordinates | | Current location of the vehicle in WGS 84 coordinates |\n# | currentLocationHeading | degrees | Current heading relative to geographic north |\n# ── OTHER (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | angularVelocityYaw | degrees/s | Vehicle rotation rate along Z (vertical) |\n# | connectivityCellularIsJammingDetected | | Indicates whether cellular radio signal jamming or interference is detected that prevents normal communication |\n# | exteriorAirTemperature | celsius | Air temperature outside the vehicle |\n# | isIgnitionOn | | Vehicle ignition status |\n# | lowVoltageBatteryCurrentVoltage | V | |\n# | speed | km/h | |\n# ── BODY (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | bodyLightsIsAirbagWarningOn | | Indicates whether the airbag/SRS warning telltale is active |\n# | bodyLockIsLocked | | Indicates whether the vehicle is locked via the central locking system |\n# | bodyTrunkFrontIsOpen | | |\n# | bodyTrunkRearIsOpen | | |\n# ── CABIN (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | cabinDoorRow1DriverSideIsOpen | | |\n# | cabinDoorRow1DriverSideWindowIsOpen | | |\n# | cabinDoorRow1PassengerSideIsOpen | | |\n# | cabinDoorRow1PassengerSideWindowIsOpen | | |\n# | cabinDoorRow2DriverSideIsOpen | | |\n# | cabinDoorRow2DriverSideWindowIsOpen | | |\n# | cabinDoorRow2PassengerSideIsOpen | | |\n# | cabinDoorRow2PassengerSideWindowIsOpen | | |\n# | cabinSeatRow1DriverSideIsBelted | | |\n# | cabinSeatRow1PassengerSideIsBelted | | |\n# | cabinSeatRow2DriverSideIsBelted | | |\n# | cabinSeatRow2MiddleIsBelted | | |\n# | cabinSeatRow2PassengerSideIsBelted | | |\n# | cabinSeatRow3DriverSideIsBelted | | |\n# | cabinSeatRow3PassengerSideIsBelted | | |\n# ── CHASSIS (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# shared: Rotational speed of a vehicle's wheel\n# shared: Pneumatic pressure in the service brake circuit or reservoir\n# | chassisAxleRow1WheelLeftSpeed | km/h | |\n# | chassisAxleRow1WheelLeftTirePressure | kPa | |\n# | chassisAxleRow1WheelRightSpeed | km/h | |\n# | chassisAxleRow1WheelRightTirePressure | kPa | |\n# | chassisAxleRow2WheelLeftTirePressure | kPa | |\n# | chassisAxleRow2WheelRightTirePressure | kPa | |\n# | chassisAxleRow3Weight | kg | |\n# | chassisAxleRow4Weight | kg | |\n# | chassisAxleRow5Weight | kg | |\n# | chassisBrakeABSIsWarningOn | | Indicates whether the ABS warning telltale is active (any non-off state) |\n# | chassisBrakeCircuit1PressurePrimary | kPa | |\n# | chassisBrakeCircuit2PressurePrimary | kPa | |\n# | chassisBrakeIsPedalPressed | | Indicates whether the brake pedal is pressed |\n# | chassisBrakePedalPosition | percent | Brake pedal position as percent |\n# | chassisParkingBrakeIsEngaged | | |\n# | chassisTireSystemIsWarningOn | | Indicates whether the tire system warning telltale is active |\n# ── OBD (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# shared: PID 2x (byte CD) - Voltage for wide range/band oxygen sensor\n# | obdBarometricPressure | kPa | PID 33 - Barometric pressure |\n# | obdCommandedEGR | percent | PID 2C - Commanded exhaust gas recirculation (EGR) |\n# | obdCommandedEVAP | percent | PID 2E - Commanded evaporative purge (EVAP) valve |\n# | obdDTCList | | List of currently active DTCs formatted according OBD II (SAE-J2012DA_201812) standard ([P|C|B|U]XXXXX ) |\n# | obdDistanceSinceDTCClear | km | PID 31 - Distance traveled since codes cleared |\n# | obdDistanceWithMIL | km | PID 21 - Distance traveled with MIL on |\n# | obdEngineLoad | percent | PID 04 - Engine load in percent - 0 = no load, 100 = full load |\n# | obdEthanolPercent | percent | PID 52 - Percentage of ethanol in the fuel |\n# | obdFuelPressure | kPa | PID 0A - Fuel pressure |\n# | obdFuelRailPressure | kPa | |\n# | obdFuelRate | l/h | PID 5E - Engine fuel rate |\n# | obdFuelTypeName | | Fuel type names decoded from PID 51 |\n# | obdIntakeTemp | celsius | PID 0F - Intake temperature |\n# | obdIsEngineBlocked | | Engine block status, 0 = engine unblocked, 1 = engine blocked |\n# | obdIsPTOActive | | PID 1E - Auxiliary input status (power take off) |\n# | obdIsPluggedIn | | Aftermarket device plugged in status |\n# | obdLongTermFuelTrim1 | percent | PID 07 - Long Term (learned) Fuel Trim - Bank 1 - negative percent leaner, positive percent richer |\n# | obdLongTermFuelTrim2 | percent | PID 09 - Long Term (learned) Fuel Trim - Bank 2 - negative percent leaner, positive percent richer |\n# | obdMAP | kPa | PID 0B - Intake manifold pressure |\n# | obdMaxMAF | g/s | PID 50 - Maximum flow for mass air flow sensor |\n# | obdO2WRSensor1Voltage | V | |\n# | obdO2WRSensor2Voltage | V | |\n# | obdOilTemperature | celsius | PID 5C - Engine oil temperature |\n# | obdRunTime | s | PID 1F - Engine run time |\n# | obdShortTermFuelTrim1 | percent | PID 06 - Short Term (immediate) Fuel Trim - Bank 1 - negative percent leaner, positive percent richer |\n# | obdStatusDTCCount | | Number of Diagnostic Trouble Codes (DTC) |\n# | obdThrottlePosition | percent | PID 11 - Throttle position - 0 = closed throttle, 100 = open throttle |\n# | obdWarmupsSinceDTCClear | | PID 30 - Number of warm-ups since codes cleared |\n# ── POWERTRAIN (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | powertrainCombustionEngineDieselExhaustFluidCapacity | l | Capacity in liters of the Diesel Exhaust Fluid Tank |\n# | powertrainCombustionEngineDieselExhaustFluidLevel | percent | Level of the Diesel Exhaust Fluid tank as percent of capacity |\n# | powertrainCombustionEngineECT | celsius | Engine coolant temperature |\n# | powertrainCombustionEngineEOP | kPa | Engine oil pressure |\n# | powertrainCombustionEngineEOT | celsius | Engine oil temperature |\n# | powertrainCombustionEngineEngineOilLevel | | |\n# | powertrainCombustionEngineEngineOilRelativeLevel | percent | Engine oil level as a percentage |\n# | powertrainCombustionEngineMAF | g/s | Grams of air drawn into engine per second |\n# | powertrainCombustionEngineSpeed | rpm | Engine speed measured as rotations per minute |\n# | powertrainCombustionEngineTPS | percent | Current throttle position |\n# | powertrainCombustionEngineTorque | Nm | |\n# | powertrainCombustionEngineTorquePercent | percent | Actual engine output torque as a percentage of reference engine torque (FMS / J1939 parameter SPN 513) |\n# | powertrainFuelSystemAbsoluteLevel | l | Current available fuel in the fuel tank expressed in liters |\n# | powertrainFuelSystemAccumulatedConsumption | l | Accumulated fuel consumption (totalized) reported by the vehicle (FMS SPN 250) |\n# | powertrainFuelSystemRelativeLevel | percent | Level in fuel tank as percent of capacity |\n# | powertrainFuelSystemSupportedFuelTypes | | High level information of fuel types supported |\n# | powertrainRange | km | Remaining range in kilometers using all energy sources available in the vehicle |\n# | powertrainTractionBatteryChargingAddedEnergy | kWh | Amount of charge added to the high voltage battery during the current charging session, expressed in kilowatt-hours |\n# | powertrainTractionBatteryChargingChargeCurrentAC | A | Current AC charging current (rms) at inlet |\n# | powertrainTractionBatteryChargingChargeLimit | percent | Target charge limit (state of charge) for battery |\n# | powertrainTractionBatteryChargingChargeVoltageUnknownType | V | Current charging voltage at inlet |\n# | powertrainTractionBatteryChargingIsCharging | | True if charging is ongoing |\n# | powertrainTractionBatteryChargingIsChargingCableConnected | | Indicates if a charging cable is physically connected to the vehicle or not |\n# | powertrainTractionBatteryChargingPower | kW | Instantaneous charging power recorded during a charging event |\n# | powertrainTractionBatteryCurrentPower | W | Current electrical energy flowing in/out of battery |\n# | powertrainTractionBatteryCurrentVoltage | V | |\n# | powertrainTractionBatteryGrossCapacity | kWh | |\n# | powertrainTractionBatteryRange | km | Remaining range in kilometers using only battery |\n# | powertrainTractionBatteryStateOfChargeCurrent | percent | Physical state of charge of the high voltage battery, relative to net capacity |\n# | powertrainTractionBatteryStateOfChargeCurrentEnergy | kWh | Physical state of charge of high voltage battery expressed in kWh |\n# | powertrainTractionBatteryStateOfHealth | percent | Calculated battery state of health at standard conditions |\n# | powertrainTractionBatteryTemperatureAverage | celsius | Current average temperature of the battery cells |\n# | powertrainTransmissionActualGear | | Actual transmission gear currently engaged |\n# | powertrainTransmissionActualGearRatio | | |\n# | powertrainTransmissionCurrentGear | | |\n# | powertrainTransmissionIsClutchSwitchOperated | | Indicates if the Clutch switch is operated, so engine and transmission are partially or fully decoupled |\n# | powertrainTransmissionRetarderActualTorque | percent | Actual retarder torque as a percentage (FMS / J1939 SPN 520) |\n# | powertrainTransmissionRetarderTorqueMode | | Active engine torque mode |\n# | powertrainTransmissionSelectedGear | | |\n# | powertrainTransmissionTemperature | celsius | The current gearbox temperature |\n# | powertrainTransmissionTravelledDistance | km | Odometer reading, total distance travelled during the lifetime of the transmission |\n# | powertrainType | | Defines the powertrain type of the vehicle |\n# ── SERVICE (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | serviceDistanceToService | km | Remaining distance to service (of any kind) |\n# | serviceTimeToService | s | Remaining time to service (of any kind) |\n\ntype Query {\n \"signals returns a collection of signals for a given vehicle DID in a given time range.\"\n signals(\n subject: String!\n \"\"\"\n interval is a time span that used for aggregatting the data with. A duration\n string is a sequence of decimal numbers, each with optional fraction and a unit\n suffix, such as \"300ms\" or \"2h45m\". Valid time units are \"ms\", \"s\", \"m\", \"h\"\n \"\"\"\n interval: String!\n from: Time!\n to: Time!\n filter: SignalFilter\n ): [SignalAggregations!]\n # Example - Hourly average speed over a time range:\n # query TimeSeries($subject:String!,$from:Time!,$to:Time!) { signals(subject:$subject,interval:\"1h\",from:$from,to:$to) { timestamp speed(agg:AVG) } }\n\n \"SignalsLatest returns the latest signals for a given vehicle DID.\"\n signalsLatest(subject: String!, filter: SignalFilter): SignalCollection\n # Example - Latest speed and battery charge:\n # query Latest($subject:String!) { signalsLatest(subject:$subject) { lastSeen speed{timestamp value} powertrainTractionBatteryStateOfChargeCurrent{timestamp value} } }\n\n \"availableSignals returns a list of queryable signal names that have stored data for a given vehicle DID.\"\n availableSignals(subject: String!, filter: SignalFilter): [String!]\n\n \"data summary of all signals for a given vehicle DID\"\n dataSummary(subject: String!, filter: SignalFilter): DataSummary\n\n \"signalsSnapshot returns the latest value of every available signal for a vehicle, along with when any signal was last seen.\"\n signalsSnapshot(subject: String!, filter: SignalFilter): SignalsSnapshotResponse\n # Example - Full snapshot of all signals for a vehicle:\n # query Snapshot($subject:String!) { signalsSnapshot(subject:$subject) { lastSeen signals { name timestamp valueNumber valueString valueLocation { latitude longitude hdop } } } }\n\n \"Latest full cloud event matching the given subject DID and optional filter.\"\n latestCloudEvent(subject: String!, filter: CloudEventFilter): CloudEvent!\n\n cloudEvents(subject: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEvent!]!\n availableCloudEventTypes(subject: String!, filter: CloudEventFilter): [CloudEventTypeSummary!]!\n \"events returns a list of events for a given vehicle DID in a given time range.\"\n events(\n \"subject is the vehicle DID to get events for.\"\n subject: String!\n \"from is the start time of the event.\"\n from: Time!\n \"to is the end time of the event.\"\n to: Time!\n \"filter is the filter to apply to the events.\"\n filter: EventFilter\n ): [Event!]\n\n \"\"\"\n Returns vehicle usage segments detected using the specified mechanism. Maximum\n date range: 31 days.\n Detection mechanisms:\n - IGNITION_DETECTION: Uses 'isIgnitionOn' signal with configurable debouncing\n - FREQUENCY_ANALYSIS: Analyzes signal update frequency to detect activity\n periods\n - CHANGE_POINT_DETECTION: CUSUM-based regime change detection\n - IDLING: Idling segments (engine rpm idle)\n - REFUEL: Refueling segments (fuel level increased)\n - RECHARGE: Charging segments (battery SoC increased)\n Segment IDs are stable and consistent across queries as long as the segment\n start is captured in the underlying data source.\n Each segment includes summary: signals, start/end location, and (when requested)\n eventCounts. A default set of signal requests is always applied (e.g. speed,\n odometer; for refuel/recharge also the level signal at start and end). When\n signalRequests is provided, those requests are added on top of the default set;\n duplicates (same name and agg) are omitted.\n \"\"\"\n segments(\n subject: String!\n from: Time!\n to: Time!\n mechanism: DetectionMechanism!\n config: SegmentConfig\n signalRequests: [SegmentSignalRequest!]\n eventRequests: [SegmentEventRequest!]\n \"Maximum number of segments to return. Default 100, max 200.\"\n limit: Int = 100\n after: Time\n ): [Segment!]!\n # Example - Trip segments with start/end locations and signal aggregates:\n # query Trips($subject:String!,$from:Time!,$to:Time!) { segments(subject:$subject,from:$from,to:$to,mechanism:FREQUENCY_ANALYSIS) { start{timestamp value{latitude longitude}} end{timestamp value{latitude longitude}} duration isOngoing signals{name agg value} eventCounts{name count} } }\n\n \"\"\"\n Returns one record per calendar day in the date range. Mechanism must be\n IGNITION_DETECTION, FREQUENCY_ANALYSIS, or CHANGE_POINT_DETECTION (IDLING,\n REFUEL, and RECHARGE not allowed). Maximum date range: 31 days.\n \"\"\"\n dailyActivity(subject: String!, from: Time!, to: Time!, mechanism: DetectionMechanism!, config: SegmentConfig, signalRequests: [SegmentSignalRequest!], eventRequests: [SegmentEventRequest!], timezone: String): [DailyActivity!]!\n # Example - Daily activity summaries:\n # query Daily($subject:String!,$from:Time!,$to:Time!) { dailyActivity(subject:$subject,from:$from,to:$to,mechanism:FREQUENCY_ANALYSIS) { segmentCount duration signals{name agg value} eventCounts{name count} } }\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 CloudEventTypeSummary { type: String!, count: Int!, firstSeen: Time!, lastSeen: Time! }\n\ntype DailyActivity { start: SignalLocation, end: SignalLocation, segmentCount: Int!, duration: Int!, signals: [SignalAggregationValue!]!, eventCounts: [EventCount!]! }\n\ntype DataSummary { numberOfSignals: Uint64!, availableSignals: [String!]!, firstSeen: Time!, lastSeen: Time!, signalDataSummary: [SignalDataSummary!]!, eventDataSummary: [EventDataSummary!]! }\n\nenum DetectionMechanism {\n \"Ignition-based detection: Segments are identified by isIgnitionOn state transitions. Most reliable for vehicles with proper ignition signal support.\"\n IGNITION_DETECTION\n \"Frequency analysis: Segments are detected by analyzing signal update patterns. Uses pre-computed materialized view for optimal performance. Ideal for real-time APIs and bulk queries.\"\n FREQUENCY_ANALYSIS\n \"\"\"\n Change point detection: Uses CUSUM algorithm to detect statistical regime\n changes. Monitors cumulative deviation in signal frequency via materialized\n view. Excellent noise resistance with 100% accuracy match to ignition baseline.\n Best alternative when ignition signal is unavailable - same accuracy, same speed\n as frequency analysis.\n \"\"\"\n CHANGE_POINT_DETECTION\n \"Idling: Segments are contiguous periods where engine RPM remains in idle range.\"\n IDLING\n \"Refuel: Detects where fuel level rises significantly.\"\n REFUEL\n \"Recharge: Hybrid detection. Uses charging signals and state of charge for detection.\"\n RECHARGE\n}\n\ntype Event { timestamp: Time!, name: String!, source: String!, durationNs: Int!, metadata: String }\n\ntype EventCount { name: String!, count: Int! }\n\ntype EventDataSummary { name: String!, numberOfEvents: Uint64!, firstSeen: Time!, lastSeen: Time! }\n\ninput EventFilter {\n name: StringValueFilter\n \"source is the name of the source connection that created the event.\"\n source: StringValueFilter\n tags: StringArrayFilter\n}\n\ninput FilterLocation {\n \"Latitude in the range [-90, 90].\"\n latitude: Float!\n \"Longitude in the range [-180, 180].\"\n longitude: Float!\n}\n\nenum FloatAggregation { AVG, MED, MAX, MIN, RAND, FIRST, LAST }\n\ninput InCircleFilter {\n center: FilterLocation!\n \"Radius of the circle around the center, in kilometers (km).\"\n radius: Float!\n}\n\ntype LatestSignal { name: String!, timestamp: Time!, valueNumber: Float, valueString: String, valueLocation: Location }\n\ntype Location { latitude: Float!, longitude: Float!, hdop: Float! }\n\nenum LocationAggregation { AVG, RAND, FIRST, LAST }\n\nenum Privilege { VEHICLE_NON_LOCATION_DATA, VEHICLE_COMMANDS, VEHICLE_CURRENT_LOCATION, VEHICLE_ALL_TIME_LOCATION, VEHICLE_VIN_CREDENTIAL, VEHICLE_APPROXIMATE_LOCATION, VEHICLE_RAW_DATA }\n\ntype Segment { start: SignalLocation!, end: SignalLocation, duration: Int!, isOngoing: Boolean!, startedBeforeRange: Boolean!, signals: [SignalAggregationValue!], eventCounts: [EventCount!] }\n\ninput SegmentConfig {\n \"\"\"\n Maximum gap (seconds) between data points before a segment is split. For\n IGNITION_DETECTION: filters noise from brief ignition OFF events. For\n FREQUENCY_ANALYSIS: maximum gap between active windows to merge. Default: 300 (5\n minutes), Min: 60, Max: 3600\n \"\"\"\n maxGapSeconds: Int = 300\n \"Minimum segment duration (seconds) to include in results. Filters very short segments (testing, engine cycling). Default: 240 (4 minutes), Min: 60, Max: 3600\"\n minSegmentDurationSeconds: Int = 240\n \"\"\"\n [FREQUENCY_ANALYSIS] Minimum signal count per window for activity detection.\n [IDLING] Minimum samples per window to consider it idle (same semantics). Higher\n values = more conservative. Lower values = more sensitive. Default: 10, Min: 1,\n Max: 3600\n \"\"\"\n signalCountThreshold: Int = 10\n \"[IDLING only] Upper bound for idle RPM. Windows with max(RPM) <= this are considered idle. Default: 1000, Min: 300, Max: 3000\"\n maxIdleRpm: Int = 1000\n \"[REFUEL and RECHARGE only] Minimum percent increase within a window to consider it a level-increase window.\"\n minIncreasePercent: Int = 15\n}\n\ninput SegmentEventRequest { name: String! }\n\ninput SegmentSignalRequest { name: String!, agg: FloatAggregation! }\n\ntype SignalAggregationValue { name: String!, agg: String!, value: Float! }\n\ntype SignalAggregations {\n \"Timestamp of the aggregated data.\"\n timestamp: Time!\n # + 117 signal fields (see SIGNAL FIELDS table above)\n}\n\ntype SignalCollection {\n \"The last time any signal was seen matching the filter.\"\n lastSeen: Time\n # + 117 signal fields (see SIGNAL FIELDS table above)\n}\n\ntype SignalDataSummary { name: String!, numberOfSignals: Uint64!, firstSeen: Time!, lastSeen: Time! }\n\ninput SignalFilter {\n \"\"\"\n Filter signals by source using an ethr DID. Example:\n \"did:ethr:137:0xcd445F4c6bDAD32b68a2939b912150Fe3C88803E\"\n \"\"\"\n source: String\n}\n\ntype SignalFloat { timestamp: Time!, value: Float! }\n\ninput SignalFloatFilter { eq: Float, neq: Float, gt: Float, lt: Float, gte: Float, lte: Float, notIn: [Float!], in: [Float!], or: [SignalFloatFilter!] }\n\ntype SignalLocation { timestamp: Time!, value: Location! }\n\ninput SignalLocationFilter {\n \"Filter for locations within a polygon. The vertices should be ordered clockwise or counterclockwise, and there must be at least 3. May produce inaccurate results around the poles and the antimeridian.\"\n inPolygon: [FilterLocation!]\n \"Filter for locations within a given distance of a given point. Distances are computed using WGS 84, and points that are exactly a distance `radius` from the `center` will be included.\"\n inCircle: InCircleFilter\n}\n\ntype SignalString { timestamp: Time!, value: String! }\n\ntype SignalsSnapshotResponse { lastSeen: Time, signals: [LatestSignal!]! }\n\nenum StringAggregation {\n \"Randomly select a value from the group.\"\n RAND\n \"Select the most frequently occurring value in the group.\"\n TOP\n \"Return a list of unique values in the group.\"\n UNIQUE\n \"Return value in group associated with the minimum time value.\"\n FIRST\n \"Return value in group associated with the maximum time value.\"\n LAST\n}\n\ninput StringArrayFilter {\n containsAny: [String!]\n containsAll: [String!]\n \"notContainsAny array of strings does not contain any of the strings in the array\"\n notContainsAny: [String!]\n \"notContainsAll array of strings does not contain all of the strings in the array\"\n notContainsAll: [String!]\n or: [StringArrayFilter!]\n}\n\ninput StringValueFilter {\n eq: String\n neq: String\n notIn: [String!]\n in: [String!]\n \"startsWith matches strings that begin with the given prefix.\"\n startsWith: String\n or: [StringValueFilter!]\n}\n" +var CondensedSchema = "scalar Address # A 20-byte Ethereum address, encoded as a checksummed hex string with 0x prefix.\nscalar JSON # Arbitrary JSON value; serialized as raw JSON (object/array), not an escaped string.\nscalar Map\nscalar Time # A point in time, encoded per RFC-3999.\nscalar Uint64 # A 64-bit unsigned integer.\n\n# ═══ SIGNAL FIELDS (117 total) ═══\n# All signals below exist on every signal type. Calling convention per type:\n# SignalAggregations:\n# fieldName(agg: LocationAggregation!): Location\n# fieldName(agg: FloatAggregation!, filter: SignalFloatFilter): Float\n# fieldName(agg: LocationAggregation!, filter: SignalLocationFilter): Location\n# fieldName(agg: StringAggregation!): String\n# SignalCollection:\n# fieldName(): SignalLocation\n# fieldName(): SignalFloat\n# fieldName(): SignalString\n# Float is the default type. Location: currentLocationApproximateCoordinates, currentLocationCoordinates. String: obdDTCList, obdFuelTypeName, powertrainCombustionEngineEngineOilLevel, powertrainFuelSystemSupportedFuelTypes, powertrainTransmissionRetarderTorqueMode, powertrainType.\n# | Signal | Unit | Description |\n# |--------|------|-------------|\n# Shared descriptions (blank rows below use these):\n# - Is item open or closed? True = Fully or partially open\n# - Is the belt engaged\n# - Measured Load on axle row 3\n# ── CURRENT (privilege: VEHICLE_ALL_TIME_LOCATION) ──\n# | currentLocationApproximateCoordinates | | Approximate location of the vehicle in WGS 84 coordinates (privilege: VEHICLE_APPROXIMATE_LOCATION VEHICLE_ALL_TIME_LOCATION) |\n# | currentLocationAltitude | m | Current altitude relative to WGS 84 reference ellipsoid, as measured at the position of GNSS receiver antenna |\n# | currentLocationCoordinates | | Current location of the vehicle in WGS 84 coordinates |\n# | currentLocationHeading | degrees | Current heading relative to geographic north |\n# ── OTHER (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | angularVelocityYaw | degrees/s | Vehicle rotation rate along Z (vertical) |\n# | connectivityCellularIsJammingDetected | | Indicates whether cellular radio signal jamming or interference is detected that prevents normal communication |\n# | exteriorAirTemperature | celsius | Air temperature outside the vehicle |\n# | isIgnitionOn | | Vehicle ignition status |\n# | lowVoltageBatteryCurrentVoltage | V | |\n# | speed | km/h | |\n# ── BODY (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | bodyLightsIsAirbagWarningOn | | Indicates whether the airbag/SRS warning telltale is active |\n# | bodyLockIsLocked | | Indicates whether the vehicle is locked via the central locking system |\n# | bodyTrunkFrontIsOpen | | |\n# | bodyTrunkRearIsOpen | | |\n# ── CABIN (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | cabinDoorRow1DriverSideIsOpen | | |\n# | cabinDoorRow1DriverSideWindowIsOpen | | |\n# | cabinDoorRow1PassengerSideIsOpen | | |\n# | cabinDoorRow1PassengerSideWindowIsOpen | | |\n# | cabinDoorRow2DriverSideIsOpen | | |\n# | cabinDoorRow2DriverSideWindowIsOpen | | |\n# | cabinDoorRow2PassengerSideIsOpen | | |\n# | cabinDoorRow2PassengerSideWindowIsOpen | | |\n# | cabinSeatRow1DriverSideIsBelted | | |\n# | cabinSeatRow1PassengerSideIsBelted | | |\n# | cabinSeatRow2DriverSideIsBelted | | |\n# | cabinSeatRow2MiddleIsBelted | | |\n# | cabinSeatRow2PassengerSideIsBelted | | |\n# | cabinSeatRow3DriverSideIsBelted | | |\n# | cabinSeatRow3PassengerSideIsBelted | | |\n# ── CHASSIS (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# shared: Rotational speed of a vehicle's wheel\n# shared: Pneumatic pressure in the service brake circuit or reservoir\n# | chassisAxleRow1WheelLeftSpeed | km/h | |\n# | chassisAxleRow1WheelLeftTirePressure | kPa | |\n# | chassisAxleRow1WheelRightSpeed | km/h | |\n# | chassisAxleRow1WheelRightTirePressure | kPa | |\n# | chassisAxleRow2WheelLeftTirePressure | kPa | |\n# | chassisAxleRow2WheelRightTirePressure | kPa | |\n# | chassisAxleRow3Weight | kg | |\n# | chassisAxleRow4Weight | kg | |\n# | chassisAxleRow5Weight | kg | |\n# | chassisBrakeABSIsWarningOn | | Indicates whether the ABS warning telltale is active (any non-off state) |\n# | chassisBrakeCircuit1PressurePrimary | kPa | |\n# | chassisBrakeCircuit2PressurePrimary | kPa | |\n# | chassisBrakeIsPedalPressed | | Indicates whether the brake pedal is pressed |\n# | chassisBrakePedalPosition | percent | Brake pedal position as percent |\n# | chassisParkingBrakeIsEngaged | | |\n# | chassisTireSystemIsWarningOn | | Indicates whether the tire system warning telltale is active |\n# ── OBD (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# shared: PID 2x (byte CD) - Voltage for wide range/band oxygen sensor\n# | obdBarometricPressure | kPa | PID 33 - Barometric pressure |\n# | obdCommandedEGR | percent | PID 2C - Commanded exhaust gas recirculation (EGR) |\n# | obdCommandedEVAP | percent | PID 2E - Commanded evaporative purge (EVAP) valve |\n# | obdDTCList | | List of currently active DTCs formatted according OBD II (SAE-J2012DA_201812) standard ([P|C|B|U]XXXXX ) |\n# | obdDistanceSinceDTCClear | km | PID 31 - Distance traveled since codes cleared |\n# | obdDistanceWithMIL | km | PID 21 - Distance traveled with MIL on |\n# | obdEngineLoad | percent | PID 04 - Engine load in percent - 0 = no load, 100 = full load |\n# | obdEthanolPercent | percent | PID 52 - Percentage of ethanol in the fuel |\n# | obdFuelPressure | kPa | PID 0A - Fuel pressure |\n# | obdFuelRailPressure | kPa | |\n# | obdFuelRate | l/h | PID 5E - Engine fuel rate |\n# | obdFuelTypeName | | Fuel type names decoded from PID 51 |\n# | obdIntakeTemp | celsius | PID 0F - Intake temperature |\n# | obdIsEngineBlocked | | Engine block status, 0 = engine unblocked, 1 = engine blocked |\n# | obdIsPTOActive | | PID 1E - Auxiliary input status (power take off) |\n# | obdIsPluggedIn | | Aftermarket device plugged in status |\n# | obdLongTermFuelTrim1 | percent | PID 07 - Long Term (learned) Fuel Trim - Bank 1 - negative percent leaner, positive percent richer |\n# | obdLongTermFuelTrim2 | percent | PID 09 - Long Term (learned) Fuel Trim - Bank 2 - negative percent leaner, positive percent richer |\n# | obdMAP | kPa | PID 0B - Intake manifold pressure |\n# | obdMaxMAF | g/s | PID 50 - Maximum flow for mass air flow sensor |\n# | obdO2WRSensor1Voltage | V | |\n# | obdO2WRSensor2Voltage | V | |\n# | obdOilTemperature | celsius | PID 5C - Engine oil temperature |\n# | obdRunTime | s | PID 1F - Engine run time |\n# | obdShortTermFuelTrim1 | percent | PID 06 - Short Term (immediate) Fuel Trim - Bank 1 - negative percent leaner, positive percent richer |\n# | obdStatusDTCCount | | Number of Diagnostic Trouble Codes (DTC) |\n# | obdThrottlePosition | percent | PID 11 - Throttle position - 0 = closed throttle, 100 = open throttle |\n# | obdWarmupsSinceDTCClear | | PID 30 - Number of warm-ups since codes cleared |\n# ── POWERTRAIN (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | powertrainCombustionEngineDieselExhaustFluidCapacity | l | Capacity in liters of the Diesel Exhaust Fluid Tank |\n# | powertrainCombustionEngineDieselExhaustFluidLevel | percent | Level of the Diesel Exhaust Fluid tank as percent of capacity |\n# | powertrainCombustionEngineECT | celsius | Engine coolant temperature |\n# | powertrainCombustionEngineEOP | kPa | Engine oil pressure |\n# | powertrainCombustionEngineEOT | celsius | Engine oil temperature |\n# | powertrainCombustionEngineEngineOilLevel | | |\n# | powertrainCombustionEngineEngineOilRelativeLevel | percent | Engine oil level as a percentage |\n# | powertrainCombustionEngineMAF | g/s | Grams of air drawn into engine per second |\n# | powertrainCombustionEngineSpeed | rpm | Engine speed measured as rotations per minute |\n# | powertrainCombustionEngineTPS | percent | Current throttle position |\n# | powertrainCombustionEngineTorque | Nm | |\n# | powertrainCombustionEngineTorquePercent | percent | Actual engine output torque as a percentage of reference engine torque (FMS / J1939 parameter SPN 513) |\n# | powertrainFuelSystemAbsoluteLevel | l | Current available fuel in the fuel tank expressed in liters |\n# | powertrainFuelSystemAccumulatedConsumption | l | Accumulated fuel consumption (totalized) reported by the vehicle (FMS SPN 250) |\n# | powertrainFuelSystemRelativeLevel | percent | Level in fuel tank as percent of capacity |\n# | powertrainFuelSystemSupportedFuelTypes | | High level information of fuel types supported |\n# | powertrainRange | km | Remaining range in kilometers using all energy sources available in the vehicle |\n# | powertrainTractionBatteryChargingAddedEnergy | kWh | Amount of charge added to the high voltage battery during the current charging session, expressed in kilowatt-hours |\n# | powertrainTractionBatteryChargingChargeCurrentAC | A | Current AC charging current (rms) at inlet |\n# | powertrainTractionBatteryChargingChargeLimit | percent | Target charge limit (state of charge) for battery |\n# | powertrainTractionBatteryChargingChargeVoltageUnknownType | V | Current charging voltage at inlet |\n# | powertrainTractionBatteryChargingIsCharging | | True if charging is ongoing |\n# | powertrainTractionBatteryChargingIsChargingCableConnected | | Indicates if a charging cable is physically connected to the vehicle or not |\n# | powertrainTractionBatteryChargingPower | kW | Instantaneous charging power recorded during a charging event |\n# | powertrainTractionBatteryCurrentPower | W | Current electrical energy flowing in/out of battery |\n# | powertrainTractionBatteryCurrentVoltage | V | |\n# | powertrainTractionBatteryGrossCapacity | kWh | |\n# | powertrainTractionBatteryRange | km | Remaining range in kilometers using only battery |\n# | powertrainTractionBatteryStateOfChargeCurrent | percent | Physical state of charge of the high voltage battery, relative to net capacity |\n# | powertrainTractionBatteryStateOfChargeCurrentEnergy | kWh | Physical state of charge of high voltage battery expressed in kWh |\n# | powertrainTractionBatteryStateOfHealth | percent | Calculated battery state of health at standard conditions |\n# | powertrainTractionBatteryTemperatureAverage | celsius | Current average temperature of the battery cells |\n# | powertrainTransmissionActualGear | | Actual transmission gear currently engaged |\n# | powertrainTransmissionActualGearRatio | | |\n# | powertrainTransmissionCurrentGear | | |\n# | powertrainTransmissionIsClutchSwitchOperated | | Indicates if the Clutch switch is operated, so engine and transmission are partially or fully decoupled |\n# | powertrainTransmissionRetarderActualTorque | percent | Actual retarder torque as a percentage (FMS / J1939 SPN 520) |\n# | powertrainTransmissionRetarderTorqueMode | | Active engine torque mode |\n# | powertrainTransmissionSelectedGear | | |\n# | powertrainTransmissionTemperature | celsius | The current gearbox temperature |\n# | powertrainTransmissionTravelledDistance | km | Odometer reading, total distance travelled during the lifetime of the transmission |\n# | powertrainType | | Defines the powertrain type of the vehicle |\n# ── SERVICE (privilege: VEHICLE_NON_LOCATION_DATA) ──\n# | serviceDistanceToService | km | Remaining distance to service (of any kind) |\n# | serviceTimeToService | s | Remaining time to service (of any kind) |\n\ntype Query {\n \"signals returns a collection of signals for a given vehicle DID in a given time range.\"\n signals(\n subject: String!\n \"\"\"\n interval is a time span that used for aggregatting the data with. A duration\n string is a sequence of decimal numbers, each with optional fraction and a unit\n suffix, such as \"300ms\" or \"2h45m\". Valid time units are \"ms\", \"s\", \"m\", \"h\"\n \"\"\"\n interval: String!\n from: Time!\n to: Time!\n filter: SignalFilter\n ): [SignalAggregations!]\n # Example - Hourly average speed over a time range:\n # query TimeSeries($subject:String!,$from:Time!,$to:Time!) { signals(subject:$subject,interval:\"1h\",from:$from,to:$to) { timestamp speed(agg:AVG) } }\n\n \"SignalsLatest returns the latest signals for a given vehicle DID.\"\n signalsLatest(subject: String!, filter: SignalFilter): SignalCollection\n # Example - Latest speed and battery charge:\n # query Latest($subject:String!) { signalsLatest(subject:$subject) { lastSeen speed{timestamp value} powertrainTractionBatteryStateOfChargeCurrent{timestamp value} } }\n\n \"availableSignals returns a list of queryable signal names that have stored data for a given vehicle DID.\"\n availableSignals(subject: String!, filter: SignalFilter): [String!]\n\n \"data summary of all signals for a given vehicle DID\"\n dataSummary(subject: String!, filter: SignalFilter): DataSummary\n\n \"signalsSnapshot returns the latest value of every available signal for a vehicle, along with when any signal was last seen.\"\n signalsSnapshot(subject: String!, filter: SignalFilter): SignalsSnapshotResponse\n # Example - Full snapshot of all signals for a vehicle:\n # query Snapshot($subject:String!) { signalsSnapshot(subject:$subject) { lastSeen signals { name timestamp valueNumber valueString valueLocation { latitude longitude hdop } } } }\n\n \"Latest full cloud event matching the given subject DID and optional filter. Tombstoned attestations are hidden from the result by default; pass includeDeleted: true to disable tombstone suppression.\"\n latestCloudEvent(subject: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEvent!\n\n cloudEvents(subject: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEvent!]!\n availableCloudEventTypes(subject: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventTypeSummary!]!\n \"events returns a list of events for a given vehicle DID in a given time range.\"\n events(\n \"subject is the vehicle DID to get events for.\"\n subject: String!\n \"from is the start time of the event.\"\n from: Time!\n \"to is the end time of the event.\"\n to: Time!\n \"filter is the filter to apply to the events.\"\n filter: EventFilter\n ): [Event!]\n\n \"\"\"\n Returns vehicle usage segments detected using the specified mechanism. Maximum\n date range: 31 days.\n Detection mechanisms:\n - IGNITION_DETECTION: Uses 'isIgnitionOn' signal with configurable debouncing\n - FREQUENCY_ANALYSIS: Analyzes signal update frequency to detect activity\n periods\n - CHANGE_POINT_DETECTION: CUSUM-based regime change detection\n - IDLING: Idling segments (engine rpm idle)\n - REFUEL: Refueling segments (fuel level increased)\n - RECHARGE: Charging segments (battery SoC increased)\n Segment IDs are stable and consistent across queries as long as the segment\n start is captured in the underlying data source.\n Each segment includes summary: signals, start/end location, and (when requested)\n eventCounts. A default set of signal requests is always applied (e.g. speed,\n odometer; for refuel/recharge also the level signal at start and end). When\n signalRequests is provided, those requests are added on top of the default set;\n duplicates (same name and agg) are omitted.\n \"\"\"\n segments(\n subject: String!\n from: Time!\n to: Time!\n mechanism: DetectionMechanism!\n config: SegmentConfig\n signalRequests: [SegmentSignalRequest!]\n eventRequests: [SegmentEventRequest!]\n \"Maximum number of segments to return. Default 100, max 200.\"\n limit: Int = 100\n after: Time\n ): [Segment!]!\n # Example - Trip segments with start/end locations and signal aggregates:\n # query Trips($subject:String!,$from:Time!,$to:Time!) { segments(subject:$subject,from:$from,to:$to,mechanism:FREQUENCY_ANALYSIS) { start{timestamp value{latitude longitude}} end{timestamp value{latitude longitude}} duration isOngoing signals{name agg value} eventCounts{name count} } }\n\n \"\"\"\n Returns one record per calendar day in the date range. Mechanism must be\n IGNITION_DETECTION, FREQUENCY_ANALYSIS, or CHANGE_POINT_DETECTION (IDLING,\n REFUEL, and RECHARGE not allowed). Maximum date range: 31 days.\n \"\"\"\n dailyActivity(subject: String!, from: Time!, to: Time!, mechanism: DetectionMechanism!, config: SegmentConfig, signalRequests: [SegmentSignalRequest!], eventRequests: [SegmentEventRequest!], timezone: String): [DailyActivity!]!\n # Example - Daily activity summaries:\n # query Daily($subject:String!,$from:Time!,$to:Time!) { dailyActivity(subject:$subject,from:$from,to:$to,mechanism:FREQUENCY_ANALYSIS) { segmentCount duration signals{name agg value} eventCounts{name count} } }\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 CloudEventTypeSummary { type: String!, count: Int!, firstSeen: Time!, lastSeen: Time! }\n\ntype DailyActivity { start: SignalLocation, end: SignalLocation, segmentCount: Int!, duration: Int!, signals: [SignalAggregationValue!]!, eventCounts: [EventCount!]! }\n\ntype DataSummary { numberOfSignals: Uint64!, availableSignals: [String!]!, firstSeen: Time!, lastSeen: Time!, signalDataSummary: [SignalDataSummary!]!, eventDataSummary: [EventDataSummary!]! }\n\nenum DetectionMechanism {\n \"Ignition-based detection: Segments are identified by isIgnitionOn state transitions. Most reliable for vehicles with proper ignition signal support.\"\n IGNITION_DETECTION\n \"Frequency analysis: Segments are detected by analyzing signal update patterns. Uses pre-computed materialized view for optimal performance. Ideal for real-time APIs and bulk queries.\"\n FREQUENCY_ANALYSIS\n \"\"\"\n Change point detection: Uses CUSUM algorithm to detect statistical regime\n changes. Monitors cumulative deviation in signal frequency via materialized\n view. Excellent noise resistance with 100% accuracy match to ignition baseline.\n Best alternative when ignition signal is unavailable - same accuracy, same speed\n as frequency analysis.\n \"\"\"\n CHANGE_POINT_DETECTION\n \"Idling: Segments are contiguous periods where engine RPM remains in idle range.\"\n IDLING\n \"Refuel: Detects where fuel level rises significantly.\"\n REFUEL\n \"Recharge: Hybrid detection. Uses charging signals and state of charge for detection.\"\n RECHARGE\n}\n\ntype Event { timestamp: Time!, name: String!, source: String!, durationNs: Int!, metadata: String }\n\ntype EventCount { name: String!, count: Int! }\n\ntype EventDataSummary { name: String!, numberOfEvents: Uint64!, firstSeen: Time!, lastSeen: Time! }\n\ninput EventFilter {\n name: StringValueFilter\n \"source is the name of the source connection that created the event.\"\n source: StringValueFilter\n tags: StringArrayFilter\n}\n\ninput FilterLocation {\n \"Latitude in the range [-90, 90].\"\n latitude: Float!\n \"Longitude in the range [-180, 180].\"\n longitude: Float!\n}\n\nenum FloatAggregation { AVG, MED, MAX, MIN, RAND, FIRST, LAST }\n\ninput InCircleFilter {\n center: FilterLocation!\n \"Radius of the circle around the center, in kilometers (km).\"\n radius: Float!\n}\n\ntype LatestSignal { name: String!, timestamp: Time!, valueNumber: Float, valueString: String, valueLocation: Location }\n\ntype Location { latitude: Float!, longitude: Float!, hdop: Float! }\n\nenum LocationAggregation { AVG, RAND, FIRST, LAST }\n\nenum Privilege { VEHICLE_NON_LOCATION_DATA, VEHICLE_COMMANDS, VEHICLE_CURRENT_LOCATION, VEHICLE_ALL_TIME_LOCATION, VEHICLE_VIN_CREDENTIAL, VEHICLE_APPROXIMATE_LOCATION, VEHICLE_RAW_DATA }\n\ntype Segment { start: SignalLocation!, end: SignalLocation, duration: Int!, isOngoing: Boolean!, startedBeforeRange: Boolean!, signals: [SignalAggregationValue!], eventCounts: [EventCount!] }\n\ninput SegmentConfig {\n \"\"\"\n Maximum gap (seconds) between data points before a segment is split. For\n IGNITION_DETECTION: filters noise from brief ignition OFF events. For\n FREQUENCY_ANALYSIS: maximum gap between active windows to merge. Default: 300 (5\n minutes), Min: 60, Max: 3600\n \"\"\"\n maxGapSeconds: Int = 300\n \"Minimum segment duration (seconds) to include in results. Filters very short segments (testing, engine cycling). Default: 240 (4 minutes), Min: 60, Max: 3600\"\n minSegmentDurationSeconds: Int = 240\n \"\"\"\n [FREQUENCY_ANALYSIS] Minimum signal count per window for activity detection.\n [IDLING] Minimum samples per window to consider it idle (same semantics). Higher\n values = more conservative. Lower values = more sensitive. Default: 10, Min: 1,\n Max: 3600\n \"\"\"\n signalCountThreshold: Int = 10\n \"[IDLING only] Upper bound for idle RPM. Windows with max(RPM) <= this are considered idle. Default: 1000, Min: 300, Max: 3000\"\n maxIdleRpm: Int = 1000\n \"[REFUEL and RECHARGE only] Minimum percent increase within a window to consider it a level-increase window.\"\n minIncreasePercent: Int = 15\n}\n\ninput SegmentEventRequest { name: String! }\n\ninput SegmentSignalRequest { name: String!, agg: FloatAggregation! }\n\ntype SignalAggregationValue { name: String!, agg: String!, value: Float! }\n\ntype SignalAggregations {\n \"Timestamp of the aggregated data.\"\n timestamp: Time!\n # + 117 signal fields (see SIGNAL FIELDS table above)\n}\n\ntype SignalCollection {\n \"The last time any signal was seen matching the filter.\"\n lastSeen: Time\n # + 117 signal fields (see SIGNAL FIELDS table above)\n}\n\ntype SignalDataSummary { name: String!, numberOfSignals: Uint64!, firstSeen: Time!, lastSeen: Time! }\n\ninput SignalFilter {\n \"\"\"\n Filter signals by source using an ethr DID. Example:\n \"did:ethr:137:0xcd445F4c6bDAD32b68a2939b912150Fe3C88803E\"\n \"\"\"\n source: String\n}\n\ntype SignalFloat { timestamp: Time!, value: Float! }\n\ninput SignalFloatFilter { eq: Float, neq: Float, gt: Float, lt: Float, gte: Float, lte: Float, notIn: [Float!], in: [Float!], or: [SignalFloatFilter!] }\n\ntype SignalLocation { timestamp: Time!, value: Location! }\n\ninput SignalLocationFilter {\n \"Filter for locations within a polygon. The vertices should be ordered clockwise or counterclockwise, and there must be at least 3. May produce inaccurate results around the poles and the antimeridian.\"\n inPolygon: [FilterLocation!]\n \"Filter for locations within a given distance of a given point. Distances are computed using WGS 84, and points that are exactly a distance `radius` from the `center` will be included.\"\n inCircle: InCircleFilter\n}\n\ntype SignalString { timestamp: Time!, value: String! }\n\ntype SignalsSnapshotResponse { lastSeen: Time, signals: [LatestSignal!]! }\n\nenum StringAggregation {\n \"Randomly select a value from the group.\"\n RAND\n \"Select the most frequently occurring value in the group.\"\n TOP\n \"Return a list of unique values in the group.\"\n UNIQUE\n \"Return value in group associated with the minimum time value.\"\n FIRST\n \"Return value in group associated with the maximum time value.\"\n LAST\n}\n\ninput StringArrayFilter {\n containsAny: [String!]\n containsAll: [String!]\n \"notContainsAny array of strings does not contain any of the strings in the array\"\n notContainsAny: [String!]\n \"notContainsAll array of strings does not contain all of the strings in the array\"\n notContainsAll: [String!]\n or: [StringArrayFilter!]\n}\n\ninput StringValueFilter {\n eq: String\n neq: String\n notIn: [String!]\n in: [String!]\n \"startsWith matches strings that begin with the given prefix.\"\n startsWith: String\n or: [StringValueFilter!]\n}\n" diff --git a/pkg/eventrepo/event_repo_test.go b/pkg/eventrepo/event_repo_test.go index 175dabf..769155f 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 { diff --git a/pkg/eventrepo/eventrepo.go b/pkg/eventrepo/eventrepo.go index 7e46737..baa5f3d 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/cloud_events.graphqls b/schema/cloud_events.graphqls index 9fa001f..da0cadf 100644 --- a/schema/cloud_events.graphqls +++ b/schema/cloud_events.graphqls @@ -68,8 +68,10 @@ input CloudEventFilter { extend type Query { """ Latest full cloud event matching the given subject DID and optional filter. + Tombstoned attestations are hidden from the result by default; pass + includeDeleted: true to disable tombstone suppression. """ - latestCloudEvent(subject: String!, filter: CloudEventFilter): CloudEvent! + latestCloudEvent(subject: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): CloudEvent! @mcpTool( name: "get_latest_cloud_event" description: "Get the latest full CloudEvent (header + JSON payload) for a subject DID, optionally filtered. Returns dataUrl (presigned S3 link) for large binary payloads instead of inlining them." @@ -78,8 +80,10 @@ extend type Query { """ List full cloud events matching the given subject DID and optional filter. + Tombstoned attestations are hidden from the result by default; pass + includeDeleted: true to disable tombstone suppression. """ - cloudEvents(subject: String!, limit: Int = 10, filter: CloudEventFilter): [CloudEvent!]! + cloudEvents(subject: String!, limit: Int = 10, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEvent!]! @mcpTool( name: "list_cloud_events" description: "List full CloudEvents (headers + JSON payloads) for a subject DID, optionally filtered and limited (default 10). Large binary payloads are returned as presigned S3 URLs via dataUrl instead of inlined data." @@ -87,9 +91,11 @@ extend 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(subject: String!, filter: CloudEventFilter): [CloudEventTypeSummary!]! + availableCloudEventTypes(subject: String!, filter: CloudEventFilter, includeDeleted: Boolean = false): [CloudEventTypeSummary!]! @mcpTool( name: "list_available_event_types" description: "Summarize the CloudEvent types present for a subject DID. Returns each type with its count, firstSeen, and lastSeen timestamps — useful for discovering what kinds of events exist before querying them."