25
loading...
This website collects cookies to deliver better user experience
query GetPosts {
posts {
id
title
body
createdBy {
id
name
}
}
}
nest new example-app
app.module.ts
and main.ts
.nest g module users
user.entity.ts
and users.service.ts
:export class User {
id: number;
name: string;
}
import { Injectable } from '@nestjs/common';
import { delay } from '../util';
import { User } from './user.entity';
@Injectable()
export class UsersService {
private users: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'Alex' },
{ id: 4, name: 'Anna' },
];
async getUsers() {
console.log('Getting users...');
await delay(3000);
return this.users;
}
}
getUsers
method we simulate database latency with a delay of 3000ms.Don't forget to add UsersService
to exports
array in users.module.ts
export class Post {
id: string;
title: string;
body: string;
userId: number;
}
import { Injectable } from '@nestjs/common';
import { delay } from '../util';
import { Post } from './post.entity';
@Injectable()
export class PostsService {
private posts: Post[] = [
{ id: 'post-1', title: 'Post 1', body: 'Lorem 1', userId: 1 },
{ id: 'post-2', title: 'Post 2', body: 'Lorem 2', userId: 1 },
{ id: 'post-3', title: 'Post 3', body: 'Lorem 3', userId: 2 },
];
async getPosts() {
console.log('Getting posts...');
await delay(3000);
return this.posts;
}
}
npm i @nestjs/graphql graphql-tools graphql apollo-server-express
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { PostsModule } from './posts/posts.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [
UsersModule,
PostsModule,
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
autoSchemaFile
property NestJS will generate GraphQL schema from types
we declare in code. However since we haven't declared any when we run npm run start:dev
we will get an error.GraphQL types
in our code. In order to do that we need to add some decorators to our entity classes:import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => Int)
id: number;
@Field()
name: string;
}
import { Query, Resolver } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver(User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User])
getUsers() {
return this.usersService.getUsers();
}
}
Don't forget to add UsersResolver
to providers array in users.module.ts
UsersResolver
the error goes away and we get a new file:# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type User {
id: Int!
name: String!
}
type Query {
getUsers: [User!]!
}
http://localhost:3000/graphql
) and execute the following query:query GetUsers {
users {
id
name
}
}
{
"data": {
"users": [
{
"id": 1,
"name": "John"
},
{
"id": 2,
"name": "Jane"
},
{
"id": 3,
"name": "Alex"
},
{
"id": 4,
"name": "Anna"
}
]
}
}
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field()
id: string;
@Field()
title: string;
@Field()
body: string;
userId: number;
}
import { Query, Resolver } from '@nestjs/graphql';
import { Post } from './post.entity';
import { PostsService } from './posts.service';
@Resolver(Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query(() => [Post], { name: 'posts' })
getPosts() {
return this.postsService.getPosts();
}
}
createdBy
field to post.entity.ts
:@Field(() => User)
createdBy?: User;
GetPosts
query from the beginning of this post. However we get an error:createdBy
field in posts.resolver.ts
. We do that by adding the following methods:@ResolveField('createdBy', () => User)
getCreatedBy(@Parent() post: Post) {
const { userId } = post;
return this.usersService.getUser(userId);
}
async getUser(id: number) {
console.log(`Getting user with id ${id}...`);
await delay(1000);
return this.users.find((user) => user.id === id);
}
UsersService
from UsersModule
and then import UsersModule
into PostsModule
.GetPosts
query and we should get the following result:{
"data": {
"posts": [
{
"id": "post-1",
"title": "Post 1",
"body": "Lorem 1",
"createdBy": {
"id": 1,
"name": "John"
}
},
{
"id": "post-2",
"title": "Post 2",
"body": "Lorem 2",
"createdBy": {
"id": 1,
"name": "John"
}
},
{
"id": "post-3",
"title": "Post 3",
"body": "Lorem 3",
"createdBy": {
"id": 2,
"name": "Jane"
}
}
]
}
}
Getting posts...
Getting user with id 1...
Getting user with id 1...
Getting user with id 2...
DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching.
npm i dataloader
import * as DataLoader from 'dataloader';
import { mapFromArray } from '../util';
import { User } from './user.entity';
import { UsersService } from './users.service';
function createUsersLoader(usersService: UsersService) {
return new DataLoader<number, User>(async (ids) => {
const users = await usersService.getUsersByIds(ids);
const usersMap = mapFromArray(users, (user) => user.id);
return ids.map((id) => usersMap[id]);
});
}
DataLoader constructor accepts a batching function as an argument. A batching function takes an array of ids
(or keys) and returns a promise that resolves to an array of values. Important thing to note here is that those values must be in the exact same order as ids
argument.
usersMap
is a simple object where keys are user ids and values are actual users:
{
1: {id: 1, name: "John"},
...
}
const usersLoader = createUsersLoader(usersService);
const user1 = await usersLoader.load(1)
const user2 = await usersLoader.load(2);
// Constructor
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
authScope: getScope(req.headers.authorization)
})
}));
// Example resolver
(parent, args, context, info) => {
if(context.authScope !== ADMIN) throw new AuthenticationError('not admin');
// Proceed
}
ApolloServer
so the context
function should be declared when declaring GraphQLModule
. In our case that's in app.module.ts
:GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
context: () => ({
randomValue: Math.random(),
}),
}),
@nestjs/graphql
there is a decorator for that:posts.resolver.ts
@Query(() => [Post], { name: 'posts' })
getPosts(@Context() context: any) {
console.log(context.randomValue);
return this.postsService.getPosts();
}
@ResolveField('createdBy', () => User)
getCreatedBy(@Parent() post: Post, @Context() context: any {
console.log(context.randomValue);
const { userId } = post;
return this.usersService.getUser(userId);
}
GetPosts
query we should see the following in the console:0.858156868751532
Getting posts...
0.858156868751532
Getting user with id 1...
0.858156868751532
Getting user with id 1...
0.858156868751532
Getting user with id 2...
randomValue
is changed.Context
decorator:@Query(() => [Post], { name: 'posts' })
getPosts(@Context('randomValue') randomValue: number) {
console.log(randomValue);
return this.postsService.getPosts();
}
@ResolveField('createdBy', () => User)
getCreatedBy(@Parent() post: Post, @Context('randomValue') randomValue: number) {
console.log(randomValue);
const { userId } = post;
return this.usersService.getUser(userId);
}
app.module.ts
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
context: () => ({
randomValue: Math.random(),
usersLoader: createUsersLoader(usersService),
}),
}),
usersLoader
as shown above we will get an error because usersService
isn't defined. To solve this we need to change the definition for GraphQLModule
to use forRootAsync
method:app.module.ts
GraphQLModule.forRootAsync({
useFactory: (usersService: UsersService) => ({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
context: () => ({
randomValue: Math.random(),
usersLoader: createUsersLoader(usersService),
}),
}),
}),
inject
property bellow useFactory
:useFactory: ...,
inject: [UsersService],
UsersService
to GraphQLModule
and we do that by importing UsersModule
into GraphQLModule
.imports: [UsersModule],
useFactory: ...
usersLoader
to GraphQL context object. Let's now see how to use it.randomValue
in our resolvers with usersLoader
:posts.resolver.ts
import { Context, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import * as DataLoader from 'dataloader';
import { User } from '../users/user.entity';
import { Post } from './post.entity';
import { PostsService } from './posts.service';
@Resolver(Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query(() => [Post], { name: 'posts' })
getPosts() {
return this.postsService.getPosts();
}
@ResolveField('createdBy', () => User)
getCreatedBy(
@Parent() post: Post,
@Context('usersLoader') usersLoader: DataLoader<number, User>,
) {
const { userId } = post;
return usersLoader.load(userId);
}
}
GetPosts
query the console output should look like this:Getting posts...
Getting users with ids (1,2)
25