Code Monkey home page Code Monkey logo

Comments (21)

EdouardBougon avatar EdouardBougon commented on March 29, 2024 10

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.

caseyduquettesc avatar caseyduquettesc commented on March 29, 2024 6

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.

obedm503 avatar obedm503 commented on March 29, 2024 5

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.

mohaalak avatar mohaalak commented on March 29, 2024 5

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.

pelssersconsultancy avatar pelssersconsultancy commented on March 29, 2024 2

@obedm503 I missed the args in my property resolver.. thx for this pointer

from graphql.

obedm503 avatar obedm503 commented on March 29, 2024 1

@pelssersconsultancy all resolvers get the context, the signature always is

@ResolveProperty()
propertyResolver(root, args, context, info){}

from graphql.

marvinroger avatar marvinroger commented on March 29, 2024

Well, never mind! Resolvers are actually still functions, so we can use a dataloader there... sorry.

from graphql.

j avatar j commented on March 29, 2024

@marvinroger any idea on clean ways to initialize a new DataLoader per request NOT in a resolver and using Nest's DI?

from graphql.

marvinroger avatar marvinroger commented on March 29, 2024

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.

Jonatthu avatar Jonatthu commented on March 29, 2024

Any example of how to use this?
@marvinroger

from graphql.

Jonatthu avatar Jonatthu commented on March 29, 2024

@obedm503 That's great thanks, Do you have more online references or different use cases for this one?

from graphql.

obedm503 avatar obedm503 commented on March 29, 2024

@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.

pelssersconsultancy avatar pelssersconsultancy commented on March 29, 2024

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.

phillip-hall avatar phillip-hall commented on March 29, 2024

@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.

caseyduquettesc avatar caseyduquettesc commented on March 29, 2024

I use @autobind from the autobind package on all my dataloader methods

from graphql.

mohaalak avatar mohaalak commented on March 29, 2024

@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.

Javaman44 avatar Javaman44 commented on March 29, 2024

Thanks EdouardBougon i search this morning for the same issue :)

from graphql.

alfonmga avatar alfonmga commented on March 29, 2024

@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.

secmohammed avatar secmohammed commented on March 29, 2024

@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.

gouroujo avatar gouroujo commented on March 29, 2024

@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.

lock avatar lock commented on March 29, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.