Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/refactor 2 #6

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"pino"
]
}
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ Refactor Phase 1:
- [x] Codebase
- [x] Dependencies
- [x] Config
- [ ] Create domain layer and refactor your code to house entities and utilize factory pattern for todo and users.
- [ ] Create a physical store like API on top of the mongoose model using an adapter pattern.
- [ ] Implement Google Auth for login, using google nodejs client.
- [x] Create domain layer and refactor your code to house entities and utilize factory pattern for todo and users.
- [x] Create a physical store like API on top of the mongoose model using an adapter pattern.
- [x] Implement Google Auth for login, using google nodejs client.

Refactor Phase 2:
---

- [x] Create application services to move the logic away from controllers.
- [x] Add pagination options to API endpoints.
- [x] Add custom exceptions to stores and services and rely on exception handling to send appropriate error messages from API.
- [x] Use custom exceptions to express errors in system and log your exceptions.
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"src/logs/*",
"src/**/*.{spec,test}.ts"
],
"exec": "ts-node -r tsconfig-paths/register --transpile-only src/index.ts"
"exec": "ts-node -r tsconfig-paths/register --transpile-only src/index.ts | bunyan -o short"
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"dependencies": {
"bcrypt": "^5.1.0",
"bunyan": "^1.8.15",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cors": "^2.8.5",
Expand All @@ -22,7 +23,9 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@types/bcrypt": "^5.0.0",
"@types/bunyan": "^1.8.8",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.1",
Expand Down
10 changes: 10 additions & 0 deletions src/application/exceptions/BadRequestException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import BaseHttpException from './base/BaseHttpException';
import { HttpStatusCode } from './types';

class BadRequestException extends BaseHttpException {
constructor(description: string) {
super('Bad Request', HttpStatusCode.BAD_REQUEST, description, true);
}
}

export default BadRequestException;
10 changes: 10 additions & 0 deletions src/application/exceptions/ConflictException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import BaseHttpException from './base/BaseHttpException';
import { HttpStatusCode } from './types';

class ConflictException extends BaseHttpException {
constructor(description: string) {
super('Conflict', HttpStatusCode.CONFLICT, description, true);
}
}

export default ConflictException;
10 changes: 10 additions & 0 deletions src/application/exceptions/InternalServerException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import BaseHttpException from './base/BaseHttpException';
import { HttpStatusCode } from './types';

class InternalServerException extends BaseHttpException {
constructor(description: string) {
super('Internal Server Error', HttpStatusCode.NOT_FOUND, description, true);
}
}

export default InternalServerException;
10 changes: 10 additions & 0 deletions src/application/exceptions/NotFoundException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import BaseHttpException from './base/BaseHttpException';
import { HttpStatusCode } from './types';

class NotFoundException extends BaseHttpException {
constructor(description: string) {
super('Not Found', HttpStatusCode.NOT_FOUND, description, true);
}
}

export default NotFoundException;
10 changes: 10 additions & 0 deletions src/application/exceptions/UnAuthorizedException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import BaseHttpException from './base/BaseHttpException';
import { HttpStatusCode } from './types';

class UnAuthorizedException extends BaseHttpException {
constructor(description: string) {
super('UnAuthorized', HttpStatusCode.UnAuthorized, description, true);
}
}

export default UnAuthorizedException;
20 changes: 20 additions & 0 deletions src/application/exceptions/base/BaseHttpException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HttpStatusCode } from '../types';

class BaseHttpException extends Error {
public readonly name: string;
public readonly httpCode: HttpStatusCode;
public readonly isOperational: boolean;

constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational?: boolean) {
super(description);
Object.setPrototypeOf(this, new.target.prototype);

this.name = name;
this.httpCode = httpCode;
this.isOperational = isOperational;

Error.captureStackTrace(this);
}
}

export default BaseHttpException;
6 changes: 6 additions & 0 deletions src/application/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import NotFoundException from './NotFoundException';
import ConflictException from './ConflictException';
import UnAuthorizedException from './UnAuthorizedException';
import InternalServerException from './InternalServerException';

export { NotFoundException, ConflictException, UnAuthorizedException, InternalServerException };
8 changes: 8 additions & 0 deletions src/application/exceptions/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum HttpStatusCode {
BAD_REQUEST = 400,
UnAuthorized = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
CONFLICT = 409,
INTERNAL_SERVER = 500,
}
37 changes: 37 additions & 0 deletions src/application/use_cases/googleUser/SignInGoogleUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import GoogleUser from '@/domain/user/GoogleUser';
import IUserRepository from '@/domain/user/repository/IUserRepository';
import { IUserModelObject } from '@/domain/user/types';
import OAuth2 from '@/infra/authorization/OAuth2';

class SignInGoogleUser {
private repository: IUserRepository;
private googleClient: OAuth2;

constructor(repository: IUserRepository, googleClient: OAuth2) {
this.repository = repository;
this.googleClient = googleClient;
}

async execute(code: string) {
const res = await this.googleClient.getToken(code);
const googleUser = await this.googleClient.getGoogleUser(res.tokens.id_token);

const user = await this.repository.findByEmail(googleUser.email);
if (!user) {
const raw: IUserModelObject = {
name: googleUser.name,
email: googleUser.email,
accessToken: res.tokens.access_token,
googleId: googleUser.sub,
};

const user = await this.repository.create(raw);

return GoogleUser.create(user);
}

return GoogleUser.create(user);
}
}

export default SignInGoogleUser;
28 changes: 28 additions & 0 deletions src/application/use_cases/todo/CreateTodo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NotFoundException } from '@/application/exceptions';
import Todo from '@/domain/todo';
import ITodoRepository from '@/domain/todo/repository/ITodoRepository';
import { ITodoCreationObject } from '@/domain/todo/types';
import IUserRepository from '@/domain/user/repository/IUserRepository';

class CreateTodo {
private todoRepository: ITodoRepository;
private userRepository: IUserRepository;

constructor(todoRepository: ITodoRepository, user_repository: IUserRepository) {
this.todoRepository = todoRepository;
this.userRepository = user_repository;
}

async execute(raw: ITodoCreationObject) {
const user = await this.userRepository.find(raw.userId);
if (!user) throw new NotFoundException("User doesn't exist");

const todoM = await this.todoRepository.create(raw);

const todo = Todo.create(todoM);

return todo;
}
}

export default CreateTodo;
18 changes: 18 additions & 0 deletions src/application/use_cases/todo/PaginatedSearchTodos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Todo from '@/domain/todo';
import ITodoRepository from '@/domain/todo/repository/ITodoRepository';

class PaginatedSearchTodos {
private repository: ITodoRepository;

constructor(repository: ITodoRepository) {
this.repository = repository;
}

async execute(id: string, page: number, limit: number) {
const todos = await this.repository.paginatedSearch(id, page, limit);

return todos.map(todo => Todo.create(todo));
}
}

export default PaginatedSearchTodos;
25 changes: 25 additions & 0 deletions src/application/use_cases/user/LoginUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NotFoundException } from '@/application/exceptions';
import BadRequestException from '@/application/exceptions/BadRequestException';
import User from '@/domain/user';
import IUserRepository from '@/domain/user/repository/IUserRepository';
import { IUserCredentialsObject } from '@/domain/user/types';

class LoginUser {
private repository: IUserRepository;

constructor(repository: IUserRepository) {
this.repository = repository;
}

async execute(credentials: IUserCredentialsObject) {
const user = await this.repository.findByEmail(credentials.email.value);
if (!user) throw new NotFoundException("User doesn't exist");

const isMatch = await credentials.password.compare(user.password);
if (!isMatch) throw new BadRequestException('Wrong password');

return User.create(user);
}
}

export default LoginUser;
29 changes: 29 additions & 0 deletions src/application/use_cases/user/RegisterUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ConflictException } from '@/application/exceptions';
import User from '@/domain/user';
import IUserRepository from '@/domain/user/repository/IUserRepository';
import { IUserCreationObject, IUserModelObject } from '@/domain/user/types';

class RegisterUser {
private repository: IUserRepository;

constructor(repository: IUserRepository) {
this.repository = repository;
}

async execute(raw: IUserCreationObject) {
let user = await this.repository.findByEmail(raw.email.value);
if (user) throw new ConflictException('User already exists');

const obj: IUserModelObject = {
...raw,
email: raw.email.value,
password: await raw.password.encode(),
};

user = await this.repository.create(obj);

return User.create(user);
}
}

export default RegisterUser;
3 changes: 2 additions & 1 deletion src/domain/shared/objects/Password.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IsString, Length } from 'class-validator';
import BCrypt from '@/domain/shared/encryption/bcrypt';
import { InternalServerException } from '@/application/exceptions';

export default class Password {
@IsString() @Length(8) private password: string;
Expand All @@ -15,7 +16,7 @@ export default class Password {

get encodedValue(): string {
if (!this.encoded) {
throw new Error('Password is not encoded yet');
throw new InternalServerException('Password is not encoded yet');
}

return this.encoded;
Expand Down
35 changes: 35 additions & 0 deletions src/domain/todo/Todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ITodo, ITodoModelObject, TodoDoc } from './types';

class Todo implements ITodo {
id: string;
title: string;
description: string;
userId: string;
active: boolean;

constructor(id: string, title: string, description: string, userId: string, active: boolean) {
this.id = id;
this.title = title;
this.description = description;
this.userId = userId;
this.active = active;
}

get values(): ITodo {
return {
id: this.id,
title: this.title,
description: this.description,
userId: this.userId,
active: this.active,
};
}

static create(obj: ITodoModelObject) {
const todo = new Todo(obj._id, obj.title, obj.description, obj.userId, obj.active);

return todo;
}
}

export default Todo;
31 changes: 0 additions & 31 deletions src/domain/todo/TodoFactory.ts

This file was deleted.

13 changes: 10 additions & 3 deletions src/domain/todo/dtos/todo.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { IsBoolean, IsString } from 'class-validator';
import { ITodoCreation } from '../types';
import { Transform } from 'class-transformer';
import { IsBoolean, IsNumber, IsString } from 'class-validator';
import { ITodoCreationObject, ITodoSearchObject } from '../types';

export class TodoCreationDto implements ITodoCreation {
export class TodoCreationDto implements ITodoCreationObject {
@IsString() title: string;
@IsString() description: string;
@IsString() userId: string;
@IsBoolean() active: boolean;
}

export class TodoSearchDto implements ITodoSearchObject {
@IsString() userId: string;
@Transform(({ value }) => Number(value)) @IsNumber() page: number;
@Transform(({ value }) => Number(value)) @IsNumber() limit: number;
}
Loading