Skip to content

Commit

Permalink
[GH-38] feat(User): add value objects
Browse files Browse the repository at this point in the history
  • Loading branch information
pablojvritx committed Dec 15, 2023
1 parent c28f4d7 commit d712916
Show file tree
Hide file tree
Showing 23 changed files with 528 additions and 3 deletions.
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"swagger-ui-express": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"uuid": "^9.0.1",
"uuid-validate": "^0.0.3",
"winston": "^3.11.0",
"winston-mongodb": "^5.1.1",
Expand Down
4 changes: 4 additions & 0 deletions src/Contexts/apiApp/Auth/application/LoginUserRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface LoginUserRequest {
email: string;
password: string;
}
5 changes: 5 additions & 0 deletions src/Contexts/apiApp/Auth/application/RegisterUserRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LoginUserRequest } from './LoginUserRequest';

export interface RegisterUserRequest extends LoginUserRequest {
username: string;
}
75 changes: 75 additions & 0 deletions src/Contexts/apiApp/Auth/domain/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { AggregateRoot } from '../../../shared/domain/AggregateRoot';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';
import { Uuid } from '../../../shared/domain/value-object/Uuid';
import { Email } from '../../../shared/domain/value-object/Email';
import { UserRoles } from './UserRoles';
import { Username } from './Username';

export class User extends AggregateRoot {
readonly id: Uuid;
readonly email: Email;
readonly username: Username;
readonly password: StringValueObject;
readonly emailValidated: boolean;
readonly roles: UserRoles;

constructor({
id,
email,
username,
password,
emailValidated,
roles
}: {
id: Uuid;
email: Email;
username: Username;
password: StringValueObject;
emailValidated: boolean;
roles: UserRoles;
}) {
super();
this.id = id;
this.email = email;
this.username = username;
this.password = password;
this.emailValidated = emailValidated;
this.roles = roles;
}

toPrimitives(): Record<string, unknown> {
return {
id: this.id.value,
email: this.email.value,
username: this.username.value,
password: this.password.value,
emailValidated: this.emailValidated,
roles: this.roles.value
};
}

static fromPrimitives({
id,
email,
username,
password,
emailValidated,
roles
}: {
id: string;
email: string;
username: string;
password: string;
emailValidated: boolean;
roles: string[];
}): User {
return new User({
id: new Uuid(id),
email: new Email(email),
username: new Username(username),
password: new StringValueObject(password),
emailValidated,
roles: new UserRoles(roles)
});
}
}
57 changes: 57 additions & 0 deletions src/Contexts/apiApp/Auth/domain/UserPatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AggregateRoot } from '../../../shared/domain/AggregateRoot';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';
import { Uuid } from '../../../shared/domain/value-object/Uuid';
import { UserRoles } from './UserRoles';

export class UserPatch extends AggregateRoot {
readonly id: Uuid;
readonly password?: StringValueObject;
readonly emailValidated?: boolean;
readonly roles?: UserRoles;

constructor({
id,
password,
emailValidated,
roles
}: {
id: Uuid;
password?: StringValueObject;
emailValidated?: boolean;
roles?: UserRoles;
}) {
super();
this.id = id;
password && (this.password = password);
emailValidated && (this.emailValidated = emailValidated);
roles && (this.roles = roles);
}

toPrimitives() {
return {
id: this.id.value,
...(this.password?.value && { password: this.password.value }),
...(this.emailValidated && { emailValidated: this.emailValidated }),
...(this.roles?.value && { roles: this.roles.value })
};
}

static fromPrimitives({
id,
password,
emailValidated,
roles
}: {
id: string;
password?: string;
emailValidated?: boolean;
roles?: string[];
}) {
return new UserPatch({
id: new Uuid(id),
...(password && { password: new StringValueObject(password) }),
...(emailValidated && { emailValidated }),
...(roles && { roles: new UserRoles(roles) })
});
}
}
11 changes: 11 additions & 0 deletions src/Contexts/apiApp/Auth/domain/UserRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Nullable } from '../../../shared/domain/Nullable';
import { User } from './User';
import { UserPatch } from './UserPatch';

export interface UserRepository {
save(user: User): Promise<void>;

update(user: UserPatch): Promise<void>;

search(email: string): Promise<Nullable<User>>;
}
21 changes: 21 additions & 0 deletions src/Contexts/apiApp/Auth/domain/UserRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError';

export class UserRoles {
private static validRoles = ['admin', 'user'];
readonly value: string[];

constructor(value: string[]) {
this.ensureRoles(value);
this.value = value;
}

private ensureRoles(value: string[]): void {
value.forEach((role) => {
if (!UserRoles.validRoles.includes(role)) {
throw new InvalidArgumentError(
`<${this.constructor.name}> does not allow the value <${role}>`
);
}
});
}
}
25 changes: 25 additions & 0 deletions src/Contexts/apiApp/Auth/domain/Username.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError';
import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject';

export class Username extends StringValueObject {
constructor(value: string) {
super(value);
this.ensureLength(value);
}

private ensureLength(value: string): string {
if (value.length < 4) {
throw new InvalidArgumentError(
'<Username> must be at least 4 characters long'
);
}

if (value.length > 20) {
throw new InvalidArgumentError(
'<Username> must be less than 20 characters long'
);
}

return value;
}
}
7 changes: 7 additions & 0 deletions src/Contexts/apiApp/Auth/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './User';
export * from '../../../shared/domain/value-object/Email';
export * from './UserRoles';
export * from './Username';
export * from './UserPatch';

export * from './UserRepository';
7 changes: 6 additions & 1 deletion src/Contexts/shared/domain/errors/InvalidArgumentError.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export class InvalidArgumentError extends Error {}
export class InvalidArgumentError extends Error {
constructor(message: string) {
super(message);
this.name = 'InvalidArgumentError';
}
}
61 changes: 61 additions & 0 deletions src/Contexts/shared/domain/value-object/Email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { InvalidArgumentError } from '../errors/InvalidArgumentError';

export class Email {
private readonly emailRegex = /^[a-zA-Z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,6}$/;
private readonly minLength = 6;
private readonly maxLength = 255;
private readonly domainsBlacklist = [
'mailinator.com',
'guerrillamail.com',
'sharklasers.com'
];
readonly value: string;

constructor(value: string) {
this.value = this.ensureIsValidEmail(value);
}

private ensureLength(value: string): string {
if (value.length < this.minLength) {
throw new InvalidArgumentError(
`<${this.constructor.name}> must be at least ${this.minLength} characters long`
);
}

if (value.length > this.maxLength) {
throw new InvalidArgumentError(
`<${this.constructor.name}> must be less than ${this.maxLength} characters long`
);
}

return value;
}

private ensureDomainsBlacklist(value: string): string {
const domain = value.split('@')[1];
if (this.domainsBlacklist.includes(domain)) {
throw new InvalidArgumentError(
`<${this.constructor.name}> does not allow the domain <${value}>`
);
}
return value;
}

private ensureIsEmailAddress(value: string): string {
if (!this.emailRegex.test(value)) {
throw new InvalidArgumentError(
`<${this.constructor.name}> does not allow the value <${value}>`
);
}
return value;
}

private ensureIsValidEmail(value: string): string {
const trimmedValue = value.trim();
this.ensureLength(trimmedValue);
this.ensureIsEmailAddress(trimmedValue);
this.ensureDomainsBlacklist(trimmedValue);

return trimmedValue;
}
}
12 changes: 11 additions & 1 deletion src/Contexts/shared/domain/value-object/StringValueObject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { InvalidArgumentError } from '../errors/InvalidArgumentError';

export class StringValueObject {
readonly value: string;

constructor(value: string) {
this.value = value;
this.value = this.ensureIsValidValue(value);
}

private ensureIsValidValue(value: string): string {
if (typeof value !== 'string') {
throw new InvalidArgumentError('Invalid value');
}

return value.trim();
}
}
6 changes: 6 additions & 0 deletions src/Contexts/shared/domain/value-object/Uuid.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import validate from 'uuid-validate';
import { v4 as uuidv4 } from 'uuid';

import { InvalidArgumentError } from '../errors/InvalidArgumentError';

export class Uuid {
Expand All @@ -13,6 +15,10 @@ export class Uuid {
return this.value;
}

static random(): Uuid {
return new Uuid(uuidv4());
}

private ensureIsValidUuid(id: string): void {
if (!validate(id)) {
throw new InvalidArgumentError(
Expand Down
Loading

0 comments on commit d712916

Please sign in to comment.