First Thoughts on GraphQL
ReactJS Conf just wrapped up last week and the videos started being released over the weekend. React Native got a lot of buzz, for extremely good reason, but equally as amazing was the session on GraphQL by Daniel Schafer and Jing Chen.
In a nutshell GraphQL allows you to query the entire state of your application in one go. The data is returned in the minimal exact shape you request. By itself that is pretty cool for many reasons. But what makes it killer is that the query language composes (actually a lot of things make it killer… but lets go with this for now). This is what lets Relay build up the query piecemeal on a component by component basis. I highly recommend watching the video of the session – I can’t do this thing justice in a paragraph!
So clearly there are some downsides but it is doable. Netflix has talked about doing similar things here with device / page specific end points. They’ve described it as making part of the client run on the server.
But GraphQL is essentially the one API Gateway to rule them all. And then you add Relay on top of it to build up the exact query you want – oh my.
So … basically this stuff is so cool I can’t wait for it so I’m going to make a crappy implementation myself.
At the top level of the graph / query (not sure what to call it exactly … lets go with graph) you have a bunch of root calls. We can represent this as a map with each root call as a key and then the fields of that root call as the value of the key. The shape of the root call can be whatever you want as long as it uniquely specifies the data you want. The examples we’ve seen so far are
One thing that I didn’t really grasp watching the video but the FAQ pointed out is that
Viewer are not some magical GraphQL thing – they are just implementations for Facebook’s needs –
Node has to do with their whole Facebook Graph stuff and
Viewer is just short hand for the current user. This means you can create whatever root calls you need for your application. Want the model in the root call? Make a
Movie(12345) root call. Have a bunch of REST/HTTP services? Make a
Resource(/api/movie/12345) root call. Want to mix github into your application? Add a
Github(/whatever/github/looks/like) root call.
Of course I’m just guessing.
As for the representation of this, I’m going to use a Variant (aka fancy tuple?). As I type this I am overcome with fear that I’m using this word wrong. But after watching Jeanine Adkisson’s talk at Clojure Conj I want to describe everything as a Variant. So for
Node(12345) we’d have
[:node 12345] and
[:resource "/api/movie/1234"]. The tuple can have as many fields as it needs.
One limit of this is that our language needs to support vectors as map keys and have meaningful equality so that if we join two graphs both with
[:node 12345] we don’t end up with two keys. Clojure is what I’m using and I think immutablejs paired with transitjs will work on the clientside (I want this to be true).
So now that we have root calls, what about the fields? I’m going with a set that has fields as keywords or tuples for nested fields.
Representing nested fields as a tuple might end up being a terrible idea – who knows! I’ve read horror stories from Clojure people about representing trees as vectors instead of maps … but come on, that looks so convenient / succinct.
The one thing missing is how to handle one-to-many. Short answer – I don’t really know.
[update: Daniel Schafer has clarified one-to-many’s in the FAQ so I updated this section]
Long answer – the FAQ has been updated with a bit more information on one-to-many’s. It sounds like there is an intermediate object between the field and the child node’s referred to as the ‘connection object’. This makes sense – you want some meta information around the collection. A primary use case is pagination as discussed in the session. In terms of representation we can keep the same style:
So yah, that is ugly, but can be represented. And if we leave
edges off we can get the count ignoring any cost associated with fetching the full collection. (and count sounds like it is still tricky though – what is a count in the world of pagination?)
Over the weekend I made a really rough implementation of the Query part of GraphQL (aka the easy 1% of all its features) before I read the Unofficial FAQ. Then the FAQ made me hate my implementation and I’m starting over and ignoring one-to-many’s for now.
So for my second take at this I’m going to make the root calls dispatch off of the first field in the variant (aka fancy tuple). I’m using a multimethod because I like the idea of being able to extend the root calls from anywhere in code.
execute multimethod will probably have to become a protocol at some point to enable validation and schema definitions – but whatever.
And lets say you want to implement the Github root call!
And now we can hydrate a graph which includes both datomic and github root calls.
For bonus points we can make executor’s return
core.async channels and select on them, but I got to leave something for the next blog post.
context here is basically the request context – if that makes sense. A user hits an end point, you get your session, cookie, and proper parameters, etc, bundle them up and that is the context. In this case we need our datomic database and the caller is responsible for making sure it is in the context. This is a bit opaque but works for now.
Now we need to execute on each field. This is going to be root call specific.
For datomic I ended up using another multimethod. The reason I went with a multimethod is to support computed fields. For example your database may only have
last-name but you want to support
name at a data schema level.
As you can see in the code example we leave a default implementation that simply gets at the entity itself so you only need specific overrides on a field by field basis. One nice thing about datomic is that all fields (attributes) are namespaced so this multimethod can be universal.
In order to expand an entity we just run
mapper on each field. This does not handle many-to-one or one-to-many. Again, I’m going to skip one-to-many for now but with many-to-one we can just use recursion.
I’m not sure how bullet proof this is, but with simple stuff it works.
core.match would be pretty swank instead of that
We can even make ‘fake’ fields.
So that is pretty neato.
One last example is something like
mutual_friends. I’m going to ignore the fact that it is one-to-many and just focus on the fact that this is a computed field that depends on context. Your database isn’t going to store the mutual friends – it is going to store each user’s friends and then do an intersection between them. This also means you need two users – a single entity isn’t enough to describe
We can use the context here. The assumption is that someplace in your middleware the current user has been shoved into the context. So if we want a
mutual_friends computed field we would do:
Again, kind of sucks that this context is opaque. Something along the lines of Prismatic’s plumbing would be cool so you could see the schema of required inputs – but its fine for now.
This post is like 20x longer than it should be, but whatever, all this stuff is so fun I couldn’t help but stream of consciousness about it.
Of course one-to-many is important and I’m currently unsure how to handle that but have a better idea due to the FAQ feedback (thanks!). I’d also like to make a weird non-datomic root call, kind of like the Github example I threw around earlier.
Finally I’d like to get validation and schema working. Datomic has a great schema that is just data. The type and cardinality are right there at your fingertips!
I really can’t agree with this post more – Facebook is killing it right now and I’m so glad they are sharing.