diff --git a/pkg/ffapi/restfilter_json.go b/pkg/ffapi/restfilter_json.go index 4b98d65..b8d2661 100644 --- a/pkg/ffapi/restfilter_json.go +++ b/pkg/ffapi/restfilter_json.go @@ -67,7 +67,8 @@ type FilterJSONKeyValues struct { } type FilterJSON struct { - Or []*FilterJSON `ffstruct:"FilterJSON" json:"or,omitempty"` + Or []*FilterJSON `ffstruct:"FilterJSON" json:"or,omitempty"` + And []*FilterJSON `ffstruct:"FilterJSON" json:"and,omitempty"` FilterJSONOps } @@ -435,6 +436,13 @@ func (jf *FilterJSON) BuildAndFilter(ctx context.Context, fb FilterBuilder, opti andFilter = andFilter.Condition(fb.In(field, rv.resolveMany(field, e.Values))) } } + for _, child := range jf.And { + subFilter, err := child.BuildSubFilter(ctx, fb, options...) + if err != nil { + return nil, err + } + andFilter.Condition(subFilter) + } if len(jf.Or) > 0 { childFilter := fb.Or() for _, child := range jf.Or { diff --git a/pkg/ffapi/restfilter_json_builder.go b/pkg/ffapi/restfilter_json_builder.go index c0ca449..b081af8 100644 --- a/pkg/ffapi/restfilter_json_builder.go +++ b/pkg/ffapi/restfilter_json_builder.go @@ -71,6 +71,9 @@ type QueryBuilder interface { // Or creates an OR condition between multiple queries Or(...QueryBuilder) QueryBuilder + // And creates an AND condition between multiple queries + And(...QueryBuilder) QueryBuilder + // Query returns the query Query() *QueryJSON } @@ -88,6 +91,10 @@ func NewQueryBuilder() QueryBuilder { return qj.ToBuilder() } +func QB() QueryBuilder { + return NewQueryBuilder() +} + // Limit sets the limit of the query func (qb *queryBuilderImpl) Limit(limit uint64) QueryBuilder { qb.rootQuery.Limit = &limit @@ -206,6 +213,14 @@ func (qb *queryBuilderImpl) Or(q ...QueryBuilder) QueryBuilder { return qb } +// And creates an AND condition between multiple queries +func (qb *queryBuilderImpl) And(q ...QueryBuilder) QueryBuilder { + for _, child := range q { + qb.statements.And = append(qb.statements.And, child.(*queryBuilderImpl).statements) + } + return qb +} + // Query returns the query func (qb *queryBuilderImpl) Query() *QueryJSON { return qb.rootQuery diff --git a/pkg/ffapi/restfilter_json_builder_test.go b/pkg/ffapi/restfilter_json_builder_test.go index a81d27c..be9c72c 100644 --- a/pkg/ffapi/restfilter_json_builder_test.go +++ b/pkg/ffapi/restfilter_json_builder_test.go @@ -149,6 +149,49 @@ func TestQuery_StringOr(t *testing.T) { assert.JSONEq(t, expectedQuery, string(jsonQuery)) } +func TestQuery_AndNestedOr(t *testing.T) { + expectedQuery := `{ + "and": [ + { + "eq": [ + { "field": "field1", "value": "aaa" } + ] + }, + { + "or": [ + { + "eq": [ + { "field": "field2", "value": "bbb" } + ] + }, + { + "eq": [ + { "field": "field2", "value": "ccc" } + ] + } + ] + } + ] + }` + + query := QB(). + And( + QB().Equal("field1", "aaa"), + QB(). + Or( + QB().Equal("field2", "bbb"), + ). + Or( + QB().Equal("field2", "ccc"), + ), + ). + Query() + + jsonQuery, err := json.Marshal(query) + assert.NoError(t, err) + assert.JSONEq(t, expectedQuery, string(jsonQuery)) +} + func assertQueryEqual(t *testing.T, jsonMap map[string]interface{}, jq *QueryJSON) { jmb, err := json.Marshal(jsonMap) assert.NoError(t, err) diff --git a/pkg/ffapi/restfilter_json_test.go b/pkg/ffapi/restfilter_json_test.go index e5982ff..c13097f 100644 --- a/pkg/ffapi/restfilter_json_test.go +++ b/pkg/ffapi/restfilter_json_test.go @@ -139,6 +139,52 @@ func TestBuildQuerySingleNestedOr(t *testing.T) { assert.Equal(t, "tag == 'a'", fi.String()) } +func TestBuildQueryAndWithNestedOr(t *testing.T) { + + var qf QueryJSON + err := json.Unmarshal([]byte(`{ + "and": [ + { + "or": [ + { + "equal": [ + { + "field": "tag", + "value": "a" + } + ] + }, + { + "equal": [ + { + "field": "tag", + "value": "b" + } + ] + } + ] + }, + { + "equal": [ + { + "field": "cid", + "value": "12345" + } + ] + } + ] + }`), &qf) + assert.NoError(t, err) + + filter, err := qf.BuildFilter(context.Background(), TestQueryFactory) + assert.NoError(t, err) + + fi, err := filter.Finalize() + assert.NoError(t, err) + + assert.Equal(t, "( ( tag == 'a' ) || ( tag == 'b' ) ) && ( cid == '12345' )", fi.String()) +} + func TestBuildQuerySkipFieldValidation(t *testing.T) { var jf *FilterJSON @@ -706,3 +752,28 @@ func TestBuildQueryJSONContainsShortNames(t *testing.T) { assert.Equal(t, "( sequence <= 12345 ) && ( sequence >> 12345 )", fi.String()) } + +func TestBuildQueryAndFail(t *testing.T) { + + var qf QueryJSON + err := json.Unmarshal([]byte(`{ + "and": [ + { + "or": [ + { + "equal": [ + { + "field": "color", + "value": "b" + } + ] + } + ] + } + ] + }`), &qf) + assert.NoError(t, err) + + _, err = qf.BuildFilter(context.Background(), TestQueryFactory) + assert.Regexp(t, "FF00142.*color", err) +} diff --git a/pkg/i18n/en_base_field_descriptions.go b/pkg/i18n/en_base_field_descriptions.go index 89b5fec..639d9dc 100644 --- a/pkg/i18n/en_base_field_descriptions.go +++ b/pkg/i18n/en_base_field_descriptions.go @@ -43,7 +43,8 @@ var ( FilterJSONSkip = ffm("FilterJSON.skip", "Number of results to skip before returning entries, for skip+limit based pagination") FilterJSONSort = ffm("FilterJSON.sort", "Array of fields to sort by. A '-' prefix on a field requests that field is sorted in descending order") FilterJSONCount = ffm("FilterJSON.count", "If true, the total number of entries that could be returned from the database will be calculated and returned as a 'total' (has a performance cost)") - FilterJSONOr = ffm("FilterJSON.or", "Array of sub-queries where any sub-query can match to return results (OR combined). Note that within each sub-query all filters must match (AND combined)") + FilterJSONOr = ffm("FilterJSON.or", "Array of sub-queries") // Note due to complex issue in swagger generator AND/OR need to be identical + FilterJSONAnd = ffm("FilterJSON.and", "Array of sub-queries") // ^^^ the `description` field is getting pushed to the sub-schema definition, and is non-deterministic FilterJSONFields = ffm("FilterJSON.fields", "Fields to return in the response") EventStreamBatchSize = ffm("eventstream.batchSize", "Maximum number of events to deliver in each batch")