How to resolve complex types with AppSync resolvers
Trivial resolvers help writing less code
Resolving fields in GraphQL
In GraphQL, resolvers can be added for any fields. These can be top-level, such as Queries and Mutations, a type defined in the schema, or even for scalar fields for complex types.
Let's see this schema:
type User {
id: ID!
name: String!
}
type Group {
id: ID!
users: [User!]!
}
type Query {
user(id: ID!): User
group(id: ID!): Group
}
schema {
query: Query
}
Here, you can define a resolver for Query.user
, Query.group
, as top-level resolvers, Group.users
as a resolver to a complex type, and also
User.id
, User.name
, and Group.id
that resolve to scalar types.
This allows fine-grained control over the resolution process, as you can customize every part of it. For example, the Query.user
can fetch a user object
from the database, the Query.group
fetch a group, and the Group.users
can query users in a group. Then all the other fields can resolve based on the
result of these resolvers.
But what if you don't want to be this granular? Maybe it's easier or more performant to fetch the group and the users in it in a single call and not break these into two operations. In that case, this multi-level resolving structure is in the way. So, is there an easy way to implement it?
Trivial resolvers
When you don't define a resolver for a field, a default one is added automatically by AppSync. These default resolvers are simple: they fetch the property of the source object that matches the field name.
Let's see an example how they work for an object!
If the Query.user
resolves to an object with this structure:
{
"id": "1",
"name": "user 1"
}
Then the User.id
can simply get the id
property, and the User.name
can get the name
.
This is the reason why you don't need that many resolvers when the database structure matches the GraphQL schema. In that case, most of the objects have the expected fields and the trivial resolvers are enough.
But the choice is always there: you can decide to overwrite the default and implement it a different way.
One example when I had to do this is when the database stored timestamps in UNIX timestamp format (seconds from epoch) while the GraphQL schema needed
AWSDateTime
which is in ISO8601 format. To bridge the gap, I had to add a resolver that called the $util.time.epochMilliSecondsToISO8601
function
with the source property.
Lists
The same works for lists too, in this case the source object needs to be an array. For example, if the Query.group
returns this JSON:
{
"id": "1",
"users": [
{
"id": "1",
"name": "user 1"
},
{
"id": "2",
"name": "user 2"
},
]
}
Then the default resolver will convert the two objects into two User types with id
and name
fields.
Supporting both cases
What if sometimes the Group
object has users
field but sometimes it doesn't? For example, maybe there is a different query that also returns a Group:
type Query {
primaryGroup: Group
# ...
}
Then let's say the Query.primaryGroup
resolves to this object:
{
"id": "1"
}
In this case, there is no users
property so the default resolver is not able to resolve this field automatically. To support this case, you can add a
Group.users
resolver and check the source object ($ctx.source
) if there is a users
and only go to the database if it is not present.