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:
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 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
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.
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}
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"}
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"}
Resolver Monad
The Resolver
type has Applicative
and Monad
instances that can be used to compose resolvers.
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)
Morpheus GraphQL provides two way of type resolving.
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.
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:
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.
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.
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
.