From 36027d06291bf344de6ba7166b000b26ee05baf7 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Wed, 11 Dec 2024 15:16:45 +0100 Subject: [PATCH 01/11] wip --- internal/api/graphql/graph/resolver/query.go | 2 +- .../api/graphql/graph/schema/common.graphqls | 5 ++ .../graphql/graph/schema/issue_match.graphqls | 6 ++ .../api/graphql/graph/schema/query.graphqls | 2 +- internal/entity/common.go | 28 ++++++++ internal/entity/cursor.go | 69 +++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 internal/entity/cursor.go diff --git a/internal/api/graphql/graph/resolver/query.go b/internal/api/graphql/graph/resolver/query.go index 7fb5d14d..11d6d75a 100644 --- a/internal/api/graphql/graph/resolver/query.go +++ b/internal/api/graphql/graph/resolver/query.go @@ -21,7 +21,7 @@ func (r *queryResolver) Issues(ctx context.Context, filter *model.IssueFilter, f return baseResolver.IssueBaseResolver(r.App, ctx, filter, first, after, nil) } -func (r *queryResolver) IssueMatches(ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string) (*model.IssueMatchConnection, error) { +func (r *queryResolver) IssueMatches(ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy *model.IssueMatchOrderBy) (*model.IssueMatchConnection, error) { return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, nil) } diff --git a/internal/api/graphql/graph/schema/common.graphqls b/internal/api/graphql/graph/schema/common.graphqls index 11b632ae..cdd91a45 100644 --- a/internal/api/graphql/graph/schema/common.graphqls +++ b/internal/api/graphql/graph/schema/common.graphqls @@ -114,3 +114,8 @@ type Metadata { updated_at: DateTime updated_by: String } + +enum OrderDirection { + asc + desc +} diff --git a/internal/api/graphql/graph/schema/issue_match.graphqls b/internal/api/graphql/graph/schema/issue_match.graphqls index 70108ed7..617b0929 100644 --- a/internal/api/graphql/graph/schema/issue_match.graphqls +++ b/internal/api/graphql/graph/schema/issue_match.graphqls @@ -65,3 +65,9 @@ enum IssueMatchStatusValues { false_positive mitigated } + +input IssueMatchOrderBy { + primaryName: OrderDirection + targetRemediationDate: OrderDirection + componentInstanceCcrn: OrderDirection +} diff --git a/internal/api/graphql/graph/schema/query.graphqls b/internal/api/graphql/graph/schema/query.graphqls index 334c1316..411ed980 100644 --- a/internal/api/graphql/graph/schema/query.graphqls +++ b/internal/api/graphql/graph/schema/query.graphqls @@ -3,7 +3,7 @@ type Query { Issues(filter: IssueFilter, first: Int, after: String): IssueConnection - IssueMatches(filter: IssueMatchFilter, first: Int, after: String): IssueMatchConnection + IssueMatches(filter: IssueMatchFilter, first: Int, after: String, orderBy: IssueMatchOrderBy): IssueMatchConnection IssueMatchChanges(filter: IssueMatchChangeFilter, first: Int, after: String): IssueMatchChangeConnection Services(filter: ServiceFilter, first: Int, after: String): ServiceConnection Components(filter: ComponentFilter, first: Int, after: String): ComponentConnection diff --git a/internal/entity/common.go b/internal/entity/common.go index dd1ecafc..dde9d6f2 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -227,3 +227,31 @@ type Metadata struct { UpdatedBy int64 `json:"updated_by"` DeletedAt time.Time `json:"deleted_at,omitempty"` } + +type DbColumnName string + +const ( + IssueMatchId DbColumnName = "issuematch_id" + IssueMatchRating DbColumnName = "issuematch_rating" + SupportGroupName DbColumnName = "supportgroup_name" +) + +type OrderDirection string + +const ( + OrderDirectionAsc OrderDirection = "asc" + OrderDirectionDesc OrderDirection = "desc" +) + +type Order struct { + By DbColumnName + Direction OrderDirection +} + +func CreateOrderMap(order []Order) map[DbColumnName]OrderDirection { + m := map[DbColumnName]OrderDirection{} + for _, o := range order { + m[o.By] = o.Direction + } + return m +} diff --git a/internal/entity/cursor.go b/internal/entity/cursor.go new file mode 100644 index 00000000..7c10a08d --- /dev/null +++ b/internal/entity/cursor.go @@ -0,0 +1,69 @@ +package entity + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" +) + +type Field struct { + Name DbColumnName + Value any + Order OrderDirection +} + +type cursors struct { + fields []Field +} + +type NewCursor func(cursors *cursors) error + +func EncodeCursor(order []Order, opts ...NewCursor) (string, error) { + var cursors cursors + for _, opt := range opts { + err := opt(&cursors) + if err != nil { + fmt.Println("err") + return "", err + } + } + + m := CreateOrderMap(order) + for _, f := range cursors.fields { + if orderDirection, ok := m[f.Name]; ok { + f.Order = orderDirection + } + } + + var buf bytes.Buffer + encoder := base64.NewEncoder(base64.StdEncoding, &buf) + err := json.NewEncoder(encoder).Encode(cursors.fields) + if err != nil { + return "", err + } + encoder.Close() + return buf.String(), nil +} + +func DecodeCursor(cursor string) ([]Field, error) { + decoded, err := base64.StdEncoding.DecodeString(cursor) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 string: %w", err) + } + + var fields []Field + if err := json.Unmarshal(decoded, &fields); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + return fields, nil +} + +func WithIssueMatch(im IssueMatch) NewCursor { + return func(cursors *cursors) error { + cursors.fields = append(cursors.fields, Field{Name: IssueMatchId, Value: im.Id, Order: OrderDirectionAsc}) + // cursors.fields = append(cursors.fields, Field{Name: IssueMatchRating, Value: im.Rating, Order: OrderDirectionAsc}) + return nil + } +} From 95bb173a659928890a0124ed2099fad47acc0bc0 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Thu, 12 Dec 2024 09:51:33 +0100 Subject: [PATCH 02/11] wip --- .../graphql/graph/baseResolver/issue_match.go | 3 +- internal/api/graphql/graph/model/models.go | 5 ++ .../graph/resolver/component_instance.go | 2 +- .../api/graphql/graph/resolver/evidence.go | 2 +- internal/api/graphql/graph/resolver/issue.go | 2 +- internal/api/graphql/graph/resolver/query.go | 2 +- internal/app/issue/issue_handler_events.go | 2 +- .../app/issue_match/issue_match_handler.go | 2 +- .../issue_match/issue_match_handler_events.go | 2 +- internal/database/interface.go | 2 +- internal/database/mariadb/issue_match.go | 2 +- internal/database/mariadb/issue_match_test.go | 76 +++++++++---------- internal/entity/common.go | 1 + 13 files changed, 55 insertions(+), 48 deletions(-) diff --git a/internal/api/graphql/graph/baseResolver/issue_match.go b/internal/api/graphql/graph/baseResolver/issue_match.go index 5ae2aa9f..45177ab4 100644 --- a/internal/api/graphql/graph/baseResolver/issue_match.go +++ b/internal/api/graphql/graph/baseResolver/issue_match.go @@ -54,7 +54,7 @@ func SingleIssueMatchBaseResolver(app app.Heureka, ctx context.Context, parent * return &issueMatch, nil } -func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, parent *model.NodeParent) (*model.IssueMatchConnection, error) { +func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy *model.IssueMatchOrderBy, parent *model.NodeParent) (*model.IssueMatchConnection, error) { requestedFields := GetPreloads(ctx) logrus.WithFields(logrus.Fields{ "requestedFields": requestedFields, @@ -119,6 +119,7 @@ func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model. } opt := GetListOptions(requestedFields) + opt.Order = model.NewOrderEntity(orderBy) issueMatches, err := app.ListIssueMatches(f, opt) diff --git a/internal/api/graphql/graph/model/models.go b/internal/api/graphql/graph/model/models.go index 184ec176..468953ca 100644 --- a/internal/api/graphql/graph/model/models.go +++ b/internal/api/graphql/graph/model/models.go @@ -49,6 +49,11 @@ var AllIssueMatchStatusValuesOrdered = []IssueMatchStatusValues{ IssueMatchStatusValuesMitigated, } +func NewOrderEntity(orderBy *IssueMatchOrderBy) []entity.Order { + //TODO convert orderBy to entity.Order + return []entity.Order{} +} + func NewPageInfo(p *entity.PageInfo) *PageInfo { if p == nil { return nil diff --git a/internal/api/graphql/graph/resolver/component_instance.go b/internal/api/graphql/graph/resolver/component_instance.go index 9dc4d4ef..77997258 100644 --- a/internal/api/graphql/graph/resolver/component_instance.go +++ b/internal/api/graphql/graph/resolver/component_instance.go @@ -33,7 +33,7 @@ func (r *componentInstanceResolver) ComponentVersion(ctx context.Context, obj *m } func (r *componentInstanceResolver) IssueMatches(ctx context.Context, obj *model.ComponentInstance, filter *model.IssueMatchFilter, first *int, after *string) (*model.IssueMatchConnection, error) { - return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, &model.NodeParent{ + return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, nil, &model.NodeParent{ Parent: obj, ParentName: model.ComponentInstanceNodeName, }) diff --git a/internal/api/graphql/graph/resolver/evidence.go b/internal/api/graphql/graph/resolver/evidence.go index 76175a33..268e20d6 100644 --- a/internal/api/graphql/graph/resolver/evidence.go +++ b/internal/api/graphql/graph/resolver/evidence.go @@ -48,7 +48,7 @@ func (r *evidenceResolver) Activity(ctx context.Context, obj *model.Evidence) (* } func (r *evidenceResolver) IssueMatches(ctx context.Context, obj *model.Evidence, filter *model.IssueMatchFilter, first *int, after *string) (*model.IssueMatchConnection, error) { - return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, &model.NodeParent{ + return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, nil, &model.NodeParent{ Parent: obj, ParentName: model.EvidenceNodeName, }) diff --git a/internal/api/graphql/graph/resolver/issue.go b/internal/api/graphql/graph/resolver/issue.go index 36b06d40..1657474a 100644 --- a/internal/api/graphql/graph/resolver/issue.go +++ b/internal/api/graphql/graph/resolver/issue.go @@ -30,7 +30,7 @@ func (r *issueResolver) Activities(ctx context.Context, obj *model.Issue, filter } func (r *issueResolver) IssueMatches(ctx context.Context, obj *model.Issue, filter *model.IssueMatchFilter, first *int, after *string) (*model.IssueMatchConnection, error) { - return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, &model.NodeParent{ + return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, nil, &model.NodeParent{ Parent: obj, ParentName: model.IssueNodeName, }) diff --git a/internal/api/graphql/graph/resolver/query.go b/internal/api/graphql/graph/resolver/query.go index 11d6d75a..31aef1a5 100644 --- a/internal/api/graphql/graph/resolver/query.go +++ b/internal/api/graphql/graph/resolver/query.go @@ -22,7 +22,7 @@ func (r *queryResolver) Issues(ctx context.Context, filter *model.IssueFilter, f } func (r *queryResolver) IssueMatches(ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy *model.IssueMatchOrderBy) (*model.IssueMatchConnection, error) { - return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, nil) + return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, orderBy, nil) } func (r *queryResolver) IssueMatchChanges(ctx context.Context, filter *model.IssueMatchChangeFilter, first *int, after *string) (*model.IssueMatchChangeConnection, error) { diff --git a/internal/app/issue/issue_handler_events.go b/internal/app/issue/issue_handler_events.go index f553601d..4242ec62 100644 --- a/internal/app/issue/issue_handler_events.go +++ b/internal/app/issue/issue_handler_events.go @@ -158,7 +158,7 @@ func createIssueMatches( issue_matches, err := db.GetIssueMatches(&entity.IssueMatchFilter{ IssueId: []*int64{&issueId}, ComponentInstanceId: []*int64{&componentInstanceId}, - }) + }, nil) if err != nil { l.WithField("event-step", "FetchIssueMatches").WithError(err).Error("Error while fetching issue matches related to assigned Component Instance") diff --git a/internal/app/issue_match/issue_match_handler.go b/internal/app/issue_match/issue_match_handler.go index 6191b451..bafabfc1 100644 --- a/internal/app/issue_match/issue_match_handler.go +++ b/internal/app/issue_match/issue_match_handler.go @@ -44,7 +44,7 @@ func (e *IssueMatchHandlerError) Error() string { func (h *issueMatchHandler) getIssueMatchResults(filter *entity.IssueMatchFilter) ([]entity.IssueMatchResult, error) { var results []entity.IssueMatchResult - ims, err := h.database.GetIssueMatches(filter) + ims, err := h.database.GetIssueMatches(filter, nil) if err != nil { return nil, err } diff --git a/internal/app/issue_match/issue_match_handler_events.go b/internal/app/issue_match/issue_match_handler_events.go index 3003e2ce..1460b30d 100644 --- a/internal/app/issue_match/issue_match_handler_events.go +++ b/internal/app/issue_match/issue_match_handler_events.go @@ -171,7 +171,7 @@ func OnComponentVersionAssignmentToComponentInstance(db database.Database, compo issue_matches, err := db.GetIssueMatches(&entity.IssueMatchFilter{ IssueId: []*int64{&issueId}, ComponentInstanceId: []*int64{&componentInstanceID}, - }) + }, nil) if err != nil { l.WithField("event-step", "FetchIssueMatches").WithError(err).Error("Error while fetching issue matches related to assigned Component Instance") diff --git a/internal/database/interface.go b/internal/database/interface.go index 8f4cfb85..c78ca1d1 100644 --- a/internal/database/interface.go +++ b/internal/database/interface.go @@ -35,7 +35,7 @@ type Database interface { GetDefaultIssuePriority() int64 GetDefaultRepositoryName() string - GetIssueMatches(*entity.IssueMatchFilter) ([]entity.IssueMatch, error) + GetIssueMatches(*entity.IssueMatchFilter, []entity.Order) ([]entity.IssueMatch, error) GetAllIssueMatchIds(*entity.IssueMatchFilter) ([]int64, error) CountIssueMatches(filter *entity.IssueMatchFilter) (int64, error) CreateIssueMatch(*entity.IssueMatch) (*entity.IssueMatch, error) diff --git a/internal/database/mariadb/issue_match.go b/internal/database/mariadb/issue_match.go index 6ba5466a..de0c8767 100644 --- a/internal/database/mariadb/issue_match.go +++ b/internal/database/mariadb/issue_match.go @@ -209,7 +209,7 @@ func (s *SqlDatabase) GetAllIssueMatchIds(filter *entity.IssueMatchFilter) ([]in return performIdScan(stmt, filterParameters, l) } -func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter) ([]entity.IssueMatch, error) { +func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatch, error) { l := logrus.WithFields(logrus.Fields{ "filter": filter, "event": "database.GetIssueMatches", diff --git a/internal/database/mariadb/issue_match_test.go b/internal/database/mariadb/issue_match_test.go index 46dfd8fa..5bb08858 100644 --- a/internal/database/mariadb/issue_match_test.go +++ b/internal/database/mariadb/issue_match_test.go @@ -135,7 +135,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { When("Getting IssueMatches", Label("GetIssueMatches"), func() { Context("and the database is empty", func() { It("can perform the query", func() { - res, err := db.GetIssueMatches(nil) + res, err := db.GetIssueMatches(nil, nil) By("throwing no error", func() { Expect(err).To(BeNil()) @@ -147,16 +147,16 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) Context("and we have 10 IssueMatches in the database", func() { var seedCollection *test.SeedCollection - var issueMatches []mariadb.IssueMatchRow + // var issueMatches []mariadb.IssueMatchRow BeforeEach(func() { seedCollection = seeder.SeedDbWithNFakeData(10) - issueMatches = seedCollection.GetValidIssueMatchRows() + // issueMatches = seedCollection.GetValidIssueMatchRows() }) Context("and using no filter", func() { It("can fetch the items correctly", func() { - res, err := db.GetIssueMatches(nil) + res, err := db.GetIssueMatches(nil, nil) By("throwing no error", func() { Expect(err).Should(BeNil()) @@ -196,7 +196,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { Id: []*int64{&im.Id.Int64}, } - entries, err := db.GetIssueMatches(filter) + entries, err := db.GetIssueMatches(filter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) @@ -224,7 +224,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { } } - entries, err := db.GetIssueMatches(filter) + entries, err := db.GetIssueMatches(filter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) @@ -254,7 +254,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { } } - entries, err := db.GetIssueMatches(filter) + entries, err := db.GetIssueMatches(filter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) @@ -284,7 +284,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { } } - entries, err := db.GetIssueMatches(filter) + entries, err := db.GetIssueMatches(filter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) @@ -322,7 +322,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { // fixture creation does not guarantee that a support group is always present if sgFound { - entries, err := db.GetIssueMatches(filter) + entries, err := db.GetIssueMatches(filter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) @@ -340,30 +340,30 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) } }) - Context("and and we use Pagination", func() { - DescribeTable("can correctly paginate ", func(pageSize int) { - test.TestPaginationOfList( - db.GetIssueMatches, - func(first *int, after *int64) *entity.IssueMatchFilter { - return &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ - First: first, - After: after, - }, - } - }, - func(entries []entity.IssueMatch) *int64 { return &entries[len(entries)-1].Id }, - len(issueMatches), - pageSize, - ) - }, - Entry("when pageSize is 1", 1), - Entry("when pageSize is 3", 3), - Entry("when pageSize is 5", 5), - Entry("when pageSize is 11", 11), - Entry("when pageSize is 100", 100), - ) - }) + // Context("and and we use Pagination", func() { + // DescribeTable("can correctly paginate ", func(pageSize int) { + // test.TestPaginationOfList( + // db.GetIssueMatches, + // func(first *int, after *int64) *entity.IssueMatchFilter { + // return &entity.IssueMatchFilter{ + // Paginated: entity.Paginated{ + // First: first, + // After: after, + // }, + // } + // }, + // func(entries []entity.IssueMatch) *int64 { return &entries[len(entries)-1].Id }, + // len(issueMatches), + // pageSize, + // ) + // }, + // Entry("when pageSize is 1", 1), + // Entry("when pageSize is 3", 3), + // Entry("when pageSize is 5", 5), + // Entry("when pageSize is 11", 11), + // Entry("when pageSize is 100", 100), + // ) + // }) }) }) }) @@ -472,7 +472,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { Id: []*int64{&issueMatch.Id}, } - im, err := db.GetIssueMatches(issueMatchFilter) + im, err := db.GetIssueMatches(issueMatchFilter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) }) @@ -517,7 +517,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { Id: []*int64{&issueMatch.Id}, } - im, err := db.GetIssueMatches(issueMatchFilter) + im, err := db.GetIssueMatches(issueMatchFilter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) }) @@ -556,7 +556,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { Id: []*int64{&issueMatch.Id}, } - im, err := db.GetIssueMatches(issueMatchFilter) + im, err := db.GetIssueMatches(issueMatchFilter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) }) @@ -597,7 +597,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { EvidenceId: []*int64{&evidence.Id}, } - im, err := db.GetIssueMatches(issueMatchFilter) + im, err := db.GetIssueMatches(issueMatchFilter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) }) @@ -626,7 +626,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { EvidenceId: []*int64{&issueMatchEvidenceRow.EvidenceId.Int64}, } - issueMatches, err := db.GetIssueMatches(issueMatchFilter) + issueMatches, err := db.GetIssueMatches(issueMatchFilter, nil) By("throwing no error", func() { Expect(err).To(BeNil()) }) diff --git a/internal/entity/common.go b/internal/entity/common.go index dde9d6f2..491fce01 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -98,6 +98,7 @@ type ListOptions struct { ShowTotalCount bool `json:"show_total_count"` ShowPageInfo bool `json:"show_page_info"` IncludeAggregations bool `json:"include_aggregations"` + Order []Order } func NewListOptions() *ListOptions { From 43241e11402b6277adbf633934c1f2a008ff7c0e Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Thu, 9 Jan 2025 16:16:09 +0100 Subject: [PATCH 03/11] wip --- .../api/graphql/graph/baseResolver/common.go | 1 + .../graphql/graph/baseResolver/issue_match.go | 6 +- internal/api/graphql/graph/model/models.go | 27 +- .../issueMatch/withOrder.graphql | 47 +++ internal/api/graphql/graph/resolver/query.go | 2 +- .../graphql/graph/schema/issue_match.graphqls | 11 +- .../api/graphql/graph/schema/query.graphqls | 2 +- internal/app/issue/issue_handler_events.go | 2 +- .../app/issue/issue_handler_events_test.go | 4 +- .../app/issue_match/issue_match_handler.go | 9 +- .../issue_match/issue_match_handler_test.go | 26 +- internal/database/mariadb/database.go | 8 + internal/database/mariadb/issue_match.go | 39 +- internal/database/mariadb/issue_match_test.go | 392 ++++++++++++++++-- internal/database/mariadb/test/common.go | 56 +++ internal/database/mariadb/test/fixture.go | 19 +- internal/e2e/issue_match_query_test.go | 96 +++++ internal/entity/common.go | 54 ++- 18 files changed, 727 insertions(+), 74 deletions(-) create mode 100644 internal/api/graphql/graph/queryCollection/issueMatch/withOrder.graphql diff --git a/internal/api/graphql/graph/baseResolver/common.go b/internal/api/graphql/graph/baseResolver/common.go index c76db9d1..b5fdc47b 100644 --- a/internal/api/graphql/graph/baseResolver/common.go +++ b/internal/api/graphql/graph/baseResolver/common.go @@ -103,5 +103,6 @@ func GetListOptions(requestedFields []string) *entity.ListOptions { ShowTotalCount: lo.Contains(requestedFields, "totalCount"), ShowPageInfo: lo.Contains(requestedFields, "pageInfo"), IncludeAggregations: lo.Contains(requestedFields, "edges.node.objectMetadata"), + Order: []entity.Order{}, } } diff --git a/internal/api/graphql/graph/baseResolver/issue_match.go b/internal/api/graphql/graph/baseResolver/issue_match.go index 45177ab4..808ddba6 100644 --- a/internal/api/graphql/graph/baseResolver/issue_match.go +++ b/internal/api/graphql/graph/baseResolver/issue_match.go @@ -54,7 +54,7 @@ func SingleIssueMatchBaseResolver(app app.Heureka, ctx context.Context, parent * return &issueMatch, nil } -func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy *model.IssueMatchOrderBy, parent *model.NodeParent) (*model.IssueMatchConnection, error) { +func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy []*model.IssueMatchOrderBy, parent *model.NodeParent) (*model.IssueMatchConnection, error) { requestedFields := GetPreloads(ctx) logrus.WithFields(logrus.Fields{ "requestedFields": requestedFields, @@ -119,7 +119,9 @@ func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model. } opt := GetListOptions(requestedFields) - opt.Order = model.NewOrderEntity(orderBy) + for _, o := range orderBy { + opt.Order = append(opt.Order, o.ToOrderEntity()) + } issueMatches, err := app.ListIssueMatches(f, opt) diff --git a/internal/api/graphql/graph/model/models.go b/internal/api/graphql/graph/model/models.go index 468953ca..50d45683 100644 --- a/internal/api/graphql/graph/model/models.go +++ b/internal/api/graphql/graph/model/models.go @@ -49,9 +49,30 @@ var AllIssueMatchStatusValuesOrdered = []IssueMatchStatusValues{ IssueMatchStatusValuesMitigated, } -func NewOrderEntity(orderBy *IssueMatchOrderBy) []entity.Order { - //TODO convert orderBy to entity.Order - return []entity.Order{} +type HasToEntity interface { + ToOrderEntity() entity.Order +} + +func (od *OrderDirection) ToOrderDirectionEntity() entity.OrderDirection { + direction := entity.OrderDirectionAsc + if *od == OrderDirectionDesc { + direction = entity.OrderDirectionDesc + } + return direction +} + +func (imo *IssueMatchOrderBy) ToOrderEntity() entity.Order { + var order entity.Order + switch *imo.By { + case IssueMatchOrderByFieldPrimaryName: + order.By = entity.IssuePrimaryName + case IssueMatchOrderByFieldComponentInstanceCcrn: + order.By = entity.ComponentInstanceCcrn + case IssueMatchOrderByFieldTargetRemediationDate: + order.By = entity.IssueMatchTargetRemediationDate + } + order.Direction = imo.Direction.ToOrderDirectionEntity() + return order } func NewPageInfo(p *entity.PageInfo) *PageInfo { diff --git a/internal/api/graphql/graph/queryCollection/issueMatch/withOrder.graphql b/internal/api/graphql/graph/queryCollection/issueMatch/withOrder.graphql new file mode 100644 index 00000000..90ca7c99 --- /dev/null +++ b/internal/api/graphql/graph/queryCollection/issueMatch/withOrder.graphql @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ($filter: IssueMatchFilter, $first: Int, $after: String, $orderBy: [IssueMatchOrderBy]) { + IssueMatches ( + filter: $filter, + first: $first, + after: $after + orderBy: $orderBy + ) { + totalCount + edges { + node { + id + targetRemediationDate + severity { + value + score + } + issueId + issue { + id + primaryName + } + componentInstanceId + componentInstance { + id + ccrn + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + isValidPage + pageNumber + nextPageAfter + pages { + after + isCurrent + pageNumber + pageCount + } + } + } +} diff --git a/internal/api/graphql/graph/resolver/query.go b/internal/api/graphql/graph/resolver/query.go index 31aef1a5..78785c4f 100644 --- a/internal/api/graphql/graph/resolver/query.go +++ b/internal/api/graphql/graph/resolver/query.go @@ -21,7 +21,7 @@ func (r *queryResolver) Issues(ctx context.Context, filter *model.IssueFilter, f return baseResolver.IssueBaseResolver(r.App, ctx, filter, first, after, nil) } -func (r *queryResolver) IssueMatches(ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy *model.IssueMatchOrderBy) (*model.IssueMatchConnection, error) { +func (r *queryResolver) IssueMatches(ctx context.Context, filter *model.IssueMatchFilter, first *int, after *string, orderBy []*model.IssueMatchOrderBy) (*model.IssueMatchConnection, error) { return baseResolver.IssueMatchBaseResolver(r.App, ctx, filter, first, after, orderBy, nil) } diff --git a/internal/api/graphql/graph/schema/issue_match.graphqls b/internal/api/graphql/graph/schema/issue_match.graphqls index 617b0929..481ed87a 100644 --- a/internal/api/graphql/graph/schema/issue_match.graphqls +++ b/internal/api/graphql/graph/schema/issue_match.graphqls @@ -67,7 +67,12 @@ enum IssueMatchStatusValues { } input IssueMatchOrderBy { - primaryName: OrderDirection - targetRemediationDate: OrderDirection - componentInstanceCcrn: OrderDirection + by: IssueMatchOrderByField + direction: OrderDirection +} + +enum IssueMatchOrderByField { + primaryName + targetRemediationDate + componentInstanceCcrn } diff --git a/internal/api/graphql/graph/schema/query.graphqls b/internal/api/graphql/graph/schema/query.graphqls index 411ed980..e63cdc12 100644 --- a/internal/api/graphql/graph/schema/query.graphqls +++ b/internal/api/graphql/graph/schema/query.graphqls @@ -3,7 +3,7 @@ type Query { Issues(filter: IssueFilter, first: Int, after: String): IssueConnection - IssueMatches(filter: IssueMatchFilter, first: Int, after: String, orderBy: IssueMatchOrderBy): IssueMatchConnection + IssueMatches(filter: IssueMatchFilter, first: Int, after: String, orderBy: [IssueMatchOrderBy]): IssueMatchConnection IssueMatchChanges(filter: IssueMatchChangeFilter, first: Int, after: String): IssueMatchChangeConnection Services(filter: ServiceFilter, first: Int, after: String): ServiceConnection Components(filter: ComponentFilter, first: Int, after: String): ComponentConnection diff --git a/internal/app/issue/issue_handler_events.go b/internal/app/issue/issue_handler_events.go index 4242ec62..5b09deb8 100644 --- a/internal/app/issue/issue_handler_events.go +++ b/internal/app/issue/issue_handler_events.go @@ -158,7 +158,7 @@ func createIssueMatches( issue_matches, err := db.GetIssueMatches(&entity.IssueMatchFilter{ IssueId: []*int64{&issueId}, ComponentInstanceId: []*int64{&componentInstanceId}, - }, nil) + }, []entity.Order{}) if err != nil { l.WithField("event-step", "FetchIssueMatches").WithError(err).Error("Error while fetching issue matches related to assigned Component Instance") diff --git a/internal/app/issue/issue_handler_events_test.go b/internal/app/issue/issue_handler_events_test.go index e6422882..06856757 100644 --- a/internal/app/issue/issue_handler_events_test.go +++ b/internal/app/issue/issue_handler_events_test.go @@ -88,7 +88,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV db.On("GetIssueMatches", &entity.IssueMatchFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, IssueId: []*int64{&issueEntity.Id}, - }).Return([]entity.IssueMatch{}, nil) + }, []entity.Order{}).Return([]entity.IssueMatch{}, nil) db.On("GetServiceIssueVariants", &entity.ServiceIssueVariantFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, @@ -123,7 +123,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV db.On("GetIssueMatches", &entity.IssueMatchFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, IssueId: []*int64{&issueEntity.Id}, - }).Return([]entity.IssueMatch{existingMatch}, nil) + }, []entity.Order{}).Return([]entity.IssueMatch{existingMatch}, nil) issue.OnComponentVersionAttachmentToIssue(db, event) db.AssertNotCalled(GinkgoT(), "CreateIssueMatch", mock.Anything) diff --git a/internal/app/issue_match/issue_match_handler.go b/internal/app/issue_match/issue_match_handler.go index bafabfc1..5ed01aa9 100644 --- a/internal/app/issue_match/issue_match_handler.go +++ b/internal/app/issue_match/issue_match_handler.go @@ -42,9 +42,9 @@ func (e *IssueMatchHandlerError) Error() string { return e.message } -func (h *issueMatchHandler) getIssueMatchResults(filter *entity.IssueMatchFilter) ([]entity.IssueMatchResult, error) { +func (h *issueMatchHandler) getIssueMatchResults(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatchResult, error) { var results []entity.IssueMatchResult - ims, err := h.database.GetIssueMatches(filter, nil) + ims, err := h.database.GetIssueMatches(filter, order) if err != nil { return nil, err } @@ -65,7 +65,8 @@ func (im *issueMatchHandler) GetIssueMatch(issueMatchId int64) (*entity.IssueMat "id": issueMatchId, }) issueMatchFilter := entity.IssueMatchFilter{Id: []*int64{&issueMatchId}} - issueMatches, err := im.ListIssueMatches(&issueMatchFilter, &entity.ListOptions{}) + options := entity.ListOptions{Order: []entity.Order{}} + issueMatches, err := im.ListIssueMatches(&issueMatchFilter, &options) if err != nil { l.Error(err) @@ -95,7 +96,7 @@ func (im *issueMatchHandler) ListIssueMatches(filter *entity.IssueMatchFilter, o "filter": filter, }) - res, err := im.getIssueMatchResults(filter) + res, err := im.getIssueMatchResults(filter, options.Order) if err != nil { l.Error(err) diff --git a/internal/app/issue_match/issue_match_handler_test.go b/internal/app/issue_match/issue_match_handler_test.go index 5155e289..1eeb6411 100644 --- a/internal/app/issue_match/issue_match_handler_test.go +++ b/internal/app/issue_match/issue_match_handler_test.go @@ -71,7 +71,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), BeforeEach(func() { options.ShowTotalCount = true - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, nil) db.On("CountIssueMatches", filter).Return(int64(1337), nil) }) @@ -97,7 +97,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), i++ ids = append(ids, i) } - db.On("GetIssueMatches", filter).Return(matches, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return(matches, nil) db.On("GetAllIssueMatchIds", filter).Return(ids, nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) res, err := issueMatchHandler.ListIssueMatches(filter, options) @@ -121,7 +121,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), Context("and the given filter does not have any matches in the database", func() { BeforeEach(func() { - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, nil) }) It("should return an empty result", func() { @@ -134,7 +134,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), }) Context("and the filter does have results in the database", func() { BeforeEach(func() { - db.On("GetIssueMatches", filter).Return(test.NNewFakeIssueMatches(15), nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return(test.NNewFakeIssueMatches(15), nil) }) It("should return the expected matches in the result", func() { issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) @@ -146,7 +146,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), Context("and the database operations throw an error", func() { BeforeEach(func() { - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{}, errors.New("some error")) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, errors.New("some error")) }) It("should return the expected matches in the result", func() { @@ -250,7 +250,7 @@ var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), f issueMatch.Status = entity.NewIssueMatchStatusValue("new") } filter.Id = []*int64{&issueMatch.Id} - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{issueMatch}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{issueMatch}, nil) updatedIssueMatch, err := issueMatchHandler.UpdateIssueMatch(&issueMatch) Expect(err).To(BeNil(), "no error should be thrown") By("setting fields", func() { @@ -273,6 +273,7 @@ var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), f issueMatchHandler im.IssueMatchHandler id int64 filter *entity.IssueMatchFilter + options *entity.ListOptions ) BeforeEach(func() { @@ -287,17 +288,18 @@ var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), f After: &after, }, } + options = entity.NewListOptions() }) It("deletes issueMatch", func() { db.On("DeleteIssueMatch", id).Return(nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, nil) err := issueMatchHandler.DeleteIssueMatch(id) Expect(err).To(BeNil(), "no error should be thrown") filter.Id = []*int64{&id} - issueMatches, err := issueMatchHandler.ListIssueMatches(filter, &entity.ListOptions{}) + issueMatches, err := issueMatchHandler.ListIssueMatches(filter, options) Expect(err).To(BeNil(), "no error should be thrown") Expect(issueMatches.Elements).To(BeEmpty(), "no error should be thrown") }) @@ -330,7 +332,7 @@ var _ = Describe("When modifying relationship of evidence and issueMatch", Label It("adds evidence to issueMatch", func() { db.On("AddEvidenceToIssueMatch", issueMatch.Id, evidence.Id).Return(nil) - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{issueMatch}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{issueMatch}, nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) issueMatch, err := issueMatchHandler.AddEvidenceToIssueMatch(issueMatch.Id, evidence.Id) Expect(err).To(BeNil(), "no error should be thrown") @@ -339,7 +341,7 @@ var _ = Describe("When modifying relationship of evidence and issueMatch", Label It("removes evidence from issueMatch", func() { db.On("RemoveEvidenceFromIssueMatch", issueMatch.Id, evidence.Id).Return(nil) - db.On("GetIssueMatches", filter).Return([]entity.IssueMatch{issueMatch}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{issueMatch}, nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) issueMatch, err := issueMatchHandler.RemoveEvidenceFromIssueMatch(issueMatch.Id, evidence.Id) Expect(err).To(BeNil(), "no error should be thrown") @@ -467,7 +469,7 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC }) It("should create issue matches for each issue", func() { - db.On("GetIssueMatches", mock.Anything).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", mock.Anything, mock.Anything).Return([]entity.IssueMatch{}, nil) // Mock CreateIssueMatch db.On("CreateIssueMatch", mock.AnythingOfType("*entity.IssueMatch")).Return(&entity.IssueMatch{}, nil).Twice() im.OnComponentVersionAssignmentToComponentInstance(db, componentInstanceID, componentVersionID) @@ -482,7 +484,7 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC issueMatch := test.NewFakeIssueMatch() issueMatch.IssueId = 2 // issue2.Id //when issueid is 2 return a fake issue match - db.On("GetIssueMatches", mock.Anything).Return([]entity.IssueMatch{issueMatch}, nil).Once() + db.On("GetIssueMatches", mock.Anything, mock.Anything).Return([]entity.IssueMatch{issueMatch}, nil).Once() }) It("should should not create new issues", func() { diff --git a/internal/database/mariadb/database.go b/internal/database/mariadb/database.go index 55dd60a8..c5592fbc 100644 --- a/internal/database/mariadb/database.go +++ b/internal/database/mariadb/database.go @@ -391,3 +391,11 @@ func getCursor(p entity.Paginated, filterStr string, stmt string) entity.Cursor Limit: limit, } } + +func GetDefaultOrder(order []entity.Order, by entity.DbColumnName, direction entity.OrderDirection) []entity.Order { + if len(order) == 0 { + order = append([]entity.Order{{By: by, Direction: direction}}, order...) + } + + return order +} diff --git a/internal/database/mariadb/issue_match.go b/internal/database/mariadb/issue_match.go index de0c8767..1f613b7f 100644 --- a/internal/database/mariadb/issue_match.go +++ b/internal/database/mariadb/issue_match.go @@ -9,6 +9,7 @@ import ( "github.com/cloudoperators/heureka/internal/entity" "github.com/jmoiron/sqlx" + "github.com/samber/lo" "github.com/sirupsen/logrus" ) @@ -47,10 +48,16 @@ func (s *SqlDatabase) getIssueMatchFilterString(filter *entity.IssueMatchFilter) return combineFilterQueries(fl, OP_AND) } -func (s *SqlDatabase) getIssueMatchJoins(filter *entity.IssueMatchFilter) string { +func (s *SqlDatabase) getIssueMatchJoins(filter *entity.IssueMatchFilter, order []entity.Order) string { joins := "" + orderByIssuePrimaryName := lo.ContainsBy(order, func(o entity.Order) bool { + return o.By == entity.IssuePrimaryName + }) + orderByCiCcrn := lo.ContainsBy(order, func(o entity.Order) bool { + return o.By == entity.ComponentInstanceCcrn + }) - if len(filter.Search) > 0 || len(filter.IssueType) > 0 || len(filter.PrimaryName) > 0 { + if len(filter.Search) > 0 || len(filter.IssueType) > 0 || len(filter.PrimaryName) > 0 || orderByIssuePrimaryName { joins = fmt.Sprintf("%s\n%s", joins, ` LEFT JOIN Issue I on I.issue_id = IM.issuematch_issue_id `) @@ -93,6 +100,13 @@ func (s *SqlDatabase) getIssueMatchJoins(filter *entity.IssueMatchFilter) string `) } } + + if orderByCiCcrn { + joins = fmt.Sprintf("%s\n%s", joins, ` + LEFT JOIN ComponentInstance CI on CI.componentinstance_id = IM.issuematch_component_instance_id + `) + } + return joins } @@ -128,14 +142,16 @@ func (s *SqlDatabase) getIssueMatchUpdateFields(issueMatch *entity.IssueMatch) s return strings.Join(fl, ", ") } -func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity.IssueMatchFilter, withCursor bool, l *logrus.Entry) (*sqlx.Stmt, []interface{}, error) { +func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity.IssueMatchFilter, withCursor bool, order []entity.Order, l *logrus.Entry) (*sqlx.Stmt, []interface{}, error) { var query string filter = s.ensureIssueMatchFilter(filter) l.WithFields(logrus.Fields{"filter": filter}) filterStr := s.getIssueMatchFilterString(filter) - joins := s.getIssueMatchJoins(filter) + joins := s.getIssueMatchJoins(filter, order) cursor := getCursor(filter.Paginated, filterStr, "IM.issuematch_id > ?") + order = GetDefaultOrder(order, entity.IssueMatchId, entity.OrderDirectionAsc) + orderStr := entity.CreateOrderString(order) whereClause := "" if filterStr != "" || withCursor { @@ -144,9 +160,9 @@ func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity. // construct final query if withCursor { - query = fmt.Sprintf(baseQuery, joins, whereClause, cursor.Statement) + query = fmt.Sprintf(baseQuery, joins, whereClause, cursor.Statement, orderStr) } else { - query = fmt.Sprintf(baseQuery, joins, whereClause) + query = fmt.Sprintf(baseQuery, joins, whereClause, orderStr) } //construct prepared statement and if where clause does exist add parameters @@ -197,10 +213,10 @@ func (s *SqlDatabase) GetAllIssueMatchIds(filter *entity.IssueMatchFilter) ([]in baseQuery := ` SELECT IM.issuematch_id FROM IssueMatch IM %s - %s GROUP BY IM.issuematch_id ORDER BY IM.issuematch_id + %s GROUP BY IM.issuematch_id ORDER BY %s ` - stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, false, l) + stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, false, []entity.Order{}, l) if err != nil { return nil, err @@ -218,10 +234,10 @@ func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []e baseQuery := ` SELECT IM.* FROM IssueMatch IM %s - %s %s GROUP BY IM.issuematch_id ORDER BY IM.issuematch_id LIMIT ? + %s %s GROUP BY IM.issuematch_id ORDER BY %s LIMIT ? ` - stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, true, l) + stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, true, order, l) if err != nil { return nil, err @@ -247,9 +263,10 @@ func (s *SqlDatabase) CountIssueMatches(filter *entity.IssueMatchFilter) (int64, SELECT count(distinct IM.issuematch_id) FROM IssueMatch IM %s %s + ORDER BY %s ` - stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, false, l) + stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, false, []entity.Order{}, l) if err != nil { return -1, err diff --git a/internal/database/mariadb/issue_match_test.go b/internal/database/mariadb/issue_match_test.go index 5bb08858..9f26f82d 100644 --- a/internal/database/mariadb/issue_match_test.go +++ b/internal/database/mariadb/issue_match_test.go @@ -5,6 +5,8 @@ package mariadb_test import ( "math/rand" + "sort" + "time" "github.com/samber/lo" @@ -147,11 +149,11 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) Context("and we have 10 IssueMatches in the database", func() { var seedCollection *test.SeedCollection - // var issueMatches []mariadb.IssueMatchRow + var issueMatches []mariadb.IssueMatchRow BeforeEach(func() { seedCollection = seeder.SeedDbWithNFakeData(10) - // issueMatches = seedCollection.GetValidIssueMatchRows() + issueMatches = seedCollection.GetValidIssueMatchRows() }) Context("and using no filter", func() { @@ -340,30 +342,31 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) } }) - // Context("and and we use Pagination", func() { - // DescribeTable("can correctly paginate ", func(pageSize int) { - // test.TestPaginationOfList( - // db.GetIssueMatches, - // func(first *int, after *int64) *entity.IssueMatchFilter { - // return &entity.IssueMatchFilter{ - // Paginated: entity.Paginated{ - // First: first, - // After: after, - // }, - // } - // }, - // func(entries []entity.IssueMatch) *int64 { return &entries[len(entries)-1].Id }, - // len(issueMatches), - // pageSize, - // ) - // }, - // Entry("when pageSize is 1", 1), - // Entry("when pageSize is 3", 3), - // Entry("when pageSize is 5", 5), - // Entry("when pageSize is 11", 11), - // Entry("when pageSize is 100", 100), - // ) - // }) + Context("and and we use Pagination", func() { + DescribeTable("can correctly paginate ", func(pageSize int) { + test.TestPaginationOfListWithOrder( + db.GetIssueMatches, + func(first *int, after *int64) *entity.IssueMatchFilter { + return &entity.IssueMatchFilter{ + Paginated: entity.Paginated{ + First: first, + After: after, + }, + } + }, + []entity.Order{}, + func(entries []entity.IssueMatch) *int64 { return &entries[len(entries)-1].Id }, + len(issueMatches), + pageSize, + ) + }, + Entry("when pageSize is 1", 1), + Entry("when pageSize is 3", 3), + Entry("when pageSize is 5", 5), + Entry("when pageSize is 11", 11), + Entry("when pageSize is 100", 100), + ) + }) }) }) }) @@ -638,3 +641,340 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) }) }) + +var _ = Describe("Ordering IssueMatches", func() { + var db *mariadb.SqlDatabase + var seeder *test.DatabaseSeeder + var seedCollection *test.SeedCollection + + BeforeEach(func() { + var err error + db = dbm.NewTestSchema() + seeder, err = test.NewDatabaseSeeder(dbm.DbConfig()) + Expect(err).To(BeNil(), "Database Seeder Setup should work") + }) + + var testOrder = func( + order []entity.Order, + verifyFunc func(res []entity.IssueMatch), + ) { + res, err := db.GetIssueMatches(nil, order) + + By("throwing no error", func() { + Expect(err).Should(BeNil()) + }) + + By("returning the correct number of results", func() { + Expect(len(res)).Should(BeIdenticalTo(len(seedCollection.IssueMatchRows))) + }) + + By("returning the correct order", func() { + verifyFunc(res) + }) + } + + When("with ASC order", Label("IssueMatchASCOrder"), func() { + + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + }) + + It("can order by id", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + return seedCollection.IssueMatchRows[i].Id.Int64 < seedCollection.IssueMatchRows[j].Id.Int64 + }) + + order := []entity.Order{ + {By: entity.IssueMatchId, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + for i, r := range res { + Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) + } + }) + }) + + It("can order by primaryName", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + issueI := seedCollection.GetIssueById(seedCollection.IssueMatchRows[i].IssueId.Int64) + issueJ := seedCollection.GetIssueById(seedCollection.IssueMatchRows[j].IssueId.Int64) + return issueI.PrimaryName.String < issueJ.PrimaryName.String + }) + + order := []entity.Order{ + {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prev string = "" + for _, r := range res { + issue := seedCollection.GetIssueById(r.IssueId) + Expect(issue).ShouldNot(BeNil()) + Expect(issue.PrimaryName.String >= prev).Should(BeTrue()) + prev = issue.PrimaryName.String + } + }) + }) + + It("can order by targetRemediationDate", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + return seedCollection.IssueMatchRows[i].TargetRemediationDate.Time.After(seedCollection.IssueMatchRows[j].TargetRemediationDate.Time) + }) + + order := []entity.Order{ + {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prev time.Time = time.Time{} + for _, r := range res { + Expect(r.TargetRemediationDate.After(prev)).Should(BeTrue()) + prev = r.TargetRemediationDate + + } + }) + }) + + It("can order by rating", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + r1 := test.SeverityToNumerical(seedCollection.IssueMatchRows[i].Rating.String) + r2 := test.SeverityToNumerical(seedCollection.IssueMatchRows[j].Rating.String) + return r1 < r2 + }) + + order := []entity.Order{ + {By: entity.IssueMatchRating, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + for i, r := range res { + Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) + } + }) + }) + + It("can order by component instance ccrn", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + ciI := seedCollection.GetComponentInstanceById(seedCollection.IssueMatchRows[i].ComponentInstanceId.Int64) + ciJ := seedCollection.GetComponentInstanceById(seedCollection.IssueMatchRows[j].ComponentInstanceId.Int64) + return ciI.CCRN.String < ciJ.CCRN.String + }) + + order := []entity.Order{ + {By: entity.ComponentInstanceCcrn, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prev string = "" + for _, r := range res { + ci := seedCollection.GetComponentInstanceById(r.ComponentInstanceId) + Expect(ci).ShouldNot(BeNil()) + Expect(ci.CCRN.String >= prev).Should(BeTrue()) + prev = ci.CCRN.String + } + }) + }) + }) + + When("with DESC order", Label("IssueMatchDESCOrder"), func() { + + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + }) + + It("can order by id", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + return seedCollection.IssueMatchRows[i].Id.Int64 > seedCollection.IssueMatchRows[j].Id.Int64 + }) + + order := []entity.Order{ + {By: entity.IssueMatchId, Direction: entity.OrderDirectionDesc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + for i, r := range res { + Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) + } + }) + }) + + It("can order by primaryName", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + issueI := seedCollection.GetIssueById(seedCollection.IssueMatchRows[i].IssueId.Int64) + issueJ := seedCollection.GetIssueById(seedCollection.IssueMatchRows[j].IssueId.Int64) + return issueI.PrimaryName.String > issueJ.PrimaryName.String + }) + + order := []entity.Order{ + {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionDesc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prev string = "\U0010FFFF" + for _, r := range res { + issue := seedCollection.GetIssueById(r.IssueId) + Expect(issue).ShouldNot(BeNil()) + Expect(issue.PrimaryName.String <= prev).Should(BeTrue()) + prev = issue.PrimaryName.String + } + }) + }) + + It("can order by targetRemediationDate", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + return seedCollection.IssueMatchRows[i].TargetRemediationDate.Time.Before(seedCollection.IssueMatchRows[j].TargetRemediationDate.Time) + }) + + order := []entity.Order{ + {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionDesc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prev time.Time = time.Now() + for _, r := range res { + Expect(r.TargetRemediationDate.Before(prev)).Should(BeTrue()) + prev = r.TargetRemediationDate + + } + }) + }) + + It("can order by rating", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + r1 := test.SeverityToNumerical(seedCollection.IssueMatchRows[i].Rating.String) + r2 := test.SeverityToNumerical(seedCollection.IssueMatchRows[j].Rating.String) + return r1 > r2 + }) + + order := []entity.Order{ + {By: entity.IssueMatchRating, Direction: entity.OrderDirectionDesc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + for i, r := range res { + Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) + } + }) + }) + + It("can order by component instance ccrn", func() { + sort.Slice(seedCollection.IssueMatchRows, func(i, j int) bool { + ciI := seedCollection.GetComponentInstanceById(seedCollection.IssueMatchRows[i].ComponentInstanceId.Int64) + ciJ := seedCollection.GetComponentInstanceById(seedCollection.IssueMatchRows[j].ComponentInstanceId.Int64) + return ciI.CCRN.String > ciJ.CCRN.String + }) + + order := []entity.Order{ + {By: entity.ComponentInstanceCcrn, Direction: entity.OrderDirectionDesc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prev string = "\U0010FFFF" + for _, r := range res { + ci := seedCollection.GetComponentInstanceById(r.ComponentInstanceId) + Expect(ci).ShouldNot(BeNil()) + Expect(ci.CCRN.String <= prev).Should(BeTrue()) + prev = ci.CCRN.String + } + }) + }) + }) + + When("multiple order by used", Label("IssueMatchMultipleOrderBy"), func() { + + BeforeEach(func() { + users := seeder.SeedUsers(10) + services := seeder.SeedServices(10) + components := seeder.SeedComponents(10) + componentVersions := seeder.SeedComponentVersions(10, components) + componentInstances := seeder.SeedComponentInstances(3, componentVersions, services) + issues := seeder.SeedIssues(3) + issueMatches := seeder.SeedIssueMatches(100, issues, componentInstances, users) + seedCollection = &test.SeedCollection{ + IssueRows: issues, + IssueMatchRows: issueMatches, + ComponentInstanceRows: componentInstances, + } + }) + + It("can order by asc issue primary name and asc targetRemediationDate", func() { + order := []entity.Order{ + {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionAsc}, + {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prevTrd time.Time = time.Time{} + var prevPn = "" + for _, r := range res { + issue := seedCollection.GetIssueById(r.IssueId) + if issue.PrimaryName.String == prevPn { + Expect(r.TargetRemediationDate.After(prevTrd)).Should(BeTrue()) + prevTrd = r.TargetRemediationDate + } else { + Expect(issue.PrimaryName.String > prevPn).To(BeTrue()) + prevTrd = time.Time{} + } + prevPn = issue.PrimaryName.String + } + }) + }) + + It("can order by asc issue primary name and desc targetRemediationDate", func() { + order := []entity.Order{ + {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionAsc}, + {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionDesc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prevTrd time.Time = time.Now() + var prevPn = "" + for _, r := range res { + issue := seedCollection.GetIssueById(r.IssueId) + if issue.PrimaryName.String == prevPn { + Expect(r.TargetRemediationDate.Before(prevTrd)).Should(BeTrue()) + prevTrd = r.TargetRemediationDate + } else { + Expect(issue.PrimaryName.String > prevPn).To(BeTrue()) + prevTrd = time.Now() + } + prevPn = issue.PrimaryName.String + } + }) + }) + + It("can order by asc rating and asc component instance ccrn and asc targetRemediationDate", func() { + order := []entity.Order{ + {By: entity.IssueMatchRating, Direction: entity.OrderDirectionAsc}, + {By: entity.ComponentInstanceCcrn, Direction: entity.OrderDirectionAsc}, + {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, + } + + testOrder(order, func(res []entity.IssueMatch) { + var prevSeverity = 0 + var prevCiCcrn = "" + var prevTrd time.Time = time.Time{} + for _, r := range res { + ci := seedCollection.GetComponentInstanceById(r.ComponentInstanceId) + if test.SeverityToNumerical(r.Severity.Value) == prevSeverity { + if ci.CCRN.String == prevCiCcrn { + Expect(r.TargetRemediationDate.After(prevTrd)).To(BeTrue()) + prevTrd = r.TargetRemediationDate + } else { + Expect(ci.CCRN.String > prevCiCcrn).To(BeTrue()) + prevCiCcrn = ci.CCRN.String + prevTrd = time.Time{} + } + } else { + Expect(test.SeverityToNumerical(r.Severity.Value) > prevSeverity).To(BeTrue()) + prevSeverity = test.SeverityToNumerical(r.Severity.Value) + prevCiCcrn = "" + prevTrd = time.Time{} + } + } + }) + }) + + }) +}) diff --git a/internal/database/mariadb/test/common.go b/internal/database/mariadb/test/common.go index c050e0e9..b7eaf176 100644 --- a/internal/database/mariadb/test/common.go +++ b/internal/database/mariadb/test/common.go @@ -8,6 +8,42 @@ import ( . "github.com/onsi/gomega" ) +// Temporary used until order is used in all entities +func TestPaginationOfListWithOrder[F entity.HeurekaFilter, E entity.HeurekaEntity]( + listFunction func(*F, []entity.Order) ([]E, error), + filterFunction func(*int, *int64) *F, + order []entity.Order, + getAfterFunction func([]E) *int64, + elementCount int, + pageSize int, +) { + quotient, remainder := elementCount/pageSize, elementCount%pageSize + expectedPages := quotient + if remainder > 0 { + expectedPages = expectedPages + 1 + } + + var after *int64 + for i := expectedPages; i > 0; i-- { + entries, err := listFunction(filterFunction(&pageSize, after), order) + + Expect(err).To(BeNil()) + + if i == 1 && remainder > 0 { + Expect(len(entries)).To(BeEquivalentTo(remainder), "on the last page we expect") + } else { + if pageSize > elementCount { + Expect(len(entries)).To(BeEquivalentTo(elementCount), "on a page with a higher pageSize then element count we expect") + } else { + Expect(len(entries)).To(BeEquivalentTo(pageSize), "on a normal page we expect the element count to be equal to the page size") + + } + } + after = getAfterFunction(entries) + + } +} + func TestPaginationOfList[F entity.HeurekaFilter, E entity.HeurekaEntity]( listFunction func(*F) ([]E, error), filterFunction func(*int, *int64) *F, @@ -41,3 +77,23 @@ func TestPaginationOfList[F entity.HeurekaFilter, E entity.HeurekaEntity]( } } + +// DB stores rating as enum +// entity.Severity.Score is based on CVSS vector and has a range between x and y +// This means a rating "Low" can have a Score 3.1, 3.3, ... +// Ordering is done based on enum on DB layer, so Score can't be used for checking order +// and needs a numerical translation +func SeverityToNumerical(s string) int { + rating := map[string]int{ + "None": 0, + "Low": 1, + "Medium": 2, + "High": 3, + "Critical": 4, + } + if val, ok := rating[s]; ok { + return val + } else { + return -1 + } +} diff --git a/internal/database/mariadb/test/fixture.go b/internal/database/mariadb/test/fixture.go index a62921fe..7c4b6574 100644 --- a/internal/database/mariadb/test/fixture.go +++ b/internal/database/mariadb/test/fixture.go @@ -9,7 +9,6 @@ import ( "math/rand" "strings" - "github.com/cloudoperators/heureka/internal/e2e/common" "github.com/cloudoperators/heureka/internal/entity" "github.com/goark/go-cvss/v3/metric" "github.com/onsi/ginkgo/v2/dsl/core" @@ -46,6 +45,24 @@ type SeedCollection struct { IssueRepositoryServiceRows []mariadb.IssueRepositoryServiceRow } +func (s *SeedCollection) GetComponentInstanceById(id int64) *mariadb.ComponentInstanceRow { + for _, ci := range s.ComponentInstanceRows { + if ci.Id.Int64 == id { + return &ci + } + } + return nil +} + +func (s *SeedCollection) GetIssueById(id int64) *mariadb.IssueRow { + for _, issue := range s.IssueRows { + if issue.Id.Int64 == id { + return &issue + } + } + return nil +} + func (s *SeedCollection) GetIssueVariantsByIssueId(id int64) []mariadb.IssueVariantRow { var r []mariadb.IssueVariantRow for _, iv := range s.IssueVariantRows { diff --git a/internal/e2e/issue_match_query_test.go b/internal/e2e/issue_match_query_test.go index a83c8e86..4cfba555 100644 --- a/internal/e2e/issue_match_query_test.go +++ b/internal/e2e/issue_match_query_test.go @@ -201,6 +201,102 @@ var _ = Describe("Getting IssueMatches via API", Label("e2e", "IssueMatches"), f Expect(*respData.IssueMatches.PageInfo.PageNumber).To(Equal(1), "Correct page number") }) }) + Context("we use ordering", Label("withOrder.graphql"), func() { + var respData struct { + IssueMatches model.IssueMatchConnection `json:"IssueMatches"` + } + + It("can order by primaryName", Label("withOrder.graphql"), func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port)) + + //@todo may need to make this more fault proof?! What if the test is executed from the root dir? does it still work? + b, err := os.ReadFile("../api/graphql/graph/queryCollection/issueMatch/withOrder.graphql") + + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + req.Var("filter", map[string]string{}) + req.Var("first", 10) + req.Var("after", "0") + req.Var("orderBy", []map[string]string{ + {"by": "primaryName", "direction": "asc"}, + }) + + req.Header.Set("Cache-Control", "no-cache") + + ctx := context.Background() + + err = client.Run(ctx, req, &respData) + + Expect(err).To(BeNil(), "Error while unmarshaling") + + By("- returns the correct result count", func() { + Expect(respData.IssueMatches.TotalCount).To(Equal(len(seedCollection.IssueMatchRows))) + Expect(len(respData.IssueMatches.Edges)).To(Equal(10)) + }) + + By("- returns the expected content in order", func() { + var prev string = "" + for _, im := range respData.IssueMatches.Edges { + Expect(*im.Node.Issue.PrimaryName >= prev).Should(BeTrue()) + prev = *im.Node.Issue.PrimaryName + } + }) + }) + + It("can order by primaryName and targetRemediationDate", Label("withOrder.graphql"), func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.Port)) + + //@todo may need to make this more fault proof?! What if the test is executed from the root dir? does it still work? + b, err := os.ReadFile("../api/graphql/graph/queryCollection/issueMatch/withOrder.graphql") + + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + req.Var("filter", map[string]string{}) + req.Var("first", 10) + req.Var("after", "0") + req.Var("orderBy", []map[string]string{ + {"by": "primaryName", "direction": "asc"}, + {"by": "targetRemediationDate", "direction": "desc"}, + }) + + req.Header.Set("Cache-Control", "no-cache") + + ctx := context.Background() + + err = client.Run(ctx, req, &respData) + + Expect(err).To(BeNil(), "Error while unmarshaling") + + By("- returns the correct result count", func() { + Expect(respData.IssueMatches.TotalCount).To(Equal(len(seedCollection.IssueMatchRows))) + Expect(len(respData.IssueMatches.Edges)).To(Equal(10)) + }) + + By("- returns the expected content in order", func() { + var prevPn string = "" + var prevTrd time.Time = time.Now() + for _, im := range respData.IssueMatches.Edges { + if *im.Node.Issue.PrimaryName == prevPn { + trd, err := time.Parse("2006-01-02T15:04:05Z", *im.Node.TargetRemediationDate) + Expect(err).To(BeNil()) + Expect(trd.Before(prevTrd)).Should(BeTrue()) + prevTrd = trd + } else { + Expect(*im.Node.Issue.PrimaryName > prevPn).To(BeTrue()) + prevTrd = time.Now() + } + prevPn = *im.Node.Issue.PrimaryName + } + }) + }) + + }) }) }) }) diff --git a/internal/entity/common.go b/internal/entity/common.go index 491fce01..92cd4b85 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -4,6 +4,7 @@ package entity import ( + "fmt" "math" "time" @@ -106,6 +107,7 @@ func NewListOptions() *ListOptions { ShowTotalCount: false, ShowPageInfo: false, IncludeAggregations: false, + Order: []Order{}, } } @@ -229,21 +231,47 @@ type Metadata struct { DeletedAt time.Time `json:"deleted_at,omitempty"` } -type DbColumnName string +type DbColumnName int const ( - IssueMatchId DbColumnName = "issuematch_id" - IssueMatchRating DbColumnName = "issuematch_rating" - SupportGroupName DbColumnName = "supportgroup_name" + ComponentInstanceCcrn DbColumnName = iota + + IssuePrimaryName + + IssueMatchId + IssueMatchRating + IssueMatchTargetRemediationDate + + SupportGroupName ) -type OrderDirection string +func (d DbColumnName) String() string { + // order of string needs to match iota order + return [...]string{ + "componentinstance_ccrn", + "issue_primary_name", + "issuematch_id", + "issuematch_rating", + "issuematch_target_remediation_date", + "supportgroup_name", + }[d] +} + +type OrderDirection int const ( - OrderDirectionAsc OrderDirection = "asc" - OrderDirectionDesc OrderDirection = "desc" + OrderDirectionAsc OrderDirection = iota + OrderDirectionDesc ) +func (o OrderDirection) String() string { + // order of string needs to match iota order + return [...]string{ + "ASC", + "DESC", + }[o] +} + type Order struct { By DbColumnName Direction OrderDirection @@ -256,3 +284,15 @@ func CreateOrderMap(order []Order) map[DbColumnName]OrderDirection { } return m } + +func CreateOrderString(order []Order) string { + orderStr := "" + for i, o := range order { + if i > 0 { + orderStr = fmt.Sprintf("%s, %s %s", orderStr, o.By, o.Direction) + } else { + orderStr = fmt.Sprintf("%s %s %s", orderStr, o.By, o.Direction) + } + } + return orderStr +} From 3464a3da5b83e1ea18b3943d8ca50d61c9467717 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Thu, 9 Jan 2025 16:21:54 +0100 Subject: [PATCH 04/11] wip --- internal/database/mariadb/test/fixture.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/database/mariadb/test/fixture.go b/internal/database/mariadb/test/fixture.go index 7c4b6574..e3e71056 100644 --- a/internal/database/mariadb/test/fixture.go +++ b/internal/database/mariadb/test/fixture.go @@ -9,6 +9,8 @@ import ( "math/rand" "strings" + e2e_common "github.com/cloudoperators/heureka/internal/e2e/common" + "github.com/cloudoperators/heureka/internal/entity" "github.com/goark/go-cvss/v3/metric" "github.com/onsi/ginkgo/v2/dsl/core" From a03011b2837655fd397c2d15d36748c56f89b3d6 Mon Sep 17 00:00:00 2001 From: License Bot Date: Thu, 9 Jan 2025 15:51:02 +0000 Subject: [PATCH 05/11] Automatic application of license header --- internal/entity/cursor.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/entity/cursor.go b/internal/entity/cursor.go index 7c10a08d..d1ed52f5 100644 --- a/internal/entity/cursor.go +++ b/internal/entity/cursor.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + package entity import ( From 7241918d3b69c12dc37c24ebf8e36cca4045e0b6 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Mon, 27 Jan 2025 09:28:04 +0100 Subject: [PATCH 06/11] wip --- .../graphql/graph/baseResolver/issue_match.go | 8 +- internal/app/common/pagination_helpers.go | 75 +++++++++++++++ .../app/issue/issue_handler_events_test.go | 6 +- .../app/issue_match/issue_match_handler.go | 21 ++-- .../issue_match/issue_match_handler_test.go | 76 ++++++++------- internal/database/interface.go | 3 +- internal/database/mariadb/database.go | 2 +- internal/database/mariadb/entity.go | 35 ++++++- internal/database/mariadb/issue_match.go | 95 +++++++++++++++++-- internal/database/mariadb/issue_match_test.go | 70 +++++++------- internal/database/mariadb/test/common.go | 10 +- internal/e2e/issue_match_query_test.go | 10 +- internal/entity/common.go | 73 ++------------ internal/entity/cursor.go | 80 +++++++++++++--- internal/entity/issue_match.go | 2 +- internal/entity/order.go | 74 +++++++++++++++ internal/entity/test/issue_match.go | 7 ++ 17 files changed, 451 insertions(+), 196 deletions(-) create mode 100644 internal/entity/order.go diff --git a/internal/api/graphql/graph/baseResolver/issue_match.go b/internal/api/graphql/graph/baseResolver/issue_match.go index 808ddba6..a16c841a 100644 --- a/internal/api/graphql/graph/baseResolver/issue_match.go +++ b/internal/api/graphql/graph/baseResolver/issue_match.go @@ -61,12 +61,6 @@ func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model. "parent": parent, }).Debug("Called IssueMatchBaseResolver") - afterId, err := ParseCursor(after) - if err != nil { - logrus.WithField("after", after).Error("IssueMatchBaseResolver: Error while parsing parameter 'after'") - return nil, NewResolverError("IssueMatchBaseResolver", "Bad Request - unable to parse cursor 'after'") - } - var eId []*int64 var ciId []*int64 var issueId []*int64 @@ -104,7 +98,7 @@ func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model. f := &entity.IssueMatchFilter{ Id: issue_match_ids, - Paginated: entity.Paginated{First: first, After: afterId}, + PaginatedX: entity.PaginatedX{First: first, After: after}, AffectedServiceCCRN: filter.AffectedService, Status: lo.Map(filter.Status, func(item *model.IssueMatchStatusValues, _ int) *string { return pointer.String(item.String()) }), SeverityValue: lo.Map(filter.Severity, func(item *model.SeverityValues, _ int) *string { return pointer.String(item.String()) }), diff --git a/internal/app/common/pagination_helpers.go b/internal/app/common/pagination_helpers.go index 8e277c7f..debcd026 100644 --- a/internal/app/common/pagination_helpers.go +++ b/internal/app/common/pagination_helpers.go @@ -33,6 +33,17 @@ func EnsurePaginated(filter *entity.Paginated) { } } +func EnsurePaginatedX(filter *entity.PaginatedX) { + if filter.First == nil { + first := 10 + filter.First = &first + } + if filter.After == nil { + var after string = "" + filter.After = &after + } +} + func GetPages(firstCursor *string, ids []int64, pageSize int) ([]entity.Page, entity.Page) { var currentCursor = util.Ptr("0") var pages []entity.Page @@ -73,6 +84,46 @@ func GetPages(firstCursor *string, ids []int64, pageSize int) ([]entity.Page, en return pages, currentPage } +func GetCursorPages(firstCursor *string, cursors []string, pageSize int) ([]entity.Page, entity.Page) { + var currentCursor = "" + var pages []entity.Page + var currentPage entity.Page + var i = 0 + var pN = 0 + var page entity.Page + for _, c := range cursors { + i++ + if i == 1 { + pN++ + page = entity.Page{ + After: ¤tCursor, + IsCurrent: false, + } + } + if c == *firstCursor { + page.IsCurrent = true + } + page.PageCount = util.Ptr(i) + if i >= pageSize { + currentCursor = c + page.PageNumber = util.Ptr(pN) + pages = append(pages, page) + i = 0 + if page.IsCurrent { + currentPage = page + } + } + } + if len(cursors)%pageSize != 0 { + page.PageNumber = util.Ptr(pN) + pages = append(pages, page) + if page.IsCurrent { + currentPage = page + } + } + return pages, currentPage +} + func GetPageInfo[T entity.HasCursor](res []T, ids []int64, pageSize int, currentCursor int64) *entity.PageInfo { var nextPageAfter *string currentAfter := util.Ptr(fmt.Sprintf("%d", currentCursor)) @@ -95,6 +146,30 @@ func GetPageInfo[T entity.HasCursor](res []T, ids []int64, pageSize int, current } } +func GetPageInfoX[T entity.HasCursor](res []T, cursors []string, pageSize int, currentCursor *string) *entity.PageInfo { + + var nextPageAfter *string + currentAfter := currentCursor + firstCursor := res[0].Cursor() + + if len(res) > 1 { + nextPageAfter = res[len(res)-1].Cursor() + } else { + nextPageAfter = firstCursor + } + + pages, currentPage := GetCursorPages(firstCursor, cursors, pageSize) + + return &entity.PageInfo{ + HasNextPage: util.Ptr(currentPage.PageNumber != nil && *currentPage.PageNumber < len(pages)), + HasPreviousPage: util.Ptr(currentPage.PageNumber != nil && *currentPage.PageNumber > 1), + IsValidPage: util.Ptr(currentPage.After != nil && currentAfter != nil && *currentPage.After == *currentAfter), + PageNumber: currentPage.PageNumber, + NextPageAfter: nextPageAfter, + Pages: pages, + } +} + func FinalizePagination[T entity.HasCursor](results []T, filter *entity.Paginated, options *entity.ListOptions) (*entity.PageInfo, []T) { var pageInfo entity.PageInfo count := len(results) diff --git a/internal/app/issue/issue_handler_events_test.go b/internal/app/issue/issue_handler_events_test.go index 06856757..4e7d6ad2 100644 --- a/internal/app/issue/issue_handler_events_test.go +++ b/internal/app/issue/issue_handler_events_test.go @@ -88,7 +88,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV db.On("GetIssueMatches", &entity.IssueMatchFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, IssueId: []*int64{&issueEntity.Id}, - }, []entity.Order{}).Return([]entity.IssueMatch{}, nil) + }, []entity.Order{}).Return([]entity.IssueMatchResult{}, nil) db.On("GetServiceIssueVariants", &entity.ServiceIssueVariantFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, @@ -113,7 +113,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV }) It("skips creation if match already exists", func() { - existingMatch := test.NewFakeIssueMatch() + existingMatch := test.NewFakeIssueMatchResult() db.On("GetServiceIssueVariants", &entity.ServiceIssueVariantFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, IssueId: []*int64{&issueEntity.Id}, @@ -123,7 +123,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV db.On("GetIssueMatches", &entity.IssueMatchFilter{ ComponentInstanceId: []*int64{&componentInstance.Id}, IssueId: []*int64{&issueEntity.Id}, - }, []entity.Order{}).Return([]entity.IssueMatch{existingMatch}, nil) + }, []entity.Order{}).Return([]entity.IssueMatchResult{existingMatch}, nil) issue.OnComponentVersionAttachmentToIssue(db, event) db.AssertNotCalled(GinkgoT(), "CreateIssueMatch", mock.Anything) diff --git a/internal/app/issue_match/issue_match_handler.go b/internal/app/issue_match/issue_match_handler.go index 5ed01aa9..d9e3732d 100644 --- a/internal/app/issue_match/issue_match_handler.go +++ b/internal/app/issue_match/issue_match_handler.go @@ -12,7 +12,6 @@ import ( "github.com/cloudoperators/heureka/internal/database" "github.com/cloudoperators/heureka/internal/entity" - "github.com/cloudoperators/heureka/pkg/util" "github.com/sirupsen/logrus" ) @@ -43,20 +42,12 @@ func (e *IssueMatchHandlerError) Error() string { } func (h *issueMatchHandler) getIssueMatchResults(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatchResult, error) { - var results []entity.IssueMatchResult ims, err := h.database.GetIssueMatches(filter, order) if err != nil { return nil, err } - for _, im := range ims { - cursor := fmt.Sprintf("%d", im.Id) - results = append(results, entity.IssueMatchResult{ - WithCursor: entity.WithCursor{Value: cursor}, - IssueMatch: util.Ptr(im), - }) - } - return results, nil + return ims, nil } func (im *issueMatchHandler) GetIssueMatch(issueMatchId int64) (*entity.IssueMatch, error) { @@ -89,14 +80,14 @@ func (im *issueMatchHandler) ListIssueMatches(filter *entity.IssueMatchFilter, o var count int64 var pageInfo *entity.PageInfo - common.EnsurePaginated(&filter.Paginated) + common.EnsurePaginatedX(&filter.PaginatedX) l := logrus.WithFields(logrus.Fields{ "event": ListIssueMatchesEventName, "filter": filter, }) - res, err := im.getIssueMatchResults(filter, options.Order) + res, err := im.database.GetIssueMatches(filter, options.Order) if err != nil { l.Error(err) @@ -105,13 +96,13 @@ func (im *issueMatchHandler) ListIssueMatches(filter *entity.IssueMatchFilter, o if options.ShowPageInfo { if len(res) > 0 { - ids, err := im.database.GetAllIssueMatchIds(filter) + cursors, err := im.database.GetAllIssueMatchCursors(filter, options.Order) if err != nil { l.Error(err) return nil, NewIssueMatchHandlerError("Error while getting all Ids") } - pageInfo = common.GetPageInfo(res, ids, *filter.First, *filter.After) - count = int64(len(ids)) + pageInfo = common.GetPageInfoX(res, cursors, *filter.First, filter.After) + count = int64(len(cursors)) } } else if options.ShowTotalCount { count, err = im.database.CountIssueMatches(filter) diff --git a/internal/app/issue_match/issue_match_handler_test.go b/internal/app/issue_match/issue_match_handler_test.go index 1eeb6411..8f094cfd 100644 --- a/internal/app/issue_match/issue_match_handler_test.go +++ b/internal/app/issue_match/issue_match_handler_test.go @@ -39,7 +39,7 @@ var _ = BeforeSuite(func() { func getIssueMatchFilter() *entity.IssueMatchFilter { return &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: nil, After: nil, }, @@ -71,7 +71,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), BeforeEach(func() { options.ShowTotalCount = true - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{}, nil) db.On("CountIssueMatches", filter).Return(int64(1337), nil) }) @@ -89,16 +89,27 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), }) DescribeTable("pagination information is correct", func(pageSize int, dbElements int, resElements int, hasNextPage bool) { filter.First = &pageSize - matches := test.NNewFakeIssueMatches(resElements) + matches := []entity.IssueMatchResult{} + for _, im := range test.NNewFakeIssueMatches(resElements) { + cursor, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, im)) + matches = append(matches, entity.IssueMatchResult{WithCursor: entity.WithCursor{Value: cursor}, IssueMatch: lo.ToPtr(im)}) + } + + // cursors := []string{} + var cursors = lo.Map(matches, func(m entity.IssueMatchResult, _ int) string { + cursor, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, *m.IssueMatch)) + return cursor + }) - var ids = lo.Map(matches, func(m entity.IssueMatch, _ int) int64 { return m.Id }) var i int64 = 0 - for len(ids) < dbElements { + for len(cursors) < dbElements { i++ - ids = append(ids, i) + im := test.NewFakeIssueMatch() + c, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, im)) + cursors = append(cursors, c) } db.On("GetIssueMatches", filter, []entity.Order{}).Return(matches, nil) - db.On("GetAllIssueMatchIds", filter).Return(ids, nil) + db.On("GetAllIssueMatchCursors", filter, []entity.Order{}).Return(cursors, nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) res, err := issueMatchHandler.ListIssueMatches(filter, options) Expect(err).To(BeNil(), "no error should be thrown") @@ -121,7 +132,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), Context("and the given filter does not have any matches in the database", func() { BeforeEach(func() { - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{}, nil) }) It("should return an empty result", func() { @@ -134,7 +145,11 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), }) Context("and the filter does have results in the database", func() { BeforeEach(func() { - db.On("GetIssueMatches", filter, []entity.Order{}).Return(test.NNewFakeIssueMatches(15), nil) + issueMatches := []entity.IssueMatchResult{} + for _, im := range test.NNewFakeIssueMatches(15) { + issueMatches = append(issueMatches, entity.IssueMatchResult{IssueMatch: lo.ToPtr(im)}) + } + db.On("GetIssueMatches", filter, []entity.Order{}).Return(issueMatches, nil) }) It("should return the expected matches in the result", func() { issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) @@ -146,7 +161,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), Context("and the database operations throw an error", func() { BeforeEach(func() { - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, errors.New("some error")) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{}, errors.New("some error")) }) It("should return the expected matches in the result", func() { @@ -222,18 +237,17 @@ var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), f var ( db *mocks.MockDatabase issueMatchHandler im.IssueMatchHandler - issueMatch entity.IssueMatch + issueMatch entity.IssueMatchResult filter *entity.IssueMatchFilter ) BeforeEach(func() { db = mocks.NewMockDatabase(GinkgoT()) - issueMatch = test.NewFakeIssueMatch() + issueMatch = test.NewFakeIssueMatchResult() first := 10 - var after int64 - after = 0 + after := "" filter = &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: &first, After: &after, }, @@ -242,7 +256,7 @@ var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), f It("updates issueMatch", func() { db.On("GetAllUserIds", mock.Anything).Return([]int64{}, nil) - db.On("UpdateIssueMatch", &issueMatch).Return(nil) + db.On("UpdateIssueMatch", issueMatch.IssueMatch).Return(nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) if issueMatch.Status == entity.NewIssueMatchStatusValue("new") { issueMatch.Status = entity.NewIssueMatchStatusValue("risk_accepted") @@ -250,8 +264,8 @@ var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), f issueMatch.Status = entity.NewIssueMatchStatusValue("new") } filter.Id = []*int64{&issueMatch.Id} - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{issueMatch}, nil) - updatedIssueMatch, err := issueMatchHandler.UpdateIssueMatch(&issueMatch) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{issueMatch}, nil) + updatedIssueMatch, err := issueMatchHandler.UpdateIssueMatch(issueMatch.IssueMatch) Expect(err).To(BeNil(), "no error should be thrown") By("setting fields", func() { Expect(updatedIssueMatch.TargetRemediationDate).To(BeEquivalentTo(issueMatch.TargetRemediationDate)) @@ -280,10 +294,9 @@ var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), f db = mocks.NewMockDatabase(GinkgoT()) id = 1 first := 10 - var after int64 - after = 0 + after := "" filter = &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: &first, After: &after, }, @@ -294,7 +307,7 @@ var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), f It("deletes issueMatch", func() { db.On("DeleteIssueMatch", id).Return(nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{}, nil) err := issueMatchHandler.DeleteIssueMatch(id) Expect(err).To(BeNil(), "no error should be thrown") @@ -310,19 +323,18 @@ var _ = Describe("When modifying relationship of evidence and issueMatch", Label db *mocks.MockDatabase issueMatchHandler im.IssueMatchHandler evidence entity.Evidence - issueMatch entity.IssueMatch + issueMatch entity.IssueMatchResult filter *entity.IssueMatchFilter ) BeforeEach(func() { db = mocks.NewMockDatabase(GinkgoT()) - issueMatch = test.NewFakeIssueMatch() + issueMatch = test.NewFakeIssueMatchResult() evidence = test.NewFakeEvidenceEntity() first := 10 - var after int64 - after = 0 + after := "" filter = &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: &first, After: &after, }, @@ -332,7 +344,7 @@ var _ = Describe("When modifying relationship of evidence and issueMatch", Label It("adds evidence to issueMatch", func() { db.On("AddEvidenceToIssueMatch", issueMatch.Id, evidence.Id).Return(nil) - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{issueMatch}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{issueMatch}, nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) issueMatch, err := issueMatchHandler.AddEvidenceToIssueMatch(issueMatch.Id, evidence.Id) Expect(err).To(BeNil(), "no error should be thrown") @@ -341,7 +353,7 @@ var _ = Describe("When modifying relationship of evidence and issueMatch", Label It("removes evidence from issueMatch", func() { db.On("RemoveEvidenceFromIssueMatch", issueMatch.Id, evidence.Id).Return(nil) - db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatch{issueMatch}, nil) + db.On("GetIssueMatches", filter, []entity.Order{}).Return([]entity.IssueMatchResult{issueMatch}, nil) issueMatchHandler = im.NewIssueMatchHandler(db, er, nil) issueMatch, err := issueMatchHandler.RemoveEvidenceFromIssueMatch(issueMatch.Id, evidence.Id) Expect(err).To(BeNil(), "no error should be thrown") @@ -469,7 +481,7 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC }) It("should create issue matches for each issue", func() { - db.On("GetIssueMatches", mock.Anything, mock.Anything).Return([]entity.IssueMatch{}, nil) + db.On("GetIssueMatches", mock.Anything, mock.Anything).Return([]entity.IssueMatchResult{}, nil) // Mock CreateIssueMatch db.On("CreateIssueMatch", mock.AnythingOfType("*entity.IssueMatch")).Return(&entity.IssueMatch{}, nil).Twice() im.OnComponentVersionAssignmentToComponentInstance(db, componentInstanceID, componentVersionID) @@ -481,10 +493,10 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC Context("when issue matches already exist", func() { BeforeEach(func() { // Fake issues - issueMatch := test.NewFakeIssueMatch() + issueMatch := test.NewFakeIssueMatchResult() issueMatch.IssueId = 2 // issue2.Id //when issueid is 2 return a fake issue match - db.On("GetIssueMatches", mock.Anything, mock.Anything).Return([]entity.IssueMatch{issueMatch}, nil).Once() + db.On("GetIssueMatches", mock.Anything, mock.Anything).Return([]entity.IssueMatchResult{issueMatch}, nil).Once() }) It("should should not create new issues", func() { diff --git a/internal/database/interface.go b/internal/database/interface.go index c78ca1d1..3bf4f517 100644 --- a/internal/database/interface.go +++ b/internal/database/interface.go @@ -35,8 +35,9 @@ type Database interface { GetDefaultIssuePriority() int64 GetDefaultRepositoryName() string - GetIssueMatches(*entity.IssueMatchFilter, []entity.Order) ([]entity.IssueMatch, error) + GetIssueMatches(*entity.IssueMatchFilter, []entity.Order) ([]entity.IssueMatchResult, error) GetAllIssueMatchIds(*entity.IssueMatchFilter) ([]int64, error) + GetAllIssueMatchCursors(*entity.IssueMatchFilter, []entity.Order) ([]string, error) CountIssueMatches(filter *entity.IssueMatchFilter) (int64, error) CreateIssueMatch(*entity.IssueMatch) (*entity.IssueMatch, error) UpdateIssueMatch(*entity.IssueMatch) error diff --git a/internal/database/mariadb/database.go b/internal/database/mariadb/database.go index c5592fbc..198bdc76 100644 --- a/internal/database/mariadb/database.go +++ b/internal/database/mariadb/database.go @@ -246,7 +246,7 @@ func performExec[T any](s *SqlDatabase, query string, item T, l *logrus.Entry) ( return res, nil } -func performListScan[T DatabaseRow, E entity.HeurekaEntity](stmt *sqlx.Stmt, filterParameters []interface{}, l *logrus.Entry, listBuilder func([]E, T) []E) ([]E, error) { +func performListScan[T DatabaseRow, E entity.HeurekaEntity | DatabaseRow](stmt *sqlx.Stmt, filterParameters []interface{}, l *logrus.Entry, listBuilder func([]E, T) []E) ([]E, error) { rows, err := stmt.Queryx(filterParameters...) if err != nil { msg := "Error while performing Query from prepared Statement" diff --git a/internal/database/mariadb/entity.go b/internal/database/mariadb/entity.go index d0111730..63c51765 100644 --- a/internal/database/mariadb/entity.go +++ b/internal/database/mariadb/entity.go @@ -59,6 +59,38 @@ func GetUserTypeValue(v sql.NullInt64) entity.UserType { } } +// RowComposite is a composite type that contains all the row types for the database +// This is used to unmarshal the database rows into the corresponding entity types in a dynamical manner +type RowComposite struct { + *IssueRow + *IssueCountRow + *GetIssuesByRow + *IssueMatchRow + *IssueAggregationsRow + *IssueVariantRow + *BaseIssueRepositoryRow + *IssueRepositoryRow + *IssueVariantWithRepository + *ComponentRow + *ComponentInstanceRow + *ComponentVersionRow + *BaseServiceRow + *ServiceRow + *GetServicesByRow + *ServiceAggregationsRow + *ActivityRow + *UserRow + *EvidenceRow + *OwnerRow + *SupportGroupRow + *SupportGroupServiceRow + *ActivityHasIssueRow + *ActivityHasServiceRow + *IssueRepositoryServiceRow + *IssueMatchChangeRow + *ServiceIssueVariantRow +} + type DatabaseRow interface { IssueRow | IssueCountRow | @@ -86,7 +118,8 @@ type DatabaseRow interface { ActivityHasServiceRow | IssueRepositoryServiceRow | IssueMatchChangeRow | - ServiceIssueVariantRow + ServiceIssueVariantRow | + RowComposite } type IssueRow struct { diff --git a/internal/database/mariadb/issue_match.go b/internal/database/mariadb/issue_match.go index 1f613b7f..15ce719b 100644 --- a/internal/database/mariadb/issue_match.go +++ b/internal/database/mariadb/issue_match.go @@ -19,9 +19,9 @@ func (s *SqlDatabase) ensureIssueMatchFilter(f *entity.IssueMatchFilter) *entity } var first = 1000 - var after int64 = 0 + var after string = "" return &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: &first, After: &after, }, @@ -149,7 +149,12 @@ func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity. filterStr := s.getIssueMatchFilterString(filter) joins := s.getIssueMatchJoins(filter, order) - cursor := getCursor(filter.Paginated, filterStr, "IM.issuematch_id > ?") + cursorFields, err := entity.DecodeCursor(filter.PaginatedX.After) + if err != nil { + return nil, nil, err + } + cursorQuery := entity.CreateCursorQuery("", cursorFields) + order = GetDefaultOrder(order, entity.IssueMatchId, entity.OrderDirectionAsc) orderStr := entity.CreateOrderString(order) @@ -158,16 +163,19 @@ func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity. whereClause = fmt.Sprintf("WHERE %s", filterStr) } + if filterStr != "" && withCursor && cursorQuery != "" { + cursorQuery = fmt.Sprintf(" AND (%s)", cursorQuery) + } + // construct final query if withCursor { - query = fmt.Sprintf(baseQuery, joins, whereClause, cursor.Statement, orderStr) + query = fmt.Sprintf(baseQuery, joins, whereClause, cursorQuery, orderStr) } else { query = fmt.Sprintf(baseQuery, joins, whereClause, orderStr) } //construct prepared statement and if where clause does exist add parameters var stmt *sqlx.Stmt - var err error stmt, err = s.db.Preparex(query) if err != nil { @@ -197,8 +205,13 @@ func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity. filterParameters = buildQueryParametersCount(filterParameters, filter.Search, wildCardFilterParamCount) if withCursor { - filterParameters = append(filterParameters, cursor.Value) - filterParameters = append(filterParameters, cursor.Limit) + p := entity.CreateCursorParameters([]any{}, cursorFields) + filterParameters = append(filterParameters, p...) + if filter.PaginatedX.First == nil { + filterParameters = append(filterParameters, 1000) + } else { + filterParameters = append(filterParameters, filter.PaginatedX.First) + } } return stmt, filterParameters, nil @@ -225,7 +238,53 @@ func (s *SqlDatabase) GetAllIssueMatchIds(filter *entity.IssueMatchFilter) ([]in return performIdScan(stmt, filterParameters, l) } -func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatch, error) { +func (s *SqlDatabase) GetAllIssueMatchCursors(filter *entity.IssueMatchFilter, order []entity.Order) ([]string, error) { + l := logrus.WithFields(logrus.Fields{ + "filter": filter, + "event": "database.GetIssueAllIssueMatchCursors", + }) + + baseQuery := ` + SELECT IM.* FROM IssueMatch IM + %s + %s GROUP BY IM.issuematch_id ORDER BY %s + ` + + stmt, filterParameters, err := s.buildIssueMatchStatement(baseQuery, filter, false, order, l) + + if err != nil { + return nil, err + } + + rows, err := performListScan( + stmt, + filterParameters, + l, + func(l []RowComposite, e RowComposite) []RowComposite { + return append(l, e) + }, + ) + + if err != nil { + return nil, err + } + + return lo.Map(rows, func(row RowComposite, _ int) string { + im := row.AsIssueMatch() + if row.IssueRow != nil { + im.Issue = lo.ToPtr(row.IssueRow.AsIssue()) + } + if row.ComponentInstanceRow != nil { + im.ComponentInstance = lo.ToPtr(row.ComponentInstanceRow.AsComponentInstance()) + } + + cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im)) + + return cursor + }), nil +} + +func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatchResult, error) { l := logrus.WithFields(logrus.Fields{ "filter": filter, "event": "database.GetIssueMatches", @@ -247,8 +306,24 @@ func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []e stmt, filterParameters, l, - func(l []entity.IssueMatch, e IssueMatchRow) []entity.IssueMatch { - return append(l, e.AsIssueMatch()) + func(l []entity.IssueMatchResult, e RowComposite) []entity.IssueMatchResult { + im := e.AsIssueMatch() + if e.IssueRow != nil { + im.Issue = lo.ToPtr(e.IssueRow.AsIssue()) + } + if e.ComponentInstanceRow != nil { + im.ComponentInstance = lo.ToPtr(e.ComponentInstanceRow.AsComponentInstance()) + } + + cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im)) + + imr := entity.IssueMatchResult{ + WithCursor: entity.WithCursor{ + Value: cursor, + }, + IssueMatch: &im, + } + return append(l, imr) }, ) } diff --git a/internal/database/mariadb/issue_match_test.go b/internal/database/mariadb/issue_match_test.go index 9f26f82d..3fabbb75 100644 --- a/internal/database/mariadb/issue_match_test.go +++ b/internal/database/mariadb/issue_match_test.go @@ -103,8 +103,8 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { It("can filter by a single issue id that does exist", func() { issueMatch := seedCollection.IssueMatchRows[rand.Intn(len(seedCollection.IssueMatchRows))] filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{}, - IssueId: []*int64{&issueMatch.IssueId.Int64}, + PaginatedX: entity.PaginatedX{}, + IssueId: []*int64{&issueMatch.IssueId.Int64}, } var imIds []int64 @@ -153,6 +153,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { BeforeEach(func() { seedCollection = seeder.SeedDbWithNFakeData(10) + seedCollection.GetValidIssueMatchRows() issueMatches = seedCollection.GetValidIssueMatchRows() }) Context("and using no filter", func() { @@ -215,8 +216,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { It("can filter by a single issue id that does exist", func() { issueMatch := seedCollection.IssueMatchRows[rand.Intn(len(seedCollection.IssueMatchRows))] filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{}, - IssueId: []*int64{&issueMatch.IssueId.Int64}, + IssueId: []*int64{&issueMatch.IssueId.Int64}, } var imIds []int64 @@ -245,7 +245,6 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { It("can filter by a single component instance id that does exist", func() { issueMatch := seedCollection.IssueMatchRows[rand.Intn(len(seedCollection.IssueMatchRows))] filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{}, ComponentInstanceId: []*int64{&issueMatch.ComponentInstanceId.Int64}, } @@ -275,7 +274,6 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { It("can filter by a single evidence id that does exist", func() { issueMatch := seedCollection.IssueMatchEvidenceRows[rand.Intn(len(seedCollection.IssueMatchEvidenceRows))] filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{}, EvidenceId: []*int64{&issueMatch.EvidenceId.Int64}, } @@ -318,7 +316,6 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{}, SupportGroupCCRN: []*string{&supportGroup.CCRN.String}, } @@ -335,7 +332,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) By("entries contain vm", func() { - _, found := lo.Find(entries, func(e entity.IssueMatch) bool { + _, found := lo.Find(entries, func(e entity.IssueMatchResult) bool { return e.Id == issueMatch.Id.Int64 }) Expect(found).To(BeTrue()) @@ -346,16 +343,19 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { DescribeTable("can correctly paginate ", func(pageSize int) { test.TestPaginationOfListWithOrder( db.GetIssueMatches, - func(first *int, after *int64) *entity.IssueMatchFilter { + func(first *int, after *int64, afterX *string) *entity.IssueMatchFilter { return &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: first, - After: after, + After: afterX, }, } }, []entity.Order{}, - func(entries []entity.IssueMatch) *int64 { return &entries[len(entries)-1].Id }, + func(entries []entity.IssueMatchResult) string { + after, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, *entries[len(entries)-1].IssueMatch)) + return after + }, len(issueMatches), pageSize, ) @@ -398,9 +398,9 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) It("does not influence the count when pagination is applied", func() { var first = 1 - var after int64 = 0 + var after string = "" filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{ + PaginatedX: entity.PaginatedX{ First: &first, After: &after, }, @@ -418,8 +418,8 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { It("does show the correct amount when filtering for an issue", func() { issueMatch := seedCollection.IssueMatchRows[rand.Intn(len(seedCollection.IssueMatchRows))] filter := &entity.IssueMatchFilter{ - Paginated: entity.Paginated{}, - IssueId: []*int64{&issueMatch.IssueId.Int64}, + PaginatedX: entity.PaginatedX{}, + IssueId: []*int64{&issueMatch.IssueId.Int64}, } var imIds []int64 @@ -656,7 +656,7 @@ var _ = Describe("Ordering IssueMatches", func() { var testOrder = func( order []entity.Order, - verifyFunc func(res []entity.IssueMatch), + verifyFunc func(res []entity.IssueMatchResult), ) { res, err := db.GetIssueMatches(nil, order) @@ -677,6 +677,7 @@ var _ = Describe("Ordering IssueMatches", func() { BeforeEach(func() { seedCollection = seeder.SeedDbWithNFakeData(10) + seedCollection.GetValidIssueMatchRows() }) It("can order by id", func() { @@ -688,7 +689,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchId, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { for i, r := range res { Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) } @@ -706,7 +707,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { var prev string = "" for _, r := range res { issue := seedCollection.GetIssueById(r.IssueId) @@ -726,7 +727,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { var prev time.Time = time.Time{} for _, r := range res { Expect(r.TargetRemediationDate.After(prev)).Should(BeTrue()) @@ -747,7 +748,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchRating, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { for i, r := range res { Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) } @@ -765,7 +766,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.ComponentInstanceCcrn, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { var prev string = "" for _, r := range res { ci := seedCollection.GetComponentInstanceById(r.ComponentInstanceId) @@ -792,7 +793,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchId, Direction: entity.OrderDirectionDesc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { for i, r := range res { Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) } @@ -810,7 +811,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionDesc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { var prev string = "\U0010FFFF" for _, r := range res { issue := seedCollection.GetIssueById(r.IssueId) @@ -830,8 +831,8 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionDesc}, } - testOrder(order, func(res []entity.IssueMatch) { - var prev time.Time = time.Now() + testOrder(order, func(res []entity.IssueMatchResult) { + var prev time.Time = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC) for _, r := range res { Expect(r.TargetRemediationDate.Before(prev)).Should(BeTrue()) prev = r.TargetRemediationDate @@ -851,7 +852,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchRating, Direction: entity.OrderDirectionDesc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { for i, r := range res { Expect(r.Id).Should(BeEquivalentTo(seedCollection.IssueMatchRows[i].Id.Int64)) } @@ -869,7 +870,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.ComponentInstanceCcrn, Direction: entity.OrderDirectionDesc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { var prev string = "\U0010FFFF" for _, r := range res { ci := seedCollection.GetComponentInstanceById(r.ComponentInstanceId) @@ -904,8 +905,8 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { - var prevTrd time.Time = time.Time{} + testOrder(order, func(res []entity.IssueMatchResult) { + var prevTrd time.Time = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC) var prevPn = "" for _, r := range res { issue := seedCollection.GetIssueById(r.IssueId) @@ -927,8 +928,8 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionDesc}, } - testOrder(order, func(res []entity.IssueMatch) { - var prevTrd time.Time = time.Now() + testOrder(order, func(res []entity.IssueMatchResult) { + var prevTrd time.Time = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC) var prevPn = "" for _, r := range res { issue := seedCollection.GetIssueById(r.IssueId) @@ -937,7 +938,7 @@ var _ = Describe("Ordering IssueMatches", func() { prevTrd = r.TargetRemediationDate } else { Expect(issue.PrimaryName.String > prevPn).To(BeTrue()) - prevTrd = time.Now() + prevTrd = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC) } prevPn = issue.PrimaryName.String } @@ -951,7 +952,7 @@ var _ = Describe("Ordering IssueMatches", func() { {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, } - testOrder(order, func(res []entity.IssueMatch) { + testOrder(order, func(res []entity.IssueMatchResult) { var prevSeverity = 0 var prevCiCcrn = "" var prevTrd time.Time = time.Time{} @@ -975,6 +976,5 @@ var _ = Describe("Ordering IssueMatches", func() { } }) }) - }) }) diff --git a/internal/database/mariadb/test/common.go b/internal/database/mariadb/test/common.go index b7eaf176..1062c83c 100644 --- a/internal/database/mariadb/test/common.go +++ b/internal/database/mariadb/test/common.go @@ -11,9 +11,9 @@ import ( // Temporary used until order is used in all entities func TestPaginationOfListWithOrder[F entity.HeurekaFilter, E entity.HeurekaEntity]( listFunction func(*F, []entity.Order) ([]E, error), - filterFunction func(*int, *int64) *F, + filterFunction func(*int, *int64, *string) *F, order []entity.Order, - getAfterFunction func([]E) *int64, + getAfterFunction func([]E) string, elementCount int, pageSize int, ) { @@ -24,8 +24,9 @@ func TestPaginationOfListWithOrder[F entity.HeurekaFilter, E entity.HeurekaEntit } var after *int64 + var afterS string for i := expectedPages; i > 0; i-- { - entries, err := listFunction(filterFunction(&pageSize, after), order) + entries, err := listFunction(filterFunction(&pageSize, after, &afterS), order) Expect(err).To(BeNil()) @@ -39,8 +40,7 @@ func TestPaginationOfListWithOrder[F entity.HeurekaFilter, E entity.HeurekaEntit } } - after = getAfterFunction(entries) - + afterS = getAfterFunction(entries) } } diff --git a/internal/e2e/issue_match_query_test.go b/internal/e2e/issue_match_query_test.go index 4cfba555..f7de97fe 100644 --- a/internal/e2e/issue_match_query_test.go +++ b/internal/e2e/issue_match_query_test.go @@ -60,7 +60,7 @@ var _ = Describe("Getting IssueMatches via API", Label("e2e", "IssueMatches"), f req.Var("filter", map[string]string{}) req.Var("first", 10) - req.Var("after", "0") + req.Var("after", "") req.Header.Set("Cache-Control", "no-cache") ctx := context.Background() @@ -98,7 +98,7 @@ var _ = Describe("Getting IssueMatches via API", Label("e2e", "IssueMatches"), f req.Var("filter", map[string]string{}) req.Var("first", 5) - req.Var("after", "0") + req.Var("after", "") req.Header.Set("Cache-Control", "no-cache") ctx := context.Background() @@ -132,7 +132,7 @@ var _ = Describe("Getting IssueMatches via API", Label("e2e", "IssueMatches"), f req.Var("filter", map[string]string{}) req.Var("first", 5) - req.Var("after", "0") + req.Var("after", "") req.Header.Set("Cache-Control", "no-cache") @@ -219,7 +219,7 @@ var _ = Describe("Getting IssueMatches via API", Label("e2e", "IssueMatches"), f req.Var("filter", map[string]string{}) req.Var("first", 10) - req.Var("after", "0") + req.Var("after", "") req.Var("orderBy", []map[string]string{ {"by": "primaryName", "direction": "asc"}, }) @@ -259,7 +259,7 @@ var _ = Describe("Getting IssueMatches via API", Label("e2e", "IssueMatches"), f req.Var("filter", map[string]string{}) req.Var("first", 10) - req.Var("after", "0") + req.Var("after", "") req.Var("orderBy", []map[string]string{ {"by": "primaryName", "direction": "asc"}, {"by": "targetRemediationDate", "direction": "desc"}, diff --git a/internal/entity/common.go b/internal/entity/common.go index 92cd4b85..7b05dc84 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -4,7 +4,6 @@ package entity import ( - "fmt" "math" "time" @@ -53,6 +52,7 @@ type HeurekaEntity interface { IssueAggregations | Issue | IssueMatch | + IssueMatchResult | IssueMatchChange | HeurekaFilter | IssueCount | @@ -145,6 +145,11 @@ type Paginated struct { After *int64 `json:"from"` } +type PaginatedX struct { + First *int `json:"first"` + After *string `json:"from"` +} + func MaxPaginated() Paginated { return Paginated{ First: util.Ptr(math.MaxInt), @@ -230,69 +235,3 @@ type Metadata struct { UpdatedBy int64 `json:"updated_by"` DeletedAt time.Time `json:"deleted_at,omitempty"` } - -type DbColumnName int - -const ( - ComponentInstanceCcrn DbColumnName = iota - - IssuePrimaryName - - IssueMatchId - IssueMatchRating - IssueMatchTargetRemediationDate - - SupportGroupName -) - -func (d DbColumnName) String() string { - // order of string needs to match iota order - return [...]string{ - "componentinstance_ccrn", - "issue_primary_name", - "issuematch_id", - "issuematch_rating", - "issuematch_target_remediation_date", - "supportgroup_name", - }[d] -} - -type OrderDirection int - -const ( - OrderDirectionAsc OrderDirection = iota - OrderDirectionDesc -) - -func (o OrderDirection) String() string { - // order of string needs to match iota order - return [...]string{ - "ASC", - "DESC", - }[o] -} - -type Order struct { - By DbColumnName - Direction OrderDirection -} - -func CreateOrderMap(order []Order) map[DbColumnName]OrderDirection { - m := map[DbColumnName]OrderDirection{} - for _, o := range order { - m[o.By] = o.Direction - } - return m -} - -func CreateOrderString(order []Order) string { - orderStr := "" - for i, o := range order { - if i > 0 { - orderStr = fmt.Sprintf("%s, %s %s", orderStr, o.By, o.Direction) - } else { - orderStr = fmt.Sprintf("%s %s %s", orderStr, o.By, o.Direction) - } - } - return orderStr -} diff --git a/internal/entity/cursor.go b/internal/entity/cursor.go index d1ed52f5..51dc1ef2 100644 --- a/internal/entity/cursor.go +++ b/internal/entity/cursor.go @@ -22,7 +22,7 @@ type cursors struct { type NewCursor func(cursors *cursors) error -func EncodeCursor(order []Order, opts ...NewCursor) (string, error) { +func EncodeCursor(opts ...NewCursor) (string, error) { var cursors cursors for _, opt := range opts { err := opt(&cursors) @@ -32,13 +32,6 @@ func EncodeCursor(order []Order, opts ...NewCursor) (string, error) { } } - m := CreateOrderMap(order) - for _, f := range cursors.fields { - if orderDirection, ok := m[f.Name]; ok { - f.Order = orderDirection - } - } - var buf bytes.Buffer encoder := base64.NewEncoder(base64.StdEncoding, &buf) err := json.NewEncoder(encoder).Encode(cursors.fields) @@ -49,13 +42,16 @@ func EncodeCursor(order []Order, opts ...NewCursor) (string, error) { return buf.String(), nil } -func DecodeCursor(cursor string) ([]Field, error) { - decoded, err := base64.StdEncoding.DecodeString(cursor) +func DecodeCursor(cursor *string) ([]Field, error) { + var fields []Field + if cursor == nil || *cursor == "" { + return fields, nil + } + decoded, err := base64.StdEncoding.DecodeString(*cursor) if err != nil { return nil, fmt.Errorf("failed to decode base64 string: %w", err) } - var fields []Field if err := json.Unmarshal(decoded, &fields); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) } @@ -63,10 +59,68 @@ func DecodeCursor(cursor string) ([]Field, error) { return fields, nil } -func WithIssueMatch(im IssueMatch) NewCursor { +func CreateCursorQuery(query string, fields []Field) string { + if len(fields) == 0 { + return query + } + + subQuery := "" + for i, f := range fields { + dir := ">" + switch f.Order { + case OrderDirectionAsc: + dir = ">" + case OrderDirectionDesc: + dir = "<" + } + if i >= len(fields)-1 { + subQuery = fmt.Sprintf("%s %s %s ? ", subQuery, f.Name, dir) + } else { + subQuery = fmt.Sprintf("%s %s = ? AND ", subQuery, f.Name) + } + } + + subQuery = fmt.Sprintf("( %s )", subQuery) + if query != "" { + subQuery = fmt.Sprintf("%s OR %s", subQuery, query) + } + + return CreateCursorQuery(subQuery, fields[:len(fields)-1]) +} + +func CreateCursorParameters(params []any, fields []Field) []any { + if len(fields) == 0 { + return params + } + + for i := 0; i < len(fields); i++ { + params = append(params, fields[i].Value) + } + + return CreateCursorParameters(params, fields[:len(fields)-1]) +} + +func WithIssueMatch(order []Order, im IssueMatch) NewCursor { + return func(cursors *cursors) error { cursors.fields = append(cursors.fields, Field{Name: IssueMatchId, Value: im.Id, Order: OrderDirectionAsc}) - // cursors.fields = append(cursors.fields, Field{Name: IssueMatchRating, Value: im.Rating, Order: OrderDirectionAsc}) + cursors.fields = append(cursors.fields, Field{Name: IssueMatchTargetRemediationDate, Value: im.TargetRemediationDate, Order: OrderDirectionAsc}) + cursors.fields = append(cursors.fields, Field{Name: IssueMatchRating, Value: im.Severity.Value, Order: OrderDirectionAsc}) + + if im.ComponentInstance != nil { + cursors.fields = append(cursors.fields, Field{Name: ComponentInstanceCcrn, Value: im.ComponentInstance.CCRN, Order: OrderDirectionAsc}) + } + if im.Issue != nil { + cursors.fields = append(cursors.fields, Field{Name: IssuePrimaryName, Value: im.Issue.PrimaryName, Order: OrderDirectionAsc}) + } + + m := CreateOrderMap(order) + for _, f := range cursors.fields { + if orderDirection, ok := m[f.Name]; ok { + f.Order = orderDirection + } + } + return nil } } diff --git a/internal/entity/issue_match.go b/internal/entity/issue_match.go index 8e020893..9488de86 100644 --- a/internal/entity/issue_match.go +++ b/internal/entity/issue_match.go @@ -55,7 +55,7 @@ type IssueMatch struct { } type IssueMatchFilter struct { - Paginated + PaginatedX Id []*int64 `json:"id"` AffectedServiceCCRN []*string `json:"affected_service_ccrn"` SeverityValue []*string `json:"severity_value"` diff --git a/internal/entity/order.go b/internal/entity/order.go new file mode 100644 index 00000000..4a7efe41 --- /dev/null +++ b/internal/entity/order.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package entity + +import "fmt" + +type DbColumnName int + +const ( + ComponentInstanceCcrn DbColumnName = iota + + IssuePrimaryName + + IssueMatchId + IssueMatchRating + IssueMatchTargetRemediationDate + + SupportGroupName +) + +var DbColumnNameMap = map[DbColumnName]string{ + ComponentInstanceCcrn: "componentinstance_ccrn", + IssuePrimaryName: "issue_primary_name", + IssueMatchId: "issuematch_id", + IssueMatchRating: "issuematch_rating", + IssueMatchTargetRemediationDate: "issuematch_target_remediation_date", + SupportGroupName: "supportgroup_name", +} + +func (d DbColumnName) String() string { + return DbColumnNameMap[d] +} + +type OrderDirection int + +const ( + OrderDirectionAsc OrderDirection = iota + OrderDirectionDesc +) + +var OrderDirectionMap = map[OrderDirection]string{ + OrderDirectionAsc: "ASC", + OrderDirectionDesc: "DESC", +} + +func (o OrderDirection) String() string { + return OrderDirectionMap[o] +} + +type Order struct { + By DbColumnName + Direction OrderDirection +} + +func CreateOrderMap(order []Order) map[DbColumnName]OrderDirection { + m := map[DbColumnName]OrderDirection{} + for _, o := range order { + m[o.By] = o.Direction + } + return m +} + +func CreateOrderString(order []Order) string { + orderStr := "" + for i, o := range order { + if i > 0 { + orderStr = fmt.Sprintf("%s, %s %s", orderStr, o.By, o.Direction) + } else { + orderStr = fmt.Sprintf("%s %s %s", orderStr, o.By, o.Direction) + } + } + return orderStr +} diff --git a/internal/entity/test/issue_match.go b/internal/entity/test/issue_match.go index 45ecfc62..6852463f 100644 --- a/internal/entity/test/issue_match.go +++ b/internal/entity/test/issue_match.go @@ -45,3 +45,10 @@ func NewRandomIssueStatus() entity.IssueMatchStatusValue { value := gofakeit.RandomString(entity.AllIssueMatchStatusValues) return entity.NewIssueMatchStatusValue(value) } + +func NewFakeIssueMatchResult() entity.IssueMatchResult { + im := NewFakeIssueMatch() + return entity.IssueMatchResult{ + IssueMatch: &im, + } +} From e4645246cb559f78f4944b579f42f00beb7378e3 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Mon, 27 Jan 2025 12:45:32 +0100 Subject: [PATCH 07/11] remove err logging --- internal/entity/cursor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/entity/cursor.go b/internal/entity/cursor.go index 51dc1ef2..29947972 100644 --- a/internal/entity/cursor.go +++ b/internal/entity/cursor.go @@ -27,7 +27,6 @@ func EncodeCursor(opts ...NewCursor) (string, error) { for _, opt := range opts { err := opt(&cursors) if err != nil { - fmt.Println("err") return "", err } } From faf977e9d093e16013ccf0c7099c16aaad164c43 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Mon, 27 Jan 2025 17:38:05 +0100 Subject: [PATCH 08/11] update docs --- docs/cursor.md | 85 ++++++++++++++++++++++++++++++++++++++ docs/ordering.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 docs/cursor.md create mode 100644 docs/ordering.md diff --git a/docs/cursor.md b/docs/cursor.md new file mode 100644 index 00000000..f1c8c052 --- /dev/null +++ b/docs/cursor.md @@ -0,0 +1,85 @@ +# Cursor + +The cursor is a list of `Field`, where `Field` is defined as: + +``` +type Field struct { + Name DbColumnName + Value any + Order OrderDirection +} +``` + +This list will be encoded as a base64 string. + +Each entity defines its own cursor values. For example `IssueMatch` allows the following cursor fields: + +- IssueMatchId +- IssueMatchTargetRemediationDate +- IssueMatchRating +- ComponentInstanceCCRN +- IssuePrimaryName + +``` +func WithIssueMatch(order []Order, im IssueMatch) NewCursor { + + return func(cursors *cursors) error { + cursors.fields = append(cursors.fields, Field{Name: IssueMatchId, Value: im.Id, Order: OrderDirectionAsc}) + cursors.fields = append(cursors.fields, Field{Name: IssueMatchTargetRemediationDate, Value: im.TargetRemediationDate, Order: OrderDirectionAsc}) + cursors.fields = append(cursors.fields, Field{Name: IssueMatchRating, Value: im.Severity.Value, Order: OrderDirectionAsc}) + + if im.ComponentInstance != nil { + cursors.fields = append(cursors.fields, Field{Name: ComponentInstanceCcrn, Value: im.ComponentInstance.CCRN, Order: OrderDirectionAsc}) + } + if im.Issue != nil { + cursors.fields = append(cursors.fields, Field{Name: IssuePrimaryName, Value: im.Issue.PrimaryName, Order: OrderDirectionAsc}) + } + + m := CreateOrderMap(order) + for _, f := range cursors.fields { + if orderDirection, ok := m[f.Name]; ok { + f.Order = orderDirection + } + } + + return nil + } +} +``` + +The cursor is returned by the database layer an can be encoded such as: + +``` + cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im)) +``` + +A order list can be passed to override the default ordering. + +## Cursor Query + +The cursor points to the starting point in a list of database rows. All elements *after* the cursor are returned. +Depending on the ordering, the query looks like: + +``` +WHERE id < cursor_id + +Or: + +Where id > cursor_id + +``` + +If the cursor contains two fields, the query needs to check for the second field, if the first field is equal: + +``` +WHERE (id = cursor_id AND primaryName > cursor_primaryName) OR (id > cursor_id) + +``` + +Similarly, for three fields: +``` +WHERE + (id = cursor_id AND primaryName = cursor_primaryName AND trd > cursor_trd) OR + (id = cursor_id AND primaryName > cursor_primaryName) OR + (id > cursor_id) +``` \ No newline at end of file diff --git a/docs/ordering.md b/docs/ordering.md new file mode 100644 index 00000000..c722d15b --- /dev/null +++ b/docs/ordering.md @@ -0,0 +1,105 @@ +# Order + +## API + +The query contains an additional `orderBy` argument: + +``` +IssueMatches(filter: IssueMatchFilter, first: Int, after: String, orderBy: [IssueMatchOrderBy]): IssueMatchConnection +``` + +The OrderBy input is defined for each model: + +``` +input IssueMatchOrderBy { + by: IssueMatchOrderByField + direction: OrderDirection +} +``` + +The `By` fields define the allowed order options: + +``` +enum IssueMatchOrderByField { + primaryName + targetRemediationDate + componentInstanceCcrn +} +``` + +The `OrderDirections` are defined in the `common.graphqls`: +``` +enum OrderDirection { + asc + desc +} +``` + +The generated order models are converted to the entity order model in `api/graph/model/models.go`: + +``` +func (imo *IssueMatchOrderBy) ToOrderEntity() entity.Order { + var order entity.Order + switch *imo.By { + case IssueMatchOrderByFieldPrimaryName: + order.By = entity.IssuePrimaryName + case IssueMatchOrderByFieldComponentInstanceCcrn: + order.By = entity.ComponentInstanceCcrn + case IssueMatchOrderByFieldTargetRemediationDate: + order.By = entity.IssueMatchTargetRemediationDate + } + order.Direction = imo.Direction.ToOrderDirectionEntity() + return order +} +``` + +## Entity + +``` +type Order struct { + By DbColumnName + Direction OrderDirection +} +``` + +The `By` field is the database column name and is defined as a constant: + +``` +var DbColumnNameMap = map[DbColumnName]string{ + ComponentInstanceCcrn: "componentinstance_ccrn", + IssuePrimaryName: "issue_primary_name", + IssueMatchId: "issuematch_id", + IssueMatchRating: "issuematch_rating", + IssueMatchTargetRemediationDate: "issuematch_target_remediation_date", + SupportGroupName: "supportgroup_name", +} +``` + + +## Database + +The `GetIssueMatches()` function has an additional order argument: + +``` +func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatchResult, error) { + ... +} +``` + +The order string is created by in `entity/order.go`: + +``` +func CreateOrderString(order []Order) string { + orderStr := "" + for i, o := range order { + if i > 0 { + orderStr = fmt.Sprintf("%s, %s %s", orderStr, o.By, o.Direction) + } else { + orderStr = fmt.Sprintf("%s %s %s", orderStr, o.By, o.Direction) + } + } + return orderStr +} +``` + + From 6cda38021955ef25f7bf4ff0a7aade60cec50961 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Tue, 4 Feb 2025 16:17:19 +0100 Subject: [PATCH 09/11] doc(ordering): add syntax highlight --- docs/ordering.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/ordering.md b/docs/ordering.md index c722d15b..4b76c2bb 100644 --- a/docs/ordering.md +++ b/docs/ordering.md @@ -4,13 +4,13 @@ The query contains an additional `orderBy` argument: -``` +```graphql IssueMatches(filter: IssueMatchFilter, first: Int, after: String, orderBy: [IssueMatchOrderBy]): IssueMatchConnection ``` The OrderBy input is defined for each model: -``` +```graphql input IssueMatchOrderBy { by: IssueMatchOrderByField direction: OrderDirection @@ -19,7 +19,7 @@ input IssueMatchOrderBy { The `By` fields define the allowed order options: -``` +```graphql enum IssueMatchOrderByField { primaryName targetRemediationDate @@ -28,7 +28,7 @@ enum IssueMatchOrderByField { ``` The `OrderDirections` are defined in the `common.graphqls`: -``` +```graphql enum OrderDirection { asc desc @@ -37,7 +37,7 @@ enum OrderDirection { The generated order models are converted to the entity order model in `api/graph/model/models.go`: -``` +```go func (imo *IssueMatchOrderBy) ToOrderEntity() entity.Order { var order entity.Order switch *imo.By { @@ -55,7 +55,7 @@ func (imo *IssueMatchOrderBy) ToOrderEntity() entity.Order { ## Entity -``` +```go type Order struct { By DbColumnName Direction OrderDirection @@ -64,7 +64,7 @@ type Order struct { The `By` field is the database column name and is defined as a constant: -``` +```go var DbColumnNameMap = map[DbColumnName]string{ ComponentInstanceCcrn: "componentinstance_ccrn", IssuePrimaryName: "issue_primary_name", @@ -80,7 +80,7 @@ var DbColumnNameMap = map[DbColumnName]string{ The `GetIssueMatches()` function has an additional order argument: -``` +```go func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatchResult, error) { ... } @@ -88,7 +88,7 @@ func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []e The order string is created by in `entity/order.go`: -``` +```go func CreateOrderString(order []Order) string { orderStr := "" for i, o := range order { From ca264ac9b9535bb58cad887aac2ba817a564d130 Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Tue, 4 Feb 2025 16:18:06 +0100 Subject: [PATCH 10/11] doc(cursor): add syntax highlight --- docs/cursor.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/cursor.md b/docs/cursor.md index f1c8c052..0a63b386 100644 --- a/docs/cursor.md +++ b/docs/cursor.md @@ -2,7 +2,7 @@ The cursor is a list of `Field`, where `Field` is defined as: -``` +```go type Field struct { Name DbColumnName Value any @@ -20,7 +20,7 @@ Each entity defines its own cursor values. For example `IssueMatch` allows the f - ComponentInstanceCCRN - IssuePrimaryName -``` +```go func WithIssueMatch(order []Order, im IssueMatch) NewCursor { return func(cursors *cursors) error { @@ -49,7 +49,7 @@ func WithIssueMatch(order []Order, im IssueMatch) NewCursor { The cursor is returned by the database layer an can be encoded such as: -``` +```go cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im)) ``` @@ -60,7 +60,7 @@ A order list can be passed to override the default ordering. The cursor points to the starting point in a list of database rows. All elements *after* the cursor are returned. Depending on the ordering, the query looks like: -``` +```sql WHERE id < cursor_id Or: @@ -71,15 +71,15 @@ Where id > cursor_id If the cursor contains two fields, the query needs to check for the second field, if the first field is equal: -``` +```sql WHERE (id = cursor_id AND primaryName > cursor_primaryName) OR (id > cursor_id) ``` Similarly, for three fields: -``` +```sql WHERE (id = cursor_id AND primaryName = cursor_primaryName AND trd > cursor_trd) OR (id = cursor_id AND primaryName > cursor_primaryName) OR (id > cursor_id) -``` \ No newline at end of file +``` From 668120ad848cd3c0158946255b24d8a34d62d36b Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Thu, 6 Feb 2025 15:28:22 +0100 Subject: [PATCH 11/11] fix comments --- .../issue_match/issue_match_handler_test.go | 8 +- .../{entity => database/mariadb}/cursor.go | 54 ++-- internal/database/mariadb/database.go | 2 +- internal/database/mariadb/issue_match.go | 40 ++- internal/database/mariadb/issue_match_test.go | 182 +++++++++++- internal/database/mariadb/order.go | 52 ++++ .../component_instance.json | 122 ++++++++ .../testdata/issue_match_cursor/issue.json | 102 +++++++ .../issue_match_cursor/issue_match.json | 272 ++++++++++++++++++ internal/database/mariadb/user_test.go | 2 +- internal/entity/order.go | 50 +--- 11 files changed, 795 insertions(+), 91 deletions(-) rename internal/{entity => database/mariadb}/cursor.go (54%) create mode 100644 internal/database/mariadb/order.go create mode 100644 internal/database/mariadb/testdata/issue_match_cursor/component_instance.json create mode 100644 internal/database/mariadb/testdata/issue_match_cursor/issue.json create mode 100644 internal/database/mariadb/testdata/issue_match_cursor/issue_match.json diff --git a/internal/app/issue_match/issue_match_handler_test.go b/internal/app/issue_match/issue_match_handler_test.go index 8f094cfd..f0bbb0a4 100644 --- a/internal/app/issue_match/issue_match_handler_test.go +++ b/internal/app/issue_match/issue_match_handler_test.go @@ -14,6 +14,7 @@ import ( "github.com/cloudoperators/heureka/internal/app/issue_repository" "github.com/cloudoperators/heureka/internal/app/issue_variant" "github.com/cloudoperators/heureka/internal/app/severity" + "github.com/cloudoperators/heureka/internal/database/mariadb" "github.com/samber/lo" @@ -91,13 +92,12 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), filter.First = &pageSize matches := []entity.IssueMatchResult{} for _, im := range test.NNewFakeIssueMatches(resElements) { - cursor, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, im)) + cursor, _ := mariadb.EncodeCursor(mariadb.WithIssueMatch([]entity.Order{}, im)) matches = append(matches, entity.IssueMatchResult{WithCursor: entity.WithCursor{Value: cursor}, IssueMatch: lo.ToPtr(im)}) } - // cursors := []string{} var cursors = lo.Map(matches, func(m entity.IssueMatchResult, _ int) string { - cursor, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, *m.IssueMatch)) + cursor, _ := mariadb.EncodeCursor(mariadb.WithIssueMatch([]entity.Order{}, *m.IssueMatch)) return cursor }) @@ -105,7 +105,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), for len(cursors) < dbElements { i++ im := test.NewFakeIssueMatch() - c, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, im)) + c, _ := mariadb.EncodeCursor(mariadb.WithIssueMatch([]entity.Order{}, im)) cursors = append(cursors, c) } db.On("GetIssueMatches", filter, []entity.Order{}).Return(matches, nil) diff --git a/internal/entity/cursor.go b/internal/database/mariadb/cursor.go similarity index 54% rename from internal/entity/cursor.go rename to internal/database/mariadb/cursor.go index 29947972..d241e700 100644 --- a/internal/entity/cursor.go +++ b/internal/database/mariadb/cursor.go @@ -1,19 +1,21 @@ // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors // SPDX-License-Identifier: Apache-2.0 -package entity +package mariadb import ( "bytes" "encoding/base64" "encoding/json" "fmt" + + "github.com/cloudoperators/heureka/internal/entity" ) type Field struct { - Name DbColumnName + Name entity.OrderByField Value any - Order OrderDirection + Order entity.OrderDirection } type cursors struct { @@ -67,21 +69,21 @@ func CreateCursorQuery(query string, fields []Field) string { for i, f := range fields { dir := ">" switch f.Order { - case OrderDirectionAsc: + case entity.OrderDirectionAsc: dir = ">" - case OrderDirectionDesc: + case entity.OrderDirectionDesc: dir = "<" } if i >= len(fields)-1 { - subQuery = fmt.Sprintf("%s %s %s ? ", subQuery, f.Name, dir) + subQuery = fmt.Sprintf("%s %s %s ? ", subQuery, ColumnName(f.Name), dir) } else { - subQuery = fmt.Sprintf("%s %s = ? AND ", subQuery, f.Name) + subQuery = fmt.Sprintf("%s %s = ? AND ", subQuery, ColumnName(f.Name)) } } subQuery = fmt.Sprintf("( %s )", subQuery) if query != "" { - subQuery = fmt.Sprintf("%s OR %s", subQuery, query) + subQuery = fmt.Sprintf("%s OR %s", query, subQuery) } return CreateCursorQuery(subQuery, fields[:len(fields)-1]) @@ -99,24 +101,28 @@ func CreateCursorParameters(params []any, fields []Field) []any { return CreateCursorParameters(params, fields[:len(fields)-1]) } -func WithIssueMatch(order []Order, im IssueMatch) NewCursor { +func WithIssueMatch(order []entity.Order, im entity.IssueMatch) NewCursor { return func(cursors *cursors) error { - cursors.fields = append(cursors.fields, Field{Name: IssueMatchId, Value: im.Id, Order: OrderDirectionAsc}) - cursors.fields = append(cursors.fields, Field{Name: IssueMatchTargetRemediationDate, Value: im.TargetRemediationDate, Order: OrderDirectionAsc}) - cursors.fields = append(cursors.fields, Field{Name: IssueMatchRating, Value: im.Severity.Value, Order: OrderDirectionAsc}) - - if im.ComponentInstance != nil { - cursors.fields = append(cursors.fields, Field{Name: ComponentInstanceCcrn, Value: im.ComponentInstance.CCRN, Order: OrderDirectionAsc}) - } - if im.Issue != nil { - cursors.fields = append(cursors.fields, Field{Name: IssuePrimaryName, Value: im.Issue.PrimaryName, Order: OrderDirectionAsc}) - } - - m := CreateOrderMap(order) - for _, f := range cursors.fields { - if orderDirection, ok := m[f.Name]; ok { - f.Order = orderDirection + order = GetDefaultOrder(order, entity.IssueMatchId, entity.OrderDirectionAsc) + for _, o := range order { + switch o.By { + case entity.IssueMatchId: + cursors.fields = append(cursors.fields, Field{Name: entity.IssueMatchId, Value: im.Id, Order: o.Direction}) + case entity.IssueMatchTargetRemediationDate: + cursors.fields = append(cursors.fields, Field{Name: entity.IssueMatchTargetRemediationDate, Value: im.TargetRemediationDate, Order: o.Direction}) + case entity.IssueMatchRating: + cursors.fields = append(cursors.fields, Field{Name: entity.IssueMatchRating, Value: im.Severity.Value, Order: o.Direction}) + case entity.ComponentInstanceCcrn: + if im.ComponentInstance != nil { + cursors.fields = append(cursors.fields, Field{Name: entity.ComponentInstanceCcrn, Value: im.ComponentInstance.CCRN, Order: o.Direction}) + } + case entity.IssuePrimaryName: + if im.Issue != nil { + cursors.fields = append(cursors.fields, Field{Name: entity.IssuePrimaryName, Value: im.Issue.PrimaryName, Order: o.Direction}) + } + default: + continue } } diff --git a/internal/database/mariadb/database.go b/internal/database/mariadb/database.go index 198bdc76..16630881 100644 --- a/internal/database/mariadb/database.go +++ b/internal/database/mariadb/database.go @@ -392,7 +392,7 @@ func getCursor(p entity.Paginated, filterStr string, stmt string) entity.Cursor } } -func GetDefaultOrder(order []entity.Order, by entity.DbColumnName, direction entity.OrderDirection) []entity.Order { +func GetDefaultOrder(order []entity.Order, by entity.OrderByField, direction entity.OrderDirection) []entity.Order { if len(order) == 0 { order = append([]entity.Order{{By: by, Direction: direction}}, order...) } diff --git a/internal/database/mariadb/issue_match.go b/internal/database/mariadb/issue_match.go index 15ce719b..704e6e65 100644 --- a/internal/database/mariadb/issue_match.go +++ b/internal/database/mariadb/issue_match.go @@ -142,21 +142,35 @@ func (s *SqlDatabase) getIssueMatchUpdateFields(issueMatch *entity.IssueMatch) s return strings.Join(fl, ", ") } +func (s *SqlDatabase) getIssueMatchColumns(order []entity.Order) string { + columns := "" + for _, o := range order { + switch o.By { + case entity.IssuePrimaryName: + columns = fmt.Sprintf("%s, I.issue_primary_name", columns) + case entity.ComponentInstanceCcrn: + columns = fmt.Sprintf("%s, CI.componentinstance_ccrn", columns) + } + } + return columns +} + func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity.IssueMatchFilter, withCursor bool, order []entity.Order, l *logrus.Entry) (*sqlx.Stmt, []interface{}, error) { var query string filter = s.ensureIssueMatchFilter(filter) l.WithFields(logrus.Fields{"filter": filter}) filterStr := s.getIssueMatchFilterString(filter) - joins := s.getIssueMatchJoins(filter, order) - cursorFields, err := entity.DecodeCursor(filter.PaginatedX.After) + cursorFields, err := DecodeCursor(filter.PaginatedX.After) if err != nil { return nil, nil, err } - cursorQuery := entity.CreateCursorQuery("", cursorFields) + cursorQuery := CreateCursorQuery("", cursorFields) order = GetDefaultOrder(order, entity.IssueMatchId, entity.OrderDirectionAsc) - orderStr := entity.CreateOrderString(order) + orderStr := CreateOrderString(order) + columns := s.getIssueMatchColumns(order) + joins := s.getIssueMatchJoins(filter, order) whereClause := "" if filterStr != "" || withCursor { @@ -169,9 +183,9 @@ func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity. // construct final query if withCursor { - query = fmt.Sprintf(baseQuery, joins, whereClause, cursorQuery, orderStr) + query = fmt.Sprintf(baseQuery, columns, joins, whereClause, cursorQuery, orderStr) } else { - query = fmt.Sprintf(baseQuery, joins, whereClause, orderStr) + query = fmt.Sprintf(baseQuery, columns, joins, whereClause, orderStr) } //construct prepared statement and if where clause does exist add parameters @@ -205,7 +219,7 @@ func (s *SqlDatabase) buildIssueMatchStatement(baseQuery string, filter *entity. filterParameters = buildQueryParametersCount(filterParameters, filter.Search, wildCardFilterParamCount) if withCursor { - p := entity.CreateCursorParameters([]any{}, cursorFields) + p := CreateCursorParameters([]any{}, cursorFields) filterParameters = append(filterParameters, p...) if filter.PaginatedX.First == nil { filterParameters = append(filterParameters, 1000) @@ -224,7 +238,7 @@ func (s *SqlDatabase) GetAllIssueMatchIds(filter *entity.IssueMatchFilter) ([]in }) baseQuery := ` - SELECT IM.issuematch_id FROM IssueMatch IM + SELECT IM.issuematch_id %s FROM IssueMatch IM %s %s GROUP BY IM.issuematch_id ORDER BY %s ` @@ -245,7 +259,7 @@ func (s *SqlDatabase) GetAllIssueMatchCursors(filter *entity.IssueMatchFilter, o }) baseQuery := ` - SELECT IM.* FROM IssueMatch IM + SELECT IM.* %s FROM IssueMatch IM %s %s GROUP BY IM.issuematch_id ORDER BY %s ` @@ -278,7 +292,7 @@ func (s *SqlDatabase) GetAllIssueMatchCursors(filter *entity.IssueMatchFilter, o im.ComponentInstance = lo.ToPtr(row.ComponentInstanceRow.AsComponentInstance()) } - cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im)) + cursor, _ := EncodeCursor(WithIssueMatch(order, im)) return cursor }), nil @@ -291,7 +305,7 @@ func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []e }) baseQuery := ` - SELECT IM.* FROM IssueMatch IM + SELECT IM.* %s FROM IssueMatch IM %s %s %s GROUP BY IM.issuematch_id ORDER BY %s LIMIT ? ` @@ -315,7 +329,7 @@ func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []e im.ComponentInstance = lo.ToPtr(e.ComponentInstanceRow.AsComponentInstance()) } - cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im)) + cursor, _ := EncodeCursor(WithIssueMatch(order, im)) imr := entity.IssueMatchResult{ WithCursor: entity.WithCursor{ @@ -335,7 +349,7 @@ func (s *SqlDatabase) CountIssueMatches(filter *entity.IssueMatchFilter) (int64, }) baseQuery := ` - SELECT count(distinct IM.issuematch_id) FROM IssueMatch IM + SELECT count(distinct IM.issuematch_id) %s FROM IssueMatch IM %s %s ORDER BY %s diff --git a/internal/database/mariadb/issue_match_test.go b/internal/database/mariadb/issue_match_test.go index 3fabbb75..35868217 100644 --- a/internal/database/mariadb/issue_match_test.go +++ b/internal/database/mariadb/issue_match_test.go @@ -4,7 +4,12 @@ package mariadb_test import ( + "database/sql" + "encoding/json" "math/rand" + "os" + "path/filepath" + "runtime" "sort" "time" @@ -353,7 +358,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }, []entity.Order{}, func(entries []entity.IssueMatchResult) string { - after, _ := entity.EncodeCursor(entity.WithIssueMatch([]entity.Order{}, *entries[len(entries)-1].IssueMatch)) + after, _ := mariadb.EncodeCursor(mariadb.WithIssueMatch([]entity.Order{}, *entries[len(entries)-1].IssueMatch)) return after }, len(issueMatches), @@ -370,6 +375,7 @@ var _ = Describe("IssueMatch", Label("database", "IssueMatch"), func() { }) }) }) + When("Counting Issue Matches", Label("CountIssueMatches"), func() { Context("and using no filter", func() { DescribeTable("it returns correct count", func(x int) { @@ -978,3 +984,177 @@ var _ = Describe("Ordering IssueMatches", func() { }) }) }) + +// getTestDataPath returns the path to the test data directory relative to the calling file +func getTestDataPath(f string) string { + // Get the current file path + _, filename, _, _ := runtime.Caller(1) + // Get the directory containing the current file + dir := filepath.Dir(filename) + // Return path to test data directory (adjust the relative path as needed) + return filepath.Join(dir, "testdata", "issue_match_cursor", f) +} + +// LoadIssueMatches loads issue matches from JSON file +func LoadIssueMatches(filename string) ([]mariadb.IssueMatchRow, error) { + // Read JSON file + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + // Parse JSON into temporary struct that matches the JSON format + type tempIssueMatch struct { + Status string `json:"status"` + Rating string `json:"rating"` + Vector string `json:"vector"` + UserID int64 `json:"user_id"` + ComponentInstanceID int64 `json:"component_instance_id"` + IssueID int64 `json:"issue_id"` + TargetRemediationDate time.Time `json:"target_remediation_date"` + } + var tempMatches []tempIssueMatch + if err := json.Unmarshal(data, &tempMatches); err != nil { + return nil, err + } + // Convert to IssueMatchRow format + matches := make([]mariadb.IssueMatchRow, len(tempMatches)) + for i, tm := range tempMatches { + matches[i] = mariadb.IssueMatchRow{ + Status: sql.NullString{String: tm.Status, Valid: true}, + Rating: sql.NullString{String: tm.Rating, Valid: true}, + Vector: sql.NullString{String: tm.Vector, Valid: true}, + UserId: sql.NullInt64{Int64: tm.UserID, Valid: true}, + ComponentInstanceId: sql.NullInt64{Int64: tm.ComponentInstanceID, Valid: true}, + IssueId: sql.NullInt64{Int64: tm.IssueID, Valid: true}, + TargetRemediationDate: sql.NullTime{Time: tm.TargetRemediationDate, Valid: true}, + } + } + return matches, nil +} + +// LoadIssues loads issues from JSON file +func LoadIssues(filename string) ([]mariadb.IssueRow, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + type tempIssue struct { + Type string `json:"type"` + PrimaryName string `json:"primary_name"` + Description string `json:"description"` + } + var tempIssues []tempIssue + if err := json.Unmarshal(data, &tempIssues); err != nil { + return nil, err + } + issues := make([]mariadb.IssueRow, len(tempIssues)) + for i, ti := range tempIssues { + issues[i] = mariadb.IssueRow{ + Type: sql.NullString{String: ti.Type, Valid: true}, + PrimaryName: sql.NullString{String: ti.PrimaryName, Valid: true}, + Description: sql.NullString{String: ti.Description, Valid: true}, + } + } + return issues, nil +} + +// LoadComponentInstances loads component instances from JSON file +func LoadComponentInstances(filename string) ([]mariadb.ComponentInstanceRow, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + type tempComponentInstance struct { + CCRN string `json:"ccrn"` + Count int16 `json:"count"` + ComponentVersionID int64 `json:"component_version_id"` + ServiceID int64 `json:"service_id"` + } + var tempComponents []tempComponentInstance + if err := json.Unmarshal(data, &tempComponents); err != nil { + return nil, err + } + components := make([]mariadb.ComponentInstanceRow, len(tempComponents)) + for i, tc := range tempComponents { + components[i] = mariadb.ComponentInstanceRow{ + CCRN: sql.NullString{String: tc.CCRN, Valid: true}, + Count: sql.NullInt16{Int16: tc.Count, Valid: true}, + ComponentVersionId: sql.NullInt64{Int64: tc.ComponentVersionID, Valid: true}, + ServiceId: sql.NullInt64{Int64: tc.ServiceID, Valid: true}, + } + } + return components, nil +} + +var _ = Describe("Using the Cursor on IssueMatches", func() { + var db *mariadb.SqlDatabase + var seeder *test.DatabaseSeeder + BeforeEach(func() { + var err error + db = dbm.NewTestSchema() + seeder, err = test.NewDatabaseSeeder(dbm.DbConfig()) + Expect(err).To(BeNil(), "Database Seeder Setup should work") + }) + var loadTestData = func() ([]mariadb.IssueMatchRow, []mariadb.IssueRow, []mariadb.ComponentInstanceRow, error) { + matches, err := LoadIssueMatches(getTestDataPath("issue_match.json")) + if err != nil { + return nil, nil, nil, err + } + issues, err := LoadIssues(getTestDataPath("issue.json")) + if err != nil { + return nil, nil, nil, err + } + components, err := LoadComponentInstances(getTestDataPath("component_instance.json")) + if err != nil { + return nil, nil, nil, err + } + return matches, issues, components, nil + } + When("multiple orders used", func() { + BeforeEach(func() { + seeder.SeedUsers(10) + seeder.SeedServices(10) + components := seeder.SeedComponents(10) + seeder.SeedComponentVersions(10, components) + matches, issues, cis, err := loadTestData() + Expect(err).To(BeNil()) + // Important: the order need to be preserved + for _, ci := range cis { + _, err := seeder.InsertFakeComponentInstance(ci) + Expect(err).To(BeNil()) + } + for _, issue := range issues { + _, err := seeder.InsertFakeIssue(issue) + Expect(err).To(BeNil()) + } + for _, match := range matches { + _, err := seeder.InsertFakeIssueMatch(match) + Expect(err).To(BeNil()) + } + }) + It("can order by primary name and target remediation date", func() { + filter := entity.IssueMatchFilter{ + Id: []*int64{lo.ToPtr(int64(10))}, + } + order := []entity.Order{ + {By: entity.IssuePrimaryName, Direction: entity.OrderDirectionAsc}, + {By: entity.IssueMatchTargetRemediationDate, Direction: entity.OrderDirectionAsc}, + } + im, err := db.GetIssueMatches(&filter, order) + Expect(err).To(BeNil()) + Expect(im).To(HaveLen(1)) + filterWithCursor := entity.IssueMatchFilter{ + PaginatedX: entity.PaginatedX{ + After: im[0].Cursor(), + }, + } + res, err := db.GetIssueMatches(&filterWithCursor, order) + Expect(err).To(BeNil()) + Expect(res[0].Id).To(BeEquivalentTo(13)) + Expect(res[1].Id).To(BeEquivalentTo(20)) + Expect(res[2].Id).To(BeEquivalentTo(24)) + Expect(res[3].Id).To(BeEquivalentTo(30)) + Expect(res[4].Id).To(BeEquivalentTo(5)) + }) + }) +}) diff --git a/internal/database/mariadb/order.go b/internal/database/mariadb/order.go new file mode 100644 index 00000000..973eb8e3 --- /dev/null +++ b/internal/database/mariadb/order.go @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package mariadb + +import ( + "fmt" + + "github.com/cloudoperators/heureka/internal/entity" +) + +func ColumnName(f entity.OrderByField) string { + switch f { + case entity.ComponentInstanceCcrn: + return "componentinstance_ccrn" + case entity.IssuePrimaryName: + return "issue_primary_name" + case entity.IssueMatchId: + return "issuematch_id" + case entity.IssueMatchRating: + return "issuematch_rating" + case entity.IssueMatchTargetRemediationDate: + return "issuematch_target_remediation_date" + case entity.SupportGroupName: + return "supportgroup_name" + default: + return "" + } +} + +func OrderDirectionStr(dir entity.OrderDirection) string { + switch dir { + case entity.OrderDirectionAsc: + return "ASC" + case entity.OrderDirectionDesc: + return "DESC" + default: + return "" + } +} + +func CreateOrderString(order []entity.Order) string { + orderStr := "" + for i, o := range order { + if i > 0 { + orderStr = fmt.Sprintf("%s, %s %s", orderStr, ColumnName(o.By), OrderDirectionStr(o.Direction)) + } else { + orderStr = fmt.Sprintf("%s %s %s", orderStr, ColumnName(o.By), OrderDirectionStr(o.Direction)) + } + } + return orderStr +} diff --git a/internal/database/mariadb/testdata/issue_match_cursor/component_instance.json b/internal/database/mariadb/testdata/issue_match_cursor/component_instance.json new file mode 100644 index 00000000..7fdd279d --- /dev/null +++ b/internal/database/mariadb/testdata/issue_match_cursor/component_instance.json @@ -0,0 +1,122 @@ +[ + { + "ccrn": "ccrn: spec=test, kind=pod, id=1", + "count": 2, + "component_version_id": 1, + "service_id": 1 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=2", + "count": 1, + "component_version_id": 1, + "service_id": 1 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=3", + "count": 3, + "component_version_id": 2, + "service_id": 2 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=4", + "count": 1, + "component_version_id": 2, + "service_id": 2 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=5", + "count": 2, + "component_version_id": 3, + "service_id": 3 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=6", + "count": 1, + "component_version_id": 3, + "service_id": 3 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=7", + "count": 4, + "component_version_id": 4, + "service_id": 4 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=8", + "count": 2, + "component_version_id": 4, + "service_id": 4 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=9", + "count": 6, + "component_version_id": 5, + "service_id": 5 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=10", + "count": 2, + "component_version_id": 5, + "service_id": 5 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=11", + "count": 3, + "component_version_id": 6, + "service_id": 6 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=12", + "count": 1, + "component_version_id": 6, + "service_id": 6 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=13", + "count": 4, + "component_version_id": 7, + "service_id": 7 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=14", + "count": 2, + "component_version_id": 7, + "service_id": 7 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=15", + "count": 3, + "component_version_id": 8, + "service_id": 8 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=16", + "count": 1, + "component_version_id": 8, + "service_id": 8 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=17", + "count": 5, + "component_version_id": 9, + "service_id": 9 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=18", + "count": 2, + "component_version_id": 9, + "service_id": 9 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=19", + "count": 4, + "component_version_id": 10, + "service_id": 10 + }, + { + "ccrn": "ccrn: spec=test, kind=pod, id=20", + "count": 2, + "component_version_id": 10, + "service_id": 10 + } +] diff --git a/internal/database/mariadb/testdata/issue_match_cursor/issue.json b/internal/database/mariadb/testdata/issue_match_cursor/issue.json new file mode 100644 index 00000000..63820de9 --- /dev/null +++ b/internal/database/mariadb/testdata/issue_match_cursor/issue.json @@ -0,0 +1,102 @@ +[ + { + "type": "vulnerability", + "primary_name": "CVE-2024-0001", + "description": "Authentication bypass vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0002", + "description": "Buffer overflow vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0003", + "description": "Command injection vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0004", + "description": "Cross-site request forgery" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0005", + "description": "Cross-site scripting vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0006", + "description": "Denial of service vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0007", + "description": "Directory traversal vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0008", + "description": "File inclusion vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0009", + "description": "Information disclosure vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0010", + "description": "Insecure direct object reference" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0011", + "description": "Memory leak vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0012", + "description": "Open redirect vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0013", + "description": "Path traversal vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0014", + "description": "Race condition vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0015", + "description": "Remote code execution vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0016", + "description": "Server-side request forgery" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0017", + "description": "SQL injection vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0018", + "description": "Timing attack vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0019", + "description": "XML external entity vulnerability" + }, + { + "type": "vulnerability", + "primary_name": "CVE-2024-0020", + "description": "Zero-day vulnerability" + } +] diff --git a/internal/database/mariadb/testdata/issue_match_cursor/issue_match.json b/internal/database/mariadb/testdata/issue_match_cursor/issue_match.json new file mode 100644 index 00000000..5512ec2e --- /dev/null +++ b/internal/database/mariadb/testdata/issue_match_cursor/issue_match.json @@ -0,0 +1,272 @@ +[ + { + "status": "Open", + "rating": "Critical", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "user_id": 1, + "component_instance_id": 1, + "issue_id": 1, + "target_remediation_date": "2024-02-01T00:00:00Z" + }, + { + "status": "Open", + "rating": "Critical", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "user_id": 2, + "component_instance_id": 2, + "issue_id": 2, + "target_remediation_date": "2024-02-05T00:00:00Z" + }, + { + "status": "Open", + "rating": "Critical", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "user_id": 3, + "component_instance_id": 3, + "issue_id": 3, + "target_remediation_date": "2024-02-10T00:00:00Z" + }, + { + "status": "Open", + "rating": "Critical", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "user_id": 4, + "component_instance_id": 4, + "issue_id": 4, + "target_remediation_date": "2024-02-15T00:00:00Z" + }, + { + "status": "Open", + "rating": "Critical", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "user_id": 5, + "component_instance_id": 5, + "issue_id": 5, + "target_remediation_date": "2024-02-20T00:00:00Z" + }, + { + "status": "Open", + "rating": "Critical", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "user_id": 6, + "component_instance_id": 6, + "issue_id": 6, + "target_remediation_date": "2024-02-25T00:00:00Z" + }, + { + "status": "Open", + "rating": "High", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + "user_id": 7, + "component_instance_id": 7, + "issue_id": 7, + "target_remediation_date": "2024-03-01T00:00:00Z" + }, + { + "status": "Open", + "rating": "High", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + "user_id": 8, + "component_instance_id": 8, + "issue_id": 8, + "target_remediation_date": "2024-03-05T00:00:00Z" + }, + { + "status": "Open", + "rating": "High", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + "user_id": 9, + "component_instance_id": 9, + "issue_id": 9, + "target_remediation_date": "2024-03-10T00:00:00Z" + }, + { + "status": "Open", + "rating": "High", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + "user_id": 10, + "component_instance_id": 10, + "issue_id": 4, + "target_remediation_date": "2024-03-15T00:00:00Z" + }, + { + "status": "Open", + "rating": "High", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + "user_id": 1, + "component_instance_id": 11, + "issue_id": 11, + "target_remediation_date": "2024-03-20T00:00:00Z" + }, + { + "status": "Open", + "rating": "High", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + "user_id": 2, + "component_instance_id": 12, + "issue_id": 12, + "target_remediation_date": "2024-03-25T00:00:00Z" + }, + { + "status": "Open", + "rating": "Medium", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", + "user_id": 3, + "component_instance_id": 13, + "issue_id": 4, + "target_remediation_date": "2024-03-26T00:00:00Z" + }, + { + "status": "Open", + "rating": "Medium", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", + "user_id": 4, + "component_instance_id": 14, + "issue_id": 14, + "target_remediation_date": "2024-03-27T00:00:00Z" + }, + { + "status": "Open", + "rating": "Medium", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", + "user_id": 5, + "component_instance_id": 15, + "issue_id": 15, + "target_remediation_date": "2024-03-28T00:00:00Z" + }, + { + "status": "Open", + "rating": "Medium", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", + "user_id": 6, + "component_instance_id": 16, + "issue_id": 16, + "target_remediation_date": "2024-03-29T00:00:00Z" + }, + { + "status": "Open", + "rating": "Medium", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", + "user_id": 7, + "component_instance_id": 17, + "issue_id": 17, + "target_remediation_date": "2024-03-30T00:00:00Z" + }, + { + "status": "Open", + "rating": "Medium", + "vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", + "user_id": 8, + "component_instance_id": 18, + "issue_id": 18, + "target_remediation_date": "2024-03-31T00:00:00Z" + }, + { + "status": "Open", + "rating": "Low", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N", + "user_id": 9, + "component_instance_id": 19, + "issue_id": 19, + "target_remediation_date": "2024-04-01T00:00:00Z" + }, + { + "status": "Open", + "rating": "Low", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N", + "user_id": 10, + "component_instance_id": 20, + "issue_id": 4, + "target_remediation_date": "2024-04-02T00:00:00Z" + }, + { + "status": "Open", + "rating": "Low", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N", + "user_id": 1, + "component_instance_id": 1, + "issue_id": 1, + "target_remediation_date": "2024-04-03T00:00:00Z" + }, + { + "status": "Open", + "rating": "Low", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N", + "user_id": 2, + "component_instance_id": 2, + "issue_id": 2, + "target_remediation_date": "2024-04-04T00:00:00Z" + }, + { + "status": "Open", + "rating": "Low", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N", + "user_id": 3, + "component_instance_id": 3, + "issue_id": 3, + "target_remediation_date": "2024-04-05T00:00:00Z" + }, + { + "status": "Open", + "rating": "Low", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N", + "user_id": 4, + "component_instance_id": 4, + "issue_id": 4, + "target_remediation_date": "2024-04-06T00:00:00Z" + }, + { + "status": "Open", + "rating": "None", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", + "user_id": 5, + "component_instance_id": 5, + "issue_id": 5, + "target_remediation_date": "2024-04-07T00:00:00Z" + }, + { + "status": "Open", + "rating": "None", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", + "user_id": 6, + "component_instance_id": 6, + "issue_id": 6, + "target_remediation_date": "2024-04-08T00:00:00Z" + }, + { + "status": "Open", + "rating": "None", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", + "user_id": 7, + "component_instance_id": 7, + "issue_id": 7, + "target_remediation_date": "2024-04-09T00:00:00Z" + }, + { + "status": "Open", + "rating": "None", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", + "user_id": 8, + "component_instance_id": 8, + "issue_id": 8, + "target_remediation_date": "2024-04-10T00:00:00Z" + }, + { + "status": "Open", + "rating": "None", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", + "user_id": 9, + "component_instance_id": 9, + "issue_id": 9, + "target_remediation_date": "2024-04-11T00:00:00Z" + }, + { + "status": "Open", + "rating": "None", + "vector": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", + "user_id": 10, + "component_instance_id": 10, + "issue_id": 4, + "target_remediation_date": "2024-04-12T00:00:00Z" + } +] diff --git a/internal/database/mariadb/user_test.go b/internal/database/mariadb/user_test.go index 9e80c14f..6bfd11ab 100644 --- a/internal/database/mariadb/user_test.go +++ b/internal/database/mariadb/user_test.go @@ -8,7 +8,7 @@ import ( "github.com/cloudoperators/heureka/internal/database/mariadb" "github.com/cloudoperators/heureka/internal/database/mariadb/test" - "github.com/cloudoperators/heureka/internal/e2e/common" + e2e_common "github.com/cloudoperators/heureka/internal/e2e/common" "github.com/cloudoperators/heureka/internal/entity" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" diff --git a/internal/entity/order.go b/internal/entity/order.go index 4a7efe41..ad70b5f0 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -3,12 +3,10 @@ package entity -import "fmt" - -type DbColumnName int +type OrderByField int const ( - ComponentInstanceCcrn DbColumnName = iota + ComponentInstanceCcrn OrderByField = iota IssuePrimaryName @@ -19,19 +17,6 @@ const ( SupportGroupName ) -var DbColumnNameMap = map[DbColumnName]string{ - ComponentInstanceCcrn: "componentinstance_ccrn", - IssuePrimaryName: "issue_primary_name", - IssueMatchId: "issuematch_id", - IssueMatchRating: "issuematch_rating", - IssueMatchTargetRemediationDate: "issuematch_target_remediation_date", - SupportGroupName: "supportgroup_name", -} - -func (d DbColumnName) String() string { - return DbColumnNameMap[d] -} - type OrderDirection int const ( @@ -39,36 +24,7 @@ const ( OrderDirectionDesc ) -var OrderDirectionMap = map[OrderDirection]string{ - OrderDirectionAsc: "ASC", - OrderDirectionDesc: "DESC", -} - -func (o OrderDirection) String() string { - return OrderDirectionMap[o] -} - type Order struct { - By DbColumnName + By OrderByField Direction OrderDirection } - -func CreateOrderMap(order []Order) map[DbColumnName]OrderDirection { - m := map[DbColumnName]OrderDirection{} - for _, o := range order { - m[o.By] = o.Direction - } - return m -} - -func CreateOrderString(order []Order) string { - orderStr := "" - for i, o := range order { - if i > 0 { - orderStr = fmt.Sprintf("%s, %s %s", orderStr, o.By, o.Direction) - } else { - orderStr = fmt.Sprintf("%s %s %s", orderStr, o.By, o.Direction) - } - } - return orderStr -}