Skip to content

Commit

Permalink
feat(GraphQL): Custom logic now supports DQL queries (#6115)
Browse files Browse the repository at this point in the history
Fixes GraphQL-600.

This PR adds DQL query (previously known as GraphQL+-) support in GraphQL through the `@custom` directive.

(cherry picked from commit 117ac3c)
  • Loading branch information
abhimanyusinghgaur authored and gja committed Aug 19, 2020
1 parent 533953b commit ad1ac8f
Show file tree
Hide file tree
Showing 42 changed files with 479 additions and 51 deletions.
169 changes: 169 additions & 0 deletions graphql/e2e/custom_logic/custom_logic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
)

const (
alphaGrpc = "localhost:9180"
alphaURL = "http://localhost:8180/graphql"
alphaAdminURL = "http://localhost:8180/admin"
subscriptionEndpoint = "ws://localhost:8180/graphql"
Expand Down Expand Up @@ -2694,3 +2695,171 @@ func TestRestCustomLogicInDeepNestedField(t *testing.T) {
]
}`)
}

func TestCustomDQL(t *testing.T) {
dg, err := testutil.DgraphClient(alphaGrpc)
require.NoError(t, err)
testutil.DropAll(t, dg)

schema := `
type Tweets {
id: ID!
text: String! @search(by: [fulltext])
user: User
timestamp: DateTime! @search
}
type User {
screen_name: String! @id
followers: Int @search
tweets: [Tweets] @hasInverse(field: user)
}
type UserTweetCount @remote {
screen_name: String
tweetCount: Int
}
type Query {
getFirstUserByFollowerCount(count: Int!): User @custom(dql: """
query getFirstUserByFollowerCount($count: int) {
getFirstUserByFollowerCount(func: eq(User.followers, $count), first: 1) {
screen_name: User.screen_name
followers: User.followers
}
}
""")
dqlTweetsByAuthorFollowers: [Tweets] @custom(dql: """
query {
var(func: type(Tweets)) @filter(anyoftext(Tweets.text, "DQL")) {
Tweets.user {
followers as User.followers
}
userFollowerCount as sum(val(followers))
}
dqlTweetsByAuthorFollowers(func: uid(userFollowerCount), orderdesc: val(userFollowerCount)) {
id: uid
text: Tweets.text
timestamp: Tweets.timestamp
}
}
""")
filteredTweetsByAuthorFollowers(search: String!): [Tweets] @custom(dql: """
query t($search: string) {
var(func: type(Tweets)) @filter(anyoftext(Tweets.text, $search)) {
Tweets.user {
followers as User.followers
}
userFollowerCount as sum(val(followers))
}
filteredTweetsByAuthorFollowers(func: uid(userFollowerCount), orderdesc: val(userFollowerCount)) {
id: uid
text: Tweets.text
timestamp: Tweets.timestamp
}
}
""")
queryUserTweetCounts: [UserTweetCount] @custom(dql: """
query {
queryUserTweetCounts(func: type(User)) {
screen_name: User.screen_name
tweetCount: count(User.tweets)
}
}
""")
}
`
updateSchemaRequireNoGQLErrors(t, schema)
time.Sleep(2 * time.Second)

params := &common.GraphQLParams{
Query: `
mutation {
addTweets(input: [
{
text: "Hello DQL!"
user: {
screen_name: "abhimanyu"
followers: 5
}
timestamp: "2020-07-29"
}
{
text: "Woah DQL works!"
user: {
screen_name: "pawan"
followers: 10
}
timestamp: "2020-07-29"
}
{
text: "hmm, It worked."
user: {
screen_name: "abhimanyu"
followers: 5
}
timestamp: "2020-07-30"
}
]) {
numUids
}
}`,
}

result := params.ExecuteAsPost(t, alphaURL)
common.RequireNoGQLErrors(t, result)

params = &common.GraphQLParams{
Query: `
query {
getFirstUserByFollowerCount(count: 10) {
screen_name
followers
}
dqlTweetsByAuthorFollowers {
text
}
filteredTweetsByAuthorFollowers(search: "hello") {
text
}
queryUserTweetCounts {
screen_name
tweetCount
}
}`,
}

result = params.ExecuteAsPost(t, alphaURL)
common.RequireNoGQLErrors(t, result)

require.JSONEq(t, `{
"getFirstUserByFollowerCount": {
"screen_name": "pawan",
"followers": 10
},
"dqlTweetsByAuthorFollowers": [
{
"text": "Woah DQL works!"
},
{
"text": "Hello DQL!"
}
],
"filteredTweetsByAuthorFollowers": [
{
"text": "Hello DQL!"
}
],
"queryUserTweetCounts": [
{
"screen_name": "abhimanyu",
"tweetCount": 2
},
{
"screen_name": "pawan",
"tweetCount": 1
}
]
}`, string(result.Data))
}
57 changes: 51 additions & 6 deletions graphql/resolve/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package resolve
import (
"context"
"encoding/json"
"errors"
"strconv"

"github.com/golang/glog"
otrace "go.opencensus.io/trace"
Expand All @@ -30,6 +32,8 @@ import (
"github.com/dgraph-io/dgraph/x"
)

var errNotScalar = errors.New("provided value is not a scalar, can't convert it to string")

// A QueryResolver can resolve a single query.
type QueryResolver interface {
Resolve(ctx context.Context, query schema.Query) *Resolved
Expand Down Expand Up @@ -111,16 +115,36 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que
}
}

dgQuery, err := qr.queryRewriter.Rewrite(ctx, query)
if err != nil {
return emptyResult(schema.GQLWrapf(err, "couldn't rewrite query %s",
query.ResponseName()))
var qry string
vars := make(map[string]string)

// DQL queries don't need any rewriting, as they are already in DQL form
if query.QueryType() == schema.DQLQuery {
qry = query.DQLQuery()
args := query.Arguments()
for k, v := range args {
// dgoapi.Request{}.Vars accepts only string values for variables,
// so need to convert all variable values to string
vStr, err := convertScalarToString(v)
if err != nil {
return emptyResult(schema.GQLWrapf(err, "couldn't convert argument %s to string",
k))
}
// the keys in dgoapi.Request{}.Vars are assumed to be prefixed with $
vars["$"+k] = vStr
}
} else {
dgQuery, err := qr.queryRewriter.Rewrite(ctx, query)
if err != nil {
return emptyResult(schema.GQLWrapf(err, "couldn't rewrite query %s",
query.ResponseName()))
}
qry = dgraph.AsString(dgQuery)
}

queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration)
queryTimer.Start()
resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: dgraph.AsString(dgQuery),
ReadOnly: true})
resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: qry, Vars: vars, ReadOnly: true})
queryTimer.Stop()

if err != nil {
Expand Down Expand Up @@ -150,3 +174,24 @@ func resolveIntrospection(ctx context.Context, q schema.Query) *Resolved {
Err: schema.AppendGQLErrs(err, err2),
}
}

// converts scalar values received from GraphQL arguments to go string
// If it is a scalar only possible cases are: string, bool, int64, float64 and nil.
func convertScalarToString(val interface{}) (string, error) {
var str string
switch v := val.(type) {
case string:
str = v
case bool:
str = strconv.FormatBool(v)
case int64:
str = strconv.FormatInt(v, 10)
case float64:
str = strconv.FormatFloat(v, 'f', -1, 64)
case nil:
str = ""
default:
return "", errNotScalar
}
return str, nil
}
7 changes: 7 additions & 0 deletions graphql/resolve/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ func (rf *resolverFactory) WithConventionResolvers(
})
}

for _, q := range s.Queries(schema.DQLQuery) {
rf.WithQueryResolver(q, func(q schema.Query) QueryResolver {
// DQL queries don't need any QueryRewriter
return NewQueryResolver(nil, fns.Ex, StdQueryCompletion())
})
}

for _, m := range s.Mutations(schema.AddMutation) {
rf.WithMutationResolver(m, func(m schema.Mutation) MutationResolver {
return NewDgraphResolver(fns.Arw(), fns.Ex, StdMutationCompletion(m.Name()))
Expand Down
3 changes: 2 additions & 1 deletion graphql/schema/gqlschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const (
SubscriptionDirective = "withSubscription"

// custom directive args and fields
dqlArg = "dql"
mode = "mode"
BATCH = "BATCH"
SINGLE = "SINGLE"
Expand Down Expand Up @@ -121,7 +122,7 @@ directive @auth(
add: AuthRule,
update: AuthRule,
delete:AuthRule) on OBJECT
directive @custom(http: CustomHTTP) on FIELD_DEFINITION
directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION
directive @remote on OBJECT | INTERFACE
directive @cascade on FIELD
Expand Down
Loading

0 comments on commit ad1ac8f

Please sign in to comment.