Dead-simple graphql with typeclasses and functional dependencies
Tags: functional, graphql, webdev
Published: 2021-02-22 05:56:07

---
title: Dead-simple graphql with typeclasses and functional dependencies
published: true
description: How to cut out boilerplate and reduce errors in your graphql queries and mutations
tags:
  - functional
  - graphql
  - webdev
cover_image: https://upload.wikimedia.org/wikipedia/commons/6/64/PureScript_Logo.png
---

When working with GraphQL queries and mutations, I want them to behave like a database: given an operation (ie upload a video), I want to look up the string I need to send, the variables the string needs and the response.

In SQL, this may be expressed like so:

```sql
select querystring, input_vars, output from graphql where action = 'upload_video';
```
Typeclasses in Haskell and PureScript are like database tables.  Using typeclasses, we can execute the above query purely with types. Let's see how!

# The GraphQL kind and the GraphQL class

Continuing the SQL analogy, a `kind` is the "type" of your column or, in our program, the type of your type.  There has been much ink spilled on comparing kinds to types of types, and it's not a perfect analogy, but in this case that's exactly what's going on.

In our example, each GraphQL query like `upload_video` will be indexed by a `data` type with `kind GraphQL`.  This index will point to three things - a type representing a string to send the server (called a `Symbol` in PureScript), the type of the input and the type of the output.

```haskell
foreign import kind GraphQL

class GraphQL (operation :: GraphQL) (gql :: Symbol) (i :: # Type) (o :: # Type) | operation -> gql i o
```

Let's see two different instances (rows) of our typeclass (table) GraphQL: one to get information from Filestack and one to upload a video to 8base.

```haskell
foreign import data GetFileStackUploadInfo :: GraphQL

instance graphqlGetFileStackUploadInfo ::
  GraphQL GetFileStackUploadInfo """
   query {
  fileUploadInfo {
    policy
    signature
    apiKey
    path
  }
}
""" () ( fileUploadInfo ::
        { policy :: String
        , signature :: String
        , apiKey :: String
        , path :: String
        }
    )

----
foreign import data VideoUpload :: GraphQL

instance graphqlVideoUpload ::
  GraphQL VideoUpload """mutation ($id: ID!, $filename: String!, $fileId: String!) {
        recordingUpdate(
            filter: {
                id: $id
            }
            data: {
                video: {
                    create: {
                        filename: $filename
                        fileId: $fileId
                    }
                }
            }
        ) {
            id
            video {
                id
                downloadUrl
                shareUrl
            }
        }
    }
""" ( id :: String, filename :: String, fileId :: String ) ( recordingUpdate ::
        { id :: String
        , video ::
            { id :: String
            , downloadUrl :: String
            , shareUrl :: String
            }
        }
    )

```

Each instance (row) of GraphQL is filled with four types (columns) where each type has the correct kind (datatype). For example, `GetFileStackUploadInfo` has the query (a multi-line string), the input variables type (in this case an empty record) and the output type (data for our file upload).

# A word on symbols

Symbols are one of the more confusing concepts in PureScript because they look exactly like strings. This works from the compiler's point of view because strings are values whereas symbols are types, so it would never confuse a string for a symbol or vice versa. But the fact that they look alike can be confusing for us humans.  What's more confusing is how something that looks like a string can be a type?

A string is just a list or array of characters with some sort of terminating "end-of-string" indicator.  Similarly, a symbol is just a list of types with kind `Char` and some sort of terminating indicator.

If we wanted to build symbols from the ground up in PureScript, we would do:

```haskell
foreign import kind Char

foreign import data D :: Char
foreign import data O :: Char
foreign import data G :: Char

foreign import kind Word

foreign import data ConsW :: Char -> Word -> Word
foreign import data EndW :: Word

type DOG = (ConsW D (ConsW O (ConsW G EndW)))
```

Thankfully, the PureScript compiler reduces this boilerplate by allowing us to do:

```haskell
type DOG = "DOG"
```

So `Symbol`-s are just a list of characters with some terminal value, making each unique symbol a unique type.

# Back to GraphQL

Now that I have my instances of GraphQL, I can create a function that sends the GraphQL to my backend (in this case, 8base).

```haskell
data Gql (operation :: GraphQL)
  = Gql

graphQL :: forall (operation :: GraphQL) (gql :: Symbol) (i :: # Type) (o :: # Type). GraphQL operation gql i o => IsSymbol gql => JSON.WriteForeign { | i } => JSON.ReadForeign { | o } => Gql operation -> Record i -> Aff { | o }
graphQL _ variables = do
  endpoint <- liftEffect $ get8baseURL
  token <- get8baseToken
  let
    output =
      { variables
      , query: replaceAll (Pattern "\n") (Replacement " ") (replaceAll (Pattern "\r\n") (Replacement " ") (reflectSymbol (SProxy :: SProxy gql)))
      }
  res <-
    AX.request
      ( AX.defaultRequest
          { url = endpoint
          , method = Left POST
          , responseFormat = ResponseFormat.string
          , content =
            Just
              (RequestBody.string (JSON.writeJSON output))
          , headers =
            [ RequestHeader "Authorization" ("Bearer " <> token) ]
          }
      )
  case res of
    Left err -> do
      liftEffect $ Log.info "Request did not go through"
      throwError (error $ AX.printError err)
    Right response -> case (JSON.readJSON response.body) of
      Left err1 -> throwError (error $ ("Could not parse " <> show response.body <> " err " <> show err1))
      Right ({ data: d } :: { data :: { | o } }) -> pure d
```

This function will work for _any_ "row" from the GraphQL "table". Meaning we can do:

```haskell
{ fileUploadInfo } <- graphQL (Gql :: Gql GetFileStackUploadInfo) {}
```

and we can also do:

```haskell
{ recordingUpdate } <-
      graphQL (Gql :: Gql VideoUpload)
        { id: input.recordingID
        , filename: fsResponse.filename
        , fileId
        }
```

Both are typesafe: they will fail with anything other than the exact input you need and they will return nothing other than the exact input you expect to receive from your typeclass defintion.

# An alternative to codegen

There are some great GraphQL codegen projects out there, and they definitely help reduce boilerplate. I personally prefer this approach because it pegs the output to the input. For example, checkout the codegen produced by [`graphql-codegen`](https://graphql-code-generator.com/) for the following graphql:

```graphql
scalar Date

schema {
  query: Query
}

type Query {
  me: User!
  user(id: ID!): User
  allUsers: [User]
  search(term: String!): [SearchResult!]!
  myChats: [Chat!]!
}

enum Role {
  USER,
  ADMIN,
}

interface Node {
  id: ID!
}

union SearchResult = User | Chat | ChatMessage

type User implements Node {
  id: ID!
  username: String!
  email: String!
  role: Role!
}

type Chat implements Node {
  id: ID!
  users: [User!]!
  messages: [ChatMessage!]!
}

type ChatMessage implements Node {
  id: ID!
  content: String!
  time: Date!
  user: User!
}
```
produces

```typescript
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  Date: any;
};


export type Query = {
  __typename?: 'Query';
  me: User;
  user?: Maybe<User>;
  allUsers?: Maybe<Array<Maybe<User>>>;
  search: Array<SearchResult>;
  myChats: Array<Chat>;
};


export type QueryUserArgs = {
  id: Scalars['ID'];
};


export type QuerySearchArgs = {
  term: Scalars['String'];
};

export enum Role {
  User = 'USER',
  Admin = 'ADMIN'
}

export type Node = {
  id: Scalars['ID'];
};

export type SearchResult = User | Chat | ChatMessage;

export type User = Node & {
  __typename?: 'User';
  id: Scalars['ID'];
  username: Scalars['String'];
  email: Scalars['String'];
  role: Role;
};

export type Chat = Node & {
  __typename?: 'Chat';
  id: Scalars['ID'];
  users: Array<User>;
  messages: Array<ChatMessage>;
};

export type ChatMessage = Node & {
  __typename?: 'ChatMessage';
  id: Scalars['ID'];
  content: Scalars['String'];
  time: Scalars['Date'];
  user: User;
};
```

So if I query:

```graphql
query { me { email } }
```

What do I get back?  In the typeclass solution, I get back something with the type `{ me :: { email :: String } }`. In the typescript solution, I get back `User`. This leads to three interrelated problems:

1. I have a lot more boilerplate now to check if the fields I want (ie `email`) are present.
1. The object model is too permissive, which means I need to hand-write a type `{ email :: String }` to enforce email's presence downstream, which defeats the purpose of codegen.
1. If `email` is not served back for whatever reason, the runtime error is shifted far downstream to the point where we try to do something with it.  In the typeclass solution, the failure happens directly at the point of query.

Note that this is no fault of graphql codegen (great project!). It's a limitation of typescript's type system and in general any system that does not have the ability to build up functional dependencies or dependent types.

# Use typeclasses!

Typeclasses with functional dependencies are a graphql-programmer's dream-come-true.  They allow you to create elegant simple relationships between graphql queries, input variables, and responses that are typesafe and predictable across your codebase. You can take typeclasses and functional dependencies for a spin in:

- [Haskell](https://en.wikipedia.org/wiki/Type_class#Functional_dependencies)
- [PureScipt](https://github.com/paf31/24-days-of-purescript-2016/blob/master/10.markdown)