Type Safe, Functional GraphQL with Purescript - Part 2

Last time we left off with a very minimal GraphQL API with its resolver function written in Purescript. Now, let's look at more complex resolver functions that have asynchronous return values.

Input and Context

First, let's take a closer look at the GraphQL resolver function. Open up your index.js file and change the rootValue to { test: function () { console.log(arguments); } }. Run the server, node src/index.js, open localhost:4000 and query { test }. Check your terminal and you should see three arguments. The first is an empty object. This is the input arguments. Since our test query has no inputs, this is empty. The second is the Express.js Request object. Finally, the third is the GraphQL info. The first argument will be very useful but we don't have use of the other two. GraphQL will change the second argument to a context variable if one is passed into the graphql config. This is very useful for passing in environment variables or other global config.

Let's model this resolver function in Purescript with some types.

type Input = Foreign
type Context = Foreign

type Resolver a = Input -> Context -> a

Purescript's Foreign type is a nice stand-in for any Javascript type when going between the two. Our Resolver type is a function that takes an Input and a Context and returns some value a. OK, so that's not really true. Purescript is curried by default so our Resolver type is actually a function that takes an Input and returns a function that takes a Context and returns an a. In Javascript this would compile to:

function resolver(input) {
  return function(context) {
    // do something to create `a`
    return a;
  }
}

This won't work with GraphQL because it expects a non-curried resolver function. We're in luck! The good people behind Purescript gave us some convenient types to model uncurried functions. For now, we'll use Fn2.

type Resolver a = Fn2 Input Context a

Let's create a slightly more complex query called echo that returns what you send it. First, update the schema in our schema.graphql file.

type Query {
  test: Boolean
  echo(text: String!): String
}

Now, revert our index.js file to have rootValue: api again. Then open the API.purs file and add our echo function.

import Prelude
import Data.Foreign (Foreign)
import Data.Function.Uncurried (Fn2, mkFn2)

type Input = Foreign
type Context = Foreign
type Resolver a = Fn2 Input Context a

test ...

echo :: Resolver String
echo = mkFn2 (\input context -> input) -- or mkFn2 const

Before this will compile we need to add some dependencies. Run bower install --save purescript-prelude purescript-foreign to install the base Prelude library and the foreign library for interfacing with Javascript values. Try to build your project with npm run build. You should see an error that it could not match a Foreign to a String. This is because our input variable is a Foreign type and we still need to extract the text value from it.

We can use Foreign's readProp function (or ! operator) to extract the value and readString to convert it to a String. These functions can cause exceptions if they fail so we have to deal with the error case. Let's use runExcept to transform the Except into an Either. Then let's use a case statement to return either the text value or the error message.

import Prelude
import Control.Monad.Except (runExcept)
import Data.Either (Either(..))
import Data.Foreign (Foreign, renderForeignError, readString)
import Data.Foreign.Index ((!))
import Data.List.NonEmpty (head)
...

echo :: Resolver String
echo = mkFn2 $ \input context ->
  case runExcept (readString =<< input ! "text") of
       Left  errors -> renderForeignError $ head errors
       Right x      -> x
-- Could also be:
-- either (renderForeignError <<< head) id $ runExcept $ readString =<< input ! "text"

Compile your project npm run build, run it node src/index.js, visit localhost:4000, and send { echo(text: "Hello World!") }. You should see the return { "data": { "echo": "Hello World!" } }.

GraphQL actually does a lot of type validation from the schema you create. If you pass an invalid type into echo(), you'll see a GraphQL error, not our error handing above. Since we know what our inputs will be we can use Purescript's Records to "type" the input without having to parse it.

type Resolver input a = Fn2 input Context a

echo :: Resolver { text :: String } String
echo = mkFn2 $ \input context -> input.text
-- Could be: mkFn2 $ const <<< _.text

Wow, that's a lot better. Having { text :: String } as our input type is telling Purescript that our input will be a Record with at least a text value. This enables us to use the dot operator to access the value.

We can do the same for our Context too. Let's pass in a context to our graphql() function in our index.js file.

app.use('/', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true,
  rootValue: api,
  context: {
    modifier: "Hello"
  }
}));

Now that we know the type of our Context we can remove the Foreign and add the type.

type Context = { modifier :: String }
type Resolver input a = Fn2 input Context a

echo :: Resolver { text :: String } String
echo = mkFn2 $ \input context -> context.modifier <> " " <> input.text

Build, run, and visit your API. Send { echo(text: "World") } and you should see "Hello World" come back. You can see how this Context is useful for passing global state or ENV into our Purescript functions.

Purescript and its Effects

Purescript is a purely functional language, which means it cannot have any side effects without specifying that it is going to do so. Side effects include accessing and modifying global state (ie. console logging, HTTP requests, database access, etc).

Most likely, when you're playing around with Purescript, you'll want to log things to the terminal for debugging and learning. However, this is an Effect and we cannot do it in our echo function above. We have to modify our Resolver type to allow for Effects and then declare the Effects we want to use in our function.

import Prelude
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Eff.Uncurried (EffFn2, mkEffFn2)

type Context = { modifier :: String }
type Effects = ( console :: CONSOLE )
type Resolver input a = EffFn2 Effects input Context a

echo :: Resolver { text :: String } String
echo = mkEffFn2 $ \input context -> do
  log input.text
  pure $ context.modifier <> " " <> input.text

We also need to install the console dependency before this will compile,
bower install --save purescript-console. Build, run, and visit your API. Send a string into echo(text: "") and you'll see the response. Check the terminal and you'll see your input logged there.

You'll notice we switched to the Eff version of the uncurried type and function. We create an Effects type to house the effects of our API and passed it to EffFn2. We then use mkEffFn2 to create our uncurried function with an Eff this time. We can use do syntax to log our input and then return out output but wrapping it in Eff using pure. One important thing to note is that we label our CONSOLE effect with console. Doing this means we can only call effectful functions that also have a CONSOLE effect labeled console.

Until Next Time

Let's pause here. We've seen how we can accept typed input from GraphQL, how to pass in global state or ENV, and how to perform effects in our resolvers.

Check out the code on Github.