GraphQL+- -> Spec Compliant GraphQL

What’s missing from Dgraph in the GraphQL API

These are things that can be done in ±, but don’t have an equiv in our GraphQL.

Order is the order you find them by scrolling through the docs.

I’ve annotated with e : easy; h : hard; i : impossible; ? : needs thought. Then I expand below on what’s required.

To tackle each one, we should take it, put up an RFC, and work on it as an individual feature.

Query:

  • Languages (?)
  • Fuzzy Matching (e)
  • uid_in (e)
  • has (e)
  • geo type + geo queries (near, within, contains, intersects) (e)
  • pagination after (e) - but there’s other standard GraphQL pagination to look at
  • count (h)
  • query variables + var blocks (h)
  • value variables + math (h)
  • aggregation : min, max, sum & avg (h)
  • GroupBy (h/i)
  • expand (i)
  • normalize (i) - because the result is in a different format to the query
  • ignorereflex (e)
  • password type (e)
  • facets (?)
  • K-Shortest Path Queries (?)
  • Recurse Query (h/i) - result and query are different formats, might be possible to ask the query in the right format though.

Mutation :

  • RDF support (?)
  • upsert block / conditional upserts (???)

How might we put each of those in to GraphQL

Laguages (?)

Query side, we’d need a GraphQL way to write name@en:pl:., so something like name @lang(qry: [en, pl, .])

That should be ok. Mutations, I’m not sure about. Don’t think "rating@en": "tastes good" is GraphQL compliant.

Needs checking what’s ok for directives in input and variables.

Fuzzy Matching (e)

Easy, just means building a filter like

name: { match: { str: "hi", dist: 10} }

uid_in (e)

Easy, just add a filter

friends: { uid_in: 0x123 }

Would match with other GraphQL filters better if it were

friends: { ids: [ ... ] }

not sure how that maps down to Dgraph though.

has (e)

Easy, just add a filter like

filter: { has: friends }

geo type + geo queries (near, within, contains, intersects) (e)

Easy, the input should already be a JSON that’s compliant, just needs types for it and filters in query for near, within, contains and intersects.

Something like homeTown: { near: { point: [long, lat], distance: N}}

pagination after (e)

Looks easy, but not sure if this is the right sort of pagination to to include.

Sometimes the Relay-style Connections for pagination are used - we’ve been asked about that already: https://github.com/dgraph-io/dgraph/issues/933#issuecomment-549452031

So rather than just supporting after, it might be better to look at what pagination is popular/most-useful in GraphQL and support that.

Similarly the node interface could be a useful thing to add. Something like:

node(id: ID!) : Node

and

nodes(ids: [ID!]!) : Node

That might also open the door to adding types to objects - at the moment an object can only get one type. We could use mutations on the node interface to add extra types. So a mutation like

updateNodes(ids: [ID!]!, ...nodePatch...)

Where the ...nodePatch... lets you do things that apply to all nodes - add types, remove types, etc.

count + query variables + var blocks + value variables + math + aggregation : min, max, sum & avg (h + ?)

These are all related and hard.

For me, this is the most interesting and most general problem. If we have a good answer to this, we have a good answer to federation and joins in GraphQL <- that would be applicable to not just GraphQL on Dgraph, but federating GraphQL services in general.

I started writing about it here sometime ago. I think this is what we should invest in.

GroupBy (h/i)

This is all about variables and aggregation, so it requires a good answer to variables, blocks and aggregations.

expand (i)

This isn’t really possible in GraphQL. Pretty sure Lee had some reasons to reject * queries. I think it’s to do with the client knowing what it wants, and asking for exactly it’s data requirements.

Even if we had some way of allowing an expand inside a query block, all other GraphQL tools would show it as an error because it’s not part of the schema.

normalize (i)

Not sure this is possible. The whole point of normalize is to produce a result that’s in a different format to the original. But that contradicts GraphQL, that must give the result in the structure of the underlying types.

There may be some way we could allow the result type if we allowed some sort of mapping type that we could show was a combination of another set of types and then we could have a normalize query that takes a query/filter and produces this result like normalize … but I still don’t think that would be ok because GraphQL should have the query and result structure the same.

ignorereflex (e)

Should be easy - just a matter of allowing the directive and adding to the rewriting.

password type (e)

Easy - GraphSchema supported this. The idea was that the password mapped to the Dgraph password type, and was present in the input when adding an object, but wasn’t built into the objects that could be queried. On top of that you can support way for client-side apps to have users and logins.

facets (?)

No idea - needs lots of thought!

K-Shortest Path Queries (?)

Not sure - GraphQL requires that the query and response are in the same format. Whereas these give a response in a different structure. We might be able to modify the GraphQL version so it somehow says it’s a shortest path query, asks in the right structure and returns path and _path_ in a combined result.

Recurse Query (h/i)

Like shortest path queries, this has a query in a different structure to the response. We’d need to have a way to write it in the same structure as the result. Should be ok, the Dgraph version is all the edges, but doesn’t have the type information to know what the edges should be attach to.

RDF support (?)

Not 100% sure, if this is possible or desirable. The input to GraphQL is meant to be JSON - e.g. the JSON variables that accompany a query or mutation. It may be possible to have RDF variables in a spec compliant way, but no other tools support that, so it’s not something that would work in any other dev tools (we could build something in to Ratel).

It’s also probably not something that people would be building a GraphQL App around.

Technically actually doing it should be a matter of type checking and non-null checking the input.

upsert block / conditional upserts (??)

No idea.

This would take quite some thought and work - there’s just query and mutation blocks in GraphQL. It’s not clear how to do an upsert block.

1 Like

This should be moved to Confluence, so it is searchable there.

Update (Jul 7, 2020): Haha… I said that ^^ .

2 Likes

I would think this one part would be somewhat easy IMO by adding a @count directive. Then you just put in there a variables for what field (aka predicate) the user wants to count by. Set this up on the Schema side so if that field is requested then that count is received.

type User {
  username: String! @id
  totalUsers: Int @count(field: username) # mapped to count(uid) as it is the id.
  hasPosts: [Post] @hasInverse(field: author)
  countPosts @count(field: hasPosts) # mapped by script to count(User.hasPosts)
}

type Post {
  id: ID!
  countPosts: Int @count(field: id) # mapped to count(uid)
  published: DateTime @search
  author: User @hasInverse(field: hasPosts)
}
2 Likes

Maybe this can help: https://hasura.io/docs/1.0/graphql/manual/queries/aggregation-queries.html

Maybe this can help: https://github.com/hasura/graphql-engine/issues/5215#issue-645944604

Maybe this can help: https://dev.to/gopeter/how-to-add-a-groupby-field-to-your-graphql-api-1f2j

Maybe you can look at this: https://www.topquadrant.com/graphql/graphql-queries.html

Maybe this can help: https://hasura.io/docs/1.0/graphql/manual/mutations/upsert.html

You can probably use a similar approach to language tags suggested above

Maybe its not needed when there are fragments or maybe it can be passed as a filter compliant with GraphQL.

Maybe if no structure works, you can just return it as a string

Ultimately my suggestion is that it would be great to have complete compliance and whatever cannot be made compliant can be moved as plugins or extensions which add additional support. Not sure if I am making sense. That way you just need to maintain and learn one language

Prisma went this route to create Prisma Schema Language (PSL) which you can see here: https://github.com/prisma/specs/tree/master/schema

But I am not sure if that is really needed. But its fine in their case since it was supposed to be an ORM.

But if you have the control of the database itself like in your case, maybe you can opt for a ntive approach.

CC: @michaelcompton @mrjn

2 Likes

This has been requested before, but since we’re aiming for spec compliant GraphQL, I thought I’d put this here.

Lists in Dgraph behave like an unordered set, which makes perfect sense from a graph point of view, but they do not suit all types of data. The GraphQL spec says that “lists are ordered sequences of values”, and I think we should aim for that.

In DQL, if we want to use unordered lists for performance benefits, perhaps we could create a directive to guarantee order?

2 Likes

Adding this here too:

Also, Jan. 2020 spec added that interfaces can implement other interfaces.

But when I try to use it in Dgraph:

resolving updateGQLSchema failed because input:151: Unexpected Name “implements”

The spec does say that but it doesn’t define what that ordering should be. Lists in DQL and hence GraphQL are also ordered i.e. on doing multiple queries with the same dataset, you would get back the result in the same order. Can you check if the spec defines a particular ordering for different scalar types that I might be missing?

Excerpt, taken from GraphQL spec def:

Lists are ordered sequences of values wrapped in square‐brackets.

P.S. Spec Ref to Lists

I think what @pawan is saying, is that they ordered. Maybe just not ordered how the user specifies. They are pragmatically ordered by Dgraph under the hood. That means how you get it once, will be the same way it is next time, unless the list changes.

I think the normal is just treat all lists like JS array and order FIFO

2 Likes

Fair enough. Ordered doesn’t always mean “in the same order as the input”. In C++, a std::set is ordered, but stores items in sorted order. In Python, however, an OrderedDict retains the order of input.

I’ve looked at the spec and issues on GitHub and there is no indication of this there either, so perhaps we can move this from “GraphQL non-compliant” back to “nice-to-have”. Lists that retain their input order are definitely very useful - in the GraphQL survey forms app (schema) we built, we needed an order: Int field containing the index of each item, just so that we could query them in order - which felt awkward.

I’m trying to get in touch with the GraphQL team via IRC/Slack/GitHub, I will update here if there is a reply.

@ajeet, Here is an existing Issue/Feature Request:

1 Like