From c2ae623e72d4e2a773a79acac785ed94924eac08 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 14 Dec 2022 11:37:13 +0100 Subject: [PATCH 1/4] create, read, findone --- .../migration.sql | 3 + prisma/schema.prisma | 2 +- src/app.module.ts | 8 ++- .../src/exceptions/DatabaseException.ts | 1 - .../users/adapters/primaries/user.proto | 10 +-- .../adapters/primaries/users.controller.ts | 67 +++++++++++++++++-- .../users/commands/create-user.command.ts | 9 +++ .../users/commands/update-user.command.ts | 9 +++ .../users/domain/dto/create-user.request.ts | 23 +++++++ .../domain/dto/find-user-by-uuid.request.ts | 7 ++ .../users/domain/dto/findUserByUuidRequest.ts | 6 -- .../users/domain/dto/update-user.request.ts | 3 + .../domain/usecases/create-user.usecase.ts | 25 +++++++ .../domain/usecases/update-user.usecase.ts | 25 +++++++ src/modules/users/mappers/user.profile.ts | 13 +++- src/modules/users/users.module.ts | 11 ++- 16 files changed, 197 insertions(+), 25 deletions(-) rename prisma/migrations/{20221213134247_init => 20221214094628_init}/migration.sql (75%) create mode 100644 src/modules/users/commands/create-user.command.ts create mode 100644 src/modules/users/commands/update-user.command.ts create mode 100644 src/modules/users/domain/dto/create-user.request.ts create mode 100644 src/modules/users/domain/dto/find-user-by-uuid.request.ts delete mode 100644 src/modules/users/domain/dto/findUserByUuidRequest.ts create mode 100644 src/modules/users/domain/dto/update-user.request.ts create mode 100644 src/modules/users/domain/usecases/create-user.usecase.ts create mode 100644 src/modules/users/domain/usecases/update-user.usecase.ts diff --git a/prisma/migrations/20221213134247_init/migration.sql b/prisma/migrations/20221214094628_init/migration.sql similarity index 75% rename from prisma/migrations/20221213134247_init/migration.sql rename to prisma/migrations/20221214094628_init/migration.sql index bd8896f..9f7f21e 100644 --- a/prisma/migrations/20221213134247_init/migration.sql +++ b/prisma/migrations/20221214094628_init/migration.sql @@ -8,3 +8,6 @@ CREATE TABLE "user" ( -- CreateIndex CREATE UNIQUE INDEX "user_uuid_key" ON "user"("uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "user"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 990312b..e6c2677 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,7 +14,7 @@ model User { uuid String @unique @default(uuid()) @db.Uuid firstName String lastName String - email String + email String @unique @@map("user") } diff --git a/src/app.module.ts b/src/app.module.ts index 3d6b766..b919317 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,15 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { UsersModule } from './modules/users/users.module'; @Module({ - imports: [ConfigModule.forRoot({ isGlobal: true }), UsersModule], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + AutomapperModule.forRoot({ strategyInitializer: classes() }), + UsersModule, + ], controllers: [], providers: [], }) diff --git a/src/modules/database/src/exceptions/DatabaseException.ts b/src/modules/database/src/exceptions/DatabaseException.ts index aa472bd..b0782a6 100644 --- a/src/modules/database/src/exceptions/DatabaseException.ts +++ b/src/modules/database/src/exceptions/DatabaseException.ts @@ -9,7 +9,6 @@ export class DatabaseException implements Error { ) { this.name = 'DatabaseException'; this.message = message ?? 'An error occured with the database.'; - if (this.message.includes('Unique constraint failed')) { this.message = 'Already exists.'; } diff --git a/src/modules/users/adapters/primaries/user.proto b/src/modules/users/adapters/primaries/user.proto index c894ef9..e59db4b 100644 --- a/src/modules/users/adapters/primaries/user.proto +++ b/src/modules/users/adapters/primaries/user.proto @@ -5,7 +5,8 @@ package user; service UsersService { rpc FindOneByUuid(UserByUuid) returns (User); rpc FindAll(UserFilter) returns (Users); - rpc Create(CreateUser) returns (User); + rpc Create(User) returns (User); + rpc Update(User) returns (User); } message UserByUuid { @@ -19,13 +20,6 @@ message User { string email = 4; } -message CreateUser { - string uuid = 1; - string firstName = 2; - string lastName = 3; - string email = 4; -} - message UserFilter {} message Users { diff --git a/src/modules/users/adapters/primaries/users.controller.ts b/src/modules/users/adapters/primaries/users.controller.ts index 8885642..00445cd 100644 --- a/src/modules/users/adapters/primaries/users.controller.ts +++ b/src/modules/users/adapters/primaries/users.controller.ts @@ -1,25 +1,80 @@ -import { Controller } from '@nestjs/common'; -import { QueryBus } from '@nestjs/cqrs'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + BadRequestException, + Body, + ConflictException, + Controller, + NotFoundException, +} from '@nestjs/common'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { FindUserByUuidRequest } from '../../domain/dto/findUserByUuidRequest'; +import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; +import { CreateUserCommand } from '../../commands/create-user.command'; +import { UpdateUserCommand } from '../../commands/update-user.command'; +import { CreateUserRequest } from '../../domain/dto/create-user.request'; +import { FindUserByUuidRequest } from '../../domain/dto/find-user-by-uuid.request'; +import { UpdateUserRequest } from '../../domain/dto/update-user.request'; import { User } from '../../domain/entities/user'; import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query'; +import { UserPresenter } from './user.presenter'; @Controller() export class UsersController { - constructor(private readonly _queryBus: QueryBus) {} + constructor( + private readonly _commandBus: CommandBus, + private readonly _queryBus: QueryBus, + @InjectMapper() private readonly _mapper: Mapper, + ) {} @GrpcMethod('UsersService', 'FindOneByUuid') - async findOneByUuid(data: FindUserByUuidRequest): Promise { + async findOneByUuid(data: FindUserByUuidRequest): Promise { const user = await this._queryBus.execute( new FindUserByUuidQuery(data.uuid), ); if (user) { - return user; + return this._mapper.map(user, User, UserPresenter); } throw new RpcException({ code: 5, message: 'User not found', }); } + + @GrpcMethod('UsersService', 'Create') + async createUser(data: CreateUserRequest): Promise { + try { + const user = await this._commandBus.execute(new CreateUserCommand(data)); + return this._mapper.map(user, User, UserPresenter); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('Already exists')) { + throw new RpcException({ + code: 6, + message: 'User already exists', + }); + } + } + throw new RpcException({}); + } + } + + @GrpcMethod('UsersService', 'Update') + async updateUser(data: UpdateUserRequest): Promise { + try { + const user = await this._commandBus.execute(new UpdateUserCommand(data)); + + return this._mapper.map(user, User, UserPresenter); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('not found')) { + throw new RpcException({ + code: 5, + message: 'User not found', + }); + } + } + throw new RpcException({}); + } + } } diff --git a/src/modules/users/commands/create-user.command.ts b/src/modules/users/commands/create-user.command.ts new file mode 100644 index 0000000..60c7160 --- /dev/null +++ b/src/modules/users/commands/create-user.command.ts @@ -0,0 +1,9 @@ +import { CreateUserRequest } from '../domain/dto/create-user.request'; + +export class CreateUserCommand { + readonly createUserRequest: CreateUserRequest; + + constructor(request: CreateUserRequest) { + this.createUserRequest = request; + } +} diff --git a/src/modules/users/commands/update-user.command.ts b/src/modules/users/commands/update-user.command.ts new file mode 100644 index 0000000..434e83b --- /dev/null +++ b/src/modules/users/commands/update-user.command.ts @@ -0,0 +1,9 @@ +import { UpdateUserRequest } from '../domain/dto/update-user.request'; + +export class UpdateUserCommand { + readonly updateUserRequest: UpdateUserRequest; + + constructor(request: UpdateUserRequest) { + this.updateUserRequest = request; + } +} diff --git a/src/modules/users/domain/dto/create-user.request.ts b/src/modules/users/domain/dto/create-user.request.ts new file mode 100644 index 0000000..f836448 --- /dev/null +++ b/src/modules/users/domain/dto/create-user.request.ts @@ -0,0 +1,23 @@ +import { AutoMap } from '@automapper/classes'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateUserRequest { + @IsString() + @AutoMap() + uuid: string; + + @IsString() + @IsNotEmpty() + @AutoMap() + firstName: string; + + @IsString() + @IsNotEmpty() + @AutoMap() + lastName: string; + + @IsString() + @IsNotEmpty() + @AutoMap() + email: string; +} diff --git a/src/modules/users/domain/dto/find-user-by-uuid.request.ts b/src/modules/users/domain/dto/find-user-by-uuid.request.ts new file mode 100644 index 0000000..2e2a70a --- /dev/null +++ b/src/modules/users/domain/dto/find-user-by-uuid.request.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class FindUserByUuidRequest { + @IsString() + @IsNotEmpty() + uuid: string; +} diff --git a/src/modules/users/domain/dto/findUserByUuidRequest.ts b/src/modules/users/domain/dto/findUserByUuidRequest.ts deleted file mode 100644 index 6b3ffb9..0000000 --- a/src/modules/users/domain/dto/findUserByUuidRequest.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsString } from 'class-validator'; - -export class FindUserByUuidRequest { - @IsString() - uuid: string; -} diff --git a/src/modules/users/domain/dto/update-user.request.ts b/src/modules/users/domain/dto/update-user.request.ts new file mode 100644 index 0000000..e18cc6b --- /dev/null +++ b/src/modules/users/domain/dto/update-user.request.ts @@ -0,0 +1,3 @@ +import { CreateUserRequest } from './create-user.request'; + +export class UpdateUserRequest extends CreateUserRequest {} diff --git a/src/modules/users/domain/usecases/create-user.usecase.ts b/src/modules/users/domain/usecases/create-user.usecase.ts new file mode 100644 index 0000000..b37c600 --- /dev/null +++ b/src/modules/users/domain/usecases/create-user.usecase.ts @@ -0,0 +1,25 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { CommandHandler } from '@nestjs/cqrs'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { CreateUserCommand } from '../../commands/create-user.command'; +import { CreateUserRequest } from '../dto/create-user.request'; +import { User } from '../entities/user'; + +@CommandHandler(CreateUserCommand) +export class CreateUserUseCase { + constructor( + private readonly _repository: UsersRepository, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + async execute(command: CreateUserCommand): Promise { + const entity = this._mapper.map( + command.createUserRequest, + CreateUserRequest, + User, + ); + + return this._repository.create(entity); + } +} diff --git a/src/modules/users/domain/usecases/update-user.usecase.ts b/src/modules/users/domain/usecases/update-user.usecase.ts new file mode 100644 index 0000000..3605913 --- /dev/null +++ b/src/modules/users/domain/usecases/update-user.usecase.ts @@ -0,0 +1,25 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { CommandHandler } from '@nestjs/cqrs'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { UpdateUserCommand } from '../../commands/update-user.command'; +import { UpdateUserRequest } from '../dto/update-user.request'; +import { User } from '../entities/user'; + +@CommandHandler(UpdateUserCommand) +export class UpdateUserUseCase { + constructor( + private readonly _repository: UsersRepository, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + + async execute(command: UpdateUserCommand): Promise { + const entity = this._mapper.map( + command.updateUserRequest, + UpdateUserRequest, + User, + ); + + return this._repository.update(command.updateUserRequest.uuid, entity); + } +} diff --git a/src/modules/users/mappers/user.profile.ts b/src/modules/users/mappers/user.profile.ts index b2fc581..b681625 100644 --- a/src/modules/users/mappers/user.profile.ts +++ b/src/modules/users/mappers/user.profile.ts @@ -1,7 +1,9 @@ -import { createMap, Mapper } from '@automapper/core'; +import { createMap, forMember, ignore, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { UserPresenter } from '../adapters/primaries/user.presenter'; +import { CreateUserRequest } from '../domain/dto/create-user.request'; +import { UpdateUserRequest } from '../domain/dto/update-user.request'; import { User } from '../domain/entities/user'; @Injectable() @@ -13,6 +15,15 @@ export class UserProfile extends AutomapperProfile { override get profile() { return (mapper) => { createMap(mapper, User, UserPresenter); + + createMap(mapper, CreateUserRequest, User); + + createMap( + mapper, + UpdateUserRequest, + User, + forMember((dest) => dest.uuid, ignore()), + ); }; } } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 8c95b25..01a8bd5 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -3,12 +3,21 @@ import { CqrsModule } from '@nestjs/cqrs'; import { DatabaseModule } from '../database/database.module'; import { UsersController } from './adapters/primaries/users.controller'; import { UsersRepository } from './adapters/secondaries/users.repository'; +import { CreateUserUseCase } from './domain/usecases/create-user.usecase'; import { FindUserByUuidUseCase } from './domain/usecases/find-user-by-uuid.usecase'; +import { UpdateUserUseCase } from './domain/usecases/update-user.usecase'; +import { UserProfile } from './mappers/user.profile'; @Module({ imports: [DatabaseModule, CqrsModule], controllers: [UsersController], - providers: [UsersRepository, FindUserByUuidUseCase], + providers: [ + UserProfile, + UsersRepository, + FindUserByUuidUseCase, + CreateUserUseCase, + UpdateUserUseCase, + ], exports: [], }) export class UsersModule {} From 95555c476b51ec09b08335664b821453035361ef Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 14 Dec 2022 11:53:50 +0100 Subject: [PATCH 2/4] add tests --- .../adapters/primaries/users.controller.ts | 8 +-- .../users/domain/dto/update-user.request.ts | 21 +++++- .../tests/unit/create-user.usecase.spec.ts | 57 ++++++++++++++++ .../unit/find-user-by-uuid.usecase.spec.ts | 1 - .../tests/unit/update-user.usecase.spec.ts | 65 +++++++++++++++++++ 5 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 src/modules/users/tests/unit/create-user.usecase.spec.ts create mode 100644 src/modules/users/tests/unit/update-user.usecase.spec.ts diff --git a/src/modules/users/adapters/primaries/users.controller.ts b/src/modules/users/adapters/primaries/users.controller.ts index 00445cd..697c99e 100644 --- a/src/modules/users/adapters/primaries/users.controller.ts +++ b/src/modules/users/adapters/primaries/users.controller.ts @@ -1,12 +1,6 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { - BadRequestException, - Body, - ConflictException, - Controller, - NotFoundException, -} from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; diff --git a/src/modules/users/domain/dto/update-user.request.ts b/src/modules/users/domain/dto/update-user.request.ts index e18cc6b..96c57ef 100644 --- a/src/modules/users/domain/dto/update-user.request.ts +++ b/src/modules/users/domain/dto/update-user.request.ts @@ -1,3 +1,20 @@ -import { CreateUserRequest } from './create-user.request'; +import { AutoMap } from '@automapper/classes'; +import { IsString } from 'class-validator'; -export class UpdateUserRequest extends CreateUserRequest {} +export class UpdateUserRequest { + @IsString() + @AutoMap() + uuid: string; + + @IsString() + @AutoMap() + firstName?: string; + + @IsString() + @AutoMap() + lastName?: string; + + @IsString() + @AutoMap() + email?: string; +} diff --git a/src/modules/users/tests/unit/create-user.usecase.spec.ts b/src/modules/users/tests/unit/create-user.usecase.spec.ts new file mode 100644 index 0000000..749a703 --- /dev/null +++ b/src/modules/users/tests/unit/create-user.usecase.spec.ts @@ -0,0 +1,57 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { CreateUserCommand } from '../../commands/create-user.command'; +import { CreateUserRequest } from '../../domain/dto/create-user.request'; +import { User } from '../../domain/entities/user'; +import { CreateUserUseCase } from '../../domain/usecases/create-user.usecase'; +import { UserProfile } from '../../mappers/user.profile'; + +const newUserRequest: CreateUserRequest = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', +}; +const newUserCommand: CreateUserCommand = new CreateUserCommand(newUserRequest); + +const mockUsersRepository = { + create: jest.fn().mockResolvedValue({ + ...newUserRequest, + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + }), +}; + +describe('CreateUserUseCase', () => { + let createUserUseCase: CreateUserUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: UsersRepository, + useValue: mockUsersRepository, + }, + CreateUserUseCase, + UserProfile, + ], + }).compile(); + + createUserUseCase = module.get(CreateUserUseCase); + }); + + it('should be defined', () => { + expect(createUserUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should create an User and returns new entity object', async () => { + const newUser: User = await createUserUseCase.execute(newUserCommand); + + expect(newUser.lastName).toBe(newUserRequest.lastName); + expect(newUser.uuid).toBeDefined(); + }); + }); +}); diff --git a/src/modules/users/tests/unit/find-user-by-uuid.usecase.spec.ts b/src/modules/users/tests/unit/find-user-by-uuid.usecase.spec.ts index 2ad1f16..b847d7f 100644 --- a/src/modules/users/tests/unit/find-user-by-uuid.usecase.spec.ts +++ b/src/modules/users/tests/unit/find-user-by-uuid.usecase.spec.ts @@ -28,7 +28,6 @@ describe('FindUserByUuidUseCase', () => { provide: UsersRepository, useValue: mockUserRepository, }, - FindUserByUuidUseCase, ], }).compile(); diff --git a/src/modules/users/tests/unit/update-user.usecase.spec.ts b/src/modules/users/tests/unit/update-user.usecase.spec.ts new file mode 100644 index 0000000..140d340 --- /dev/null +++ b/src/modules/users/tests/unit/update-user.usecase.spec.ts @@ -0,0 +1,65 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { UpdateUserCommand } from '../../commands/update-user.command'; +import { UpdateUserRequest } from '../../domain/dto/update-user.request'; +import { User } from '../../domain/entities/user'; +import { UpdateUserUseCase } from '../../domain/usecases/update-user.usecase'; +import { UserProfile } from '../../mappers/user.profile'; + +const originalUser: User = new User(); +originalUser.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +originalUser.lastName = 'Doe'; + +const updateUserRequest: UpdateUserRequest = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + lastName: 'Dane', +}; + +const updateUserCommand: UpdateUserCommand = new UpdateUserCommand( + updateUserRequest, +); + +const mockUsersRepository = { + update: jest.fn().mockImplementation((uuid: string, params: any) => { + originalUser.lastName = params.lastName; + + return Promise.resolve(originalUser); + }), +}; + +describe('UpdateUserUseCase', () => { + let updateUserUseCase: UpdateUserUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + + providers: [ + { + provide: UsersRepository, + useValue: mockUsersRepository, + }, + UpdateUserUseCase, + UserProfile, + ], + }).compile(); + + updateUserUseCase = module.get(UpdateUserUseCase); + }); + + it('should be defined', () => { + expect(updateUserUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should update an User', async () => { + const updatedUser: User = await updateUserUseCase.execute( + updateUserCommand, + ); + + expect(updatedUser.lastName).toBe(updateUserRequest.lastName); + }); + }); +}); From c410ad05ee0bd1a65be9668a80daf054e7de9a48 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 14 Dec 2022 12:10:27 +0100 Subject: [PATCH 3/4] add createdAt, updatedAt --- .../migration.sql | 9 +++++---- prisma/schema.prisma | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) rename prisma/migrations/{20221214094628_init => 20221214110358_init}/migration.sql (52%) diff --git a/prisma/migrations/20221214094628_init/migration.sql b/prisma/migrations/20221214110358_init/migration.sql similarity index 52% rename from prisma/migrations/20221214094628_init/migration.sql rename to prisma/migrations/20221214110358_init/migration.sql index 9f7f21e..eea1424 100644 --- a/prisma/migrations/20221214094628_init/migration.sql +++ b/prisma/migrations/20221214110358_init/migration.sql @@ -3,11 +3,12 @@ CREATE TABLE "user" ( "uuid" UUID NOT NULL, "firstName" TEXT NOT NULL, "lastName" TEXT NOT NULL, - "email" TEXT NOT NULL + "email" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "user_pkey" PRIMARY KEY ("uuid") ); --- CreateIndex -CREATE UNIQUE INDEX "user_uuid_key" ON "user"("uuid"); - -- CreateIndex CREATE UNIQUE INDEX "user_email_key" ON "user"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e6c2677..5c56f48 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,10 +11,12 @@ datasource db { } model User { - uuid String @unique @default(uuid()) @db.Uuid + uuid String @id @default(uuid()) @db.Uuid firstName String lastName String - email String @unique + email String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("user") } From ed09ceea1508dfc6c657aa1230e54102640cabef Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 14 Dec 2022 12:49:43 +0100 Subject: [PATCH 4/4] delete user --- .../users/adapters/primaries/user.proto | 3 + .../adapters/primaries/users.controller.ts | 23 +++++++ .../users/commands/delete-user.command.ts | 7 ++ .../domain/usecases/delete-user.usecase.ts | 12 ++++ .../tests/unit/delete-user.usecase.spec.ts | 69 +++++++++++++++++++ src/modules/users/users.module.ts | 2 + 6 files changed, 116 insertions(+) create mode 100644 src/modules/users/commands/delete-user.command.ts create mode 100644 src/modules/users/domain/usecases/delete-user.usecase.ts create mode 100644 src/modules/users/tests/unit/delete-user.usecase.spec.ts diff --git a/src/modules/users/adapters/primaries/user.proto b/src/modules/users/adapters/primaries/user.proto index e59db4b..6955c19 100644 --- a/src/modules/users/adapters/primaries/user.proto +++ b/src/modules/users/adapters/primaries/user.proto @@ -7,6 +7,7 @@ service UsersService { rpc FindAll(UserFilter) returns (Users); rpc Create(User) returns (User); rpc Update(User) returns (User); + rpc Delete(UserByUuid) returns (Empty); } message UserByUuid { @@ -25,3 +26,5 @@ message UserFilter {} message Users { repeated User users = 1; } + +message Empty {} diff --git a/src/modules/users/adapters/primaries/users.controller.ts b/src/modules/users/adapters/primaries/users.controller.ts index 697c99e..df5636f 100644 --- a/src/modules/users/adapters/primaries/users.controller.ts +++ b/src/modules/users/adapters/primaries/users.controller.ts @@ -5,6 +5,7 @@ import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; import { CreateUserCommand } from '../../commands/create-user.command'; +import { DeleteUserCommand } from '../../commands/delete-user.command'; import { UpdateUserCommand } from '../../commands/update-user.command'; import { CreateUserRequest } from '../../domain/dto/create-user.request'; import { FindUserByUuidRequest } from '../../domain/dto/find-user-by-uuid.request'; @@ -71,4 +72,26 @@ export class UsersController { throw new RpcException({}); } } + + @GrpcMethod('UsersService', 'Delete') + async deleteUser(data: FindUserByUuidRequest): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const user = await this._commandBus.execute( + new DeleteUserCommand(data.uuid), + ); + + return Promise.resolve(); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('not found')) { + throw new RpcException({ + code: 5, + message: 'User not found', + }); + } + } + throw new RpcException({}); + } + } } diff --git a/src/modules/users/commands/delete-user.command.ts b/src/modules/users/commands/delete-user.command.ts new file mode 100644 index 0000000..2e69a67 --- /dev/null +++ b/src/modules/users/commands/delete-user.command.ts @@ -0,0 +1,7 @@ +export class DeleteUserCommand { + readonly uuid: string; + + constructor(uuid: string) { + this.uuid = uuid; + } +} diff --git a/src/modules/users/domain/usecases/delete-user.usecase.ts b/src/modules/users/domain/usecases/delete-user.usecase.ts new file mode 100644 index 0000000..4378b61 --- /dev/null +++ b/src/modules/users/domain/usecases/delete-user.usecase.ts @@ -0,0 +1,12 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { DeleteUserCommand } from '../../commands/delete-user.command'; + +@CommandHandler(DeleteUserCommand) +export class DeleteUserUseCase { + constructor(private readonly _repository: UsersRepository) {} + + async execute(command: DeleteUserCommand): Promise { + return this._repository.delete(command.uuid); + } +} diff --git a/src/modules/users/tests/unit/delete-user.usecase.spec.ts b/src/modules/users/tests/unit/delete-user.usecase.spec.ts new file mode 100644 index 0000000..b88fd9f --- /dev/null +++ b/src/modules/users/tests/unit/delete-user.usecase.spec.ts @@ -0,0 +1,69 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { DeleteUserCommand } from '../../commands/delete-user.command'; +import { DeleteUserUseCase } from '../../domain/usecases/delete-user.usecase'; + +const usersMock = [ + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a92', + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@email.com', + }, + { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a93', + firstName: 'Jimmy', + lastName: 'Doe', + email: 'jimmy.doe@email.com', + }, +]; + +const mockUsersRepository = { + delete: jest.fn().mockImplementation((uuid: string) => { + usersMock.forEach((user, index) => { + if (user.uuid === uuid) { + usersMock.splice(index, 1); + return Promise.resolve(); + } + }); + }), +}; + +describe('DeleteUserUseCase', () => { + let deleteUserUseCase: DeleteUserUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: UsersRepository, + useValue: mockUsersRepository, + }, + DeleteUserUseCase, + ], + }).compile(); + + deleteUserUseCase = module.get(DeleteUserUseCase); + }); + + it('should be defined', () => { + expect(deleteUserUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should delete an User', async () => { + const savedUuid = usersMock[0].uuid; + const deleteUserCommand = new DeleteUserCommand(savedUuid); + await deleteUserUseCase.execute(deleteUserCommand); + + const deletedUser = usersMock.find((user) => user.uuid === savedUuid); + expect(deletedUser).toBeUndefined(); + }); + }); +}); diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 01a8bd5..2668167 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -4,6 +4,7 @@ import { DatabaseModule } from '../database/database.module'; import { UsersController } from './adapters/primaries/users.controller'; import { UsersRepository } from './adapters/secondaries/users.repository'; import { CreateUserUseCase } from './domain/usecases/create-user.usecase'; +import { DeleteUserUseCase } from './domain/usecases/delete-user.usecase'; import { FindUserByUuidUseCase } from './domain/usecases/find-user-by-uuid.usecase'; import { UpdateUserUseCase } from './domain/usecases/update-user.usecase'; import { UserProfile } from './mappers/user.profile'; @@ -17,6 +18,7 @@ import { UserProfile } from './mappers/user.profile'; FindUserByUuidUseCase, CreateUserUseCase, UpdateUserUseCase, + DeleteUserUseCase, ], exports: [], })