nestjs / docs.nestjs.com

The official documentation https://docs.nestjs.com 📕
MIT License
1.2k stars 1.74k forks source link

How to use DataLoader? #1135

Open zuohuadong opened 6 years ago

zuohuadong commented 6 years ago

Are there any relevant examples?

https://github.com/facebook/dataloader

Mirokko commented 6 years ago

You probably wanna start with Interceptors.

Mirokko commented 6 years ago

@twilroad Nope, but link above is basically everything you need to get started. Interceptor can be used for caching (you can see it in docs), but with slight modifications you can store result from next handler. As a param, you will need a cache key, you can pass it like shown here https://docs.nestjs.com/advanced/mixins.

caseyduquettesc commented 6 years ago

https://github.com/nestjs/graphql/issues/2#issuecomment-426076580

babbarankit commented 6 years ago

Sample Code

export class ApplicationModule implements NestModule {
  constructor(
    private readonly articleService: ArticleService,
    private readonly graphQLFactory: GraphQLFactory,
    private readonly userService: UserService,
    private readonly cinemaClassicsService: CinemaClassicsService,
    private readonly artformCategoryService: ArtformCategoryService,
    private readonly configService: ConfigService,
    private readonly geoChapterService: GeoChapterService,
    private readonly downloadCategoryService: DownloadCategoryService,
    private readonly artformService: ArtformService,
    private readonly personalProfileService: PersonalProfileService,
    private readonly artistProfileService: ArtistProfileService,
    private readonly artistListService: ArtistListService,
  ) {}
  configure(consumer: MiddlewareConsumer) {
    const awsSettings = this.configService.getAWSConfig();
    const disableCors = this.configService.disableCors();
    let headers: {
      'Access-Control-Allow-Origin'?: string;
    } = {};
    if (disableCors) {
      headers = { 'Access-Control-Allow-Origin': 'http://localhost:3010' };
    }
    consumer
      .apply(
        require('react-s3-uploader/s3router')({
          bucket: awsSettings.defaultBucket,
          region: awsSettings.defaultRegion,
          signatureVersion: 'v4',
          headers,
          ACL: 'public-read',
          uniquePrefix: true,
        }),
      )
      .forRoutes('/s3');
    consumer.apply(AuthMiddleware).forRoutes('/health/anonymous');

    consumer.apply(AuthMiddleware).forRoutes('/health/authenticated');

    consumer.apply(AuthMiddleware).forRoutes('/query');

    const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql', './../node_modules/@ankitbabbar/instiapp-api-core/src/**/*.graphql');
    const resolvers = {
      DateTime: GraphQLDateTime,
      Date: GraphQLDate,
      Time: GraphQLTime,
    };

    const schema = this.graphQLFactory.createSchema({
      typeDefs,
      resolvers,
      resolverValidationOptions: {
        requireResolversForResolveType: false,
      },
    });

    consumer
      .apply(
        graphqlExpress((req, res) => {
          const viewer = res.locals.viewer;
          const dataLoaders: ContribDataLoaders = {
            user: new DataLoader((ids: number[]) => this.userService.getManyByIds(viewer, ids)),
            article: new DataLoader((ids: number[]) => this.articleService.getManyByIds(viewer, ids)),
            cinemaClassics: new DataLoader((ids: number[]) => this.cinemaClassicsService.getManyByIds(viewer, ids)),
            artform: new DataLoader((ids: number[]) => this.artformService.getManyByIds(viewer, ids)),
            geoChapter: new DataLoader((ids: number[]) => this.geoChapterService.getManyByIds(viewer, ids)),
            artformCategory: new DataLoader((ids: number[]) => this.artformCategoryService.getManyByIds(viewer, ids)),
            downloadCategory: new DataLoader((ids: number[]) => this.downloadCategoryService.getManyByIds(viewer, ids)),
            personalProfile: new DataLoader((ids: number[]) => this.personalProfileService.getManyByIds(viewer, ids)),
            artistList: new DataLoader((ids: number[]) => this.artistListService.getManyByIds(viewer, ids)),
            artistProfile: new DataLoader((ids: number[]) => this.artistProfileService.getManyByIds(viewer, ids)),
          };
          const context: IGQLContext = {
            viewer,
            dataLoaders,
          };
          return {
            schema,
            rootValue: req,
            formatError,
            context,
          };
        }),
      )
      .forRoutes('/query');
  }
}
biels commented 6 years ago

How would this work with the new GraphQLModule.forRoot({...options})? Where should we initialize the context? Maybe creating a wrapper module and doing it on configure(consumer) somehow? It should be a different instance of data loader(s) for each different request.

mohaalak commented 5 years ago

How would this work with the new GraphQLModule.forRoot({...options})? Where should we initialize the context? Maybe creating a wrapper module and doing it on configure(consumer) somehow? It should be a different instance of data loader(s) for each different request.

take a look at this approach https://github.com/nestjs/graphql/issues/2#issuecomment-439650698

rychkog commented 5 years ago

@kamilmysliwiec Dataloader is supposed to batch all the resolved entity ids and query them in one go as a single SQL query.

But for some reason when used in Nest it executes more than one query for a single request.

Here is an example

Suppose we have a following GQL query

query {
  persons {
    name
    address {
      street
    }
  }
}

So addresses for all the persons from this query should be fetched using a single SQL query (using dataloader approach) but in reality, I have something like this:

1) All the persons fetched 2) Dataloader collects and loads address ids for persons 1 and 2 (SQL query 1) 3) Then for person 3 (SQL query 2) 4) Then for persons 4 and 5 (SQL query 3)

I know that dataloader should be invoked on a nextTick but looks like there is something in a way NestJS implements resolvers.

Any help is appreciated, maybe I just missed something regarding the dataloader..

I think this is related https://github.com/graphql/dataloader/issues/150

rychkog commented 5 years ago

For those who faced the same issue, I've mentioned above. It was caused by the fact that on every GQL property resolution Nest invokes my Auth global guard which also does lots of async stuff, interfering with dataloader's execution.

I've added a flag that checks whether a user is already authenticated in the guard in order to prevent its redundant executions.

@kamilmysliwiec Is this expected behavior that guards (probably interceptors, filters, etc. as well) get executed on every GQL property resolution?

kamilmysliwiec commented 5 years ago

@kamilmysliwiec Is this expected behavior that guards (probably interceptors, filters, etc. as well) get executed on every GQL property resolution?

@rychkog this is fixed in @nestjs/graphql@next already. I'll publish this release as soon as possible.

jakubnavratil commented 5 years ago

@kamilmysliwiec maybe this could be somewhat optional? you disabled all guards, filters and interceptors. what if I want to run guard againts some property?

In my case I was running interceptor altering the context and this was breaking change. I think this ability could be actually in ResolveProperty options or something, especially guards should be allowed.

kamilmysliwiec commented 5 years ago

@trubit it would have a big impact on the performance. I'd suggest moving this logic inside the resolve property handler.

EdouardBougon commented 5 years ago

I agree with @trubit , it's a breaking change.

For example, I use guard to control access to my properties, with a AuthGuard and RoleGuard. Some properties are like "query", with filter, pagination etc... and need the same rules as query or mutation.

So now, we will have 2 different implementations to check roles and auth ...

jakubnavratil commented 5 years ago

I imagine this line https://github.com/nestjs/graphql/blob/1504e021af3119feb8d8ecaed29b47d695939d9c/lib/services/resolvers-explorer.service.ts#L134 could be somehow referenced in @ResolveProperty's decorator options. At least making it possible to add guards on properties. Of course it takes some performance hits, was it so big?

vnenkpet commented 5 years ago

@zuohuadong How I do it is I use request-scoped providers that create instances of injectable services that have dataloaders in them.

export interface IDataLoader<K, V> {
  load(id: K): Promise<V>;
}
import DataLoader from 'dataloader';
import { UserEntity } from '../user.entity';
import { IDataLoader } from 'src/api/common/interfaces/data-loader.interface';
import { Connection } from 'typeorm';

export class UsersDataLoader implements IDataLoader<string, UserEntity> {
  constructor(private readonly dataLoader: DataLoader<string, UserEntity>) {}

  public static async create(connection: Connection): Promise<UsersDataLoader> {
    const usersRepo = connection.getRepository<UserEntity>(UserEntity);
    const dataLoader = new DataLoader<string, UserEntity>(async keys => {
      const loadedEntities = await usersRepo.findByIds(keys);
      return keys.map(key => loadedEntities.find(entity => entity.id === key)); // sort by keys
    });

    return new UsersDataLoader(dataLoader);
  }

  public async load(id: string) {
    return this.dataLoader.load(id);
  }
}
import { Provider, Scope } from '@nestjs/common';
import { Types } from 'src/common/types';
import { UsersDataLoader } from './users.data-loader';

export const usersDataLoaderProvider: Provider = {
  inject: [Types.DATABASE_CONNECTION],
  useFactory: UsersDataLoader.create,
  provide: UsersDataLoader,
  scope: Scope.REQUEST,
};

Then you simply inject it as a regular service in the resolver.

@Resolver(of => PostType)
export class PostResolver {
  constructor(
    private readonly usersDataLoader: UsersDataLoader,
  ) {}
  @ResolveProperty('author')
  public async author(@Parent() post: PostEntity) {
    return this.usersDataLoader.load(post.authorId);
  }
}

This way you can also use the dataloaders anywhere in the code, like in service classes, not only in resolver methods.

secmohammed commented 5 years ago

@vnenkpet this throws an error Nest can't resolve dependencies of the UserDataLoader (?).

secmohammed commented 5 years ago

it took me like 2 days debugging how to use DataLoader with Nestjs, and finally I could make it. so here is what I did perhaps this might help someone else in the future.

first create your contract as @vnenkpet did.

export interface IDataLoader<K, V> {
    load(id: K): Promise<V>;
}

then create your user loader for example.

import { Injectable, Inject, forwardRef } from "@nestjs/common";
import { ModuleRef } from "@nestjs/core";
import { UserDTO } from "@commerce/shared";
import DataLoader = require("dataloader"); // commonjs module

import { IDataLoader } from "../contracts/nest-dataloader";
import { UserService } from "../users/user.service";

@Injectable()
// you can replace the UserDTO with your entity or whatever, just pass what you want to accept and return in the end.
export class UserDataLoader implements IDataLoader<string, UserDTO> {
    constructor(private readonly dataLoader: DataLoader<any, any>) {}

    public static async create(
        userService: UserService // injecting the user service as I'm going to use it to fetch the users by ids
    ): Promise<UserDataLoader> {
        // your Dataloader, which again accepts the ids and return whatever you want.
        const dataloader = new DataLoader<string, UserDTO>(async ids => {
            let users = await userService.fetchUsersByIds(ids);
            // after having the response, map the ids you already have with the users you got from the service by ids. So Dataloader can navigate later.
            return ids.map(key => users.find(entity => entity.id === key));
        });
        return new UserDataLoader(dataloader);
    }
    public async load(id: string) {
        return this.dataLoader.load(id);
    }
}

In my scenario, I had to use this userService at another module, so here is the code for that.

at your users module you must put that service at providers property in order to work, and you have to export it from there as you are going to use it in another module ( so you can import it from there )

// at your users module.
@Module({
    providers:  [UserService, UserResolver],
    exports: [UserService]
})

at the other module you are trying to use the loader at ( which in my case was products module and I needed to fetch users associated with these products.)

// at your products module or whatever module.

@Module({
    providers: [
        ProductResolver,
        ProductService,
        {
            inject: [UserService],
            useFactory: UserDataLoader.create,
            provide: UserDataLoader,
            scope: Scope.REQUEST
        }
    ],
    imports: [UsersModule]
})

then finally at ProductResolver, I injected it.


 constructor(
        private readonly usersDataLoader: UserDataLoader
    ) {}

then when resolving the property at the same resolver.

 @ResolveProperty("user", () => UserDTO)
    async user(@Parent() product: ProductDTO): Promise<UserDTO> {
        return this.usersDataLoader.load(product.user.id);
    }
jeppe-smith commented 5 years ago

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}
pablor21 commented 5 years ago

@secmohammed @jeppe-smith For some reason, your solution doesn't fix the n+1 problem, I've enabled the query logger in mysql and I can see that the fetch queries for the related objects are fired one by one. If I remove the Scope.REQUEST, then the queries are executed once, but the loader cache keeps the results until the app closes, this will be problematic if we are loading a huge amount of related objects (since the objects are saved on memory), we could end up loading the whole database in memory...

secmohammed commented 5 years ago

Dataloader is designed to end up the cached version by the end of the request. It was designed to do so. this will help you to batch a multiple requests with the ids and by the end of the promise fetch all of the requests at once. you can integrate with a Redis server or so to accomplish having a cached version of your data. Also, Having a N+1 is basically based on your sql query you are trying to do, it should be where id is in (...your ids), and this one will be batched at the very last of your promise to fetch them at once. However, I don't know how Scope.Request makes the change.

pablor21 commented 5 years ago

The problem seems be that the DataLoader is instantiated multiple times inside the request: on each result... The Scope.DEFAULT, obviously keep the same instance of DataLoader, since it's singleton...

EdouardBougon commented 5 years ago

I agree with @pablor21, I encountered the same problem with multiple instance by request. May be it's a bug with Nest. So, for now, I use this solution: https://github.com/nestjs/graphql/issues/2#issuecomment-498968008

jeppe-smith commented 5 years ago

@pablor21 How are you sending requests that cause multiple instances of the dataloader? If I send a query like

query {
  first: getFixtureByCode(code: 855172) {
    id
  }
  second: getFixtureByCode(code: 855172) {
    id
  }
  third: getFixtureByCode(code: 855174) {
    id
  }
}

I get a single instance of dataloader and my database is queried once with { codes: [855172, 855174] } as I would expect it to.

krislefeber commented 5 years ago

I created a small npm package, nestjs-dataloader, since I also struggled with this. Hopefully it helps someone else.

Mautriz commented 4 years ago

I struggled a lot trying to understand this, I'll just write my solution which probably isn't the cleanest but it's easiest for myself at least

PS: the "genericOneToManyDataLoader" can be of course custom made made for every field, I was just experimenting some generic way (btw, if someone can help me make it typesafe I would appreciate a lot, I don't know how to do it, now it just returns "any[]")

export const genericOneToManyDataLoader = (t: any, keyField: string) =>
  new DataLoader<number, typeof t[]>(async (keys: number[]) => {
    const items = await getRepository(t).find({
      where: { [keyField]: In(keys) },
    });

    return keys.map(id => items.filter(el => el[keyField] === id));
  });

export interface MyContext {
  req: Request;
  res: Response;
  commentDataLoader: ReturnType<typeof genericOneToManyDataLoader>;
  postDataLoader: ReturnType<typeof genericOneToManyDataLoader>;
}

@Module({
  imports: [
    PostModule,
    CommentModule,
    UserModule,
    GraphQLModule.forRoot({
      playground: true,
      debug: true,
      autoSchemaFile: true,
      installSubscriptionHandlers: true,
      context: ({ req, res }) => ({
        req,
        res,
        commentDataLoader: genericOneToManyDataLoader(Comment, 'postId'),
        postDataLoader: genericOneToManyDataLoader(Post, 'userId'),
      }),
    }),
    DatabaseModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
// user.resolver.ts -- its literally the same in comments
 @ResolveField(() => [Post], { nullable: 'items' })
  async posts(@Parent() parent: User, @Context() ctx: MyContext) {
    return ctx.postDataLoader.load(parent.id);
  }
Mautriz commented 4 years ago

UPDATE: I needed services in loaders. and the above solution of mine was bad, I ended up using @jeppe-smith s one which worked very well It generated three queries (as expected) for a users -> posts -> comments (many -> many -> many) query, and to me it seems the easiest solution

Thank you a lot for that, I don't understand why others say this generates multiple dataloaders/queries, I get only one per request as expected

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}
TreeMan360 commented 4 years ago

https://github.com/TreeMan360/nestjs-graphql-dataloader

WillSquire commented 4 years ago

This is what I've got:

import { Injectable, Scope } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import DataLoader from 'dataloader'
import { Repository } from 'typeorm'
import { Project } from './project.entity'

@Injectable({ scope: Scope.REQUEST })
export class ProjectLoader {
  constructor(
    @InjectRepository(Project)
    private readonly projectRepository: Repository<Project>,
  ) {}

  readonly findOneByIds = new DataLoader<string, Project | undefined>(async (ids) => {
    const projects = await this.projectRepository
      .createQueryBuilder('project')
      .where('project.id IN (:...ids)', { ids })
      .getMany()

    return ids.map((id) => projects.find((n) => n.id === id))
  })
}

Perhaps it's a naive attempt, but I've made the loader a data access layer containing the database logic, which the service layer accesses and doesn't overlap with. Trouble I'm having is @Inject(CONTEXT) private context seems to throw an error when implemented like in the docs: https://docs.nestjs.com/fundamentals/injection-scopes. Really need to initialise the dataloaders with the current user within the request to work out access rights, like in the dataloader docs: https://github.com/graphql/dataloader#creating-a-new-dataloader-per-request.

Anyone else having this issue?

EDIT: Got it working getting the current user through the request, although I couldn't get the param decorator to work at the moment (like with the resolvers):

import { Inject, Injectable, Scope } from '@nestjs/common'
import { CONTEXT } from '@nestjs/graphql'
import { InjectRepository } from '@nestjs/typeorm'
import DataLoader from 'dataloader'
import { Repository } from 'typeorm'
import { Project } from './project.entity'

interface RequestUser {
  req: {
    user: { id: string }
  }
}

@Injectable({ scope: Scope.REQUEST })
export class ProjectLoader {
  constructor(
    @Inject(CONTEXT) private context: RequestUser,
    @InjectRepository(Project)
    private readonly projectRepository: Repository<Project>
  ) {}

  readonly findOneById = new DataLoader<string, Project | undefined>(
    async (ids) => {
      const user = this.context.req.user // Would ideally use decorator for this instead (i.e. from createParamDecorator)
      const projects = await this.projectRepository
        .createQueryBuilder('project')
        .where('project.id IN (:...ids)', { ids })
        .andWhere('project.owner = :userId', { userId: user.id })
        .getMany()

      return ids.map((id) => projects.find((n) => n.id === id))
    }
  )
}
incompletude commented 4 years ago

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

This is by far the most elegant solution.

Artimunor commented 4 years ago

I too have the problem when implementing the soltion marked by @kamilmysliwiec as Tutorial that I see multiple queries executed. The only difference between the provided solution and my solution is that I am loading a related entity based on a key of the main value, I do however map them accordingly so don't see how that could be the problem. Is there something specific to take into account to make sure they keys are batched before sent to the service function? My solution does not provide multiple queries in one request to from the frontend to the backend either by the way, it asks for a list of entities and then loads sublists of entities which should be batched by the dataloader but are not. I am using @ResolveField for that field.

Any ideas?

bandreetto commented 3 years ago

@Artimunor I was having a problem with @incompletude solution because not every field resolver in my application was using dataloaders, thus desyncing the execution and preventing the dataloaders to batch. When I applied the dataloaders to every field resolver of my graph it worked correcty batching every query on the same depth of the graph.

johnfrades commented 3 years ago

I came up with the following that seems to work nicely. fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

This is by far the most elegant solution.

Thanks for this man! I've been looking on this solution, works perfectly on @ResolveField. I am initially using nestjs-dataloader but encountering problem if there are 2 or more interceptors being applied, it's like you can only use 1 dataloader on your whole app. Thanks for this solution.

johnfrades commented 3 years ago

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

For some reason, the @nestjs/event-emitter is not working if it detects any injectables that has Scope.REQUEST on it (Even if that service has nothing to do with the module thats using the event emitter). That's why i temporarily remove the Scope.REQUEST on the dataloader injectables to make my event emitter work

RAllner commented 3 years ago

Hey guys. I also tried the solution provided by @jeppe-smith. This works fine on ResolveFields and such, but only on Queries. Could be my general misunderstanding of the whole graphQL part of NestJS but I stumbled upon it while implementing this solution. I had as well problems to implement the callBack async like described here. Just to mention that for others: The dataloaders on the ResolveFields are not getting used in case of a mutation or subscriptions.

babymum7 commented 3 years ago

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

This solution first seems elegant, but it quite a bad design because the dataloader is a request scope provider, so any other providers that depend on it will also become request scope. It means that the resolver provider will be recreated every incoming request. Refer to https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy

kamilmysliwiec commented 3 years ago

This solution first seems elegant, but it quite a bad design because the dataloader is a request scope provider, so any other providers that depend on it will also become request scope.

Just FYI: there's nothing wrong with that. This is precisely why the "request-scoped" providers feature was added to the framework.

knownasilya commented 3 years ago

Haven't seen dataloaders for typeorm relationships like https://github.com/slaypni/type-graphql-dataloader#with-typeorm

Has anyone done anything like that? I'm assuming the above doesn't work with nestjs/graphql since it doesn't use type-graphql.

kasvith commented 3 years ago

I strongly believe this should be properly addressed in docs. At least some guidance regarding how to use DataLoader with NestJS(similar to Authz with Casl).

kasvith commented 3 years ago

For anyone who is struggling with this, I wrote an article about How I integrated Dataloader with NestJS

Hope this helps anyone else who is struggling with the same problem 😃

https://kasvith.me/posts/using-dataloader-with-nestjs/