Relations between Types
The most important part of GraphQL is a relation between types.
Relation via FieldConfig
Assume you have Author
and Post
types.
import { schemaComposer } from 'graphql-compose';
const AuthorTC = schemaComposer.createObjectTC({
name: 'Author',
fields: {
id: 'Int!',
firstName: 'String',
lastName: 'String',
},
});
const PostTC = schemaComposer.createObjectTC({
name: 'Post',
fields: {
id: 'Int!',
title: 'String',
votes: 'Int',
authorId: 'Int',
},
});
GraphQL allows to create additional fields in your types which may provide data from another type. For example, you may add a field posts
to the Author
type and write a resolve
function, so that this field will return an array of posts only for the current Author.
It can be done in the following manner:
AuthorTC.addFields({
posts: {
type: [PostTC], // array of Posts
resolve: (author, args, context, info) => {
return DB.Posts.find({ authorId: author.id });
},
},
});
It's quite easy. But now let's improve our relation and add new arguments, limit
and skip
:
AuthorTC.addFields({
posts: {
type: [PostTC], // array of Posts
args: {
limit: {
type: 'Int',
defaultValue: 10,
},
skip: 'Int',
},
resolve: (author, args, context, info) => {
return DB.Posts
.find({ authorId: author.id })
.limit(args.limit)
.skip(args.skip || 0);
},
},
});
What if we want provide a filter
argument, which adds the ability to filter by creation date, and min number of votes?
That would be achieved by the following code:
AuthorTC.addFields({
posts: {
type: [PostTC], // array of Posts
args: {
limit: {
type: 'Int',
defaultValue: 10,
},
skip: 'Int',
filter: `
input PostsFilterInput {
createdAtMin: Date
votesMin: Int
}
`,
},
resolve: (source, args, context, info) => {
const criteria = { authorId: source.id };
if (args.filter) {
if (args.filter.createdAtMin) criteria.createdAt = { $gt: args.filter.createdAtMin };
if (args.filter.votesMin) criteria.votes = { $gt: args.filter.votesMin };
}
return DB.Posts
.find(criteria)
.limit(args.limit)
.skip(args.skip || 0);
},
},
});
Hm, it has become quite long. And what if you have other Types which have relations with Posts (eg. Reviewer, Reader)? Copy/pasting our resolve
method probably is not a good idea. That's because in the future you may want to add a new filter
property, and that would mean scanning all your code and adding additional logic in all FieldConfigs
. So if you're met with such a problem, the next section is for you.
Relation via Resolver
If you need to use the same FieldConfigs in different Types graphql-compose provides the Resolver class. You may create a Resolver which will define type
, args
and resolve
and reuse it everywhere you need in your Schema.
However if you put posts
resolver in a separate file, you will face another problem
- in
Author
type you will usecriteria = { authorId: source.id }
for the resolve method; - in
Reviewer
-criteria = { reviewers: { $has: source.id } }
and so on.
In this case it's better to improve args.filter
by allowing to set authorId
and reviewerId
via arguments:
import { schemaComposer } from 'graphql-compose';
const postsResolver = schemaComposer.createResolver({
type: [PostTC], // array of Posts
args: {
limit: {
type: 'Int',
defaultValue: 10,
},
skip: 'Int',
filter: `
input PostsFilterInput {
createdAtMin: Date
votesMin: Int
authorId: ID
reviewerId: ID
}
`,
},
resolve: (source, args, context, info) => {
const { filter } = args;
const criteria = {};
if (filter) {
if (filter.createdAtMin) criteria.createdAt = { $gt: filter.createdAtMin };
if (filter.votesMin) criteria.votes = { $gt: filter.votesMin };
if (filter.authorId) criteria.authorId = filter.authorId;
if (filter.reviewerId) criteria.reviewerId = { $has: filter.reviewerId };
}
return DB.Posts
.find(criteria)
.limit(args.limit)
.skip(args.skip || 0);
},
});
And now you may create relations via ObjectTypeComposer.addRelation
method like so:
AuthorTC.addRelation('posts', {
resolver: () => postsResolver,
prepareArgs: {
filter: source => ({ authorId: source.id }),
},
projection: { id: true },
});
ReviewerTC.addRelation('posts', {
resolver: () => postsResolver,
prepareArgs: {
filter: source => ({ reviewerId: source.id }),
},
projection: { id: true },
});
ObjectTypeComposer.addRelation()
addRelation
method has the following arguments:
ObjectTypeComposer.addRelation(
fieldName: string,
opts: {
resolver: () => Resolver,
prepareArgs?: ObjectTypeComposerRelationArgsMapper,
projection?: ProjectionType,
description?: string,
deprecationReason?: string,
})
): ObjectTypeComposer<any, any>
resolver
Should be an arrow function that returns Resolver
. Wrapping resolver in an arrow function helps solving the hoisting
problem (when two types import each other).
prepareArgs
At runtime we should have the ability to prepare (ie. assign a value to) the args that will be passed to Resolver.
For example our Resolver has the arguments filter
, limit
, skip
and sort
.
prepareArgs
provides a way to set them up:
limit: 10
- hideslimit
arg from schema and set it equal to 10filter: (source) => value
- hidesfilter
arg form schema and at runtime evaluate its valuesort: null
- disables argument (hides it from schema and do not pass it to resolver)- all undescribed args (like
skip
) will be avaliable in the schema and will be avaliable in query
projection
Is a very useful option for extending requested fields in your query. It's very good practice to request from database only the fields included in our query. But sometimes we need additional fields, for example to provide the findById
resolver with an authorId
. For this purpose you need to use projection
.
PostTC.addRelation('author', {
resolver: () => AuthorTC.getResolver('findById'),
prepareArgs: {
id: (source) => source.authorId,
},
projection: { authorId: true },
});
So when you write the query
{
post {
author {
firstName
}
}
}
it will be transformed to
{
post {
author {
firstName
}
authorId # <--- added by `projection` option
}
}
Without projection
the resolver would try to populate the author
field, but args.authorId
would be undefined
. It would therefore be impossible for the query filter to find matching authors
and populate the author
field. Normally when a client wants to retrieve the author
field in a GraphQL Query, it would also need to provide the authorId
explicitly. By using a projection
we lift that responsility from the client, making querying easier and less cluttered.