Update: On April 16th 2021, Slash GraphQL was officially renamed Dgraph Cloud. All other information below still applies.
Surveyo is a survey tool powered by GraphQL. It lets you quickly spin up and respond to surveys, and advanced users can use the GraphQL endpoint to run complex queries.
After reading this, you will be able to design a schema for your own app, as well as handle authentication/authorization with Auth0. We will also talk about deploying your own version of Surveyo using Slash One-Click Deploy.
In our app, a user can create survey forms. Each form has multiple fields, and each field has its own type. For our app, we created text, dates, rating, and single choice fields (i.e. radio buttons). Ideally, something like this is represented as an algebraic data type. In GraphQL, we have 3 mechanisms for this - interfaces, unions, and enums.
A simple Form
type represents a new survey form. A Form
has an id
, title
and a list of Fields
:
type Form {
id: ID!
title: String!
fields: [Field!]!
}
Representing a Field
is a slightly more complex, since different kinds of fields require different parameters. For instance, a RatingField
must store the maximum possible rating (an integer), and a SingleChoiceField
must contain a list of options (strings). TextField
and DateField
do not require anything extra. Let’s try to represent these in our schema.
Here, we create a generic Field
type as an interface, and have every subtype implement its own fields:
interface Field {
id: ID!
title: String!
}
type DateField implements Field {
dummy: Int
}
type RatingField implements Field {
maxRating: Int!
}
type SingleChoiceField implements Field {
options: [String!]!
}
type TextField implements Field {
dummy: Int
}
Note that we had to create a dummy
field for DateField
and TextField
, because Dgraph currently does not support empty types (we’ve created an issue for it here).
This works great in theory. The types make sense, and we can even query fields based on the type, using inline fragments:
getForm(id: "0x1") {
id
title
fields {
id
title
kind: __typename
... on RatingField {
maxRating
}
... on SingleChoiceField {
options
}
}
}
Notice how we query for __typename
. This field is automatically generated, and serves as a tag to allow us to know the type of the object when it is served to us as a JSON (more on that later).
Elegant as this is, it has a problem: you can’t create a Form
in a single mutation using this. Interfaces are an abstract type, and cannot be instantiated directly. This means that we cannot construct a list of interfaces in-place:
addForm(input: [{
title: "New Form"
fields: [
"""
This field cannot be created, because GraphQL has no way of knowing its type!
"""
{
title: "New Field"
}
]
}]) {
form { id }
}
Instead, we have to perform a separate mutation to create each different type of field, and use their IDs as references while creating the Form
.
addForm(input: [{
title: "New Form"
fields: [
{ id: "0x1" }
{ id: "0x2" }
]
}]) {
form { id }
}
Of course, this is not a good idea, since you’d be making a lot of extra network calls, and if anything happens to your app halfway through, you’d be left with orphaned Fields
in your database. We’re looking for a way to make this better, and we’ve started a discussion on it here.
An alternative to this is unions, and they’re coming soon to Dgraph! Unions work just like interfaces do, except there don’t need to be any common fields. However, they possess the same downside that interfaces do - it’s impossible to construct a list of unions in-place. There’s an RFC in place that will make it much easier to model this type of schema in future, but we won’t dwell on this for now.
Another way of achieving what we want is a tagged union. Here, we do sacrifice some type safety, but we gain the ability to create a complete Form in a single mutation, since all the fields have concrete types. This is what it looks like:
type Field {
id: ID!
title: String!
kind: FieldKind!
options: [String!]
maxRating: Int
}
enum FieldKind {
Date
Rating
SingleChoice
Text
}
We use enums to indicate the exact type; options
and maxRating
are nullable fields, and are only filled when the FieldKind
is set to Rating
and SingleChoice
respectively.
Now that our Field
s work correctly, we have another issue to solve. In Dgraph, the list type actually behaves like an unordered set. We’ve currently created a list of fields, but the order in which they show up in our app isn’t guaranteed!
Usually, in cases like these, we ensure that the fields are correctly ordered using the order
argument in our query. The problem here is that there is no actual way to know what the expected order needs to be. We added an index: Int!
field to handle this. When creating the form, we assign an index to each Field
, so that we can query them in order.
This, too, could be improved, and there’s a discussion on it here.
While the database isn’t providing the same level of type safety any more, we can use TypeScript to gain some of it back via the client. Here’s how we can represent a Field
in TypeScript:
type Field = DateField | RatingField | SingleChoiceField | TextField;
interface DateField extends BaseField {
kind: 'DateField';
}
interface RatingField extends BaseField {
kind: 'RatingField';
maxRating: number;
}
interface SingleChoiceField extends BaseField {
kind: 'SingleChoiceField';
options: string[];
}
interface TextField extends BaseField {
kind: 'TextField';
}
interface BaseField {
id: string;
title: string;
}
This works the same way with all the above schemas. In the case of inheritance/unions, querying for kind: __typename
will tell TypeScript what type we’re dealing with. In tagged enums, the kind
field directly provides that function.
Once we’ve defined our types in this way, TypeScript won’t allow us to access to an invalid field, providing much better type safety to our client.
Slash GraphQL has an @auth
directive to specify authorization rules for different types in your schema. We use this to create and read permissions in our schema. For example, form responses can be only be read by the creator of the form:
type Response @auth(
query: {
rule: """
query ($USER: String!) {
queryResponse {
form {
creator(filter: { email: { eq: $USER } } ) {
email
}
}
}
}
"""
}
) {
...
}
Authentication in Slash GraphQL works using a signed JWT. We will use Auth0 to obtain a JWT for authentication. You can follow these steps to enable authentication in your instance of Surveyo:
User
field.AddUser
.Since steps 1 and 2 have been already been discussed in the linked documentation, we will discuss steps 3 and 4 in this section.
Go to the Auth0 dashboard, and under Applications, create a Machine to Machine application:
Next, you may have to add an authorized API for this application to the list of audiences in the GraphQL schema in Slash, which should look like https://<auth0-tenant>.auth0.com/api/v2/
.
AddUser
Whenever a new user signs up for the first time, we want to create a user in our Slash instance. We want to restrict this so that only Auth0 can create a new user. If we were to do this on the client-side, we would open a security vulnerability where anyone would be able to create users in our database. So, in our backend, a user can only be created by someone with the AddUser
role. See the following snippet in schema enforces this:
type User @auth(
add: { rule: "{$role: {eq: \"AddUser\"}}" }
) {
}
This user creation happens from a Post-User Registration Hook in Auth0 just after a user has signed up. The machine-to-machine application created in the previous section is called from this Hook to get a JWT that authorizes it to perform the mutation. This JavaScript code snippet demonstrates this in detail.
In the Hook, when the token is being fetched from the machine-to-machine application a Client Credentials Exchange Hook is called which adds the claim for the AddUser
role using this snippet.
All Auth0 configuration related snippets are present in the GitHub repository here. Don’t forget to update clientID
, clientSecret
, etc. before using them.
Once done, you will have a Client Credential Exchange Hook and a Post-Registration Hook like so:
In this blog, we introduced the Surveyo app, discussed some of the design decisions we made regarding the schema, and also discussed how to set up authorization in the app. The frontend was written in React, and is easily customizable. Deploy your own instance of Surveyo using Slash One-Click Deploy today!