diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 5d8668af9d4..40f5d6d2de7 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -797,7 +797,8 @@ func RunAll(t *testing.T) { t.Run("query only typename", queryOnlyTypename) t.Run("query nested only typename", querynestedOnlyTypename) t.Run("test onlytypename for interface types", onlytypenameForInterface) - t.Run("entitites Query on extended type", entitiesQuery) + t.Run("entitites Query on extended type with key field of type String", entitiesQueryWithKeyFieldOfTypeString) + t.Run("entitites Query on extended type with key field of type Int", entitiesQueryWithKeyFieldOfTypeInt) t.Run("get state by xid", getStateByXid) t.Run("get state without args", getStateWithoutArgs) diff --git a/graphql/e2e/common/query.go b/graphql/e2e/common/query.go index a6a3892790e..5ebbd443793 100644 --- a/graphql/e2e/common/query.go +++ b/graphql/e2e/common/query.go @@ -381,10 +381,10 @@ func allPosts(t *testing.T) []*post { return result.QueryPost } -func entitiesQuery(t *testing.T) { +func entitiesQueryWithKeyFieldOfTypeString(t *testing.T) { addSpaceShipParams := &GraphQLParams{ - Query: `mutation addSpaceShip($id1: String!, $missionId1: String! ) { - addSpaceShip(input: [{id: $id1, missions: [{id: $missionId1, designation: "Apollo1"}]} ]) { + Query: `mutation addSpaceShip($id1: String!, $id2: String!, $id3: String!, $id4: String! ) { + addSpaceShip(input: [{id: $id1, missions: [{id: "Mission1", designation: "Apollo1"}]},{id: $id2, missions: [{id: "Mission2", designation: "Apollo2"}]},{id: $id3, missions: [{id: "Mission3", designation: "Apollo3"}]}, {id: $id4, missions: [{id: "Mission4", designation: "Apollo4"}]}]){ spaceShip { id missions { @@ -395,8 +395,10 @@ func entitiesQuery(t *testing.T) { } }`, Variables: map[string]interface{}{ - "id1": "SpaceShip1", - "missionId1": "Mission1", + "id1": "SpaceShip1", + "id2": "SpaceShip2", + "id3": "SpaceShip3", + "id4": "SpaceShip4", }, } @@ -404,8 +406,8 @@ func entitiesQuery(t *testing.T) { RequireNoGQLErrors(t, gqlResponse) entitiesQueryParams := &GraphQLParams{ - Query: `query _entities($typeName: String!, $id1: String!){ - _entities(representations: [{__typename: $typeName, id: $id1}]) { + Query: `query _entities($typeName: String!, $id1: String!, $id2: String!, $id3: String!, $id4: String!){ + _entities(representations: [{__typename: $typeName, id: $id4},{__typename: $typeName, id: $id2},{__typename: $typeName, id: $id1},{__typename: $typeName, id: $id3},{__typename: $typeName, id: $id1}]) { ... on SpaceShip { missions(order: {asc: id}){ id @@ -417,32 +419,77 @@ func entitiesQuery(t *testing.T) { Variables: map[string]interface{}{ "typeName": "SpaceShip", "id1": "SpaceShip1", + "id2": "SpaceShip2", + "id3": "SpaceShip3", + "id4": "SpaceShip4", }, } entitiesResp := entitiesQueryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, entitiesResp) - expectedJSON := `{ - "_entities": [ - { - "missions": [ - { - "id": "Mission1", - "designation": "Apollo1" - } - ] - } - ] - }` + expectedJSON := `{"_entities":[{"missions":[{"designation":"Apollo4","id":"Mission4"}]},{"missions":[{"designation":"Apollo2","id":"Mission2"}]},{"missions":[{"designation":"Apollo1","id":"Mission1"}]},{"missions":[{"designation":"Apollo3","id":"Mission3"}]},{"missions":[{"designation":"Apollo1","id":"Mission1"}]}]}` + + JSONEqGraphQL(t, expectedJSON, string(entitiesResp.Data)) + + spaceShipDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"SpaceShip1", "SpaceShip2", "SpaceShip3", "SpaceShip4"}}} + DeleteGqlType(t, "SpaceShip", spaceShipDeleteFilter, 4, nil) + + missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1", "Mission2", "Mission3", "Mission4"}}} + DeleteGqlType(t, "Mission", missionDeleteFilter, 4, nil) + +} + +func entitiesQueryWithKeyFieldOfTypeInt(t *testing.T) { + addPlanetParams := &GraphQLParams{ + Query: `mutation { + addPlanet(input: [{id: 1, missions: [{id: "Mission1", designation: "Apollo1"}]},{id: 2, missions: [{id: "Mission2", designation: "Apollo2"}]},{id: 3, missions: [{id: "Mission3", designation: "Apollo3"}]}, {id: 4, missions: [{id: "Mission4", designation: "Apollo4"}]}]){ + planet { + id + missions { + id + designation + } + } + } + }`, + } + + gqlResponse := addPlanetParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + entitiesQueryParams := &GraphQLParams{ + Query: `query _entities($typeName: String!, $id1: Int!, $id2: Int!, $id3: Int!, $id4: Int!){ + _entities(representations: [{__typename: $typeName, id: $id4},{__typename: $typeName, id: $id2},{__typename: $typeName, id: $id1},{__typename: $typeName, id: $id3},{__typename: $typeName, id: $id1}]) { + ... on Planet { + missions(order: {asc: id}){ + id + designation + } + } + } + }`, + Variables: map[string]interface{}{ + "typeName": "Planet", + "id1": 1, + "id2": 2, + "id3": 3, + "id4": 4, + }, + } + + entitiesResp := entitiesQueryParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, entitiesResp) + + expectedJSON := `{"_entities":[{"missions":[{"designation":"Apollo4","id":"Mission4"}]},{"missions":[{"designation":"Apollo2","id":"Mission2"}]},{"missions":[{"designation":"Apollo1","id":"Mission1"}]},{"missions":[{"designation":"Apollo3","id":"Mission3"}]},{"missions":[{"designation":"Apollo1","id":"Mission1"}]}]}` - testutil.CompareJSON(t, expectedJSON, string(entitiesResp.Data)) + JSONEqGraphQL(t, expectedJSON, string(entitiesResp.Data)) - spaceShipDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"SpaceShip1"}}} - DeleteGqlType(t, "SpaceShip", spaceShipDeleteFilter, 1, nil) + planetDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []int{1, 2, 3, 4}}} + DeleteGqlType(t, "Planet", planetDeleteFilter, 4, nil) - missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1"}}} - DeleteGqlType(t, "Mission", missionDeleteFilter, 1, nil) + missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1", "Mission2", "Mission3", "Mission4"}}} + DeleteGqlType(t, "Mission", missionDeleteFilter, 4, nil) } diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index 7f06c40c698..8904c99edf3 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -327,6 +327,11 @@ type SpaceShip @key(fields: "id") @extends { missions: [Mission] } +type Planet @key(fields: "id") @extends { + id: Int! @id @external + missions: [Mission] +} + type Region { id: String! @id name: String! diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 94f3e648411..b5e67d865a6 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -432,6 +432,20 @@ "type": "uid", "list": true }, + { + "predicate": "Planet.id", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ], + "upsert": true + }, + { + "predicate": "Planet.missions", + "type": "uid", + "list": true + }, { "predicate": "State.capital", "type": "string" @@ -1137,6 +1151,17 @@ ], "name": "SpaceShip" }, + { + "fields": [ + { + "name": "Planet.id" + }, + { + "name": "Planet.missions" + } + ], + "name": "Planet" + }, { "fields": [ { diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 46b40225a99..521712dbe5d 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -345,6 +345,11 @@ type SpaceShip @key(fields: "id") @extends { missions: [Mission] } +type Planet @key(fields: "id") @extends { + id: Int! @id @external + missions: [Mission] +} + type Region { id: String! @id name: String! diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index 490d2604cf2..443ff83b7df 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -593,6 +593,20 @@ "type": "uid", "list": true }, + { + "predicate": "Planet.id", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ], + "upsert": true + }, + { + "predicate": "Planet.missions", + "type": "uid", + "list": true + }, { "predicate": "Starship.length", "type": "float" @@ -1359,6 +1373,17 @@ ], "name": "Zoo" }, + { + "fields": [ + { + "name": "Planet.missions" + }, + { + "name": "Planet.id" + } + ], + "name": "Planet" + }, { "fields": [ { diff --git a/graphql/resolve/auth_query_test.yaml b/graphql/resolve/auth_query_test.yaml index 80241b29f3f..65be3d5837c 100644 --- a/graphql/resolve/auth_query_test.yaml +++ b/graphql/resolve/auth_query_test.yaml @@ -1887,14 +1887,14 @@ USER: "user" dgquery: |- query { - _entities(func: uid(_EntityRoot)) { + _entities(func: uid(_EntityRoot), orderasc: Mission.id) { dgraph.type Mission.id : Mission.id Mission.designation : Mission.designation Mission.startDate : Mission.startDate dgraph.uid : uid } - _EntityRoot as var(func: uid(Mission_1)) @filter(uid(Mission_Auth2)) + _EntityRoot as var(func: uid(Mission_1), orderasc: Mission.id) @filter(uid(Mission_Auth2)) Mission_1 as var(func: eq(Mission.id, "0x1", "0x2", "0x3")) @filter(type(Mission)) Mission_Auth2 as var(func: uid(Mission_1)) @filter(eq(Mission.supervisorName, "user")) @cascade { Mission.id : Mission.id @@ -1916,7 +1916,7 @@ USER: "user" dgquery: |- query { - _entities(func: uid(_EntityRoot)) { + _entities(func: uid(_EntityRoot), orderasc: Astronaut.id) { dgraph.type Astronaut.missions : Astronaut.missions @filter(uid(Mission_1)) { Mission.designation : Mission.designation @@ -1924,7 +1924,7 @@ } dgraph.uid : uid } - _EntityRoot as var(func: uid(Astronaut_4)) + _EntityRoot as var(func: uid(Astronaut_4), orderasc: Astronaut.id) Astronaut_4 as var(func: eq(Astronaut.id, "0x1", "0x2", "0x3")) @filter(type(Astronaut)) var(func: uid(_EntityRoot)) { Mission_2 as Astronaut.missions @@ -1968,11 +1968,11 @@ ROLE: "admin" dgquery: |- query { - _entities(func: uid(_EntityRoot)) { + _entities(func: uid(_EntityRoot), orderasc: Astronaut.id) { dgraph.type dgraph.uid : uid } - _EntityRoot as var(func: uid(Astronaut_3)) + _EntityRoot as var(func: uid(Astronaut_3), orderasc: Astronaut.id) Astronaut_3 as var(func: eq(Astronaut.id, "0x1", "0x2", "0x3")) @filter(type(Astronaut)) } diff --git a/graphql/resolve/query.go b/graphql/resolve/query.go index 497c3e49e2b..505297b85ea 100644 --- a/graphql/resolve/query.go +++ b/graphql/resolve/query.go @@ -56,14 +56,22 @@ func (qr QueryResolverFunc) Resolve(ctx context.Context, query schema.Query) *Re // NewQueryResolver creates a new query resolver. The resolver runs the pipeline: // 1) rewrite the query using qr (return error if failed) // 2) execute the rewritten query with ex (return error if failed) +// 3) process the result with rc func NewQueryResolver(qr QueryRewriter, ex DgraphExecutor) QueryResolver { - return &queryResolver{queryRewriter: qr, executor: ex} + return &queryResolver{queryRewriter: qr, executor: ex, resultCompleter: CompletionFunc(noopCompletion)} +} + +// NewEntitiesQueryResolver creates a new query resolver for `_entities` query. +// It is introduced because result completion works little different for `_entities` query. +func NewEntitiesQueryResolver(qr QueryRewriter, ex DgraphExecutor) QueryResolver { + return &queryResolver{queryRewriter: qr, executor: ex, resultCompleter: CompletionFunc(entitiesQueryCompletion)} } // a queryResolver can resolve a single GraphQL query field. type queryResolver struct { - queryRewriter QueryRewriter - executor DgraphExecutor + queryRewriter QueryRewriter + executor DgraphExecutor + resultCompleter ResultCompleter } func (qr *queryResolver) Resolve(ctx context.Context, query schema.Query) *Resolved { @@ -82,6 +90,7 @@ func (qr *queryResolver) Resolve(ctx context.Context, query schema.Query) *Resol defer timer.Stop() resolved := qr.rewriteAndExecute(ctx, query) + qr.resultCompleter.Complete(ctx, resolved) resolverTrace.Dgraph = resolved.Extensions.Tracing.Execution.Resolvers[0].Dgraph resolved.Extensions.Tracing.Execution.Resolvers[0] = resolverTrace return resolved diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 2aceac1d6a6..4ad2153408b 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -160,25 +160,15 @@ func (qr *queryRewriter) Rewrite( } } -// entitiesQuery rewrites the Apollo `_entities` Query which is sent from the Apollo gateway to a DQL query. -// This query is sent to the Dgraph service to resolve types `extended` and defined by this service. -func entitiesQuery(field schema.Query, authRw *authRewriter) ([]*gql.GraphQuery, error) { - - // Input Argument to the Query is a List of "__typename" and "keyField" pair. - // For this type Extension:- - // extend type Product @key(fields: "upc") { - // upc: String @external - // reviews: [Review] - // } - // Input to the Query will be - // "_representations": [ - // { - // "__typename": "Product", - // "upc": "B00005N5PF" - // }, - // ... - // ] +type parsedRepresentations struct { + keyFieldName string + typeName string + keyFieldIsID bool + keyFieldValueList []interface{} +} +// parseReporesentationsArgument parses the "_representations" argument in the "_entities" query. +func parseRepresentationsArgument(field schema.Query) (*parsedRepresentations, error) { representations, ok := field.ArgValue("representations").([]interface{}) if !ok { return nil, fmt.Errorf("Error parsing `representations` argument") @@ -235,7 +225,38 @@ func entitiesQuery(field schema.Query, authRw *authRewriter) ([]*gql.GraphQuery, typeName = k } - typeDefn := field.BuildType(typeName) + return &parsedRepresentations{ + keyFieldName: keyFieldName, + typeName: typeName, + keyFieldIsID: keyFieldIsID, + keyFieldValueList: keyFieldValueList}, nil +} + +// entitiesQuery rewrites the Apollo `_entities` Query which is sent from the Apollo gateway to a DQL query. +// This query is sent to the Dgraph service to resolve types `extended` and defined by this service. +func entitiesQuery(field schema.Query, authRw *authRewriter) ([]*gql.GraphQuery, error) { + + // Input Argument to the Query is a List of "__typename" and "keyField" pair. + // For this type Extension:- + // extend type Product @key(fields: "upc") { + // upc: String @external + // reviews: [Review] + // } + // Input to the Query will be + // "_representations": [ + // { + // "__typename": "Product", + // "upc": "B00005N5PF" + // }, + // ... + // ] + + parsedRepr, err := parseRepresentationsArgument(field) + if err != nil { + return nil, err + } + + typeDefn := field.BuildType(parsedRepr.typeName) rbac := authRw.evaluateStaticRules(typeDefn) dgQuery := &gql.GraphQuery{ @@ -261,10 +282,16 @@ func entitiesQuery(field schema.Query, authRw *authRewriter) ([]*gql.GraphQuery, // If the key field is of ID type and is not an external field // then we query it using the `uid` otherwise we treat it as string // and query using `eq` function. - if keyFieldIsID && !typeDefn.Field(keyFieldName).IsExternal() { - addUIDFunc(dgQuery, convertIDs(keyFieldValueList)) + // We also don't need to add Order to the query as the results are + // automatically returned in the ascending order of the uids. + if parsedRepr.keyFieldIsID && !typeDefn.Field(parsedRepr.keyFieldName).IsExternal() { + addUIDFunc(dgQuery, convertIDs(parsedRepr.keyFieldValueList)) } else { - addEqFunc(dgQuery, typeDefn.DgraphPredicate(keyFieldName), keyFieldValueList) + addEqFunc(dgQuery, typeDefn.DgraphPredicate(parsedRepr.keyFieldName), parsedRepr.keyFieldValueList) + // Add the ascending Order of the keyField in the query. + // The result will be converted into the exact in the resultCompletion step. + dgQuery.Order = append(dgQuery.Order, + &pb.Order{Attr: typeDefn.DgraphPredicate(parsedRepr.keyFieldName)}) } // AddTypeFilter in as the Filter to the Root the Query. // Query will be like :- diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index 264ba9be54f..9d4a51e395e 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -3298,7 +3298,7 @@ } dgquery: |- query { - _entities(func: eq(Astronaut.id, "0x1", "0x2")) @filter(type(Astronaut)) { + _entities(func: eq(Astronaut.id, "0x1", "0x2"), orderasc: Astronaut.id) @filter(type(Astronaut)) { dgraph.type Astronaut.missions : Astronaut.missions { Mission.designation : Mission.designation @@ -3322,7 +3322,7 @@ } dgquery: |- query { - _entities(func: eq(SpaceShip.id, "0x1", "0x2")) @filter(type(SpaceShip)) { + _entities(func: eq(SpaceShip.id, "0x1", "0x2"), orderasc: SpaceShip.id) @filter(type(SpaceShip)) { dgraph.type SpaceShip.missions : SpaceShip.missions { Mission.designation : Mission.designation diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index 78a80a3d53a..2836e1aea13 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "net/http" + "sort" "strings" "sync" "time" @@ -84,6 +85,12 @@ type ResolverFactory interface { WithSchemaIntrospection() ResolverFactory } +// A ResultCompleter can take a []byte slice representing an intermediate result +// in resolving field and applies a completion step. +type ResultCompleter interface { + Complete(ctx context.Context, resolved *Resolved) +} + // RequestResolver can process GraphQL requests and write GraphQL JSON responses. // A schema.Request may contain any number of queries or mutations (never both). // RequestResolver.Resolve() resolves all of them by finding the resolved answers @@ -142,6 +149,15 @@ type Resolved struct { Extensions *schema.Extensions } +// CompletionFunc is an adapter that allows us to compose completions and build a +// ResultCompleter from a function. Based on the http.HandlerFunc pattern. +type CompletionFunc func(ctx context.Context, resolved *Resolved) + +// Complete calls cf(ctx, resolved) +func (cf CompletionFunc) Complete(ctx context.Context, resolved *Resolved) { + cf(ctx, resolved) +} + // NewDgraphExecutor builds a DgraphExecutor for proxying requests through dgraph. func NewDgraphExecutor() DgraphExecutor { return newDgraphExecutor(&dgraph.DgraphEx{}) @@ -220,13 +236,18 @@ func (rf *resolverFactory) WithConventionResolvers( queries := append(s.Queries(schema.GetQuery), s.Queries(schema.FilterQuery)...) queries = append(queries, s.Queries(schema.PasswordQuery)...) queries = append(queries, s.Queries(schema.AggregateQuery)...) - queries = append(queries, s.Queries(schema.EntitiesQuery)...) for _, q := range queries { rf.WithQueryResolver(q, func(q schema.Query) QueryResolver { return NewQueryResolver(fns.Qrw, fns.Ex) }) } + for _, q := range s.Queries(schema.EntitiesQuery) { + rf.WithQueryResolver(q, func(q schema.Query) QueryResolver { + return NewEntitiesQueryResolver(fns.Qrw, fns.Ex) + }) + } + for _, q := range s.Queries(schema.HTTPQuery) { rf.WithQueryResolver(q, func(q schema.Query) QueryResolver { return NewHTTPQueryResolver(nil) @@ -302,6 +323,102 @@ func NewResolverFactory( } } +// entitiesCompletion transform the result of the `_entities` query. +// It changes the order of the result to the order of keyField in the +// `_representations` argument. +func entitiesQueryCompletion(ctx context.Context, resolved *Resolved) { + // return if Data is not present + if len(resolved.Data) == 0 { + return + } + + var data map[string][]interface{} + err := schema.Unmarshal(resolved.Data, &data) + if err != nil { + resolved.Err = schema.AppendGQLErrs(resolved.Err, err) + return + } + + // fetch the keyFieldValueList from the query arguments. + repr, err := parseRepresentationsArgument(resolved.Field.(schema.Query)) + if err != nil { + resolved.Err = schema.AppendGQLErrs(resolved.Err, err) + return + } + + typeDefn := resolved.Field.(schema.Query).BuildType(repr.typeName) + keyFieldType := typeDefn.Field(repr.keyFieldName).Type().Name() + + // store the index of the keyField Values present in the argument in a map. + // key in the map is of type interface because there are multiple types like String, + // Int, Int64 allowed as @id. There could be duplicate keys in the representations + // so the value of map is a list of integers containing all the indices for a key. + indexMap := make(map[interface{}][]int) + uniqueKeyList := make([]interface{}, 0) + for i, key := range repr.keyFieldValueList { + indexMap[key] = append(indexMap[key], i) + } + + // Create a list containing unique keys and then sort in ascending order because this + // will be the order in which the data is received. + // for eg: for keys: {1, 2, 4, 1, 3} is converted into {1, 2, 4, 3} and then {1, 2, 3, 4} + // this will be the order of received data from the dgraph. + for k := range indexMap { + uniqueKeyList = append(uniqueKeyList, k) + } + sort.Slice(uniqueKeyList, func(i, j int) bool { + switch val := uniqueKeyList[i].(type) { + case string: + return val < uniqueKeyList[j].(string) + case json.Number: + switch keyFieldType { + case "Int", "Int64": + val1, _ := val.Int64() + val2, _ := uniqueKeyList[j].(json.Number).Int64() + return val1 < val2 + case "Float": + val1, _ := val.Float64() + val2, _ := uniqueKeyList[j].(json.Number).Float64() + return val1 < val2 + } + case int64: + return val < uniqueKeyList[j].(int64) + case float64: + return val < uniqueKeyList[j].(float64) + } + return false + }) + + // create the new output according to the index of the keyFields present in the argument. + entitiesQryResp := data["_entities"] + + // if `entitiesQueryResp` contains less number of elements than the number of unique keys + // which is because the object related to certain key is not present in the dgraph. + // This will end into an error at the Gateway, so no need to order the result here. + if len(entitiesQryResp) < len(uniqueKeyList) { + return + } + + // Reorder the output response according to the order of the keys in the representations argument. + output := make([]interface{}, len(repr.keyFieldValueList)) + for i, key := range uniqueKeyList { + for _, idx := range indexMap[key] { + output[idx] = entitiesQryResp[i] + } + } + + // replace the result obtained from the dgraph and marshal back. + data["_entities"] = output + resolved.Data, err = json.Marshal(data) + if err != nil { + resolved.Err = schema.AppendGQLErrs(resolved.Err, err) + } + +} + +// noopCompletion just passes back it's result and err arguments +func noopCompletion(ctx context.Context, resolved *Resolved) {} + func (rf *resolverFactory) queryResolverFor(query schema.Query) QueryResolver { rf.RLock() defer rf.RUnlock()