Morpheus GraphQL provides two way of type resolving.
ResolveNamed
to define the
resolver for each type. More information on this approach can be
found in the next section.Named Resolvers
As mentioned earlier, in this approach we use ResolveNamed
to define the resolver function 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
.
instance Monad m => ResolveNamed m (Post (NamedResolverT m)) where
type Dep (Post (NamedResolverT m)) = ID
resolveNamed uid =
pure
Post
{ title = resolve (getPostTitleById uid)
}
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.
instance Monad m => ResolveNamed m (Query (NamedResolverT m)) where
type Dep (Query (NamedResolverT m)) = ()
resolveNamed () =
pure
Query
{ posts = resolve getPostIds,
post = \(Arg arg) -> resolve (pure (Just arg))
}
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.
instance Monad m => ResolveNamed m (Author (NamedResolverT m)) where
type Dep (Author (NamedResolverT m)) = ID
resolveNamed uid =
pure
Author
{ name = resolve (getAuthorName uid),
posts = resolve (getAuthorPosts uid)
}
instance Monad m => ResolveNamed m (Query (NamedResolverT m)) where
type Dep (Query (NamedResolverT m)) = ()
resolveNamed () = pure Query { authors = resolve getAuthorIds }
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.
instance Monad m => ResolveNamed m (Post (NamedResolverT m)) where
type Dep (Post (NamedResolverT m)) = ID
resolveNamed uid =
pure
Post
{ author = resolve (pure uid)
}
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
.