Microsoft recently announced that their GraphQL resolvers for Azure Cosmos DB is in public preview, and if, like me, you’ve made enough graph jokes in the work chat you are getting nominated to check it out. I have written this blog post to show worked examples for what I think are the most obvious use cases, starting from scratch all the way to a fully realised API. This post would not be possible without the help of kind strangers on the Internet, specifically this blog by Andrei Kamenev, with special thanks to commenter @sajins2005.
What even is a GraphQL Resolver, and how does this relate to my precious Cosmos DB Collections?
GraphQL is fast becoming the API technology of choice for developers and with the release of this tool Microsoft has given anyone with an existing Cosmos DB NoSQL collection the ability to expose this data in a brand new way.
But wait!!!
Before you spin up a new API manager Instance to try this out, does it even make sense to try map your data this way? Luckily, in most cases, there are some applications for this, but if you have very simple non relational data this may be more effort than it’s worth.
Graphs are all about the connections between nodes, and how to traverse paths between them, which is why for this example the data I used is a representation of the Auckland Train System, because I look at it every morning on my way to work.
What you need to get started
- An Azure Subscription you can play around in
- An API Manager Instance
- A Cosmos DB Instance
- Some knowledge of GraphQL
STEP 0
If you are just reading this for info, feel free to skim along as needed, but if you are following along feel free to grab the sample data I mocked up for this, and spin up some Azure resources.
Let’s Get Started
When creating the API we need to have a schema file already, so let’s start with that.
Schema pt 1
In your text editor of choice create a file called schema.gql or schema.graphql.
We need to define some types for our Nodes in the graph.
Our first type will be for a station
type Station{
id: ID!
name: String!
code: String!
}
All our Stations need an id, and they also definitely a name, and a code to display on the terminal signs. We are making these non nullable with the ! at the end of the type declaration.
If this is not there then the field is optional.
We will also define our Line Node.
type Line{
id: ID!
line: String!
}
Note: I have used line here as the “name” of the Line to avoid confusion in nested queries later on, this can also be avoided this way.
API Manager
In your API Manager instance, go to the ‘APIs’ blade > ‘+ Add API’ > ‘GraphQL’ and then fill out the form. Since we are creating a Graphql API on top of existing data the type is Synthetic.
You do not need a fallback endpoint or api url suffix, but feel free to put these in if you have them.
Resolvers
What even is a resolver? Because GraphQL is designed to represent non-graph databases like a graph, your data as it exists will not match the shape of the nodes and edges described in your schema file, you have to map the fields and queries yourself. This is more work up front but saves the end user time down the road.
Before we write our first resolver though, we have to design our first query.
Querying the graph
In your schema file add this Query.
type Query{
station(id: String!): Station
}
This Query shows that if we want a single station node we will have to pass a variable called ‘id’ in as a string.
Now we can write our first resolver.
Resolver #1
In the Resolvers tab fill out the form , click +Create and fill out the form as above, then in the policy section write the code that makes a read request for one item from a collection.
You can hide the connection string as a keyvault secret, I have left it obfuscated here.
You can get inputs by using @() notation to access variables, in this case the “id” value of Arguments.
Click Create to save.
This resolver is nice and easy, since a read request returns only a single JSON object.
The resolver attempts to map the Station schema onto this, finds that it can, and returns a Station.
We can test this in the test tab.
(Ignore the lines field for now).
query {
station(id: "9") {
code
id
name
}
}
Returns
{
"data": {
"station": {
"code": "BRT",
"id": "9",
"name": "Britomart"
}
}
}
Perfect!
We get exactly one station back (and my favourite station too, what a coincidence :P)
You should now be able to write your own query for finding a single Line node, and searching for a Station by code instead of id.
The next logical step from here would be to write a query request, if we think we might be expecting more than one result, for example if you wanted to just see all the stations at once.
Resolving a Query Request
The schema for this query is even simpler
type Query{
stations: [Station]
}
We don’t need any inputs, and we want one or more stations returned in an array.
<Etc>
<query-request>
<sql-statement>select * from c</sql-statement>
</query-request>
<response>
<set-body>
@{ var response = context.Response.Body.As<JObject>();
return response["items"].ToString();
}
</set-body>
</response>
</Etc>
Note: c in this context refers to the container, but is arbitrary and can be replaced.
Because a query request will return a JSON object that looks like this:
{"_rid": "Rp123546Y=\fw", "items": "{<All the stations>}", "_count" : 45, "endCursor":"","hasNextPage":"False"}
We have to add a tag with a tag mapping the items value to be the response. Since this maps to the return type of the query that is expected, you can get all the stations.
query {
stations {
code
name
}
}
Returns:
{
"data": {
"stations": [
{
"code": "HBC",
"name": "Hibiscus Coast"
},
{
"code": "Etc",
"name": "And so on"
}
]
}
}
You can now write a Query and Resolver to get all Lines yourself.
Now we know how to get nodes, we get to the part that makes Graphs and Transport Networks fun, the connections.
Edges, Connections
In the screenshots earlier in the article you may have noticed some fields I told you to ignore. These are how the Nodes relate to each other, in this case the Lines that each Station is on, and all the Stations that a Line services.
In the schema add these new fields
type Station{
id: ID!
name: String!
code: String!
lines: [Line]!
}
type Line{
id: ID!
line: String!
stations: [Station]!
}
Here I have made sure the lines and stations fields are mandatory, even if it is an empty array.
Now when we query a station, if we want the lines that service it, we will have to write a resolver for that specific field, because that data does not exist in the Station object, only a link to it.
Resolving Edges
Note in this case the Type is set to Station instead of Query, and we are resolving specifically the lines field.Note that in the parameter @(context.GraphQL.Parent[“line”]) has Parent here, where previously it was Arguments. That is because this field resolver will only ever execute as the child of a parent request, so we look in a different place in the context for it, the aptly named Parent key.
Also, even though the parent request was pointed at the Station Container, this one is pointing to Lines. In fact, there is nothing stopping you resolving requests to the same Cosmos DB instance, this new resolver complements the existing HTTP and Azure SQL Resolvers, so you can make a graph out of disparate data sources.
Lets test it in the test tab.
query {
station(id: "9") {
lines {
line
}
name
}
}
We can see that the nested queries have worked and returned the name of the station, and all the Lines associated with it:
{
"data": {
"station": {
"lines": [
{
"line": "Eastern"
},
{
"line": "Southern"
},
{
"line": "Western"
},
{
"line": "Onehunga"
},
{
"line": "Northern Busway"
}
],
"name": "Britomart"
}
}
}
Exactly what we want.
You should now be able to write the Resolver for the stations: [Station]! field on the Lines object.
Before we move on to Mutating the graph, I’ll share some quirks of the system you can exploit.
type Query{
stationEither(either: String):[Station]
}
Since the id and code fields are the same type, you can write a query that looks for either at the same time (but be careful of RU usage).
<sql-statement>
SELECT * FROM c where c.id = @either or c.code = @either
</sql-statement>
Mutating the Graph
A Mutation is how you change the graph, as opposed to querying it.
To do so you need to define a Type in your schema.
type Mutation{
createStation(id: String!, name:String!, code:String!,line: String): Station
}
This Mutations takes in a bunch of variables, and returns a station object.
The return type is just to echo back the object created, we could just have it as a Boolean value if we wanted.
The Resolver for this is:
<write-request>
<partition-key>
@(context.GraphQL.Arguments["id"].ToString())
</partition-key>
<set-body template="liquid">
{"id" : "{{body.arguments.id}}",
"name" : "{{body.arguments.name}}",
"code" : "{{body.arguments.code}}",
"line" : {{body.arguments.line}}
}
</set-body>
</write-request>
This example shows the other way you can manipulate data in a Resolver policy, with a Liquid map.
Example Mutation:
mutation CreateStation {
createStation(id: "99", name: "TEST", code: "TST", line: "[\"5\",\"6\"]") {
id
name
code
}
}
By default a write request is an upsert operation, so you can also use this to edit the graph.
Deleting Nodes
<delete-request>
<id>@(context.GraphQL.Arguments["id"].ToString())</id>
<partition-key>
@(context.GraphQL.Arguments["id"].ToString())
</partition-key>
</delete-request>
Once you have played around with it, you can associate your API with a product to publish it, and then test it with Postman.
Testing with Postman
I’ll leave the details of exposing and accessing your API to you, but once you have you can set up a Postman Collection for your co-workers to use.
Conclusion
Hopefully this guide will help you along the way when you try to set up GraphQL resolvers for Cosmos DB, feel free to leave a comment if this has helped you out.