From faecee6a86605ffdcda122931d110ce4ad6a0fbc Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Sat, 8 Jun 2024 01:16:44 +1000 Subject: [PATCH 01/13] Adding nested filtering to Dgraph. --- graphql/resolve/mutation_rewriter.go | 3 +- graphql/resolve/query.go | 2 + graphql/resolve/query_rewriter.go | 109 +++++++++++++++++++++------ graphql/schema/gqlschema.go | 9 +++ 4 files changed, 98 insertions(+), 25 deletions(-) diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index a5914347b76..9e53bdcfc4f 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -996,7 +996,8 @@ func RewriteUpsertQueryFromMutation( addTypeFunc(dgQuery[0], m.MutatedType().DgraphName()) } - _ = addFilter(dgQuery[0], m.MutatedType(), filter) + _, filterQueries := addFilter(dgQuery[0], m.MutatedType(), filter, m.Alias()) + dgQuery = append(dgQuery, filterQueries...) } else { // It means this is called from upsert with Add mutation. // nodeID will be uid of the node to be upserted. We add UID func diff --git a/graphql/resolve/query.go b/graphql/resolve/query.go index 07ffce52c44..eece22c538b 100644 --- a/graphql/resolve/query.go +++ b/graphql/resolve/query.go @@ -17,6 +17,7 @@ package resolve import ( + "fmt" "context" "encoding/json" "errors" @@ -127,6 +128,7 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que query.ResponseName())) } qry := dgraph.AsString(dgQuery) + fmt.Println("myquery - ", qry) queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration) queryTimer.Start() diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 509e593a772..4407ae91828 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -257,7 +257,8 @@ func aggregateQuery(query schema.Query, authRw *authRewriter) []*dql.GraphQuery // Add filter filter, _ := query.ArgValue("filter").(map[string]interface{}) - _ = addFilter(dgQuery[0], mainType, filter) + _, filterQueries := addFilter(dgQuery[0], mainType, filter, query.Alias()) + dgQuery = append(dgQuery, filterQueries...) dgQuery = authRw.addAuthQueries(mainType, dgQuery, rbac) @@ -475,7 +476,8 @@ func rewriteAsQueryByIds( addUIDFunc(dgQuery[0], intersection(ids, uids)) } - addArgumentsToField(dgQuery[0], field) + includedQueries := addArgumentsToField(dgQuery[0], field) + dgQuery = append(dgQuery, includedQueries...) // The function getQueryByIds is called for passwordQuery or fetching query result types // after making a mutation. In both cases, we want the selectionSet to use the `query` auth @@ -501,11 +503,12 @@ func rewriteAsQueryByIds( // addArgumentsToField adds various different arguments to a field, such as // filter, order and pagination. -func addArgumentsToField(dgQuery *dql.GraphQuery, field schema.Field) { +func addArgumentsToField(dgQuery *dql.GraphQuery, field schema.Field) []*dql.GraphQuery { filter, _ := field.ArgValue("filter").(map[string]interface{}) - _ = addFilter(dgQuery, field.Type(), filter) + _, filterQueries := addFilter(dgQuery, field.Type(), filter, field.Alias()) addOrder(dgQuery, field) addPagination(dgQuery, field) + return filterQueries } func addTopLevelTypeFilter(query *dql.GraphQuery, field schema.Field) { @@ -957,7 +960,9 @@ func rewriteAsQuery(field schema.Field, authRw *authRewriter) []*dql.GraphQuery return dgQuery } - addArgumentsToField(dgQuery[0], field) + includedQueries := addArgumentsToField(dgQuery[0], field) + dgQuery = append(dgQuery, includedQueries...) + selectionAuth := addSelectionSetFrom(dgQuery[0], field, authRw) // we don't need to query uid for auth queries, as they always have at least one field in their // selection set. @@ -1465,7 +1470,7 @@ func buildAggregateFields( // Filter for aggregate Fields. This is added to all count aggregate fields // and mainField fieldFilter, _ := f.ArgValue("filter").(map[string]interface{}) - _ = addFilter(mainField, constructedForType, fieldFilter) + _, filterQueries := addFilter(mainField, constructedForType, fieldFilter, f.Alias()) // Add type filter in case the Dgraph predicate for which the aggregate // field belongs to is a reverse edge @@ -1489,7 +1494,9 @@ func buildAggregateFields( Attr: "count(" + constructedForDgraphPredicate + ")", } // Add filter to count aggregation field. - _ = addFilter(aggregateChild, constructedForType, fieldFilter) + // TODO: IA: add filter queries + _, aggFilterQueries := addFilter(aggregateChild, constructedForType, fieldFilter, f.Alias()) + filterQueries = append(filterQueries, aggFilterQueries...) // Add type filter in case the Dgraph predicate for which the aggregate // field belongs to is a reverse edge @@ -1597,6 +1604,7 @@ func buildAggregateFields( // not added to them. aggregateChildren = append(aggregateChildren, otherAggregateChildren...) retAuthQueries = append(retAuthQueries, fieldAuth...) + retAuthQueries = append(retAuthQueries, filterQueries...) return aggregateChildren, retAuthQueries } @@ -1681,10 +1689,13 @@ func addSelectionSetFrom( filter, _ := f.ArgValue("filter").(map[string]interface{}) // if this field has been filtered out by the filter, then don't add it in DQL query - if includeField := addFilter(child, f.Type(), filter); !includeField { + includeField, filterQueries := addFilter(child, f.Type(), filter, field.Alias()) + if !includeField { continue } + authQueries = append(authQueries, filterQueries...) + // Add type filter in case the Dgraph predicate is a reverse edge if strings.HasPrefix(f.DgraphPredicate(), "~") { addTypeFilter(child, f.Type()) @@ -1889,9 +1900,12 @@ func idFilter(filter map[string]interface{}, idField schema.FieldDefinition) []u // addFilter adds a filter to the input DQL query. It returns false if the field for which the // filter was specified should not be included in the DQL query. // Currently, it would only be false for a union field when no memberTypes are queried. -func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{}) bool { +func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{}, queryName string)(bool, []*dql.GraphQuery){ + + filterQueries := []*dql.GraphQuery{} + if len(filter) == 0 { - return true + return true, filterQueries } // There are two cases here. @@ -1916,15 +1930,15 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} if filter, includeField := buildUnionFilter(typ, filter); includeField { q.Filter = filter } else { - return false + return false, filterQueries } } else { - q.Filter = buildFilter(typ, filter) + q.Filter, filterQueries = buildFilter(typ, filter, queryName) } if filterAtRoot { addTypeFilter(q, typ) } - return true + return true, filterQueries } // buildFilter builds a Dgraph dql.FilterTree from a GraphQL 'filter' arg. @@ -1953,8 +1967,9 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} // ATM those will probably generate junk that might cause a Dgraph error. And // bubble back to the user as a GraphQL error when the query fails. Really, // they should fail query validation and never get here. -func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree { +func buildFilter(typ schema.Type, filter map[string]interface{}, queryName string) (*dql.FilterTree, []*dql.GraphQuery) { + var queries []*dql.GraphQuery var ands []*dql.FilterTree var or *dql.FilterTree // Get a stable ordering so we generate the same thing each time. @@ -1970,6 +1985,10 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree if filter[field] == nil { continue } + + // derive variable name for nested objects + qn := queryName + "_" + field + switch field { // In 'and', 'or' and 'not' cases, filter[field] must be a map[string]interface{} @@ -1988,12 +2007,14 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree // ... and: [{}] switch v := filter[field].(type) { case map[string]interface{}: - ft := buildFilter(typ, v) + ft, qs := buildFilter(typ, v, qn) ands = append(ands, ft) + queries = append(queries, qs...) case []interface{}: for _, obj := range v { - ft := buildFilter(typ, obj.(map[string]interface{})) + ft, qs := buildFilter(typ, obj.(map[string]interface{}), qn) ands = append(ands, ft) + queries = append(queries, qs...) } } case "or": @@ -2008,12 +2029,15 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree // ... or: [{}] switch v := filter[field].(type) { case map[string]interface{}: - or = buildFilter(typ, v) + cond, qs := buildFilter(typ, v, qn) + or = cond + queries = append(queries, qs...) case []interface{}: ors := make([]*dql.FilterTree, 0, len(v)) for _, obj := range v { - ft := buildFilter(typ, obj.(map[string]interface{})) + ft, qs := buildFilter(typ, obj.(map[string]interface{}), qn) ors = append(ors, ft) + queries = append(queries, qs...) } or = &dql.FilterTree{ Child: ors, @@ -2025,13 +2049,46 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree // we are here ^^ // -> // @filter(anyofterms(Post.title, "GraphQL") AND NOT eq(Post.isPublished, true)) - not := buildFilter(typ, filter[field].(map[string]interface{})) + not, qs := buildFilter(typ, filter[field].(map[string]interface{}), qn) ands = append(ands, &dql.FilterTree{ Op: "not", Child: []*dql.FilterTree{not}, }) + queries = append(queries, qs...) default: + + // handle nested object filtering + if typ.Field(field).Inverse() != nil { + fil, qs := buildFilter(typ.Field(field).Type(), filter[field].(map[string]interface{}), qn) + queries = append(queries, qs...) + + // add the uids of the nested object + ands = append(ands, &dql.FilterTree{ + Op: "and", + Child: []*dql.FilterTree{{ + Func: &dql.Function{ + Name: "uid", + Args: []dql.Arg{{Value: qn}}, + }, + }}, + }) + + // generate filter var query for nested object + queries = append(queries, &dql.GraphQuery{ + Attr: "var", + Func: &dql.Function{ + Name: "type", + Args: []dql.Arg{{Value: typ.Field(field).Type().Name()}}, + }, + Filter: fil, + Children: []*dql.GraphQuery{{ + Attr: typ.Field(field).Inverse().DgraphPredicate(), + Var: qn, + }}, + }) + continue + } //// It's a base case like: //// title: { anyofterms: "GraphQL" } -> anyofterms(Post.title: "GraphQL") //// numLikes: { between : { min : 10, max:100 }} @@ -2048,7 +2105,9 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree // the filters with null values will be ignored in query rewriting. if fn == "eq" { hasFilterMap := map[string]interface{}{"not": map[string]interface{}{"has": []interface{}{field}}} - ands = append(ands, buildFilter(typ, hasFilterMap)) + ft, qs := buildFilter(typ, hasFilterMap, qn) + ands = append(ands, ft) + queries = append(queries, qs...) } continue } @@ -2192,7 +2251,7 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree var andFt *dql.FilterTree if len(ands) == 0 { - return or + return or, queries } else if len(ands) == 1 { andFt = ands[0] } else if len(ands) > 1 { @@ -2203,13 +2262,13 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *dql.FilterTree } if or == nil { - return andFt + return andFt, queries } return &dql.FilterTree{ Op: "or", Child: []*dql.FilterTree{andFt, or}, - } + }, queries } func buildHasFilterList(typ schema.Type, fieldsSlice []interface{}) []*dql.FilterTree { @@ -2294,11 +2353,13 @@ func buildUnionFilter(typ schema.Type, filter map[string]interface{}) (*dql.Filt memberTypeFt = &dql.FilterTree{Func: buildTypeFunc(memberType.DgraphName())} } else { // else we need to query only the nodes which match the filter for that member type + // TODO: IA: capture filter queries + ft, _ := buildFilter(memberType, memberTypeFilter, "qn") memberTypeFt = &dql.FilterTree{ Op: "and", Child: []*dql.FilterTree{ {Func: buildTypeFunc(memberType.DgraphName())}, - buildFilter(memberType, memberTypeFilter), + ft, }, } } diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 3fff2a7bae2..4f393dd3a89 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -1588,6 +1588,15 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map }) mergeAndAddFilters(filterTypes, schema, filterName) + }else if (fld.Type.Name() == "Group"){ + filterName := fld.Type.Name() + "Filter" + filter.Fields = append(filter.Fields, + &ast.FieldDefinition{ + Name: fld.Name, + Type: &ast.Type{ + NamedType: filterName, + }, + }) } } From 83ed6244e10d00f65466d251169a1ca50f6ae19a Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Sat, 8 Jun 2024 02:04:18 +1000 Subject: [PATCH 02/13] fix filter for union type --- graphql/resolve/query_rewriter.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 4407ae91828..8cfaa8c0eda 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -1494,7 +1494,6 @@ func buildAggregateFields( Attr: "count(" + constructedForDgraphPredicate + ")", } // Add filter to count aggregation field. - // TODO: IA: add filter queries _, aggFilterQueries := addFilter(aggregateChild, constructedForType, fieldFilter, f.Alias()) filterQueries = append(filterQueries, aggFilterQueries...) @@ -1927,7 +1926,7 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} } if typ.IsUnion() { - if filter, includeField := buildUnionFilter(typ, filter); includeField { + if filter, filterQueries, includeField := buildUnionFilter(typ, filter, queryName); includeField { q.Filter = filter } else { return false, filterQueries @@ -2330,12 +2329,13 @@ func buildMultiPolygon(multipolygon map[string]interface{}, buf *bytes.Buffer) { x.Check2(buf.WriteString("]")) } -func buildUnionFilter(typ schema.Type, filter map[string]interface{}) (*dql.FilterTree, bool) { +func buildUnionFilter(typ schema.Type, filter map[string]interface{}, queryName string) (*dql.FilterTree, []*dql.GraphQuery, bool) { + var queries []*dql.GraphQuery memberTypesList, ok := filter["memberTypes"].([]interface{}) // if memberTypes was specified to be an empty list like: { memberTypes: [], ...}, // then we don't need to include the field, on which the filter was specified, in the query. if ok && len(memberTypesList) == 0 { - return nil, false + return nil, queries, false } ft := &dql.FilterTree{ @@ -2353,8 +2353,8 @@ func buildUnionFilter(typ schema.Type, filter map[string]interface{}) (*dql.Filt memberTypeFt = &dql.FilterTree{Func: buildTypeFunc(memberType.DgraphName())} } else { // else we need to query only the nodes which match the filter for that member type - // TODO: IA: capture filter queries - ft, _ := buildFilter(memberType, memberTypeFilter, "qn") + ft, qs := buildFilter(memberType, memberTypeFilter, queryName) + queries = qs memberTypeFt = &dql.FilterTree{ Op: "and", Child: []*dql.FilterTree{ @@ -2367,7 +2367,7 @@ func buildUnionFilter(typ schema.Type, filter map[string]interface{}) (*dql.Filt } // return true because we want to include the field with filter in query - return ft, true + return ft, queries, true } func maybeQuoteArg(fn string, arg interface{}) string { From 55f5b6e8d5dad8c2903e6126ed1755076c479130 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Sun, 9 Jun 2024 15:48:52 +1000 Subject: [PATCH 03/13] fix filter for interfaces --- graphql/resolve/query.go | 2 -- graphql/schema/gqlschema.go | 16 ++++++------- graphql/schema/rules.go | 15 +++++++++++++ graphql/schema/wrappers.go | 45 +++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/graphql/resolve/query.go b/graphql/resolve/query.go index eece22c538b..2193aef30a9 100644 --- a/graphql/resolve/query.go +++ b/graphql/resolve/query.go @@ -128,8 +128,6 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que query.ResponseName())) } qry := dgraph.AsString(dgQuery) - fmt.Println("myquery - ", qry) - queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration) queryTimer.Start() resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: qry, ReadOnly: true}, query) diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 4f393dd3a89..a02847db953 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -1472,6 +1472,13 @@ func addPaginationArguments(fld *ast.FieldDefinition) { // getFilterTypes converts search arguments of a field to graphql filter types. func getFilterTypes(schema *ast.Schema, fld *ast.FieldDefinition, filterName string) []string { + + // Return the object filter if the field is an object that is searchable. + fldType := schema.Types[fld.Type.Name()] + if isCustomType(schema, fld) && hasFilterable(fldType) && hasSearchDirective(fld) { + return []string{fld.Type.Name() + "Filter"} + } + searchArgs := getSearchArgs(fld) filterNames := make([]string, len(searchArgs)) @@ -1588,15 +1595,6 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map }) mergeAndAddFilters(filterTypes, schema, filterName) - }else if (fld.Type.Name() == "Group"){ - filterName := fld.Type.Name() + "Filter" - filter.Fields = append(filter.Fields, - &ast.FieldDefinition{ - Name: fld.Name, - Type: &ast.Type{ - NamedType: filterName, - }, - }) } } diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index c5799bddd0b..94c1dc063f7 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -1075,6 +1075,21 @@ func searchValidation( return nil } + // If the field is an object, it is require to have an inverse edge for filtering. + // It's not enough to just check for the @hasInverse directive as it + // may be defined in the inverse type. + if isCustomType(sch, field) { + if !hasInverseReference(sch, typ, field) { + errs = append(errs, gqlerror.ErrorPosf( + dir.Position, + "Type %s; Field %s: has the @search directive for type %s "+ + "but also requires the @hasInverse directive.", + typ.Name, field.Name, field.Type.Name())) + return errs + } + return nil + } + errs = append(errs, gqlerror.ErrorPosf( dir.Position, "Type %s; Field %s: has the @search directive but fields of type %s "+ diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index e756d681a36..2bbe1a2bcdb 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -2372,6 +2372,11 @@ func hasEmbeddingDirective(fd *ast.FieldDefinition) bool { return id != nil } +func hasSearchDirective(fd *ast.FieldDefinition) bool { + id := fd.Directives.ForName(searchDirective) + return id != nil +} + func (fd *fieldDefinition) HasInterfaceArg() bool { if fd.fieldDef == nil { return false @@ -2396,6 +2401,46 @@ func hasInterfaceArg(fd *ast.FieldDefinition) bool { return false } +// hasInverseReference checks if an inverse predicate is configured for an object. +func hasInverseReference(sch *ast.Schema, typ *ast.Definition, fd *ast.FieldDefinition) bool { + // check that the @hasInverse directive is provided + id := fd.Directives.ForName(inverseDirective) + if id != nil { + return true + } + + // also check the reference type. + refType := sch.Types[fd.Type.Name()] + for _, refField := range refType.Fields { + + refFieldType := sch.Types[refField.Type.Name()] + if refField.Type.Name() != typ.Name && !typ.OneOf(refFieldType.Interfaces...){ + continue + } + + refFieldDir := refField.Directives.ForName(inverseDirective); + if refFieldDir == nil { + continue + } + + invField := refFieldDir.Arguments.ForName("field") + if invField == nil { + continue + } + + invFieldName := invField.Value.Raw + if invFieldName == fd.Name { + return true + } + } + return false +} + +func isCustomType(sch *ast.Schema, fd *ast.FieldDefinition) bool { + _, ok := inbuiltTypeToDgraph[fd.Type.Name()] + return !ok && sch.Types[fd.Type.Name()].Kind == ast.Object +} + func isID(fd *ast.FieldDefinition) bool { return fd.Type.Name() == "ID" } From 9fddbf743350f16ad8b34e0c12eaf518550f823b Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Sun, 9 Jun 2024 15:51:30 +1000 Subject: [PATCH 04/13] remove unused import --- graphql/resolve/query.go | 1 - 1 file changed, 1 deletion(-) diff --git a/graphql/resolve/query.go b/graphql/resolve/query.go index 2193aef30a9..6fe86fa23cb 100644 --- a/graphql/resolve/query.go +++ b/graphql/resolve/query.go @@ -17,7 +17,6 @@ package resolve import ( - "fmt" "context" "encoding/json" "errors" From 01c84caf763298286351d929860e416c2bad0d74 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Sun, 9 Jun 2024 23:43:08 +1000 Subject: [PATCH 05/13] =?UTF-8?q?=E2=9C=A8=20(query=5Frewriter.go):=20Add?= =?UTF-8?q?=20check=20for=20search=20directive=20in=20nested=20object=20fi?= =?UTF-8?q?ltering=20=E2=99=BB=EF=B8=8F=20(query=5Frewriter.go):=20Refacto?= =?UTF-8?q?r=20to=20store=20field=20definition=20in=20a=20variable=20for?= =?UTF-8?q?=20reuse=20=E2=9C=A8=20(wrappers.go):=20Add=20HasSearchDirectiv?= =?UTF-8?q?e=20method=20to=20FieldDefinition=20interface=20and=20its=20imp?= =?UTF-8?q?lementation=20to=20check=20for=20search=20directive=20in=20a=20?= =?UTF-8?q?field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphql/resolve/query_rewriter.go | 11 ++++++----- graphql/schema/wrappers.go | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 8cfaa8c0eda..486139e3845 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -2058,9 +2058,10 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin default: // handle nested object filtering - if typ.Field(field).Inverse() != nil { - fil, qs := buildFilter(typ.Field(field).Type(), filter[field].(map[string]interface{}), qn) - queries = append(queries, qs...) + fd := typ.Field(field) + if fd != nil && fd.HasSearchDirective() && fd.Inverse() != nil { + fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), qn) + queries = append(queries, qs...) // add the uids of the nested object ands = append(ands, &dql.FilterTree{ @@ -2078,11 +2079,11 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin Attr: "var", Func: &dql.Function{ Name: "type", - Args: []dql.Arg{{Value: typ.Field(field).Type().Name()}}, + Args: []dql.Arg{{Value: fd.Type().Name()}}, }, Filter: fil, Children: []*dql.GraphQuery{{ - Attr: typ.Field(field).Inverse().DgraphPredicate(), + Attr: fd.Inverse().DgraphPredicate(), Var: qn, }}, }) diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 2bbe1a2bcdb..f44114eda1d 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -283,6 +283,7 @@ type FieldDefinition interface { IsID() bool IsExternal() bool HasIDDirective() bool + HasSearchDirective() bool HasEmbeddingDirective() bool EmbeddingSearchMetric() string HasInterfaceArg() bool @@ -2372,6 +2373,13 @@ func hasEmbeddingDirective(fd *ast.FieldDefinition) bool { return id != nil } +func (fd *fieldDefinition) HasSearchDirective() bool { + if fd.fieldDef == nil { + return false + } + return hasSearchDirective(fd.fieldDef) +} + func hasSearchDirective(fd *ast.FieldDefinition) bool { id := fd.Directives.ForName(searchDirective) return id != nil From c48b191151bcabcf0c483084dc243d372fe4b518 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Mon, 10 Jun 2024 22:23:54 +1000 Subject: [PATCH 06/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(query.go):=20Add=20?= =?UTF-8?q?space=20for=20better=20code=20readability=20=E2=9C=A8=20(query?= =?UTF-8?q?=5Frewriter.go):=20Remove=20unnecessary=20assignment=20of=20agg?= =?UTF-8?q?FilterQueries=20=F0=9F=92=A1=20(query=5Frewriter.go):=20Add=20c?= =?UTF-8?q?omments=20to=20explain=20nested=20object=20filtering=20?= =?UTF-8?q?=F0=9F=90=9B=20(query=5Frewriter.go):=20Fix=20nested=20object?= =?UTF-8?q?=20filtering=20to=20handle=20nested=20fields=20correctly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphql/resolve/query.go | 1 + graphql/resolve/query_rewriter.go | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/graphql/resolve/query.go b/graphql/resolve/query.go index 6fe86fa23cb..07ffce52c44 100644 --- a/graphql/resolve/query.go +++ b/graphql/resolve/query.go @@ -127,6 +127,7 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que query.ResponseName())) } qry := dgraph.AsString(dgQuery) + queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration) queryTimer.Start() resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: qry, ReadOnly: true}, query) diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 486139e3845..83935c56043 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -1494,8 +1494,7 @@ func buildAggregateFields( Attr: "count(" + constructedForDgraphPredicate + ")", } // Add filter to count aggregation field. - _, aggFilterQueries := addFilter(aggregateChild, constructedForType, fieldFilter, f.Alias()) - filterQueries = append(filterQueries, aggFilterQueries...) + addFilter(aggregateChild, constructedForType, fieldFilter, f.Alias()) // Add type filter in case the Dgraph predicate for which the aggregate // field belongs to is a reverse edge @@ -1948,6 +1947,8 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} // filter: { title: { anyofterms: "GraphQL" }, isPublished: true, ... } // or // filter: { title: { anyofterms: "GraphQL" }, and: { not: { ... } } } +// or +// filter: { : { ... }, ... } // etc // // typ is the GraphQL type we are filtering on, and is needed to turn for example @@ -2056,8 +2057,15 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin }) queries = append(queries, qs...) default: - - // handle nested object filtering + // Handle nested object filtering + // + // filter: { : { ... }, ... } + // we are here ^^ + // -> + // var() @filter(){ + // nested_field_name as + // } + // root() @filter(var(nested_field_name)) fd := typ.Field(field) if fd != nil && fd.HasSearchDirective() && fd.Inverse() != nil { fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), qn) @@ -2089,6 +2097,7 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin }) continue } + //// It's a base case like: //// title: { anyofterms: "GraphQL" } -> anyofterms(Post.title: "GraphQL") //// numLikes: { between : { min : 10, max:100 }} From c4aa1b913cc2fdde2ea721e36e31a276add53ef0 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Thu, 13 Jun 2024 02:28:28 +1000 Subject: [PATCH 07/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(query=5Frewriter.go?= =?UTF-8?q?):=20Refactor=20code=20to=20improve=20readability=20and=20maint?= =?UTF-8?q?ainability=20=E2=9C=A8=20(query=5Ftest.yaml,=20schema.graphql,?= =?UTF-8?q?=20gqlschema=5Ftest.yml):=20Add=20tests=20and=20schema=20for=20?= =?UTF-8?q?nested=20filtering=20=F0=9F=90=9B=20(gqlschema.go,=20rules.go):?= =?UTF-8?q?=20Fix=20validation=20for=20@search=20directive=20to=20require?= =?UTF-8?q?=20@hasInverse=20directive=20when=20necessary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (wrappers.go): Rename function 'hasInverseReference' to 'hasInverse' for clarity 💡 (wrappers.go): Remove unnecessary semicolon and improve code formatting ✨ (wrappers.go): Extend 'isCustomType' function to include 'ast.Interface' kind 🐛 (wrappers.go): Add additional check for inverse type when querying from an interface, not the implemented type --- graphql/resolve/query_rewriter.go | 14 +++--- graphql/resolve/query_test.yaml | 64 ++++++++++++++++++++++- graphql/resolve/schema.graphql | 16 ++++++ graphql/schema/gqlschema.go | 4 +- graphql/schema/gqlschema_test.yml | 52 +++++++++++++++++-- graphql/schema/rules.go | 4 +- graphql/schema/wrappers.go | 84 +++++++++++++++++++++++-------- 7 files changed, 201 insertions(+), 37 deletions(-) diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 83935c56043..049167d118f 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -1898,8 +1898,8 @@ func idFilter(filter map[string]interface{}, idField schema.FieldDefinition) []u // addFilter adds a filter to the input DQL query. It returns false if the field for which the // filter was specified should not be included in the DQL query. // Currently, it would only be false for a union field when no memberTypes are queried. -func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{}, queryName string)(bool, []*dql.GraphQuery){ - +func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{}, queryName string) (bool, []*dql.GraphQuery) { + filterQueries := []*dql.GraphQuery{} if len(filter) == 0 { @@ -2058,7 +2058,7 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin queries = append(queries, qs...) default: // Handle nested object filtering - // + // // filter: { : { ... }, ... } // we are here ^^ // -> @@ -2069,11 +2069,11 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin fd := typ.Field(field) if fd != nil && fd.HasSearchDirective() && fd.Inverse() != nil { fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), qn) - queries = append(queries, qs...) - + queries = append(queries, qs...) + // add the uids of the nested object ands = append(ands, &dql.FilterTree{ - Op: "and", + Op: "and", Child: []*dql.FilterTree{{ Func: &dql.Function{ Name: "uid", @@ -2092,7 +2092,7 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin Filter: fil, Children: []*dql.GraphQuery{{ Attr: fd.Inverse().DgraphPredicate(), - Var: qn, + Var: qn, }}, }) continue diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index ab15599d020..8274c824743 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -3517,4 +3517,66 @@ dgraph.uid : uid ProjectDotProduct.vector_distance : val(distance) } - } \ No newline at end of file + } + +- name: "query nested type with interface" + gqlquery: | + query { + queryNested_X(filter: {s: {eq: ""}, y: {s: {eq: ""}}}) { + s + } + } + dgquery: |- + query { + queryNested_X(func: type(Nested_X)) @filter((eq(Nested_X.s, "") AND (uid(queryNested_X_y)))) { + Nested_X.s : Nested_X.s + dgraph.uid : uid + } + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + queryNested_X_y as Nested_Z.x + } + } + +- name: "query nested type from type with interface" + gqlquery: | + query { + queryNested_Y(filter: {s: {eq: ""}, x: {s: {eq: ""}}}) { + s + } + } + dgquery: |- + query { + queryNested_Y(func: type(Nested_Y)) @filter((eq(Nested_Y.s, "") AND (uid(queryNested_Y_x)))) { + Nested_Y.s : Nested_Y.s + dgraph.uid : uid + } + var(func: type(Nested_X)) @filter(eq(Nested_X.s, "")) { + queryNested_Y_x as Nested_X.y + } + } + +- name: "query nested type from interface" + gqlquery: | + query { + queryNested_Z(filter: { x: {s: {eq: ""}}}) { + x { s } + } + } + dgquery: |- + query { + queryNested_Z(func: type(Nested_Z)) @filter((uid(queryNested_Z_x))) { + dgraph.type + Nested_Z.x : Nested_Z.x { + Nested_X.s : Nested_X.s + dgraph.uid : uid + } + dgraph.uid : uid + } + var(func: type(Nested_X)) @filter(eq(Nested_X.s, "")) { + queryNested_Z_x as Nested_X.y + } + } + +# query deeply nested +# query nested with OR condition +# query nested with AND condition \ No newline at end of file diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index 6b469284860..4daf6e7011d 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -522,3 +522,19 @@ type ProjectDotProduct { title: String description_v: [Float!] @embedding @search(by: ["hnsw(metric: dotproduct, exponent: 4)"]) } + +""" +This types are used to validate nested filting. +""" +type Nested_X { + s: String @search(by: [hash]) + y: Nested_Y @hasInverse(field: x) @search +} + +type Nested_Y implements Nested_Z{ + s: String @search(by: [hash]) +} + +interface Nested_Z { + x: Nested_X @search +} \ No newline at end of file diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index a02847db953..4496c5d4c6c 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -1472,13 +1472,13 @@ func addPaginationArguments(fld *ast.FieldDefinition) { // getFilterTypes converts search arguments of a field to graphql filter types. func getFilterTypes(schema *ast.Schema, fld *ast.FieldDefinition, filterName string) []string { - + // Return the object filter if the field is an object that is searchable. fldType := schema.Types[fld.Type.Name()] if isCustomType(schema, fld) && hasFilterable(fldType) && hasSearchDirective(fld) { return []string{fld.Type.Name() + "Filter"} } - + searchArgs := getSearchArgs(fld) filterNames := make([]string, len(searchArgs)) diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index eb206de22fa..a904a6ddcc1 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -404,7 +404,7 @@ invalid_schemas: ] - - name: "Search will error on type that can't have the @search" + name: "Search will error on type that require @hasInverse directive" input: | type X { y: Y @search @@ -413,8 +413,22 @@ invalid_schemas: y: String } errlist: [ - {"message": "Type X; Field y: has the @search directive but fields of type Y - can't have the @search directive.", + {"message": "Type X; Field y: has the @search directive for type Y but also requires + the @hasInverse directive.", + "locations":[{"line":2, "column":9}]} + ] + - + name: "Search will error on interface that require @hasInverse directive" + input: | + type X { + y: Y @search + } + interface Y { + y: String + } + errlist: [ + {"message": "Type X; Field y: has the @search directive for type Y but also requires + the @hasInverse directive.", "locations":[{"line":2, "column":9}]} ] @@ -3150,6 +3164,38 @@ valid_schemas: A } + - + name: "Correct search on object type" + input: | + type X { + y: Y @hasInverse(field: x) @search + y2: Y @search + y3: Y @hasInverse(field: x3) @search + } + type Y { + x: X + x2: X @hasInverse(field: y2) + x3: X @hasInverse(field: y3) @search + } + + - + name: "Correct search on interface type" + input: | + type X { + y: Y @hasInverse(field: x) @search + y2: Y @search + y3: Y @hasInverse(field: x3) @search + y4: Y + y5: Y @hasInverse(field: x5) + } + interface Y { + x: X + x2: X @hasInverse(field: y2) + x3: X @hasInverse(field: y3) @search + x4: X @hasInverse(field: y4) @search + x5: X @search + } + - name: "dgraph directive with correct reverse field works" input: | diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index 94c1dc063f7..60df640e01a 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -1079,11 +1079,11 @@ func searchValidation( // It's not enough to just check for the @hasInverse directive as it // may be defined in the inverse type. if isCustomType(sch, field) { - if !hasInverseReference(sch, typ, field) { + if !hasInverse(sch, typ, field) { errs = append(errs, gqlerror.ErrorPosf( dir.Position, "Type %s; Field %s: has the @search directive for type %s "+ - "but also requires the @hasInverse directive.", + "but also requires the @hasInverse directive.", typ.Name, field.Name, field.Type.Name())) return errs } diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index f44114eda1d..fa869b5fc34 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -2409,8 +2409,8 @@ func hasInterfaceArg(fd *ast.FieldDefinition) bool { return false } -// hasInverseReference checks if an inverse predicate is configured for an object. -func hasInverseReference(sch *ast.Schema, typ *ast.Definition, fd *ast.FieldDefinition) bool { +// hasInverse checks if an inverse predicate is configured for an object. +func hasInverse(sch *ast.Schema, typ *ast.Definition, fd *ast.FieldDefinition) bool { // check that the @hasInverse directive is provided id := fd.Directives.ForName(inverseDirective) if id != nil { @@ -2420,13 +2420,13 @@ func hasInverseReference(sch *ast.Schema, typ *ast.Definition, fd *ast.FieldDefi // also check the reference type. refType := sch.Types[fd.Type.Name()] for _, refField := range refType.Fields { - + refFieldType := sch.Types[refField.Type.Name()] - if refField.Type.Name() != typ.Name && !typ.OneOf(refFieldType.Interfaces...){ + if refField.Type.Name() != typ.Name && !typ.OneOf(refFieldType.Interfaces...) { continue } - refFieldDir := refField.Directives.ForName(inverseDirective); + refFieldDir := refField.Directives.ForName(inverseDirective) if refFieldDir == nil { continue } @@ -2446,7 +2446,8 @@ func hasInverseReference(sch *ast.Schema, typ *ast.Definition, fd *ast.FieldDefi func isCustomType(sch *ast.Schema, fd *ast.FieldDefinition) bool { _, ok := inbuiltTypeToDgraph[fd.Type.Name()] - return !ok && sch.Types[fd.Type.Name()].Kind == ast.Object + return !ok && (sch.Types[fd.Type.Name()].Kind == ast.Object || + sch.Types[fd.Type.Name()].Kind == ast.Interface) } func isID(fd *ast.FieldDefinition) bool { @@ -2467,29 +2468,68 @@ func (fd *fieldDefinition) ParentType() Type { func (fd *fieldDefinition) Inverse() FieldDefinition { - invDirective := fd.fieldDef.Directives.ForName(inverseDirective) - if invDirective == nil { + if fd.fieldDef == nil { return nil } - invFieldArg := invDirective.Arguments.ForName(inverseArg) - if invFieldArg == nil { - return nil // really not possible - } + invDirective := fd.fieldDef.Directives.ForName(inverseDirective) + if invDirective != nil { - typeWrapper := fd.Type() - // typ must exist if the schema passed GQL validation - typ := fd.inSchema.schema.Types[typeWrapper.Name()] + invFieldArg := invDirective.Arguments.ForName(inverseArg) + if invFieldArg == nil { + return nil // really not possible + } - // fld must exist if the schema passed our validation - fld := typ.Fields.ForName(invFieldArg.Value.Raw) + typeWrapper := fd.Type() + // typ must exist if the schema passed GQL validation + typ := fd.inSchema.schema.Types[typeWrapper.Name()] - return &fieldDefinition{ - fieldDef: fld, - inSchema: fd.inSchema, - dgraphPredicate: fd.dgraphPredicate, - parentType: typeWrapper, + // fld must exist if the schema passed our validation + fld := typ.Fields.ForName(invFieldArg.Value.Raw) + + return &fieldDefinition{ + fieldDef: fld, + inSchema: fd.inSchema, + dgraphPredicate: fd.dgraphPredicate, + parentType: typeWrapper, + } + } else { + // also check the inverse type especially when querying from an interface + // and not the implemented type. In this case the interface won't have the + // inverse field. + typeWrapper := fd.Type() + // typ must exist if the schema passed GQL validation + typ := fd.inSchema.schema.Types[typeWrapper.Name()] + + for _, refField := range typ.Fields { + + refFieldType := fd.inSchema.schema.Types[refField.Type.Name()] + if refField.Type.Name() != typ.Name && !fd.inSchema.schema.Types[fd.ParentType().Name()].OneOf(refFieldType.Interfaces...) { + continue + } + + refFieldDir := refField.Directives.ForName(inverseDirective) + if refFieldDir == nil { + continue + } + + invField := refFieldDir.Arguments.ForName("field") + if invField == nil { + continue + } + + invFieldName := invField.Value.Raw + if invFieldName == fd.Name() { + return &fieldDefinition{ + fieldDef: refField, + inSchema: fd.inSchema, + dgraphPredicate: fd.dgraphPredicate, + parentType: typeWrapper, + } + } + } } + return nil } func (fd *fieldDefinition) WithMemberType(memberType string) FieldDefinition { From 3544b65fb7f11c6059e4386d3f0280be6cbf50af Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Fri, 14 Jun 2024 00:34:01 +1000 Subject: [PATCH 08/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(query=5Frewriter.go?= =?UTF-8?q?):=20Refactor=20nested=20object=20filter=20building=20logic=20f?= =?UTF-8?q?or=20readability=20=E2=9C=A8=20(query=5Ftest.yaml):=20Add=20tes?= =?UTF-8?q?t=20cases=20for=20deeply=20nested=20object=20queries=20and=20AN?= =?UTF-8?q?D/OR=20conditions=20=F0=9F=90=9B=20(gqlschema.go,=20rules.go,?= =?UTF-8?q?=20wrappers.go):=20Fix=20custom=20type=20detection=20by=20passi?= =?UTF-8?q?ng=20type=20instead=20of=20field=20definition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphql/resolve/query_rewriter.go | 56 +++++++++++++++------------- graphql/resolve/query_test.yaml | 62 +++++++++++++++++++++++++++++-- graphql/schema/gqlschema.go | 2 +- graphql/schema/rules.go | 2 +- graphql/schema/wrappers.go | 8 ++-- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 049167d118f..29b2e7ef306 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -2067,35 +2067,39 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin // } // root() @filter(var(nested_field_name)) fd := typ.Field(field) - if fd != nil && fd.HasSearchDirective() && fd.Inverse() != nil { - fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), qn) - queries = append(queries, qs...) + if fd != nil && fd.HasSearchDirective() { - // add the uids of the nested object - ands = append(ands, &dql.FilterTree{ - Op: "and", - Child: []*dql.FilterTree{{ + if inv := fd.Inverse(); inv != nil { + + fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), qn) + queries = append(queries, qs...) + + // add the uids of the nested object + ands = append(ands, &dql.FilterTree{ + Op: "and", + Child: []*dql.FilterTree{{ + Func: &dql.Function{ + Name: "uid", + Args: []dql.Arg{{Value: qn}}, + }, + }}, + }) + + // generate filter var query for nested object + queries = append(queries, &dql.GraphQuery{ + Attr: "var", Func: &dql.Function{ - Name: "uid", - Args: []dql.Arg{{Value: qn}}, + Name: "type", + Args: []dql.Arg{{Value: fd.Type().Name()}}, }, - }}, - }) - - // generate filter var query for nested object - queries = append(queries, &dql.GraphQuery{ - Attr: "var", - Func: &dql.Function{ - Name: "type", - Args: []dql.Arg{{Value: fd.Type().Name()}}, - }, - Filter: fil, - Children: []*dql.GraphQuery{{ - Attr: fd.Inverse().DgraphPredicate(), - Var: qn, - }}, - }) - continue + Filter: fil, + Children: []*dql.GraphQuery{{ + Attr: inv.DgraphPredicate(), + Var: qn, + }}, + }) + continue + } } //// It's a base case like: diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index 8274c824743..6211434e16b 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -3576,7 +3576,63 @@ queryNested_Z_x as Nested_X.y } } +- name: "query deeply nested object" + gqlquery: | + query { + queryNested_Z(filter: { x: { y: {s: {eq: ""}}}}) { + x { s } + } + } + dgquery: |- + query { + queryNested_Z(func: type(Nested_Z)) @filter((uid(queryNested_Z_x))) { + dgraph.type + Nested_Z.x : Nested_Z.x { + Nested_X.s : Nested_X.s + dgraph.uid : uid + } + dgraph.uid : uid + } + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + queryNested_Z_x_y as Nested_Z.x + } + var(func: type(Nested_X)) @filter((uid(queryNested_Z_x_y))) { + queryNested_Z_x as Nested_X.y + } + } + +- name: "query nested with AND condition" + gqlquery: | + query { + queryNested_X(filter: {s: {eq: ""}, and: { y: {s: {eq: ""}}}}) { + s + } + } + dgquery: |- + query { + queryNested_X(func: type(Nested_X)) @filter(((uid(queryNested_X_and_y)) AND eq(Nested_X.s, ""))) { + Nested_X.s : Nested_X.s + dgraph.uid : uid + } + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + queryNested_X_and_y as Nested_Z.x + } + } -# query deeply nested -# query nested with OR condition -# query nested with AND condition \ No newline at end of file +- name: "query nested with OR condition" + gqlquery: | + query { + queryNested_X(filter: {s: {eq: ""}, or: { y: {s: {eq: ""}}}}) { + s + } + } + dgquery: |- + query { + queryNested_X(func: type(Nested_X)) @filter((eq(Nested_X.s, "") OR ((uid(queryNested_X_or_y))))) { + Nested_X.s : Nested_X.s + dgraph.uid : uid + } + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + queryNested_X_or_y as Nested_Z.x + } + } \ No newline at end of file diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 4496c5d4c6c..0ec93ab3bc6 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -1475,7 +1475,7 @@ func getFilterTypes(schema *ast.Schema, fld *ast.FieldDefinition, filterName str // Return the object filter if the field is an object that is searchable. fldType := schema.Types[fld.Type.Name()] - if isCustomType(schema, fld) && hasFilterable(fldType) && hasSearchDirective(fld) { + if isCustomType(schema, fld.Type) && hasFilterable(fldType) && hasSearchDirective(fld) { return []string{fld.Type.Name() + "Filter"} } diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index 60df640e01a..48a3e9a3c4d 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -1078,7 +1078,7 @@ func searchValidation( // If the field is an object, it is require to have an inverse edge for filtering. // It's not enough to just check for the @hasInverse directive as it // may be defined in the inverse type. - if isCustomType(sch, field) { + if isCustomType(sch, field.Type) { if !hasInverse(sch, typ, field) { errs = append(errs, gqlerror.ErrorPosf( dir.Position, diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index fa869b5fc34..658500a92e2 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -2444,10 +2444,10 @@ func hasInverse(sch *ast.Schema, typ *ast.Definition, fd *ast.FieldDefinition) b return false } -func isCustomType(sch *ast.Schema, fd *ast.FieldDefinition) bool { - _, ok := inbuiltTypeToDgraph[fd.Type.Name()] - return !ok && (sch.Types[fd.Type.Name()].Kind == ast.Object || - sch.Types[fd.Type.Name()].Kind == ast.Interface) +func isCustomType(sch *ast.Schema, t *ast.Type) bool { + _, ok := inbuiltTypeToDgraph[t.Name()] + return !ok && (sch.Types[t.Name()].Kind == ast.Object || + sch.Types[t.Name()].Kind == ast.Interface) } func isID(fd *ast.FieldDefinition) bool { From 3a5f682814ecbd41f3b7ba6d176d76e5bfba725e Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Fri, 14 Jun 2024 00:54:21 +1000 Subject: [PATCH 09/13] =?UTF-8?q?=E2=9C=A8=20(query=5Ftest.yaml):=20Add=20?= =?UTF-8?q?new=20test=20case=20"query=20nested=20with=20aggregate=20functi?= =?UTF-8?q?on"=20to=20increase=20test=20coverage=20and=20ensure=20the=20ag?= =?UTF-8?q?gregate=20function=20works=20as=20expected=20in=20nested=20quer?= =?UTF-8?q?ies.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphql/resolve/query_test.yaml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index 6211434e16b..fe93d817b3c 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -3635,4 +3635,24 @@ var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { queryNested_X_or_y as Nested_Z.x } - } \ No newline at end of file + } + +- name: "query nested with aggregate function" + gqlquery: | + query { + aggregateNested_X(filter: {s: {eq: ""}, or: { y: {s: {eq: ""}}}}) { + sMax + } + } + dgquery: |- + query { + aggregateNested_X() { + Nested_XAggregateResult.sMax : max(val(sVar)) + } + var(func: type(Nested_X)) @filter((eq(Nested_X.s, "") OR ((uid(aggregateNested_X_or_y))))) { + sVar as Nested_X.s + } + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + aggregateNested_X_or_y as Nested_Z.x + } + } From a64fea4b675bb4b4f6f36747c4d601f8c1b0e42f Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Sat, 15 Jun 2024 01:30:04 +1000 Subject: [PATCH 10/13] =?UTF-8?q?=E2=9C=A8=20(schema.graphql):=20Add=20Nes?= =?UTF-8?q?ted=5FX,=20Nested=5FY,=20and=20Nested=5FZ=20types=20to=20suppor?= =?UTF-8?q?t=20nested=20filtering=20=E2=9C=A8=20(auth=5Fquery=5Ftest.yaml)?= =?UTF-8?q?:=20Add=20test=20case=20for=20nested=20filter=20query=20?= =?UTF-8?q?=F0=9F=90=9B=20(mutation=5Frewriter.go):=20Update=20rewriteAsQu?= =?UTF-8?q?eryByIds=20and=20addFilter=20functions=20to=20include=20mutatio?= =?UTF-8?q?n=20alias=20for=20better=20query=20identification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (query_rewriter.go): Refactor filterQueries to varQry for better semantics ✨ (query_rewriter.go): Add queryName parameter to rewriteAsQuery and addArgumentsToField functions to support query aliasing 🐛 (query_rewriter.go): Fix issue where only the first query was returned in rewriteRuleNode, now returns all queries including nested var queries --- graphql/e2e/auth/schema.graphql | 33 +++++++++++++++++ graphql/resolve/auth_query_test.yaml | 37 +++++++++++++++++++ graphql/resolve/mutation_rewriter.go | 10 ++--- graphql/resolve/query_rewriter.go | 55 +++++++++++++++------------- 4 files changed, 104 insertions(+), 31 deletions(-) diff --git a/graphql/e2e/auth/schema.graphql b/graphql/e2e/auth/schema.graphql index 4148b373968..a174821fd15 100644 --- a/graphql/e2e/auth/schema.graphql +++ b/graphql/e2e/auth/schema.graphql @@ -974,3 +974,36 @@ type Home @auth( favouriteMember: HomeMember } # union testing - end + +""" +This types are used to validate nested filting. +""" +type Nested_X @auth( + # only return homes with either: + # 1. a Dog member which has something to eat + # 2. or a Plant member + query: { or: [{ rule: """ + query { + queryNested_X(filter: {b: true, and: {y: {s: {eq: ""}}}}){ + __typename + } + } + """}, { rule: """ + query { + queryNested_X(filter: {b: true, and: {y: {s: {eq: ""}}}}){ + __typename + } + } + """}]} +) { + b: Boolean @search + y: Nested_Y @hasInverse(field: x) @search +} + +type Nested_Y implements Nested_Z{ + s: String @search(by: [hash]) +} + +interface Nested_Z { + x: Nested_X @search +} diff --git a/graphql/resolve/auth_query_test.yaml b/graphql/resolve/auth_query_test.yaml index a0083d44266..bbd6ae64eec 100644 --- a/graphql/resolve/auth_query_test.yaml +++ b/graphql/resolve/auth_query_test.yaml @@ -2148,3 +2148,40 @@ Person.id : uid } } + +- + name: "Query auth rules with nested filter" + gqlquery: | + query{ + queryNested_X{ + b + y { s } + } + } + jwtvar: + USER: "0x5" + dgquery: |- + query { + queryNested_X(func: uid(Nested_XRoot)) { + Nested_X.b : Nested_X.b + Nested_X.y : Nested_X.y @filter(uid(Nested_Y_1)) { + Nested_Y.s : Nested_Y.s + dgraph.uid : uid + } + dgraph.uid : uid + } + Nested_XRoot as var(func: uid(Nested_X_3)) @filter((uid(Nested_X_Auth4) OR uid(Nested_X_Auth5))) + Nested_X_3 as var(func: type(Nested_X)) + Nested_X_Auth4 as var(func: uid(Nested_X_3)) @filter(((uid(Nested_X_Auth4_and_y)) AND eq(Nested_X.b, true))) @cascade + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + Nested_X_Auth4_and_y as Nested_Z.x + } + Nested_X_Auth5 as var(func: uid(Nested_X_3)) @filter(((uid(Nested_X_Auth5_and_y)) AND eq(Nested_X.b, true))) @cascade + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { + Nested_X_Auth5_and_y as Nested_Z.x + } + var(func: uid(Nested_XRoot)) { + Nested_Y_2 as Nested_X.y + } + Nested_Y_1 as var(func: uid(Nested_Y_2)) + } diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index 9e53bdcfc4f..8aba0979580 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -807,7 +807,7 @@ func (arw *AddRewriter) FromMutationResult( return nil, errs } // No errors are thrown while rewriting queries by Ids. - return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), nil + return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw, mutation.Alias()), nil } // FromMutationResult rewrites the query part of a GraphQL update mutation into a Dgraph query. @@ -843,7 +843,7 @@ func (urw *UpdateRewriter) FromMutationResult( parentVarName: mutation.MutatedType().Name() + "Root", } authRw.hasAuthRules = hasAuthRules(mutation.QueryField(), authRw) - return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), nil + return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw, mutation.Alias()), nil } func (arw *AddRewriter) MutatedRootUIDs( @@ -996,8 +996,8 @@ func RewriteUpsertQueryFromMutation( addTypeFunc(dgQuery[0], m.MutatedType().DgraphName()) } - _, filterQueries := addFilter(dgQuery[0], m.MutatedType(), filter, m.Alias()) - dgQuery = append(dgQuery, filterQueries...) + _, varQry := addFilter(dgQuery[0], m.MutatedType(), filter, m.Alias()) + dgQuery = append(dgQuery, varQry...) } else { // It means this is called from upsert with Add mutation. // nodeID will be uid of the node to be upserted. We add UID func @@ -1114,7 +1114,7 @@ func (drw *deleteRewriter) Rewrite( } // these queries are responsible for querying the queryField - queryFieldQry := rewriteAsQuery(queryField, queryAuthRw) + queryFieldQry := rewriteAsQuery(queryField, queryAuthRw, MutationQueryVar) // we don't want the `x` query to show up in GraphQL JSON response while querying the query // field. So, need to make it `var` query and remove any children from it as there can be diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 29b2e7ef306..ef88c5910fe 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -157,7 +157,7 @@ func (qr *queryRewriter) Rewrite( case schema.SimilarByEmbeddingQuery: return rewriteAsSimilarByEmbeddingQuery(gqlQuery, authRw), nil case schema.FilterQuery: - return rewriteAsQuery(gqlQuery, authRw), nil + return rewriteAsQuery(gqlQuery, authRw, gqlQuery.Alias()), nil case schema.PasswordQuery: return passwordQuery(gqlQuery, authRw) case schema.AggregateQuery: @@ -257,8 +257,8 @@ func aggregateQuery(query schema.Query, authRw *authRewriter) []*dql.GraphQuery // Add filter filter, _ := query.ArgValue("filter").(map[string]interface{}) - _, filterQueries := addFilter(dgQuery[0], mainType, filter, query.Alias()) - dgQuery = append(dgQuery, filterQueries...) + _, varQry := addFilter(dgQuery[0], mainType, filter, query.Alias()) + dgQuery = append(dgQuery, varQry...) dgQuery = authRw.addAuthQueries(mainType, dgQuery, rbac) @@ -452,7 +452,8 @@ func addUID(dgQuery *dql.GraphQuery) { func rewriteAsQueryByIds( field schema.Field, uids []uint64, - authRw *authRewriter) []*dql.GraphQuery { + authRw *authRewriter, + queryName string) []*dql.GraphQuery { if field == nil { return nil } @@ -476,7 +477,7 @@ func rewriteAsQueryByIds( addUIDFunc(dgQuery[0], intersection(ids, uids)) } - includedQueries := addArgumentsToField(dgQuery[0], field) + includedQueries := addArgumentsToField(dgQuery[0], field, queryName) dgQuery = append(dgQuery, includedQueries...) // The function getQueryByIds is called for passwordQuery or fetching query result types @@ -503,12 +504,12 @@ func rewriteAsQueryByIds( // addArgumentsToField adds various different arguments to a field, such as // filter, order and pagination. -func addArgumentsToField(dgQuery *dql.GraphQuery, field schema.Field) []*dql.GraphQuery { +func addArgumentsToField(dgQuery *dql.GraphQuery, field schema.Field, queryName string) []*dql.GraphQuery { filter, _ := field.ArgValue("filter").(map[string]interface{}) - _, filterQueries := addFilter(dgQuery, field.Type(), filter, field.Alias()) + _, varQry := addFilter(dgQuery, field.Type(), filter, queryName) addOrder(dgQuery, field) addPagination(dgQuery, field) - return filterQueries + return varQry } func addTopLevelTypeFilter(query *dql.GraphQuery, field schema.Field) { @@ -548,7 +549,7 @@ func rewriteAsGet( } if len(xidArgToVal) == 0 { - dgQuery = rewriteAsQueryByIds(query, []uint64{uid}, auth) + dgQuery = rewriteAsQueryByIds(query, []uint64{uid}, auth, query.Alias()) // Add the type filter to the top level get query. When the auth has been written into the // query the top level get query may be present in query's children. @@ -777,7 +778,7 @@ func rewriteAsSimilarByIdQuery( }, Order: []*pb.Order{{Attr: "val(distance)", Desc: false}}, } - addArgumentsToField(sortQuery, query) + addArgumentsToField(sortQuery, query, query.Alias()) dgQuery = append(dgQuery, aggQuery, similarQuery, sortQuery) return dgQuery @@ -806,7 +807,7 @@ func rewriteAsSimilarByIdQuery( func rewriteAsSimilarByEmbeddingQuery( query schema.Query, auth *authRewriter) []*dql.GraphQuery { - dgQuery := rewriteAsQuery(query, auth) + dgQuery := rewriteAsQuery(query, auth, query.Alias()) // Remember dgQuery[0].Children as result type for the last block // in the rewritten query @@ -954,14 +955,14 @@ func addCommonRules( return []*dql.GraphQuery{dgQuery}, rbac } -func rewriteAsQuery(field schema.Field, authRw *authRewriter) []*dql.GraphQuery { +func rewriteAsQuery(field schema.Field, authRw *authRewriter, queryName string) []*dql.GraphQuery { dgQuery, rbac := addCommonRules(field, field.Type(), authRw) if rbac == schema.Negative { return dgQuery } - includedQueries := addArgumentsToField(dgQuery[0], field) - dgQuery = append(dgQuery, includedQueries...) + varQry := addArgumentsToField(dgQuery[0], field, queryName) + dgQuery = append(dgQuery, varQry...) selectionAuth := addSelectionSetFrom(dgQuery[0], field, authRw) // we don't need to query uid for auth queries, as they always have at least one field in their @@ -1326,14 +1327,15 @@ func (authRw *authRewriter) rewriteRuleNode( // build // Todo2 as var(func: uid(Todo1)) @cascade { ...auth query 1... } varName := authRw.varGen.Next(typ, "", "", authRw.isWritingAuth) - r1 := rewriteAsQuery(qry, authRw) + r1 := rewriteAsQuery(qry, authRw, varName) r1[0].Var = varName r1[0].Attr = "var" if len(r1[0].Cascade) == 0 { r1[0].Cascade = append(r1[0].Cascade, "__all__") } - return []*dql.GraphQuery{r1[0]}, &dql.FilterTree{ + // return all queries, including the nested var queries. + return r1, &dql.FilterTree{ Func: &dql.Function{ Name: "uid", Args: []dql.Arg{{Value: varName}}, @@ -1470,7 +1472,7 @@ func buildAggregateFields( // Filter for aggregate Fields. This is added to all count aggregate fields // and mainField fieldFilter, _ := f.ArgValue("filter").(map[string]interface{}) - _, filterQueries := addFilter(mainField, constructedForType, fieldFilter, f.Alias()) + _, varQry := addFilter(mainField, constructedForType, fieldFilter, f.Alias()) // Add type filter in case the Dgraph predicate for which the aggregate // field belongs to is a reverse edge @@ -1602,7 +1604,7 @@ func buildAggregateFields( // not added to them. aggregateChildren = append(aggregateChildren, otherAggregateChildren...) retAuthQueries = append(retAuthQueries, fieldAuth...) - retAuthQueries = append(retAuthQueries, filterQueries...) + retAuthQueries = append(retAuthQueries, varQry...) return aggregateChildren, retAuthQueries } @@ -1687,12 +1689,12 @@ func addSelectionSetFrom( filter, _ := f.ArgValue("filter").(map[string]interface{}) // if this field has been filtered out by the filter, then don't add it in DQL query - includeField, filterQueries := addFilter(child, f.Type(), filter, field.Alias()) + includeField, varQry := addFilter(child, f.Type(), filter, f.Alias()) if !includeField { continue } - authQueries = append(authQueries, filterQueries...) + authQueries = append(authQueries, varQry...) // Add type filter in case the Dgraph predicate is a reverse edge if strings.HasPrefix(f.DgraphPredicate(), "~") { @@ -1900,10 +1902,10 @@ func idFilter(filter map[string]interface{}, idField schema.FieldDefinition) []u // Currently, it would only be false for a union field when no memberTypes are queried. func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{}, queryName string) (bool, []*dql.GraphQuery) { - filterQueries := []*dql.GraphQuery{} + varQry := []*dql.GraphQuery{} if len(filter) == 0 { - return true, filterQueries + return true, varQry } // There are two cases here. @@ -1925,18 +1927,19 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} } if typ.IsUnion() { - if filter, filterQueries, includeField := buildUnionFilter(typ, filter, queryName); includeField { + if filter, varq, includeField := buildUnionFilter(typ, filter, queryName); includeField { q.Filter = filter + varQry = varq } else { - return false, filterQueries + return false, varQry } } else { - q.Filter, filterQueries = buildFilter(typ, filter, queryName) + q.Filter, varQry = buildFilter(typ, filter, queryName) } if filterAtRoot { addTypeFilter(q, typ) } - return true, filterQueries + return true, varQry } // buildFilter builds a Dgraph dql.FilterTree from a GraphQL 'filter' arg. From 00e48767f6909aa2488f53340d1f39bf5ecdcee8 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Tue, 18 Jun 2024 23:51:10 +1000 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=90=9B=20(schema.graphql):=20Update?= =?UTF-8?q?=20Nested=5FX=20and=20Nested=5FY=20types=20to=20fix=20query=20r?= =?UTF-8?q?ules=20for=20more=20test=20coverage=20=E2=9C=A8=20(auth=5Fdelet?= =?UTF-8?q?e=5Ftest.yaml):=20Add=20new=20test=20case=20for=20delete=20sele?= =?UTF-8?q?ction=20by=20nested=20filter=20=E2=9C=A8=20(auth=5Fquery=5Ftest?= =?UTF-8?q?.yaml):=20Update=20existing=20test=20case=20and=20add=20new=20o?= =?UTF-8?q?nes=20for=20positive,=20negative,=20and=20uncertain=20auth=20ru?= =?UTF-8?q?les=20with=20nested=20filter=20=F0=9F=90=9B=20(mutation=5Frewri?= =?UTF-8?q?ter.go):=20Fix=20addFilter=20function=20call=20to=20include=20a?= =?UTF-8?q?uthRw=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (query_rewriter.go): Refactor addFilter and buildFilter functions to include authRewriter parameter for better access control 💡 (query_rewriter.go): Update function calls to addFilter and buildFilter to pass authRewriter parameter for improved security ♻️ (query_rewriter.go): Refactor variable name from 'queries' to 'varQry' for better readability ✨ (query_rewriter.go): Add 'auth' parameter to 'buildFilter' and 'buildUnionFilter' functions to support authorization rewriting in queries --- graphql/e2e/auth/schema.graphql | 33 ++++--- graphql/resolve/auth_delete_test.yaml | 27 ++++++ graphql/resolve/auth_query_test.yaml | 94 +++++++++++++++++--- graphql/resolve/mutation_rewriter.go | 2 +- graphql/resolve/query_rewriter.go | 118 +++++++++++++++++--------- 5 files changed, 210 insertions(+), 64 deletions(-) diff --git a/graphql/e2e/auth/schema.graphql b/graphql/e2e/auth/schema.graphql index a174821fd15..ddfe04675ef 100644 --- a/graphql/e2e/auth/schema.graphql +++ b/graphql/e2e/auth/schema.graphql @@ -979,28 +979,35 @@ type Home @auth( This types are used to validate nested filting. """ type Nested_X @auth( - # only return homes with either: - # 1. a Dog member which has something to eat - # 2. or a Plant member - query: { or: [{ rule: """ - query { - queryNested_X(filter: {b: true, and: {y: {s: {eq: ""}}}}){ - __typename - } - } - """}, { rule: """ + query: { rule: """ query { - queryNested_X(filter: {b: true, and: {y: {s: {eq: ""}}}}){ + queryNested_X(filter: {y: {s: {eq: "y"}}}){ __typename } } - """}]} + """} ) { b: Boolean @search y: Nested_Y @hasInverse(field: x) @search } -type Nested_Y implements Nested_Z{ +type Nested_Y implements Nested_Z @auth( + query: { + or: [ + { rule: "{ $rbac: { eq: \"positive\" } }" } + { and: [ + { rule: "{ $rbac: { eq: \"uncertain\" } }" } + { rule: """ + query { + queryNested_Y(filter: {x: {b: true}}){ + __typename + } + } + """ + } + ] + }] + }) { s: String @search(by: [hash]) } diff --git a/graphql/resolve/auth_delete_test.yaml b/graphql/resolve/auth_delete_test.yaml index 66d7b97f361..c28d0b6d3e9 100644 --- a/graphql/resolve/auth_delete_test.yaml +++ b/graphql/resolve/auth_delete_test.yaml @@ -880,3 +880,30 @@ } } +- name: "Delete selection by nested filter" + gqlquery: | + mutation { + deleteNested_X(filter: {y: {s: {eq: "-"}}}) { + numUids + } + } + variables: + jwtvar: + dgmutations: + - deletejson: | + [{ + "uid": "uid(x)" + },{ + "Nested_Z.x": {"uid": "uid(x)"}, + "uid": "uid(Nested_Y_3)" + }] + dgquery: |- + query { + x as deleteNested_X(func: type(Nested_X)) @filter((uid(deleteNested_X_y))) { + uid + Nested_Y_3 as Nested_X.y + } + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "-")) { + deleteNested_X_y as Nested_Z.x + } + } diff --git a/graphql/resolve/auth_query_test.yaml b/graphql/resolve/auth_query_test.yaml index bbd6ae64eec..b71ca86f787 100644 --- a/graphql/resolve/auth_query_test.yaml +++ b/graphql/resolve/auth_query_test.yaml @@ -2150,16 +2150,16 @@ } - - name: "Query auth rules with nested filter" + name: "Query positive auth rules with nested filter" gqlquery: | query{ - queryNested_X{ + queryNested_X(filter: {y: {s: {eq: "-"}}}) { b y { s } } } jwtvar: - USER: "0x5" + rbac: positive dgquery: |- query { queryNested_X(func: uid(Nested_XRoot)) { @@ -2170,18 +2170,90 @@ } dgraph.uid : uid } - Nested_XRoot as var(func: uid(Nested_X_3)) @filter((uid(Nested_X_Auth4) OR uid(Nested_X_Auth5))) - Nested_X_3 as var(func: type(Nested_X)) - Nested_X_Auth4 as var(func: uid(Nested_X_3)) @filter(((uid(Nested_X_Auth4_and_y)) AND eq(Nested_X.b, true))) @cascade - var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { - Nested_X_Auth4_and_y as Nested_Z.x + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "-")) { + queryNested_X_y as Nested_Z.x } - Nested_X_Auth5 as var(func: uid(Nested_X_3)) @filter(((uid(Nested_X_Auth5_and_y)) AND eq(Nested_X.b, true))) @cascade - var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "")) { - Nested_X_Auth5_and_y as Nested_Z.x + Nested_XRoot as var(func: uid(Nested_X_3)) @filter(uid(Nested_X_Auth4)) + Nested_X_3 as var(func: type(Nested_X)) @filter((uid(queryNested_X_y))) + Nested_X_Auth4 as var(func: uid(Nested_X_3)) @filter((uid(Nested_X_Auth4_y))) @cascade + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "y")) { + Nested_X_Auth4_y as Nested_Z.x } var(func: uid(Nested_XRoot)) { Nested_Y_2 as Nested_X.y } Nested_Y_1 as var(func: uid(Nested_Y_2)) } + +- + name: "Query negative auth rules with nested filter" + gqlquery: | + query{ + queryNested_X(filter: {y: {s: {eq: "-"}}}) { + b + y { s } + } + } + jwtvar: + rbac: negative + dgquery: |- + query { + queryNested_X(func: uid(Nested_XRoot)) { + Nested_X.b : Nested_X.b + dgraph.uid : uid + } + queryNested_X_y as var() + Nested_XRoot as var(func: uid(Nested_X_3)) @filter(uid(Nested_X_Auth4)) + Nested_X_3 as var(func: type(Nested_X)) @filter((uid(queryNested_X_y))) + Nested_X_Auth4 as var(func: uid(Nested_X_3)) @filter((uid(Nested_X_Auth4_y))) @cascade + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "y")) { + Nested_X_Auth4_y as Nested_Z.x + } + } + +- + name: "Query uncertain auth rules with nested filter" + gqlquery: | + query{ + queryNested_X(filter: {y: {s: {eq: "-"}}}) { + b + y { s } + } + } + jwtvar: + rbac: uncertain + dgquery: |- + query { + queryNested_X(func: uid(Nested_XRoot)) { + Nested_X.b : Nested_X.b + Nested_X.y : Nested_X.y @filter(uid(Nested_Y_3)) { + Nested_Y.s : Nested_Y.s + dgraph.uid : uid + } + dgraph.uid : uid + } + var(func: uid(queryNested_X_yRoot)) { + queryNested_X_y as Nested_Z.x + } + queryNested_X_yRoot as var(func: uid(Nested_Y_1)) @filter(uid(Nested_Y_Auth2)) + Nested_Y_1 as var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "-")) + Nested_Y_Auth2 as var(func: uid(Nested_Y_1)) @filter((uid(Nested_Y_Auth2_x))) @cascade + var(func: type(Nested_X)) @filter(eq(Nested_X.b, true)) { + Nested_Y_Auth2_x as Nested_X.y + } + Nested_XRoot as var(func: uid(Nested_X_6)) @filter(uid(Nested_X_Auth7)) + Nested_X_6 as var(func: type(Nested_X)) @filter((uid(queryNested_X_y))) + Nested_X_Auth7 as var(func: uid(Nested_X_6)) @filter((uid(Nested_X_Auth7_y))) @cascade + var(func: type(Nested_Y)) @filter(eq(Nested_Y.s, "y")) { + Nested_X_Auth7_y as Nested_Z.x + } + var(func: uid(Nested_XRoot)) { + Nested_Y_4 as Nested_X.y + } + Nested_Y_3 as var(func: uid(Nested_Y_4)) @filter(uid(Nested_Y_Auth5)) + Nested_Y_Auth5 as var(func: uid(Nested_Y_4)) @filter((uid(Nested_Y_Auth5_x))) @cascade + var(func: type(Nested_X)) @filter(eq(Nested_X.b, true)) { + Nested_Y_Auth5_x as Nested_X.y + } + } + \ No newline at end of file diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index 8aba0979580..01fca5d91e2 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -996,7 +996,7 @@ func RewriteUpsertQueryFromMutation( addTypeFunc(dgQuery[0], m.MutatedType().DgraphName()) } - _, varQry := addFilter(dgQuery[0], m.MutatedType(), filter, m.Alias()) + _, varQry := addFilter(dgQuery[0], m.MutatedType(), filter, authRw, m.Alias()) dgQuery = append(dgQuery, varQry...) } else { // It means this is called from upsert with Add mutation. diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index ef88c5910fe..bd24994973e 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -257,7 +257,7 @@ func aggregateQuery(query schema.Query, authRw *authRewriter) []*dql.GraphQuery // Add filter filter, _ := query.ArgValue("filter").(map[string]interface{}) - _, varQry := addFilter(dgQuery[0], mainType, filter, query.Alias()) + _, varQry := addFilter(dgQuery[0], mainType, filter, authRw, query.Alias()) dgQuery = append(dgQuery, varQry...) dgQuery = authRw.addAuthQueries(mainType, dgQuery, rbac) @@ -477,7 +477,7 @@ func rewriteAsQueryByIds( addUIDFunc(dgQuery[0], intersection(ids, uids)) } - includedQueries := addArgumentsToField(dgQuery[0], field, queryName) + includedQueries := addArgumentsToField(dgQuery[0], field, authRw, queryName) dgQuery = append(dgQuery, includedQueries...) // The function getQueryByIds is called for passwordQuery or fetching query result types @@ -504,9 +504,12 @@ func rewriteAsQueryByIds( // addArgumentsToField adds various different arguments to a field, such as // filter, order and pagination. -func addArgumentsToField(dgQuery *dql.GraphQuery, field schema.Field, queryName string) []*dql.GraphQuery { +func addArgumentsToField(dgQuery *dql.GraphQuery, + field schema.Field, + auth *authRewriter, + queryName string) []*dql.GraphQuery { filter, _ := field.ArgValue("filter").(map[string]interface{}) - _, varQry := addFilter(dgQuery, field.Type(), filter, queryName) + _, varQry := addFilter(dgQuery, field.Type(), filter, auth, queryName) addOrder(dgQuery, field) addPagination(dgQuery, field) return varQry @@ -778,7 +781,7 @@ func rewriteAsSimilarByIdQuery( }, Order: []*pb.Order{{Attr: "val(distance)", Desc: false}}, } - addArgumentsToField(sortQuery, query, query.Alias()) + addArgumentsToField(sortQuery, query, auth, query.Alias()) dgQuery = append(dgQuery, aggQuery, similarQuery, sortQuery) return dgQuery @@ -961,7 +964,7 @@ func rewriteAsQuery(field schema.Field, authRw *authRewriter, queryName string) return dgQuery } - varQry := addArgumentsToField(dgQuery[0], field, queryName) + varQry := addArgumentsToField(dgQuery[0], field, authRw, queryName) dgQuery = append(dgQuery, varQry...) selectionAuth := addSelectionSetFrom(dgQuery[0], field, authRw) @@ -1472,7 +1475,7 @@ func buildAggregateFields( // Filter for aggregate Fields. This is added to all count aggregate fields // and mainField fieldFilter, _ := f.ArgValue("filter").(map[string]interface{}) - _, varQry := addFilter(mainField, constructedForType, fieldFilter, f.Alias()) + _, varQry := addFilter(mainField, constructedForType, fieldFilter, auth, f.Alias()) // Add type filter in case the Dgraph predicate for which the aggregate // field belongs to is a reverse edge @@ -1496,7 +1499,7 @@ func buildAggregateFields( Attr: "count(" + constructedForDgraphPredicate + ")", } // Add filter to count aggregation field. - addFilter(aggregateChild, constructedForType, fieldFilter, f.Alias()) + addFilter(aggregateChild, constructedForType, fieldFilter, auth, f.Alias()) // Add type filter in case the Dgraph predicate for which the aggregate // field belongs to is a reverse edge @@ -1689,7 +1692,7 @@ func addSelectionSetFrom( filter, _ := f.ArgValue("filter").(map[string]interface{}) // if this field has been filtered out by the filter, then don't add it in DQL query - includeField, varQry := addFilter(child, f.Type(), filter, f.Alias()) + includeField, varQry := addFilter(child, f.Type(), filter, auth, f.Alias()) if !includeField { continue } @@ -1900,7 +1903,11 @@ func idFilter(filter map[string]interface{}, idField schema.FieldDefinition) []u // addFilter adds a filter to the input DQL query. It returns false if the field for which the // filter was specified should not be included in the DQL query. // Currently, it would only be false for a union field when no memberTypes are queried. -func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{}, queryName string) (bool, []*dql.GraphQuery) { +func addFilter(q *dql.GraphQuery, + typ schema.Type, + filter map[string]interface{}, + auth *authRewriter, + queryName string) (bool, []*dql.GraphQuery) { varQry := []*dql.GraphQuery{} @@ -1927,14 +1934,14 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} } if typ.IsUnion() { - if filter, varq, includeField := buildUnionFilter(typ, filter, queryName); includeField { + if filter, varq, includeField := buildUnionFilter(typ, filter, auth, queryName); includeField { q.Filter = filter varQry = varq } else { return false, varQry } } else { - q.Filter, varQry = buildFilter(typ, filter, queryName) + q.Filter, varQry = buildFilter(typ, filter, auth, queryName) } if filterAtRoot { addTypeFilter(q, typ) @@ -1970,9 +1977,12 @@ func addFilter(q *dql.GraphQuery, typ schema.Type, filter map[string]interface{} // ATM those will probably generate junk that might cause a Dgraph error. And // bubble back to the user as a GraphQL error when the query fails. Really, // they should fail query validation and never get here. -func buildFilter(typ schema.Type, filter map[string]interface{}, queryName string) (*dql.FilterTree, []*dql.GraphQuery) { +func buildFilter(typ schema.Type, + filter map[string]interface{}, + auth *authRewriter, + queryName string) (*dql.FilterTree, []*dql.GraphQuery) { - var queries []*dql.GraphQuery + var varQry []*dql.GraphQuery var ands []*dql.FilterTree var or *dql.FilterTree // Get a stable ordering so we generate the same thing each time. @@ -2010,14 +2020,14 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin // ... and: [{}] switch v := filter[field].(type) { case map[string]interface{}: - ft, qs := buildFilter(typ, v, qn) + ft, qs := buildFilter(typ, v, auth, qn) ands = append(ands, ft) - queries = append(queries, qs...) + varQry = append(varQry, qs...) case []interface{}: for _, obj := range v { - ft, qs := buildFilter(typ, obj.(map[string]interface{}), qn) + ft, qs := buildFilter(typ, obj.(map[string]interface{}), auth, qn) ands = append(ands, ft) - queries = append(queries, qs...) + varQry = append(varQry, qs...) } } case "or": @@ -2032,15 +2042,15 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin // ... or: [{}] switch v := filter[field].(type) { case map[string]interface{}: - cond, qs := buildFilter(typ, v, qn) + cond, qs := buildFilter(typ, v, auth, qn) or = cond - queries = append(queries, qs...) + varQry = append(varQry, qs...) case []interface{}: ors := make([]*dql.FilterTree, 0, len(v)) for _, obj := range v { - ft, qs := buildFilter(typ, obj.(map[string]interface{}), qn) + ft, qs := buildFilter(typ, obj.(map[string]interface{}), auth, qn) ors = append(ors, ft) - queries = append(queries, qs...) + varQry = append(varQry, qs...) } or = &dql.FilterTree{ Child: ors, @@ -2052,13 +2062,13 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin // we are here ^^ // -> // @filter(anyofterms(Post.title, "GraphQL") AND NOT eq(Post.isPublished, true)) - not, qs := buildFilter(typ, filter[field].(map[string]interface{}), qn) + not, qs := buildFilter(typ, filter[field].(map[string]interface{}), auth, qn) ands = append(ands, &dql.FilterTree{ Op: "not", Child: []*dql.FilterTree{not}, }) - queries = append(queries, qs...) + varQry = append(varQry, qs...) default: // Handle nested object filtering // @@ -2074,8 +2084,8 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin if inv := fd.Inverse(); inv != nil { - fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), qn) - queries = append(queries, qs...) + fil, qs := buildFilter(fd.Type(), filter[field].(map[string]interface{}), auth, qn) + varQry = append(varQry, qs...) // add the uids of the nested object ands = append(ands, &dql.FilterTree{ @@ -2089,7 +2099,7 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin }) // generate filter var query for nested object - queries = append(queries, &dql.GraphQuery{ + nestedQry := &dql.GraphQuery{ Attr: "var", Func: &dql.Function{ Name: "type", @@ -2100,7 +2110,33 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin Attr: inv.DgraphPredicate(), Var: qn, }}, - }) + } + + // add auth queries to nested field + nestedQrys := []*dql.GraphQuery{nestedQry} + + if !auth.isWritingAuth { + wr := &authRewriter{ + authVariables: auth.authVariables, + varGen: auth.varGen, + selector: auth.selector, + parentVarName: qn + "Root", + isWritingAuth: auth.isWritingAuth, + } + + rbac := wr.evaluateStaticRules(fd.Type()) + if rbac == schema.Uncertain { + nestedQrys = wr.addAuthQueries(fd.Type(), nestedQrys, rbac) + } else if rbac == schema.Negative { + nestedQry.Attr = "var()" + nestedQry.Var = qn + nestedQry.Func = nil + nestedQry.Filter = nil + nestedQry.Children = nil + } + } + + varQry = append(varQry, nestedQrys...) continue } } @@ -2121,9 +2157,9 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin // the filters with null values will be ignored in query rewriting. if fn == "eq" { hasFilterMap := map[string]interface{}{"not": map[string]interface{}{"has": []interface{}{field}}} - ft, qs := buildFilter(typ, hasFilterMap, qn) + ft, qs := buildFilter(typ, hasFilterMap, auth, qn) ands = append(ands, ft) - queries = append(queries, qs...) + varQry = append(varQry, qs...) } continue } @@ -2267,7 +2303,7 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin var andFt *dql.FilterTree if len(ands) == 0 { - return or, queries + return or, varQry } else if len(ands) == 1 { andFt = ands[0] } else if len(ands) > 1 { @@ -2278,13 +2314,13 @@ func buildFilter(typ schema.Type, filter map[string]interface{}, queryName strin } if or == nil { - return andFt, queries + return andFt, varQry } return &dql.FilterTree{ Op: "or", Child: []*dql.FilterTree{andFt, or}, - }, queries + }, varQry } func buildHasFilterList(typ schema.Type, fieldsSlice []interface{}) []*dql.FilterTree { @@ -2346,13 +2382,17 @@ func buildMultiPolygon(multipolygon map[string]interface{}, buf *bytes.Buffer) { x.Check2(buf.WriteString("]")) } -func buildUnionFilter(typ schema.Type, filter map[string]interface{}, queryName string) (*dql.FilterTree, []*dql.GraphQuery, bool) { - var queries []*dql.GraphQuery +func buildUnionFilter(typ schema.Type, + filter map[string]interface{}, + auth *authRewriter, + queryName string) (*dql.FilterTree, []*dql.GraphQuery, bool) { + + var varQry []*dql.GraphQuery memberTypesList, ok := filter["memberTypes"].([]interface{}) // if memberTypes was specified to be an empty list like: { memberTypes: [], ...}, // then we don't need to include the field, on which the filter was specified, in the query. if ok && len(memberTypesList) == 0 { - return nil, queries, false + return nil, varQry, false } ft := &dql.FilterTree{ @@ -2370,8 +2410,8 @@ func buildUnionFilter(typ schema.Type, filter map[string]interface{}, queryName memberTypeFt = &dql.FilterTree{Func: buildTypeFunc(memberType.DgraphName())} } else { // else we need to query only the nodes which match the filter for that member type - ft, qs := buildFilter(memberType, memberTypeFilter, queryName) - queries = qs + ft, qs := buildFilter(memberType, memberTypeFilter, auth, queryName) + varQry = qs memberTypeFt = &dql.FilterTree{ Op: "and", Child: []*dql.FilterTree{ @@ -2384,7 +2424,7 @@ func buildUnionFilter(typ schema.Type, filter map[string]interface{}, queryName } // return true because we want to include the field with filter in query - return ft, queries, true + return ft, varQry, true } func maybeQuoteArg(fn string, arg interface{}) string { From a3ab96552c61f180a41342ea67bb322b3153dd10 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Mon, 24 Jun 2024 20:27:11 +1000 Subject: [PATCH 12/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(wrappers.go):=20Ref?= =?UTF-8?q?actor=20condition=20check=20in=20Inverse=20function=20for=20bet?= =?UTF-8?q?ter=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphql/schema/wrappers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 658500a92e2..7e4a518ddb9 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -2504,7 +2504,8 @@ func (fd *fieldDefinition) Inverse() FieldDefinition { for _, refField := range typ.Fields { refFieldType := fd.inSchema.schema.Types[refField.Type.Name()] - if refField.Type.Name() != typ.Name && !fd.inSchema.schema.Types[fd.ParentType().Name()].OneOf(refFieldType.Interfaces...) { + if refField.Type.Name() != typ.Name && + !fd.inSchema.schema.Types[fd.ParentType().Name()].OneOf(refFieldType.Interfaces...) { continue } From c95ecba76dd00983816c9f4ea4e129f70754fee6 Mon Sep 17 00:00:00 2001 From: Idowu Ayoola Date: Mon, 24 Jun 2024 20:30:14 +1000 Subject: [PATCH 13/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(query.go):=20Remove?= =?UTF-8?q?=20unnecessary=20line=20break=20to=20improve=20code=20readabili?= =?UTF-8?q?ty=20=E2=9C=A8=20(query.go):=20Add=20timer=20to=20track=20query?= =?UTF-8?q?=20execution=20time=20for=20performance=20monitoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphql/resolve/query.go | 1 - 1 file changed, 1 deletion(-) diff --git a/graphql/resolve/query.go b/graphql/resolve/query.go index 07ffce52c44..6fe86fa23cb 100644 --- a/graphql/resolve/query.go +++ b/graphql/resolve/query.go @@ -127,7 +127,6 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que query.ResponseName())) } qry := dgraph.AsString(dgQuery) - queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration) queryTimer.Start() resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: qry, ReadOnly: true}, query)