Inspired by GraphQL and making use of the wonderful Github API I decided to take a stab implementing a portion of the as-of-yet unreleased GraphQL specification. This is a query only API and maps to just a subset of Github entities — namely Users, Organizations, Repositories, and Commits.
The server is written in Clojure and the source code can be found here, although it is in no condition to be used for anything other than a good laugh.
This has been a blast to write and I want to thank the GraphQL team at Facebook for being so open and helpful. I can't wait until GraphQL and Relay are out in the world. API's will get cleaner (no more clientside concerns leaking in) and declarative data requirements will be a boon to both productivity and quality.
The simplest query is a single entity with some immediate properties. Here we are using the Repository root with an id of `facebook/react` and getting just the basic information.
Nested relationships can also be queried. For example Github Repositories have an Owner which we can include in the query. This query does not auto-run, click on `RUN` to execute the query.
We don't have to stop at a single nested relationship, we can also look at the Repository's Organization in the same query.
Finally, nested objects don't have to be limited to relations. We can represent a datetime as a nested object even though it is just a string in the Github API.
So far I've only shown one to many relations. We can also query related collections. For example we can look at some of the contributors to the React Repository.
In the query for collections we are able to get the count as well as a collection of nodes — in this case corresponding to a User entity. We also specify that we only want the first 10. This is not necessary but if left off we'd get back all contributors which is probably not desirable. For the purposes of this demo I've also limited the server to returning 50-100 items at max from a collection. A cursor was also included which can be used for paging (shown later).
It might also be interesting to look at the profile tab for this query. Click on the `PROFILE` button to view timing information for the query. Each piece of data in the query result has timing information associated with it. The dark blue bar is how long that particular item took to `execute` while the light blue bar is how long it took the full value — including children — to evaluate.
Note that every contributor was fetched in parallel. I am using Aleph for the HTTP library as well as Manifold for the query execution (thank you Zach Tellman). These are two non-blocking API's for Clojure. Collections are represented as streams and individual items in the stream as deferreds. This means nothing in execution is blocking and the query can be traversed in parallel.
Go ahead and bump the number up from 10 to something higher if you'd like. I have no idea what my rate limiting is for Github but so far have yet to hit it while developing and I've made some terrible, terrible mistakes.
For pagination all that we have to do is grab a cursor and include an `after` call on the contributors. In GraphQL cursors are opaque objects so can include all kind of smart information to improve pagination. For my purposes they are dumb ids and we always start at page 1 and just drop until we find the cursor. So ya ... room for improvement!
Relay is supposed to make this even easier. Simply change your `first` count and the proper `after` is created. Have I mentioned I can't wait?
So `first` and `after` are just two examples of calls but each collection can export other calls for use. For instance Facebook has a call roughly like `birthday_in_range(date1, date2)` which I would presume returns friends with birthdays in the date range given. I haven't spent much time on this but did create a `starts_with` call as a proof of concept. This will also introduce the Organization root which does what you might expect.
For the curious in the crowd I've implemented calls as transducers. So as you chain calls they get `comp`d into a single transducer and passed to Manifold to transform the stream. Right now calls are also order dependent. Whether these are good ideas is something the people writing the GraphQL spec will answer for me :).
Another feature of GraphQL is supplying arguments to a field. I'm actually not sure if this is implemented the same as a call or not — again the spec will answer this. I've implemented them differently than calls. The only example I have of this is providing a date format string to a date object.
As trivial of an example as this is I actually think it is pretty useful. I would use this.
Multiple Root Values
Everything so far has been a single root value, but we can also request multiple ids at once.
Here we are getting back information on multiple Users as well as their Organizations and Repositories. This is another fun one to look at the profile graph for.
I've implemented this as two separate roots — User and Users. I'm interested how this ends up working in Relay with a single client side store.
Not all roots need to be parameterized. For example Facebook has one called Viewer which corresponds to the current user. I don't have a similar usecase but for giggles I made a Facebook root which corresponds to the Facebook Organization.
It is identical in structure to a query result from `Organization(facebook)`.
GraphQL also supports inspecting objects. When you define an object you can specify a description, the fields, a description of each field, etc. I don't support descriptions (well I do, I just don't have any because /effort) but we can still inspect type and field information.
This type of schema documentation allows tools to be built up around GraphQL. Nifty.
Each query gets validated before it is executed. The code that does this is by far my most hated part of the project, but it does produce reasonable error messages.
All bets are off for execution runtime exceptions :)
This wraps up the tour of my little implemenation. Thanks again to the GraphQL team. I seriously can't wait to get my hands on the real stuff.
I leave you with a query which should without a doubt kill my rate limit. Enjoy!