Skip to content

Commit 580aa5f

Browse files
feat!: change record filter behaviour (#23)
* feat(search)!: change filter behaviour * feat!: change filter behaviour when fetching records
1 parent 6eff531 commit 580aa5f

14 files changed

+170
-162
lines changed

api/handlers/search_handler.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,6 @@ func newCachingTypeRepo(repo models.TypeRepository) *cachingTypeRepo {
135135
}
136136
}
137137

138-
func filterConfigFromValues(values url.Values) map[string][]string {
139-
var filter = make(map[string][]string)
140-
for key, fields := range values {
141-
// filters are of form "filter.{field}", apart from "filter.type", which is used
142-
// for building the type whitelist.
143-
if !strings.HasPrefix(key, filterPrefix) || strings.EqualFold(key, whiteListQueryParamKey) {
144-
continue
145-
}
146-
filterKey := strings.TrimPrefix(key, filterPrefix)
147-
filter[filterKey] = fields
148-
}
149-
return filter
150-
}
151-
152138
func parseTypeWhiteList(values url.Values) (types []string) {
153139
for _, typ := range values[whiteListQueryParamKey] {
154140
typList := strings.Split(typ, ",")

api/handlers/search_handler_test.go

Lines changed: 32 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@ import (
99
"net/http/httptest"
1010
"net/url"
1111
"reflect"
12-
"strconv"
1312
"testing"
1413

1514
"github.com/odpf/columbus/api/handlers"
1615
"github.com/odpf/columbus/lib/mock"
1716
"github.com/odpf/columbus/models"
1817

18+
"github.com/stretchr/testify/assert"
1919
testifyMock "github.com/stretchr/testify/mock"
20+
"github.com/stretchr/testify/require"
2021
)
2122

2223
func TestSearchHandler(t *testing.T) {
2324
ctx := context.Background()
2425
// todo: pass testCase to ValidateResponse
2526
type testCase struct {
2627
Title string
27-
SearchText string
2828
ExpectStatus int
29-
RequestParams map[string][]string
29+
Querystring string
3030
InitRepo func(testCase, *mock.TypeRepository)
3131
InitSearcher func(testCase, *mock.RecordSearcher)
3232
ValidateResponse func(testCase, io.Reader) error
@@ -56,11 +56,11 @@ func TestSearchHandler(t *testing.T) {
5656
Title: "should return HTTP 400 if 'text' parameter is empty or missing",
5757
ExpectStatus: http.StatusBadRequest,
5858
ValidateResponse: func(tc testCase, body io.Reader) error { return nil },
59-
SearchText: "",
59+
Querystring: "",
6060
},
6161
{
62-
Title: "should report HTTP 500 if record searcher fails",
63-
SearchText: "test",
62+
Title: "should report HTTP 500 if record searcher fails",
63+
Querystring: "text=test",
6464
InitSearcher: func(tc testCase, searcher *mock.RecordSearcher) {
6565
err := fmt.Errorf("service unavailable")
6666
searcher.On("Search", ctx, testifyMock.AnythingOfType("models.SearchConfig")).
@@ -69,8 +69,8 @@ func TestSearchHandler(t *testing.T) {
6969
ExpectStatus: http.StatusInternalServerError,
7070
},
7171
{
72-
Title: "should return an error if looking up an type detail fails",
73-
SearchText: "test",
72+
Title: "should return an error if looking up an type detail fails",
73+
Querystring: "text=test",
7474
InitSearcher: func(tc testCase, searcher *mock.RecordSearcher) {
7575
results := []models.SearchResult{
7676
{
@@ -88,55 +88,31 @@ func TestSearchHandler(t *testing.T) {
8888
ExpectStatus: http.StatusInternalServerError,
8989
},
9090
{
91-
Title: "should pass filter to search config format",
92-
SearchText: "resource",
93-
RequestParams: map[string][]string{
94-
// "landscape" is not a valid filter key. All filters
95-
// begin with the "filter." prefix. Adding this here is just a little
96-
// extra check to make sure that the handler correctly parses the filters.
97-
"landscape": {"id", "vn"},
98-
"filter.landscape": {"th"},
99-
"filter.type": {"dagger"},
100-
},
101-
InitRepo: withTypes(testdata.Type),
91+
Title: "should pass filter to search config format",
92+
Querystring: "text=resource&landscape=id,vn&filter.data.landscape=th&filter.type=topic&filter.service=kafka,rabbitmq",
10293
InitSearcher: func(tc testCase, searcher *mock.RecordSearcher) {
10394
cfg := models.SearchConfig{
104-
Text: tc.SearchText,
105-
TypeWhiteList: tc.RequestParams["filter.type"],
95+
Text: "resource",
96+
TypeWhiteList: []string{"topic"},
10697
Filters: map[string][]string{
107-
"landscape": tc.RequestParams["filter.landscape"],
98+
"service": {"kafka", "rabbitmq"},
99+
"data.landscape": {"th"},
108100
},
109101
}
110102

111-
results := []models.SearchResult{
112-
{
113-
TypeName: testdata.Type.Name,
114-
Record: models.Record{
115-
Urn: "test-1",
116-
Name: "test 1",
117-
Service: "test-service",
118-
Data: map[string]interface{}{
119-
"id": "test-1",
120-
"title": "test 1",
121-
"landscape": "th",
122-
"entity": "odpf",
123-
},
124-
},
125-
},
126-
}
127-
searcher.On("Search", ctx, cfg).Return(results, nil)
103+
searcher.On("Search", ctx, cfg).Return([]models.SearchResult{}, nil)
128104
return
129105
},
130106
ValidateResponse: func(tc testCase, body io.Reader) error {
131107
return nil
132108
},
133109
},
134110
{
135-
Title: "should return the matched documents",
136-
SearchText: "test",
111+
Title: "should return the matched documents",
112+
Querystring: "text=test",
137113
InitSearcher: func(tc testCase, searcher *mock.RecordSearcher) {
138114
cfg := models.SearchConfig{
139-
Text: tc.SearchText,
115+
Text: "test",
140116
Filters: make(map[string][]string),
141117
}
142118
response := []models.SearchResult{
@@ -190,17 +166,13 @@ func TestSearchHandler(t *testing.T) {
190166
},
191167
},
192168
{
193-
Title: "should return the requested number of records",
194-
RequestParams: map[string][]string{
195-
"size": {"15"},
196-
},
197-
SearchText: "resource",
198-
InitRepo: withTypes(testdata.Type),
169+
Title: "should return the requested number of records",
170+
Querystring: "text=resource&size=10",
171+
InitRepo: withTypes(testdata.Type),
199172
InitSearcher: func(tc testCase, searcher *mock.RecordSearcher) {
200-
maxResults, _ := strconv.Atoi(tc.RequestParams["size"][0])
201173
cfg := models.SearchConfig{
202-
Text: tc.SearchText,
203-
MaxResults: maxResults,
174+
Text: "resource",
175+
MaxResults: 10,
204176
Filters: make(map[string][]string),
205177
}
206178

@@ -239,10 +211,11 @@ func TestSearchHandler(t *testing.T) {
239211
if err != nil {
240212
return fmt.Errorf("error reading response body: %v", err)
241213
}
242-
expectedResults, _ := strconv.Atoi(tc.RequestParams["size"][0])
243-
actualResults := len(payload)
244-
if expectedResults != actualResults {
245-
return fmt.Errorf("expected search request to return %d results, returned %d results instead", expectedResults, actualResults)
214+
215+
expectedSize := 10
216+
actualSize := len(payload)
217+
if expectedSize != actualSize {
218+
return fmt.Errorf("expected search request to return %d results, returned %d results instead", expectedSize, actualSize)
246219
}
247220
return nil
248221
},
@@ -263,15 +236,9 @@ func TestSearchHandler(t *testing.T) {
263236
defer recordSearcher.AssertExpectations(t)
264237
defer typeRepo.AssertExpectations(t)
265238

266-
params := url.Values{}
267-
params.Add("text", testCase.SearchText)
268-
if testCase.RequestParams != nil {
269-
for key, values := range testCase.RequestParams {
270-
for _, value := range values {
271-
params.Add(key, value)
272-
}
273-
}
274-
}
239+
params, err := url.ParseQuery(testCase.Querystring)
240+
require.NoError(t, err)
241+
275242
requestURL := "/?" + params.Encode()
276243
rr := httptest.NewRequest(http.MethodGet, requestURL, nil)
277244
rw := httptest.NewRecorder()
@@ -283,10 +250,7 @@ func TestSearchHandler(t *testing.T) {
283250
if expectStatus == 0 {
284251
expectStatus = http.StatusOK
285252
}
286-
if rw.Code != expectStatus {
287-
t.Errorf("expected handler to return http status %d, was %d instead", expectStatus, rw.Code)
288-
return
289-
}
253+
assert.Equal(t, expectStatus, rw.Code)
290254
if testCase.ValidateResponse != nil {
291255
if err := testCase.ValidateResponse(testCase, rw.Body); err != nil {
292256
t.Errorf("error validating handler response: %v", err)

api/handlers/type_handler_test.go

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -780,36 +780,39 @@ func TestTypeHandler(t *testing.T) {
780780
{
781781
Description: "should return an http 200 irrespective of environment value",
782782
TypeName: "dagger",
783-
QueryStrings: "filter.environment=nonexisting",
783+
QueryStrings: "filter.data.environment=nonexisting",
784784
ExpectStatus: http.StatusOK,
785785
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
786786
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
787787
rr := new(mock.RecordRepository)
788-
rr.On("GetAll", ctx, map[string][]string{"environment": {"nonexisting"}}).Return(daggerRecords, nil)
788+
rr.On("GetAll", ctx, map[string][]string{"data.environment": {"nonexisting"}}).Return(daggerRecords, nil)
789789
rrf.On("For", daggerType).Return(rr, nil)
790790
},
791791
},
792792
{
793-
Description: "should return an http 200 even if the environment is not provided",
793+
Description: "should create filter from querystring",
794794
TypeName: "dagger",
795-
QueryStrings: "",
795+
QueryStrings: "filter.service=kafka,rabbitmq&filter.data.company=appel",
796796
ExpectStatus: http.StatusOK,
797797
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
798798
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
799799
rr := new(mock.RecordRepository)
800-
rr.On("GetAll", ctx, map[string][]string{}).Return(daggerRecords, nil)
800+
rr.On("GetAll", ctx, map[string][]string{
801+
"service": {"kafka", "rabbitmq"},
802+
"data.company": {"appel"},
803+
}).Return(daggerRecords, nil)
801804
rrf.On("For", daggerType).Return(rr, nil)
802805
},
803806
},
804807
{
805808
Description: "should return all records for an type",
806809
TypeName: "dagger",
807-
QueryStrings: "filter.environment=test",
810+
QueryStrings: "filter.data.environment=test",
808811
ExpectStatus: http.StatusOK,
809812
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
810813
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
811814
rr := new(mock.RecordRepository)
812-
rr.On("GetAll", ctx, map[string][]string{"environment": {"test"}}).Return(daggerRecords, nil)
815+
rr.On("GetAll", ctx, map[string][]string{"data.environment": {"test"}}).Return(daggerRecords, nil)
813816
rrf.On("For", daggerType).Return(rr, nil)
814817
},
815818
PostCheck: func(tc *testCase, resp *http.Response) error {
@@ -828,12 +831,12 @@ func TestTypeHandler(t *testing.T) {
828831
{
829832
Description: "should return the subset of fields specified via select parameter",
830833
TypeName: "dagger",
831-
QueryStrings: "filter.environment=test&select=" + url.QueryEscape("urn,owner"),
834+
QueryStrings: "filter.data.environment=test&select=" + url.QueryEscape("urn,owner"),
832835
ExpectStatus: http.StatusOK,
833836
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
834837
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
835838
rr := new(mock.RecordRepository)
836-
rr.On("GetAll", ctx, map[string][]string{"environment": {"test"}}).Return(daggerRecords, nil)
839+
rr.On("GetAll", ctx, map[string][]string{"data.environment": {"test"}}).Return(daggerRecords, nil)
837840
rrf.On("For", daggerType).Return(rr, nil)
838841
},
839842
PostCheck: func(tc *testCase, resp *http.Response) error {
@@ -867,38 +870,10 @@ func TestTypeHandler(t *testing.T) {
867870
return nil
868871
},
869872
},
870-
{
871-
Description: "should support landscape and entity filters",
872-
TypeName: "dagger",
873-
QueryStrings: "filter.environment=test&filter.landscape=id&filter.entity=odpf",
874-
ExpectStatus: http.StatusOK,
875-
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
876-
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
877-
rr := new(mock.RecordRepository)
878-
filters := map[string][]string{
879-
"landscape": {"id"},
880-
"entity": {"odpf"},
881-
"environment": {"test"},
882-
}
883-
rr.On("GetAll", ctx, filters).Return(daggerRecords, nil)
884-
rrf.On("For", daggerType).Return(rr, nil)
885-
},
886-
PostCheck: func(tc *testCase, resp *http.Response) error {
887-
var response []models.Record
888-
err := json.NewDecoder(resp.Body).Decode(&response)
889-
if err != nil {
890-
return fmt.Errorf("error parsing response payload: %v", err)
891-
}
892-
if reflect.DeepEqual(response, daggerRecords) == false {
893-
return fmt.Errorf("expected handler to return %v, returned %v instead", daggerRecords, response)
894-
}
895-
return nil
896-
},
897-
},
898873
{
899874
Description: "(internal) should return http 500 if the handler fails to construct record repository",
900875
TypeName: "dagger",
901-
QueryStrings: "filter.environment=test",
876+
QueryStrings: "filter.data.environment=test",
902877
ExpectStatus: http.StatusInternalServerError,
903878
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
904879
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
@@ -910,13 +885,13 @@ func TestTypeHandler(t *testing.T) {
910885
{
911886
Description: "(internal) should return an http 500 if calling recordRepository.GetAll fails",
912887
TypeName: "dagger",
913-
QueryStrings: "filter.environment=test",
888+
QueryStrings: "filter.data.environment=test",
914889
ExpectStatus: http.StatusInternalServerError,
915890
Setup: func(tc *testCase, er *mock.TypeRepository, rrf *mock.RecordRepositoryFactory) {
916891
er.On("GetByName", ctx, daggerType.Name).Return(daggerType, nil)
917892
rr := new(mock.RecordRepository)
918893
err := fmt.Errorf("temporarily unavailable")
919-
rr.On("GetAll", ctx, map[string][]string{"environment": {"test"}}).Return([]models.Record{}, err)
894+
rr.On("GetAll", ctx, map[string][]string{"data.environment": {"test"}}).Return([]models.Record{}, err)
920895
rrf.On("For", daggerType).Return(rr, nil)
921896
},
922897
},

api/handlers/utils.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package handlers
2+
3+
import (
4+
"net/url"
5+
"strings"
6+
)
7+
8+
func filterConfigFromValues(querystring url.Values) map[string][]string {
9+
var filter = make(map[string][]string)
10+
for key, values := range querystring {
11+
// filters are of form "filter.{field}", apart from "filter.type", which is used
12+
// for building the type whitelist.
13+
if !strings.HasPrefix(key, filterPrefix) || strings.EqualFold(key, whiteListQueryParamKey) {
14+
continue
15+
}
16+
17+
var filterValues []string
18+
for _, value := range values {
19+
for _, v := range strings.Split(value, ",") {
20+
filterValues = append(filterValues, v)
21+
}
22+
}
23+
24+
filterKey := strings.TrimPrefix(key, filterPrefix)
25+
filter[filterKey] = filterValues
26+
}
27+
return filter
28+
}

store/record_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func (repo *RecordRepository) termsQuery(filters models.RecordFilter) (io.Reader
217217
values = append(values, val)
218218
}
219219

220-
key := fmt.Sprintf("data.%s.keyword", key)
220+
key := fmt.Sprintf("%s.keyword", key)
221221
termQueries = append(termQueries, elastic.NewTermsQuery(key, values...))
222222
}
223223
boolQuery := elastic.NewBoolQuery().Must(termQueries...)

0 commit comments

Comments
 (0)