npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@webundsoehne/nestjs-graphql-typeorm-dataloader

v4.0.6

Published

A library to utilize the graphql-dataloader with nestjs and typeorm.

Downloads

450

Readme


@webundsoehne/nestjs-graphql-typeorm-dataloader

Version Downloads/week Dependencies semantic-release

Description

nestjs-graphql-typeorm-dataloader implements the middleware and the decorators needed to enable graphql data-loader in the entity or DTO level. So instead of defining the data loader in every resolve field, it is easier to just define it over a more generic field and let it handle which type of relations does it have to resolve and load the data properly. This also eliminates the setup for how to load the data, for which key to load the data for every field resolver.


What it does

  • Registering a nestjs interceptor or graphql plugin enables to set up the connection to give it a unique id to containerize the request better. This enables the data to be loaded for multiple resolve fields.
  • Entities decorated with the nestjs graphql extensions field contains the metadata on how to solve this exact field.
  • field-middleware that is initiated explicitly for that field solves the field fetching according to the direction of the extension metadata and solves the given relation by the database manager.

Setup

Global Setup for Project

This plugin requires an interceptor to keep track of the request ids in a container environment, which could be done by nest itself but the second thing is you inject the connection of typeorm to field-middleware which nestjs does not enable to do. This will write a unique key to every request in the context directly. If you are going to use typeorm explicitly which this plugin is intended for you have to pass in the connection, since dependency injection is not available to fetch in the field-middleware.

You can choose between one of two options.

Interceptor

Import the interceptor and use it globally or module-scoped in your project.

import { getConnection } from 'typeorm'
import { DataLoaderInterceptor } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'

@Module({
  imports: [
    {
      provide: APP_INTERCEPTOR,
      useFactory: (): DataLoaderInterceptor => new DataLoaderInterceptor({ typeormGetConnection: getConnection })
    }
  ]
})
export class ServerModule {}

Apollo Plugin

Initially designed this plugin to use the apollo plugin method, then converted it to an interceptor. Even though they basically do the same thing the apollo-server-plugin is also there. This can be only global scoped since it is injected into the apollo-server itself.

import { getConnection } from 'typeorm'
import { ApolloServerDataLoaderPlugin } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'

@Module({
  imports: [
    GraphQLModule.forRoot({
      // ...
      buildSchemaOptions: {
        plugins: [new ApolloServerDataLoaderPlugin({ typeormGetConnection: getConnection })]
      }
    })
  ]
})
export class ServerModule {}

Field Middleware

Field middleware can either be injected to Field, FieldResolver, or globally.

Unfortunately nest.js does not allow to tamper with the GraphQL set up, so I could not overwrite the middleware field while you are decorating the field with extension so this stayed as a manual process.

This is due to graphql resolvers and field-resolvers inside the nest.js only registered once, therefore you can not lazily register metadata afterward, this behavior as I understand it can be seen in field decorator and field resolver decorator and following the behavior to add metadata for resolvers.

Injecting to a Specific Field

While you will see this making sense in the upcoming examples, it should just be done as follows.

@Field(() => [DocumentHistoryEntity], { nullable: true, middleware: [TypeormLoaderMiddleware] })

Injecting it Globally

To inject this middleware for each field, which will cause a little overhead but not much since it is pretty basic to check the context to have matching keys can be done as follows. But for more specific control over the fields, you can always use the injecting to a specific field approach.

import { getConnection } from 'typeorm'
import { ApolloServerDataLoaderPlugin, TypeormLoaderMiddleware } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'

@Module({
  providers: [
    imports: [
      GraphQLModule.forRoot({
        // ...
        buildSchemaOptions: {
          fieldMiddleware: [TypeormLoaderMiddleware]
        }
      })
    ]
  ]
})
export class ServerModule {}

Resolving Relations

Entities or DTOs should be decorated with directions on how to resolve a relation.

The only critical thing here is getting the relation ids of the relation. Since typeorm already exposes fetching relation ids, another field can be decorated with RelationId and since the parent document will be injected into the function in TypeormLoaderExtension as an argument, this relation ids will be resolved and typeorm metadata will indicate the relation type and it will use the appropriate dataLoader with the given field. You can omit this field's serialization by marking it without a field decorator.

You can either use these decorators in the entity, DTO, or resolver. But the intention is to keep this in the DTOs or entities to define resolving them generically. You can also further process the output result, the GraphQL way.

If you define the resolver and extensions at the entity or DTO level, you do not need to define any field resolvers for a given field, and it will be resolved automatically.

Please do not forget to set the middleware per field if you did not set it globally, else it won't work at all.

Owning Side of the Relationship

Imagine a relationship where every company has a corporation. So it is an incoming relationship from the other side where we inherit the foreign key.

import { TypeormLoaderExtension, TypeormLoaderMiddleware } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'
// ...

@ObjectType()
@Entity('company')
export class CompanyEntity extends BaseEntityWithPrimary<CompanyEntity> {
  // ...

  @Field(() => UUID)
  @Column({ name: 'corporation_id', type: 'uuid' })
  @IsUUID()
  corporationId: string

  // relations-incoming

  @Field(() => CorporationEntity, { middleware: [TypeormLoaderMiddleware] })
  @ManyToOne(() => CorporationEntity, (corporation) => corporation.companies, {
    onUpdate: 'CASCADE',
    onDelete: 'RESTRICT'
  })
  @JoinColumn({ name: 'corporation_id' })
  @TypeormLoaderExtension((company: CompanyEntity) => company.corporationId)
  corporation: CorporationEntity

  // ...
}

Complimentary Side of the Relationship

Using Self-Key

If you want to directly utilize a key from the other side of the relationship, you can set the option as follows. So it is an outgoing relationship where the other side inherits the foreign key.

Imagine that the user can have multiple user-companies, where the other side has the companyId as a foreign key already.

This only works with oneToOne and oneToMany.

import { TypeormLoaderExtension, TypeormLoaderMiddleware } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'
// ...

@ObjectType()
@Entity('company')
export class CompanyEntity extends BaseEntityWithPrimary<CompanyEntity> {
  // ...

  // relations-outgoing

  @Field(() => [UserCompanyEntity], { nullable: true })
  @OneToMany(() => UserCompanyEntity, (userCompany) => userCompany.company, {
    nullable: true,
    onDelete: 'CASCADE'
  })
  @TypeormLoaderExtension((userCompany: UserCompanyEntity) => userCompany.companyId, { selfKey: true })
  userCompanies?: UserCompanyEntity[]

  // ...
}

Using Relation Directly

If the other option does not work out, or you don't have the foreign key joined to the column, you can use the relation id directly.

import { TypeormLoaderExtension, TypeormLoaderMiddleware } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'
import { RelationId } from 'typeorm'
// ...

@ObjectType()
@Entity('company')
export class CompanyEntity extends BaseEntityWithPrimary<CompanyEntity> {
  // ...

  @RelationId((company: CompanyEntity) => company.userCompanies)
  userCompanyIds: string[]

  // relations-outgoing

  @Field(() => [UserCompanyEntity], { nullable: true })
  @OneToMany(() => UserCompanyEntity, (userCompany) => userCompany.company, {
    nullable: true,
    onDelete: 'CASCADE'
  })
  @TypeormLoaderExtension((user: UserEntity) => user.userCompanyIds)
  userCompanies?: UserCompanyEntity[]

  // ...
}

Custom Loader

You can also define your own data loader, but this time it should be in the resolver itself.

import { CustomLoaderExtension, CustomLoaderMiddleware } from '@webundsoehne/nestjs-graphql-typeorm-dataloader'

@Resolver(() => UserEntity)
export class UserResolver {
  @ResolveField('documents', () => UserEntity, {
    nullable: true,
    middleware: [ CustomLoaderMiddleware ]
  })
  @CustomLoaderExtension(async (ids, { context }) => {
    const documents = await this.documentRepository.find({
      where: { user: { id: In(ids) } }
    })

    const documentById = groupBy(documents, 'userId')

    return ids.map((id) => documentById[id] ?? [])
  })
  public resolveDocuments(@Parent() user: UserEntity): (dataloader: DataLoader<number, Photo[]>) => DocumentEntity[]  {
    return (dataloader: DataLoader<string, DocumentEntity[]>) =>
      dataloader.load(user.id)
    }
  }
}

Further Process Data

Since this will resolve value and use the next function to forward it, you can later process the data utilizing a field-resolver.


Based on type-graphql-dataloader.