#CQRS and mongoose

7 messages · Page 1 of 1 (latest)

ancient radish
#

Hi, I'm trying to learn the concept of CQRS and DDD. so I started a personal project. As of now I have only one object called appointment that is affected by typicel CRUD operations.

Since nestjs documentation is to say the less minimal. I looked for some implementations of CQRS using mongoose such as https://github.dev/kyhsa93/nestjs-rest-cqrs-example and https://github.dev/mguay22/nestjs-ddd.

I noticed both of this have something in common and is that they use an object factory. In the case of the second link, it uses a factory on top of the already existing schema, which is something I dont really understand why is neccesary, to me it just seems like a verbose and unnecessary layer since it doesnt even act as a middleware.

So I tried to implement it without said extra layer, making the schema itself extend from AggregateRoot. But now I get an error when doing a create operation.

TypeError: appointment.commit is not a function

For instance the object is correctly created in the db, it just fails when trying to commit it in the publisher

Is there something I'm missing here?
do I really have to create a factory to act as a another schema but extending from AggregateRoot?

#

this is the Schema, as you see I extended it to AggregateRoot

@Schema({
  timestamps: true,
})
export class Appointment extends AggregateRoot{
  @Prop({ type: AppointmentInformation })
  info: AppointmentInformation;

  @Prop()
  start: string;

  @Prop()
  end: string;
}

export const AppointmentSchema = SchemaFactory.createForClass(Appointment);

export const AppointmentFeature = MongooseModule.forFeature([
  { name: Appointment.name, schema: AppointmentSchema },
])

This is my controller

  @Post()
  async createAppointment(@Body() payload: CreateAppointmentDto): Promise<void> {
    await this.commandBus.execute<CreateAppointmentCommand, Appointment>(
      new CreateAppointmentCommand(payload)
    );
  }

my command hancler:

@CommandHandler(CreateAppointmentCommand)
export class CreateAppointmentHandler implements ICommandHandler<CreateAppointmentCommand> {
  constructor(
    private readonly repository: AppointmentRepository,
    private readonly publisher: EventPublisher,
  ) {}

  async execute(command: CreateAppointmentCommand): Promise<Appointment> {
    const { payload } = command;

    const appointment = this.publisher.mergeObjectContext(
      //TODO: set mapper to entity class instead of using as
      await this.repository.createAppointment(payload as Appointment)
    );
    appointment.commit()
    return appointment;
  }
}

my repo, which is only in charge of comunincating with mongoose, typical create, update etc.

export class AppointmentRepository extends BaseEntityRepository<Appointment> {
  constructor(
    @InjectModel(Appointment.name) private readonly appointmentModel: Model<Appointment>,
  ) {
    super(appointmentModel);
  }

  createAppointment(appointment: Appointment) {
    return this.create(appointment);
  }
}
#

and finally the baseRepo, which is just ageneric with mongoose methods

export abstract class BaseEntityRepository<T extends AggregateRoot> {
  protected constructor(
    protected readonly entityModel: Model<T>
  ) {}

  protected create(entity: T): Promise<T> {
    return  this.entityModel.create<T>(entity);
  }
}
weak gorge
#

Unfortunately, it's impossible to say what is wrong here, without a working reproduction repo.

I'd also put forward that those two repos you linked to are going to lead you down a deep and ugly rabbit hole. I'd highly suggest not to wrap your code around DDD at all. That is my personal opinion. But, the main reason is, you'll be fighting with the tools Nest give you when you do. Keep DDD in your mind, when working with your application, don't make it concrete in your code. Does that make sense?

I wrote an article about this (sort of). https://dev.to/smolinari/nestjs-and-project-structure-what-to-do-1223

If you keep within the constructs of what I show, you can also do CQRS (and keep DDD in your mind). CQRS is just a layer of abstraction, which changes the communication between your features/ modules (aggregates) from dependency injection to an event bus and changes a couple of other aspects, like splitting up read and command entities.

On a side note, if you go with GraphQL, this split is done for you practically with queries and mutations or rather, the incoming requests are split up into queries and commands. And, you don't need to give up dependency injection either. But, that is also my opinion. 🙂

Ok, back to the subject of CQRS. So, with CQRS and as I noted, instead of using dependency injection, you are basically building out microservices in your monolith ( setting up your code a step closer to being able to move your service out of the monolith into true microservices). In other words, your aggregates communicate between each other via a bus system and not via dependency injection. It'a a major layer of abstraction added to a regular Nest app, but allows for (even better) scalability of your app. And it is the main reason why you would ever need it. 🙂

ancient radish
#

Hey, thanks for the response. I just read your article and find it interesting. I don't think I'm too far from the structure you create in the blog, I'm just doing it in a CQRS way I guess, replacing action services such as logIn, logOut and register into its own command/command handlers.

So I guess my real issue (and I don't know if it may be a bug related to @tulip sparrow/mongoose package) is the retrieved object not having the methods from its extended class.

I've uploaded a minimal repo where you can reproduce my issue, unfortunately I don't know if there's any online tool similar to stack-blitz where you can test it without cloning and installing it. https://github.com/Gharsnull/appointments-test.

if by any chance you have the time you can just use postman or whatever and test the endpoint, if it helps I'll attatch the postman collection already set up.

weak gorge
#

So, Mongoose Schema/ Entity classes shouldn't have methods.

#

Your appointment "model" should be, in the end, a service, not your entity or rather not mixed with your entity.