From 1b7a55ea5e93df3ff1b192ba77777d2de2216f73 Mon Sep 17 00:00:00 2001 From: MisterAzix Date: Thu, 15 May 2025 16:10:56 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20link=20talk=20speaker=20to=20user=20?= =?UTF-8?q?=F0=9F=A9=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 13 ++++ prisma/schema.prisma | 6 +- .../api/controller/talk.controller.ts | 20 +++-- src/adapters/api/mapper/create-talk.mapper.ts | 9 ++- ...ts => get-all-talks-with-detail.mapper.ts} | 18 +++-- src/adapters/api/mapper/update-talk.mapper.ts | 8 +- .../api/request/create-talk.request.ts | 8 -- .../api/request/update-talk.request.ts | 8 -- .../api/response/create-talk.response.ts | 6 +- ... => get-all-talks-with-detail.response.ts} | 38 +++++++--- .../api/response/update-talk.response.ts | 6 +- .../in-memory/in-memory-talk.repository.ts | 32 +++++--- .../in-memory/in-memory-user.repository.ts | 4 +- ...r.ts => prisma-talk-with-detail.mapper.ts} | 52 ++++++++++--- .../prisma/mapper/prisma-talk.mapper.ts | 4 +- src/adapters/prisma/prisma-talk.repository.ts | 22 +++--- src/app.module.ts | 8 +- src/config/domain-error.filter.ts | 4 + .../error/UserNotAllowedToUpdateTalkError.ts | 3 + src/core/domain/model/Talk.ts | 6 +- ...alkWithRoomDetail.ts => TalkWithDetail.ts} | 10 ++- src/core/domain/repository/talk.repository.ts | 10 +-- ...ate-talk-creation-request.use-case.spec.ts | 74 ++++++++++++++++--- .../get-all-talks-by-status.use-case.spec.ts | 8 +- ...ate-talk-creation-request.use-case.spec.ts | 46 +++++++++--- .../create-talk-creation-request.use-case.ts | 12 ++- .../get-all-talks-by-status.use-case.ts | 8 +- .../update-talk-creation-request.use-case.ts | 41 ++++++---- 28 files changed, 339 insertions(+), 145 deletions(-) create mode 100644 prisma/migrations/20250515133714_link_talk_speaker_to_user/migration.sql rename src/adapters/api/mapper/{get-all-talks-with-room-detail.mapper.ts => get-all-talks-with-detail.mapper.ts} (52%) rename src/adapters/api/response/{get-all-talks-with-room-detail.response.ts => get-all-talks-with-detail.response.ts} (73%) rename src/adapters/prisma/mapper/{prisma-talk-with-room.mapper.ts => prisma-talk-with-detail.mapper.ts} (68%) create mode 100644 src/core/domain/error/UserNotAllowedToUpdateTalkError.ts rename src/core/domain/model/{TalkWithRoomDetail.ts => TalkWithDetail.ts} (78%) diff --git a/prisma/migrations/20250515133714_link_talk_speaker_to_user/migration.sql b/prisma/migrations/20250515133714_link_talk_speaker_to_user/migration.sql new file mode 100644 index 0000000..2256de4 --- /dev/null +++ b/prisma/migrations/20250515133714_link_talk_speaker_to_user/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `speaker` on the `Talk` table. All the data in the column will be lost. + - Added the required column `speakerId` to the `Talk` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Talk" DROP COLUMN "speaker", +ADD COLUMN "speakerId" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "Talk" ADD CONSTRAINT "Talk_speakerId_fkey" FOREIGN KEY ("speakerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 526e90e..1b5ada5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,12 +58,13 @@ model Talk { title String subject TalkSubject description String - speaker String + level TalkLevel @default(INTERMEDIATE) startTime DateTime endTime DateTime + speakerId String + speaker User @relation(fields: [speakerId], references: [id]) roomId String room Room @relation(fields: [roomId], references: [id]) - level TalkLevel @default(INTERMEDIATE) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -75,4 +76,5 @@ model User { type UserType @default(SPEAKER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + Talk Talk[] } diff --git a/src/adapters/api/controller/talk.controller.ts b/src/adapters/api/controller/talk.controller.ts index a4d5a9c..9304cd7 100644 --- a/src/adapters/api/controller/talk.controller.ts +++ b/src/adapters/api/controller/talk.controller.ts @@ -29,8 +29,8 @@ import { CreateTalkMapper } from '../mapper/create-talk.mapper'; import { GetAllTalksByStatusRequest } from '../request/get-all-talks-by-status.request'; import { GetAllTalksByStatusUseCase } from '../../../core/usecases/get-all-talks-by-status.use-case'; import { TalkStatus } from '../../../core/domain/type/TalkStatus'; -import { GetAllTalksWithRoomDetailResponse } from '../response/get-all-talks-with-room-detail.response'; -import { GetAllTalksWithRoomDetailMapper } from '../mapper/get-all-talks-with-room-detail.mapper'; +import { GetAllTalksWithDetailResponse } from '../response/get-all-talks-with-detail.response'; +import { GetAllTalksWithDetailMapper } from '../mapper/get-all-talks-with-detail.mapper'; import { UserType } from '../../../core/domain/type/UserType'; import { Roles } from '../decorator/roles.decorator'; import { Public } from '../decorator/public.decorator'; @@ -38,6 +38,8 @@ import { UpdateTalkRequest } from '../request/update-talk.request'; import { UpdateTalkMapper } from '../mapper/update-talk.mapper'; import { UpdateTalkCreationRequestUseCase } from '../../../core/usecases/update-talk-creation-request.use-case'; import { UpdateTalkResponse } from '../response/update-talk.response'; +import { CurrentUser } from '../decorator/current-user.decorator'; +import { ProfileRequest } from '../request/profile.request'; @Controller('/talks') export class TalkController { @@ -59,7 +61,7 @@ export class TalkController { @ApiOperation({ summary: 'Get all talks by status' }) @ApiOkResponse({ description: 'List of all talks', - type: GetAllTalksWithRoomDetailResponse, + type: GetAllTalksWithDetailResponse, }) @ApiInternalServerErrorResponse({ description: 'Internal server error', @@ -67,11 +69,11 @@ export class TalkController { async getAllTalks( @Query('status') status: TalkStatus, - ): Promise { + ): Promise { const request: GetAllTalksByStatusRequest = { status }; - const talksWithRoomDetail = + const talksWithDetail = await this.getAllTalksByStatusUseCase.execute(request); - return GetAllTalksWithRoomDetailMapper.fromDomain(talksWithRoomDetail); + return GetAllTalksWithDetailMapper.fromDomain(talksWithDetail); } @Roles(UserType.PLANNER, UserType.SPEAKER) @@ -99,9 +101,10 @@ export class TalkController { description: 'Unauthorized access', }) async createTalk( + @CurrentUser() user: ProfileRequest, @Body() body: CreateTalkRequest, ): Promise { - const command = CreateTalkMapper.toDomain(body); + const command = CreateTalkMapper.toDomain(user.id, body); const talk = await this.createTalkUseCase.execute(command); return CreateTalkMapper.fromDomain(talk); } @@ -131,10 +134,11 @@ export class TalkController { description: 'Unauthorized access', }) async updateTalk( + @CurrentUser() user: ProfileRequest, @Param('talkId') talkId: string, @Body() body: UpdateTalkRequest, ): Promise { - const command = UpdateTalkMapper.toDomain(talkId, body); + const command = UpdateTalkMapper.toDomain(user, talkId, body); const talk = await this.updateTalkUseCase.execute(command); return UpdateTalkMapper.fromDomain(talk); } diff --git a/src/adapters/api/mapper/create-talk.mapper.ts b/src/adapters/api/mapper/create-talk.mapper.ts index 3b579ff..70fe406 100644 --- a/src/adapters/api/mapper/create-talk.mapper.ts +++ b/src/adapters/api/mapper/create-talk.mapper.ts @@ -4,12 +4,15 @@ import { CreateTalkResponse } from '../response/create-talk.response'; import { Talk } from '../../../core/domain/model/Talk'; export class CreateTalkMapper { - static toDomain(request: CreateTalkRequest): CreateTalkCommand { + static toDomain( + speakerId: string, + request: CreateTalkRequest, + ): CreateTalkCommand { return { title: request.title, subject: request.subject, description: request.description, - speaker: request.speaker, + speakerId: speakerId, roomId: request.roomId, level: request.level, startTime: new Date(request.startTime), @@ -24,7 +27,7 @@ export class CreateTalkMapper { title: talk.title, subject: talk.subject, description: talk.description, - speaker: talk.speaker, + speakerId: talk.speakerId, roomId: talk.roomId, level: talk.level, startTime: talk.startTime.toISOString(), diff --git a/src/adapters/api/mapper/get-all-talks-with-room-detail.mapper.ts b/src/adapters/api/mapper/get-all-talks-with-detail.mapper.ts similarity index 52% rename from src/adapters/api/mapper/get-all-talks-with-room-detail.mapper.ts rename to src/adapters/api/mapper/get-all-talks-with-detail.mapper.ts index 04706f6..4eb9764 100644 --- a/src/adapters/api/mapper/get-all-talks-with-room-detail.mapper.ts +++ b/src/adapters/api/mapper/get-all-talks-with-detail.mapper.ts @@ -1,18 +1,17 @@ -import { GetAllTalksWithRoomDetailResponse } from '../response/get-all-talks-with-room-detail.response'; -import { TalkWithRoomDetail } from '../../../core/domain/model/TalkWithRoomDetail'; +import { GetAllTalksWithDetailResponse } from '../response/get-all-talks-with-detail.response'; +import { TalkWithDetail } from '../../../core/domain/model/TalkWithDetail'; -export class GetAllTalksWithRoomDetailMapper { +export class GetAllTalksWithDetailMapper { static fromDomain( - talksWithRoomDetail: TalkWithRoomDetail[], - ): GetAllTalksWithRoomDetailResponse { + talksWithDetail: TalkWithDetail[], + ): GetAllTalksWithDetailResponse { return { - talks: talksWithRoomDetail.map((talk) => ({ + talks: talksWithDetail.map((talk) => ({ id: talk.id, status: talk.status, title: talk.title, subject: talk.subject, description: talk.description, - speaker: talk.speaker, level: talk.level, startTime: talk.startTime.toISOString(), endTime: talk.endTime.toISOString(), @@ -21,6 +20,11 @@ export class GetAllTalksWithRoomDetailMapper { name: talk.room.name, capacity: talk.room.capacity, }, + speaker: { + id: talk.speaker.id, + email: talk.speaker.email, + type: talk.speaker.type, + }, createdAt: talk.createdAt, updatedAt: talk.updatedAt, })), diff --git a/src/adapters/api/mapper/update-talk.mapper.ts b/src/adapters/api/mapper/update-talk.mapper.ts index 336b569..ae0f53d 100644 --- a/src/adapters/api/mapper/update-talk.mapper.ts +++ b/src/adapters/api/mapper/update-talk.mapper.ts @@ -2,18 +2,20 @@ import { Talk } from '../../../core/domain/model/Talk'; import { UpdateTalkRequest } from '../request/update-talk.request'; import { UpdateTalkCommand } from '../../../core/usecases/update-talk-creation-request.use-case'; import { UpdateTalkResponse } from '../response/update-talk.response'; +import { ProfileRequest } from '../request/profile.request'; export class UpdateTalkMapper { static toDomain( + currentUser: ProfileRequest, talkId: string, request: UpdateTalkRequest, ): UpdateTalkCommand { return { - talkId: talkId, + currentUser, + talkId, title: request.title, subject: request.subject, description: request.description, - speaker: request.speaker, roomId: request.roomId, level: request.level, startTime: new Date(request.startTime), @@ -28,7 +30,7 @@ export class UpdateTalkMapper { title: talk.title, subject: talk.subject, description: talk.description, - speaker: talk.speaker, + speakerId: talk.speakerId, roomId: talk.roomId, level: talk.level, startTime: talk.startTime.toISOString(), diff --git a/src/adapters/api/request/create-talk.request.ts b/src/adapters/api/request/create-talk.request.ts index 29c5cad..9b9bb1e 100644 --- a/src/adapters/api/request/create-talk.request.ts +++ b/src/adapters/api/request/create-talk.request.ts @@ -27,12 +27,6 @@ export class CreateTalkRequest { @MaxLength(512) description: string; - @ApiProperty() - @IsString() - @MinLength(2) - @MaxLength(64) - speaker: string; - @ApiProperty() @IsUUID() roomId: string; @@ -53,7 +47,6 @@ export class CreateTalkRequest { title: string, subject: TalkSubject, description: string, - speaker: string, roomId: string, level: TalkLevel, startTime: string, @@ -62,7 +55,6 @@ export class CreateTalkRequest { this.title = title; this.subject = subject; this.description = description; - this.speaker = speaker; this.roomId = roomId; this.level = level; this.startTime = startTime; diff --git a/src/adapters/api/request/update-talk.request.ts b/src/adapters/api/request/update-talk.request.ts index 71e365e..c77940c 100644 --- a/src/adapters/api/request/update-talk.request.ts +++ b/src/adapters/api/request/update-talk.request.ts @@ -27,12 +27,6 @@ export class UpdateTalkRequest { @MaxLength(512) description: string; - @ApiProperty() - @IsString() - @MinLength(2) - @MaxLength(64) - speaker: string; - @ApiProperty() @IsUUID() roomId: string; @@ -53,7 +47,6 @@ export class UpdateTalkRequest { title: string, subject: TalkSubject, description: string, - speaker: string, roomId: string, level: TalkLevel, startTime: string, @@ -62,7 +55,6 @@ export class UpdateTalkRequest { this.title = title; this.subject = subject; this.description = description; - this.speaker = speaker; this.roomId = roomId; this.level = level; this.startTime = startTime; diff --git a/src/adapters/api/response/create-talk.response.ts b/src/adapters/api/response/create-talk.response.ts index e74cb3c..4681303 100644 --- a/src/adapters/api/response/create-talk.response.ts +++ b/src/adapters/api/response/create-talk.response.ts @@ -20,7 +20,7 @@ export class CreateTalkResponse { description: string; @ApiProperty() - speaker: string; + speakerId: string; @ApiProperty() roomId: string; @@ -46,7 +46,7 @@ export class CreateTalkResponse { title: string, subject: TalkSubject, description: string, - speaker: string, + speakerId: string, roomId: string, level: TalkLevel, startTime: string, @@ -59,7 +59,7 @@ export class CreateTalkResponse { this.title = title; this.subject = subject; this.description = description; - this.speaker = speaker; + this.speakerId = speakerId; this.roomId = roomId; this.level = level; this.startTime = startTime; diff --git a/src/adapters/api/response/get-all-talks-with-room-detail.response.ts b/src/adapters/api/response/get-all-talks-with-detail.response.ts similarity index 73% rename from src/adapters/api/response/get-all-talks-with-room-detail.response.ts rename to src/adapters/api/response/get-all-talks-with-detail.response.ts index 3ae3b0a..02b4567 100644 --- a/src/adapters/api/response/get-all-talks-with-room-detail.response.ts +++ b/src/adapters/api/response/get-all-talks-with-detail.response.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { TalkStatus } from '../../../core/domain/type/TalkStatus'; import { TalkSubject } from '../../../core/domain/type/TalkSubject'; import { TalkLevel } from '../../../core/domain/type/TalkLevel'; +import { UserType } from '../../../core/domain/type/UserType'; class RoomDetail { @ApiProperty() @@ -20,7 +21,24 @@ class RoomDetail { } } -class TalksWithRoomDetail { +class SpeakerDetail { + @ApiProperty() + id: string; + + @ApiProperty() + email: string; + + @ApiProperty({ enum: UserType }) + type: UserType; + + constructor(id: string, email: string, type: UserType) { + this.id = id; + this.email = email; + this.type = type; + } +} + +class TalksWithDetail { @ApiProperty() id: string; @@ -36,9 +54,6 @@ class TalksWithRoomDetail { @ApiProperty() description: string; - @ApiProperty() - speaker: string; - @ApiProperty({ enum: TalkLevel }) level: TalkLevel; @@ -51,6 +66,9 @@ class TalksWithRoomDetail { @ApiProperty({ type: () => RoomDetail }) room: RoomDetail; + @ApiProperty({ type: () => SpeakerDetail }) + speaker: SpeakerDetail; + @ApiProperty() createdAt: Date; @@ -63,11 +81,11 @@ class TalksWithRoomDetail { title: string, subject: TalkSubject, description: string, - speaker: string, level: TalkLevel, startTime: string, endTime: string, room: RoomDetail, + speaker: SpeakerDetail, createdAt: Date, updatedAt: Date, ) { @@ -76,21 +94,21 @@ class TalksWithRoomDetail { this.title = title; this.subject = subject; this.description = description; - this.speaker = speaker; this.level = level; this.startTime = startTime; this.endTime = endTime; this.room = room; + this.speaker = speaker; this.createdAt = createdAt; this.updatedAt = updatedAt; } } -export class GetAllTalksWithRoomDetailResponse { - @ApiProperty({ type: [TalksWithRoomDetail] }) - talks: TalksWithRoomDetail[]; +export class GetAllTalksWithDetailResponse { + @ApiProperty({ type: [TalksWithDetail] }) + talks: TalksWithDetail[]; - constructor(talks: TalksWithRoomDetail[]) { + constructor(talks: TalksWithDetail[]) { this.talks = talks; } } diff --git a/src/adapters/api/response/update-talk.response.ts b/src/adapters/api/response/update-talk.response.ts index 4b05703..18d6b1c 100644 --- a/src/adapters/api/response/update-talk.response.ts +++ b/src/adapters/api/response/update-talk.response.ts @@ -20,7 +20,7 @@ export class UpdateTalkResponse { description: string; @ApiProperty() - speaker: string; + speakerId: string; @ApiProperty() roomId: string; @@ -46,7 +46,7 @@ export class UpdateTalkResponse { title: string, subject: TalkSubject, description: string, - speaker: string, + speakerId: string, roomId: string, level: TalkLevel, startTime: string, @@ -59,7 +59,7 @@ export class UpdateTalkResponse { this.title = title; this.subject = subject; this.description = description; - this.speaker = speaker; + this.speakerId = speakerId; this.roomId = roomId; this.level = level; this.startTime = startTime; diff --git a/src/adapters/in-memory/in-memory-talk.repository.ts b/src/adapters/in-memory/in-memory-talk.repository.ts index f1f6ac3..44b2d32 100644 --- a/src/adapters/in-memory/in-memory-talk.repository.ts +++ b/src/adapters/in-memory/in-memory-talk.repository.ts @@ -3,14 +3,19 @@ import { TalkRepository } from '../../core/domain/repository/talk.repository'; import { Talk } from '../../core/domain/model/Talk'; import { TalkStatus } from '../../core/domain/type/TalkStatus'; import { RoomRepository } from '../../core/domain/repository/room.repository'; -import { TalkWithRoomDetail } from '../../core/domain/model/TalkWithRoomDetail'; +import { TalkWithDetail } from '../../core/domain/model/TalkWithDetail'; import { Room } from '../../core/domain/model/Room'; +import { User } from '../../core/domain/model/User'; +import { UserRepository } from '../../core/domain/repository/user.repository'; @Injectable() export class InMemoryTalkRepository implements TalkRepository { private talks: Map = new Map(); - constructor(private readonly roomRepository: RoomRepository) {} + constructor( + private readonly roomRepository: RoomRepository, + private readonly userRepository: UserRepository, + ) {} create(talk: Talk): any { this.talks.set(talk.id, talk); @@ -34,41 +39,46 @@ export class InMemoryTalkRepository implements TalkRepository { ); } - async findByStatusWithRoomDetails( + async findByStatusWithDetails( status?: TalkStatus, - ): Promise { + ): Promise { const rooms = await this.roomRepository.findAll(); + const speakers = await this.userRepository.findAll(); const talks = status ? Array.from(this.talks.values()).filter((talk) => talk.status === status) : Array.from(this.talks.values()); - return this.enrichTalksWithRoomDetails(talks, rooms); + return this.enrichTalksWithDetails(talks, rooms, speakers); } - async findAllWithRoomDetail(): Promise { + async findAllWithRoomDetail(): Promise { const rooms = await this.roomRepository.findAll(); + const speakers = await this.userRepository.findAll(); const talks = Array.from(this.talks.values()); - return this.enrichTalksWithRoomDetails(talks, rooms); + return this.enrichTalksWithDetails(talks, rooms, speakers); } - private enrichTalksWithRoomDetails( + private enrichTalksWithDetails( talks: Talk[], rooms: Room[], - ): TalkWithRoomDetail[] { + speakers: User[], + ): TalkWithDetail[] { return talks.map((talk) => { const room = rooms.find((room) => room.id === talk.roomId); - return new TalkWithRoomDetail( + const speaker = speakers.find((speaker) => speaker.id === talk.speakerId); + return new TalkWithDetail( talk.id, talk.status, talk.title, talk.subject, talk.description, - talk.speaker, + talk.speakerId, talk.roomId, talk.level, talk.startTime, talk.endTime, room || ({} as Room), + speaker || ({} as User), talk.updatedAt, talk.createdAt, ); diff --git a/src/adapters/in-memory/in-memory-user.repository.ts b/src/adapters/in-memory/in-memory-user.repository.ts index 66bbe4e..dfd0001 100644 --- a/src/adapters/in-memory/in-memory-user.repository.ts +++ b/src/adapters/in-memory/in-memory-user.repository.ts @@ -6,9 +6,9 @@ import { Injectable } from '@nestjs/common'; export class InMemoryUserRepository implements UserRepository { private users: Map = new Map(); - create(data: Pick): User { + create(data: Pick): User { const user = new User( - crypto.randomUUID(), + data.id, data.email, data.password, data.type, diff --git a/src/adapters/prisma/mapper/prisma-talk-with-room.mapper.ts b/src/adapters/prisma/mapper/prisma-talk-with-detail.mapper.ts similarity index 68% rename from src/adapters/prisma/mapper/prisma-talk-with-room.mapper.ts rename to src/adapters/prisma/mapper/prisma-talk-with-detail.mapper.ts index 5ec0d4e..3295487 100644 --- a/src/adapters/prisma/mapper/prisma-talk-with-room.mapper.ts +++ b/src/adapters/prisma/mapper/prisma-talk-with-detail.mapper.ts @@ -1,23 +1,32 @@ import { EntityMapper } from '../../../core/base/entity-mapper'; -import { Talk as TalkEntity, Room as RoomEntity, $Enums } from '@prisma/client'; +import { + Talk as TalkEntity, + Room as RoomEntity, + User as UserEntity, + $Enums, +} from '@prisma/client'; import { TalkSubject } from '../../../core/domain/type/TalkSubject'; import { TalkStatus } from '../../../core/domain/type/TalkStatus'; import { TalkLevel } from '../../../core/domain/type/TalkLevel'; -import { TalkWithRoomDetail } from '../../../core/domain/model/TalkWithRoomDetail'; +import { TalkWithDetail } from '../../../core/domain/model/TalkWithDetail'; +import { UserType } from '../../../core/domain/type/UserType'; -type TalkEntityWithRoom = TalkEntity & { room: RoomEntity }; +type TalkEntityWithDetail = TalkEntity & { + room: RoomEntity; + speaker: Omit; +}; -export class PrismaTalkWithRoomMapper - implements EntityMapper +export class PrismaTalkWithDetailMapper + implements EntityMapper { - fromDomain(model: TalkWithRoomDetail): TalkEntityWithRoom { + fromDomain(model: TalkWithDetail): TalkEntityWithDetail { return { id: model.id, status: model.status, title: model.title, subject: model.subject, description: model.description, - speaker: model.speaker, + speakerId: model.speakerId, roomId: model.roomId, level: model.level, startTime: model.startTime, @@ -31,17 +40,24 @@ export class PrismaTalkWithRoomMapper updatedAt: model.room.updatedAt, createdAt: model.room.createdAt, }, + speaker: { + id: model.speaker.id, + email: model.speaker.email, + type: model.speaker.type, + updatedAt: model.speaker.updatedAt, + createdAt: model.speaker.createdAt, + }, }; } - toDomain(entity: TalkEntityWithRoom): TalkWithRoomDetail { + toDomain(entity: TalkEntityWithDetail): TalkWithDetail { return { id: entity.id, status: this.mapTalkStatusToDomain(entity.status), title: entity.title, subject: this.mapTalkSubjectToDomain(entity.subject), description: entity.description, - speaker: entity.speaker, + speakerId: entity.speakerId, roomId: entity.roomId, level: this.mapTalkLevelToDomain(entity.level), startTime: entity.startTime, @@ -53,6 +69,13 @@ export class PrismaTalkWithRoomMapper updatedAt: entity.room.updatedAt, createdAt: entity.room.createdAt, }, + speaker: { + id: entity.speaker.id, + email: entity.speaker.email, + type: this.mapUserTypeToDomain(entity.speaker.type), + updatedAt: entity.speaker.updatedAt, + createdAt: entity.speaker.createdAt, + }, updatedAt: entity.updatedAt, createdAt: entity.createdAt, }; @@ -110,4 +133,15 @@ export class PrismaTalkWithRoomMapper throw new Error('Invalid talk level'); } } + + private mapUserTypeToDomain(type: $Enums.UserType): UserType { + switch (type) { + case 'PLANNER': + return UserType.PLANNER; + case 'SPEAKER': + return UserType.SPEAKER; + default: + throw new Error('Invalid user type'); + } + } } diff --git a/src/adapters/prisma/mapper/prisma-talk.mapper.ts b/src/adapters/prisma/mapper/prisma-talk.mapper.ts index 626a2a9..b0fef9f 100644 --- a/src/adapters/prisma/mapper/prisma-talk.mapper.ts +++ b/src/adapters/prisma/mapper/prisma-talk.mapper.ts @@ -13,7 +13,7 @@ export class PrismaTalkMapper implements EntityMapper { title: model.title, subject: model.subject, description: model.description, - speaker: model.speaker, + speakerId: model.speakerId, roomId: model.roomId, level: model.level, startTime: model.startTime, @@ -30,7 +30,7 @@ export class PrismaTalkMapper implements EntityMapper { title: entity.title, subject: this.mapTalkSubjectToDomain(entity.subject), description: entity.description, - speaker: entity.speaker, + speakerId: entity.speakerId, roomId: entity.roomId, level: this.mapTalkLevelToDomain(entity.level), startTime: entity.startTime, diff --git a/src/adapters/prisma/prisma-talk.repository.ts b/src/adapters/prisma/prisma-talk.repository.ts index cf4c14b..c9f3347 100644 --- a/src/adapters/prisma/prisma-talk.repository.ts +++ b/src/adapters/prisma/prisma-talk.repository.ts @@ -4,14 +4,14 @@ import { Talk } from '../../core/domain/model/Talk'; import { PrismaService } from './prisma.service'; import { PrismaTalkMapper } from './mapper/prisma-talk.mapper'; import { TalkStatus } from '../../core/domain/type/TalkStatus'; -import { TalkWithRoomDetail } from '../../core/domain/model/TalkWithRoomDetail'; -import { PrismaTalkWithRoomMapper } from './mapper/prisma-talk-with-room.mapper'; +import { TalkWithDetail } from '../../core/domain/model/TalkWithDetail'; +import { PrismaTalkWithDetailMapper } from './mapper/prisma-talk-with-detail.mapper'; @Injectable() export class PrismaTalkRepository implements TalkRepository { private mapper: PrismaTalkMapper = new PrismaTalkMapper(); - private mapperWithRoom: PrismaTalkWithRoomMapper = - new PrismaTalkWithRoomMapper(); + private mapperWithDetail: PrismaTalkWithDetailMapper = + new PrismaTalkWithDetailMapper(); constructor(private readonly prisma: PrismaService) {} @@ -47,23 +47,23 @@ export class PrismaTalkRepository implements TalkRepository { return entities.map((entity) => this.mapper.toDomain(entity)); } - async findByStatusWithRoomDetails( + async findByStatusWithDetails( status?: TalkStatus, - ): Promise { + ): Promise { const talks = await this.prisma.talk.findMany({ where: status ? { status } : undefined, - include: { room: true }, + include: { room: true, speaker: true }, }); - return talks.map((entity) => this.mapperWithRoom.toDomain(entity)); + return talks.map((entity) => this.mapperWithDetail.toDomain(entity)); } - async findAllWithRoomDetail(): Promise { + async findAllWithRoomDetail(): Promise { const talks = await this.prisma.talk.findMany({ - include: { room: true }, + include: { room: true, speaker: true }, }); - return talks.map((entity) => this.mapperWithRoom.toDomain(entity)); + return talks.map((entity) => this.mapperWithDetail.toDomain(entity)); } async update(id: string, talk: Talk): Promise { diff --git a/src/app.module.ts b/src/app.module.ts index 67df5af..e1f3121 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -78,7 +78,13 @@ import { UpdateTalkCreationRequestUseCase } from './core/usecases/update-talk-cr useFactory: ( talkRepository: TalkRepository, roomRepository: RoomRepository, - ) => new CreateTalkCreationRequestUseCase(talkRepository, roomRepository), + userRepository: UserRepository, + ) => + new CreateTalkCreationRequestUseCase( + talkRepository, + roomRepository, + userRepository, + ), inject: [TalkRepository, RoomRepository], }, { diff --git a/src/config/domain-error.filter.ts b/src/config/domain-error.filter.ts index 2c1445c..05f811c 100644 --- a/src/config/domain-error.filter.ts +++ b/src/config/domain-error.filter.ts @@ -14,6 +14,7 @@ import { WrongEmailFormatError } from '../core/domain/error/WrongEmailFormatErro import { WrongPasswordFormatError } from '../core/domain/error/WrongPasswordFormatError'; import { TalkAlreadyApprovedOrRejectedError } from '../core/domain/error/TalkAlreadyApprovedOrRejectedError'; import { TalkNotFoundError } from '../core/domain/error/TalkNotFoundError'; +import { UserNotAllowedToUpdateTalkError } from '../core/domain/error/UserNotAllowedToUpdateTalkError'; @Catch(DomainError) export class DomainErrorFilter implements ExceptionFilter { @@ -54,6 +55,9 @@ export class DomainErrorFilter implements ExceptionFilter { ) { return HttpStatus.CONFLICT; } + if (exception instanceof UserNotAllowedToUpdateTalkError) { + return HttpStatus.FORBIDDEN; + } return HttpStatus.INTERNAL_SERVER_ERROR; } } diff --git a/src/core/domain/error/UserNotAllowedToUpdateTalkError.ts b/src/core/domain/error/UserNotAllowedToUpdateTalkError.ts new file mode 100644 index 0000000..703856c --- /dev/null +++ b/src/core/domain/error/UserNotAllowedToUpdateTalkError.ts @@ -0,0 +1,3 @@ +import { DomainError } from '../../base/domain-error'; + +export class UserNotAllowedToUpdateTalkError extends DomainError {} diff --git a/src/core/domain/model/Talk.ts b/src/core/domain/model/Talk.ts index 7a3df6a..762c994 100644 --- a/src/core/domain/model/Talk.ts +++ b/src/core/domain/model/Talk.ts @@ -8,7 +8,7 @@ export class Talk extends DomainModel { title: string; subject: TalkSubject; description: string; - speaker: string; + speakerId: string; roomId: string; level: TalkLevel; startTime: Date; @@ -22,7 +22,7 @@ export class Talk extends DomainModel { title: string, subject: TalkSubject, description: string, - speaker: string, + speakerId: string, roomId: string, level: TalkLevel, startTime: Date, @@ -35,7 +35,7 @@ export class Talk extends DomainModel { this.title = title; this.subject = subject; this.description = description; - this.speaker = speaker; + this.speakerId = speakerId; this.roomId = roomId; this.level = level; this.startTime = startTime; diff --git a/src/core/domain/model/TalkWithRoomDetail.ts b/src/core/domain/model/TalkWithDetail.ts similarity index 78% rename from src/core/domain/model/TalkWithRoomDetail.ts rename to src/core/domain/model/TalkWithDetail.ts index 0e393b0..bb461a5 100644 --- a/src/core/domain/model/TalkWithRoomDetail.ts +++ b/src/core/domain/model/TalkWithDetail.ts @@ -3,9 +3,11 @@ import { Talk } from './Talk'; import { TalkStatus } from '../type/TalkStatus'; import { TalkSubject } from '../type/TalkSubject'; import { TalkLevel } from '../type/TalkLevel'; +import { User } from './User'; -export class TalkWithRoomDetail extends Talk { +export class TalkWithDetail extends Talk { room: Room; + speaker: Omit; constructor( id: string, @@ -13,12 +15,13 @@ export class TalkWithRoomDetail extends Talk { title: string, subject: TalkSubject, description: string, - speaker: string, + speakerId: string, roomId: string, level: TalkLevel, startTime: Date, endTime: Date, room: Room, + speaker: User, updatedAt?: Date, createdAt?: Date, ) { @@ -28,7 +31,7 @@ export class TalkWithRoomDetail extends Talk { title, subject, description, - speaker, + speakerId, roomId, level, startTime, @@ -37,5 +40,6 @@ export class TalkWithRoomDetail extends Talk { createdAt, ); this.room = room; + this.speaker = speaker; } } diff --git a/src/core/domain/repository/talk.repository.ts b/src/core/domain/repository/talk.repository.ts index bdd6390..9dac783 100644 --- a/src/core/domain/repository/talk.repository.ts +++ b/src/core/domain/repository/talk.repository.ts @@ -1,17 +1,17 @@ import { Repository } from '../../base/repository'; import { Talk } from '../model/Talk'; import { TalkStatus } from '../type/TalkStatus'; -import { TalkWithRoomDetail } from '../model/TalkWithRoomDetail'; +import { TalkWithDetail } from '../model/TalkWithDetail'; export abstract class TalkRepository extends Repository { abstract findByRoomIdAndStatuses( roomId: string, statuses: TalkStatus[], ): Promise | Talk[]; - abstract findByStatusWithRoomDetails( + abstract findByStatusWithDetails( status?: TalkStatus, - ): Promise | TalkWithRoomDetail[]; + ): Promise | TalkWithDetail[]; abstract findAllWithRoomDetail(): - | Promise - | TalkWithRoomDetail[]; + | Promise + | TalkWithDetail[]; } diff --git a/src/core/usecases/__test__/create-talk-creation-request.use-case.spec.ts b/src/core/usecases/__test__/create-talk-creation-request.use-case.spec.ts index c172c57..8100f6f 100644 --- a/src/core/usecases/__test__/create-talk-creation-request.use-case.spec.ts +++ b/src/core/usecases/__test__/create-talk-creation-request.use-case.spec.ts @@ -10,20 +10,28 @@ import { RoomNotFoundError } from '../../domain/error/RoomNotFoundError'; import { TalkSubject } from '../../domain/type/TalkSubject'; import { TalkStatus } from '../../domain/type/TalkStatus'; import { TalkLevel } from '../../domain/type/TalkLevel'; +import { InMemoryUserRepository } from '../../../adapters/in-memory/in-memory-user.repository'; +import { UserRepository } from '../../domain/repository/user.repository'; +import { UserType } from '../../domain/type/UserType'; +import { UserNotFoundError } from '../../domain/error/UserNotFoundError'; describe('CreateTalkCreationRequestUseCase', () => { let talkRepository: TalkRepository; let roomRepository: RoomRepository; + let userRepository: UserRepository; let createTalkUseCase: CreateTalkCreationRequestUseCase; beforeEach(async () => { roomRepository = new InMemoryRoomRepository(); talkRepository = new InMemoryTalkRepository(roomRepository); + userRepository = new InMemoryUserRepository(); await talkRepository.removeAll(); await roomRepository.removeAll(); + await userRepository.removeAll(); createTalkUseCase = new CreateTalkCreationRequestUseCase( talkRepository, roomRepository, + userRepository, ); }); @@ -33,20 +41,25 @@ describe('CreateTalkCreationRequestUseCase', () => { it('should return created talk', async () => { // Given - const roomEntity = { + await userRepository.create({ + id: 'speaker-1', + email: 'john.doe@example.com', + password: 'password123', + type: UserType.SPEAKER, + }); + await roomRepository.create({ id: 'room-1', name: 'Room 1', capacity: 10, createdAt: new Date(), updatedAt: new Date(), - }; - await roomRepository.create(roomEntity); + }); const command: CreateTalkCommand = { title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -67,7 +80,7 @@ describe('CreateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -79,14 +92,46 @@ describe('CreateTalkCreationRequestUseCase', () => { }); }); + it('should throw error when speaker not found', async () => { + // Given + const command: CreateTalkCommand = { + title: 'La Clean Architecture pour les nuls', + subject: TalkSubject.WEB_DEVELOPMENT, + description: + 'Une introduction à la Clean Architecture dans le monde TypeScript.', + speakerId: 'speaker-1', + roomId: 'room-1', + level: TalkLevel.BEGINNER, + startTime: new Date('2023-10-01T10:00:00Z'), + endTime: new Date('2023-10-01T11:00:00Z'), + }; + + // When + try { + await createTalkUseCase.execute(command); + } catch (error) { + // Then + expect(error).toBeInstanceOf(UserNotFoundError); + expect((error as Error).message).toEqual( + 'User with email speaker-1 not found', + ); + } + }); + it('should throw error when room not found', async () => { // Given + await userRepository.create({ + id: 'speaker-1', + email: 'john.doe@example.com', + password: 'password123', + type: UserType.SPEAKER, + }); const command: CreateTalkCommand = { title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -112,7 +157,7 @@ describe('CreateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T11:00:00Z'), @@ -135,7 +180,7 @@ describe('CreateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T08:00:00Z'), @@ -164,7 +209,7 @@ describe('CreateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:30:00Z'), @@ -193,7 +238,7 @@ describe('CreateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:30:00Z'), @@ -212,6 +257,13 @@ describe('CreateTalkCreationRequestUseCase', () => { startTime: Date, endTime: Date, ): Promise { + await userRepository.create({ + id: 'speaker-1', + email: 'john.doe@example.com', + password: 'password123', + type: UserType.SPEAKER, + }); + await roomRepository.create({ id: 'room-1', name: 'Room 1', @@ -227,7 +279,7 @@ describe('CreateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime, diff --git a/src/core/usecases/__test__/get-all-talks-by-status.use-case.spec.ts b/src/core/usecases/__test__/get-all-talks-by-status.use-case.spec.ts index f6edf46..a34a27d 100644 --- a/src/core/usecases/__test__/get-all-talks-by-status.use-case.spec.ts +++ b/src/core/usecases/__test__/get-all-talks-by-status.use-case.spec.ts @@ -6,15 +6,19 @@ import { TalkSubject } from '../../domain/type/TalkSubject'; import { RoomRepository } from '../../domain/repository/room.repository'; import { InMemoryRoomRepository } from '../../../adapters/in-memory/in-memory-room.repository'; import { TalkLevel } from '../../domain/type/TalkLevel'; +import { InMemoryUserRepository } from '../../../adapters/in-memory/in-memory-user.repository'; +import { UserRepository } from '../../domain/repository/user.repository'; describe('GetAllTalksByStatusUseCase', () => { let talkRepository: TalkRepository; let roomRepository: RoomRepository; + let userRepository: UserRepository; let getAllTalksByStatusUseCase: GetAllTalksByStatusUseCase; beforeEach(async () => { roomRepository = new InMemoryRoomRepository(); - talkRepository = new InMemoryTalkRepository(roomRepository); + userRepository = new InMemoryUserRepository(); + talkRepository = new InMemoryTalkRepository(roomRepository, userRepository); await talkRepository.removeAll(); await roomRepository.removeAll(); getAllTalksByStatusUseCase = new GetAllTalksByStatusUseCase(talkRepository); @@ -156,7 +160,7 @@ describe('GetAllTalksByStatusUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), diff --git a/src/core/usecases/__test__/update-talk-creation-request.use-case.spec.ts b/src/core/usecases/__test__/update-talk-creation-request.use-case.spec.ts index 42d1ed3..6038f2b 100644 --- a/src/core/usecases/__test__/update-talk-creation-request.use-case.spec.ts +++ b/src/core/usecases/__test__/update-talk-creation-request.use-case.spec.ts @@ -11,15 +11,20 @@ import { UpdateTalkCreationRequestUseCase, } from '../update-talk-creation-request.use-case'; import { TalkNotFoundError } from '../../domain/error/TalkNotFoundError'; +import { UserType } from '../../domain/type/UserType'; +import { UserRepository } from '../../domain/repository/user.repository'; +import { InMemoryUserRepository } from '../../../adapters/in-memory/in-memory-user.repository'; describe('UpdateTalkCreationRequestUseCase', () => { let talkRepository: TalkRepository; let roomRepository: RoomRepository; + let userRepository: UserRepository; let updateTalkUseCase: UpdateTalkCreationRequestUseCase; beforeEach(async () => { roomRepository = new InMemoryRoomRepository(); - talkRepository = new InMemoryTalkRepository(roomRepository); + userRepository = new InMemoryUserRepository(); + talkRepository = new InMemoryTalkRepository(roomRepository, userRepository); await talkRepository.removeAll(); await roomRepository.removeAll(); updateTalkUseCase = new UpdateTalkCreationRequestUseCase( @@ -42,12 +47,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { new Date('2023-10-01T11:00:00Z'), ); const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId, title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -68,7 +76,7 @@ describe('UpdateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -83,12 +91,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { it('should throw error when talk not found', async () => { // Given const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId: 'talk-1', title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -117,12 +128,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { new Date('2023-10-01T11:00:00Z'), ); const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId, title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-2', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:00:00Z'), @@ -151,12 +165,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { new Date('2023-10-01T11:00:00Z'), ); const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId, title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T11:00:00Z'), @@ -182,12 +199,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { new Date('2023-10-01T11:00:00Z'), ); const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId, title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T08:00:00Z'), @@ -219,12 +239,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { new Date('2023-10-01T11:00:00Z'), ); const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId, title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:30:00Z'), @@ -256,12 +279,15 @@ describe('UpdateTalkCreationRequestUseCase', () => { new Date('2023-10-01T11:00:00Z'), ); const command: UpdateTalkCommand = { + currentUser: { + id: 'speaker-1', + type: UserType.SPEAKER, + }, talkId, title: 'La Clean Architecture pour les nuls', subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime: new Date('2023-10-01T10:30:00Z'), @@ -296,7 +322,7 @@ describe('UpdateTalkCreationRequestUseCase', () => { subject: TalkSubject.WEB_DEVELOPMENT, description: 'Une introduction à la Clean Architecture dans le monde TypeScript.', - speaker: 'John Doe', + speakerId: 'speaker-1', roomId: 'room-1', level: TalkLevel.BEGINNER, startTime, diff --git a/src/core/usecases/create-talk-creation-request.use-case.ts b/src/core/usecases/create-talk-creation-request.use-case.ts index 96227cf..fdaa476 100644 --- a/src/core/usecases/create-talk-creation-request.use-case.ts +++ b/src/core/usecases/create-talk-creation-request.use-case.ts @@ -8,12 +8,14 @@ import { RoomNotFoundError } from '../domain/error/RoomNotFoundError'; import { TalkSubject } from '../domain/type/TalkSubject'; import { TalkStatus } from '../domain/type/TalkStatus'; import { TalkLevel } from '../domain/type/TalkLevel'; +import { UserRepository } from '../domain/repository/user.repository'; +import { UserNotFoundError } from '../domain/error/UserNotFoundError'; export type CreateTalkCommand = { title: string; subject: TalkSubject; description: string; - speaker: string; + speakerId: string; roomId: string; level: TalkLevel; startTime: Date; @@ -30,6 +32,7 @@ export class CreateTalkCreationRequestUseCase constructor( private readonly talkRepository: TalkRepository, private readonly roomRepository: RoomRepository, + private readonly userRepository: UserRepository, ) {} async execute(command: CreateTalkCommand): Promise { @@ -48,6 +51,11 @@ export class CreateTalkCreationRequestUseCase ); } + const speaker = await this.userRepository.findById(command.speakerId); + if (!speaker) { + throw new UserNotFoundError(command.speakerId); + } + const room = await this.roomRepository.findById(command.roomId); if (!room) { throw new RoomNotFoundError(command.roomId); @@ -75,7 +83,7 @@ export class CreateTalkCreationRequestUseCase command.title, command.subject, command.description, - command.speaker, + command.speakerId, command.roomId, command.level, command.startTime, diff --git a/src/core/usecases/get-all-talks-by-status.use-case.ts b/src/core/usecases/get-all-talks-by-status.use-case.ts index 7187513..ea85b27 100644 --- a/src/core/usecases/get-all-talks-by-status.use-case.ts +++ b/src/core/usecases/get-all-talks-by-status.use-case.ts @@ -1,23 +1,23 @@ import { UseCase } from '../base/use-case'; import { TalkRepository } from '../domain/repository/talk.repository'; import { TalkStatus } from '../domain/type/TalkStatus'; -import { TalkWithRoomDetail } from '../domain/model/TalkWithRoomDetail'; +import { TalkWithDetail } from '../domain/model/TalkWithDetail'; export type GetAllTalksByStatusCommand = { status?: TalkStatus; }; export class GetAllTalksByStatusUseCase - implements UseCase + implements UseCase { constructor(private readonly talkRepository: TalkRepository) {} async execute( command: GetAllTalksByStatusCommand, - ): Promise { + ): Promise { const { status } = command; if (status && Object.values(TalkStatus).includes(status)) { - return this.talkRepository.findByStatusWithRoomDetails(status); + return this.talkRepository.findByStatusWithDetails(status); } return this.talkRepository.findAllWithRoomDetail(); } diff --git a/src/core/usecases/update-talk-creation-request.use-case.ts b/src/core/usecases/update-talk-creation-request.use-case.ts index 53be7f8..32957c7 100644 --- a/src/core/usecases/update-talk-creation-request.use-case.ts +++ b/src/core/usecases/update-talk-creation-request.use-case.ts @@ -10,13 +10,16 @@ import { TalkStatus } from '../domain/type/TalkStatus'; import { TalkLevel } from '../domain/type/TalkLevel'; import { TalkAlreadyApprovedOrRejectedError } from '../domain/error/TalkAlreadyApprovedOrRejectedError'; import { TalkNotFoundError } from '../domain/error/TalkNotFoundError'; +import { UserNotAllowedToUpdateTalkError } from '../domain/error/UserNotAllowedToUpdateTalkError'; +import { User } from '../domain/model/User'; +import { UserType } from '../domain/type/UserType'; export type UpdateTalkCommand = { + currentUser: Pick; talkId: string; title: string; subject: TalkSubject; description: string; - speaker: string; roomId: string; level: TalkLevel; startTime: Date; @@ -40,6 +43,11 @@ export class UpdateTalkCreationRequestUseCase if (!existingTalk) { throw new TalkNotFoundError(command.talkId); } + if (!this.canExecute(existingTalk, command.currentUser)) { + throw new UserNotAllowedToUpdateTalkError( + 'User not allowed to update talk', + ); + } if (existingTalk.status !== TalkStatus.PENDING_APPROVAL) { throw new TalkAlreadyApprovedOrRejectedError( existingTalk.id, @@ -85,23 +93,28 @@ export class UpdateTalkCreationRequestUseCase } } - const talk = new Talk( - crypto.randomUUID(), - TalkStatus.PENDING_APPROVAL, - command.title, - command.subject, - command.description, - command.speaker, - command.roomId, - command.level, - command.startTime, - command.endTime, - ); + existingTalk.title = command.title; + existingTalk.subject = command.subject; + existingTalk.description = command.description; + existingTalk.roomId = command.roomId; + existingTalk.level = command.level; + existingTalk.startTime = command.startTime; + existingTalk.endTime = command.endTime; - const updatedTalk = await this.talkRepository.update(command.talkId, talk); + const updatedTalk = await this.talkRepository.update( + existingTalk.id, + existingTalk, + ); if (!updatedTalk) { throw new TalkNotFoundError(command.talkId); } return updatedTalk; } + + private canExecute( + existingTalk: Talk, + user: Pick, + ): boolean { + return existingTalk.speakerId === user.id || user.type === UserType.PLANNER; + } }