Listen to user.deleted events to delete the corresponding user ads

This commit is contained in:
Romain Thouvenin 2024-04-26 12:31:16 +02:00
parent 492bb3ca44
commit 9fb7ef2eac
8 changed files with 175 additions and 5 deletions

View File

@ -20,6 +20,10 @@ export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
'matcher-ad.creation-failed';
export const MATCHER_AD_CREATION_FAILED_QUEUE = 'ad.matcher-ad.creation-failed';
export const USER_DELETED_MESSAGE_HANDLER = 'userDeleted';
export const USER_DELETED_ROUTING_KEY = 'user.deleted';
export const USER_DELETED_QUEUE = 'ad.user.deleted';
// configuration
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';
export const SERVICE_CONFIGURATION_DELETE_QUEUE = 'ad-configuration-delete';

View File

@ -12,6 +12,7 @@ import {
import { AdMapper } from './ad.mapper';
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service';
import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler';
@ -32,6 +33,7 @@ import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ad
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler';
import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler';
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
const grpcControllers = [
CreateAdGrpcController,
@ -44,6 +46,7 @@ const grpcControllers = [
const messageHandlers = [
MatcherAdCreatedMessageHandler,
MatcherAdCreationFailedMessageHandler,
UserDeletedMessageHandler,
];
const eventHandlers: Provider[] = [
@ -54,6 +57,7 @@ const eventHandlers: Provider[] = [
const commandHandlers: Provider[] = [
CreateAdService,
DeleteAdService,
DeleteUserAdsService,
ValidateAdService,
InvalidateAdService,
];

View File

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

View File

@ -0,0 +1,29 @@
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CommandBus,
CommandHandler,
ICommandHandler,
QueryBus,
} from '@nestjs/cqrs';
import { FindAdsByUserIdQuery } from '../../queries/find-ads-by-user-id/find-ads-by-user-id.query';
import { DeleteAdCommand } from '../delete-ad/delete-ad.command';
import { DeleteUserAdsCommand } from './delete-user-ads.command';
@CommandHandler(DeleteUserAdsCommand)
export class DeleteUserAdsService implements ICommandHandler {
constructor(
private readonly queryBus: QueryBus,
private readonly commandBus: CommandBus,
) {}
async execute(command: DeleteUserAdsCommand): Promise<void> {
const ads: AdEntity[] = await this.queryBus.execute(
new FindAdsByUserIdQuery(command.id),
);
await Promise.all(
ads.map((ad) =>
this.commandBus.execute(new DeleteAdCommand({ id: ad.id })),
),
);
}
}

View File

@ -0,0 +1,23 @@
import { IntegrationEvent } from '@mobicoop/ddd-library';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { DeleteUserAdsCommand } from '@modules/ad/core/application/commands/delete-user-ads/delete-user-ads.command';
import { Injectable } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { USER_DELETED_MESSAGE_HANDLER } from '@src/app.constants';
type UserDeletedEvent = IntegrationEvent;
@Injectable()
export class UserDeletedMessageHandler {
constructor(private readonly commandBus: CommandBus) {}
@RabbitSubscribe({
name: USER_DELETED_MESSAGE_HANDLER,
})
public async userDeleted(message: string) {
const deletedUser: UserDeletedEvent = JSON.parse(message);
await this.commandBus.execute(
new DeleteUserAdsCommand({ id: deletedUser.id }),
);
}
}

View File

@ -1,6 +1,11 @@
import { Module, Provider } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
MessageBrokerPublisher,
} from '@mobicoop/message-broker-module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
MATCHER_AD_CREATED_MESSAGE_HANDLER,
@ -10,12 +15,10 @@ import {
MATCHER_AD_CREATION_FAILED_QUEUE,
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
SERVICE_NAME,
USER_DELETED_MESSAGE_HANDLER,
USER_DELETED_QUEUE,
USER_DELETED_ROUTING_KEY,
} from '@src/app.constants';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
MessageBrokerPublisher,
} from '@mobicoop/message-broker-module';
const imports = [
MessageBrokerModule.forRootAsync({
@ -41,6 +44,10 @@ const imports = [
routingKey: MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
queue: MATCHER_AD_CREATION_FAILED_QUEUE,
},
[USER_DELETED_MESSAGE_HANDLER]: {
routingKey: USER_DELETED_ROUTING_KEY,
queue: USER_DELETED_QUEUE,
},
},
}),
}),

View File

@ -0,0 +1,53 @@
import { DeleteUserAdsCommand } from '@modules/ad/core/application/commands/delete-user-ads/delete-user-ads.command';
import { DeleteUserAdsService } from '@modules/ad/core/application/commands/delete-user-ads/delete-user-ads.service';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
import { punctualPassengerCreateAdProps } from './ad.fixtures';
const userAds = [
AdEntity.create(punctualPassengerCreateAdProps()),
AdEntity.create(punctualPassengerCreateAdProps()),
];
const mockQueryBus = {
execute: jest.fn().mockImplementation(() => userAds),
};
const mockCommandBus = {
execute: jest.fn(),
};
describe('delete-user-ads.service', () => {
let deleteUserAdsService: DeleteUserAdsService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: CommandBus,
useValue: mockCommandBus,
},
DeleteUserAdsService,
],
}).compile();
deleteUserAdsService =
module.get<DeleteUserAdsService>(DeleteUserAdsService);
});
it('should be defined', () => {
expect(deleteUserAdsService).toBeDefined();
});
it('should call the delete command for each ad returned by the query', async () => {
await deleteUserAdsService.execute(
new DeleteUserAdsCommand({ id: userAds[0].getProps().userId }),
);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(userAds.length);
});
});

View File

@ -0,0 +1,43 @@
import { UserDeletedMessageHandler } from '@modules/ad/interface/message-handlers/user-deleted.message-handler';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const mockCommandBus = {
execute: jest.fn(),
};
describe('Matcher Ad Created Message Handler', () => {
let userDeletedMessageHandler: UserDeletedMessageHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
UserDeletedMessageHandler,
],
}).compile();
userDeletedMessageHandler = module.get<UserDeletedMessageHandler>(
UserDeletedMessageHandler,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(userDeletedMessageHandler).toBeDefined();
});
it('should call the command bus', async () => {
const userId = '4eb6a6af-ecfd-41c3-9118-473a507014d4';
const userDeletedMessage = `{"id":"${userId}"}`;
await userDeletedMessageHandler.userDeleted(userDeletedMessage);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
expect(mockCommandBus.execute.mock.lastCall[0].id).toBe(userId);
});
});