SEPTEMBER 8 2020
Building a Survey Forms App with GraphQL

Update: On April 16th 2021, Slash GraphQL was officially renamed Dgraph Cloud. All other information below still applies.
Introduction
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.
TL;DR
Tech Stack
- Frontend: React, Ant Design
- Backend: Slash GraphQL, Auth0
- Code: Github Repo
- Slash GraphQL makes it easy to clone sample apps and deploy a fresh instance using One-Click Deploy. Follow the docs here to deploy your own instance of Surveyo.
Designing a Schema
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.
Using Interfaces
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.
Using Unions
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.
Using Enums
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.
Lists
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.
Representing the Schema in TypeScript
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.
Authorization
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:
- Create a Single Page Application in the Auth0 dashboard.
- Create a "Rule" in Auth0's dashboard to add the claim to the token with the
User
field. - Create a Machine to Machine application.
- Create a Hook for
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.
Creating a Machine to Machine Application
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/
.
Creating Hooks for 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:
Conclusion
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!