NestJS Graphql automation library for building performant API
The library allows to build efficient graphql API helping overcome n+1 problem and building hasura-like search interface with the minimum dependencies.
With the library you will be able to build queries like that easily, using decorators and having full controll over everything.
{
users(
where: {
id: { in: [1,2,3,4] }
task_title: { like: "%Task%" }
}
order_by: {email: ASC, created_at: DESC}
paginate: {page: 1, per_page: 10}
) {
id
fname
lname
email
tasks(order_by: {id: ASC_NULLS_LAST}) {
id
title
}
}
}
npm i nestjs-graphql-tools
or
yarn add nestjs-graphql-tools
@GraphqlLoader()@Loader() parameter as a first parameter@Resolver(() => UserObjectType)
export class UserResolver {
@ResolveField(() => TaskObjectType)
@GraphqlLoader()
async tasks(
@Loader() loader: LoaderData<TaskObjectType, number>,
@Args('story_points') story_points: number, // custom search arg
) {
const tasks = await getRepository(Task).find({
where: {
assignee_id: In<number>(loader.ids) // assignee_id is foreign key from Task to User table
story_points
}
});
return loader.helpers.mapOneToManyRelation(tasks, loader.ids, 'assignee_id'); // this helper will construct an object like { <assignee_id>: Task }. Graphql expects this shape.
}
}
@Resolver(() => TaskObjectType)
export class TaskResolver {
constructor(
@InjectRepository(User) public readonly userRepository: Repository<User>
) {}
@ResolveField(() => UserObjectType)
@GraphqlLoader({
foreignKey: 'assignee_id' // Here we're providing foreigh key. Decorator gather all the keys from parent and provide it in loader.ids
})
async assignee(
@Loader() loader: LoaderData<TaskObjectType, number>,
@Filter(() => UserObjectType) filter: Brackets,
) {
const qb = this.userRepository.createQueryBuilder('u')
.where(filter)
.andWhere({
id: In(loader.ids) // Here will be assigne_ids
})
const users = await qb.getMany();
return loader.helpers.mapManyToOneRelation(users, loader.ids); // This helper provide the shape {assignee_id: User}
}
}
@GraphqlLoader decorator provides ability to preload polymorphic relations
To be able to use it you need to decorate your resolver with @GraphqlLoader decorator. Decorator has parameter which allows to specify fields which needs to be gathered for polymorphic relation.
@GraphqlLoader({
polymorphic: {
idField: 'description_id', // Name of polymorphic id attribute of the parent model
typeField: 'description_type' // Name of polymorphic type attribute of the parent model
}
})
This decorator will aggregate all types and provide ids for each type. All aggregated types will be aveilable in @Loader decorator. It has attribute which called `polymorphicTypes.
PolmorphicTypes attribute shape
[
{
type: string | number
ids: string[] | number[]
}
]
// Parent class
// task.resolver.ts
@Resolver(() => TaskObjectType)
export class TaskResolver {
constructor(
@InjectRepository(Task) public readonly taskRepository: Repository<Task>,
@InjectRepository(Description) public readonly descriptionRepository: Repository<Description>
) {}
@ResolveField(() => [DescriptionObjectType])
@GraphqlLoader()
async descriptions(
@Loader() loader: LoaderData<TaskObjectType, number>,
@SelectedUnionTypes({
nestedPolymorphicResolverName: 'descriptionable',
}) selectedUnions: SelectedUnionTypesResult // <-- This decorator will gather and provide selected union types. NestedPolymorphicResolverName argument allows to specify where specifically it should gather the fields
) {
// Mapping graphql types to the database types
const selectedTypes = Array.from(selectedUnions.types.keys()).map(type => {
switch (type) {
case DescriptionTextObjectType.name:
return DescriptionType.Text;
case DescriptionChecklistObjectType.name:
return DescriptionType.Checklist;
}
});
const qb = this.descriptionRepository.createQueryBuilder('d')
.andWhere({
task_id: In(loader.ids),
description_type: In(selectedTypes) // finding only selected types
})
const descriptions = await qb.getMany();
return loader.helpers.mapOneToManyRelation(descriptions, loader.ids, 'task_id');
}
}
// Polymorphic resolver
// description.resolver.ts
@Resolver(() => DescriptionObjectType)
export class DescriptionResolver {
constructor(
@InjectRepository(DescriptionText) public readonly descriptionTextRepository: Repository<DescriptionText>,
@InjectRepository(DescriptionChecklist) public readonly descriptionChecklistRepository: Repository<DescriptionChecklist>,
) {}
@ResolveField(() => [DescriptionableUnion], { nullable: true })
@GraphqlLoader({ // <-- We will load description_id field of parent model to the ids and description_type field to the type
polymorphic: {
idField: 'description_id',
typeField: 'description_type'
}
})
async descriptionable(
@Loader() loader: PolymorphicLoaderData<[DescriptionText | DescriptionChecklist], number, DescriptionType>, // <-- It will return aggregated polymorphicTypes
@SelectedUnionTypes() types: SelectedUnionTypesResult // <-- It will extract from the query and return selected union types
) {
const results = []; // <-- We need to gather all entities to the single array
for (const item of loader.polimorphicTypes) {
switch(item.descriminator) {
case DescriptionType.Text:
const textDescriptions = await this.descriptionTextRepository.createQueryBuilder()
.select(types.getFields(DescriptionTextObjectType))
.where({
id: In(item.ids)
})
.getRawMany();
results.push({ descriminator: DescriptionType.Text, entities: textDescriptions })
break;
case DescriptionType.Checklist:
const checklistDescriptions = await this.descriptionChecklistRepository.createQueryBuilder()
.select(types.getFields(DescriptionChecklistObjectType))
.where({
id: In(item.ids)
})
.getRawMany();
results.push({ descriminator: DescriptionType.Checklist, entities: checklistDescriptions })
break;
default: break;
}
}
return loader.helpers.mapOneToManyPolymorphicRelation(results, loader.ids); // <-- This helper will change shape of responce to the shape which is sutable for graphql
}
}
You can find complete example in src/descriptions folder
Filter is giving ability to filter out entities by the condition. Condition looks similar to hasura interface using operators eq, neq, gt, gte, lt, lte, in, like, notlike, between, notbetween, null.
By default it generates filter based on provided model. It supports only first level of the tables hierachy. If you need to search in depth you can declare custom filters (example 3).
{
users(where: {id: {eq: 1}}) {
id
}
}
{
users(
where: {
and: [
{
email: {like: "yahoo.com"}
}
{
email: {like: "google.com"}
}
],
or: {
id: {
between: [1,2,3]
}
}
}
) {
id
}
}
@Filter() parameter with type of FilterArgs@Filter() will return typeorm compatible condition which you can use in your query builder.@Resolver(() => UserObjectType)
export class UserResolver {
constructor(
@InjectRepository(Task) public readonly taskRepository: Repository<Task>,
@InjectRepository(User) public readonly userRepository: Repository<User>
) {}
@Query(() => [UserObjectType])
users(
@Filter(() => UserObjectType) filter: FilterArgs, // It will return typeorm condition
@Args('task_title', {nullable: true}) taskTitle: string, // You can add custom additional filter if needed
) {
const qb = this.userRepository.createQueryBuilder('u')
.leftJoin('task', 't', 't.assignee_id = u.id')
.where(filter)
.distinct();
if (taskTitle) { // mixed filters
qb.andWhere(`t.title ilike :title`, { title: `%${taskTitle}%` })
}
return qb.getMany()
}
}
@Resolver(() => UserObjectType)
export class UserResolver {
constructor(@InjectRepository(Task) public readonly taskRepository: Repository<Task>) {}
@ResolveField(() => TaskObjectType)
@GraphqlLoader()
async tasks(
@Loader() loader: LoaderData<TaskObjectType, number>,
@Filter(() => TaskObjectType) filter: FilterArgs,
) {
const qb = this.taskRepository.createQueryBuilder()
.where(filter)
.andWhere({
assignee_id: In<number>(loader.ids)
});
const tasks = await qb.getMany();
return loader.helpers.mapOneToManyRelation(tasks, loader.ids, 'assignee_id');
}
}
@InputType()
export class UserFilterInputType {
@FilterField(() => String, { sqlExp: 't.title'})
task_title: string;
@FilterField(() => String, { sqlExp: 't.story_points'})
task_story_points: number;
@FilterField(() => String, { sqlExp: 'concat(u.fname, \' \', u.lname)'})
full_name: string;
}
// Resolver
@Resolver(() => UserObjectType)
export class UserResolver {
constructor(
@InjectRepository(Task) public readonly taskRepository: Repository<Task>,
@InjectRepository(StoryModel) public readonly storyRepository: Repository<StoryModel>,
@InjectRepository(User) public readonly userRepository: Repository<User>
) {}
@Query(() => [UserObjectType])
users(
@Filter(() => [UserObjectType, UserFilterInputType]) filter: FilterArgs, // <-- Object model and Filter model. It is possible to provide only one model or more that 2.
@Sorting(() => UserObjectType, { sqlAlias: 'u' }) sorting: SortArgs<UserObjectType>
) {
const qb = this.userRepository.createQueryBuilder('u')
.leftJoin('task', 't', 't.assignee_id = u.id')
.where(filter)
.orderBy(sorting);
return qb.getMany()
}
}
You can also exclude some fields from the DTO filter. Read Exclusions.
Raw filters allow to get access to the user provided raw value right from the code. This feature allows to build your own filters interpreters.
How to use raw filters:
1. Add @Filter({ raw: true }) parameter with type of RawFilterArgs<T> where T is your filter type
2. @Filter() will return raw filter data.
```typesript export class UserResolver { @Query(() => [UserObjectType]) async usersRaw(
$ claude mcp add Nestjs-Graphql-Tools \
-- python -m otcore.mcp_server <graph>