Giter VIP home page Giter VIP logo

Comments (38)

jeppe-smith avatar jeppe-smith commented on May 5, 2024 27

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
    }
  }
}

from docs.nestjs.com.

vnenkpet avatar vnenkpet commented on May 5, 2024 25

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

from docs.nestjs.com.

krislefeber avatar krislefeber commented on May 5, 2024 8

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

from docs.nestjs.com.

kasvith avatar kasvith commented on May 5, 2024 8

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

from docs.nestjs.com.

kasvith avatar kasvith commented on May 5, 2024 7

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/

from docs.nestjs.com.

Mautriz avatar Mautriz commented on May 5, 2024 6

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);
  }

from docs.nestjs.com.

secmohammed avatar secmohammed commented on May 5, 2024 5

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);
    }

from docs.nestjs.com.

incompletude avatar incompletude commented on May 5, 2024 5

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.

from docs.nestjs.com.

kamilmysliwiec avatar kamilmysliwiec commented on May 5, 2024 4

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

from docs.nestjs.com.

TreeMan360 avatar TreeMan360 commented on May 5, 2024 4

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

from docs.nestjs.com.

bandreetto avatar bandreetto commented on May 5, 2024 2

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

from docs.nestjs.com.

kamilmysliwiec avatar kamilmysliwiec commented on May 5, 2024 2

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.

from docs.nestjs.com.

jakubnavratil avatar jakubnavratil commented on May 5, 2024 1

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

from docs.nestjs.com.

Mautriz avatar Mautriz commented on May 5, 2024 1

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
    }
  }
}

from docs.nestjs.com.

Artimunor avatar Artimunor commented on May 5, 2024 1

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?

from docs.nestjs.com.

babymum7 avatar babymum7 commented on May 5, 2024 1

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

from docs.nestjs.com.

Mirokko avatar Mirokko commented on May 5, 2024

You probably wanna start with Interceptors.

from docs.nestjs.com.

Mirokko avatar Mirokko commented on May 5, 2024

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

from docs.nestjs.com.

caseyduquettesc avatar caseyduquettesc commented on May 5, 2024

nestjs/graphql#2 (comment)

from docs.nestjs.com.

babbarankit avatar babbarankit commented on May 5, 2024

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');
  }
}

from docs.nestjs.com.

biels avatar biels commented on May 5, 2024

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.

from docs.nestjs.com.

mohaalak avatar mohaalak commented on May 5, 2024

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
nestjs/graphql#2 (comment)

from docs.nestjs.com.

rychkog avatar rychkog commented on May 5, 2024

@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 graphql/dataloader#150

from docs.nestjs.com.

rychkog avatar rychkog commented on May 5, 2024

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?

from docs.nestjs.com.

kamilmysliwiec avatar kamilmysliwiec commented on May 5, 2024

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

from docs.nestjs.com.

EdouardBougon avatar EdouardBougon commented on May 5, 2024

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

from docs.nestjs.com.

jakubnavratil avatar jakubnavratil commented on May 5, 2024

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?

from docs.nestjs.com.

secmohammed avatar secmohammed commented on May 5, 2024

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

from docs.nestjs.com.

pablor21 avatar pablor21 commented on May 5, 2024

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

from docs.nestjs.com.

secmohammed avatar secmohammed commented on May 5, 2024

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.

from docs.nestjs.com.

pablor21 avatar pablor21 commented on May 5, 2024

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

from docs.nestjs.com.

EdouardBougon avatar EdouardBougon commented on May 5, 2024

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: nestjs/graphql#2 (comment)

from docs.nestjs.com.

jeppe-smith avatar jeppe-smith commented on May 5, 2024

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

from docs.nestjs.com.

WillSquire avatar WillSquire commented on May 5, 2024

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))
    }
  )
}

from docs.nestjs.com.

johnfrades avatar johnfrades commented on May 5, 2024

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.

from docs.nestjs.com.

johnfrades avatar johnfrades commented on May 5, 2024

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

from docs.nestjs.com.

RAllner avatar RAllner commented on May 5, 2024

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.

from docs.nestjs.com.

knownasilya avatar knownasilya commented on May 5, 2024

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.

from docs.nestjs.com.

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.