58 |
64 |
65 | {errorMessage && (
66 |
67 | ⚠️
68 | {errorMessage}
69 |
70 | )}
71 |
72 |
73 |
74 |
82 |
83 |
84 |
85 |
86 | {isAddingUser && (
87 |
92 | )}
93 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clean Architecture - "Users" Kata
2 |
3 | ## General Description
4 |
5 | ### Functional Requirements
6 |
7 | - Display list of users.
8 | - Add a new user.
9 | - A user must contain name, email, and password as required fields.
10 | - Email must be a valid email.
11 | - Password must be at least 8 characters, with at least one letter and one number.
12 | - The app should show an error if trying to add two users with the same email.
13 | - **Extra Challenge**:
14 | - Add address as part of the required information for the user (address, zip code, city).
15 | - Only one user per domain can exist (1 user with gmail, etc...).
16 |
17 | ### Development Platform
18 |
19 | - The UI will be a command line or terminal app.
20 | - All data in memory, no persistence between executions.
21 | - The data source could be an API or Database in the future.
22 | - The UI could change in the future to a web app, mobile app, or desktop app.
23 | - Business rules must be validated through unit tests.
24 | - In tests, dependencies will be replaced by manual fake dependencies.
25 | - Equality tests must also be created for value objects and entities.
26 | - Use MVP for the presentation layer.
27 |
28 | ## Part 1 - Entities
29 |
30 | - Define entities and value objects.
31 | - Business rules must be tested by creating unit tests.
32 | - Rules:
33 |
34 | - The user must have name, email, and password as required fields.
35 | - Email must be a valid email.
36 | - Password must have a minimum length of 8 characters, have at least one letter and one number.
37 | - Two instances of the same email must be equal.
38 | - Two instances of the same password must be equal in a comparison.
39 | - Two instances of user, with the same id, must be equal in a comparison.
40 |
41 | - **Extra Challenge**:
42 | - Add address as part of the required information for the user (address, zip code, city).
43 |
44 | ## Part 2 - Use Cases
45 |
46 | - Define the use cases, repositories for:
47 | - Displaying list of users.
48 | - Adding a new user.
49 | - Application rules must be tested by creating unit tests.
50 | - In unit tests, you can use manual fake objects.
51 | - Rules:
52 | - The application must not allow adding two users with the same email.
53 | - **Extra Challenge**:
54 | - Only one user per domain can exist (1 user with gmail, etc...).
55 |
56 | ## Part 3 - UI
57 | - Create a console application that invokes the use cases.
58 | - Use MVP
59 | - Define the presenter and the UI
60 | - The presenter must be conditioned by the UI it adapts
61 |
62 | ## Part 4
63 | - Modify the app to have persistence between executions using a database, locally stored file, or another solution. Maintain the domain and presentation layers without modifications.
64 | - Replace the console or terminal UI with a technology of your preference, keeping the domain and data layers unmodified.
65 | - Only the corresponding adapters should change and the composition root if necessary.
66 | - Add address as part of the necessary information for the user (address, zip code, city) and create the necessary tests.
67 | - Only one user per domain can exist (1 user with gmail.com, 1 user with outlook.com, etc.) and create the necessary tests.
--------------------------------------------------------------------------------
/packages/core/src/users/application/use-cases/AddNewUserUseCase.ts:
--------------------------------------------------------------------------------
1 | import { User } from "../../domain/entities/User";
2 | import { UserValidationError } from "../../domain/errors/UserValidationError";
3 | import type { UserRepository } from "../../domain/repositories/UserRepository";
4 | import { Address } from "../../domain/value-objects/Address.value";
5 | import { Email } from "../../domain/value-objects/Email.value";
6 | import { Id } from "../../domain/value-objects/Id.value";
7 | import { Password } from "../../domain/value-objects/Password.value";
8 | import { UserDTOMapper } from "../dto/UserDTOMapper";
9 | import type { UserInputDTO } from "../dto/UserInputDTO";
10 | import type { UserOutputDTO } from "../dto/UserOutputDTO";
11 | import {
12 | EmailAlreadyInUseError,
13 | EmailDomainAlreadyInUseError,
14 | } from "../errors/UserApplicationError";
15 |
16 | /*
17 | * Add New User (Use Case)
18 | * This use case is responsible for adding a new user to the system.
19 | * Application Rules:
20 | * - The application must not allow adding two users with the same email.
21 | * - Only one user per domain can exist (1 user with gmail, etc...).
22 | */
23 |
24 | export class AddNewUserUseCase {
25 | constructor(private readonly usersRepository: UserRepository) {}
26 |
27 | async execute(
28 | userData: UserInputDTO,
29 | ): Promise<{ user?: UserOutputDTO; errors?: (UserValidationError | Error)[] }> {
30 | const errors: (UserValidationError | EmailAlreadyInUseError | EmailDomainAlreadyInUseError)[] =
31 | [];
32 |
33 | // Validating input data
34 | const emailOrError = Email.create(userData.email);
35 | if (emailOrError instanceof UserValidationError) errors.push(emailOrError);
36 |
37 | const passwordOrError = Password.create(userData.password);
38 | if (passwordOrError instanceof UserValidationError) errors.push(passwordOrError);
39 |
40 | const { address, city, zipCode } = userData;
41 | const addressOrError = Address.create({ address, city, zipCode });
42 | if (addressOrError instanceof UserValidationError) errors.push(addressOrError);
43 |
44 | if (errors.length > 0) {
45 | return { errors };
46 | }
47 |
48 | // Validating Business Rules
49 | const emailVO = emailOrError as Email;
50 |
51 | const existingEmailUser = await this.usersRepository.findByEmail(emailVO);
52 | if (existingEmailUser) errors.push(new EmailAlreadyInUseError(emailVO.value));
53 |
54 | const emailDomain = emailVO.getDomain();
55 | const existingEmailDomain = await this.usersRepository.findByDomain(emailDomain);
56 | if (existingEmailDomain) errors.push(new EmailDomainAlreadyInUseError(emailDomain));
57 |
58 | if (errors.length > 0) {
59 | return { errors };
60 | }
61 |
62 | // Creating User Entity and saving it
63 | const userId = Id.generate();
64 | const userOrError = User.create({
65 | id: userId as Id,
66 | username: userData.username,
67 | email: emailVO,
68 | password: passwordOrError as Password,
69 | address: addressOrError as Address,
70 | });
71 |
72 | if (userOrError instanceof UserValidationError) {
73 | errors.push(userOrError);
74 | return { errors };
75 | }
76 |
77 | const userSaved = await this.usersRepository.save(userOrError as User);
78 | const userDTO = UserDTOMapper.toOutputDTO(userSaved);
79 | return { user: userDTO };
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/packages/core/src/users/infra/database/UserDatabaseRepository.ts:
--------------------------------------------------------------------------------
1 | import type { Collection, Db, MongoClient } from "mongodb";
2 | import type { User } from "../../domain/entities/User";
3 | import type { UserRepository } from "../../domain/repositories/UserRepository";
4 | import type { Email } from "../../domain/value-objects/Email.value";
5 | import type { Id } from "../../domain/value-objects/Id.value";
6 | import { UserPersistenceOnDbCorruptionError } from "../errors/UserInfraError";
7 | import type { UserModel } from "./UserDBModel";
8 | import { UserPersistenceMapper } from "./UserPersistenceMapper";
9 |
10 | export class UserDatabaseRepository implements UserRepository {
11 | private db: Db;
12 | private collection: Collection