From 547d0b8293e78e12dd226c1c0bf0b0654453dba0 Mon Sep 17 00:00:00 2001 From: michalkrzyz Date: Mon, 12 Aug 2024 13:34:11 +0200 Subject: [PATCH] feat(filter): Implement wild card searching for issue #95 (#131) * feat(filter): Implement wild card searching for issue #95 - Add search filter for wildcard-like searching - Add compose makefile targets Signed-off-by: Michal Krzyz * Automatic application of license header --------- Signed-off-by: Michal Krzyz Co-authored-by: License Bot Co-authored-by: David Rochow --- Makefile | 29 +++++++-- docker-compose.yaml | 1 + .../api/graphql/graph/baseResolver/issue.go | 2 + internal/api/graphql/graph/generated.go | 9 ++- .../api/graphql/graph/model/models_gen.go | 1 + .../api/graphql/graph/schema/issue.graphqls | 4 +- internal/database/mariadb/database.go | 8 ++- internal/database/mariadb/issue.go | 9 ++- internal/database/mariadb/issue_test.go | 65 +++++++++++++++++++ internal/entity/issue.go | 1 + internal/mocks/mock_Database.go | 2 +- internal/mocks/mock_Heureka.go | 2 +- 12 files changed, 121 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 71e18184..6092166d 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ VERSION ?= $(shell git log -1 --pretty=format:"%H") OS := $(shell go env GOOS) ARCH := $(shell go env GOARCH) -.PHONY: all test doc gqlgen test-all test-e2e test-app test-db +.PHONY: all test doc gqlgen mockery test-all test-e2e test-app test-db fmt compose-prepare compose-up compose-down compose-restart compose-build # Source the .env file to use the env vars with make -include .env @@ -54,17 +54,34 @@ gqlgen: mockery: mockery +GINKGO := go run github.com/onsi/ginkgo/v2/ginkgo test-all: - ginkgo -r + $(GINKGO) -r test-e2e: - ginkgo internal/e2e + $(GINKGO) -r internal/e2e test-app: - ginkgo internal/app + $(GINKGO) -r internal/app test-db: - ginkgo internal/database/mariadb + $(GINKGO) -r internal/database/mariadb fmt: - go fmt ./... \ No newline at end of file + go fmt ./... + +DOCKER_COMPOSE := docker-compose -f docker-compose.yaml +DOCKER_COMPOSE_SERVICES := heureka-app heureka-db +compose-prepare: + sed 's/^SEED_MODE=false/SEED_MODE=true/g' .test.env > .env + +compose-up: + $(DOCKER_COMPOSE) up -d $(DOCKER_COMPOSE_SERVICES) + +compose-down: + $(DOCKER_COMPOSE) down $(DOCKER_COMPOSE_SERVICES) + +compose-restart: compose-down compose-up + +compose-build: + $(DOCKER_COMPOSE) build $(DOCKER_COMPOSE_SERVICES) diff --git a/docker-compose.yaml b/docker-compose.yaml index e72f2373..637c2c67 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,6 +33,7 @@ services: heureka-app: build: . + container_name: heureka-app profiles: - heureka environment: diff --git a/internal/api/graphql/graph/baseResolver/issue.go b/internal/api/graphql/graph/baseResolver/issue.go index 4073bb39..2e686596 100644 --- a/internal/api/graphql/graph/baseResolver/issue.go +++ b/internal/api/graphql/graph/baseResolver/issue.go @@ -105,6 +105,8 @@ func IssueBaseResolver(app app.Heureka, ctx context.Context, filter *model.Issue PrimaryName: filter.PrimaryName, Type: lo.Map(filter.IssueType, func(item *model.IssueTypes, _ int) *string { return pointer.String(item.String()) }), + Search: filter.Search, + IssueMatchStatus: nil, //@todo Implement IssueMatchDiscoveryDate: nil, //@todo Implement IssueMatchTargetRemediationDate: nil, //@todo Implement diff --git a/internal/api/graphql/graph/generated.go b/internal/api/graphql/graph/generated.go index bcfa65fe..c1bf4d99 100644 --- a/internal/api/graphql/graph/generated.go +++ b/internal/api/graphql/graph/generated.go @@ -23433,7 +23433,7 @@ func (ec *executionContext) unmarshalInputIssueFilter(ctx context.Context, obj i asMap[k] = v } - fieldsInOrder := [...]string{"affectedService", "primaryName", "issueMatchStatus", "issueType", "componentVersionId"} + fieldsInOrder := [...]string{"affectedService", "primaryName", "issueMatchStatus", "issueType", "componentVersionId", "search"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -23475,6 +23475,13 @@ func (ec *executionContext) unmarshalInputIssueFilter(ctx context.Context, obj i return it, err } it.ComponentVersionID = data + case "search": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("search")) + data, err := ec.unmarshalOString2ᚕᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Search = data } } diff --git a/internal/api/graphql/graph/model/models_gen.go b/internal/api/graphql/graph/model/models_gen.go index b9fc7d37..17012273 100644 --- a/internal/api/graphql/graph/model/models_gen.go +++ b/internal/api/graphql/graph/model/models_gen.go @@ -337,6 +337,7 @@ type IssueFilter struct { IssueMatchStatus []*IssueMatchStatusValues `json:"issueMatchStatus,omitempty"` IssueType []*IssueTypes `json:"issueType,omitempty"` ComponentVersionID []*string `json:"componentVersionId,omitempty"` + Search []*string `json:"search,omitempty"` } type IssueInput struct { diff --git a/internal/api/graphql/graph/schema/issue.graphqls b/internal/api/graphql/graph/schema/issue.graphqls index 5332baba..5236b9f8 100644 --- a/internal/api/graphql/graph/schema/issue.graphqls +++ b/internal/api/graphql/graph/schema/issue.graphqls @@ -61,6 +61,8 @@ input IssueFilter { componentVersionId: [String], + search: [String], + # leave away for MVP # cveDescription: [String] # fromAdvisory: ID, @@ -72,4 +74,4 @@ enum IssueTypes { Vulnerability, PolicyViolation, SecurityEvent -} \ No newline at end of file +} diff --git a/internal/database/mariadb/database.go b/internal/database/mariadb/database.go index 35383b92..6df0a77e 100644 --- a/internal/database/mariadb/database.go +++ b/internal/database/mariadb/database.go @@ -165,8 +165,14 @@ func buildFilterQuery[T any](filter []T, expr string, op string) string { } func buildQueryParameters[T any](params []interface{}, filter []T) []interface{} { + return buildQueryParametersCount(params, filter, 1) +} + +func buildQueryParametersCount[T any](params []interface{}, filter []T, count int) []interface{} { for _, item := range filter { - params = append(params, item) + for i := 0; i < count; i++ { + params = append(params, item) + } } return params } diff --git a/internal/database/mariadb/issue.go b/internal/database/mariadb/issue.go index 4bc45330..620bd546 100644 --- a/internal/database/mariadb/issue.go +++ b/internal/database/mariadb/issue.go @@ -12,6 +12,11 @@ import ( "github.wdf.sap.corp/cc/heureka/internal/entity" ) +const ( + wildCardFilterQuery = "IV.issuevariant_secondary_name LIKE Concat('%',?,'%') OR I.issue_primary_name LIKE Concat('%',?,'%')" + wildCardFilterParamCount = 2 +) + func (s *SqlDatabase) getIssueFilterString(filter *entity.IssueFilter) string { var fl []string fl = append(fl, buildFilterQuery(filter.ServiceName, "S.service_name = ?", OP_OR)) @@ -23,6 +28,7 @@ func (s *SqlDatabase) getIssueFilterString(filter *entity.IssueFilter) string { fl = append(fl, buildFilterQuery(filter.IssueVariantId, "IV.issuevariant_id = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.Type, "I.issue_type = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.PrimaryName, "I.issue_primary_name = ?", OP_OR)) + fl = append(fl, buildFilterQuery(filter.Search, wildCardFilterQuery, OP_OR)) fl = append(fl, "I.issue_deleted_at IS NULL") return combineFilterQueries(fl, OP_AND) @@ -55,7 +61,7 @@ func (s *SqlDatabase) getIssueJoins(filter *entity.IssueFilter, withAggregations `) } - if len(filter.IssueVariantId) > 0 { + if len(filter.IssueVariantId) > 0 || len(filter.Search) > 0 { joins = fmt.Sprintf("%s\n%s", joins, ` LEFT JOIN IssueVariant IV ON I.issue_id = IV.issuevariant_issue_id `) @@ -164,6 +170,7 @@ func (s *SqlDatabase) buildIssueStatement(baseQuery string, filter *entity.Issue filterParameters = buildQueryParameters(filterParameters, filter.IssueVariantId) filterParameters = buildQueryParameters(filterParameters, filter.Type) filterParameters = buildQueryParameters(filterParameters, filter.PrimaryName) + filterParameters = buildQueryParametersCount(filterParameters, filter.Search, wildCardFilterParamCount) if withCursor { filterParameters = append(filterParameters, cursor.Value) filterParameters = append(filterParameters, cursor.Limit) diff --git a/internal/database/mariadb/issue_test.go b/internal/database/mariadb/issue_test.go index e7ead789..e5def47d 100644 --- a/internal/database/mariadb/issue_test.go +++ b/internal/database/mariadb/issue_test.go @@ -326,6 +326,71 @@ var _ = Describe("Issue", Label("database", "Issue"), func() { Expect(entry.Type).To(BeEquivalentTo(issueType)) } }) + It("can filter issue PrimaryName using wild card search", func() { + row := seedCollection.IssueRows[rand.Intn(len(seedCollection.IssueRows))] + + const charactersToRemoveFromBeginning = 2 + const charactersToRemoveFromEnd = 2 + const minimalCharactersToKeep = 5 + + start := charactersToRemoveFromBeginning + end := len(row.PrimaryName.String) - charactersToRemoveFromEnd + + Expect(start+minimalCharactersToKeep < end).To(BeTrue()) + + searchStr := row.PrimaryName.String[start:end] + filter := &entity.IssueFilter{Search: []*string{&searchStr}} + + entries, err := db.GetIssues(filter) + + issueIds := []int64{} + for _, entry := range entries { + issueIds = append(issueIds, entry.Id) + } + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + + By("at least one element was discarded (filtered)", func() { + Expect(len(seedCollection.IssueRows) > len(issueIds)).To(BeTrue()) + }) + + By("returning the expected elements", func() { + Expect(issueIds).To(ContainElement(row.Id.Int64)) + }) + }) + It("can filter issue variant SecondaryName using wild card search", func() { + // select an issueVariant + issueVariantRow := seedCollection.IssueVariantRows[rand.Intn(len(seedCollection.IssueVariantRows))] + + const charactersToRemoveFromBeginning = 2 + const charactersToRemoveFromEnd = 2 + const minimalCharactersToKeep = 5 + + start := charactersToRemoveFromBeginning + end := len(issueVariantRow.SecondaryName.String) - charactersToRemoveFromEnd + + Expect(start+minimalCharactersToKeep < end).To(BeTrue()) + + searchStr := issueVariantRow.SecondaryName.String[start:end] + filter := &entity.IssueFilter{Search: []*string{&searchStr}} + + entries, err := db.GetIssues(filter) + + issueIds := []int64{} + for _, entry := range entries { + issueIds = append(issueIds, entry.Id) + } + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + + By("returning the expected elements", func() { + Expect(issueIds).To(ContainElement(issueVariantRow.IssueId.Int64)) + }) + }) }) Context("and using pagination", func() { DescribeTable("can correctly paginate", func(pageSize int) { diff --git a/internal/entity/issue.go b/internal/entity/issue.go index b29970a3..6b114872 100644 --- a/internal/entity/issue.go +++ b/internal/entity/issue.go @@ -59,6 +59,7 @@ type IssueFilter struct { IssueMatchId []*int64 `json:"issue_match_id"` ComponentVersionId []*int64 `json:"component_version_id"` IssueVariantId []*int64 `json:"issue_variant_id"` + Search []*string `json:"search"` IssueMatchStatus []*string `json:"issue_match_status"` IssueMatchDiscoveryDate *TimeFilter `json:"issue_match_discovery_date"` IssueMatchTargetRemediationDate *TimeFilter `json:"issue_match_target_remediation_date"` diff --git a/internal/mocks/mock_Database.go b/internal/mocks/mock_Database.go index 1ebb54ad..e6a280f7 100644 --- a/internal/mocks/mock_Database.go +++ b/internal/mocks/mock_Database.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors // SPDX-License-Identifier: Apache-2.0 -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package mocks diff --git a/internal/mocks/mock_Heureka.go b/internal/mocks/mock_Heureka.go index b149f09f..0fe2db3b 100644 --- a/internal/mocks/mock_Heureka.go +++ b/internal/mocks/mock_Heureka.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors // SPDX-License-Identifier: Apache-2.0 -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package mocks