integration events

This commit is contained in:
sbriat
2023-07-24 15:06:15 +02:00
parent 3ac7460c83
commit e8c40a6386
44 changed files with 1053 additions and 2496 deletions

View File

@@ -2,7 +2,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
import { UserCreatedDomainEvent } from '../../domain/events/user-created.domain-events';
import { UserCreatedDomainEvent } from '../../domain/events/user-created.domain-event';
import { UserCreatedIntegrationEvent } from '../events/user-created.integration-event';
import { USER_CREATED_ROUTING_KEY } from '@modules/user/user.constants';
@Injectable()
export class PublishMessageWhenUserIsCreatedDomainEventHandler {
@@ -13,6 +15,17 @@ export class PublishMessageWhenUserIsCreatedDomainEventHandler {
@OnEvent(UserCreatedDomainEvent.name, { async: true, promisify: true })
async handle(event: UserCreatedDomainEvent): Promise<any> {
this.messagePublisher.publish('user.created', JSON.stringify(event));
const userCreatedIntegrationEvent = new UserCreatedIntegrationEvent({
id: event.aggregateId,
firstName: event.firstName,
lastName: event.lastName,
email: event.email,
phone: event.phone,
metadata: event.metadata,
});
this.messagePublisher.publish(
USER_CREATED_ROUTING_KEY,
JSON.stringify(userCreatedIntegrationEvent),
);
}
}

View File

@@ -3,6 +3,8 @@ import { OnEvent } from '@nestjs/event-emitter';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
import { UserDeletedDomainEvent } from '../../domain/events/user-deleted.domain-event';
import { UserDeletedIntegrationEvent } from '../events/user-deleted.integration-event';
import { USER_DELETED_ROUTING_KEY } from '@modules/user/user.constants';
@Injectable()
export class PublishMessageWhenUserIsDeletedDomainEventHandler {
@@ -13,6 +15,13 @@ export class PublishMessageWhenUserIsDeletedDomainEventHandler {
@OnEvent(UserDeletedDomainEvent.name, { async: true, promisify: true })
async handle(event: UserDeletedDomainEvent): Promise<any> {
this.messagePublisher.publish('user.deleted', JSON.stringify(event));
const userDeletedIntegrationEvent = new UserDeletedIntegrationEvent({
id: event.aggregateId,
metadata: event.metadata,
});
this.messagePublisher.publish(
USER_DELETED_ROUTING_KEY,
JSON.stringify(userDeletedIntegrationEvent),
);
}
}

View File

@@ -2,7 +2,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
import { UserUpdatedDomainEvent } from '../../domain/events/user-updated.domain-events';
import { UserUpdatedDomainEvent } from '../../domain/events/user-updated.domain-event';
import { UserUpdatedIntegrationEvent } from '../events/user-updated.integration-event';
import { USER_UPDATED_ROUTING_KEY } from '@modules/user/user.constants';
@Injectable()
export class PublishMessageWhenUserIsUpdatedDomainEventHandler {
@@ -13,6 +15,17 @@ export class PublishMessageWhenUserIsUpdatedDomainEventHandler {
@OnEvent(UserUpdatedDomainEvent.name, { async: true, promisify: true })
async handle(event: UserUpdatedDomainEvent): Promise<any> {
this.messagePublisher.publish('user.updated', JSON.stringify(event));
const userUpdatedIntegrationEvent = new UserUpdatedIntegrationEvent({
id: event.aggregateId,
firstName: event.firstName,
lastName: event.lastName,
email: event.email,
phone: event.phone,
metadata: event.metadata,
});
this.messagePublisher.publish(
USER_UPDATED_ROUTING_KEY,
JSON.stringify(userUpdatedIntegrationEvent),
);
}
}

View File

@@ -0,0 +1,16 @@
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
export class UserCreatedIntegrationEvent extends IntegrationEvent {
readonly firstName: string;
readonly lastName: string;
readonly email: string;
readonly phone: string;
constructor(props: IntegrationEventProps<UserCreatedIntegrationEvent>) {
super(props);
this.firstName = props.firstName;
this.lastName = props.lastName;
this.email = props.email;
this.phone = props.phone;
}
}

View File

@@ -0,0 +1,7 @@
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
export class UserDeletedIntegrationEvent extends IntegrationEvent {
constructor(props: IntegrationEventProps<UserDeletedIntegrationEvent>) {
super(props);
}
}

View File

@@ -0,0 +1,16 @@
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
export class UserUpdatedIntegrationEvent extends IntegrationEvent {
readonly firstName: string;
readonly lastName: string;
readonly email: string;
readonly phone: string;
constructor(props: IntegrationEventProps<UserUpdatedIntegrationEvent>) {
super(props);
this.firstName = props.firstName;
this.lastName = props.lastName;
this.email = props.email;
this.phone = props.phone;
}
}

View File

@@ -1,8 +1,8 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import { CreateUserProps, UpdateUserProps, UserProps } from './user.types';
import { UserCreatedDomainEvent } from './events/user-created.domain-events';
import { UserUpdatedDomainEvent } from './events/user-updated.domain-events';
import { UserCreatedDomainEvent } from './events/user-created.domain-event';
import { UserUpdatedDomainEvent } from './events/user-updated.domain-event';
import { UserDeletedDomainEvent } from './events/user-deleted.domain-event';
export class UserEntity extends AggregateRoot<UserProps> {

View File

@@ -11,22 +11,27 @@ import { UserRepositoryPort } from '../core/application/ports/user.repository.po
import { UserMapper } from '../user.mapper';
import { USER_MESSAGE_PUBLISHER } from '../user.di-tokens';
export type UserModel = {
export type UserBaseModel = {
uuid: string;
firstName: string;
lastName: string;
email: string;
phone: string;
createdAt: Date;
updatedAt: Date;
};
export type UserReadModel = UserBaseModel & {
createdAt?: Date;
updatedAt?: Date;
};
export type UserWriteModel = UserBaseModel;
/**
* Repository is used for retrieving/saving domain entities
* */
@Injectable()
export class UserRepository
extends PrismaRepositoryBase<UserEntity, UserModel, UserModel>
extends PrismaRepositoryBase<UserEntity, UserReadModel, UserWriteModel>
implements UserRepositoryPort
{
constructor(

View File

@@ -25,11 +25,12 @@ import {
export class UpdateUserGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('UserService', 'UpdateUser')
@GrpcMethod('UserService', 'Update')
async updateUser(data: UpdateUserRequestDto): Promise<IdResponse> {
try {
const aggregateID: AggregateID = await this.commandBus.execute(
new UpdateUserCommand({
id: data.id,
firstName: data.firstName,
lastName: data.lastName,
email: data.email,

View File

@@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PublishMessageWhenUserIsCreatedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler';
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-events';
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-event';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
@@ -48,7 +48,7 @@ describe('Publish message when user is created domain event handler', () => {
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
'user.created',
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","firstName":"John","lastName":"Doe","email":"john.doe@email.com","phone":"+33611223344","metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"firstName":"John","lastName":"Doe","email":"john.doe@email.com","phone":"+33611223344"}',
);
});
});

View File

@@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-events';
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-event';
import { PublishMessageWhenUserIsDeletedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler';
const mockMessagePublisher = {
@@ -48,7 +48,7 @@ describe('Publish message when user is deleted domain event handler', () => {
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
'user.deleted',
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","firstName":"John","lastName":"Doe","email":"john.doe@email.com","phone":"+33611223344","metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000}}',
);
});
});

View File

@@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler';
import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-events';
import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-event';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
@@ -48,7 +48,7 @@ describe('Publish message when user is updated domain event handler', () => {
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
'user.updated',
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","firstName":"Jane","lastName":"Doe","email":"jane.doe@email.com","phone":"+33611223344","metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"firstName":"Jane","lastName":"Doe","email":"jane.doe@email.com","phone":"+33611223344"}',
);
});
});

View File

@@ -1,6 +1,6 @@
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-events';
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-event';
import { UserDeletedDomainEvent } from '@modules/user/core/domain/events/user-deleted.domain-event';
import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-events';
import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-event';
import { UserEntity } from '@modules/user/core/domain/user.entity';
import {
CreateUserProps,

View File

@@ -1,5 +1,8 @@
import { UserEntity } from '@modules/user/core/domain/user.entity';
import { UserModel } from '@modules/user/infrastructure/user.repository';
import {
UserReadModel,
UserWriteModel,
} from '@modules/user/infrastructure/user.repository';
import { UserResponseDto } from '@modules/user/interface/dtos/user.response.dto';
import { UserMapper } from '@modules/user/user.mapper';
import { Test } from '@nestjs/testing';
@@ -16,7 +19,7 @@ const userEntity: UserEntity = new UserEntity({
createdAt: now,
updatedAt: now,
});
const userModel: UserModel = {
const userReadModel: UserReadModel = {
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
firstName: 'John',
lastName: 'Doe',
@@ -41,12 +44,12 @@ describe('User Mapper', () => {
});
it('should map domain entity to persistence data', async () => {
const mapped: UserModel = userMapper.toPersistence(userEntity);
const mapped: UserWriteModel = userMapper.toPersistence(userEntity);
expect(mapped.lastName).toBe('Doe');
});
it('should map persisted data to domain entity', async () => {
const mapped: UserEntity = userMapper.toDomain(userModel);
const mapped: UserEntity = userMapper.toDomain(userReadModel);
expect(mapped.getProps().firstName).toBe('John');
});

View File

@@ -0,0 +1,3 @@
export const USER_CREATED_ROUTING_KEY = 'user.created';
export const USER_UPDATED_ROUTING_KEY = 'user.updated';
export const USER_DELETED_ROUTING_KEY = 'user.deleted';

View File

@@ -1,7 +1,10 @@
import { Mapper } from '@mobicoop/ddd-library';
import { Injectable } from '@nestjs/common';
import { UserEntity } from './core/domain/user.entity';
import { UserModel } from './infrastructure/user.repository';
import {
UserReadModel,
UserWriteModel,
} from './infrastructure/user.repository';
import { UserResponseDto } from './interface/dtos/user.response.dto';
/**
@@ -13,23 +16,21 @@ import { UserResponseDto } from './interface/dtos/user.response.dto';
@Injectable()
export class UserMapper
implements Mapper<UserEntity, UserModel, UserModel, UserResponseDto>
implements Mapper<UserEntity, UserReadModel, UserWriteModel, UserResponseDto>
{
toPersistence = (entity: UserEntity): UserModel => {
toPersistence = (entity: UserEntity): UserWriteModel => {
const copy = entity.getProps();
const record: UserModel = {
const record: UserWriteModel = {
uuid: copy.id,
firstName: copy.firstName,
lastName: copy.lastName,
email: copy.email,
phone: copy.phone,
createdAt: copy.createdAt,
updatedAt: copy.updatedAt,
};
return record;
};
toDomain = (record: UserModel): UserEntity => {
toDomain = (record: UserReadModel): UserEntity => {
const entity = new UserEntity({
id: record.uuid,
createdAt: new Date(record.createdAt),
@@ -53,12 +54,4 @@ export class UserMapper
response.phone = props.phone;
return response;
};
/* ^ Data returned to the user is whitelisted to avoid leaks.
If a new property is added, like password or a
credit card number, it won't be returned
unless you specifically allow this.
(avoid blacklisting, which will return everything
but blacklisted items, which can lead to a data leak).
*/
}

View File

@@ -13,6 +13,13 @@ import { USER_MESSAGE_PUBLISHER, USER_REPOSITORY } from './user.di-tokens';
import { UserRepository } from './infrastructure/user.repository';
import { UserMapper } from './user.mapper';
import { PrismaService } from './infrastructure/prisma.service';
import { DeleteUserGrpcController } from './interface/grpc-controllers/delete-user.grpc.controller';
import { FindUserByIdGrpcController } from './interface/grpc-controllers/find-user-by-id.grpc.controller';
import { DeleteUserService } from './core/application/commands/delete-user/delete-user.service';
import { FindUserByIdQueryHandler } from './core/application/queries/find-user-by-id/find-user-by-id.query-handler';
import { PublishMessageWhenUserIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler';
import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler';
import { PublishMessageWhenUserIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler';
const imports = [
CqrsModule,
@@ -30,9 +37,26 @@ const imports = [
}),
];
const grpcControllers = [CreateUserGrpcController, UpdateUserGrpcController];
const grpcControllers = [
CreateUserGrpcController,
UpdateUserGrpcController,
DeleteUserGrpcController,
FindUserByIdGrpcController,
];
const commandHandlers: Provider[] = [CreateUserService, UpdateUserService];
const eventHandlers: Provider[] = [
PublishMessageWhenUserIsCreatedDomainEventHandler,
PublishMessageWhenUserIsUpdatedDomainEventHandler,
PublishMessageWhenUserIsDeletedDomainEventHandler,
];
const commandHandlers: Provider[] = [
CreateUserService,
UpdateUserService,
DeleteUserService,
];
const queryHandlers: Provider[] = [FindUserByIdQueryHandler];
const mappers: Provider[] = [UserMapper];
@@ -56,7 +80,9 @@ const orms: Provider[] = [PrismaService];
imports,
controllers: [...grpcControllers],
providers: [
...eventHandlers,
...commandHandlers,
...queryHandlers,
...mappers,
...repositories,
...messagePublishers,