Cómo montar tu primer agregado, paso a paso

Vamos a construir un Product completo: un value object con reglas, un agregado con una invariante que sólo él puede juzgar, y el endpoint que devuelve 422 cuando algo no cuadra. Todo el código está sacado del repositorio de ejemplo, donde corre en CI.

Paso 1 · Instalar

npm i @nestjslatam/ddd-lib@4.0.0 @nestjs/cqrs

Sin ^. La API se ha movido entre versiones mayores y el compilador no detecta la mayoría de los cambios; clavar la versión es lo que te deja actualizar cuando tú decidas.

Paso 2 · Un value object con reglas

import { StringValueObject, AbstractRuleValidator, BrokenRulesException }
  from \'@nestjslatam/ddd-lib\';

class NameLengthValidator extends AbstractRuleValidator<Name> {
  public addRules(): void {
    const value = this.subject.getValue();

    // OJO: la condición es verdadera cuando la regla está ROTA.
    if (value.length < 3)   this.addBrokenRule(\'value\', \'Mínimo 3 caracteres\');
    if (value.length > 100) this.addBrokenRule(\'value\', \'Máximo 100 caracteres\');
  }
}

export class Name extends StringValueObject {
  static create(value: string): Name {
    const name = new Name(value);

    if (!name.isValid) {                                   // ← imprescindible
      throw new BrokenRulesException(\'Name\', name.brokenRules.getBrokenRules());
    }

    return name;
  }

  override addValidators(): void {
    super.addValidators();                                 // ← imprescindible
    this.validatorRules.add(new NameLengthValidator(this));
  }
}

Las dos líneas marcadas son el 80 % de los fallos que verás al empezar, y ninguna de las dos produce un error si falta:

  • Sin if (!name.isValid) → create() devuelve objetos inválidos, en silencio.
  • Sin super.addValidators() → los validadores de la clase base desaparecen, y \'\' pasa a ser un nombre válido.

Paso 3 · El agregado

export class Product extends DddAggregateRoot<Product, IProductProps> {
  private constructor(props: IProductProps, id?: IdValueObject) {
    super(props, { id });          // el id va en la BOLSA DE OPCIONES
    this.trackingState.markAsNew();
  }

  static create(name: Name, description: Description, price: Price): Product {
    const product = new Product({ name, description, price, status: ProductStatus.ACTIVE });

    if (!product.isValid) {
      throw new BrokenRulesException(\'Product\', product.brokenRules.getBrokenRules());
    }

    product.apply(new ProductCreatedEvent(product.id.getValue()));
    return product;
  }

  override addValidators(): void {
    super.addValidators();
    this.validatorRules.add(new ProductBusinessRulesValidator(this));
  }
}

Tres detalles que cuestan una tarde si los descubres solo: dos argumentos de tipo (<Product, IProductProps>), el id va dentro de { id }, y markAsNew() lo llamas tú — el seguimiento de estado no adivina.

Paso 4 · Una regla que sólo el agregado puede ver

export class ProductBusinessRulesValidator extends AbstractRuleValidator<Product> {
  public addRules(): void {
    const { name, description } = this.subject.props;

    if (description.getValue().length <= name.getValue().length) {
      this.addBrokenRule(\'props.description\', \'La descripción debe ser más larga que el nombre\');
    }
  }
}

Aquí es donde el patrón empieza a pagar. name es válido. description es válida. La combinación no lo es, y ningún value object puede saberlo porque ninguno ve al otro. La raíz de agregado es el único sitio del sistema desde donde esa frase se puede escribir.

Paso 5 · El handler

@CommandHandler(CreateProductCommand)
export class CreateProductCommandHandler implements ICommandHandler<CreateProductCommand, string> {
  constructor(
    private readonly publisher: EventPublisher,
    private readonly repository: ProductRepository,
  ) {}

  async execute(command: CreateProductCommand): Promise<string> {
    const product = Product.create(
      Name.create(command.name),
      Description.create(command.description),
      Price.create(command.price),
    );

    await this.repository.save(product);                  // 1. persistir
    this.publisher.mergeObjectContext(product).commit();  // 2. despachar

    return product.id.getValue();
  }
}

El orden importa: publicar antes de guardar abre una ventana en la que un manejador lee un producto que todavía no existe. Y sin .commit() el comando triunfa, devuelve su respuesta, y todos tus @EventsHandler se saltan en silencio.

Paso 6 · Comprobar que funciona

npx ddd validate

Audita las cuatro reglas del idioma contra tu código y tu versión instalada. Es un comando determinista, sin modelo, y se puede poner en CI tal cual.

Para seguir

La versión larga, con el ciclo de vida de un pedido y las entidades hijas, está en Tu primer agregado. Y la guía del CLI construye un dominio entero comando a comando.

Leave a Comment