Building a Survey Forms App with GraphQL - Dgraph Blog

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

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 Fields 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:

  1. Create a Single Page Application in the Auth0 dashboard.
  2. Create a “Rule” in Auth0's dashboard to add the claim to the token with the User field.
  3. Create a Machine to Machine application.
  4. 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!


This is a companion discussion topic for the original entry at https://dgraph.io/blog/post/surveyo-into/