diff --git a/internal/api/graphql/gqlgen.yml b/internal/api/graphql/gqlgen.yml index 79b9674f..706b32d0 100644 --- a/internal/api/graphql/gqlgen.yml +++ b/internal/api/graphql/gqlgen.yml @@ -133,6 +133,8 @@ models: resolver: true issues: resolver: true + componentInstances: + resolver: true Service: fields: owners: diff --git a/internal/api/graphql/graph/baseResolver/component_instance.go b/internal/api/graphql/graph/baseResolver/component_instance.go index 83538971..fd6c1fe6 100644 --- a/internal/api/graphql/graph/baseResolver/component_instance.go +++ b/internal/api/graphql/graph/baseResolver/component_instance.go @@ -67,6 +67,7 @@ func ComponentInstanceBaseResolver(app app.Heureka, ctx context.Context, filter var imId []*int64 var serviceId []*int64 + var copmonentVersionId []*int64 if parent != nil { parentId := parent.Parent.GetID() pid, err := ParseCursor(&parentId) @@ -80,6 +81,8 @@ func ComponentInstanceBaseResolver(app app.Heureka, ctx context.Context, filter imId = []*int64{pid} case model.ServiceNodeName: serviceId = []*int64{pid} + case model.ComponentVersionNodeName: + copmonentVersionId = []*int64{pid} } } @@ -88,9 +91,10 @@ func ComponentInstanceBaseResolver(app app.Heureka, ctx context.Context, filter } f := &entity.ComponentInstanceFilter{ - Paginated: entity.Paginated{First: first, After: afterId}, - IssueMatchId: imId, - ServiceId: serviceId, + Paginated: entity.Paginated{First: first, After: afterId}, + IssueMatchId: imId, + ServiceId: serviceId, + ComponentVersionId: copmonentVersionId, } opt := GetListOptions(requestedFields) diff --git a/internal/api/graphql/graph/generated.go b/internal/api/graphql/graph/generated.go index 73f98935..269c5d93 100644 --- a/internal/api/graphql/graph/generated.go +++ b/internal/api/graphql/graph/generated.go @@ -172,11 +172,12 @@ type ComplexityRoot struct { } ComponentVersion struct { - Component func(childComplexity int) int - ComponentID func(childComplexity int) int - ID func(childComplexity int) int - Issues func(childComplexity int, first *int, after *string) int - Version func(childComplexity int) int + Component func(childComplexity int) int + ComponentID func(childComplexity int) int + ComponentInstances func(childComplexity int, first *int, after *string) int + ID func(childComplexity int) int + Issues func(childComplexity int, first *int, after *string) int + Version func(childComplexity int) int } ComponentVersionConnection struct { @@ -521,6 +522,7 @@ type ComponentInstanceResolver interface { type ComponentVersionResolver interface { Component(ctx context.Context, obj *model.ComponentVersion) (*model.Component, error) Issues(ctx context.Context, obj *model.ComponentVersion, first *int, after *string) (*model.IssueConnection, error) + ComponentInstances(ctx context.Context, obj *model.ComponentVersion, first *int, after *string) (*model.ComponentInstanceConnection, error) } type EvidenceResolver interface { Author(ctx context.Context, obj *model.Evidence) (*model.User, error) @@ -1171,6 +1173,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ComponentVersion.ComponentID(childComplexity), true + case "ComponentVersion.componentInstances": + if e.complexity.ComponentVersion.ComponentInstances == nil { + break + } + + args, err := ec.field_ComponentVersion_componentInstances_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.ComponentVersion.ComponentInstances(childComplexity, args["first"].(*int), args["after"].(*string)), true + case "ComponentVersion.id": if e.complexity.ComponentVersion.ID == nil { break @@ -3490,6 +3504,30 @@ func (ec *executionContext) field_ComponentInstance_issueMatches_args(ctx contex return args, nil } +func (ec *executionContext) field_ComponentVersion_componentInstances_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *int + if tmp, ok := rawArgs["first"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + arg0, err = ec.unmarshalOInt2ᚖint(ctx, tmp) + if err != nil { + return nil, err + } + } + args["first"] = arg0 + var arg1 *string + if tmp, ok := rawArgs["after"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["after"] = arg1 + return args, nil +} + func (ec *executionContext) field_ComponentVersion_issues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -8288,6 +8326,8 @@ func (ec *executionContext) fieldContext_ComponentInstance_componentVersion(_ co return ec.fieldContext_ComponentVersion_component(ctx, field) case "issues": return ec.fieldContext_ComponentVersion_issues(ctx, field) + case "componentInstances": + return ec.fieldContext_ComponentVersion_componentInstances(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ComponentVersion", field.Name) }, @@ -9028,6 +9068,66 @@ func (ec *executionContext) fieldContext_ComponentVersion_issues(ctx context.Con return fc, nil } +func (ec *executionContext) _ComponentVersion_componentInstances(ctx context.Context, field graphql.CollectedField, obj *model.ComponentVersion) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ComponentVersion_componentInstances(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) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ComponentVersion().ComponentInstances(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.ComponentInstanceConnection) + fc.Result = res + return ec.marshalOComponentInstanceConnection2ᚖgithubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐComponentInstanceConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ComponentVersion_componentInstances(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ComponentVersion", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "totalCount": + return ec.fieldContext_ComponentInstanceConnection_totalCount(ctx, field) + case "edges": + return ec.fieldContext_ComponentInstanceConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_ComponentInstanceConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ComponentInstanceConnection", 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_ComponentVersion_componentInstances_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _ComponentVersionConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.ComponentVersionConnection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ComponentVersionConnection_totalCount(ctx, field) if err != nil { @@ -9226,6 +9326,8 @@ func (ec *executionContext) fieldContext_ComponentVersionEdge_node(_ context.Con return ec.fieldContext_ComponentVersion_component(ctx, field) case "issues": return ec.fieldContext_ComponentVersion_issues(ctx, field) + case "componentInstances": + return ec.fieldContext_ComponentVersion_componentInstances(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ComponentVersion", field.Name) }, @@ -15187,6 +15289,8 @@ func (ec *executionContext) fieldContext_Mutation_createComponentVersion(ctx con return ec.fieldContext_ComponentVersion_component(ctx, field) case "issues": return ec.fieldContext_ComponentVersion_issues(ctx, field) + case "componentInstances": + return ec.fieldContext_ComponentVersion_componentInstances(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ComponentVersion", field.Name) }, @@ -15254,6 +15358,8 @@ func (ec *executionContext) fieldContext_Mutation_updateComponentVersion(ctx con return ec.fieldContext_ComponentVersion_component(ctx, field) case "issues": return ec.fieldContext_ComponentVersion_issues(ctx, field) + case "componentInstances": + return ec.fieldContext_ComponentVersion_componentInstances(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ComponentVersion", field.Name) }, @@ -25025,6 +25131,39 @@ func (ec *executionContext) _ComponentVersion(ctx context.Context, sel ast.Selec continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "componentInstances": + 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._ComponentVersion_componentInstances(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)) diff --git a/internal/api/graphql/graph/model/models_gen.go b/internal/api/graphql/graph/model/models_gen.go index 13a8f7bb..63dc7bd1 100644 --- a/internal/api/graphql/graph/model/models_gen.go +++ b/internal/api/graphql/graph/model/models_gen.go @@ -199,11 +199,12 @@ type ComponentInstanceInput struct { } type ComponentVersion struct { - ID string `json:"id"` - Version *string `json:"version,omitempty"` - ComponentID *string `json:"componentId,omitempty"` - Component *Component `json:"component,omitempty"` - Issues *IssueConnection `json:"issues,omitempty"` + ID string `json:"id"` + Version *string `json:"version,omitempty"` + ComponentID *string `json:"componentId,omitempty"` + Component *Component `json:"component,omitempty"` + Issues *IssueConnection `json:"issues,omitempty"` + ComponentInstances *ComponentInstanceConnection `json:"componentInstances,omitempty"` } func (ComponentVersion) IsNode() {} diff --git a/internal/api/graphql/graph/queryCollection/componentVersion/directRelations.graphql b/internal/api/graphql/graph/queryCollection/componentVersion/directRelations.graphql index 74c74eb5..9caeb6b1 100644 --- a/internal/api/graphql/graph/queryCollection/componentVersion/directRelations.graphql +++ b/internal/api/graphql/graph/queryCollection/componentVersion/directRelations.graphql @@ -33,6 +33,16 @@ query ($filter: ComponentVersionFilter, $first: Int, $after: String) { nextPageAfter } } + componentInstances { + totalCount + edges { + node { + id + ccrn + componentVersionId + } + } + } } cursor } diff --git a/internal/api/graphql/graph/resolver/component_version.go b/internal/api/graphql/graph/resolver/component_version.go index 2a32bc9f..516c1e38 100644 --- a/internal/api/graphql/graph/resolver/component_version.go +++ b/internal/api/graphql/graph/resolver/component_version.go @@ -44,6 +44,14 @@ func (r *componentVersionResolver) Issues(ctx context.Context, obj *model.Compon }) } +// ComponentInstances is the resolver for the componentInstances field. +func (r *componentVersionResolver) ComponentInstances(ctx context.Context, obj *model.ComponentVersion, first *int, after *string) (*model.ComponentInstanceConnection, error) { + return baseResolver.ComponentInstanceBaseResolver(r.App, ctx, nil, first, after, &model.NodeParent{ + Parent: obj, + ParentName: model.ComponentVersionNodeName, + }) +} + // ComponentVersion returns graph.ComponentVersionResolver implementation. func (r *Resolver) ComponentVersion() graph.ComponentVersionResolver { return &componentVersionResolver{r} diff --git a/internal/api/graphql/graph/schema/component_version.graphqls b/internal/api/graphql/graph/schema/component_version.graphqls index 74382730..c553b5d7 100644 --- a/internal/api/graphql/graph/schema/component_version.graphqls +++ b/internal/api/graphql/graph/schema/component_version.graphqls @@ -7,6 +7,7 @@ type ComponentVersion implements Node { componentId: String component: Component issues(first: Int, after: String): IssueConnection + componentInstances(first: Int, after: String): ComponentInstanceConnection } input ComponentVersionInput { diff --git a/internal/database/mariadb/component_instance.go b/internal/database/mariadb/component_instance.go index aefeb6e0..ac658008 100644 --- a/internal/database/mariadb/component_instance.go +++ b/internal/database/mariadb/component_instance.go @@ -40,6 +40,7 @@ func (s *SqlDatabase) getComponentInstanceFilterString(filter *entity.ComponentI fl = append(fl, buildFilterQuery(filter.Id, "CI.componentinstance_id = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.IssueMatchId, "IM.issuematch_id = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.ServiceId, "CI.componentinstance_service_id = ?", OP_OR)) + fl = append(fl, buildFilterQuery(filter.ComponentVersionId, "CI.componentinstance_component_version_id = ?", OP_OR)) fl = append(fl, "CI.componentinstance_deleted_at IS NULL") filterStr := combineFilterQueries(fl, OP_AND) @@ -113,6 +114,7 @@ func (s *SqlDatabase) buildComponentInstanceStatement(baseQuery string, filter * filterParameters = buildQueryParameters(filterParameters, filter.Id) filterParameters = buildQueryParameters(filterParameters, filter.IssueMatchId) filterParameters = buildQueryParameters(filterParameters, filter.ServiceId) + filterParameters = buildQueryParameters(filterParameters, filter.ComponentVersionId) if withCursor { filterParameters = append(filterParameters, cursor.Value) filterParameters = append(filterParameters, cursor.Limit) diff --git a/internal/database/mariadb/component_instance_test.go b/internal/database/mariadb/component_instance_test.go index f5c49337..e7a8351e 100644 --- a/internal/database/mariadb/component_instance_test.go +++ b/internal/database/mariadb/component_instance_test.go @@ -97,6 +97,32 @@ var _ = Describe("ComponentInstance - ", Label("database", "ComponentInstance"), Expect(entries[0]).To(BeEquivalentTo(ciId)) }) }) + It("can filter by a single componentVersion id that does exist", func() { + // select a component version + cvRow := seedCollection.ComponentVersionRows[rand.Intn(len(seedCollection.ComponentVersionRows))] + + // collect all componentInstance ids that belong to the component version + ciIds := []int64{} + for _, ciRow := range seedCollection.ComponentInstanceRows { + if ciRow.ComponentVersionId.Int64 == cvRow.Id.Int64 { + ciIds = append(ciIds, ciRow.ServiceId.Int64) + } + } + + filter := &entity.ComponentInstanceFilter{ + ComponentVersionId: []*int64{&cvRow.Id.Int64}, + } + + entries, err := db.GetAllComponentInstanceIds(filter) + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + + By("returning expected elements", func() { + Expect(len(entries)).To(BeEquivalentTo(len(ciIds))) + }) + }) }) }) }) diff --git a/internal/e2e/component_version_query_test.go b/internal/e2e/component_version_query_test.go index 533a1dbe..4e75b493 100644 --- a/internal/e2e/component_version_query_test.go +++ b/internal/e2e/component_version_query_test.go @@ -163,6 +163,14 @@ var _ = Describe("Getting ComponentVersions via API", Label("e2e", "ComponentVer Expect(issueFound).To(BeTrue(), "attached issue does exist and belongs to componentVersion") } + for _, ci := range cv.Node.ComponentInstances.Edges { + Expect(ci.Node.ID).ToNot(BeNil(), "componentInstance has a ID set") + Expect(ci.Node.Ccrn).ToNot(BeNil(), "componentInstance has ccrn set") + + Expect(*ci.Node.ComponentVersionID).To(BeEquivalentTo(cv.Node.ID)) + + } + if cv.Node.Component != nil { Expect(cv.Node.Component.ID).ToNot(BeNil(), "component has a ID set") Expect(cv.Node.Component.Name).ToNot(BeNil(), "component has a name set") diff --git a/internal/entity/component_instance.go b/internal/entity/component_instance.go index 887a65e7..0851a080 100644 --- a/internal/entity/component_instance.go +++ b/internal/entity/component_instance.go @@ -7,9 +7,10 @@ import "time" type ComponentInstanceFilter struct { Paginated - IssueMatchId []*int64 `json:"issue_match_id"` - ServiceId []*int64 `json:"service_id"` - Id []*int64 `json:"id"` + IssueMatchId []*int64 `json:"issue_match_id"` + ServiceId []*int64 `json:"service_id"` + ComponentVersionId []*int64 `json:"component_version_id"` + Id []*int64 `json:"id"` } type ComponentInstanceAggregations struct{}