Supporting GraphQL+- queries in GraphQL

We plan to add support for GraphQL± queries in GraphQL. This doc talks about how that would look, what are some of the design decisions and things that would be and would not be supported in the first version.

Implementation

We would support these as custom queries. We already have the @custom directive, another field called dql would be added to it.

For the schema below

type Tweets {
    id: String! @id
    text: String! @search(by: [fulltext])
    user: User
    timestamp: DateTime! @search
    score: Int @search
    streams: String @search
}
type User {
    screen_name: String! @id
    tweets: [Tweets] @hasInverse(field: user)
    followers: Int @search
}
type Query {
  myCustomTweetQuery(searchTerm: String!): [User] @custom({ dql: """
 query ($searchTerm: string) {
        var(func: type(Tweets)) @filter(anyoftext(Tweets.text, $searchTerm)) {
        Tweets.user {
            f as User.followers
        }
    }

    myCustomTweetQuery(func: uid(f), orderdesc: val(f)) {
        screen_name
        User.tweets(first: 5) {
            text
            score
        }
    }
}"""})
}

Note - The underlying GraphQL± query should have a named query with the same name as your GraphQL custom query i.e. myCustomTweetQuery in this case. We’ll only look for that key in the response.

The above query would filter tweets having GraphQL in them, get the authors for them and store them in a variable. The next named query would get the top authors and their first 5 tweets. Something like this isn’t possible right now. The way this would work is that we call the underlying GraphQL± query and then apply the GraphQL completion logic (ensure non-null fields are present etc.) on the result that we get. We can similarly have aggregation queries working.

There are certain caveats with this:

  • Auth - Auth rules won’t work directly with this since you are bypassing the GraphQL part by executing a GraphQL± query. To make this work properly, we would need to parse the GraphQL± query and apply the auth rules there before sending it to Dgraph. This would require some work to get working properly and for now, such queries should error out.

  • The data requested by the user would be limited by what they have asked in the underlying GraphQL± query. So if the ± query didn’t fetch the followers for a user but the GraphQL query asked for it, we won’t be able to return them. We could be smarter about this in a later iteration and write the bits that are asked by the user into the GraphQL± query.

Example

GraphQL query

queryTweets {
  text
  score
  user {
    name
  }
}

but if the underlying GraphQL± query just returned the tweet text and score, then we just return that. We don’t rewrite the ± query based on the GraphQL query for now.

  • Arguments won’t be allowed in the GraphQL query - Filters, pagination and ordering applied in the GraphQL query won’t directly be translated to the underlying query. Say you did a GraphQL query like
queryTweets {
  user(filter: {...}) {
    followers(first: 5) {
       ...
    }
  }
}

To apply the filter and other arguments properly we would again have the parse the underlying GraphQL± query and modify it to translate all of this to it. In the first version, we could avoid such queries and return an error for them.

CC: @abhimanyusinghgaur @mrjn @michaelcompton

The searchTerm could be used like this:

type Query {
  myCustomTweetQuery(searchTerm: String!): [Tweets] @custom({ dql: """
    query ($searchTerm: string) {
        var(func: type(Tweets)) @filter(anyoftext(Tweets.text, $searchTerm)) {
            Tweets.user {
                f as User.followers
            }
        }

        myCustomTweetQuery(func: uid(f), orderdesc: val(f)) {
            User.tweets(first: 5) {
                text
                score
            }
        }
    }
  """})
}
2 Likes

These sound good to me.

How would you parse User.tweets in myCustomTweetQuery to a list of Tweets? Do you need normalize for that?

1 Like

Good catch! The return type should have been [User] since we are returning the users in myCustomTweetQuery with the max followers and their tweets. I have updated it. If we used normalize then it can also be Tweets.

1 Like

How would you do a count type query here? Like a virtual field User.count_tweets?

Say a user has a field called numTweets like below.

type User {
    screen_name: String! @id
    tweets: [Tweets] @hasInverse(field: user)
    numTweets: Int
}

Then you could have a custom query like this

type Query {
  usersWithTweetCount: [User] @custom({ dql: """
  {
    usersWithTweetCount(func: type(User)) {
        User.screen_name
        numTweets: count(User.tweets)
    }
}"""})
}

and your GraphQL query would like below

{
  usersWithTweetCount {
    screenName
    numTweets
  }
}

which would return the users and the number of tweets for them.

Note - How the alias for the count field is the same as the virtual field name. This is important for us to map and verify the result.

Queries for max, min and other aggregations would also work similarly.

1 Like

But that’s unintuitive, since if I queryUser { numTweets }, I’ll get null or something.

Wouldn’t it be better if the dql is attached to the field, so that no matter how I arrive at the user (say from tweet.user or from queryuser directly or whatever), it will always have the value?m

We are not going to add that now but eventually we would end up supporting that as well through a directive or something. As you said that will help with non-custom queries. There is a separation between how custom queries work and how normal queries work. Custom queries as shared above just fetch the data using the GraphQL± logic and make sure the return types are as expected whereas normal queries also do the rewrite from GraphQL => ± before fetching the data.

Also, note that if the user wanted a different return type (different from the User so as to avoid getting null as response for normal queries), they could declare it with the @remote directive. Queries/Mutations are not generated for such a type.

There’s a further issue with count, because you’ll have to get the @count into dgraph, so there’ll need to be a solution for that.

I agree.

I’ve got no argument with doing a first cut, but this will need smoothing over before a release (but how do we smooth over once it’s on Slash - it’s harder to make breaking changes there).

I wonder if it’s worth thinking of this in terms of what the user is trying to do. I kinda see two paths:

  • I really want something bespoke - I think that path is solved by our existing @custom and the JS idea, both of which can call ± in the end if they want.
  • I want to lightly power up my GraphQL by injecting a bit of ± magic.

Adding ± to the schema is really the second path, so I’d be expecting it to compile that magic into the existing queries, rather than just giving me a fixed answer.

E.g. what if myCustomTweetQuery was required to be a variable, rather than a block. If it’s a variable, we can compile the rest of the GraphQL fields into the normal ± query that’s rooted at that variable, we can also apply auth in the usual way on that variable. Same thing would then work for custom ± on fields - we just compile the given custom into the query and pick up the expected variable (f we know the variable is a scalar, we can even then allow the user to add that to the filters).

You can count tweets (edges) without having a count index.