Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(Query): Enable persistent queries in dgraph #6788

Merged
merged 10 commits into from
Oct 27, 2020
104 changes: 104 additions & 0 deletions edgraph/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ package edgraph
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"time"

"github.com/dgraph-io/dgo/v200/protos/api"
"github.com/dgraph-io/dgraph/graphql/schema"
"github.com/dgraph-io/ristretto/z"
"github.com/golang/glog"
"github.com/pkg/errors"
)

// ResetCors make the dgraph to accept all the origins if no origins were given
Expand Down Expand Up @@ -151,3 +155,103 @@ func UpdateSchemaHistory(ctx context.Context, schema string) error {
_, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize)
return err
}

// ProcessPersistedQuery stores and retrieves persisted queries by following waterfall logic:
// 1. If sha256Hash is not provided process queries without persisting
// 2. If sha256Hash is provided try retrieving persisted queries
// 2a. Persisted Query not found
// i) If query is not provided then throw "PersistedQueryNotFound"
// ii) If query is provided then store query in dgraph only if sha256 of the query is correct
// otherwise throw "provided sha does not match query"
// 2b. Persisted Query found
// i) If query is not provided then update gqlRes with the found query and proceed
// ii) If query is provided then match query retrieved, if identical do nothing else
// throw "query does not match persisted query"
func ProcessPersistedQuery(ctx context.Context, gqlReq *schema.Request) error {
query := gqlReq.Query
sha256Hash := gqlReq.Extensions.PersistedQuery.Sha256Hash

if len(sha256Hash) == 0 {
return nil
}

queryForSHA := fmt.Sprintf(`query{
me(func: eq(dgraph.graphql.p_sha256hash, %s)){
all-seeing-code marked this conversation as resolved.
Show resolved Hide resolved
dgraph.graphql.p_query
}
}`, sha256Hash)
req := &api.Request{
Query: queryForSHA,
ReadOnly: true,
}
storedQuery, _ := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize)

type shaQueryResponse struct {
Me []struct {
PersistedQuery string `json:"dgraph.graphql.p_query"`
} `json:"me"`
}

shaQueryRes := &shaQueryResponse{}
if err := json.Unmarshal(storedQuery.Json, shaQueryRes); err != nil {
return err
}

if len(shaQueryRes.Me) == 0 {
if len(query) == 0 {
return errors.New("PersistedQueryNotFound")
}
if !hashMatches(query, sha256Hash) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we do this at the top of the function? Either way

return errors.New("provided sha does not match query")
}

req := &api.Request{
Mutations: []*api.Mutation{
{
Set: []*api.NQuad{
{
Subject: "_:a",
Predicate: "dgraph.graphql.p_query",
ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: query}},
},
{
Subject: "_:a",
Predicate: "dgraph.graphql.p_sha256hash",
ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: sha256Hash}},
},
{
Subject: "_:a",
Predicate: "dgraph.type",
ObjectValue: &api.Value{Val: &api.Value_StrVal{
StrVal: "dgraph.graphql.persisted_query"}},
},
},
},
},
CommitNow: true,
}

_, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize)
return err

}

if len(shaQueryRes.Me) != 1 {
return fmt.Errorf("same sha returned %d queries", len(shaQueryRes.Me))
}

if len(query) > 0 && shaQueryRes.Me[0].PersistedQuery != query {
return errors.New("query does not match persisted query")
}

gqlReq.Query = shaQueryRes.Me[0].PersistedQuery
return nil

}

func hashMatches(query, sha256Hash string) bool {
hasher := sha256.New()
hasher.Write([]byte(query))
hashGenerated := hex.EncodeToString(hasher.Sum(nil))
return hashGenerated == sha256Hash
}
9 changes: 9 additions & 0 deletions graphql/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ const (
created_at: DateTime! @dgraph(pred: "dgraph.graphql.schema_created_at")
}

"""
PersistedQuery contains the query and sha256hash of the query.
"""
type PersistedQuery @dgraph(type: "dgraph.graphql.persisted_query") {
query: String! @dgraph(pred: "dgraph.graphql.p_query")
sha256Hash: String! @id @dgraph(pred: "dgraph.graphql.p_sha256hash")
}

"""
A NodeState is the state of an individual node in the Dgraph cluster.
"""
Expand Down Expand Up @@ -280,6 +288,7 @@ const (
config: Config
getAllowedCORSOrigins: Cors
querySchemaHistory(first: Int, offset: Int): [SchemaHistory]
queryPersistedQuery: PersistedQuery
` + adminQueries + `
}

Expand Down
8 changes: 8 additions & 0 deletions graphql/schema/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ type Request struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
Extensions struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Leave a comment here, that we should add explicit types for all extensions that we support in dgraph

PersistedQuery PersistedQuery
}

Header http.Header
}

// PersistedQuery represents the query struct received from clients like Apollo
type PersistedQuery struct {
Sha256Hash string
}

// Operation finds the operation in req, if it is a valid request for GraphQL
// schema s. If the request is GraphQL valid, it must contain a single valid
// Operation. If either the request is malformed or doesn't contain a valid
Expand Down
22 changes: 18 additions & 4 deletions graphql/web/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"strings"
"time"

"github.com/dgraph-io/dgraph/edgraph"
"github.com/dgraph-io/dgraph/graphql/api"
"github.com/dgraph-io/dgraph/graphql/authorization"
"github.com/dgraph-io/dgraph/graphql/resolve"
Expand Down Expand Up @@ -211,12 +212,17 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
gqlReq, err := getRequest(ctx, r)

if err != nil {
res = schema.ErrorResponse(err)
} else {
gqlReq.Header = r.Header
res = gh.resolver.Resolve(ctx, gqlReq)
write(w, schema.ErrorResponse(err), strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"))
return
}

if err = edgraph.ProcessPersistedQuery(ctx, gqlReq); err != nil {
write(w, schema.ErrorResponse(err), strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"))
return
}

res = gh.resolver.Resolve(ctx, gqlReq)

write(w, res, strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"))
}

Expand Down Expand Up @@ -253,6 +259,13 @@ func getRequest(ctx context.Context, r *http.Request) (*schema.Request, error) {
query := r.URL.Query()
gqlReq.Query = query.Get("query")
gqlReq.OperationName = query.Get("operationName")
if extensions, ok := query["extensions"]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

check length(extensions) before json decoding

d := json.NewDecoder(strings.NewReader(extensions[0]))
d.UseNumber()
if err := d.Decode(&gqlReq.Extensions); err != nil {
return nil, errors.Wrap(err, "Not a valid GraphQL request body")
}
}
variables, ok := query["variables"]
if ok {
d := json.NewDecoder(strings.NewReader(variables[0]))
Expand Down Expand Up @@ -292,6 +305,7 @@ func getRequest(ctx context.Context, r *http.Request) (*schema.Request, error) {
return nil,
errors.New("Unrecognised request method. Please use GET or POST for GraphQL requests")
}
gqlReq.Header = r.Header

return gqlReq, nil
}
Expand Down
21 changes: 21 additions & 0 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,17 @@ func initialTypesInternal(all bool) []*pb.TypeUpdate {
ValueType: pb.Posting_DATETIME,
},
},
}, &pb.TypeUpdate{
TypeName: "dgraph.graphql.persisted_query",
Fields: []*pb.SchemaUpdate{
{
Predicate: "dgraph.graphql.p_query",
ValueType: pb.Posting_STRING,
}, {
Predicate: "dgraph.graphql.p_sha256hash",
ValueType: pb.Posting_STRING,
},
},
})

if all || x.WorkerConfig.AclEnabled {
Expand Down Expand Up @@ -678,6 +689,16 @@ func initialSchemaInternal(all bool) []*pb.SchemaUpdate {
}, &pb.SchemaUpdate{
Predicate: "dgraph.graphql.schema_created_at",
ValueType: pb.Posting_DATETIME,
}, &pb.SchemaUpdate{
Predicate: "dgraph.graphql.p_query",
ValueType: pb.Posting_STRING,
Directive: pb.SchemaUpdate_INDEX,
all-seeing-code marked this conversation as resolved.
Show resolved Hide resolved
Tokenizer: []string{"exact"},
}, &pb.SchemaUpdate{
Predicate: "dgraph.graphql.p_sha256hash",
ValueType: pb.Posting_STRING,
Directive: pb.SchemaUpdate_INDEX,
Tokenizer: []string{"exact"},
})

if all || x.WorkerConfig.AclEnabled {
Expand Down
11 changes: 6 additions & 5 deletions x/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,12 @@ var internalPredicateMap = map[string]struct{}{
}

var preDefinedTypeMap = map[string]struct{}{
"dgraph.graphql": {},
"dgraph.type.User": {},
"dgraph.type.Group": {},
"dgraph.type.Rule": {},
"dgraph.graphql.history": {},
"dgraph.graphql": {},
"dgraph.type.User": {},
"dgraph.type.Group": {},
"dgraph.type.Rule": {},
"dgraph.graphql.history": {},
"dgraph.graphql.persisted_query": {},
all-seeing-code marked this conversation as resolved.
Show resolved Hide resolved
}

// IsGraphqlReservedPredicate returns true if it is the predicate is reserved by graphql.
Expand Down