Search code examples
mikro-orm

How to properly use MirkoORM custom repository with NestJS?


When registering entities through MirkoORM.forFeature() method it automatically registers the repositories if I have the entity properly set up.

This is an example entity I have

@Entity({
    tableName: 'refresh_token',
    customRepository: () => RefreshTokenRepository
})
export default class RefreshToken extends BaseEntity<RefreshToken, 'id'> {
    [EntityRepositoryType]?: RefreshTokenRepository

    @PrimaryKey({
        type: 'uuid',
        nullable: false,
        autoincrement: false,
        unique: true,
        primary: true
    })
    id!: string
}

In my application service I use the following:

@Injectable()
export default class AuthenticationService {
    private logger = new Logger(AuthenticationService.name)

    constructor(
        private readonly orm: MikroORM,
        private readonly jwtService: JwtService,
        private readonly passwordVerifier: PasswordVerifier,
        private readonly userRepository: UserRepository,
        private readonly refreshTokenRepository: RefreshTokenRepository
    ) {}

    @CreateRequestContext()
    private async passwordGrantType(
        payload: IssueTokenPayload
    ): Promise<Either<AccessDeniedError | UnexpectedError, TokenResponse>> {
        const em = this.orm.em
    }

Using this.orm.em will give me a new instance indeed but then there's the question: if I inject the private readonly refreshTokenRepository: RefreshTokenRepository will this give me a fresh instance of the EM inside the repository?

Is there a difference using const em = this.orm.em or const em = this.refreshTokenRepository.getEntityManager()?

If I need to inject orm: MirkoORM to get a fresh instance then what is the point of injecting this repository?

EDIT 1

I have added this to my main.ts

;(async function () {
const app = await NestFactory.create(AppModule, { bufferLogs: true })
const orm = app.get(MikroORM)

const loggerService = app.get(AppBindings.Logger)

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

Using this with my repository does not work. I still get populated collections used in previous methods.

Example: when user is accessing a protected endpoint profile I will check if user has required roles and scopes. (during each request I will attach scopes to the role = this will be refactored later) For this I will query the roleRepository inside my JWT strategy and populate permissions for it.

async permissionsOfRole(roleId: string): Promise<Permission[]> {

        const role = await this.findOne({ id: roleId }, { populate: ['permissions'] })

        if (!role) return null

        return role.permissions.getItems()
}

After the JWT strategy is run, I will get the user from repository with roles loaded eagerly. The roles collection entities have permissions collection populated already although they shouldn't be. What am I doing wrong?

User entity:

export default class User extends AggregateRoot < User, 'id' > {
    [EntityRepositoryType] ? : UserRepository

    // More code here

    @ManyToMany({
        eager: true,
        entity: () => Role,
        pivotEntity: () => UserRole,
        joinColumn: 'user_id',
        inverseJoinColumn: 'role_id'
    })
    roles ? = new Collection < Role > (this)
}

Role entity

export default class Role extends AggregateRoot < Role, 'id' > {
    [EntityRepositoryType] ? : RoleRepository

    @Property({
        type: 'character',
        length: 50,
        nullable: false
    })
    name!: string
    @ManyToMany({
        eager: false,
        entity: () => Permission,
        pivotEntity: () => RolePermission,
        joinColumn: 'role_id',
        inverseJoinColumn: 'permission_id'
    })
    permissions = new Collection < Permission > (this)
}

Solution

  • The decorator will create a new context and it will be respected with the repositories too. You work with global instances, but they respect the context automatically.

    https://mikro-orm.io/docs/identity-map#how-does-requestcontext-helper-work

    Note that you should prefer creating the contexts via middleware which is registered automatically by the @mikro-orm/nestjs package (the forRoot does it). The decorator way is only for very specific use cases (e.g. cron jobs), and you should generally not use it on a service method, I'd call what you did an antipattern in the first place (but it depends on your app setup, hard to guess).