NestJS Graphql automation library for building performant API
The library allows to build efficient graphql API helping overcome n+1 problem with the minimum dependencies. It provides a bunch of decorators that make life easier.
- NestJS-Graphql-Tools - the library that enchance experience in building Garphql API
- Description
- Extentions
- Overview
- Installation
- Loader usage guide
- Polymorphic relations
- Field extraction
- Federation
- More examples
- Contribution
- License
npm i nestjs-graphql-tools
or
yarn add nestjs-graphql-tools
- Decorate your resolver with
@GraphqlLoader()
- Add
@Loader()
parameter as a first parameter - @Loader will return you LoaderData interface which includes ids of entities and helpers for constructing sutable object for graphql
@Resolver(() => UserObjectType)
export class UserResolver {
@ResolveField(() => TaskObjectType)
@GraphqlLoader() // <-- It's important to add decorator here
async tasks(
@Loader() loader: LoaderData<TaskObjectType, number>, // <-- and here
@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>,
) {
const qb = this.userRepository.createQueryBuilder('u')
.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
The library allows to gather only requested field from the query and provides it as an array to the parameter variable.
Simple graphql query
{
tasks {
id
title
}
}
Resolver
@Resolver(() => TaskObjectType)
export class TaskResolver {
constructor(@InjectRepository(Task) public readonly taskRepository: Repository<Task>) {}
@Query(() => [TaskObjectType])
async tasks(
@SelectedFields({sqlAlias: 't'}) selectedFields: SelectedFieldsResult // Requested fields will be here. sqlAlias is optional thing. It useful in case if you're using alias in query builder
) {
const res = await this.taskRepository.createQueryBuilder('t')
.select(selectedFields.fieldsData.fieldsString) // fieldsString return array of strings
.getMany();
return res;
}
}
The query will generate typeorm request with only requested fields
SELECT "t"."id" AS "t_id", "t"."title" AS "t_title" FROM "task" "t"
Basic support of federation already in place. Just add to your method with @ResolveReference()
one more decorator @GraphqlLoader()
This examples is the reference to official example https://github.com/nestjs/nest/tree/master/sample/31-graphql-federation-code-first. Clone https://github.com/nestjs/nest/tree/master/sample/31-graphql-federation-code-first (download specific directory with https://download-directory.github.io/ or with chrome extention https://chrome.google.com/webstore/detail/gitzip-for-github/ffabmkklhbepgcgfonabamgnfafbdlkn)
- Annotate method resolveReference of
users-application/src/users/users.resolver.ts
// users-application/src/users/users.resolver.ts
@ResolveReference()
@GraphqlLoader()
async resolveReference(
@Loader() loader: LoaderData<User, number>,
) {
const ids = loader.ids;
const users = this.usersService.findByIds(ids);
return loader.helpers.mapManyToOneRelation(users, loader.ids, 'id')
}
- Add method findByIds to
users-application/src/users/users.service.ts
// users-application/src/users/users.service.ts
@Injectable()
export class UsersService {
private users: User[] = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Richard Roe' },
];
findByIds(idsList: number[]): User[] {
return this.users.filter((user) => idsList.some(id => Number(id) === user.id));
}
}
-
Install dependencies of 3 projects : npm ci in gateway, posts-application, users-application.
-
Run all projects in order :
cd users-application && npm run start
cd posts-application && npm run start
cd gateway && npm run start
-
Go to localhost:3001/graphql and send graphql request to gateway
{
posts {
id
title
authorId
user {
id
name
}
}
}
You can find more examples in the src folder
If you want to contribute please create new PR with good description.
How to run the project:
- Run dev server
yarn install
yarn start:dev
On the first run, server will seed up the database with testing dataset.
- Reach out
http://localhost:3000/graphql
NestJS Graphql tools is GNU GPLv3 licensed.