Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mr2011/issue 310/issue match ordering #452

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
85 changes: 85 additions & 0 deletions docs/cursor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Cursor

The cursor is a list of `Field`, where `Field` is defined as:

```go
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

```go
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:

```go
cursor, _ := entity.EncodeCursor(entity.WithIssueMatch(order, im))
```

A order list can be passed to override the default ordering.

## Cursor Query
MR2011 marked this conversation as resolved.
Show resolved Hide resolved

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:

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)
```
105 changes: 105 additions & 0 deletions docs/ordering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Order

## API

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
}
```

The `By` fields define the allowed order options:

```graphql
enum IssueMatchOrderByField {
primaryName
targetRemediationDate
componentInstanceCcrn
}
```

The `OrderDirections` are defined in the `common.graphqls`:
```graphql
enum OrderDirection {
asc
desc
}
```

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 {
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

```go
type Order struct {
By DbColumnName
Direction OrderDirection
}
```

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",
IssueMatchId: "issuematch_id",
IssueMatchRating: "issuematch_rating",
IssueMatchTargetRemediationDate: "issuematch_target_remediation_date",
SupportGroupName: "supportgroup_name",
}
```


## Database

The `GetIssueMatches()` function has an additional order argument:

```go
func (s *SqlDatabase) GetIssueMatches(filter *entity.IssueMatchFilter, order []entity.Order) ([]entity.IssueMatchResult, error) {
...
}
```

The order string is created by in `entity/order.go`:

```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
}
```


1 change: 1 addition & 0 deletions internal/api/graphql/graph/baseResolver/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
}
}
13 changes: 5 additions & 8 deletions internal/api/graphql/graph/baseResolver/issue_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,13 @@ 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,
"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
Expand Down Expand Up @@ -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()) }),
Expand All @@ -120,6 +114,9 @@ func IssueMatchBaseResolver(app app.Heureka, ctx context.Context, filter *model.
}

opt := GetListOptions(requestedFields)
for _, o := range orderBy {
opt.Order = append(opt.Order, o.ToOrderEntity())
}

issueMatches, err := app.ListIssueMatches(f, opt)

Expand Down
26 changes: 26 additions & 0 deletions internal/api/graphql/graph/model/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ var AllIssueMatchStatusValuesOrdered = []IssueMatchStatusValues{
IssueMatchStatusValuesMitigated,
}

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 {
if p == nil {
return nil
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/api/graphql/graph/resolver/evidence.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/api/graphql/graph/resolver/issue.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions internal/api/graphql/graph/resolver/query.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions internal/api/graphql/graph/schema/common.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ type Metadata {
updated_by: String
}

enum OrderDirection {
asc
desc
}

enum StateFilter {
Active,
Deleted
Expand Down
11 changes: 11 additions & 0 deletions internal/api/graphql/graph/schema/issue_match.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,14 @@ enum IssueMatchStatusValues {
false_positive
mitigated
}

input IssueMatchOrderBy {
by: IssueMatchOrderByField
direction: OrderDirection
}

enum IssueMatchOrderByField {
primaryName
targetRemediationDate
componentInstanceCcrn
}
2 changes: 1 addition & 1 deletion internal/api/graphql/graph/schema/query.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading