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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With