Skip to content

Commit 6840b3f

Browse files
authored
Merge pull request #199 from hyperledger/top-level-and
Add option for explicit nested AND sub-queries
2 parents 300d3ce + 964b677 commit 6840b3f

File tree

5 files changed

+140
-2
lines changed

5 files changed

+140
-2
lines changed

pkg/ffapi/restfilter_json.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ type FilterJSONKeyValues struct {
6767
}
6868

6969
type FilterJSON struct {
70-
Or []*FilterJSON `ffstruct:"FilterJSON" json:"or,omitempty"`
70+
Or []*FilterJSON `ffstruct:"FilterJSON" json:"or,omitempty"`
71+
And []*FilterJSON `ffstruct:"FilterJSON" json:"and,omitempty"`
7172
FilterJSONOps
7273
}
7374

@@ -435,6 +436,13 @@ func (jf *FilterJSON) BuildAndFilter(ctx context.Context, fb FilterBuilder, opti
435436
andFilter = andFilter.Condition(fb.In(field, rv.resolveMany(field, e.Values)))
436437
}
437438
}
439+
for _, child := range jf.And {
440+
subFilter, err := child.BuildSubFilter(ctx, fb, options...)
441+
if err != nil {
442+
return nil, err
443+
}
444+
andFilter.Condition(subFilter)
445+
}
438446
if len(jf.Or) > 0 {
439447
childFilter := fb.Or()
440448
for _, child := range jf.Or {

pkg/ffapi/restfilter_json_builder.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ type QueryBuilder interface {
7171
// Or creates an OR condition between multiple queries
7272
Or(...QueryBuilder) QueryBuilder
7373

74+
// And creates an AND condition between multiple queries
75+
And(...QueryBuilder) QueryBuilder
76+
7477
// Query returns the query
7578
Query() *QueryJSON
7679
}
@@ -88,6 +91,10 @@ func NewQueryBuilder() QueryBuilder {
8891
return qj.ToBuilder()
8992
}
9093

94+
func QB() QueryBuilder {
95+
return NewQueryBuilder()
96+
}
97+
9198
// Limit sets the limit of the query
9299
func (qb *queryBuilderImpl) Limit(limit uint64) QueryBuilder {
93100
qb.rootQuery.Limit = &limit
@@ -206,6 +213,14 @@ func (qb *queryBuilderImpl) Or(q ...QueryBuilder) QueryBuilder {
206213
return qb
207214
}
208215

216+
// And creates an AND condition between multiple queries
217+
func (qb *queryBuilderImpl) And(q ...QueryBuilder) QueryBuilder {
218+
for _, child := range q {
219+
qb.statements.And = append(qb.statements.And, child.(*queryBuilderImpl).statements)
220+
}
221+
return qb
222+
}
223+
209224
// Query returns the query
210225
func (qb *queryBuilderImpl) Query() *QueryJSON {
211226
return qb.rootQuery

pkg/ffapi/restfilter_json_builder_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,49 @@ func TestQuery_StringOr(t *testing.T) {
149149
assert.JSONEq(t, expectedQuery, string(jsonQuery))
150150
}
151151

152+
func TestQuery_AndNestedOr(t *testing.T) {
153+
expectedQuery := `{
154+
"and": [
155+
{
156+
"eq": [
157+
{ "field": "field1", "value": "aaa" }
158+
]
159+
},
160+
{
161+
"or": [
162+
{
163+
"eq": [
164+
{ "field": "field2", "value": "bbb" }
165+
]
166+
},
167+
{
168+
"eq": [
169+
{ "field": "field2", "value": "ccc" }
170+
]
171+
}
172+
]
173+
}
174+
]
175+
}`
176+
177+
query := QB().
178+
And(
179+
QB().Equal("field1", "aaa"),
180+
QB().
181+
Or(
182+
QB().Equal("field2", "bbb"),
183+
).
184+
Or(
185+
QB().Equal("field2", "ccc"),
186+
),
187+
).
188+
Query()
189+
190+
jsonQuery, err := json.Marshal(query)
191+
assert.NoError(t, err)
192+
assert.JSONEq(t, expectedQuery, string(jsonQuery))
193+
}
194+
152195
func assertQueryEqual(t *testing.T, jsonMap map[string]interface{}, jq *QueryJSON) {
153196
jmb, err := json.Marshal(jsonMap)
154197
assert.NoError(t, err)

pkg/ffapi/restfilter_json_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,52 @@ func TestBuildQuerySingleNestedOr(t *testing.T) {
139139
assert.Equal(t, "tag == 'a'", fi.String())
140140
}
141141

142+
func TestBuildQueryAndWithNestedOr(t *testing.T) {
143+
144+
var qf QueryJSON
145+
err := json.Unmarshal([]byte(`{
146+
"and": [
147+
{
148+
"or": [
149+
{
150+
"equal": [
151+
{
152+
"field": "tag",
153+
"value": "a"
154+
}
155+
]
156+
},
157+
{
158+
"equal": [
159+
{
160+
"field": "tag",
161+
"value": "b"
162+
}
163+
]
164+
}
165+
]
166+
},
167+
{
168+
"equal": [
169+
{
170+
"field": "cid",
171+
"value": "12345"
172+
}
173+
]
174+
}
175+
]
176+
}`), &qf)
177+
assert.NoError(t, err)
178+
179+
filter, err := qf.BuildFilter(context.Background(), TestQueryFactory)
180+
assert.NoError(t, err)
181+
182+
fi, err := filter.Finalize()
183+
assert.NoError(t, err)
184+
185+
assert.Equal(t, "( ( tag == 'a' ) || ( tag == 'b' ) ) && ( cid == '12345' )", fi.String())
186+
}
187+
142188
func TestBuildQuerySkipFieldValidation(t *testing.T) {
143189

144190
var jf *FilterJSON
@@ -706,3 +752,28 @@ func TestBuildQueryJSONContainsShortNames(t *testing.T) {
706752

707753
assert.Equal(t, "( sequence <= 12345 ) && ( sequence >> 12345 )", fi.String())
708754
}
755+
756+
func TestBuildQueryAndFail(t *testing.T) {
757+
758+
var qf QueryJSON
759+
err := json.Unmarshal([]byte(`{
760+
"and": [
761+
{
762+
"or": [
763+
{
764+
"equal": [
765+
{
766+
"field": "color",
767+
"value": "b"
768+
}
769+
]
770+
}
771+
]
772+
}
773+
]
774+
}`), &qf)
775+
assert.NoError(t, err)
776+
777+
_, err = qf.BuildFilter(context.Background(), TestQueryFactory)
778+
assert.Regexp(t, "FF00142.*color", err)
779+
}

pkg/i18n/en_base_field_descriptions.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ var (
4343
FilterJSONSkip = ffm("FilterJSON.skip", "Number of results to skip before returning entries, for skip+limit based pagination")
4444
FilterJSONSort = ffm("FilterJSON.sort", "Array of fields to sort by. A '-' prefix on a field requests that field is sorted in descending order")
4545
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)")
46-
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)")
46+
FilterJSONOr = ffm("FilterJSON.or", "Array of sub-queries") // Note due to complex issue in swagger generator AND/OR need to be identical
47+
FilterJSONAnd = ffm("FilterJSON.and", "Array of sub-queries") // ^^^ the `description` field is getting pushed to the sub-schema definition, and is non-deterministic
4748
FilterJSONFields = ffm("FilterJSON.fields", "Fields to return in the response")
4849

4950
EventStreamBatchSize = ffm("eventstream.batchSize", "Maximum number of events to deliver in each batch")

0 commit comments

Comments
 (0)