diff --git a/internal/graph/arguments.go b/internal/graph/arguments.go index f02076a2..ea90c0a3 100644 --- a/internal/graph/arguments.go +++ b/internal/graph/arguments.go @@ -69,6 +69,14 @@ func addSignalAggregation(aggArgs *model.AggregatedSignalArgs, child *graphql.Fi Agg: typedAgg, Alias: alias, }) + case model.LocationAggregation: + filter, _ := child.Args["filter"].(*model.SignalLocationFilter) + aggArgs.LocationArgs = append(aggArgs.LocationArgs, model.LocationSignalArgs{ + Name: name, + Agg: typedAgg, + Alias: alias, + Filter: filter, + }) default: return fmt.Errorf("unknown aggregation type: %T", agg) } diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 2c049111..b530fee0 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -78,6 +78,12 @@ type ComplexityRoot struct { Timestamp func(childComplexity int) int } + Location struct { + Hdop func(childComplexity int) int + Latitude func(childComplexity int) int + Longitude func(childComplexity int) int + } + POMVC struct { RawVc func(childComplexity int) int RecordedBy func(childComplexity int) int @@ -113,6 +119,7 @@ type ComplexityRoot struct { ChassisAxleRow1WheelRightTirePressure func(childComplexity int, agg model.FloatAggregation, filter *model.SignalFloatFilter) int ChassisAxleRow2WheelLeftTirePressure func(childComplexity int, agg model.FloatAggregation, filter *model.SignalFloatFilter) int ChassisAxleRow2WheelRightTirePressure func(childComplexity int, agg model.FloatAggregation, filter *model.SignalFloatFilter) int + CurrentLocation func(childComplexity int, agg model.LocationAggregation, filter *model.SignalLocationFilter) int CurrentLocationAltitude func(childComplexity int, agg model.FloatAggregation, filter *model.SignalFloatFilter) int CurrentLocationApproximateLatitude func(childComplexity int, agg model.FloatAggregation) int CurrentLocationApproximateLongitude func(childComplexity int, agg model.FloatAggregation) int @@ -376,6 +383,7 @@ type SignalAggregationsResolver interface { PowertrainType(ctx context.Context, obj *model.SignalAggregations, agg model.StringAggregation) (*string, error) ServiceDistanceToService(ctx context.Context, obj *model.SignalAggregations, agg model.FloatAggregation, filter *model.SignalFloatFilter) (*float64, error) Speed(ctx context.Context, obj *model.SignalAggregations, agg model.FloatAggregation, filter *model.SignalFloatFilter) (*float64, error) + CurrentLocation(ctx context.Context, obj *model.SignalAggregations, agg model.LocationAggregation, filter *model.SignalLocationFilter) (*model.Location, error) } type executableSchema struct { @@ -502,6 +510,27 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Event.Timestamp(childComplexity), true + case "Location.hdop": + if e.complexity.Location.Hdop == nil { + break + } + + return e.complexity.Location.Hdop(childComplexity), true + + case "Location.latitude": + if e.complexity.Location.Latitude == nil { + break + } + + return e.complexity.Location.Latitude(childComplexity), true + + case "Location.longitude": + if e.complexity.Location.Longitude == nil { + break + } + + return e.complexity.Location.Longitude(childComplexity), true + case "POMVC.rawVC": if e.complexity.POMVC.RawVc == nil { break @@ -813,6 +842,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.SignalAggregations.ChassisAxleRow2WheelRightTirePressure(childComplexity, args["agg"].(model.FloatAggregation), args["filter"].(*model.SignalFloatFilter)), true + case "SignalAggregations.currentLocation": + if e.complexity.SignalAggregations.CurrentLocation == nil { + break + } + + args, err := ec.field_SignalAggregations_currentLocation_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.SignalAggregations.CurrentLocation(childComplexity, args["agg"].(model.LocationAggregation), args["filter"].(*model.SignalLocationFilter)), true + case "SignalAggregations.currentLocationAltitude": if e.complexity.SignalAggregations.CurrentLocationAltitude == nil { break @@ -2249,9 +2290,12 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputAftermarketDeviceBy, ec.unmarshalInputAttestationFilter, + ec.unmarshalInputCircleCenter, ec.unmarshalInputEventFilter, ec.unmarshalInputSignalFilter, ec.unmarshalInputSignalFloatFilter, + ec.unmarshalInputSignalLocationCircleFilter, + ec.unmarshalInputSignalLocationFilter, ec.unmarshalInputStringValueFilter, ) first := true @@ -2658,6 +2702,7 @@ input SignalFloatFilter { lte: Float notIn: [Float!] in: [Float!] + or: [SignalFloatFilter!] } `, BuiltIn: false}, {Name: "../../schema/device_activity.graphqls", Input: `extend type Query { @@ -4025,6 +4070,39 @@ extend type SignalCollection { } +`, BuiltIn: false}, + {Name: "../../schema/signals_location.graphqls", Input: `extend type SignalAggregations { + currentLocation( + agg: LocationAggregation!, + filter: SignalLocationFilter + ): Location @requiresAllOfPrivileges(privileges: [VEHICLE_ALL_TIME_LOCATION]) @goField(name: "CurrentLocation", forceResolver: true) @isSignal @hasAggregation +} + +type Location { + latitude: Float! + longitude: Float! + hdop: Float! +} + +# It's tempting to do all of the float aggregations here, but anything +# involving ordering seems tough. +enum LocationAggregation { + FIRST +} + +input SignalLocationFilter { + inCircle: SignalLocationCircleFilter +} + +input SignalLocationCircleFilter { + center: CircleCenter! + radius: Float! +} + +input CircleCenter { + latitude: Float! + longitude: Float! +} `, BuiltIn: false}, {Name: "../../schema/vc.graphqls", Input: `extend type Query { """ @@ -4649,6 +4727,57 @@ func (ec *executionContext) field_SignalAggregations_currentLocationLongitude_ar return args, nil } +func (ec *executionContext) field_SignalAggregations_currentLocation_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_SignalAggregations_currentLocation_argsAgg(ctx, rawArgs) + if err != nil { + return nil, err + } + args["agg"] = arg0 + arg1, err := ec.field_SignalAggregations_currentLocation_argsFilter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["filter"] = arg1 + return args, nil +} +func (ec *executionContext) field_SignalAggregations_currentLocation_argsAgg( + ctx context.Context, + rawArgs map[string]any, +) (model.LocationAggregation, error) { + if _, ok := rawArgs["agg"]; !ok { + var zeroVal model.LocationAggregation + return zeroVal, nil + } + + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("agg")) + if tmp, ok := rawArgs["agg"]; ok { + return ec.unmarshalNLocationAggregation2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐLocationAggregation(ctx, tmp) + } + + var zeroVal model.LocationAggregation + return zeroVal, nil +} + +func (ec *executionContext) field_SignalAggregations_currentLocation_argsFilter( + ctx context.Context, + rawArgs map[string]any, +) (*model.SignalLocationFilter, error) { + if _, ok := rawArgs["filter"]; !ok { + var zeroVal *model.SignalLocationFilter + return zeroVal, nil + } + + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + if tmp, ok := rawArgs["filter"]; ok { + return ec.unmarshalOSignalLocationFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalLocationFilter(ctx, tmp) + } + + var zeroVal *model.SignalLocationFilter + return zeroVal, nil +} + func (ec *executionContext) field_SignalAggregations_dimoAftermarketHDOP_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -6234,6 +6363,138 @@ func (ec *executionContext) fieldContext_Event_metadata(_ context.Context, field return fc, nil } +func (ec *executionContext) _Location_latitude(ctx context.Context, field graphql.CollectedField, obj *model.Location) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Location_latitude(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Latitude, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Location_latitude(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Location", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Float does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Location_longitude(ctx context.Context, field graphql.CollectedField, obj *model.Location) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Location_longitude(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Longitude, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Location_longitude(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Location", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Float does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Location_hdop(ctx context.Context, field graphql.CollectedField, obj *model.Location) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Location_hdop(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Hdop, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Location_hdop(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Location", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Float does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _POMVC_vehicleTokenId(ctx context.Context, field graphql.CollectedField, obj *model.Pomvc) (ret graphql.Marshaler) { fc, err := ec.fieldContext_POMVC_vehicleTokenId(ctx, field) if err != nil { @@ -6660,6 +6921,8 @@ func (ec *executionContext) fieldContext_Query_signals(ctx context.Context, fiel return ec.fieldContext_SignalAggregations_serviceDistanceToService(ctx, field) case "speed": return ec.fieldContext_SignalAggregations_speed(ctx, field) + case "currentLocation": + return ec.fieldContext_SignalAggregations_currentLocation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type SignalAggregations", field.Name) }, @@ -14996,6 +15259,107 @@ func (ec *executionContext) fieldContext_SignalAggregations_speed(ctx context.Co return fc, nil } +func (ec *executionContext) _SignalAggregations_currentLocation(ctx context.Context, field graphql.CollectedField, obj *model.SignalAggregations) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SignalAggregations_currentLocation(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + directive0 := func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.SignalAggregations().CurrentLocation(rctx, obj, fc.Args["agg"].(model.LocationAggregation), fc.Args["filter"].(*model.SignalLocationFilter)) + } + + directive1 := func(ctx context.Context) (any, error) { + privileges, err := ec.unmarshalNPrivilege2ᚕgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐPrivilegeᚄ(ctx, []any{"VEHICLE_ALL_TIME_LOCATION"}) + if err != nil { + var zeroVal *model.Location + return zeroVal, err + } + if ec.directives.RequiresAllOfPrivileges == nil { + var zeroVal *model.Location + return zeroVal, errors.New("directive requiresAllOfPrivileges is not implemented") + } + return ec.directives.RequiresAllOfPrivileges(ctx, obj, directive0, privileges) + } + directive2 := func(ctx context.Context) (any, error) { + if ec.directives.IsSignal == nil { + var zeroVal *model.Location + return zeroVal, errors.New("directive isSignal is not implemented") + } + return ec.directives.IsSignal(ctx, obj, directive1) + } + directive3 := func(ctx context.Context) (any, error) { + if ec.directives.HasAggregation == nil { + var zeroVal *model.Location + return zeroVal, errors.New("directive hasAggregation is not implemented") + } + return ec.directives.HasAggregation(ctx, obj, directive2) + } + + tmp, err := directive3(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*model.Location); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/DIMO-Network/telemetry-api/internal/graph/model.Location`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Location) + fc.Result = res + return ec.marshalOLocation2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐLocation(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SignalAggregations_currentLocation(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SignalAggregations", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "latitude": + return ec.fieldContext_Location_latitude(ctx, field) + case "longitude": + return ec.fieldContext_Location_longitude(ctx, field) + case "hdop": + return ec.fieldContext_Location_hdop(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Location", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_SignalAggregations_currentLocation_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _SignalCollection_lastSeen(ctx context.Context, field graphql.CollectedField, obj *model.SignalCollection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_SignalCollection_lastSeen(ctx, field) if err != nil { @@ -24045,6 +24409,40 @@ func (ec *executionContext) unmarshalInputAttestationFilter(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputCircleCenter(ctx context.Context, obj any) (model.CircleCenter, error) { + var it model.CircleCenter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"latitude", "longitude"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "latitude": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("latitude")) + data, err := ec.unmarshalNFloat2float64(ctx, v) + if err != nil { + return it, err + } + it.Latitude = data + case "longitude": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("longitude")) + data, err := ec.unmarshalNFloat2float64(ctx, v) + if err != nil { + return it, err + } + it.Longitude = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputEventFilter(ctx context.Context, obj any) (model.EventFilter, error) { var it model.EventFilter asMap := map[string]any{} @@ -24113,7 +24511,7 @@ func (ec *executionContext) unmarshalInputSignalFloatFilter(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"eq", "neq", "gt", "lt", "gte", "lte", "notIn", "in"} + fieldsInOrder := [...]string{"eq", "neq", "gt", "lt", "gte", "lte", "notIn", "in", "or"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -24176,6 +24574,74 @@ func (ec *executionContext) unmarshalInputSignalFloatFilter(ctx context.Context, return it, err } it.In = data + case "or": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("or")) + data, err := ec.unmarshalOSignalFloatFilter2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFloatFilterᚄ(ctx, v) + if err != nil { + return it, err + } + it.Or = data + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputSignalLocationCircleFilter(ctx context.Context, obj any) (model.SignalLocationCircleFilter, error) { + var it model.SignalLocationCircleFilter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"center", "radius"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "center": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("center")) + data, err := ec.unmarshalNCircleCenter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐCircleCenter(ctx, v) + if err != nil { + return it, err + } + it.Center = data + case "radius": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("radius")) + data, err := ec.unmarshalNFloat2float64(ctx, v) + if err != nil { + return it, err + } + it.Radius = data + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputSignalLocationFilter(ctx context.Context, obj any) (model.SignalLocationFilter, error) { + var it model.SignalLocationFilter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"inCircle"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "inCircle": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inCircle")) + data, err := ec.unmarshalOSignalLocationCircleFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalLocationCircleFilter(ctx, v) + if err != nil { + return it, err + } + it.InCircle = data } } @@ -24406,6 +24872,55 @@ func (ec *executionContext) _Event(ctx context.Context, sel ast.SelectionSet, ob return out } +var locationImplementors = []string{"Location"} + +func (ec *executionContext) _Location(ctx context.Context, sel ast.SelectionSet, obj *model.Location) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, locationImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Location") + case "latitude": + out.Values[i] = ec._Location_latitude(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "longitude": + out.Values[i] = ec._Location_longitude(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "hdop": + out.Values[i] = ec._Location_hdop(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var pOMVCImplementors = []string{"POMVC"} func (ec *executionContext) _POMVC(ctx context.Context, sel ast.SelectionSet, obj *model.Pomvc) graphql.Marshaler { @@ -27277,6 +27792,39 @@ func (ec *executionContext) _SignalAggregations(ctx context.Context, sel ast.Sel continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "currentLocation": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._SignalAggregations_currentLocation(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -28010,6 +28558,11 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) unmarshalNCircleCenter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐCircleCenter(ctx context.Context, v any) (*model.CircleCenter, error) { + res, err := ec.unmarshalInputCircleCenter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNEvent2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐEvent(ctx context.Context, sel ast.SelectionSet, v *model.Event) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -28062,6 +28615,16 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) unmarshalNLocationAggregation2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐLocationAggregation(ctx context.Context, v any) (model.LocationAggregation, error) { + var res model.LocationAggregation + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNLocationAggregation2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐLocationAggregation(ctx context.Context, sel ast.SelectionSet, v model.LocationAggregation) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNPrivilege2githubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐPrivilege(ctx context.Context, v any) (model.Privilege, error) { var res model.Privilege err := res.UnmarshalGQL(v) @@ -28141,6 +28704,11 @@ func (ec *executionContext) marshalNSignalAggregations2ᚖgithubᚗcomᚋDIMOᚑ return ec._SignalAggregations(ctx, sel, v) } +func (ec *executionContext) unmarshalNSignalFloatFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFloatFilter(ctx context.Context, v any) (*model.SignalFloatFilter, error) { + res, err := ec.unmarshalInputSignalFloatFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) @@ -28673,6 +29241,13 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele return res } +func (ec *executionContext) marshalOLocation2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐLocation(ctx context.Context, sel ast.SelectionSet, v *model.Location) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Location(ctx, sel, v) +} + func (ec *executionContext) marshalOPOMVC2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐPomvc(ctx context.Context, sel ast.SelectionSet, v *model.Pomvc) graphql.Marshaler { if v == nil { return graphql.Null @@ -28749,6 +29324,24 @@ func (ec *executionContext) marshalOSignalFloat2ᚖgithubᚗcomᚋDIMOᚑNetwork return ec._SignalFloat(ctx, sel, v) } +func (ec *executionContext) unmarshalOSignalFloatFilter2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFloatFilterᚄ(ctx context.Context, v any) ([]*model.SignalFloatFilter, error) { + if v == nil { + return nil, nil + } + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]*model.SignalFloatFilter, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNSignalFloatFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFloatFilter(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + func (ec *executionContext) unmarshalOSignalFloatFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalFloatFilter(ctx context.Context, v any) (*model.SignalFloatFilter, error) { if v == nil { return nil, nil @@ -28757,6 +29350,22 @@ func (ec *executionContext) unmarshalOSignalFloatFilter2ᚖgithubᚗcomᚋDIMO return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalOSignalLocationCircleFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalLocationCircleFilter(ctx context.Context, v any) (*model.SignalLocationCircleFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputSignalLocationCircleFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOSignalLocationFilter2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalLocationFilter(ctx context.Context, v any) (*model.SignalLocationFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputSignalLocationFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalOSignalString2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalString(ctx context.Context, sel ast.SelectionSet, v *model.SignalString) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 4b8bf02c..72a9d57e 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -58,6 +58,11 @@ type AttestationFilter struct { Limit *int `json:"limit,omitempty"` } +type CircleCenter struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + type DeviceActivity struct { // lastActive indicates the start of a 3 hour block during which the device was last active. LastActive *time.Time `json:"lastActive,omitempty"` @@ -83,6 +88,12 @@ type EventFilter struct { Source *StringValueFilter `json:"source,omitempty"` } +type Location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Hdop float64 `json:"hdop"` +} + type Pomvc struct { // vehicleTokenId is the token ID of the vehicle. VehicleTokenID *int `json:"vehicleTokenId,omitempty"` @@ -418,14 +429,24 @@ type SignalFloat struct { } type SignalFloatFilter struct { - Eq *float64 `json:"eq,omitempty"` - Neq *float64 `json:"neq,omitempty"` - Gt *float64 `json:"gt,omitempty"` - Lt *float64 `json:"lt,omitempty"` - Gte *float64 `json:"gte,omitempty"` - Lte *float64 `json:"lte,omitempty"` - NotIn []float64 `json:"notIn,omitempty"` - In []float64 `json:"in,omitempty"` + Eq *float64 `json:"eq,omitempty"` + Neq *float64 `json:"neq,omitempty"` + Gt *float64 `json:"gt,omitempty"` + Lt *float64 `json:"lt,omitempty"` + Gte *float64 `json:"gte,omitempty"` + Lte *float64 `json:"lte,omitempty"` + NotIn []float64 `json:"notIn,omitempty"` + In []float64 `json:"in,omitempty"` + Or []*SignalFloatFilter `json:"or,omitempty"` +} + +type SignalLocationCircleFilter struct { + Center *CircleCenter `json:"center"` + Radius float64 `json:"radius"` +} + +type SignalLocationFilter struct { + InCircle *SignalLocationCircleFilter `json:"inCircle,omitempty"` } type SignalString struct { @@ -528,6 +549,59 @@ func (e FloatAggregation) MarshalJSON() ([]byte, error) { return buf.Bytes(), nil } +type LocationAggregation string + +const ( + LocationAggregationFirst LocationAggregation = "FIRST" +) + +var AllLocationAggregation = []LocationAggregation{ + LocationAggregationFirst, +} + +func (e LocationAggregation) IsValid() bool { + switch e { + case LocationAggregationFirst: + return true + } + return false +} + +func (e LocationAggregation) String() string { + return string(e) +} + +func (e *LocationAggregation) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = LocationAggregation(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid LocationAggregation", str) + } + return nil +} + +func (e LocationAggregation) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *LocationAggregation) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e LocationAggregation) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + type Privilege string const ( diff --git a/internal/graph/model/signalArgs.go b/internal/graph/model/signalArgs.go index e7cc6ead..7cdfe529 100644 --- a/internal/graph/model/signalArgs.go +++ b/internal/graph/model/signalArgs.go @@ -43,6 +43,8 @@ type AggregatedSignalArgs struct { FloatArgs []FloatSignalArgs // StringArgs represents arguments for each string signal. StringArgs []StringSignalArgs + // LocationArgs represents arguments for each location signal. + LocationArgs []LocationSignalArgs // ApproxLocArgs ApproxLocArgs map[FloatAggregation]struct{} } @@ -70,3 +72,16 @@ type StringSignalArgs struct { // an alias then this will be the same as Name. Alias string } + +// LocationSignalArgs is the arguments for querying location signals. +type LocationSignalArgs struct { + // Name is the signal name. + Name string + // Agg is the aggregation type. + Agg LocationAggregation + // Alias is the GraphQL field alias. If the client doesn't specify + // an alias then this will be the same as Name. + Alias string + // Filter is an optional set of geographic filters. + Filter *SignalLocationFilter +} diff --git a/internal/graph/model/signal_aggs.go b/internal/graph/model/signal_aggs.go index 5950c5a1..072a4631 100644 --- a/internal/graph/model/signal_aggs.go +++ b/internal/graph/model/signal_aggs.go @@ -19,6 +19,8 @@ type SignalAggregations struct { ValueNumbers map[string]float64 `json:"-"` // Alias to value ValueStrings map[string]string `json:"-"` + // Alias to value + ValueLocations map[string]Location `json:"-"` // Aggregation cross non-approximate field name to value AppLocNumbers map[AppLocKey]float64 `json:"-"` diff --git a/internal/graph/signals_location.resolvers.go b/internal/graph/signals_location.resolvers.go new file mode 100644 index 00000000..9c53bb07 --- /dev/null +++ b/internal/graph/signals_location.resolvers.go @@ -0,0 +1,22 @@ +package graph + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.76 + +import ( + "context" + + "github.com/99designs/gqlgen/graphql" + "github.com/DIMO-Network/telemetry-api/internal/graph/model" +) + +// CurrentLocation is the resolver for the currentLocation field. +func (r *signalAggregationsResolver) CurrentLocation(ctx context.Context, obj *model.SignalAggregations, agg model.LocationAggregation, filter *model.SignalLocationFilter) (*model.Location, error) { + fieldCtx := graphql.GetFieldContext(ctx) + vl, ok := obj.ValueLocations[fieldCtx.Field.Alias] + if !ok { + return nil, nil + } + return &vl, nil +} diff --git a/internal/repositories/repositories.go b/internal/repositories/repositories.go index ffa93e63..8a4a8e97 100644 --- a/internal/repositories/repositories.go +++ b/internal/repositories/repositories.go @@ -91,10 +91,11 @@ func (r *Repository) GetSignal(ctx context.Context, aggArgs *model.AggregatedSig if !lastTS.Equal(signal.Timestamp) { lastTS = signal.Timestamp currAggs = &model.SignalAggregations{ - Timestamp: signal.Timestamp, - ValueNumbers: make(map[string]float64), - ValueStrings: make(map[string]string), - AppLocNumbers: make(map[model.AppLocKey]float64), + Timestamp: signal.Timestamp, + ValueNumbers: make(map[string]float64), + ValueStrings: make(map[string]string), + ValueLocations: make(map[string]model.Location), + AppLocNumbers: make(map[model.AppLocKey]float64), } allAggs = append(allAggs, currAggs) } @@ -118,6 +119,15 @@ func (r *Repository) GetSignal(ctx context.Context, aggArgs *model.AggregatedSig name := parityToLocationSignalName[aggParity] agg := model.AllFloatAggregation[aggIndex] currAggs.AppLocNumbers[model.AppLocKey{Aggregation: agg, Name: name}] = signal.ValueNumber + case ch.LocType: + if len(aggArgs.LocationArgs) <= int(signal.SignalIndex) { + return nil, fmt.Errorf("only %d location signal requests, but the query returned index %d", len(aggArgs.LocationArgs), signal.SignalIndex) + } + currAggs.ValueLocations[aggArgs.LocationArgs[signal.SignalIndex].Alias] = model.Location{ + Latitude: signal.ValueLocation.Latitude, + Longitude: signal.ValueLocation.Longitude, + Hdop: signal.ValueLocation.HDOP, + } default: return nil, fmt.Errorf("scanned a row with unrecognized type number %d", signal.SignalType) } diff --git a/internal/service/ch/ch.go b/internal/service/ch/ch.go index f730b033..1c22ce7e 100644 --- a/internal/service/ch/ch.go +++ b/internal/service/ch/ch.go @@ -3,7 +3,6 @@ package ch import ( "context" - "crypto/tls" "fmt" "time" @@ -41,9 +40,9 @@ func NewService(settings config.Settings) (*Service, error) { Password: settings.Clickhouse.Password, Database: settings.Clickhouse.Database, }, - TLS: &tls.Config{ - RootCAs: settings.Clickhouse.RootCAs, - }, + // TLS: &tls.Config{ + // RootCAs: settings.Clickhouse.RootCAs, + // }, Settings: map[string]any{ // ClickHouse will interrupt a query if the projected execution time exceeds the specified max_execution_time. // The estimated execution time is calculated after `timeout_before_checking_execution_speed` @@ -116,7 +115,7 @@ func (m *AliasHandleMapper) Alias(handle string) string { // The signals are sorted by timestamp in ascending order. // The timestamp on each signal is for the start of the interval. func (s *Service) GetAggregatedSignals(ctx context.Context, aggArgs *model.AggregatedSignalArgs) ([]*AggSignal, error) { - if len(aggArgs.FloatArgs) == 0 && len(aggArgs.StringArgs) == 0 && len(aggArgs.ApproxLocArgs) == 0 { + if len(aggArgs.FloatArgs) == 0 && len(aggArgs.StringArgs) == 0 && len(aggArgs.ApproxLocArgs) == 0 && len(aggArgs.LocationArgs) == 0 { return []*AggSignal{}, nil } @@ -183,6 +182,8 @@ type AggSignal struct { // ValueNumber is the value for this row if it is of float or // approximate location type. ValueString string + // ValueLocation is the value for this row if it is of location type. + ValueLocation vss.Location } func (s *Service) getAggSignals(ctx context.Context, stmt string, args []any) ([]*AggSignal, error) { @@ -190,10 +191,16 @@ func (s *Service) getAggSignals(ctx context.Context, stmt string, args []any) ([ if err != nil { return nil, fmt.Errorf("failed querying clickhouse: %w", err) } + + rows, err = s.conn.Query(ctx, stmt, args...) + if err != nil { + return nil, fmt.Errorf("failed querying clickhouse: %w", err) + } + signals := []*AggSignal{} for rows.Next() { var signal AggSignal - err := rows.Scan(&signal.SignalType, &signal.SignalIndex, &signal.Timestamp, &signal.ValueNumber, &signal.ValueString) + err := rows.Scan(&signal.SignalType, &signal.SignalIndex, &signal.Timestamp, &signal.ValueNumber, &signal.ValueString, &signal.ValueLocation) if err != nil { _ = rows.Close() return nil, fmt.Errorf("failed scanning clickhouse row: %w", err) diff --git a/internal/service/ch/queries.go b/internal/service/ch/queries.go index 11c6f1cb..bd78e384 100644 --- a/internal/service/ch/queries.go +++ b/internal/service/ch/queries.go @@ -19,6 +19,7 @@ const ( IntervalGroup = "group_timestamp" AggNumberCol = "agg_number" AggStringCol = "agg_string" + AggLocationCol = "agg_location" aggTableName = "agg_table" tokenIDWhere = vss.TokenIDCol + " = ?" eventSubjectWhere = vss.EventSubjectCol + " = ?" @@ -94,6 +95,8 @@ const ( // AppLocType is the type for rows needed to compute approximate // locations. AppLocType FieldType = 3 + // LocType is the type for rows with location tuple values. + LocType FieldType = 4 ) func (t *FieldType) Scan(value any) error { @@ -102,7 +105,7 @@ func (t *FieldType) Scan(value any) error { return fmt.Errorf("expected value of type uint8, but got type %T", value) } - if w == 0 || w > 3 { + if w == 0 || w > 4 { return fmt.Errorf("invalid value %d for field type", w) } @@ -172,6 +175,19 @@ func selectNumberAggs(numberAggs []model.FloatSignalArgs, appLocAggs map[model.F return qm.Select(caseStmt) } +func selectLocationAggs(locAggs []model.LocationSignalArgs) qm.QueryMod { + if len(locAggs) == 0 { + return qm.Select("NULL AS " + vss.ValueLocationCol) + } + // Add a CASE statement for each name and its corresponding aggregation function + caseStmts := make([]string, 0, len(locAggs)) + for i := range locAggs { // Only one aggregation, so skip this. + caseStmts = append(caseStmts, fmt.Sprintf("WHEN %s = %d AND %s = %d THEN %s", signalTypeCol, LocType, signalIndexCol, i, "argMin(value_location, timestamp)")) + } + caseStmt := fmt.Sprintf("CASE %s ELSE CAST((0, 0, 0) AS Tuple(latitude Float64, longitude Float64, hdop Float64)) END AS %s", strings.Join(caseStmts, " "), AggLocationCol) + return qm.Select(caseStmt) +} + func selectStringAggs(stringAggs []model.StringSignalArgs) qm.QueryMod { if len(stringAggs) == 0 { return qm.Select("NULL AS " + vss.ValueStringCol) @@ -351,7 +367,7 @@ func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { return "", nil, nil } - numAggs := len(aggArgs.FloatArgs) + len(aggArgs.StringArgs) + 2*len(aggArgs.ApproxLocArgs) + numAggs := len(aggArgs.FloatArgs) + len(aggArgs.StringArgs) + 2*len(aggArgs.ApproxLocArgs) + len(aggArgs.LocationArgs) if numAggs == 0 { return "", nil, errors.New("no aggregations requested") } @@ -373,6 +389,9 @@ func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { aggTableEntry(AppLocType, 2*i+1, vss.FieldCurrentLocationLatitude)) } } + for i, agg := range aggArgs.LocationArgs { + valuesArgs = append(valuesArgs, aggTableEntry(LocType, i, agg.Name)) + } valueTable := fmt.Sprintf("VALUES('%s', %s) as %s ON %s.%s = %s.%s", valueTableDef, strings.Join(valuesArgs, ", "), aggTableName, vss.TableName, vss.NameCol, aggTableName, vss.NameCol) floatFilters := []qm.QueryMod{ @@ -387,42 +406,37 @@ func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { qmhelper.Where(signalTypeCol, qmhelper.EQ, FloatType), qmhelper.Where(signalIndexCol, qmhelper.EQ, i), } - if fil := agg.Filter; fil != nil { - if fil.Eq != nil { - fieldFilters = append(fieldFilters, qmhelper.Where(vss.ValueNumberCol, qmhelper.EQ, *fil.Eq)) - } - if fil.Neq != nil { - fieldFilters = append(fieldFilters, qmhelper.Where(vss.ValueNumberCol, qmhelper.NEQ, *fil.Neq)) - } - if fil.Gt != nil { - fieldFilters = append(fieldFilters, qmhelper.Where(vss.ValueNumberCol, qmhelper.GT, *fil.Gt)) - } - if fil.Lt != nil { - fieldFilters = append(fieldFilters, qmhelper.Where(vss.ValueNumberCol, qmhelper.LT, *fil.Lt)) - } - if fil.Gte != nil { - fieldFilters = append(fieldFilters, qmhelper.Where(vss.ValueNumberCol, qmhelper.GTE, *fil.Gte)) - } - if fil.Lte != nil { - fieldFilters = append(fieldFilters, qmhelper.Where(vss.ValueNumberCol, qmhelper.LTE, *fil.Lte)) - } - if len(fil.NotIn) != 0 { - fieldFilters = append(fieldFilters, qm.WhereNotIn(vss.ValueNumberCol+" NOT IN ?", fil.NotIn)) - } - if len(fil.In) != 0 { - fieldFilters = append(fieldFilters, qm.WhereIn(vss.ValueNumberCol+" IN ?", fil.In)) - } - } + fieldFilters = append(fieldFilters, buildConditionList(agg.Filter)...) floatFilters = append(floatFilters, qm.Or2(qm.Expr(fieldFilters...))) } + locFilters := []qm.QueryMod{ + qmhelper.Where(signalTypeCol, qmhelper.NEQ, LocType), + } + + for i, agg := range aggArgs.LocationArgs { + fieldFilters := []qm.QueryMod{ + qmhelper.Where(signalTypeCol, qmhelper.EQ, LocType), + qmhelper.Where(signalIndexCol, qmhelper.EQ, i), + } + + if agg.Filter != nil && agg.Filter.InCircle != nil { + fieldFilters = append(fieldFilters, + qm.Where("greatCircleDistance(value_location.longitude, value_location.latitude, ?, ?) < ?", agg.Filter.InCircle.Center.Longitude, agg.Filter.InCircle.Center.Latitude, agg.Filter.InCircle.Radius), + ) + } + + locFilters = append(locFilters, qm.Or2(qm.Expr(fieldFilters...))) + } + mods := []qm.QueryMod{ qm.Select(signalTypeCol), qm.Select(signalIndexCol), selectInterval(aggArgs.Interval, aggArgs.FromTS), selectNumberAggs(aggArgs.FloatArgs, aggArgs.ApproxLocArgs), selectStringAggs(aggArgs.StringArgs), + selectLocationAggs(aggArgs.LocationArgs), qm.Where(tokenIDWhere, aggArgs.TokenID), qm.Where(timestampFrom, aggArgs.FromTS), qm.Where(timestampTo, aggArgs.ToTS), @@ -434,12 +448,62 @@ func getAggQuery(aggArgs *model.AggregatedSignalArgs) (string, []any, error) { qm.OrderBy(groupAsc), } mods = append(mods, getFilterMods(aggArgs.Filter)...) - mods = append(mods, qm.Expr(floatFilters...)) + mods = append(mods, qm.Expr(floatFilters...), qm.Expr(locFilters...)) stmt, args := newQuery(mods...) return stmt, args, nil } +func buildConditionList(fil *model.SignalFloatFilter) []qm.QueryMod { + if fil == nil { + return nil + } + + var mods []qm.QueryMod + + if fil.Eq != nil { + mods = append(mods, qmhelper.Where(vss.ValueNumberCol, qmhelper.EQ, *fil.Eq)) + } + if fil.Neq != nil { + mods = append(mods, qmhelper.Where(vss.ValueNumberCol, qmhelper.NEQ, *fil.Neq)) + } + if fil.Gt != nil { + mods = append(mods, qmhelper.Where(vss.ValueNumberCol, qmhelper.GT, *fil.Gt)) + } + if fil.Lt != nil { + mods = append(mods, qmhelper.Where(vss.ValueNumberCol, qmhelper.LT, *fil.Lt)) + } + if fil.Gte != nil { + mods = append(mods, qmhelper.Where(vss.ValueNumberCol, qmhelper.GTE, *fil.Gte)) + } + if fil.Lte != nil { + mods = append(mods, qmhelper.Where(vss.ValueNumberCol, qmhelper.LTE, *fil.Lte)) + } + if len(fil.NotIn) != 0 { + mods = append(mods, qm.WhereNotIn(vss.ValueNumberCol+" NOT IN ?", fil.NotIn)) + } + if len(fil.In) != 0 { + mods = append(mods, qm.WhereIn(vss.ValueNumberCol+" IN ?", fil.In)) + } + + var orMods []qm.QueryMod + for _, cond := range fil.Or { + clauseMods := buildConditionList(cond) + if len(clauseMods) != 0 { + if len(orMods) == 0 { + orMods = append(orMods, qm.Expr(clauseMods...)) + } else { + orMods = append(orMods, qm.Or2(qm.Expr(clauseMods...))) + } + } + } + if len(orMods) != 0 { + mods = append(mods, qm.Expr(orMods...)) + } + + return mods +} + func aggTableEntry(ft FieldType, index int, name string) string { return fmt.Sprintf("(%d, %d, '%s')", ft, index, name) } diff --git a/schema/base.graphqls b/schema/base.graphqls index 21a49c9f..94d1447b 100644 --- a/schema/base.graphqls +++ b/schema/base.graphqls @@ -190,4 +190,5 @@ input SignalFloatFilter { lte: Float notIn: [Float!] in: [Float!] + or: [SignalFloatFilter!] } diff --git a/schema/signals_location.graphqls b/schema/signals_location.graphqls new file mode 100644 index 00000000..6058cfef --- /dev/null +++ b/schema/signals_location.graphqls @@ -0,0 +1,32 @@ +extend type SignalAggregations { + currentLocation( + agg: LocationAggregation!, + filter: SignalLocationFilter + ): Location @requiresAllOfPrivileges(privileges: [VEHICLE_ALL_TIME_LOCATION]) @goField(name: "CurrentLocation", forceResolver: true) @isSignal @hasAggregation +} + +type Location { + latitude: Float! + longitude: Float! + hdop: Float! +} + +# It's tempting to do all of the float aggregations here, but anything +# involving ordering seems tough. +enum LocationAggregation { + FIRST +} + +input SignalLocationFilter { + inCircle: SignalLocationCircleFilter +} + +input SignalLocationCircleFilter { + center: CircleCenter! + radius: Float! +} + +input CircleCenter { + latitude: Float! + longitude: Float! +}