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.