Design Patterns - Repository Pattern in TypeScript using MikroORM

Before you look at the Repository pattern example in TypeScript using MikroORM, you can see my post about what is the Repository pattern.

Example of the generic repository interface in TypeScript

interface Repository<T extends AggregateRoot> {
  getAll(): Promise<T[]>;

  getOne(id: UniqueId): Promise<T>;

  add(t: T): void;

  update(t: T): void;
}

About this code snippet:

  • The generic T extends from AggregateRoot, so it forces the generic repository to work with Aggregate Roots instead of Entities.
  • The method getOne has a Value Object as parameter.

Example of the specialized repository abstract class in TypeScript

abstract class UserRepository implements Repository<User> {
  abstract getAll(): Promise<User[]>;

  abstract getOne(id: UniqueId): Promise<User>;

  abstract add(user: User): void;

  abstract update(user: User): void;

  abstract delete(id: UniqueId): void;
}

About this code snippet:

  • In this example the Aggregate Root User can be deleted, so the specialized repository has a method for that action, otherwise it would not have them.

Example of the implementation of the specialized repository in TypeScript

For this example I am using MikroORM:

MikroORM

MikroORM is an ORM for JavaScript and TypeScript that allows handling transactions automatically, it will be useful to group the repository actions in a transaction and then apply it on the database using the Unit of Work pattern.

But before implementing the repository pattern, we must create the MikroORM entity, this is an Infrastructure layer entity, so it is different from the Domain layer entity and its only function is to be a schema for the persistence of the data in the database.

import {
  Entity,
  EntityRepositoryType,
  PrimaryKey,
  Property,
  SerializedPrimaryKey,
  Unique,
} from '@mikro-orm/core';
import { ObjectId } from '@mikro-orm/mongodb';
import { UserRepositoryMongoDb } from '../repositories';

@Entity({ collection: 'users' })
export class UserEntity {
  [EntityRepositoryType]?: UserRepositoryMongoDb;

  @PrimaryKey()
  _id: ObjectId;

  @SerializedPrimaryKey()
  id!: string;

  @Unique()
  @Property()
  email: string;
}

In a MikroORM entity you need to explicitly declare the repository name, in this example is UserRepositoryMongoDb.

Now that MikroORM knows with which entity it is going to work, we can write the repository implementation:

import {
  EntityData,
  EntityRepository,
  MikroORM,
  Repository,
} from '@mikro-orm/core';
import { UserEntity } from '../entities';

@Repository(UserEntity)
export class UserRepositoryMongoDb
  extends EntityRepository<UserEntity>
  implements UserRepository
{
  constructor(private readonly orm: MikroORM) {
    super(orm.em, UserEntity);
  }

  async getOne(id: UniqueId): Promise<User> {
    const userEntity = await this.findOne(id);
    if (!userEntity) return null;
    const user = userEntityToUser(userEntity);
    return user;
  }

  async getAll(): Promise<User[]> {
    const usersEntities = await this.findAll();
    const users = usersEntities.map((u) => userEntityToUser(u));
    return users;
  }

  add(user: User): void {
    const newValues: EntityData<UserEntity> = {
      id: user.id,
      email: user.email,
    };
    const newUserEntity = this.create(newValues);
    this.orm.em.persist(newUserEntity);
  }

  update(user: User): void {
    const userEntityFromDb = this.getReference(user.id);
    const newValues: EntityData<UserEntity> = {
      email: user.email,
    };
    this.orm.em.assign(userEntityFromDb, newValues);
  }

  delete(id: UniqueId): void {
    const userEntity = this.getReference(id);
    this.remove(userEntity);
  }
}

About this code snippet:

  • MikroORM is injected into the constructor using dependency injection.
  • Since the repository pattern must return the Domain layer user, the data obtained from the database must first be mapped, for which in this example the userEntityToUser function is being used.
  • The MikroORM getReference function allows to obtain a reference of the entity, so you can work with the entity without first consulting it in the database.