type-graphql icon indicating copy to clipboard operation
type-graphql copied to clipboard

Integration with Nest

Open stevefan1999-personal opened this issue 5 years ago • 85 comments

Owner note

Scroll to typegraphql-nestjs package info ⬇️

Original post

I was wondering if there could be more support for Nest.js, in particular, integration with an authorization endpoint is needed. Right now, I'm using this NPM package + TypeORM, following the sample, so far so good but unfortunately the source was nowhere to be found, and so I have to workaround my auth checker which required me to pass the database module into the context but it is very dangerous. So what about an official extension of TypeGraphQL to Nest.JS? This would make it easier to write universal services.

stevefan1999-personal avatar Aug 29 '18 07:08 stevefan1999-personal

HI @stevefan1999 ,

I am the author of that package, and here is the source https://github.com/MagnusCloudCorp/nestjs-type-graphql,

But it's really really basic integration I made for a small project, just a matter to respect NestJs lifecycle.

You can easily put the same code inside your app, so you have full control.

If you have any question!

Salimlou avatar Aug 29 '18 08:08 Salimlou

The goal of TypeGraphQL integration with Nest is to create a replacement/substitute for the GraphQL (Apollo) module for Nest, so all the Nest features like pipes or guards would work. This requires a lot of changes in TypeGraphQL core so it will be done later 😞

MichalLytek avatar Aug 29 '18 08:08 MichalLytek

Would love to see it! ❤️

kamilmysliwiec avatar Aug 30 '18 15:08 kamilmysliwiec

I have this working without needing anything fancy

import { Injectable, Logger } from '@nestjs/common';
import { GqlOptionsFactory, GqlModuleOptions } from '@nestjs/graphql';
import { buildSchema } from 'type-graphql';

@Injectable()
export class GraphqlConfigService implements GqlOptionsFactory {
  async createGqlOptions(): Promise<GqlModuleOptions> {
    const schema = await buildSchema({
      resolvers: [__dirname + '../**/*.resolver.ts'],
    });

    return {
      debug: true,
      playground: true,
      schema,
    };
  }
}

And then in the app.module...

  imports: [
    GraphQLModule.forRootAsync({
      useClass: GraphqlConfigService,
    }),
  ],

Sacro avatar Sep 20 '18 21:09 Sacro

@Sacro Do you have injects available for TGQL resolvers in runtime?

stevefan1999-personal avatar Sep 21 '18 02:09 stevefan1999-personal

I'm not sure I follow.

My current repo is at https://gitlab.benwoodward.me.uk/pokedex/backend

Sacro avatar Sep 21 '18 10:09 Sacro

@Sacro In this case, you aren't using NestJS injections at all in resolvers far as I see, you also used Active Record pattern so you evaded the need for a repository. Unfortunately, I was indeed using AR pattern (with @nestjs/typeorm package) and some other providers so Injectable is definitely needed.

But the problem is, NestJS doesn't have a global container, it is re-entrant-able, so instead, the container is attached by an instance context by design.

stevefan1999-personal avatar Sep 21 '18 10:09 stevefan1999-personal

@stevefan1999-personal Injecting repositories seems to work fine for me when I build off @Sacro's awesome integration work. Here is one of my resolvers that I moved from @nestjs/graphql to type-graphql along with the original statements.

item.resolvers.ts

// import { Resolver, Query } from '@nestjs/graphql';
import { Resolver, Query } from 'type-graphql';

import { Item } from './item.entity';
import { ItemService } from './item.service';

// @Resolver('Item')
@Resolver(_of => Item)
export class ItemsResolvers {
  constructor(private readonly itemsService: ItemService) {}

  // @Query()
  @Query(_returns => [Item])
  public async getItems(): Promise<Item[]> {
    return this.itemsService.findAll();
  }
}

item.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Item } from './item.entity';

@Injectable()
export class ItemService {
  constructor(@InjectRepository(Item) private readonly itemRepository: Repository<Item>) {}

  public async findAll(): Promise<Item[]> {
    return this.itemRepository.find();
  }
}

caseyduquettesc avatar Sep 27 '18 06:09 caseyduquettesc

I just realized that integration could be fairly easy. I'll provide a POC once I find some time :)

kamilmysliwiec avatar Sep 27 '18 07:09 kamilmysliwiec

I just realized that integration could be fairly easy. I'll provide a POC once I find some time :)

Hello Kamil, do you have any news about this POC ? I'm trying to find the best integration method for type-graphql and Nest.js.

ghost avatar Oct 09 '18 14:10 ghost

Hey @kamilmysliwiec I found type-graphql this past weekend, which I spend looking at many possible graphql solutions (from prisma to postgraphile to type-graphql). My opinion, it rocks! Just like NestJS, you can build anything :-) I think it's the most mature and feature complete library out there, and it will be AWESOME if it works smoothly with Nest - we can't wait for nest/type-graphql ;-)

So, I've been trying a couple of days to make them work and eventually they did, but with quite a few hacks, for example:

The type-graphql @Resolver classes are injected their service props fine initially, but then type-graphql creates it's own instance of the resolver and it is using it. Of course, it misses to DI the new instance, so all injections are lost.

So I had to hack it like:

let service: TheService;
@Resolver()
export class TheResolver {  
  constructor(private _service: TheService) {
    if (_service) service = _service; // workaround for DI 
  }
  @Query(ret => Them)
  getEm() { return service.getEm(); }
}

but its not nice or scalable etc.

So if we can make this integration happen, it will be really awesome!!!

anodynos avatar Oct 09 '18 23:10 anodynos

Hey @anodynos, you can look at this working solution :

src/app/app.module.ts

import * as path from 'path';

import { APP_INTERCEPTOR } from '@nestjs/core';
import { Module, CacheModule, CacheInterceptor } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GraphQLModule } from '@nestjs/graphql';
import { ConfigModule } from 'nestjs-config';

import { TypeOrmConfigService } from './typeorm.config.service';
import { GraphqlConfigService } from './graphql.config.service';

import { PaysModule } from 'pays/pays.module';

@Module({
  imports: [
    ConfigModule.load(path.resolve(__dirname, 'config/**/*.{ts,js}')),
    CacheModule.register(),
    TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService }),
    GraphQLModule.forRootAsync({ useClass: GraphqlConfigService }),
    PaysModule,
  ],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {
}

src/app/graphql.config.service.ts

import { Injectable } from '@nestjs/common';
import { GqlOptionsFactory, GqlModuleOptions } from '@nestjs/graphql';
import { ConfigService } from 'nestjs-config';
import { useContainer, buildSchema } from 'type-graphql';

@Injectable()
export class GraphqlConfigService implements GqlOptionsFactory {
  constructor(private readonly config: ConfigService) { }
  
  setContainer(container: any) {
    useContainer(container);
  }

  async createGqlOptions(): Promise<GqlModuleOptions> {
    const schema = await buildSchema({
      resolvers: [ __dirname + '../**/*.resolver.ts' ],
    });

    return {
      debug: this.config._isDev(),
      playground: this.config._isDev(),
      schema,
    };
  }
}

src/pays/pays.service.ts

import { Injectable } from '@nestjs/common';

import { Pays } from './pays.entity';

@Injectable()
export class PaysService {
 async findAll(): Promise<Pays[]> {
    return Pays.find();
  }
}

src/pays/pays.resolver.ts

import { Injectable } from '@nestjs/common';
import { Resolver, Query, Arg, Int } from 'type-graphql';

import { Pays } from './pays.entity';
import { PaysService } from './pays.service';

@Resolver(of => Pays)
export class PaysResolver {
  constructor(private readonly paysService: PaysService) { }
  
  @Query(returns => [Pays])
  async pays(): Promise<Pays[]> {
    return this.paysService.findAll();
  }
}

src/main.ts

// Nest App
const app = await NestFactory.create(AppModule, server);
const graphQlConfigService = app.get(GraphqlConfigService);
graphQlConfigService.setContainer(app);

ghost avatar Oct 10 '18 14:10 ghost

That method still won't use any guards or interceptors that you have setup in NestJS. The only way I've gotten requests to go through those is by using Nest's @Resolver decorator, which can't be used on the same class as type-graphql's @Resolver decorator.

caseyduquettesc avatar Oct 10 '18 17:10 caseyduquettesc

Thank you Christopher @christopherdupont - I had a similar setup at some point, didn't work :-( Does yours work without the problem/workaround I described above?

Basicaly I took code / ideas from @Sacro https://gitlab.benwoodward.me.uk/pokedex/backend I think my when I tried to @InjectRepository on my service and the service into the Resolver, I started having the problem I said:

@Resolver(of => Pays)                                                            
export class PaysResolver {
  constructor(private readonly paysService: PaysService) { }   // <---- THIS IS CALLED TWICE - 1st time with `paysService` instance (instantiated by NestJS) and second time by type-graphql, with `null`.

If your's doesn't have this problem, I 'd like to examine your setup - can you please put a min working example on a repo?

@caseyduquettesc Sure, there are a lot of integration points (and pains!!!) still... We're can't wait for @nest/type-graphql@~1.0 :-)

anodynos avatar Oct 10 '18 20:10 anodynos

Thank you @christopherdupont - I had a look and I think it works because you're using

@Entity({ name: 't_pays' })
@ObjectType()
export class Pays extends BaseEntity

I think if you start using Repository based entities or start using more DI, you'll start facing problems.

anodynos avatar Oct 10 '18 22:10 anodynos

I created a module for NestJs, combining the typegraphql and nestjs decorators, the type graphql will generate the schema from typescript and NestJS will provide the resolvers, so guards, pipes, interceptors, injection is on NestJS but schema generating is by type graphql.

https://github.com/mohaalak/nestjs_typegraphql

mohaalak avatar Nov 15 '18 22:11 mohaalak

I have just finished POC integration locally. The biggest issue that I'm encountering frequently so far is an inconsistency between graphql libraries. Hence, I'm quite often seeing this error: https://19majkel94.github.io/type-graphql/docs/faq.html#i-got-error-like-cannot-use-graphqlschema-object-object-from-another-module-or-realm-how-to-fix-that

which makes it almost impossible to provide a good developer experience for the end-user (calling npm dedupe is always required). This issue has been discussed here already: https://github.com/19majkel94/type-graphql/issues/192 and I see that it's not going to be fixed. However, I would love to keep type-graphql as a peer dependency (and graphql is nest peer dep as well), therefore, these libraries would be able to share the same graphql package by default. So the question goes here (cc @19majkel94): since the graphql package has to stay as a normal dependency of the type-graphql, could you expose another function which works same as buildSchema but instead of returning a schema, it would additionally call printSchema(schema) (from internal graphql-tools) and return us a string? :) For now (as a workaround), I have to emit schema to the file and afterward, read file content to get the stringified schema (what heavily affects library performance).

kamilmysliwiec avatar Jan 04 '19 17:01 kamilmysliwiec

@kamilmysliwiec

However, I would love to keep type-graphql as a peer dependency (and graphql is nest peer dep as well), therefore, these libraries would be able to share the same graphql package by default.

The problem is that TypeGraphQL heavily depends on graphql-js (and @types/graphql) and it's hard to control the version using peer dependencies mechanism. I can bump major version (with 0.x.y it's not a problem) when bumping peer deps minor version of graphql-js but this might fail in runtime (cannot read property 'directives' of undefined) as many people ignore npm peer deps warnings in console on install.

could you expose another function which works same as buildSchema but instead of returning a schema, it would additionally call printSchema(schema) (from internal graphql-tools) and return us a string? :)

emitSchemaFile options (as well as emitSchemaDefinitionFile helper function) are only a 3-lines wrapper around printSchema and writeFile. It doesn't change the schema building pipeline, it only acts as a helper for better DX because many people think that buildSchema return a special object, not a standard graphql-js schema.

So all you need to do is:

const schema = await buildSchema(options);
const schemaString = printSchema(schema);

I also have a PoC of building apollo-like resolversMap on a branch, this might help you with the integration: https://github.com/19majkel94/type-graphql/tree/resolvers-map

MichalLytek avatar Jan 05 '19 21:01 MichalLytek

Actually, I took a look into the source code of these functions already. The problem is that when I have tried to do the exact same thing as you proposed above^

const schema = await buildSchema(options);
const schemaString = printSchema(schema);

I was also getting the graphql inconsistency error, because in this case, buildSchema uses nested graphql dependency of type-graphql (and schema is built using this internal version) while my imported printSchema would come from another graphql package.

kamilmysliwiec avatar Jan 05 '19 21:01 kamilmysliwiec

@kamilmysliwiec You're right, I will try to finish the work on generating typedefs-resolvers instead of the executable schema as soon as possible.

I will also try to find a way to detect the version of graphql in node_modules to throw error when peer dependency version isn't correct. I see that the whole ecosystem is switching to having graphql as a peer dependency, so this will solve a lot of compatibility problems.

And sorry for the late response 😞

MichalLytek avatar Jan 11 '19 17:01 MichalLytek

@kamilmysliwiec is there any chance that you share your code to see how you attempted to integrate Type-graphql to Nest?

I am really looking forward to be able to use Type-graphql with Nest.

apiel avatar Jan 21 '19 10:01 apiel

@kamilmysliwiec Thanks to #233 and #239, you now should be able to finish your integration PoC 🎉

You can install the new 0.17.0 beta version - npm i type-graphql@next 🚀 Stable release coming soon 👀

MichalLytek avatar Jan 24 '19 19:01 MichalLytek

Awesome, I'll give it a shot asap 💥

kamilmysliwiec avatar Jan 25 '19 09:01 kamilmysliwiec

@kamilmysliwiec I can't wait to use Type-graphql with Nest.

natqe avatar Jan 25 '19 13:01 natqe

@kamilmysliwiec Just curious, how are things progressing with your PoC?

michelcve avatar Feb 12 '19 10:02 michelcve

I'm actually ready to publish it (waiting for the stable release of type-graphql)

kamilmysliwiec avatar Feb 15 '19 19:02 kamilmysliwiec

@kamilmysliwiec That's great news! Is there a preview that works with type-graphql@next? I'm not sure how long the wait for the stable release of type-graphql is gonne be, but I fear it's still some time until 1.0.0 is ready?

michelcve avatar Feb 16 '19 09:02 michelcve

@michelcve I'm saying about the 0.17.0 :)

kamilmysliwiec avatar Feb 16 '19 09:02 kamilmysliwiec

Phew ;-) (though I have no idea when that version is due ;-))

michelcve avatar Feb 16 '19 13:02 michelcve

I'm actually ready to publish it

I'm glad the changes proved to be enough to make the integration possible.

I'm finishing #180 and after that will release 0.17 stable 🚀 Then large refactoring #183 is coming 🛠

MichalLytek avatar Feb 16 '19 16:02 MichalLytek