Morpheus GraphQL

AboutServerClientExamplesNamed Resolvers

Resolving

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.
  2. Named resolvers: In this approach, we use the type class ResolveNamed to define the resolver for each type. More information on this approach can be found in the next section.

Named Resolvers

Note: this feature will be introduced in version 0.18.0

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.