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 {}