mirror of
https://gitlab.com/mobicoop/v3/service/user.git
synced 2026-01-11 22:02:39 +00:00
integration events
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserDeletedIntegrationEvent extends IntegrationEvent {
|
||||
constructor(props: IntegrationEventProps<UserDeletedIntegrationEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
3
src/modules/user/user.constants.ts
Normal file
3
src/modules/user/user.constants.ts
Normal 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';
|
||||
@@ -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).
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user