Comments (21)
Since @nestjs/graphql": "^6.2.0" (775beca#diff-b57b423096aa8ca93a6f5575b56e3f3f), Interceptor (and guard, filter) are disabled for properties. So the example of @mohaalak isn't working anymore.
I've adapted it in this way:
Update DataLoaderInterceptor. It will be executed only one time for the query, mutation or subscription:
import {
CallHandler,
ExecutionContext,
Injectable,
InternalServerErrorException,
NestInterceptor,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { GqlExecutionContext, GraphQLExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
import { NestDataLoader } from '../interfaces/nest-dataloader';
/**
* Context key where get loader function will be store
*/
export const GET_LOADER_CONTEXT_KEY: string = 'GET_LOADER_CONTEXT_KEY';
@Injectable()
export class DataLoaderInterceptor implements NestInterceptor {
constructor(
private readonly moduleRef: ModuleRef,
) {}
/**
* @inheritdoc
*/
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const graphqlExecutionContext: GraphQLExecutionContext = GqlExecutionContext.create(context);
const ctx: any = graphqlExecutionContext.getContext();
if (ctx[GET_LOADER_CONTEXT_KEY] === undefined) {
ctx[GET_LOADER_CONTEXT_KEY] = (type: string): NestDataLoader => {
if (ctx[type] === undefined) {
try {
ctx[type] = this.moduleRef
.get<NestDataLoader>(type, { strict: false })
.generateDataLoader();
} catch (e) {
throw new InternalServerErrorException(`The loader ${type} is not provided`);
}
}
return ctx[type];
};
}
return next.handle();
}
}
Transform Loader decorator to a parameter decorator:
import { createParamDecorator, InternalServerErrorException } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { DataLoaderInterceptor, GET_LOADER_CONTEXT_KEY } from '../interceptors/data-loader.interceptor';
export const Loader: (type: string) => ParameterDecorator = createParamDecorator(
(type: string, [__, ___, ctx, ____]: any) => {
if (ctx[GET_LOADER_CONTEXT_KEY] === undefined) {
throw new InternalServerErrorException(`
You should provide interceptor ${DataLoaderInterceptor.name} globaly with ${APP_INTERCEPTOR}
`);
}
return ctx[GET_LOADER_CONTEXT_KEY](type);
},
);
And now, how to used it with a property:
@ResolveProperty('photo', () => Photo)
async photo(
@Root() user: User,
@Loader(PhotoLoader.name) photoLoader: DataLoader<User['id'], Photo>,
): Promise<Photo> {
return photoLoader.load(user.id);
}
from graphql.
If anyone is looking for a more "Nest-y" recipe. Here's a DataLoaderInterceptor
I use to create new loaders per request. Things to keep in mind -- in GQL requests, the interceptors are triggered multiple times so the interceptor needs to check if it actually needs to create the loaders or not. Secondly, because the interceptor isn't executed before the GQL context creation, the GQL can't assign the actual data loaders because they don't yet exist. Instead, I've wrapped them in a function to be called by any resolvers that want the loaders.
custom.d.ts
/** Patch the Request type to know about custom properties we assign */
declare namespace Express {
export interface Request {
id?: string;
user?: string;
dataLoaders: import('./src/common/interceptors/dataloader.interceptor').DataLoaders;
}
}
src/common/utils.ts
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Request } from 'express';
import { GraphQLResolveInfo } from 'graphql';
export function getRequestFromContext(context: ExecutionContext): Request {
const request = context.switchToHttp().getRequest<Request>();
// Graphql endpoints need a context creation
if (!request) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
} else {
// Interestingly, graphql field resolvers pass through the guards again. I suppose that's good?
// These executions however provide different inputs than a fresh Http or GQL request.
// In order to authenticate these, we can retrieve the original request from the context
// that we configured in the GraphQL options in app.module.
// I assign a user to every request in a middleware not shown here
if (!request.user) {
const [parent, , ctx, info]: [any, never, any, GraphQLResolveInfo] = context.getArgs();
// Checking if this looks like a GQL subquery, is this hacky?
if (parent && info.parentType) {
return ctx.req;
}
}
return request;
}
}
src/modules/item/item.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import autobind from 'autobind-decorator';
import { Repository } from 'typeorm';
import { RelatedItem } from '../related-item/related-item.entity';
import { Item } from './item.entity';
@Injectable()
export class ItemService {
constructor(@InjectRepository(Item) private readonly itemRepository: Repository<Item>) {}
public async findAll(): Promise<Item[]> {
return await this.itemRepository.find();
}
public async findOneById(id: number): Promise<Item | undefined> {
return await this.itemRepository.findOne({ where: { id } });
}
@autobind
public async relatedItemsOfItems(ids: number[]): Promise<(RelatedItem | undefined)[]> {
const items = await this.itemRepository
.createQueryBuilder('item')
.leftJoinAndSelect('item.relatedItem', 'relatedItem')
.where('item.id IN (:...ids)', { ids })
.getMany();
return items.map(item => item.relatedItem);
}
}
src/common/interceptors/dataloader.interceptor.ts
import { ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import DataLoader from 'dataloader';
import { Observable } from 'rxjs';
import { MyLogger } from '../../logger/my-logger.service';
import { ItemService } from '../../modules/item/item.service';
import { RelatedItem } from '../../modules/related-item/related-item.entity';
import { getRequestFromContext } from '../utils';
/**
* The DataLoaders type available on the request.
* In custom.d.ts, I've set this type on request
*/
export interface DataLoaders {
relatedItemLoader: DataLoader<number, RelatedItem | undefined>;
}
/**
* GQL context function type to get DataLoaders. When the GQL context is created, the interceptor
* hasn't actually run yet, so a function is provided to return them at time of execution.
*/
export type GetDataLoaders = () => DataLoaders;
/**
* Creates new instances of DataLoaders on every request and makes them available on `request.dataLoaders`.
*/
@Injectable()
export class DataLoaderInterceptor implements NestInterceptor {
constructor(private readonly logger: MyLogger, private readonly itemService: ItemService) {}
public intercept(context: ExecutionContext, call$: Observable<any>): Observable<any> {
const request = getRequestFromContext(context);
// If the request already has data loaders, then do not create them again or the benefits are negated.
if (request.dataLoaders) {
this.logger.debug('Data loaders exist', this.constructor.name);
} else {
this.logger.debug('Creating data loaders', this.constructor.name);
// Create new instances of DataLoaders per request
request.dataLoaders = {
relatedItemLoader: new DataLoader<number, RelatedItem | undefined>(this.itemService.relatedItemsOfItems),
};
}
return call$;
}
}
src/modules/item/item.resolver.ts
import { Args, Context, Parent, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
import { GetDataLoaders } from '../../common/interceptors/dataloader.interceptor';
import { RelatedItem } from '../related-item/related-item.entity';
import { Item } from './item.entity';
import { ItemService } from './item.service';
@Resolver('Item')
export class ItemResolver {
constructor(private readonly itemService: ItemService) {}
@Query()
public async getItems(): Promise<Item[]> {
return this.itemService.findAll();
}
@Query('item')
public async getItem(@Args('id') id: number): Promise<Item | undefined> {
return await this.itemService.findOneById(id);
}
@ResolveProperty('relatedItem')
public async getRelatedItem(
@Parent() item: Item,
@Context('getDataLoaders') getDataLoaders: GetDataLoaders,
): Promise<RelatedItem | undefined> {
return getDataLoaders().relatedItemLoader.load(item.id);
}
}
src/modules/application/app.module.ts
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { GraphQLModule } from '@nestjs/graphql';
import { Request } from 'express';
import depthLimit from 'graphql-depth-limit';
import { join } from 'path';
import { DataLoaderInterceptor } from '../../common/interceptors/dataloader.interceptor';
import { CSPMiddleware } from '../../common/middlewares/csp.middleware';
import { CSRFMiddleware } from '../../common/middlewares/csrf.middleware';
import { RequestLoggerMiddleware } from '../../common/middlewares/request-logger.middleware';
import { ThrottleMiddleware } from '../../common/middlewares/throttle.middleware';
import { LoggerModule } from '../../logger/my-logger.module';
import { MyLogger } from '../../logger/my-logger.service';
import { DatabaseModule } from '../database/database.module';
import { ItemModule } from '../item/item.module';
import { RelatedItemModule } from '../related-item/related-item.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
DatabaseModule,
GraphQLModule.forRootAsync({
imports: [LoggerModule],
inject: [MyLogger],
useFactory: async (logger: MyLogger) => ({
context: ({ req }: { req: Request }) => ({
req,
getDataLoaders: () => req.dataLoaders,
}),
definitions: {
path: join(process.cwd(), '../shared/src/graphql.schema.ts'),
},
formatError: (error: Error) => {
logger.error(error);
return error;
},
typePaths: ['src/modules/**/*.graphql'],
validationRules: [depthLimit(10)],
}),
}),
LoggerModule,
ItemModule,
RelatedItemModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_INTERCEPTOR,
useClass: DataLoaderInterceptor,
},
],
})
export class AppModule implements NestModule {
public configure(consumer: MiddlewareConsumer): void {
consumer
.apply(RequestLoggerMiddleware, CSPMiddleware, ThrottleMiddleware, CSRFMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}
from graphql.
this is how I ended up implementing it
// app.module.ts
import {
Module,
MiddlewaresConsumer,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql';
import { graphqlExpress } from 'apollo-server-express';
import * as DataLoader from 'dataloader';
import { Request } from 'express';
import { CatService } from './cat/cat.service';
import { CatResolver } from './cat/cat.resolver';
@Module({
imports: [
GraphQLModule,
],
components: [
CatService,
CatResolver,
],
})
export class ApplicationModule implements NestModule {
constructor(
private readonly graphQLFactory: GraphQLFactory,
private readonly catService: CatService,
) {}
configure(consumer: MiddlewaresConsumer) {
const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.gql');
const schema = this.graphQLFactory.createSchema({ typeDefs });
consumer
.apply(
graphqlExpress((req: Request) => {
// this function is executed on every request
// so a new catLoader is created each time
const context = {
catLoader: new DataLoader((catIds: string[]) =>
// then your service can query you db or something
// just make sure whatever CatService#getMany returns
// in the same order as the ids as per DataLoader rules
this.catService.getMany(catIds),
),
};
return {
schema,
rootValue: req,
context,
};
}),
)
.forRoutes({ path: '/graphql', method: RequestMethod.ALL });
}
}
// ./cat/cat.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { Request } from 'express';
@Resolver('Cat')
export class CatResolver {
@Query('cat')
get(req: Request, args, context) {
// use the catLoader that comes through the "context"
return context.catLoader.load(id);
}
}
from graphql.
I've written a more generic approach to this with the help of decorator and moduleRef and interceptors, first of all, there is an interface for writing data loader wrapper
import DataLoader from 'dataloader';
export interface NestDataLoader {
/**
* Should return a new instance of dataloader each time
*/
generateDataLoader(): DataLoader<any, any>;
}
then we make a decorator
import { ReflectMetadata, Type } from '@nestjs/common';
import { NestDataLoader } from './dataloader.interface';
/**
* it's just a decorator for reflecting metaData
* @param loader class that implement nestDataLoader
*/
export const Loader = (loader: Type<NestDataLoader>) =>
ReflectMetadata('dataloader', loader);
now let's make data loader then I'll tell you how can we inject it to our resolver function
import { NestDataLoader } from 'src/common/dataloader.interface';
import { UserService } from './users.service';
import { Injectable } from '@nestjs/common';
import * as DataLoader from 'dataloader';
import { User } from './model/user';
@Injectable()
export class UserLoader implements NestDataLoader {
constructor(private readonly userService: UserService) {}
generateDataLoader(): DataLoader<any, any> {
// it should instantiate a data laoder each time
return new DataLoader<number, User>(this.userService.findMany);
}
}
here we can see that we added @Injectable()
so in the constructor we can get any service that we want to use for our data loader,
and generateDataLoader
should generate a new data loader it will call on each request
then we should add it to our module's providers
now how we can use this data loader in our resolver with a global interceptor
import {
NestInterceptor,
ExecutionContext,
Injectable,
Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { GqlExecutionContext } from 'nest-type-graphql';
import { Reflector, ModuleRef } from '@nestjs/core';
import { NestDataLoader } from './dataloader.interface';
@Injectable()
export class DataLoaderInterceptor implements NestInterceptor {
constructor(
private readonly reflector: Reflector,
private readonly moduleRef: ModuleRef,
) {}
intercept(context: ExecutionContext, call$: Observable<any>) {
// we get from reflector if there is requested any dataloader for this handler
const type = this.reflector.get<Type<NestDataLoader>>(
'dataloader',
context.getHandler(),
);
if (type) {
// GqlExecutionContext is available in @nestjs/graphql also nest-type-graphql
const graphqlExecutionContext = GqlExecutionContext.create(context);
const ctx = graphqlExecutionContext.getContext();
// check if we have add this dataloader on context or not and name it the loader class
if (!ctx[type.name]) {
/*
module ref will get the injected data loader {strict: false} is there
so it search imported modules too
**/
ctx[type.name] = this.moduleRef
.get<NestDataLoader>(type, { strict: false })
.generateDataLoader();
}
}
return call$;
}
}
I used reflector
to get the reflection that we used in our decorator,
and then I check if this dataloader is loaded before if not
then I get an instance of Loader Class that we wrote, with moduleRef
with all services injected, now we generate a new DataLoader and provide it in the context of graphql.
now we can get in resolver this way
class PostResolver {
...
@Loader(UserLoader)
user(
@Parent() post: Post,
@Context('UserLoader') userLoader: Dataloader<number, User>,
) {
return userLoader.load(post.userId);
}
}
pay attention that in @Loader
we provide the Class but in @Context
we provide the name of class in string, also it will give us the dataloader not the class.
remember to add interceptor globally using
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: DataLoaderInterceptor,
},
],
})
export class AppModule {}
so from now on you should write a DataLoader Wrapper Class that implements NestDataLoader with all the services you need, provide it in the module, and use it in any resolver you want.
@kamilmysliwiec should I make this changes and give a pull request?
from graphql.
@obedm503 I missed the args in my property resolver.. thx for this pointer
from graphql.
@pelssersconsultancy all resolvers get the context, the signature always is
@ResolveProperty()
propertyResolver(root, args, context, info){}
from graphql.
Well, never mind! Resolvers are actually still functions, so we can use a dataloader there... sorry.
from graphql.
@marvinroger any idea on clean ways to initialize a new DataLoader per request NOT in a resolver and using Nest's DI?
from graphql.
I was thinking about this, but I did not have time to implement this in my side-project yet. I thought about a ‘dataloader’ module which would create every dataloaders. A middleware might be added right before the graphql one, it would call a method on the ‘dataloader’ module that would return a map of all dataloaders. Then, this map can be added as context for the graphql resolvers
from graphql.
Any example of how to use this?
@marvinroger
from graphql.
@obedm503 That's great thanks, Do you have more online references or different use cases for this one?
from graphql.
@Jonatthu I based it on this https://youtu.be/2cSVIWDUSn4?t=4m6s. even tho he's using just express without nest, the same concepts apply
from graphql.
Hi all, I think the example given is pretty useless. You typically would want to use a dataloader for property resolvers, so the question is... how can we pass a dataloader to the property resolver function? I managed to inject the dataloaders using the example but a property resolver does not receive this context as parameter
from graphql.
@mohaalak I have tried your solution, very nice by the way, however the dataloader is failing to call my service method. I am getting this error "Cannot read property 'fieldRepository' of undefined"
Here is my Loader class implementation;
@Injectable()
export class FieldLoader implements NestDataLoader {
constructor(private readonly fieldService: FieldService) {}
generateDataLoader(): DataLoader<any, any> {
return new DataLoader<string, Field>(this.fieldService.findManyByTable);
}
}
Here is my FieldService class implementation
@Injectable()
export class FieldService {
constructor(@InjectRepository(Field) private readonly fieldService: Repository<Field>) {}
async findManyByTable(ids: string[]): Promise<Field[]> {
return await this.fieldRepository.find({tableId: In(ids)});
}
From what I can gather, "this" is not bound to an instance of FieldService when DataLoader calls the "findManyByTable" method. Any idea why?
from graphql.
I use @autobind
from the autobind package on all my dataloader methods
from graphql.
@mohaalak I have tried your solution, very nice by the way, however the dataloader is failing to call my service method. I am getting this error "Cannot read property 'fieldRepository' of undefined"
Here is my Loader class implementation;
@Injectable() export class FieldLoader implements NestDataLoader { constructor(private readonly fieldService: FieldService) {} generateDataLoader(): DataLoader<any, any> { return new DataLoader<string, Field>(this.fieldService.findManyByTable); } }Here is my FieldService class implementation
@Injectable() export class FieldService { constructor(@InjectRepository(Field) private readonly fieldService: Repository<Field>) {} async findManyByTable(ids: string[]): Promise<Field[]> { return await this.fieldRepository.find({tableId: In(ids)}); }From what I can gather, "this" is not bound to an instance of FieldService when DataLoader calls the "findManyByTable" method. Any idea why?
cause we send just method to dataloader this.fieldService.findManyByTable
so this
is not bound to FieldService
but is bounded to dataloader
cause it's the dataloader that calls this method,
you can use AutoBind package that @caseyduquettesc mentioned, but I simply write my function as a property of class with arrow functions.
@Injectable()
export class FieldService {
constructor(@InjectRepository(Field) private readonly fieldService: Repository<Field>) {}
public findManyByTable= async (ids: string[]): Promise<Field[]> {
return await this.fieldRepository.find({tableId: In(ids)});
}
}
from graphql.
Thanks EdouardBougon i search this morning for the same issue :)
from graphql.
@EdouardBougon How NestDataLoader
interface looks like? Could you share it? thanks
EDIT: Nevermind I found it.
import DataLoader from 'dataloader';
export interface NestDataLoader {
/**
* Should return a new instance of dataloader each time
*/
generateDataLoader(): DataLoader<any, any>;
}
from graphql.
@EdouardBougon
It keeps logging "The loader UserDataLoader is not provided" , any idea to tackle this? , I checked on the error and apparently Nest cannot find the user loader as it does not exist in current context.
from graphql.
@EdouardBougon
It keeps logging "The loader UserDataLoader is not provided" , any idea to tackle this? , I checked on the error and apparently Nest cannot find the user loader as it does not exist in current context.
you should try import * as DataLoader from 'dataloader';
There is an issue in the typing of dataloader and the default import, don't know what exactly
from graphql.
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
from graphql.
Related Issues (20)
- TypeScript definitions generated with wrong case HOT 2
- `GraphQLModule` should throw an error if schema files are not found HOT 2
- Expose GraphQLModule options and gqlSchemaHost to support Serverless
- How to send partial data with error object using @Context
- CustomScalar parseLiteral doesn't allow null return type HOT 2
- Unable to Specify forRoutes Argument When Assigning Middleware to '/graphql' Endpoint
- ResolveField shouldn't be called when the field already has a value HOT 1
- Support for Custom Fields Decorators in Mapped types
- Extend `dateScalarMode` and `numberScalarMode` to allow custom scalars to be used by default
- Subscription graphql this on resolve does not access services being injected in the constructor HOT 1
- Support for @defer directive in GraphQL HOT 2
- @nestjs/graphql compatibility with ts-morph 22.0.0 HOT 1
- Regular Expression Denial of Service (ReDoS) vulnerability HOT 2
- Feat | clarifying `DefinitionsGeneratorOptions#defaultTypeMapping` supported types
- Incomplete schema produced with code-first approach: directives are omitted HOT 1
- @nestjs/graphql depends on vulnerable version of ws HOT 1
- Upgrade WS dependency - ws affected by a DoS when handling a request with many HTTP headers HOT 3
- v12.2.0 - Error: Schema must contain uniquely named types but contains multiple types named "DateTime"
- npm warn deprecated @apollo/[email protected] HOT 1
- Allow sortSchema via GraphQLDefinitionsFactory HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from graphql.