mikro-orm / mikro-orm

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
https://mikro-orm.io
MIT License
7.64k stars 526 forks source link

Provide Example App for NestJS with Apollo GraphQL and MikroORM (w/ clean Identity Map per Request) #1835

Closed woltob closed 3 years ago

woltob commented 3 years ago

Hi B4nan,

you're doing amazing work with Mikro ORM and I can see this rising to become the standard ORM for NodeJS w/ Typescript, especially with NestJS.

Since GraphQL APIs are becoming more and more common for new projects where front-ends would be tightly coupled with their respective back-end APIs, I was wondering whether you (with the help of some NestJS and Apollo experts), could provide an example app with a go to configuration and setup, especially with regards to a clean per-request Identity Map via the Async Storage of the newer Node frameworks.

Required features:

I think this would be a good setup and base for many more project to start from.

Keep up the amazing work!

B4nan commented 3 years ago

Can't help much with GQL, never really used it myself, but I can see how this combinations is popular - would be indeed great to have such example app, more complete one similar to the realworld example app.

Regarding AsyncLocalStorage, it is already used by default in v5 with the RequestContext helper. I don't plan to add more guides for v4, if something, will target v5.

Would be probably best to adjust some existing example project and just swap the ORMs (and adjust the ORM specific parts). Do you know of something usable? Otherwise I don't have a clue, most of the points you mentioned are something I never used myself, so can't provide best practices 🤷.

Will pin this for some time, maybe someone can help.

briandiephuis commented 3 years ago

After a long love-hate relation with TypeORM I'm now checking out MikroORM. I usually work with NestJS, GraphQL and TypeGraphQL (code-first approach), so I'll give that a try. I'm not aware of any specific example using these libraries, but I'll give it a go to create RealWorld like app (is there a GQL spec for this?) based on my experience. Together we should be able to figure this out :+1: I'm personally interested in adding dataloaders scoped to a single request, so I'll try that too.

I'll post here when I have something to show.

woltob commented 3 years ago

@briandiephuis are you using Postgres as the database?

briandiephuis commented 3 years ago

@woltob Yes, I refactored the example to Postgres :muscle:

I also decided to put the authentication (sign in, sign out, refresh token and forced sign-out) outside of GraphQL. This has worked well for me in the past and seems to be best practice for token based auth. This allows us to use a http-only refresh cookie on the /auth route. Token based auth through GQL would make it so that the refresh cookies would have to be sent on every request which doubles the size of auth-related stuff on each request. I'm interested in what you all think about this :) (More details in the readme)

It's a bit more work than expected, and far from finished, but I'm getting somewhere now. See: https://github.com/briandiephuis/nestjs-realworld-example-app

I found an example of using Mikro-ORM with TypeGraphQL here: https://github.com/MichalLytek/type-graphql/tree/master/examples/mikro-orm but it is built on a bare ApolloServer. There they fork/clone the identity manager for each request here. I found this in the docs:

app.use((req, res, next) => {
  RequestContext.create(orm.em, next);
});

@B4nan How can we get access to the orm object to clone the EntityManager when using NestJS? I think I should add it to each gql request here

briandiephuis commented 3 years ago

To elaborate on the problem; currently lazy loaded relations do not work:

The user.entity has this relation;

  @Field(() => [Article])
  @OneToMany(() => Article, (article) => article.author)
  articles = new Collection<Article>(this);

Pretty much exactly as shown here: https://github.com/MichalLytek/type-graphql/blob/master/examples/mikro-orm/entities/recipe.ts#L22

I'm planning of implementing dataloaders, and using lazy-loaded relations is usually a bad practise - but they are super useful for fast prototyping. Plus, dataloaders also require a request scoped entity manager.

To reproduce Create a User: ```gql mutation createUser($input: CreateUserInput!) { createUser(input: $input) { id } } ``` With variables: ```json { "input": { "email": "d.duck@example.com", "password": "daisy01", "username": "Donald" } } ``` Then sign in (POST to `http://localhost:8000/auth`) with JSON body: ```JSON { "email": "d.duck@example.com", "password": "daisy01" } ``` Create an article using that auth token ```gql mutation createArticle($input: CreateArticleInput!) { createArticle(input: $input) { id title body description } } ``` ```JSON { "input": { "body": "Article body", "description": "Awesome article", "title": "Awesome article", "tagList": [ "article", "awesome" ] } } ``` Then request those, the user does not show any articles; ```gql query me { me { id username image bio articles { id title description } } } ``` Response: ```json { "data": { "me": { "id": "1", "username": "Donald", "image": "", "bio": "", "articles": [] // <- should show the created article here (it is in the database with the correct link to the user) } } } ```
B4nan commented 3 years ago

Be sure to read this: https://github.com/mikro-orm/nestjs/issues/22

Also as I already said, I really can't help much with GQL, so if you want help, please try to narrow it down to something that is not GQL related.

Also worth reading: https://jenyus.web.app/blog/2021-03-07-nestjs-starter

notmedia commented 3 years ago

Also having troubles with getting orm object for get clear identity map on each request.

Like the docs says, we should use middleware:

app.use((req, res, next) => {
  RequestContext.create(orm.em, next);
});

It's not a GQL problem, with Nest we have injected MikroORM Module and I don't understand how I can get orm object outside to use it in middleware. @B4nan can you explain please?

notmedia commented 3 years ago

Hm.. maybe I found the solution

@briandiephuis try this:

import { MikroORM } from '@mikro-orm/core';

import { AppModule } from './app.module';
import { storage } from './storage';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const orm = app.get(MikroORM);

  app.use((req, res, next) => {
    storage.run(orm.em.fork(true, true), next);
  });

 // or without AsyncLocalStorage

 app.use((req, res, next) => {
  RequestContext.create(orm.em, next);
 });
}
briandiephuis commented 3 years ago

Thanks for the tip @notmedia . That seems to work, and I've also found a more NestJS way by formatting your solution as NestJS middleware.

@Injectable()
export class MikroOrmRequestContextMiddleware implements NestMiddleware {
  constructor(private readonly orm: MikroORM) {}

  use(req: AppRequest, res: Response, next: NextFunction): void {
    RequestContext.create(this.orm.em, next);
  }
}

Now both dataloaded relations and more naively loaded relations work. Without pagination, though. That'll be a whole other story.

Getting auto-loaded relations to work seems to be impossible. But like I said, it's basically only useful for quick prototyping so I'm not too sad about that.

I'm swamped with work but I'll try to finish this example app :smile:

notmedia commented 3 years ago

Yeah, this approach is better

rathax commented 3 years ago

has anyone got a per-request Identity Map working when using nestJs + graphql with the fastify adapter?

notmedia commented 3 years ago

@rathax have you tried our approaches we posted above? If there is a bug, could you please post it here?

rathax commented 3 years ago

@notmedia

yes, that's why I have asked if anyone got it working with fastify or if I just have missed something.

notmedia commented 3 years ago

@rathax I can't help you without any information

jamesmeneghello commented 3 years ago

We've got a commercial project that has NestJS set up with MikroORM / GraphQL with extensive support for authn/z, caching, cursor/offset pagination, extremely flexible query conditions, etc... I can't share the code, but I can talk about bits of it if anyone has questions.

e.g.: we don't use a dataloader, instead we break down the GraphQL query to determine which relations need loading and populate that all-at-once, instead - basically a heavily modified version of this: https://github.com/driescroons/graphql-fields-to-relations

B4nan commented 3 years ago

e.g.: we don't use a dataloader, instead we break down the GraphQL query to determine which relations need loading and populate that all-at-once, instead - basically a heavily modified version of this: driescroons/graphql-fields-to-relations

What about contributing back to that library, or releasing your own version? :] Or an article about how you do things. I am quite sure all of that would be quite welcome.

woltob commented 3 years ago

Hi @jamesmeneghello ,

that sounds amazing. Perhaps you can publish a stripped down version of your code and setup (especially the configuration). I, and I guess many others, would be very interested in comparing and discussing solutions. Also, a Medium article with code bits and pieces would be much appreciated.

Such a barebone boilerplate project would be an amazing starting point for others to venture out and build something amazing.

Let us know, thanks!

jamesmeneghello commented 3 years ago

Let me have a think about the best way to disseminate it - it'll probably take a couple of weeks to write up anyway, and it's pretty opinionated, but it has some fairly cool stuff in it.

driescroons commented 3 years ago

@jamesmeneghello Would love to have a talk about this! I'd love to elaborate on the repo, especially when we're getting closer to v5.