Skip to content

Alternative NestJS Approach #44

@parisholley

Description

@parisholley

In moving to a persist/flush model system like Mikro, I find it important to prevent developers on my team from making mistakes like using some global entity manager with a persisted identity map across requests. At the moment, it is very easy to inject Mikro into any service, controller, etc without realizing the repercussions. At a high level, these are my requirements:

  • No one should be able access a singleton instance of EntityManager except for the Mikro module itself.
  • If there is ever a need to access the application-level singleton, it should be done through some obvious abstraction like orm.getGlobalEntityManager()
  • No magic should be involved with request scopes such as node.js domains (deprecated) or pulling things from global statics. Given that a service does not know if it needs to be ran in a transaction or not, guidance/best practice should be to always pass an instance of EntityManager to your downstream services via function arguments
  • The Mikro NestJS module should provide interceptors and decorators for making request scoped work easier (i'm using this for both REST and GraphQL endpoints), example:

EntityManagerInterceptor.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

export default class EntityManagerInterceptor implements NestInterceptor {
  constructor(private em: EntityManager) {}

  async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
    context['em'] = this.em.fork();

    return next.handle().pipe(
      mergeMap(async (output) => {
        await context['em'].flush();

        return output;
      })
    );
  }
}

RequestEntityManager.ts

export default createParamDecorator((data: unknown, context: ExecutionContext) => {
  if (!context['em']) {
    throw new Error('Could not find a request-scoped EntityManager');
  }

  return context['em'];
});
// graphql
@Mutation()
myMutation(@RequestEntityManager() em: EntityManager){}

// rest
@Get()
async myEndpoint(@RequestEntityManager() em: EntityManager){}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions