Morpheus GraphQL

Server
Client
Examples
Code Gen

Server

Type System

Morpheus GraphQL covers all GraphQL data types with an equivalent Haskell representation. A prerequisite for these representation types is that they must be derived by Generic and provide corresponding GQLType instances.

Object types

Object types are represented in Morpheus with Haskell records, where the parameter m passes the resolution monad to the field resolution functions. The following code snippet, for example, defines the type Deity with a nullable field power and a non-nullable field name.

data Deity m = Deity
  { name :: m Text         -- Non-Nullable Field
  , power :: m Maybe Text   -- Nullable Field
  } deriving
    ( Generic
    , GQLType
    )

Arguments

GraphQL arguments can be represented with two ways:

Haskell records

we can use Haskell records to declare GraphQL arguments, where each field of a record represents a particular argument, and can be accessed by name.

data Query m = Query
  { deity :: DeityArgs -> m Deity
  } deriving
    ( Generic
    , GQLType
    )

data DeityArgs = DeityArgs
  { name      :: Text        -- Required Argument
  , mythology :: Maybe Text  -- Optional Argument
  } deriving
     ( Generic,
       GQLType
     )

This approach is quite convenient for representing multiple arguments, but cumbersome if we only need one argument for each field. That is why we also introduce "Tagged Arguments".

Tagged function arguments

Tagged arguments leverage type-level literals and enable GraphQL arguments to be represented as a chain of named function arguments. e.g. the following type defines GraphQL field deity with the optional argument name of type String.

data Query m = Query
  { deity :: Arg "name" (Maybe Text) -> m Deity
  } deriving
    ( Generic
    , GQLType
    )

Query

the GraphQL query type is represented in Morpheus GraphQL as a regular object type named Query.

data Query m = Query
  { deity ::  m Deity
  } deriving
    ( Generic
    , GQLType
    )

Mutations

In addition to queries, Morpheus also supports mutations. They behave just like regular queries and are defined similarly:

newtype Mutation m = Mutation
  { createDeity :: MutArgs -> m Deity
  } deriving (Generic, GQLType)

rootResolver :: RootResolver IO  () Query Mutation Undefined
rootResolver =
  RootResolver
    { queryResolver = Query {...}
    , mutationResolver = Mutation { createDeity }
    , subscriptionResolver = Undefined
    }
    where
      -- Mutation Without Event Triggering
      createDeity :: MutArgs -> ResolverM () IO Deity
      createDeity_args = lift setDBAddress

gqlApi :: ByteString -> IO ByteString
gqlApi = interpreter rootResolver

Subscriptions

In morpheus subscription and mutation communicate with Events, Event consists with user defined Channel and Content.

Every subscription has its own Channel by which it will be triggered

data Channel
  = ChannelA
  | ChannelB

data Content
  = ContentA Int
  | ContentB Text

newtype Query m = Query
  { deity :: m Deity
  } deriving (Generic,GQLType)

newtype Mutation m = Mutation
  { createDeity :: m Deity
  } deriving (Generic,GQLType)

newtype Subscription (m :: * -> *) = Subscription
  { newDeity :: SubscriptionField (m Deity),
  }
  deriving (Generic,GQLType)


type APIEvent = Event Channel Content

rootResolver :: RootResolver IO APIEvent Query Mutation Subscription
rootResolver = RootResolver
  { queryResolver        = Query { deity = fetchDeity }
  , mutationResolver     = Mutation { createDeity }
  , subscriptionResolver = Subscription { newDeity }
  }
 where
  -- Mutation Without Event Triggering
  createDeity :: ResolverM EVENT IO Address
  createDeity = do
      requireAuthorized
      publish [Event { channels = [ChannelA], content = ContentA 1 }]
      lift dbCreateDeity
  newDeity :: SubscriptionField (ResolverS EVENT IO Deity)
  newDeity = subscribe ChannelA $ do
    -- executed only once
    -- immediate response on failures
    requireAuthorized
    pure $ \(Event _ content) -> do
        -- executes on every event
        lift (getDBAddress content)

Scalar types

any Haskell data type can be represented as a GraphQL scalar type. In order to do this, the type must be associated as SCALAR and implemented with DecodeScalar and EncodeScalar instances.

data Odd = Odd Int  deriving (Generic)

instance DecodeScalar Euro where
  decodeScalar (Int x) = pure $ Odd (... )
  decodeScalar _ = Left "invalid Value!"

instance EncodeScalar Euro where
  encodeScalar (Odd value) = Int value

instance GQLType Odd where
  type KIND Odd = SCALAR

Enumeration types

Data types where all constructors are empty are derived as GraphQL enums.

data City
  = Athens
  | Sparta
  | Corinth
  | Delphi
  | Argos
  deriving
    ( Generic
    , GQLType
    )

Lists and Non-Null

GraphQL Lists are represented with Haskell Lists. However, since in Haskell each type is intrinsically not nullable, nullable GraphQL fields are represented with Maybe Haskell data type and non-nullable GraphQL fields with regular Haskell datatypes.

Interfaces

GraphQL interfaces is represented in Morpheus with TypeGuard. in the following data type definition every use of PersonInterface will be represented as GraphQL interface Person and allow server to resolve different types from union PersonImplements.

All types of the union PersonImplements must be objects and contain fields of type Person, otherwise the derivation fails.

-- interface Person
data Person m = Person { name ::  m Text }
  deriving
    (
      Generic,
      GQLType
    )

data PersonImplements m
  = PersonImplementsUser (User m)
  | PersonImplementsDeity (Deity m)
  deriving
    (
      Generic,
      GQLType
    )

-- typeGuard guards all variabts of union with person fields
type PersonInterface m = TypeGuard Person (PersonImplements m)

Unions

To use union type, all you have to do is derive the GQLType class. Using GraphQL fragments, the arguments of each data constructor can be accessed from the GraphQL client.

data Character
  = CharacterDeity Deity -- will be unwrapped, since Character + Deity = CharacterDeity
  | SomeDeity Deity -- will be wrapped since Character + Deity != SomeDeity
  | Creature { creatureName :: Text, creatureAge :: Int }
  | Demigod Text Text
  | Zeus
  deriving (Generic, GQLType)

where Deity is an object.

As we see, there are different kinds of unions. Morpheus handles them all.

This type will be represented as

union Character = Deity | SomeDeity | Creature | SomeMulti | Zeus

type SomeDeity {
  _0: Deity!
}

type Creature {
  creatureName: String!
  creatureAge: Int!
}

type Demigod {
  _0: Int!
  _1: String!
}

type Zeus {
  _: Unit!
}

By default, union members will be generated with wrapper objects. There is one exception to this: if a constructor of a type is the type name concatenated with the name of the contained type, it will be referenced directly. That is, given:

data Song = { songName :: Text, songDuration :: Float } deriving (Generic, GQLType)

data Skit = { skitName :: Text, skitDuration :: Float } deriving (Generic, GQLType)

data WrappedNode
  = WrappedSong Song
  | WrappedSkit Skit
  deriving (Generic, GQLType)

data NonWrapped
  = NonWrappedSong Song
  | NonWrappedSkit Skit
  deriving (Generic, GQLType)

You will get the following schema:

# has wrapper types
union WrappedNode = WrappedSong | WrappedSkit

# is a direct union
union NonWrapped = Song | Skit

type WrappedSong {
  _0: Song!
}

type WrappedSKit {
  _0: Skit!
}

type Song {
  songDuration: Float!
  songName: String!
}

type Skit {
  skitDuration: Float!
  skitName: String!
}
  • for all other unions will be generated new object type. for types without record syntax, fields will be automatically indexed.

  • empty constructors will get field _associaced with type Unit.

Input types

Like object types, input types are represented by Haskell records. However, they are not permitted to have monad parameters, as they represent serialisable values.

data Deity = Deity
  { name :: Text         -- Non-Nullable Field
  , power :: Maybe Text   -- Nullable Field
  } deriving
    ( Generic
    , GQLType
    )

Directives

Default GQL Directives

Moprheus GraphQL allows the use of standard GQL directives by applying them to GQLType.directives with the functions enumDirective, fieldDirective, typeDirective. The following examples demonstrate the use of the Deprecated directive on enums and type fields.

Field Diretives

data Deity = Deity
  { name :: Text,
    power :: Maybe Text
  }
  deriving
    (Generic)

instance GQLType Deity where
  directives _ =
    fieldDirective "power" Deprecated {reason = Just "some reason"}
      <> fieldDirective "name" Deprecated {reason = Nothing}

Enum Diretives

data City
  = Athens
  | Sparta
  | Corinth
  | Delphi
  deriving
    (Generic)

instance GQLType City where
  directives _ =
    enumDirective "Sparta" Deprecated {reason = Nothing}
      <> enumDirective "Delphi" Deprecated {reason = Just "oracle left the place"}

Custom Directive Definition

Moprheus GraphQL provides an API for defining custom GQL directives. These directive definitions should implement GQLType, Generic and GQLDirective instances. depending on GQLDirective.DIRECTIVE_LOCATIONS API requires implementation of one of the following visitor typeclass instances: VisitType, VisitField, VisitEnum.

The following example demonstrates the `RemovePrefix`` directive definition, which removes prefixes from GQL objects and input objects.

import Data.Morpheus ()
import Data.Morpheus.Types
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)

newtype RemovePrefix = RemovePrefix {prefix :: Text}
  deriving (Generic, GQLType)

instance GQLDirective RemovePrefix where
  type
    DIRECTIVE_LOCATIONS RemovePrefix =
      '[ 'LOCATION_OBJECT,
         'LOCATION_INPUT_OBJECT
       ]

instance VisitType RemovePrefix where
  visitTypeName RemovePrefix {prefix} _ = T.drop (T.length prefix)

now we can apply this directive on type MythologyDeity to remove prefix Mythology.

data MythologyDeity = MythologyDeity
  { name :: Text,
    power :: Maybe Text
  }
  deriving
    (Generic)

instance GQLType MythologyDeity where
  directives _ = typeDirective  RemovePrefix {prefix = "Mythology"}

Resolving

Applicative and Monad instances

Resolver Monad

The Resolver type has Applicative and Monad instances that can be used to compose resolvers.

Handling Errors

for errors you can use use either liftEither, MonadError or MonadFail: at the and they have same result.

with liftEither

resolveDeity :: DeityArgs -> ResolverQ e IO Deity
resolveDeity DeityArgs {} = liftEither $ pure $ Left "db error"

with MonadFail

resolveDeity :: DeityArgs -> ResolverQ e IO Deity
resolveDeity DeityArgs { } = fail "db error"

with MonadError

resolveDeity :: DeityArgs -> ResolverQ e IO Deity
resolveDeity DeityArgs { } = throwError ("db error" :: GQLError)

Resolving Approaches

Morpheus GraphQL provides two way of type resolving.

  1. Values as resolvers: In this approach, you specify values for the type definitions, where the resolvers are regular functions. However, this approach can sometimes lead to the possibility of overwhelming the database with a huge amount of queries.
    For this reason, the second approach, which involves automatic batching, might be the more suitable solution for you.

  2. Named resolvers: In this approach, we use the type class ResolveNamed to define the resolver function for each type that handles a batched list of dependencies. More information on this approach can be found in the next section.

Named Resolvers (Batching)

As mentioned earlier, in this approach we use ResolveNamed to define the resolver function (with batching) for each type. In this resolver definition, each type also defines its dependency (identifier), which is used by the compiler to provide a corresponding output resolution for certain input values. That is, if we want to resolve a type as a field of another type, we must specify a type dependency value for that particular type instead of the type value. For a better illustration, let's look at the following example:

App/Posts.hs

Let's say we want to create a GraphQL app for a blogging website where we can either retrieve all posts or retrieve them by ID. Scheme definition for this application would be as follows.

newtype Post m = Post
  { title :: m Text
  }
  deriving
    ( Generic,
      GQLType
    )

data Query m = Query
  { posts :: m [Post m],
    post :: Arg "id" ID -> m (Maybe (Post m))
  }
  deriving
    ( Generic,
      GQLType
    )

Now that we have type definitions, we can define their resolvers, starting with type Post. The following instance specifies that for each unique ID we can resolve the corresponding Post, where the post title is retrieved by the post ID.

getPostTitles :: Monad m => [ID] -> m [Maybe Text]
getPostTitles = <your db query>

resolvePosts :: Monad m => [ID] -> m [Maybe (Post (NamedResolverT m))]
resolvePosts ids = do
  titles <- getPostTitles ids
  pure (map (fmap toPost) titles)
  where
    toPost text = Post {title = lift (pure text)}

instance ResolveNamed m (Post (NamedResolverT m)) where
  type Dep (Post (NamedResolverT m)) = ID
  resolveBatched = resolvePosts

Let's go to the next step and define a query resolver. Since the query does not require an ID, we define its dependency with the unit type.

To resolve the post and posts fields, we only get post ids and pass them to the resolve function, which then resolves the corresponding Post values by calling the ResolveNamed instance of the type Post with those ids.

allPostIds :: m [ID]
allPostIds = <your db query>

resolveQuery :: Monad m => Query (NamedResolverT m)
resolveQuery =
  Query
    { posts = resolve allPostIds,
      post = \(Arg arg) -> resolve (pure arg)
    }

instance ResolveNamed m (Query (NamedResolverT m)) where
  type Dep (Query (NamedResolverT m)) = ()
  resolveBatched = pure . map (const $ Just resolveQuery)

In the last step, we can derive the GraphQL application using the data type NamedResolvers by using a single constructor NamedResolvers without any fields.

postsApp :: App () IO
postsApp =
  deriveApp
    (NamedResolvers :: NamedResolvers IO () Query Undefined Undefined)

In the background, the function deriveApp traverses the data types and calls their own instances of NamedResolver for each object and union type. In this way, a ResolverMaps (with type Map TypeName (DependencyValue -> ResolveValue)) is derived that can be used in GraphQL query execution.

As you can see, the ResolverMaps derived in this way can be merged if the types with the same name have the same GraphQL kind and the same dependency. Therefore, types in applications derived with NamedResolvers can be safely extended, which we will see in the next section.

App/Authors.hs

Let's say there is another team that wants to use the Posts application as well, but also needs to provide Authors information. The new application should allow querying of all existing Authors and extend the post type with the field author.

One way to address these new requirements would be to rewrite our old application, but that will impact (or even break) the existing application. Here, named resolvers can be of additional help to us, as Apps derived with named resolvers can be merged. We can define our Authors app separately and then merge it with the existing one.

In the following code snippets we define the Author and Query types.

data Author m = Author
  { name :: m Text,
    posts :: m [Post m]
  } deriving (Generic, GQLType)

data Query m = Query
  { authors :: m [Author m]
  }
  deriving (Generic, GQLType)

As you can see, we can query authors, with each Author having their fields name and posts. in the same manner as before, we can also provide their resolver implementation.

getAuthorPostIds :: Monad m => [ID] -> m [[ID]]
getAuthorPostIds = <your db query>

getAuthorNames :: Monad m => [ID] -> m [Text]
getAuthorNames = <your db query>

getAuthors :: Monad m => [ID] -> m [Maybe (Author (NamedResolverT m))]
getAuthors ids = do
  postIds <- getAuthorPostIds ids
  names <- getAuthorNames ids
  pure (zipWith toAuthor names postIds)

toAuthor :: Monad m => Text -> [ID] -> Maybe (Author (NamedResolverT m))
toAuthor authorName postId =
  Just
    Author
      { name = lift (pure authorName),
        posts = resolve (pure postId)
      }

instance ResolveNamed m (Author (NamedResolverT m)) where
  type Dep (Author (NamedResolverT m)) = ID
  resolveBatched = getAuthors

getAllAuthorIds :: Monad m => m [ID]
getAllAuthorIds = undefined

resolveQuery :: Monad m => Query (NamedResolverT m)
resolveQuery = Query {authors = resolve getAllAuthorIds}

instance ResolveNamed m (Query (NamedResolverT m)) where
  type Dep (Query (NamedResolverT m)) = ()
  resolveBatched = pure . map (const $ Just resolveQuery)

At this stage, we have already implemented Authors and Query and now we can also start thinking about the Post Type.

First note, that the post type used in this app does not need to be imported from the App/Posts.hs. We can simply define our type Post with the new field author and all other fields associated with the post type will be automatically completed by the app App/Posts.hs, after the merging.

-- is alternative to extend type
newtype Post m = Post
  { author :: m (Author m)
  } deriving
    ( Generic
    , GQLType
    )

Now we can start implementing the resolver for it. It is of critical importance here, that the dependency of this type is the same as the dependency of Post in App/Posts.hs. If the argument of the function does not match, one of the implementations will be unable to decode the argument during resolution and it will fail.

getPostAuthorIds :: Monad m => [ID] -> m [ID]
getPostAuthorIds = <your db query>

resolvePosts :: Monad m => [ID] -> m [Maybe (Post (NamedResolverT m))]
resolvePosts ids = do
  autors <- getPostAuthorIds ids
  pure (map toPost autors)
  where
    toPost autorId = Just $ Post {author = resolve (pure autorId)}

instance ResolveNamed m (Post (NamedResolverT m)) where
  type Dep (Post (NamedResolverT m)) = ID
  resolveBatched = resolvePosts

Since all resolvers are implemented, we can also derive the application. Note that this application can be used as a standalone application, however the standalone version can only display the information provided by the Authors, i.e. the Post type will only have one field authors, and in the query we can only access authors.

authorsApp :: App () IO
authorsApp =
  deriveApp
    (NamedResolvers :: NamedResolvers IO () Query Undefined Undefined)

However, if we want to access information from both apps, the next section will show us how to merge them.

App.hs

The data type App has a Semigroup instance that allows to join multiple apps together.

app :: App () IO
app = authorsApp <> postsApp

Since both the Post type definitions have the same dependency ID, the interpreter safely merge these two apps where type Post will be extended with new field author.