7
votes

So there is an official example here

https://github.com/kamilmysliwiec/nest-cqrs-example

and I tried to create my own for three simple features:

  • Fetching all users
  • Fetching one user by id
  • Creating a user

I'm using TypeORM and have a basic User entity. So based on the official sample code I created a command handler for creating users ( create.user.handler.ts ):

@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
  constructor(
    private readonly usersRepository: UsersRepository,
    private readonly eventPublisher: EventPublisher,
  ) {}

  public async execute(command: CreateUserCommand): Promise<void> {
    const createdUser: User = await this.usersRepository.createUser(
      command.username,
      command.password,
    );

    // const userAggregate: UserAggregate = this.eventPublisher.mergeObjectContext(
    //   createdUser,
    // );
    // userAggregate.doSomething(createdUser);
    // userAggregate.commit();
  }
}

All it does is persisting a new user entity to the database. But what I didn't get for now is what to do with the User aggregate. When merging the object context I can't pass in the created user entity. And further I don't know which logic should be handled by the aggregate. So based on this user aggregate ( user.model.ts ):

export class User extends AggregateRoot {
  constructor(private readonly id: string) {
    super();
  }

  public doSomething(user: UserEntity): void {
    // do something here?

    this.apply(new UserCreatedEvent(user));
  }
}

I know that I can raise the event, that a new user was created and push it to the history. But is it the only thing it's responsible for?

So how would I setup the user aggregate and create user handler correctly?

When passing in the created entity I get an error like

Argument of type 'User' is not assignable to parameter of type 'AggregateRoot'. Type 'User' is missing the following properties from type 'AggregateRoot': autoCommit, publish, commit, uncommit, and 7 more.ts(2345)

which makes sense because the TypeORM entity extends BaseEntity and the aggregate extends AggregateRoot. Unforunately the official example doesn't show how to deal with aggregates AND database entities.

Update: Deleted link to temp repository, as it no longer exists.

3

3 Answers

5
votes

The answer by @cojack assumes that your TypeORM entity definition and CQRS AggregateRoot are defined by one and the same class. Many NestJS + CQRS projects are implemented like that (Though it is not best-practice and violates the single-responsibility principle. And your domain classes should not have dependencies on your database).

As you state in the comments this is problematic if your TypeORM definition must extend from BaseEntity, as you cannot also extend from AggregateRoot.

But there is no requirement to do so. In your command handler you can make calls to the UsersRepository to e.g. validate if a user with that name already exists, and then to create the user.

Then in your mergeObjectContext you can instantiate the aggregate root entity defined in a separate class and pass appropriate information from the User entity you just created.

An example of this can be found in the nestjs-rest-cqrs-example project:

@CommandHandler(CreateAccountCommand)
export class CreateAccountCommandHandler implements ICommandHandler<CreateAccountCommand> {
  constructor(
    @InjectRepository(AccountEntity) private readonly repository: AccountRepository,
    private readonly publisher: EventPublisher,
  ) {}

  async execute(command: CreateAccountCommand): Promise<void> {
    // Validation of the command (check if account with this email already exists).
    await this.repository.findOne({ where: [{ email: command.email }]}).then((item) => {
      if (item) throw new HttpException('Conflict', HttpStatus.CONFLICT);
    });

    const { name, email, password } = command;
    // Create the account via the repository. The TypeORM entity is returned.
    const result = await this.repository.save(new CreateAccountMapper(name, email, bcrypt.hashSync(password)));

    const account: Account = this.publisher.mergeObjectContext(
      // Create the AggregateRoot entity to be passed to the context.
      new Account(result.id, name, email, result.password, result.active),
    );
    account.commit();
  }
}

This project does not do much in terms of events in the aggregate root, but you get the idea, I hope.

There are many ways to lay out the CQRS model and places to add your logic. For a more complex, but well laid-out example see e.g. Daruma Backend by Adrian Lopez.

4
votes

This doSomething function which you mention, it's exactly for what you have described, to emit an event. I doesn't see any additional features for it, because we moved all of the business logic from "models" to "services", so yeah.

Reply to comment,because comments have disabilities

Instead of:

    const createdUser: User = await this.usersRepository.createUser(
      command.username,
      command.password,
    );

    const userAggregate: UserAggregate = this.eventPublisher.mergeObjectContext(
      createdUser,
    );
    userAggregate.raiseUserCreatedEvent(createdUser);

Do

    const userAggregate: UserAggregate = this.eventPublisher.mergeObjectContext(
      await this.usersRepository.createUser(
        command.username,
        command.password,
      )
    );
    userAggregate.raiseUserCreatedEvent();

then in user.model do:

public raiseUserCreatedEvent(): void {
    this.apply(new UserCreatedEvent(this));
  }

btw, if you will get rid of type : User in line:

const createdUser: User = await this.usersRepository.createUser(

it might work, because you explisity set a type here, which is not necessary, typescript can guess type by his own, so you don't have to do it by your own, in this kind of situation.

0
votes

mergeObjectContext method should receive AggregateRoot instance as an argument

const userAggregate = this.eventPublisher.mergeObjectContext(new User(createdUser.id);