Merge branch 'crud' into 'main'

Crud

See merge request mobicoop/lab/v3/services/user!1
This commit is contained in:
Gsk54 2022-12-14 11:50:16 +00:00
commit ff3ac73ecb
23 changed files with 459 additions and 36 deletions

View File

@ -1,10 +0,0 @@
-- CreateTable
CREATE TABLE "user" (
"uuid" UUID NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"email" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "user_uuid_key" ON "user"("uuid");

View File

@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "user" (
"uuid" UUID NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" 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_email_key" ON "user"("email");

View File

@ -11,10 +11,12 @@ datasource db {
} }
model User { model User {
uuid String @unique @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
firstName String firstName String
lastName String lastName String
email String email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("user") @@map("user")
} }

View File

@ -1,9 +1,15 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { UsersModule } from './modules/users/users.module'; import { UsersModule } from './modules/users/users.module';
@Module({ @Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), UsersModule], imports: [
ConfigModule.forRoot({ isGlobal: true }),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
UsersModule,
],
controllers: [], controllers: [],
providers: [], providers: [],
}) })

View File

@ -9,7 +9,6 @@ export class DatabaseException implements Error {
) { ) {
this.name = 'DatabaseException'; this.name = 'DatabaseException';
this.message = message ?? 'An error occured with the database.'; this.message = message ?? 'An error occured with the database.';
if (this.message.includes('Unique constraint failed')) { if (this.message.includes('Unique constraint failed')) {
this.message = 'Already exists.'; this.message = 'Already exists.';
} }

View File

@ -5,7 +5,9 @@ package user;
service UsersService { service UsersService {
rpc FindOneByUuid(UserByUuid) returns (User); rpc FindOneByUuid(UserByUuid) returns (User);
rpc FindAll(UserFilter) returns (Users); rpc FindAll(UserFilter) returns (Users);
rpc Create(CreateUser) returns (User); rpc Create(User) returns (User);
rpc Update(User) returns (User);
rpc Delete(UserByUuid) returns (Empty);
} }
message UserByUuid { message UserByUuid {
@ -19,15 +21,10 @@ message User {
string email = 4; string email = 4;
} }
message CreateUser {
string uuid = 1;
string firstName = 2;
string lastName = 3;
string email = 4;
}
message UserFilter {} message UserFilter {}
message Users { message Users {
repeated User users = 1; repeated User users = 1;
} }
message Empty {}

View File

@ -1,25 +1,97 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { Controller } from '@nestjs/common'; import { Controller } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs'; import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; 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 { 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';
import { UpdateUserRequest } from '../../domain/dto/update-user.request';
import { User } from '../../domain/entities/user'; import { User } from '../../domain/entities/user';
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query'; import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
import { UserPresenter } from './user.presenter';
@Controller() @Controller()
export class UsersController { 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') @GrpcMethod('UsersService', 'FindOneByUuid')
async findOneByUuid(data: FindUserByUuidRequest): Promise<User> { async findOneByUuid(data: FindUserByUuidRequest): Promise<UserPresenter> {
const user = await this._queryBus.execute( const user = await this._queryBus.execute(
new FindUserByUuidQuery(data.uuid), new FindUserByUuidQuery(data.uuid),
); );
if (user) { if (user) {
return user; return this._mapper.map(user, User, UserPresenter);
} }
throw new RpcException({ throw new RpcException({
code: 5, code: 5,
message: 'User not found', message: 'User not found',
}); });
} }
@GrpcMethod('UsersService', 'Create')
async createUser(data: CreateUserRequest): Promise<UserPresenter> {
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<UserPresenter> {
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({});
}
}
@GrpcMethod('UsersService', 'Delete')
async deleteUser(data: FindUserByUuidRequest): Promise<void> {
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({});
}
}
} }

View File

@ -0,0 +1,9 @@
import { CreateUserRequest } from '../domain/dto/create-user.request';
export class CreateUserCommand {
readonly createUserRequest: CreateUserRequest;
constructor(request: CreateUserRequest) {
this.createUserRequest = request;
}
}

View File

@ -0,0 +1,7 @@
export class DeleteUserCommand {
readonly uuid: string;
constructor(uuid: string) {
this.uuid = uuid;
}
}

View File

@ -0,0 +1,9 @@
import { UpdateUserRequest } from '../domain/dto/update-user.request';
export class UpdateUserCommand {
readonly updateUserRequest: UpdateUserRequest;
constructor(request: UpdateUserRequest) {
this.updateUserRequest = request;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class FindUserByUuidRequest {
@IsString()
@IsNotEmpty()
uuid: string;
}

View File

@ -1,6 +0,0 @@
import { IsString } from 'class-validator';
export class FindUserByUuidRequest {
@IsString()
uuid: string;
}

View File

@ -0,0 +1,20 @@
import { AutoMap } from '@automapper/classes';
import { IsString } from 'class-validator';
export class UpdateUserRequest {
@IsString()
@AutoMap()
uuid: string;
@IsString()
@AutoMap()
firstName?: string;
@IsString()
@AutoMap()
lastName?: string;
@IsString()
@AutoMap()
email?: string;
}

View File

@ -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<User> {
const entity = this._mapper.map(
command.createUserRequest,
CreateUserRequest,
User,
);
return this._repository.create(entity);
}
}

View File

@ -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<void> {
return this._repository.delete(command.uuid);
}
}

View File

@ -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<User> {
const entity = this._mapper.map(
command.updateUserRequest,
UpdateUserRequest,
User,
);
return this._repository.update(command.updateUserRequest.uuid, entity);
}
}

View File

@ -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 { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UserPresenter } from '../adapters/primaries/user.presenter'; 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'; import { User } from '../domain/entities/user';
@Injectable() @Injectable()
@ -13,6 +15,15 @@ export class UserProfile extends AutomapperProfile {
override get profile() { override get profile() {
return (mapper) => { return (mapper) => {
createMap(mapper, User, UserPresenter); createMap(mapper, User, UserPresenter);
createMap(mapper, CreateUserRequest, User);
createMap(
mapper,
UpdateUserRequest,
User,
forMember((dest) => dest.uuid, ignore()),
);
}; };
} }
} }

View File

@ -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>(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();
});
});
});

View File

@ -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>(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();
});
});
});

View File

@ -28,7 +28,6 @@ describe('FindUserByUuidUseCase', () => {
provide: UsersRepository, provide: UsersRepository,
useValue: mockUserRepository, useValue: mockUserRepository,
}, },
FindUserByUuidUseCase, FindUserByUuidUseCase,
], ],
}).compile(); }).compile();

View File

@ -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>(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);
});
});
});

View File

@ -3,12 +3,23 @@ import { CqrsModule } from '@nestjs/cqrs';
import { DatabaseModule } from '../database/database.module'; import { DatabaseModule } from '../database/database.module';
import { UsersController } from './adapters/primaries/users.controller'; import { UsersController } from './adapters/primaries/users.controller';
import { UsersRepository } from './adapters/secondaries/users.repository'; 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 { FindUserByUuidUseCase } from './domain/usecases/find-user-by-uuid.usecase';
import { UpdateUserUseCase } from './domain/usecases/update-user.usecase';
import { UserProfile } from './mappers/user.profile';
@Module({ @Module({
imports: [DatabaseModule, CqrsModule], imports: [DatabaseModule, CqrsModule],
controllers: [UsersController], controllers: [UsersController],
providers: [UsersRepository, FindUserByUuidUseCase], providers: [
UserProfile,
UsersRepository,
FindUserByUuidUseCase,
CreateUserUseCase,
UpdateUserUseCase,
DeleteUserUseCase,
],
exports: [], exports: [],
}) })
export class UsersModule {} export class UsersModule {}