Skip to content

feat(GraphQL): Add support for has filter on list of fields. #7363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 5, 2021
1 change: 1 addition & 0 deletions graphql/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ func RunAll(t *testing.T) {
t.Run("date filters", dateFilters)
t.Run("float filters", floatFilters)
t.Run("has filters", hasFilters)
t.Run("has filter on list of fields", hasFilterOnListOfFields)
t.Run("Int filters", int32Filters)
t.Run("Int64 filters", int64Filters)
t.Run("boolean filters", booleanFilters)
Expand Down
12 changes: 12 additions & 0 deletions graphql/e2e/common/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,18 @@ func hasFilters(t *testing.T) {
cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{newPost})
}

func hasFilterOnListOfFields(t *testing.T) {
newCountry := addCountry(t, postExecutor)
newAuthor := addAuthor(t, newCountry.ID, postExecutor)
newPost := addPostWithNullText(t, newAuthor.ID, newCountry.ID, postExecutor)
Filter := map[string]interface{}{"not": map[string]interface{}{"has": []interface{}{"text", "numViews"}}}
Expected := []*post{
{Title: "No text"},
}
postTest(t, Filter, Expected)
cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{newPost})
}

func int64Filters(t *testing.T) {
cases := map[string]struct {
Filter interface{}
Expand Down
6 changes: 3 additions & 3 deletions graphql/e2e/schema/apollo_service_response.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ input AddMissionInput {

input AstronautFilter {
id: [ID!]
has: AstronautHasFilter
has: [AstronautHasFilter]
and: [AstronautFilter]
or: [AstronautFilter]
not: AstronautFilter
Expand All @@ -427,7 +427,7 @@ input AstronautRef {

input CarFilter {
id: [ID!]
has: CarHasFilter
has: [CarHasFilter]
and: [CarFilter]
or: [CarFilter]
not: CarFilter
Expand All @@ -450,7 +450,7 @@ input CarRef {

input MissionFilter {
id: [ID!]
has: MissionHasFilter
has: [MissionHasFilter]
and: [MissionFilter]
or: [MissionFilter]
not: MissionFilter
Expand Down
2 changes: 1 addition & 1 deletion graphql/e2e/schema/generatedSchema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ input AddAuthorInput {

input AuthorFilter {
id: [ID!]
has: AuthorHasFilter
has: [AuthorHasFilter]
and: [AuthorFilter]
or: [AuthorFilter]
not: AuthorFilter
Expand Down
102 changes: 54 additions & 48 deletions graphql/resolve/query_rewriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -1826,63 +1826,53 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *gql.FilterTree
},
})
case []interface{}:
// has: [comments, text] -> has(comments) AND has(text)
// ids: [ 0x123, 0x124]

// If ids is an @external field then it gets rewritten just like `in` filter
// ids: [0x123, 0x124] -> eq(typeName.ids, "0x123", 0x124)
if typ.Field(field).IsExternal() {
fn := "eq"
args := []gql.Arg{{Value: typ.DgraphPredicate(field)}}
for _, v := range dgFunc {
args = append(args, gql.Arg{Value: maybeQuoteArg(fn, v)})
switch field {
case "has":
ands = append(ands, buildHasFilterList(typ, dgFunc)...)
default:
// If ids is an @external field then it gets rewritten just like `in` filter
// ids: [0x123, 0x124] -> eq(typeName.ids, "0x123", 0x124)
if typ.Field(field).IsExternal() {
fn := "eq"
args := []gql.Arg{{Value: typ.DgraphPredicate(field)}}
for _, v := range dgFunc {
args = append(args, gql.Arg{Value: maybeQuoteArg(fn, v)})
}
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: fn,
Args: args,
},
})
} else {
// if it is not an @external field then it is rewritten as uid filter.
// ids: [ 0x123, 0x124 ] -> uid(0x123, 0x124)
ids := convertIDs(dgFunc)
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: "uid",
UID: ids,
},
})
}
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: fn,
Args: args,
},
})
} else {
// if it is not an @external field then it is rewritten as uid filter.
// ids: [ 0x123, 0x124 ] -> uid(0x123, 0x124)
ids := convertIDs(dgFunc)
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: "uid",
UID: ids,
},
})
}
case interface{}:
// has: comments -> has(Post.comments)
// OR
// isPublished: true -> eq(Post.isPublished, true)
// OR an enum case
// postType: Question -> eq(Post.postType, "Question")
switch field {
case "has":
fieldName := fmt.Sprintf("%v", dgFunc)
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: field,
Args: []gql.Arg{
{Value: typ.DgraphPredicate(fieldName)},
},
},
})

default:
fn := "eq"
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: fn,
Args: []gql.Arg{
{Value: typ.DgraphPredicate(field)},
{Value: fmt.Sprintf("%v", dgFunc)},
},
fn := "eq"
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: fn,
Args: []gql.Arg{
{Value: typ.DgraphPredicate(field)},
{Value: fmt.Sprintf("%v", dgFunc)},
},
})
}
},
})
}
}
}
Expand All @@ -1909,6 +1899,22 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *gql.FilterTree
}
}

func buildHasFilterList(typ schema.Type, fieldsSlice []interface{}) []*gql.FilterTree {
var ands []*gql.FilterTree
fn := "has"
for _, fieldName := range fieldsSlice {
ands = append(ands, &gql.FilterTree{
Func: &gql.Function{
Name: fn,
Args: []gql.Arg{
{Value: typ.DgraphPredicate(fieldName.(string))},
},
},
})
}
return ands
}

func buildPoint(point map[string]interface{}, buf *bytes.Buffer) {
x.Check2(buf.WriteString(fmt.Sprintf("[%v,%v]", point[schema.Longitude],
point[schema.Latitude])))
Expand Down
37 changes: 37 additions & 0 deletions graphql/resolve/query_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,21 @@
}
}

-
name: "has Filter on list of fields"
gqlquery: |
query {
queryTeacher(filter: {has: [subject, teaches ] } ) {
name
}
}
dgquery: |-
query {
queryTeacher(func: type(Teacher)) @filter((has(Teacher.subject) AND has(Teacher.teaches))) {
name : People.name
dgraph.uid : uid
}
}
- name: "Query Has Filter on type which has neither ID field nor any search argument"
gqlquery: |
query {
Expand Down Expand Up @@ -1213,6 +1228,28 @@
dgraph.uid : uid
}
}
-
name: "Deep filter with has filter on list of fields"
gqlquery: |
query {
queryAuthor {
name
posts(filter: { has : [tags, text] }) {
title
}
}
}
dgquery: |-
query {
queryAuthor(func: type(Author)) {
name : Author.name
posts : Author.posts @filter((has(Post.tags) AND has(Post.text))) {
title : Post.title
dgraph.uid : uid
}
dgraph.uid : uid
}
}

-
name: "Deep filter with has and other filters"
Expand Down
2 changes: 1 addition & 1 deletion graphql/schema/gqlschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1516,7 +1516,7 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition) {
// Has filter makes sense only if there is atleast one non ID field in the defn
if len(getFieldsWithoutIDType(schema, defn)) > 0 {
filter.Fields = append(filter.Fields,
&ast.FieldDefinition{Name: "has", Type: &ast.Type{NamedType: defn.Name + "HasFilter"}},
&ast.FieldDefinition{Name: "has", Type: &ast.Type{Elem: &ast.Type{NamedType: defn.Name + "HasFilter"}}},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ input TodoFilter {
id: [ID!]
isPublic: Boolean
dateCompleted: StringTermFilter
has: TodoHasFilter
has: [TodoHasFilter]
and: [TodoFilter]
or: [TodoFilter]
not: TodoFilter
Expand Down Expand Up @@ -426,7 +426,7 @@ input UpdateUserInput {

input UserFilter {
username: StringHashFilter
has: UserHasFilter
has: [UserHasFilter]
and: [UserFilter]
or: [UserFilter]
not: UserFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ input AddCarInput {

input CarFilter {
id: [ID!]
has: CarHasFilter
has: [CarHasFilter]
and: [CarFilter]
or: [CarFilter]
not: CarFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ input AddMissionInput {

input AstronautFilter {
id: [ID!]
has: AstronautHasFilter
has: [AstronautHasFilter]
and: [AstronautFilter]
or: [AstronautFilter]
not: AstronautFilter
Expand All @@ -383,7 +383,7 @@ input AstronautRef {

input MissionFilter {
id: [ID!]
has: MissionHasFilter
has: [MissionHasFilter]
and: [MissionFilter]
or: [MissionFilter]
not: MissionFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ input AddHumanInput {
input CharacterFilter {
id: [ID!]
name: StringExactFilter
has: CharacterHasFilter
has: [CharacterHasFilter]
and: [CharacterFilter]
or: [CharacterFilter]
not: CharacterFilter
Expand All @@ -400,7 +400,7 @@ input CharacterRef {
input HumanFilter {
id: [ID!]
name: StringExactFilter
has: HumanHasFilter
has: [HumanHasFilter]
and: [HumanFilter]
or: [HumanFilter]
not: HumanFilter
Expand Down Expand Up @@ -429,7 +429,7 @@ input HumanRef {

input PersonFilter {
id: [ID!]
has: PersonHasFilter
has: [PersonHasFilter]
and: [PersonFilter]
or: [PersonFilter]
not: PersonFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ input AddUserInput {

input CountryFilter {
code: StringHashFilter
has: CountryHasFilter
has: [CountryHasFilter]
and: [CountryFilter]
or: [CountryFilter]
not: CountryFilter
Expand All @@ -564,7 +564,7 @@ input CountryRef {

input ProductFilter {
id: [ID!]
has: ProductHasFilter
has: [ProductHasFilter]
and: [ProductFilter]
or: [ProductFilter]
not: ProductFilter
Expand All @@ -587,7 +587,7 @@ input ProductRef {

input ReviewsFilter {
id: [ID!]
has: ReviewsHasFilter
has: [ReviewsHasFilter]
and: [ReviewsFilter]
or: [ReviewsFilter]
not: ReviewsFilter
Expand All @@ -610,7 +610,7 @@ input ReviewsRef {

input SchoolFilter {
id: [ID!]
has: SchoolHasFilter
has: [SchoolHasFilter]
and: [SchoolFilter]
or: [SchoolFilter]
not: SchoolFilter
Expand All @@ -627,7 +627,7 @@ input SchoolRef {

input StudentFilter {
id: [ID!]
has: StudentHasFilter
has: [StudentHasFilter]
and: [StudentFilter]
or: [StudentFilter]
not: StudentFilter
Expand Down Expand Up @@ -688,7 +688,7 @@ input UpdateUserInput {

input UserFilter {
name: StringHashFilter
has: UserHasFilter
has: [UserHasFilter]
and: [UserFilter]
or: [UserFilter]
not: UserFilter
Expand Down
Loading