Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion pkg/ffapi/restfilter_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions pkg/ffapi/restfilter_json_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions pkg/ffapi/restfilter_json_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
71 changes: 71 additions & 0 deletions pkg/ffapi/restfilter_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
3 changes: 2 additions & 1 deletion pkg/i18n/en_base_field_descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +46 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the swagger generator is this:

        or:
          items:
            $ref: '#/components/schemas/FilterJSON'
          type: array

Then depending on non-deterministic behavior, the FilterJSON schema gets a description from _ethertheandoror` definition.

That makes swagger generation non-deterministic.

IMHO this is a bug, and the swagger should look like this - but that would be a hard contribution to build+validate to the openapi3gen package:

        or:
          description: "the description of the reference to the object"
          items:
            $ref: '#/components/schemas/FilterJSON'
          type: array

Copy link
Contributor

@EnriqueL8 EnriqueL8 Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to raise this as an issue to the openapi3gen pkg or/and this repo before we forget about this thinking?

FilterJSONFields = ffm("FilterJSON.fields", "Fields to return in the response")

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