diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ada8f50 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "pino" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 6cb06c1..7f982e2 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index b665908..58183a8 100644 --- a/nodemon.json +++ b/nodemon.json @@ -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" } diff --git a/package.json b/package.json index d281255..0f7da8d 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/application/exceptions/BadRequestException.ts b/src/application/exceptions/BadRequestException.ts new file mode 100644 index 0000000..381cd7a --- /dev/null +++ b/src/application/exceptions/BadRequestException.ts @@ -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; diff --git a/src/application/exceptions/ConflictException.ts b/src/application/exceptions/ConflictException.ts new file mode 100644 index 0000000..84d91d7 --- /dev/null +++ b/src/application/exceptions/ConflictException.ts @@ -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; diff --git a/src/application/exceptions/InternalServerException.ts b/src/application/exceptions/InternalServerException.ts new file mode 100644 index 0000000..ca2b27d --- /dev/null +++ b/src/application/exceptions/InternalServerException.ts @@ -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; diff --git a/src/application/exceptions/NotFoundException.ts b/src/application/exceptions/NotFoundException.ts new file mode 100644 index 0000000..ca2bf87 --- /dev/null +++ b/src/application/exceptions/NotFoundException.ts @@ -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; diff --git a/src/application/exceptions/UnAuthorizedException.ts b/src/application/exceptions/UnAuthorizedException.ts new file mode 100644 index 0000000..9314023 --- /dev/null +++ b/src/application/exceptions/UnAuthorizedException.ts @@ -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; diff --git a/src/application/exceptions/base/BaseHttpException.ts b/src/application/exceptions/base/BaseHttpException.ts new file mode 100644 index 0000000..b3e00a5 --- /dev/null +++ b/src/application/exceptions/base/BaseHttpException.ts @@ -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; diff --git a/src/application/exceptions/index.ts b/src/application/exceptions/index.ts new file mode 100644 index 0000000..55406f5 --- /dev/null +++ b/src/application/exceptions/index.ts @@ -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 }; diff --git a/src/application/exceptions/types/index.ts b/src/application/exceptions/types/index.ts new file mode 100644 index 0000000..d8eb04b --- /dev/null +++ b/src/application/exceptions/types/index.ts @@ -0,0 +1,8 @@ +export enum HttpStatusCode { + BAD_REQUEST = 400, + UnAuthorized = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + CONFLICT = 409, + INTERNAL_SERVER = 500, +} diff --git a/src/application/use_cases/googleUser/SignInGoogleUser.ts b/src/application/use_cases/googleUser/SignInGoogleUser.ts new file mode 100644 index 0000000..7c7d249 --- /dev/null +++ b/src/application/use_cases/googleUser/SignInGoogleUser.ts @@ -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; diff --git a/src/application/use_cases/todo/CreateTodo.ts b/src/application/use_cases/todo/CreateTodo.ts new file mode 100644 index 0000000..5966c54 --- /dev/null +++ b/src/application/use_cases/todo/CreateTodo.ts @@ -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; diff --git a/src/application/use_cases/todo/PaginatedSearchTodos.ts b/src/application/use_cases/todo/PaginatedSearchTodos.ts new file mode 100644 index 0000000..2ca771e --- /dev/null +++ b/src/application/use_cases/todo/PaginatedSearchTodos.ts @@ -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; diff --git a/src/application/use_cases/user/LoginUser.ts b/src/application/use_cases/user/LoginUser.ts new file mode 100644 index 0000000..0cde992 --- /dev/null +++ b/src/application/use_cases/user/LoginUser.ts @@ -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; diff --git a/src/application/use_cases/user/RegisterUser.ts b/src/application/use_cases/user/RegisterUser.ts new file mode 100644 index 0000000..17657bf --- /dev/null +++ b/src/application/use_cases/user/RegisterUser.ts @@ -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; diff --git a/src/domain/shared/objects/Password.ts b/src/domain/shared/objects/Password.ts index 55262eb..a92f45d 100644 --- a/src/domain/shared/objects/Password.ts +++ b/src/domain/shared/objects/Password.ts @@ -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; @@ -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; diff --git a/src/domain/todo/Todo.ts b/src/domain/todo/Todo.ts new file mode 100644 index 0000000..3b819df --- /dev/null +++ b/src/domain/todo/Todo.ts @@ -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; diff --git a/src/domain/todo/TodoFactory.ts b/src/domain/todo/TodoFactory.ts deleted file mode 100644 index 139ec6c..0000000 --- a/src/domain/todo/TodoFactory.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Todo from '.'; -import TodoService from './todo.service'; -import { ITodoCreation } from './types'; - -class TodoFactory { - async create(todoObj: ITodoCreation) { - const service = new TodoService(); - const todoM = await service.create(todoObj); - await todoM.save(); - - return new Todo(todoM); - } - - async load(id: string) { - const service = new TodoService(); - const todo = await service.findById(id); - - return new Todo(todo); - } - - async loadForUser(userId: string) { - const service = new TodoService(); - const todoList = await service.findByUserId(userId); - - return todoList.map(todo => { - return new Todo(todo); - }); - } -} - -export default TodoFactory; diff --git a/src/domain/todo/dtos/todo.dto.ts b/src/domain/todo/dtos/todo.dto.ts index eaaa69c..cddca18 100644 --- a/src/domain/todo/dtos/todo.dto.ts +++ b/src/domain/todo/dtos/todo.dto.ts @@ -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; +} diff --git a/src/domain/todo/index.ts b/src/domain/todo/index.ts index 6026215..4850b83 100644 --- a/src/domain/todo/index.ts +++ b/src/domain/todo/index.ts @@ -1,38 +1,3 @@ -import TodoService from './todo.service'; -import { ITodo, ITodoCreation, TodoDoc } from './types'; - -class Todo implements ITodo { - id: string; - title: string; - description: string; - userId: string; - active: boolean; - - private service: TodoService; - - constructor(obj: TodoDoc) { - this.id = obj._id; - this.title = obj.title; - this.description = obj.description; - this.userId = obj.userId; - this.active = obj.active; - - this.service = new TodoService(); - } - - async update(obj: ITodoCreation) { - return await this.service.update(this.id, obj); - } - - get values(): ITodo { - return { - id: this.id, - title: this.title, - description: this.description, - userId: this.userId, - active: this.active, - }; - } -} +import Todo from './Todo'; export default Todo; diff --git a/src/domain/todo/repository/ITodoRepository.ts b/src/domain/todo/repository/ITodoRepository.ts new file mode 100644 index 0000000..36a7bf5 --- /dev/null +++ b/src/domain/todo/repository/ITodoRepository.ts @@ -0,0 +1,10 @@ +import { ITodoModelObject, TodoDoc } from '../types'; + +export default interface ITodoRepository { + create: (obj: ITodoModelObject) => Promise; + update: (id: string, obj: ITodoModelObject) => Promise; + delete: (id: string) => Promise; + find: (id: string) => Promise; + search: (userId: string) => Promise; + paginatedSearch: (userId: string, page: number, limit: number) => Promise; +} diff --git a/src/domain/todo/todo.service.ts b/src/domain/todo/todo.service.ts deleted file mode 100644 index 1df8cbc..0000000 --- a/src/domain/todo/todo.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import TodoRepository from '@/infra/persistence/repositories/todo.repository'; -import { ITodoCreation } from './types'; - -class TodoService { - private repository: TodoRepository; - - constructor() { - this.repository = new TodoRepository(); - } - - async create(todoObj: ITodoCreation) { - return await this.repository.create(todoObj); - } - - async findById(id: string) { - return await this.repository.find(id); - } - - async findByUserId(userId: string) { - return await this.repository.findByUserId(userId); - } - - async update(id: string, todoObj: ITodoCreation) { - this.repository.update(id, todoObj); - } - - async delete(id: string) { - return await this.repository.delete(id); - } -} - -export default TodoService; diff --git a/src/domain/todo/types/index.d.ts b/src/domain/todo/types/index.d.ts index 684fdcf..9462879 100644 --- a/src/domain/todo/types/index.d.ts +++ b/src/domain/todo/types/index.d.ts @@ -9,10 +9,15 @@ export declare interface ITodo { active: boolean; } -export interface ITodoModel extends Omit { +export declare interface ITodoModelObject extends Omit { _id?: string; } -export type TodoDoc = Document & ITodo; +export declare interface ITodoCreationObject extends Omit {} +export declare type TodoDoc = Document & ITodo; -export declare interface ITodoCreation extends Omit {} +export declare interface ITodoSearchObject { + userId: string; + page: number; + limit: number; +} diff --git a/src/domain/user/GoogleUser.ts b/src/domain/user/GoogleUser.ts index c5a3295..f7a6d4e 100644 --- a/src/domain/user/GoogleUser.ts +++ b/src/domain/user/GoogleUser.ts @@ -1,15 +1,22 @@ -import { IGoogleUser } from './types'; +import Email from '../shared/objects/Email'; +import { GenderEnum, IUserModelObject } from './types'; import User from './User'; -class GoogleUser extends User implements IGoogleUser { +class GoogleUser extends User { accessToken: string; googleId: string; - constructor(userObj) { - super(userObj); + constructor(id: string, name: string, email: Email, gender: GenderEnum, dob: Date, accessToken: string, googleId: string) { + super(id, name, email, null, gender, dob); - this.accessToken = userObj.accessToken; - this.googleId = userObj.googleId; + this.accessToken = accessToken; + this.googleId = googleId; + } + + static create(raw: IUserModelObject) { + const email = new Email(raw.email); + + return new GoogleUser(raw._id, raw.name, email, raw.gender, raw.dob, raw.accessToken, raw.googleId); } } diff --git a/src/domain/user/User.ts b/src/domain/user/User.ts index dee3fb7..4a00ead 100644 --- a/src/domain/user/User.ts +++ b/src/domain/user/User.ts @@ -1,6 +1,6 @@ import Email from '../shared/objects/Email'; import Password from '../shared/objects/Password'; -import { GenderEnum, IUser, IUserExposed, UserDoc } from './types'; +import { GenderEnum, IUser, IUserExposed, IUserModelObject } from './types'; class User implements IUser { id: string; @@ -10,13 +10,13 @@ class User implements IUser { gender: GenderEnum; dob: Date; - constructor(userObj: UserDoc) { - this.id = userObj._id; - this.name = userObj.name; - this.email = new Email(userObj.email); - this.password = new Password(userObj.password); - this.gender = userObj.gender; - this.dob = userObj.dob; + constructor(id: string, name: string, email: Email, password: Password, gender: GenderEnum, dob: Date) { + this.id = id; + this.name = name; + this.email = email; + this.password = password; + this.gender = gender; + this.dob = dob; } get values(): IUserExposed { @@ -28,6 +28,13 @@ class User implements IUser { dob: this.dob, }; } + + static create(raw: IUserModelObject) { + const email = new Email(raw.email); + const password = new Password(raw.password); + + return new User(raw._id, raw.name, email, password, raw.gender, raw.dob); + } } export default User; diff --git a/src/domain/user/UserFactory.ts b/src/domain/user/UserFactory.ts deleted file mode 100644 index 4c9c175..0000000 --- a/src/domain/user/UserFactory.ts +++ /dev/null @@ -1,71 +0,0 @@ -import configs from '@/infra/authorization/configs'; -import OAuth2 from '@/infra/authorization/OAuth2'; -import { TokenPayload } from 'google-auth-library'; -import User from '.'; -import Email from '../shared/objects/Email'; -import { UserCredentialsDto } from './dtos/user.dtos'; -import GoogleUser from './GoogleUser'; -import { IGoogleUser, IUserCreation } from './types'; -import UserService from './user.service'; - -class UserFactory { - private oAuth2Client: OAuth2; - private service: UserService; - - constructor() { - this.service = new UserService(); - this.oAuth2Client = new OAuth2(configs.googleAuth.web); - } - - async create(userObj: IUserCreation) { - const user = await this.service.register(userObj); - - return new User(user); - } - - async load(id: string) { - const user = await this.service.find(id); - - return new User(user); - } - - async loadByLogin(obj: UserCredentialsDto) { - const user = await this.service.login(obj); - - return new User(user); - } - - async createGoogleUser(code: string) { - const res = await this.oAuth2Client.getToken(code); - const googleUser = await this.oAuth2Client.getGoogleUser(res.tokens.id_token); - - return await this.saveGoogleUser(googleUser, res.tokens.access_token); - } - - async loadGoogleUser(code: string) { - const res = await this.oAuth2Client.getToken(code); - const googleUser = await this.oAuth2Client.getGoogleUser(res.tokens.id_token); - - const user = await this.service.findByEmail(googleUser.email); - if (!user) { - return await this.saveGoogleUser(googleUser, res.tokens.access_token); - } - - return new GoogleUser(user); - } - - private async saveGoogleUser(userObj: TokenPayload, accessToken: string) { - const params: IGoogleUser = { - name: userObj.name, - email: new Email(userObj.email), - accessToken: accessToken, - googleId: userObj.sub, - }; - - const user = await this.service.register(params); - - return new GoogleUser(user); - } -} - -export default UserFactory; diff --git a/src/domain/user/dtos/user.dtos.ts b/src/domain/user/dtos/user.dtos.ts index a733fb4..9c7dcf1 100644 --- a/src/domain/user/dtos/user.dtos.ts +++ b/src/domain/user/dtos/user.dtos.ts @@ -1,8 +1,8 @@ import Email from '@domain/shared/objects/Email'; import Password from '@domain/shared/objects/Password'; +import { IUserCreationObject, IUserCredentialsObject } from '@domain/user/types'; import { Transform, Type } from 'class-transformer'; import { IsDate, IsEnum, IsString, ValidateNested } from 'class-validator'; -import { IUser } from '@domain/user/types'; export enum GenderEnum { MALE = 'MALE', @@ -10,7 +10,7 @@ export enum GenderEnum { OTHER = 'OTHER', } -export class UserCreationDto implements IUser { +export class UserCreationDto implements IUserCreationObject { @IsString() name: string; @Transform(({ value }) => new Email(value)) @ValidateNested() email: Email; @Transform(({ value }) => new Password(value)) @ValidateNested() password: Password; @@ -18,7 +18,7 @@ export class UserCreationDto implements IUser { @Type(() => Date) @IsDate() dob: Date; } -export class UserCredentialsDto { +export class UserCredentialsDto implements IUserCredentialsObject { @Transform(({ value }) => new Email(value)) @ValidateNested() email: Email; @Transform(({ value }) => new Password(value)) @ValidateNested() password: Password; } diff --git a/src/domain/user/repository/IUserRepository.ts b/src/domain/user/repository/IUserRepository.ts new file mode 100644 index 0000000..512b910 --- /dev/null +++ b/src/domain/user/repository/IUserRepository.ts @@ -0,0 +1,9 @@ +import { IUserModelObject, UserDoc } from '../types'; + +export default interface IUserRepository { + create: (obj: IUserModelObject) => Promise; + update: (id: string, obj: IUserModelObject) => Promise; + delete: (id: string) => Promise; + find: (id: string) => Promise; + findByEmail: (email: string) => Promise; +} diff --git a/src/domain/user/types/index.d.ts b/src/domain/user/types/index.d.ts index 02a34d0..7423dec 100644 --- a/src/domain/user/types/index.d.ts +++ b/src/domain/user/types/index.d.ts @@ -16,29 +16,25 @@ export declare interface IUser { password?: Password; gender?: GenderEnum; dob?: Date; + accessToken?: string; + googleId?: string; } -export declare interface IUserModel extends Omit { +export declare interface IUserModelObject extends Omit { _id?: string; email: string; - password: string; + password?: string; } -export declare interface IUserCreation extends Omit {} +export declare interface IUserCreationObject extends Omit {} -interface IUserExposed extends Omit { - email: string; -} - -export declare type UserDoc = Document & IUserModel; - -export declare interface IGoogleUser extends IUser { - accessToken: string; - googleId: string; +export declare interface IUserCredentialsObject { + email: Email; + password: Password; } -export declare interface IGoogleUserModel extends Omit { - _id?: string; +export declare interface IUserExposed extends Omit { email: string; - password: string; } + +export declare type UserDoc = Document & IUserModelObject; diff --git a/src/domain/user/user.service.ts b/src/domain/user/user.service.ts deleted file mode 100644 index b82b33f..0000000 --- a/src/domain/user/user.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import UserRepository from '@infra/persistence/repositories/user.repository'; -import { UserCredentialsDto } from './dtos/user.dtos'; -import { IUserCreation, IUserModel } from './types'; - -class UserService { - private repository: UserRepository; - - constructor() { - this.repository = new UserRepository(); - } - - async register(userObj: IUserCreation) { - const user = await this.repository.findByEmail(userObj.email.value); - if (user) throw new Error('User already exists'); - - const params: IUserModel = { - ...userObj, - email: userObj.email.value, - password: await userObj?.password?.encode(), - }; - - return await this.repository.create(params); - } - - async login(userCreds: UserCredentialsDto) { - const user = await this.repository.findByEmail(userCreds.email.value); - if (!user) throw new Error('User not found'); - - const matchPassword = userCreds.password.compare(user.password); - if (!matchPassword) throw new Error('Wrong password'); - - return user; - } - - async find(id: string) { - return await this.repository.find(id); - } - - async findByEmail(email: string) { - return await this.repository.findByEmail(email); - } -} - -export default UserService; diff --git a/src/infra/persistence/database/database.adopter.ts b/src/infra/persistence/database/database.port.ts similarity index 65% rename from src/infra/persistence/database/database.adopter.ts rename to src/infra/persistence/database/database.port.ts index 273ec0a..8c54240 100644 --- a/src/infra/persistence/database/database.adopter.ts +++ b/src/infra/persistence/database/database.port.ts @@ -1,4 +1,4 @@ -abstract class DatabaseAdopter { +abstract class DatabasePort { protected module: any; protected url: string; @@ -7,4 +7,4 @@ abstract class DatabaseAdopter { abstract connect(options?: {}): Promise; } -export default DatabaseAdopter; +export default DatabasePort; diff --git a/src/infra/persistence/database/mongoose/mongoose.adopter.ts b/src/infra/persistence/database/mongoose/mongoose.adopter.ts index 0b4eaf9..d227706 100644 --- a/src/infra/persistence/database/mongoose/mongoose.adopter.ts +++ b/src/infra/persistence/database/mongoose/mongoose.adopter.ts @@ -1,9 +1,10 @@ import mongoose, { ConnectOptions } from 'mongoose'; import { isEmpty } from 'lodash'; import { IDatabaseConfigs } from '../interfaces'; -import DatabaseAdopter from '../database.adopter'; +import DatabasePort from '../database.port'; +import logger from '@/infra/utils/logger'; -class MongooseAdopter extends DatabaseAdopter { +class MongooseAdopter extends DatabasePort { protected options: ConnectOptions = {}; constructor(private readonly configs: IDatabaseConfigs) { @@ -19,10 +20,10 @@ class MongooseAdopter extends DatabaseAdopter { async connect(): Promise { try { await mongoose.connect(this.url, this.options); - console.log('Connected to Mongo'); + logger.info('Connected to Mongo'); return true; } catch (error) { - console.error(error); + logger.error(error); return false; } } diff --git a/src/infra/persistence/models/todo.model.ts b/src/infra/persistence/models/todo.model.ts index ae933e9..74a8230 100644 --- a/src/infra/persistence/models/todo.model.ts +++ b/src/infra/persistence/models/todo.model.ts @@ -1,8 +1,8 @@ -import { ITodoModel } from '@domain/todo/types'; +import { ITodoModelObject } from '@domain/todo/types'; import { v4 as uuidv4 } from 'uuid'; import ModelFactory from './ModelFactory'; -const model = new ModelFactory('Todo', { +const model = new ModelFactory('Todo', { _id: { type: String, default: uuidv4() }, title: String, description: String, diff --git a/src/infra/persistence/models/user.model.ts b/src/infra/persistence/models/user.model.ts index 05052fd..2990f09 100644 --- a/src/infra/persistence/models/user.model.ts +++ b/src/infra/persistence/models/user.model.ts @@ -1,8 +1,8 @@ -import { IGoogleUser, IUserModel } from '@domain/user/types'; +import { IGoogleUser, IUserModelObject } from '@domain/user/types'; import { v4 as uuidv4 } from 'uuid'; import ModelFactory from './ModelFactory'; -const model = new ModelFactory('User', { +const model = new ModelFactory('User', { _id: { type: String, default: uuidv4() }, name: { type: String, required: true }, email: { type: String, required: true }, diff --git a/src/infra/persistence/repositories/base/BaseRepository.ts b/src/infra/persistence/repositories/base/BaseRepository.ts deleted file mode 100644 index 3f2c18c..0000000 --- a/src/infra/persistence/repositories/base/BaseRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Document, Model, QueryOptions } from 'mongoose'; - -abstract class BaseRepository> { - protected model: S; - - constructor(model: S) { - this.model = model; - } - - abstract create(obj: T): Promise & T>; - - abstract find(id: string): Promise & T>; - - abstract update(id: string, obj: T, options: QueryOptions): Promise & T>; - - abstract delete(id: string): Promise & T>; -} - -export default BaseRepository; diff --git a/src/infra/persistence/repositories/todo.repository.ts b/src/infra/persistence/repositories/todo.repository.ts index 3269ce0..9db5974 100644 --- a/src/infra/persistence/repositories/todo.repository.ts +++ b/src/infra/persistence/repositories/todo.repository.ts @@ -1,32 +1,35 @@ -import { ITodoModel } from '@/domain/todo/types'; -import Todo from '@/infra/persistence/models/todo.model'; -import { QueryOptions } from 'mongoose'; -import BaseRepository from './base/BaseRepository'; - -class TodoRepository extends BaseRepository { - constructor() { - super(Todo); - } +import ITodoRepository from '@/domain/todo/repository/ITodoRepository'; +import { ITodoModelObject } from '@/domain/todo/types'; +import TodoModel from '@/infra/persistence/models/todo.model'; +import { v4 as uuidv4 } from 'uuid'; - async create(todoObj: ITodoModel) { - const todo = new this.model(todoObj); +class TodoRepository implements ITodoRepository { + async create(todoObj: ITodoModelObject) { + todoObj._id = uuidv4(); + const todo = new TodoModel(todoObj); return await todo.save(); } async find(id: string) { - return await this.model.findById(id); + return await TodoModel.findById(id); + } + + async search(userId: string) { + return await TodoModel.where({ userId }); } - async findByUserId(userId: string) { - return await this.model.where({ userId }); + async paginatedSearch(userId: string, page: number = 2, limit: number = 10) { + const skip = limit * (page - 1); + + return await TodoModel.find({ userId }, {}, { skip, limit }); } - async update(id: string, todoObj: ITodoModel, options?: QueryOptions) { - return await this.model.findByIdAndUpdate(id, todoObj, options); + async update(id: string, todoObj: ITodoModelObject) { + return await TodoModel.findByIdAndUpdate(id, todoObj); } async delete(id: string) { - return await this.model.findByIdAndDelete(id); + return await TodoModel.findByIdAndDelete(id); } } diff --git a/src/infra/persistence/repositories/user.repository.ts b/src/infra/persistence/repositories/user.repository.ts index 0c196c6..5aa1b56 100644 --- a/src/infra/persistence/repositories/user.repository.ts +++ b/src/infra/persistence/repositories/user.repository.ts @@ -1,32 +1,29 @@ -import { IUserModel } from '@/domain/user/types'; -import User from '@infra/persistence/models/user.model'; -import { QueryOptions } from 'mongoose'; -import BaseRepository from './base/BaseRepository'; +import IUserRepository from '@/domain/user/repository/IUserRepository'; +import { IUserModelObject } from '@/domain/user/types'; +import UserModel from '@infra/persistence/models/user.model'; +import { v4 as uuidv4 } from 'uuid'; -class UserRepository extends BaseRepository { - constructor() { - super(User); - } - - async create(obj: IUserModel) { - const user = new this.model(obj); +class UserRepository implements IUserRepository { + async create(obj: IUserModelObject) { + obj._id = uuidv4(); + const user = new UserModel(obj); return await user.save(); } async find(id: string) { - return await this.model.findById(id); + return await UserModel.findById(id); } async findByEmail(email: string) { - return await this.model.findOne({ email }); + return await UserModel.findOne({ email }); } - async update(id: string, obj: IUserModel, options: QueryOptions) { - return await this.model.findByIdAndUpdate(id, obj, options); + async update(id: string, obj: IUserModelObject) { + return await UserModel.findByIdAndUpdate(id, obj); } async delete(id: string) { - return await this.model.findByIdAndDelete(id); + return await UserModel.findByIdAndDelete(id); } } diff --git a/src/infra/utils/logger.ts b/src/infra/utils/logger.ts new file mode 100644 index 0000000..0d0203d --- /dev/null +++ b/src/infra/utils/logger.ts @@ -0,0 +1,7 @@ +import bunyan from 'bunyan'; + +const logger = bunyan.createLogger({ + name: 'todo-ddd', +}); + +export default logger; diff --git a/src/presentation/App.ts b/src/presentation/App.ts index d0d16f4..abf9690 100644 --- a/src/presentation/App.ts +++ b/src/presentation/App.ts @@ -1,7 +1,9 @@ -import DatabaseAdopter from '@/infra/persistence/database/database.adopter'; +import BaseHttpException from '@/application/exceptions/base/BaseHttpException'; +import { HttpStatusCode } from '@/application/exceptions/types'; +import DatabasePort from '@/infra/persistence/database/database.port'; +import logger from '@/infra/utils/logger'; import cors from 'cors'; -import express, { NextFunction, Response } from 'express'; -import { Req } from './interfaces/express'; +import express, { NextFunction, Request, Response } from 'express'; import BaseRouter from './routes/base/BaseRouter'; class App { private port: string | number; @@ -16,22 +18,22 @@ class App { start() { this.app.listen(this.port, () => { - console.log(`Listing to ${this.port}`); + logger.info(`Listing to ${this.port}`); }); } applyMiddleware() { this.app.use( cors({ - origin: process.env.ORIGIN, - credentials: Boolean(process.env.CORS_CREDENTIALS), + origin: '*', + credentials: true, }), ); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); } - async connectDatabase(adopter: DatabaseAdopter) { + async connectDatabase(adopter: DatabasePort) { await adopter.connect(); } @@ -44,9 +46,13 @@ class App { } private handleErrorResponse() { - this.app.use((err: any, _req: Req, res: Response, _next: NextFunction) => { - console.error(err.stack); - res.status(500).json({ error: err.message }); + this.app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { + logger.error(err); + if (err instanceof BaseHttpException) { + res.status(err.httpCode).json({ success: false, error: err.name, message: err.message }); + } else { + res.status(HttpStatusCode.INTERNAL_SERVER).json({ success: false, message: err.message }); + } }); } } diff --git a/src/presentation/controllers/auth.controller.ts b/src/presentation/controllers/auth.controller.ts index b4daa24..4d7e039 100644 --- a/src/presentation/controllers/auth.controller.ts +++ b/src/presentation/controllers/auth.controller.ts @@ -1,21 +1,58 @@ -import AuthService from '@/domain/auth/auth.service'; +import BadRequestException from '@/application/exceptions/BadRequestException'; +import SignInGoogleUser from '@/application/use_cases/googleUser/SignInGoogleUser'; +import LoginUser from '@/application/use_cases/user/LoginUser'; +import RegisterUser from '@/application/use_cases/user/RegisterUser'; +import { IUserCreationObject, IUserCredentialsObject } from '@/domain/user/types'; +import configs from '@/infra/authorization/configs'; +import OAuth2 from '@/infra/authorization/OAuth2'; +import UserRepository from '@/infra/persistence/repositories/user.repository'; import { IHandler } from '../interfaces/express'; import BaseController from './base/BaseController'; class AuthController extends BaseController { - private authService: AuthService; + register: IHandler = async (req, res) => { + const body: IUserCreationObject = req.body; - constructor() { - super(); + const repository = new UserRepository(); - this.authService = new AuthService(); - } + const useCase = new RegisterUser(repository); + + const user = await useCase.execute(body); + + res.status(201).json({ success: true, data: user.values }); + }; + + login: IHandler = async (req, res) => { + const body: IUserCredentialsObject = req.body; + + const repository = new UserRepository(); + + const useCase = new LoginUser(repository); + + const user = await useCase.execute(body); + + res.status(201).json({ success: true, data: user.values }); + }; getAuthUrl: IHandler = async (req, res) => { - const url = this.authService.getAuthUrl(); + const googleClient = new OAuth2(configs.googleAuth.web); + const url = googleClient.generateAuthUrl(); res.status(200).json({ success: true, data: url }); }; + + googleSignIn: IHandler = async (req, res) => { + const { code } = req.body; + if (!code) throw new BadRequestException('Code missing!'); + + const repository = new UserRepository(); + const googleClient = new OAuth2(configs.googleAuth.web); + const useCase = new SignInGoogleUser(repository, googleClient); + + const user = await useCase.execute(code); + + res.status(200).json({ success: true, data: user.values }); + }; } export default AuthController; diff --git a/src/presentation/controllers/todo.controller.ts b/src/presentation/controllers/todo.controller.ts index 05941f7..2f28987 100644 --- a/src/presentation/controllers/todo.controller.ts +++ b/src/presentation/controllers/todo.controller.ts @@ -1,38 +1,35 @@ -import TodoFactory from '@/domain/todo/TodoFactory'; +import CreateTodo from '@/application/use_cases/todo/CreateTodo'; +import PaginatedSearchTodos from '@/application/use_cases/todo/PaginatedSearchTodos'; +import { ITodoCreationObject, ITodoSearchObject } from '@/domain/todo/types'; +import TodoRepository from '@/infra/persistence/repositories/todo.repository'; +import UserRepository from '@/infra/persistence/repositories/user.repository'; import { IHandler } from '../interfaces/express'; import BaseController from './base/BaseController'; class TodoController extends BaseController { - private factory: TodoFactory; - - constructor() { - super(); + create: IHandler = async (req, res, next) => { + const body: ITodoCreationObject = req.body; - this.factory = new TodoFactory(); - } + const todoRepository = new TodoRepository(); + const userRepository = new UserRepository(); - create: IHandler = async (req, res, next) => { - const todoObj = req.body; + const useCase = new CreateTodo(todoRepository, userRepository); - const todo = await this.factory.create(todoObj); + const todo = await useCase.execute(body); - res.status(201).json({ success: true, data: todo.values }); + res.status(201).json({ success: true, data: todo }); }; - find: IHandler = async (req, res, next) => { - const { id } = req.params; + search: IHandler = async (req, res, next) => { + const { userId, page, limit } = req.query as ITodoSearchObject; - const todo = await this.factory.load(id); - - res.status(200).json({ data: todo.values }); - }; + const todoRepository = new TodoRepository(); - findByUserId: IHandler = async (req, res, next) => { - const { userId } = req.params; + const useCase = new PaginatedSearchTodos(todoRepository); - const todos = await this.factory.loadForUser(userId); + const todos = await useCase.execute(userId, page, limit); - res.status(200).json({ success: true, data: todos }); + res.status(200).json({ success: true, data: { items: todos, count: todos.length } }); }; } diff --git a/src/presentation/controllers/user.controller.ts b/src/presentation/controllers/user.controller.ts index 0b587fb..9cfade5 100644 --- a/src/presentation/controllers/user.controller.ts +++ b/src/presentation/controllers/user.controller.ts @@ -1,53 +1,9 @@ -import { UserCredentialsDto } from '@/domain/user/dtos/user.dtos'; -import { IUserCreation } from '@/domain/user/types'; -import UserFactory from '@/domain/user/UserFactory'; -import { IHandler } from '../interfaces/express'; import BaseController from './base/BaseController'; class UserController extends BaseController { - private factory: UserFactory; - constructor() { super(); - - this.factory = new UserFactory(); } - - register: IHandler = async (req, res) => { - const userObj: IUserCreation = req.body; - - const user = await this.factory.create(userObj); - - res.status(201).json({ success: true, data: user.values }); - }; - - login: IHandler = async (req, res, next) => { - const userCreds: UserCredentialsDto = req.body; - - const user = await this.factory.loadByLogin(userCreds); - - res.status(200).json({ success: true, data: user.values }); - }; - - signUpWithGoogle: IHandler = async (req, res) => { - const { code } = req.body; - - if (!code) throw new Error('Code missing!'); - - const user = await this.factory.createGoogleUser(code); - - res.status(201).json({ success: true, data: user.values }); - }; - - signInWithGoogle: IHandler = async (req, res) => { - const { code } = req.body; - - if (!code) throw new Error('Code missing!'); - - const user = await this.factory.loadGoogleUser(code); - - res.status(200).json({ success: true, data: user.values }); - }; } export default UserController; diff --git a/src/presentation/interfaces/express.d.ts b/src/presentation/interfaces/express.d.ts index ba684e2..ab28d69 100644 --- a/src/presentation/interfaces/express.d.ts +++ b/src/presentation/interfaces/express.d.ts @@ -1,7 +1,6 @@ import { NextFunction, Request, Response } from 'express'; +import { ParamsDictionary, Query } from 'express-serve-static-core'; -export declare interface Req extends Request {} +export declare type IHandler = (req: Request, res: Response, next: NextFunction) => void; -export declare type IHandler = (req: Req, res: Response, next: NextFunction) => void; - -export declare type IErrorHandler = (err: any, req: Req, res: Response, next: NextFunction) => void; +export declare type IErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => void; diff --git a/src/presentation/middleware/validation.middleware.ts b/src/presentation/middleware/validation.middleware.ts index e4145b3..4d53fcf 100644 --- a/src/presentation/middleware/validation.middleware.ts +++ b/src/presentation/middleware/validation.middleware.ts @@ -1,3 +1,4 @@ +import BadRequestException from '@/application/exceptions/BadRequestException'; import { plainToInstance } from 'class-transformer'; import { validate, ValidationError, ValidatorOptions } from 'class-validator'; import { RequestHandler } from 'express'; @@ -7,7 +8,7 @@ type RequestParamsFrom = 'body' | 'query' | 'params'; const stringifyValidationErrors = (errors: Array): string => { return errors .map((error: ValidationError) => { - if (error.children.length > 0) { + if (error.children?.length > 0) { return stringifyValidationErrors(error.children); } return Object.values(error.constraints); @@ -23,7 +24,7 @@ const validationMiddleware = ( return async (req, _res, next) => { try { const errors = await validate(plainToInstance(validator, req[params]), options); - if (errors.length > 0) throw new Error(stringifyValidationErrors(errors)); + if (errors.length > 0) throw new BadRequestException(stringifyValidationErrors(errors)); req[params] = plainToInstance(validator, req[params]); next(); diff --git a/src/presentation/routes/auth.router.ts b/src/presentation/routes/auth.router.ts index a094553..1b74142 100644 --- a/src/presentation/routes/auth.router.ts +++ b/src/presentation/routes/auth.router.ts @@ -1,4 +1,6 @@ +import { UserCreationDto, UserCredentialsDto } from '@/domain/user/dtos/user.dtos'; import AuthController from '@presentation/controllers/auth.controller'; +import validationMiddleware from '../middleware/validation.middleware'; import BaseRouter from './base/BaseRouter'; class AuthRouter extends BaseRouter { @@ -7,7 +9,10 @@ class AuthRouter extends BaseRouter { } protected routes(): void { + this.post('/register', validationMiddleware(UserCreationDto), this.controller.register); + this.post('/login', validationMiddleware(UserCredentialsDto), this.controller.login); this.get('/googleUrl', this.controller.getAuthUrl); + this.post('/google_sign_in', this.controller.googleSignIn); } } diff --git a/src/presentation/routes/base/BaseRouter.ts b/src/presentation/routes/base/BaseRouter.ts index dfc99fd..849fa1c 100644 --- a/src/presentation/routes/base/BaseRouter.ts +++ b/src/presentation/routes/base/BaseRouter.ts @@ -4,6 +4,7 @@ import express, { IRouter, RequestHandler } from 'express'; import { isArray } from 'lodash'; import tryCatchWrapper from '@/presentation/middleware/tryCatch.wrapper'; import BaseController from '@presentation/controllers/base/BaseController'; +import logger from '@/infra/utils/logger'; export type RouterClass = { new (): BaseRouter }; export type Methods = 'all' | 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head'; @@ -46,7 +47,7 @@ export default abstract class BaseRouter { // handlers = [authenticationMiddleware, ...handlers]; } - console.info(`${method.toUpperCase()}: ${this.parseEndPoint(path)}`); + logger.info(`${method.toUpperCase()}: ${this.parseEndPoint(path)}`); this.router[method](this.parseEndPoint(path), ...handlers); } diff --git a/src/presentation/routes/todo.router.ts b/src/presentation/routes/todo.router.ts index d9f034c..2297fbe 100644 --- a/src/presentation/routes/todo.router.ts +++ b/src/presentation/routes/todo.router.ts @@ -1,4 +1,4 @@ -import { TodoCreationDto } from '@/domain/todo/dtos/todo.dto'; +import { TodoCreationDto, TodoSearchDto } from '@/domain/todo/dtos/todo.dto'; import TodoController from '../controllers/todo.controller'; import validationMiddleware from '../middleware/validation.middleware'; import AuthorizedRouter from './base/AuthorizedRouter'; @@ -10,8 +10,7 @@ class TodoRouter extends AuthorizedRouter { protected routes(): void { this.post('/', validationMiddleware(TodoCreationDto), this.controller.create); - this.get('/:id', this.controller.find); - this.get('/user/:userId', this.controller.findByUserId); + this.get('/', validationMiddleware(TodoSearchDto, 'query'), this.controller.search); } } diff --git a/src/presentation/routes/user.router.ts b/src/presentation/routes/user.router.ts index 899ef23..a0f07d3 100644 --- a/src/presentation/routes/user.router.ts +++ b/src/presentation/routes/user.router.ts @@ -8,12 +8,7 @@ class UserRouter extends AuthorizedRouter { super(new UserController(), path); } - protected routes(): void { - this.post('/register', validationMiddleware(UserCreationDto), this.controller.register); - this.post('/login', validationMiddleware(UserCredentialsDto), this.controller.login); - this.post('/signUpWithGoogle', this.controller.signUpWithGoogle); - this.post('/signInWithGoogle', this.controller.signInWithGoogle); - } + protected routes(): void {} } export default UserRouter; diff --git a/yarn.lock b/yarn.lock index 3d333b6..6858b87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -744,6 +744,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@faker-js/faker@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" + integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== + "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" @@ -833,6 +838,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/bunyan@^1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.8.tgz#8d6d33f090f37c07e2a80af30ae728450a101008" + integrity sha512-Cblq+Yydg3u+sGiz2mjHjC5MPmdjY+No4qvHrF+BUhblsmSfMvsHLbOG62tPbonsqBj6sbWv1LHcsoe5Jw+/Ow== + dependencies: + "@types/node" "*" + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -1091,6 +1103,16 @@ buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -1267,6 +1289,13 @@ dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -1472,6 +1501,17 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1792,7 +1832,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -minimatch@^3.1.1, minimatch@^3.1.2: +"minimatch@2 || 3", minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1829,6 +1869,18 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +moment@^2.19.3: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + mongodb-connection-string-url@^2.5.4: version "2.6.0" resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" @@ -1889,11 +1941,30 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg== + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + mylas@^2.1.9: version "2.1.13" resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4" integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg== +nan@^2.14.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -2105,6 +2176,13 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -2117,6 +2195,11 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"