From 55ec5970daff9ef51f59e23eca17b312d392542a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 10 Jul 2023 13:38:07 +0200 Subject: [PATCH] feat: message operations use cases. (#652) Co-authored-by: Mo --- .../Message/MessageRepositoryInterface.ts | 2 + .../DeleteAllMessagesSentToUser.spec.ts | 60 +++++++++++++++ .../DeleteAllMessagesSentToUser.ts | 30 ++++++++ .../DeleteAllMessagesSentToUserDTO.ts | 3 + .../DeleteMessage/DeleteMessage.spec.ts | 77 +++++++++++++++++++ .../Messaging/DeleteMessage/DeleteMessage.ts | 38 +++++++++ .../DeleteMessage/DeleteMessageDTO.ts | 4 + .../GetMessagesSentByUser.spec.ts | 32 ++++++++ .../GetMessagesSentByUser.ts | 21 +++++ .../GetMessagesSentByUserDTO.ts | 3 + .../GetMessagesSentToUser.spec.ts | 32 ++++++++ .../GetMessagesSentToUser.ts | 21 +++++ .../GetMessagesSentToUserDTO.ts | 3 + .../Infra/TypeORM/TypeORMMessageRepository.ts | 22 ++++++ 14 files changed, 348 insertions(+) create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUserDTO.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessageDTO.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUserDTO.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts diff --git a/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts b/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts index b4812f4a4..dce22a9ea 100644 --- a/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts @@ -4,6 +4,8 @@ import { Message } from './Message' export interface MessageRepositoryInterface { findByUuid: (uuid: Uuid) => Promise + findByRecipientUuid: (uuid: Uuid) => Promise + findBySenderUuid: (uuid: Uuid) => Promise findByRecipientUuidAndReplaceabilityIdentifier: (dto: { recipientUuid: Uuid replaceabilityIdentifier: string diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.spec.ts new file mode 100644 index 000000000..011b7d291 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.spec.ts @@ -0,0 +1,60 @@ +import { Result, Timestamps, Uuid } from '@standardnotes/domain-core' +import { Message } from '../../../Message/Message' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { DeleteMessage } from '../DeleteMessage/DeleteMessage' +import { DeleteAllMessagesSentToUser } from './DeleteAllMessagesSentToUser' + +describe('DeleteAllMessagesSentToUser', () => { + let messageRepository: MessageRepositoryInterface + let deleteMessageUseCase: DeleteMessage + let message: Message + + const createUseCase = () => new DeleteAllMessagesSentToUser(messageRepository, deleteMessageUseCase) + + beforeEach(() => { + message = Message.create({ + senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + recipientUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + encryptedMessage: 'encryptedMessage', + replaceabilityIdentifier: 'replaceabilityIdentifier', + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + messageRepository = {} as jest.Mocked + messageRepository.findByRecipientUuid = jest.fn().mockReturnValue([message]) + + deleteMessageUseCase = {} as jest.Mocked + deleteMessageUseCase.execute = jest.fn().mockReturnValue(Result.ok()) + }) + + it('should delete all messages sent to user', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('should return error when recipient uuid is invalid', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + recipientUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Given value is not a valid uuid: invalid') + }) + + it('should return error when delete message use case fails', async () => { + const useCase = createUseCase() + deleteMessageUseCase.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('error') + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.ts new file mode 100644 index 000000000..22b23375e --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser.ts @@ -0,0 +1,30 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { DeleteAllMessagesSentToUserDTO } from './DeleteAllMessagesSentToUserDTO' +import { DeleteMessage } from '../DeleteMessage/DeleteMessage' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' + +export class DeleteAllMessagesSentToUser implements UseCaseInterface { + constructor(private messageRepository: MessageRepositoryInterface, private deleteMessageUseCase: DeleteMessage) {} + + async execute(dto: DeleteAllMessagesSentToUserDTO): Promise> { + const recipientUuidOrError = Uuid.create(dto.recipientUuid) + if (recipientUuidOrError.isFailed()) { + return Result.fail(recipientUuidOrError.getError()) + } + const recipientUuid = recipientUuidOrError.getValue() + + const messages = await this.messageRepository.findByRecipientUuid(recipientUuid) + + for (const message of messages) { + const result = await this.deleteMessageUseCase.execute({ + originatorUuid: recipientUuid.value, + messageUuid: message.id.toString(), + }) + if (result.isFailed()) { + return Result.fail(result.getError()) + } + } + + return Result.ok() + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUserDTO.ts new file mode 100644 index 000000000..d8c2189d3 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUserDTO.ts @@ -0,0 +1,3 @@ +export interface DeleteAllMessagesSentToUserDTO { + recipientUuid: string +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.spec.ts new file mode 100644 index 000000000..85652eb75 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.spec.ts @@ -0,0 +1,77 @@ +import { Timestamps, Uuid } from '@standardnotes/domain-core' +import { Message } from '../../../Message/Message' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { DeleteMessage } from './DeleteMessage' + +describe('DeleteMessage', () => { + let messageRepository: MessageRepositoryInterface + let message: Message + + const createUseCase = () => new DeleteMessage(messageRepository) + + beforeEach(() => { + message = Message.create({ + senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + recipientUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + encryptedMessage: 'encryptedMessage', + replaceabilityIdentifier: 'replaceabilityIdentifier', + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + messageRepository = {} as jest.Mocked + messageRepository.remove = jest.fn() + messageRepository.findByUuid = jest.fn().mockReturnValue(message) + }) + + it('should remove message', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + messageUuid: '00000000-0000-0000-0000-000000000000', + originatorUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('should return error when message is not found', async () => { + messageRepository.findByUuid = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + const result = await useCase.execute({ + messageUuid: '00000000-0000-0000-0000-000000000000', + originatorUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if originator is neither the sender nor the recipient', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + messageUuid: '00000000-0000-0000-0000-000000000000', + originatorUuid: '11111111-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error when message uuid is invalid', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + messageUuid: 'invalid', + originatorUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error when originator uuid is invalid', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + messageUuid: '00000000-0000-0000-0000-000000000000', + originatorUuid: 'invalid', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.ts new file mode 100644 index 000000000..6ca69d93e --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessage.ts @@ -0,0 +1,38 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { DeleteMessageDTO } from './DeleteMessageDTO' + +export class DeleteMessage implements UseCaseInterface { + constructor(private messageRepository: MessageRepositoryInterface) {} + + async execute(dto: DeleteMessageDTO): Promise> { + const originatorUuidOrError = Uuid.create(dto.originatorUuid) + if (originatorUuidOrError.isFailed()) { + return Result.fail(originatorUuidOrError.getError()) + } + const originatorUuid = originatorUuidOrError.getValue() + + const messageUuidOrError = Uuid.create(dto.messageUuid) + if (messageUuidOrError.isFailed()) { + return Result.fail(messageUuidOrError.getError()) + } + const messageUuid = messageUuidOrError.getValue() + + const message = await this.messageRepository.findByUuid(messageUuid) + if (!message) { + return Result.fail('Message not found') + } + + const isSentByOriginator = message.props.senderUuid.equals(originatorUuid) + const isSentToOriginator = message.props.recipientUuid.equals(originatorUuid) + + if (!isSentByOriginator && !isSentToOriginator) { + return Result.fail('Not authorized to delete this message') + } + + await this.messageRepository.remove(message) + + return Result.ok() + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessageDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessageDTO.ts new file mode 100644 index 000000000..be6609ef7 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/DeleteMessage/DeleteMessageDTO.ts @@ -0,0 +1,4 @@ +export interface DeleteMessageDTO { + originatorUuid: string + messageUuid: string +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.spec.ts new file mode 100644 index 000000000..91aa68fba --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.spec.ts @@ -0,0 +1,32 @@ +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { GetMessagesSentByUser } from './GetMessagesSentByUser' + +describe('GetMessagesSentByUser', () => { + let messageRepository: MessageRepositoryInterface + + const createUseCase = () => new GetMessagesSentByUser(messageRepository) + + beforeEach(() => { + messageRepository = {} as jest.Mocked + messageRepository.findBySenderUuid = jest.fn().mockReturnValue([]) + }) + + it('should return messages sent by user', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + senderUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.getValue()).toEqual([]) + }) + + it('should return error when sender uuid is invalid', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + senderUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Given value is not a valid uuid: invalid') + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.ts new file mode 100644 index 000000000..c56a50d2e --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser.ts @@ -0,0 +1,21 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { Message } from '../../../Message/Message' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { GetMessagesSentByUserDTO } from './GetMessagesSentByUserDTO' + +export class GetMessagesSentByUser implements UseCaseInterface { + constructor(private messageRepository: MessageRepositoryInterface) {} + + async execute(dto: GetMessagesSentByUserDTO): Promise> { + const senderUuidOrError = Uuid.create(dto.senderUuid) + if (senderUuidOrError.isFailed()) { + return Result.fail(senderUuidOrError.getError()) + } + const senderUuid = senderUuidOrError.getValue() + + const messages = await this.messageRepository.findBySenderUuid(senderUuid) + + return Result.ok(messages) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUserDTO.ts new file mode 100644 index 000000000..6971e66ce --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUserDTO.ts @@ -0,0 +1,3 @@ +export interface GetMessagesSentByUserDTO { + senderUuid: string +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts new file mode 100644 index 000000000..d7ffb2605 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts @@ -0,0 +1,32 @@ +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { GetMessagesSentToUser } from './GetMessagesSentToUser' + +describe('GetMessagesSentToUser', () => { + let messageRepository: MessageRepositoryInterface + + const createUseCase = () => new GetMessagesSentToUser(messageRepository) + + beforeEach(() => { + messageRepository = {} as jest.Mocked + messageRepository.findByRecipientUuid = jest.fn().mockReturnValue([]) + }) + + it('should return messages sent to user', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.getValue()).toEqual([]) + }) + + it('should return error when recipient uuid is invalid', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + recipientUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Given value is not a valid uuid: invalid') + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts new file mode 100644 index 000000000..afb2abe07 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts @@ -0,0 +1,21 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { Message } from '../../../Message/Message' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { GetMessagesSentToUserDTO } from './GetMessagesSentToUserDTO' + +export class GetMessagesSentToUser implements UseCaseInterface { + constructor(private messageRepository: MessageRepositoryInterface) {} + + async execute(dto: GetMessagesSentToUserDTO): Promise> { + const recipientUuidOrError = Uuid.create(dto.recipientUuid) + if (recipientUuidOrError.isFailed()) { + return Result.fail(recipientUuidOrError.getError()) + } + const recipientUuid = recipientUuidOrError.getValue() + + const messages = await this.messageRepository.findByRecipientUuid(recipientUuid) + + return Result.ok(messages) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts new file mode 100644 index 000000000..2547227d3 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts @@ -0,0 +1,3 @@ +export interface GetMessagesSentToUserDTO { + recipientUuid: string +} diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts index ef1bfe6a5..10d8ab5c1 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts @@ -11,6 +11,28 @@ export class TypeORMMessageRepository implements MessageRepositoryInterface { private mapper: MapperInterface, ) {} + async findByRecipientUuid(uuid: Uuid): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('message') + .where('message.recipient_uuid = :recipientUuid', { + recipientUuid: uuid.value, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } + + async findBySenderUuid(uuid: Uuid): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('message') + .where('message.sender_uuid = :senderUuid', { + senderUuid: uuid.value, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } + async findByRecipientUuidAndReplaceabilityIdentifier(dto: { recipientUuid: Uuid replaceabilityIdentifier: string