Búsqueda de sitios web

typescript - NestJs CQRS - configuración de agregados con entidades TypeORM coexistentes


Así que hay un ejemplo oficial aquí

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

y traté de crear el mío para tres características simples:

  • Obteniendo todos los usuarios
  • Obteniendo un usuario por id
  • Creando un usuario

Estoy usando TypeORM y tengo una entidad básica de Usuario. Así que, basándome en el código de ejemplo oficial, creé un manejador de comandos para crear usuarios (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();
  }
}

Todo lo que hace es persistir una nueva entidad de usuario en la base de datos. Pero lo que no entiendo por ahora es qué hacer con el agregado de Usuario. Al fusionar el contexto del objeto no puedo pasar la entidad de usuario creada. Y además no sé qué lógica debería ser manejada por el agregado. Así que basado en este agregado de usuario ( 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));
  }
}

Sé que puedo generar el evento, que se creó un nuevo usuario, y enviarlo al historial. ¿Pero es lo único de lo que se encarga?

Entonces, ¿cómo configuraría correctamente el agregado de usuario y crearía el manejador de usuario?

Al pasar la entidad creada, obtengo un error como

El argumento de tipo 'User' no se puede asignar al parámetro de tipo 'AggregateRoot'. Al tipo 'User' le faltan las siguientes propiedades del tipo 'AggregateRoot': autoCommit, publish, commit, uncommit, y 7 más.ts(2345)

lo cual tiene sentido porque la entidad de TypeORM extiende BaseEntity y el agregado extiende AggregateRoot. Desafortunadamente, el ejemplo oficial no muestra cómo manejar agregados Y entidades de base de datos.

Actualización: Enlace al repositorio temporal eliminado, ya que ya no existe.

Respuesta #1

La respuesta de @cojack asume que la definición de tu entidad TypeORM y tu AggregateRoot de CQRS están definidas por una y la misma clase. Muchos proyectos de NestJS + CQRS se implementan así (aunque no es una mejor práctica y viola el principio de responsabilidad única. Y tus clases de dominio no deberían tener dependencias de tu base de datos).

Como indicas en los comentarios, esto es problemático si tu definición de TypeORM debe extender de BaseEntity, ya que no puedes extender también de AggregateRoot.

Pero no hay ningún requisito para hacerlo. En tu manejador de comandos puedes hacer llamadas al UsersRepository para, por ejemplo, validar si ya existe un usuario con ese nombre y luego crear el usuario.

Luego, en su mergeObjectContext puede instanciar la entidad raíz agregada definida en una clase separada y pasar la información adecuada de la entidad Usuario que acaba de crear.

Un ejemplo de esto se puede encontrar en el proyecto nestjs-rest-cqrs-example:

@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();
  }
}

Este proyecto no hace mucho en términos de eventos en la raíz agregada, pero espero que te hagas una idea.

Hay muchas formas de estructurar el modelo CQRS y lugares donde agregar tu lógica. Para un ejemplo más complejo, pero bien estructurado, véase por ejemplo Daruma Backend de Adrian Lopez.

Respuesta #2

Esta función doSomething que mencionas, es exactamente para lo que has descrito, para emitir un evento. No veo ninguna característica adicional para ella, porque trasladamos toda la lógica de negocio de los "modelos" a los "servicios", así que sí.

Responder al comentario, porque los comentarios tienen discapacidades

En lugar de:

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

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

Hacer

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

entonces en user.model haz:

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

por cierto, si vas a deshacerte del tipo : User en la línea:

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

podría funcionar, porque explícitamente estableciste un tipo aquí, lo cual no es necesario, Typescript puede adivinar el tipo por sí mismo, así que no tienes que hacerlo por tu cuenta, en este tipo de situación.

Artículos relacionados