Notes I make throughout my studies on:
Available at: Backend GoBarber
So far, the backend is separating files by type, for example: services, which is handling all business rules. Thus, the structure is disorganized, in case the structure grows.
Backend folder structure
From now on, they will be separated by Domain .
It is the area of knowledge of that module / file.
Example: User, all files related to this domain will be part of the same module.
Domain / Module Layer folder structure
Note: Previously, the User entity was part of the models folder , which basically has the same concept of entities : How we managed to represent information in the application.
Example: Database, errors, middlewares, routes and etc.
Shared folder structure
Example: Database, Automatic Email Service and so on …
Folder structure of the Infra Layer
Folder structure of the Domain Layer with the Infrastructure Layer
"./src"
{
"@modules/*": ["modules/*"],
"@config/*": ["config/*"],
"@shared/*": ["shared/*]
}
{
"build": "tsc",
"dev:server": "ts-node-dev -r tsconfig-paths/register --inspect --transpileOnly --ignore-watch node_modules src/shared/infra/http/server.ts",
"typeorm": "ts-node-dev -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
},
$ yarn add tsconfig-paths -D
// AppointmentsRepository that inherits methods from a standard TypeORM repository.
import { EntityRepository , Repository } from 'typeorm' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
@ EntityRepository ( Appointment )
class AppointmentsRepository extends Repository < Appointment > {
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = await this . findOne ( {
where : { date } ,
} ) ;
return findAppointment ;
}
}
export default AppointmentsRepository ;
import Appointment from '../infra/typeorm/entities/Appointment' ;
export default interface IAppointmentsRepository {
findByDate ( date : Date ) : Promise < Appointment | undefined > ;
}
import { EntityRepository , Repository } from 'typeorm' ;
import IAppointmentsRepository from '@ modules / appointments / repositories / IAppointmentsRepository' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
@ EntityRepository ( Appointment )
class AppointmentsRepository extends Repository < Appointment > implements IAppointmentsRepository {
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = await this . findOne ( {
where : { date } ,
}) ;
return findAppointment ;
}
}
export default AppointmentsRepository ;
When one class extends another, it means that it will inherit the methods. The implements, on the other hand, serve as a shape (format or rule) to be followed by the class, but not that it inherits its methods.
export default interface ICreateAppointmentDTO {
provider_id : string;
date: Date ;
}
import { getRepository , Repository } from 'typeorm' ;
import IAppointmentsRepository from '@ modules / appointments / repositories / IAppointmentsRepository' ;
import ICreateAppointmentDTO from '@ modules / appointments / dtos / ICreateAppointmentDTO' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
class AppointmentsRepository implements IAppointmentsRepository {
private ormRepository : Repository < Appointment > ;
constructor ( ) {
this . ormRepository = getRepository ( Appointment ) ;
}
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = await this . ormRepository . findOne ( {
where : { date } ,
} ) ;
return findAppointment ;
}
public async create ( { provider_id , date } : ICraeteAppointmentDTO ) : Promise < Appointment > {
const appointment = this . ormRepository . create ( {
provider_id ,
gives you
} ) ;
await this . ormRepository . save ( appointment ) ;
return appointment ;
}
}
export default AppointmentsRepository ;
This principle defines that:
That is, instead of my service depending directly on the typeorm repository , it now depends only on the interface of the repository, which will be passed through the route .
The route is part of the infra layer, which is the same as the TypeORM, so it is not necessary to apply the same principle to it. The service is part of the domain layer, so it must be decoupled from the infrastructure layer in such a way that it is not aware of its operation.
import { startOfHour } from 'date-fns' ;
import { getCustomRepository } from 'typeorm' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import AppointmentsRepository from '../repositories/AppointmentsRepository' ;
Request interface {
provider_id : string ;
date : Date ;
}
class CreateAppointmentService {
public async execute ( { provider_id , date } : Request ) : Promise < Appointment > {
// depending on the repository for the typeorm
const appointmentsRepository = getCustomRepository ( AppointmentsRepository ) ;
const appointmentDate = startOfHour ( date ) ;
const findAppointmentInSameDate = await appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await appointmentsRepository . create ( {
provider_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
import { startOfHour } from 'date-fns' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import IAppointmentsRepository from '../repositories/IAppointmentsRepository' ;
interface IRequest {
provider_id : string ;
date : Date ;
}
class CreateAppointmentService {
// depending only on the interface (abstraction) of the repository, which will be passed to the constructor when a new repository is instantiated on the route.
constructor ( private appointmentsRepository : IAppointmensRepository ) { }
public async execute ( { provider_id , date } : Request ) : Promise < Appointment > {
const appointmentDate = startOfHour ( date ) ;
const findAppointmentInSameDate = await this . appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await this . appointmentsRepository . create ( {
provider_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
Note: The Appointment entity is still provided by TypeORM, but in this case the Dependecy Inversion Principle will not be applied in order not to ‘hinder’ the understanding of this principle.
// modules / users / repositories / IUsersRepository
import User from '../infra/typeorm/entities/User' ;
import ICreateUserDTO from '../dtos/ICreateUserDTO' ;
export default interface IUsersRepository {
findByEmail ( email : string ) : Promise < User | undefined > ;
findById ( id : string ) : Promise < User | undefined > ;
create ( data : ICreateUserDTO ) : Promise < User > ;
save ( user : User ) : Promise < User > ;
}
// modules / users / dtos / ICreateUserDTO
export default interface ICreateUserDTO {
name : string;
email: string ;
password: string ;
}
// modules / users / infra / typeorm / repositories / UsersRepository
import { getRepository , Repository } from 'typeorm' ;
import User from '../entities/User' ;
import IUsersRepository from '@ modules / users / repositories / IUsersRepository' ;
class UsersRepository implements IUsersRepository {
private ormRepository : Repository < User > ;
constructor ( ) {
this . ormRepository = getRepository ( User ) ;
}
public async findByEmail ( email : string ) : Promise < User > {
const user = this . ormRepository . findOne ( { where : { email } } ) ;
return user ;
}
public async findById ( id : string ) : Promise < User > {
const user = this . ormRepository . findOne ( id ) ;
return user ;
}
public async create ( user : User ) : Promise < User > {
const user = this . ormRepository . create ( user ) ;
return this . ormRepository . save ( user ) ;
}
public async save ( user : User ) : Promise < User > {
return this . ormRepository . save ( user ) ;
}
}
import { hash } from 'bcryptjs' ;
import AppError from '@ shared / errors / AppError' ;
import User from '../infra/typeorm/entities/User' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
interface IRequest {
name : string ;
email : string ;
password : string ;
}
class CreateUserService {
constructor ( private usersRepository : IUsersRepository ) { }
public async execute ( { name , email , password } : IRequest ) : Promise < User > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( checkUserExist ) {
throw new AppError ( 'Email already used!' ) ;
}
const hashedPassword = await hash ( password , 8 ) ;
const user = await this . usersRepository . create ( {
name ,
email ,
password : hashedPassword ,
} ) ;
return user ;
}
}
export default CreateUserService ;
Make these changes to the other services.
import { container } from 'tsyringe' ;
import IAppointmentsRepository from '@ modules / appointsments / repositories / IAppointmentsRepository' ;
import IUsersRepository from '@ modules / users / repositories / IUsersRepository' ;
import AppointmentsRepository from '@ modules / appointments / infra / typeorm / repositories / AppointmentsRepository' ;
import UsersRepository from '@ modules / users / infra / typeorm / repositories / UsersRepository' ;
container . registerSingleton < IAppointmentsRepository > ( 'AppointmentsRepository' , AppointmentsRepository ) ;
container . registerSingleton < IUsersRepository > ( 'UsersRepository' , UsersRepository ) ;
import { startOfHour } from 'date-fns' ;
import { injectable , inject } from 'tsyringe' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import IAppointmentsRepository from '../repositories/IAppointmentsRepository' ;
interface IRequest {
provider_id : string ;
date : Date ;
}
// add the tsyringe decorator
@ injectable ( )
class CreateAppointmentService {
constructor (
// and will inject this dependency whenever the service is instantiated.
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository ,
) { }
public async execute ( { provider_id , date } : IRequest ) : Promise < Appointment > {
const appointmentDate = startOfHour ( date ) ;
const findAppointmentInSameDate = await this . appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await this . appointmentsRepository . create ( {
provider_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
import { Router } from 'express' ;
import { container } from 'tsyringe' ;
import CreateAppointmentService from '@ modules / appointments / services / CreateAppointmentService' ;
const appointmentsRouter = Router ( ) ;
appointmentsRouter . post ( '/' , async ( request , response ) => {
const { provider_id , date } = request . body ;
// const appointmentsRepository = new AppointmentsRepository ();
// const createAppointments = new CreateAppointmentService ();
// put it this way
const createAppointments = container . resolve ( CreateAppointmentService )
. . .
} ) ;
...
import '@ shared / container' ;
...
If more methods are needed, I must create a new controller.
// users / infra / http / controllers / SessionsController.ts
import { Request , Response } from 'express' ;
import { container } from 'tsyringe' ;
import AuthenticateUserService from '@ modules / users / services / AuthenticateUserService' ;
class SessionsController {
public async create ( request : Request , response : Response ) : Promise < Response > {
const { password , email } = request . body ;
const authenticateUser = container . resolve ( AuthenticateUserService ) ;
const { user , token } = await authenticateUser . execute ( { password , email } ) ;
delete user . password ;
return response . json ( { user , token } ) ;
}
}
export default SessionsController ;
// sessions.routes.ts
import { Router } from 'express' ;
import SessionsController from '../controllers/SessionsController' ;
const sessionsRouter = Router ( ) ;
const sessionsController = new SessionsController ( ) ;
sessionsRouter . post ( '/' , sessionsController . create ) ;
export default sessionsRouter ;
We have created tests to ensure that our application continues to function properly regardless of the number of functionality and the number of developers.
It does not depend on another part of the application or external service.
You will never own:
Side effect example: Trigger an email whenever a new user is created
It tests a complete functionality, passing through several layers of the application.
Rota -> Controller -> Service -> Repository -> Service -> …
Example: Creating a new user with sending email.
Example:
Example: When the user signs up for the application, they should receive a welcome email.
$ yarn add jest -D
$ yarn jest --init
$ yarn add @ types / jest ts-jest -D
test ( 'sum two numbers' , ( ) => {
expect ( 1 + 2 ) . toBe ( 3 ) ;
} ) ;
describe ( 'Create Appointment' , ( ) => {
it ( 'should be able to create a new appointment' , ( ) => {
} ) ;
} ) ;
// @modules / appointments / repositories / fakes / FakeAppointmentsRepository
import { uuid } from 'uuidv4' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointments' ;
import ICreateAppointmentDTO from '../dtos/ICreateAppointmentsDTO' ;
import IAppointmentsRepository from '../IAppointmentsRepository' ;
class FakeAppointmentsRepository implements IAppointmentsRepository {
private appointments : Appointments [ ] = [ ] ;
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = this . appointments . find (
appointment => appointment . date === date ,
) ;
return findAppointment ;
}
public async create ( { provider_id , date } : ICreateAppointmentDTO ) : Promise < Appointment > {
const appointment = new Appointment ( ) ;
Object . assign ( appointment , { id : uuid ( ) , date , provider_id } ) ;
this . appointments . push ( appointment ) ;
return appointment ;
}
}
export default FakeAppointmentsRepository ;
import FakeAppointmentsRepository from '@ modules / appoinetments / repositories / fakes / FakeAppointmentsRepository' ;
import CreateAppointmentService from './CreateAppointmentService' ;
describe ( 'Create Appointment' , ( ) => {
it ( 'should be able to create a new appointment' , ( ) => {
const fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
const createAppointment = new CreateAppointmentService ( fakeAppointmentsRepository ) ;
const appointment = await createAppointment . execute ( {
date : new Date ( ) ,
provider_id : '4444'
} )
expect ( appointment ) . toHaveProperty ( 'id' ) ;
expect ( appointment . provider_id ) . toBe ( '4444' ) ;
} ) ;
} ) ;
When trying to execute, it will give error, because the test file does not understand the import @modules
const { pathsToModuleNameMapper } = require ( 'ts-jest / utils' ) ;
const { compilerOptions } = require ( './tsconfig.json' ) ;
module . exports = {
moduleNameMapper : pathsToModuleNameMapper ( compilerOptions . paths , { prefix : '<rootDir> / src /' } )
}
$ yarn test
collectCoverage : true ,
collectCoverageFrom : [
'<rootDir> /src/modules/**/*.ts'
] ,
coverageDir : ' coverage ' , // this folder will be created at the root that will hold the
coverageReporters reports : [
' text -summary ' ,
' lcov ' ,
] ,
$ yarn test
import AppError from '@ shared / errors / AppError' ;
import FakeAppointmentsRepository from '../repositories/fakes/FakeAppointmentsRepository' ;
import CreateAppointmentService from './CreateAppointmentService' ;
describe ( 'CreateAppointment' , ( ) => {
it ( 'should be able to create a new appointment' , async ( ) => {
const fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
const createAppointment = new CreateAppointmentService (
fakeAppointmentsRepository ,
) ;
const appointment = await createAppointment . execute ( {
date : new Date ( ) ,
provider_id : '4444' ,
} ) ;
expect ( appointment ) . toHaveProperty ( 'id' ) ;
expect ( appointment . provider_id ) . toBe ( '4444' ) ;
} ) ;
it ( 'should not be able to create two appointments at the same time' , async ( ) => {
const fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
const createAppointment = new CreateAppointmentService (
fakeAppointmentsRepository ,
) ;
const date = new Date ( 2020 , 5 , 10 , 14 ) ;
await createAppointment . execute ( {
date ,
provider_id : '4444' ,
} ) ;
expect (
createAppointment . execute ( {
date ,
provider_id : '4444' ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
} ) ;
// @modules /users/repositories/fakes/FakeUsersRepository.ts
import { uuid } from 'uuidv4' ;
import IUsersRepository from '../IUsersRepository' ;
import User from '../../infra/typeorm/entities/User' ;
import ICreateUserDTO from '../../dtos/ICreateUserDTO' ;
class FakeUsersRepository implements IUsersRepository {
private users : User [ ] = [ ] ;
public async findByEmail ( email : string ) : Promise < User | undefined > {
const findUser = this . users . find ( user => user . email === email ) ;
return findUser ;
}
public async findById ( id : string ) : Promise < User | undefined > {
const findUser = this . users . find ( user => user . id === id ) ;
return findUser ;
}
public async create ( userData : ICreateUserDTO ) : Promise < User > {
const user = new User ( ) ;
Object . assign ( user , { id : uuid ( ) } , userData ) ;
this . users . push ( user ) ;
return user ;
}
public async save ( user : User ) : Promise < User > {
const findIndex = this . users . findIndex ( findUser => findUser . id === user . id ) ;
this . users [ findIndex ] = user ;
return user ;
}
}
import AppError from '@ shared / errors / AppError' ;
import CreateUserService from './CreateUserService' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
describe ( 'CreateUser' , ( ) => {
it ( 'should be able to create a new user' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const createUser = new CreateUserService ( fakeUserRepository ) ;
const user = await createUser . execute ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456'
} ) ;
expect ( user ) . toHaveProperty ( 'id' ) ;
} ) ;
it ( 'should not be able to create two user with the same email' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const createUser = new CreateUserService ( fakeUserRepository ) ;
await createUser . execute ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456'
} ) ;
expect ( createUser . execute ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456'
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
} ) ;
$ yarn test
If there is an error related to reflect-metadata, go to jest.config.js and put
setupFiles : [
'reflect-metadata'
] ,
import CreateUserService from './CreateUserService' ;
import AuthenticateUserService from './AuthenticateUserService' ;
describe ( 'AuthenticateUser' , ( ) => {
it ( 'should be able to authenticate' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const createUser = new CreateUserService ( fakeUsersRepository ) ;
const authenticateUser = new AuthenticateUserService ( fakeUsersRepository) ) ;
const user = await createUser . execute ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} )
const authUser = await authenticateUser . execute ( {
email : 'testunit@example.com' ,
password : '123456' ,
} )
expect ( authUser ) . toHaveProperty ( 'token' ) ;
expect ( authUser . user ) . toEqual ( user ) ;
} ) ;
} ) ;
// CreateUserService
import { hash } from 'bcryptjs' ;
...
@ injectable ( )
class CreateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
) { }
public async execute ( { name , email , password } : IRequest ) : Promise < User > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( checkUserExist ) {
throw new AppError ( 'Email already used!' ) ;
}
const hashedPassword = await hash ( password , 8 ) ;
...
}
}
export default CreateUserService ;
import { compare } from 'bcryptjs' ;
...
@ injectable ( )
class AuthenticateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
) { }
public async execute ( { password , email } : IRequest ) : Promise < IResponse > {
const user = await this . usersRepository . findByEmail ( email ) ;
if ( ! user ) {
throw new AppError ( 'Incorrect email / password combination.' , 401 ) ;
}
const passwordMatched = await compare (
password ,
user . password ,
) ;
...
}
}
export default AuthenticateUserService ;
// @modules / users / providers / HashProvider / models
export default interface IHashProvider {
generateHash ( payload : string ) : Promise < string > ;
compareHash ( payload : string , hashed : string ) : Promise < boolean > ;
}
// @modules / users / provider / HashProvider / implementations
import { hash , compare } from 'bcrypjs' ;
import IHashProvider from '../models/IHashProvider' ;
class BCryptHashProvider implements IHashProvider {
public async generateHash ( payload : string ) : Promise < string > {
return hash ( payload , 8 ) ;
}
public async compareHash ( payload : string , hashed : string ) : Promise < string > {
return compare ( payload , hashed ) ;
}
}
export default BCryptHashProvider ;
// CreateUserService
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
@ injectable ( )
class CreateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
private hashProvider : IHashProvider ,
) { }
public async execute ( { name , email , password } : IRequest ) : Promise < User > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( checkUserExist ) {
throw new AppError ( 'Email already used!' ) ;
}
const hashedPassword = await this . hashProvider . generateHash ( password ) ;
const user = await this . usersRepository . create ( {
name ,
email ,
password : hashedPassword ,
} ) ;
return user ;
}
}
export default CreateUserService ;
// AuthenticateUserService
import { injectable , inject } from 'tsyringe' ;
import { sign } from 'jsonwebtoken' ;
import authConfig from '@ config / auth' ;
import AppError from '@ shared / errors / AppError' ;
import User from '../infra/typeorm/entities/User' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
interface IRequest {
email : string ;
password : string ;
}
IResponse interface {
user : User ;
token : string ;
}
@ injectable ( )
class AuthenticateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
private hashProvider : IHashProvider ,
) { }
public async execute ( { password , email } : IRequest ) : Promise < IResponse > {
const user = await this . usersRepository . findByEmail ( email ) ;
if ( ! user ) {
throw new AppError ( 'Incorrect email / password combination.' , 401 ) ;
}
const passwordMatched = await this . hashProvider . compareHash (
password ,
user . password ,
) ;
if ( ! passwordMatched ) {
throw new AppError ( 'Incorrect email / password combination.' , 401 ) ;
}
const { secret , expiresIn } = authConfig . jwt ;
const token = sign ( { } , secret , {
subject : user . id ,
expiresIn ,
} ) ;
return { user , token } ;
}
}
export default AuthenticateUserService ;
// @modules /users/provider/index.ts
import { container } from 'tsyringe' ;
import BCryptHashProvider from './HashProvider/implementations/BCryptHashProvider' ;
import IHashProvider from './HashProvider/models/IHashProvider' ;
container . registerSingleTon < IHashProvider > ( 'HashProvider' , BCryptHashProvider ) ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
@ injectable ( )
class CreateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
) { }
...
}
}
export default CreateUserService ;
// @shared /container/index.ts
import '@ modules / users / providers' ;
...
import IHashProvider from '../models/IHashProvider' ;
class FakeHashProvider implements IHashProvider {
public async generateHash ( payload : string ) : Promise < string > {
return payload ;
}
public async compareHash ( payload : string , hashed : string ) : Promise < boolean > {
return payload === hashed ;
}
} ;
export default FakeHashProvider ;
import CreateUserService from './CreateUserService' ;
import AuthenticateUserService from './AuthenticateUserService' ;
import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProviders' ;
describe ( 'AuthenticateUser' , ( ) => {
it ( 'should be able to authenticate' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeHashProvider = new FakeHashProvider ( ) ;
const createUser = new CreateUserService (
fakeUsersRepository ,
fakeHashProvider
) ;
const authenticateUser = new AuthenticateUserService (
fakeUsersRepository ,
fakeHashProvider
) ;
const user = await createUser . execute ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} )
const authUser = await authenticateUser . execute ( {
email : 'testunit@example.com' ,
password : '123456' ,
} )
expect ( authUser ) . toHaveProperty ( 'token' ) ;
expect ( authUser . user ) . toEqual ( user ) ;
} ) ;
} ) ;
class UpdateUserAvatarService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
) { }
public async execute ( { user_id , avatarFilename } : IRequest ) : Promise < User > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'Only authenticated user can change avatar' , 401 ) ;
}
// this direct dependency on the multer must be removed, as it ends up hurting liskov substitution and dependency inversion principle
if ( user . avatar ) {
const userAvatarFilePath = path . join ( uploadConfig . directory , user . avatar ) ;
const userAvatarFileExists = await fs . promises . stat ( userAvatarFilePath ) ;
if ( userAvatarFileExists ) {
await fs . promises . unlink ( userAvatarFilePath ) ;
}
user . avatar = avatarFilename ;
await this . usersRepository . save ( user ) ;
return user ;
}
}
}
export default UpdateUserAvatarService ;
//shared/container/providers/StorageProvider/models/IStorageProvider.ts
export default interface IStorageProvider {
saveFile ( file : string ) : Promise < string > ;
deleteFile ( file : string ) : Promise < string > ;
}
...
const tmpFolder = path . resolve ( __dirname , '..' , '..' , 'tmp' ) ;
export default {
tmpFolder ,
uploadsFolder : path . join ( tmpFolder , 'uploads' ) ;
...
}
//shared/container/providers/StorageProvider/implementations/DiskStorageProvider.ts
import fs from 'fs' ;
import path from 'path' ;
import uploadConfig from '@ config / upload' ;
import IStorageProvider from '../models/IStorageProvider' ;
class DiskStorageProvider implements IStorageProvider {
public async saveFile ( file : string ) : Promise < string > {
await fs . promises . rename (
path . resolve ( uploadConfig . tmpFolder , file ) ,
path . resolve ( uploadConfig . uploadsFolder , file ) ,
);
return file ;
}
public async deleteFile ( file : string ) : Promise < void > {
const filePath = path . resolves ( uploadConfig . uploadsFolder , file ) ;
try {
await fs . promises . stat ( filePath ) ;
} catch {
return ;
}
await fs . promises . unlink ( filePath ) ;
}
}
export default DiskStorageProvider ;
import { container } from 'tsyringe' ;
import IStorageProvider from './StorageProvider/models/IStorageProvider' ;
import DiskStorageProvider from './StorageProvider/implementations/DiskStorageProvider' ;
container . registerSingleton < IStorageProvider > (
'StorageProvider' ,
DiskStorageProvider ) ;
import path from 'path' ;
import fs from 'fs' ;
import uploadConfig from '@ config / upload' ;
import AppError from '@ shared / errors / AppError' ;
import { injectable , inject } from 'tsyringe' ;
import IStorageProvider from '@ shared / container / providers / StorageProvider / models / IStorageProvider' ;
import User from '../infra/typeorm/entities/User' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
interface IRequest {
user_id : string ;
avatarFilename : string ;
}
@ injectable ( )
class UpdateUserAvatarService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'StorageProvider' )
private storageProvider : IStorageProvider ,
) { }
public async execute ( { user_id , avatarFileName } : IRequest ) : Promise < User > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'Only authenticated user can change avatar' , 401 ) ;
}
if ( user . avatar ) {
await this . storageProvider . deleteFile ( user . avatar ) ;
}
const filePath = await this . storageProvider . saveFile ( avatarFileName ) ;
user . avatar = filePath ;
await this . usersRepository . save ( user ) ;
return user ;
}
}
export default UpdateUserAvatarService ;
// @shared /container/providers/StorageProvider/fakes/FakeStorageProvider.ts
import IStorageProvider from '../models/IStorageProvider' ;
class FakeStorageProvider implements IStorageProvider {
private storage : string [ ] = [ ] ;
public async saveFile ( file : string ) : Promise < string > {
this . storage . push ( file ) ;
return file ;
}
public async deleteFile ( file : string ) : Promise < void > {
const findIndex = this . storage . findIndex ( item => item === file ) ;
this . storage . splice ( findIndex , 1 ) ;
}
}
export default FakeStorageProvider ;
import AppError from '@ shared / errors / AppError' ;
import FakeUsersRepository from '../repositories/fakes/FakeUserRepository' ;
import FakeStorageProvider from '@ shared / container / providers / StorageProvider / fakes / FakeStorageProvider' ;
import UpdateUserAvatarService from './UpdateUserAvatarService' ;
describe ( 'UpdateUserAvatarService' , ( ) => {
it ( 'should be able to update user avatar' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeStorageProvider = new FakeStorageProvider ( ) ;
const updateUserAvatarService = new UpdateUserAvatarService (
fakeUsersRepository ,
fakeStorageProvider
) ;
const user = await fakeUsersRepository . create ( {
name : 'Test Unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await updateUserAvatarService . execute ( {
user_id : user . id ,
avatarFileName : 'avatar.jpg' ,
} )
expect ( user . avatar ) . toBe ( 'avatar.jpg' ) ;
} ) ;
it ( 'should not be able to update avatar from non existing user' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeStorageProvider = new FakeStorageProvider ( ) ;
const updateUserAvatarService = new UpdateUserAvatarService (
fakeUsersRepository ,
fakeStorageProvider
) ;
expect ( updateUserAvatarService . execute ( {
user_id : 'non-existing-id' ,
avatarFileName : 'avatar.jpg' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should delete old avatar when updating a new one' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeStorageProvider = new FakeStorageProvider ( ) ;
const updateUserAvatarService = new UpdateUserAvatarService (
fakeUsersRepository ,
fakeStorageProvider
) ;
const deleteFile = jest . spyOn ( fakeStorageProvider , 'deleteFile' ) ;
const user = await fakeUsersRepository . create ( {
name : 'Test Unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await updateUserAvatarService . execute ( {
user_id : user . id ,
avatarFileName : 'avatar.jpg' ,
} )
await updateUserAvatarService . execute ( {
user_id : user . id ,
avatarFileName : ' avatar2.jpg ' ,
} )
expect ( deleteFile ) . toHaveBeenCalledWithin ( 'avatar.jpg' ) ;
expect ( user . avatar ) . toBe ( ' avatar2.jpg ' ) ;
} ) ;
} ) ;
const deleteFile = jest.spyOn (fakeStorageProvider, ‘deleteFile’) checks whether the function was called or not, and in the end I hope it was called with the avatar.jpg parameter .
Made in Software Engineering.
I must :
- Mapear os Requisitos
- Conhecer as Regras de Negócio
In the real world, MANY MEETINGS must be held with the CUSTOMER so that MANY NOTES are made , to MAP FUNCTIONALITIES and BUSINESS RULES !
Is BASED on Feedback from the customer to be improved. I won’t always get it right the first time.
I must think of a project in a simple way . There is no point creating many features if the customer uses only 20% of them and wants them to be improved.
Create with Macro Functions in mind .
Inside they are well defined , but we can see them as an application screen .
And each macro functionality will be well described and documented, being divided into: Functional, Non-Functional Requirements and Business Rules.
Functional Requirements
Non-Functional Requirements
Example: if 400 users try to recover their password at the same time, it would be trying to send 400 emails at the same time , which could cause a slow service . Therefore, the approach adopted will be of queue (background job) , where the queue will be processed little by little by the tool.
Business rules
Functional Requirements
Non-Functional Requirements
Business rules
Functional Requirements
Non-Functional Requirements
It is possible that the provider is reloading the page many times throughout the day. Soon, the cache will be cleared and stored only when you have a new schedule.
Business rules
Functional Requirements
Non-Functional Requirements
This way, you will avoid processing costs of the machine.
Business rules
Problem
How am I going to write tests for a feature that doesn’t even exist yet?
Solution
For this, I will create a basic and minimal structure, so that it is possible to fully test a functionality even if it does not yet exist.
Create the MailProvider in the shared folder, since sending email is not necessarily just a password recovery request.
Create in @ shared / container / providers / MailProvider the models folders (rules that implementations must follow), implementations (which are the email service providers), fakes (for testing) .
// models / IMailProvider
export default interface IMailProvider {
sendMail ( to : string , body : string ) : Promise < void > ;
}
// fakes / FakeMailProvider
import IMailProvider from '../models/IMailProvider' ;
interface IMessage {
to : string ;
body : string ;
}
class FakeMailProvider implements IMailProvider {
private messages : IMessage [ ] = [ ] ;
public async sendMail ( to : string , body : string ) : Promise < void > {
this . messages . push ( {
to ,
body ,
} ) ;
}
}
export default FakeMailProvider ;
// SendForgotPasswordEmailService.ts
import { injectable , inject } from 'tsyringe' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IMailProvider from '@ shared / container / providers / MailProvider / models / IMailProvider' ;
interface IRequest {
email : string ;
}
@ injectable ( )
class SendForgotPasswordEmailService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'MailProvider' )
private mailProvider : IMailProvider
)
public async execute ( { email } : IRequest ) : Promise < void > {
this . mailProvider . sendMail ( email , 'Password recovery request' ) ;
} ;
}
export default SendForgotPasswordEmailService
// SendForgotPasswordEmailService.spec.ts
import SendForgotPasswordEmailService from './SendForgotPasswordEmailService' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import FakeMailProvider from '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ;
describe ( 'SendForgotPasswordEmail' , ( ) => {
it ( 'should be able to recover the password using email' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeMailProvider = new FakeMailProvider ( ) ;
const sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
) ;
const sendMail = jest . spyOn ( fakeMailProvider , 'sendMail' ) ;
await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ;
expect ( sendMail ) . toHaveBeenCalled ( ) ;
} )
} ) ;
describe ( 'SendForgotPasswordEmail' , ( ) => {
it ( 'should be able to recover the password using email' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeMailProvider = new FakeMailProvider ( ) ;
const sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
) ;
const sendMail = jest . spyOn ( fakeMailProvider , 'sendMail' ) ;
await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ;
expect ( sendMail ) . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should not be able to recover the password a non-existing user' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeMailProvider = new FakeMailProvider ( ) ;
const sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
) ;
await expect (
sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} )
} ) ;
If I run this new test ( should not be able to recover the password a non-existing user ), an ‘error’ will occur saying that: ‘an error was expected, but the promise has been resolved’. That is, instead of being reject , it was resolved .
If it is observed in the service SendForgotPasswordEmailService.ts , it is possible to see that there is no verification whether the user exists or not before sending the email, therefore, it must be added.
// SendForgotPasswordEmailService.ts
import { injectable , inject } from 'tsyringe' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IMailProvider from '@ shared / container / providers / MailProvider / models / IMailProvider' ;
interface IRequest {
email : string ;
}
@ injectable ( )
class SendForgotPasswordEmailService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'MailProvider' )
private mailProvider : IMailProvider
)
public async execute ( { email } : IRequest ) : Promise < void > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( ! checkUserExist ) {
throw new AppError ( 'User does not exists.' ) ;
}
this . mailProvider . sendMail ( email , 'Password recovery request' ) ;
} ;
}
export default SendForgotPasswordEmailService
Thus, it was possible to notice that the service was being built based on the application tests.
Problem
It was observed that in the link that the user will receive by email, it must have an encryption (token) so that it is not possible to change the password of another user, and to ensure that the password recovery has been provided with a request.
Solution
// @modules /users/infra/typeorm/entities/UserToken.ts
import {
Entity ,
PrimaryGeneratedColumn ,
Column ,
CreateDateColumn ,
UpdateDateColumn ,
Generated ,
} from 'typeorm' ;
@ Entity ( 'user_tokens' )
class UserToken {
@ PrimaryGeneratedColumn ( 'uuid' )
id : string ;
@ Column ( )
@ Generated ( 'uuid' )
token : string ;
@ Column ( )
user_id : string ;
@ CreateDateColumn ( )
created_at : Date ;
@ UpdateDateColumn ( )
updated_at : Date ;
}
export default UserToken ;
import UserToken from '../infra/typeorm/entities/UserToken' ;
export default interface IUserTokenRepository {
generate ( user_id : string ) : Promise < UserToken > ;
}
import { uuid } from 'uuidv4' ;
import UserToken from '../../infra/typeorm/entities/UserToken' ;
import IUserTokenRepository from '../IUserTokenRepository' ;
class FakeUserTokensRepository implements IUserTokenRepository {
public async generate ( user_id : string ) : Promise < UserToken > {
const userToken = new UserToken ( ) ;
Object . assign ( userToken , {
id : uuid ( ) ,
token : uuid ( ) ,
user_id ,
} )
return userToken ;
}
}
export default FakeUserTokensRepository ;
import { injectable , inject } from 'tsyringe' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IUserTokensRepository from '../repositories/IUserTokensRepository' ;
import IMailProvider from '@ shared / container / providers / MailProvider / models / IMailProvider' ;
interface IRequest {
email : string ;
}
@ injectable ( )
class SendForgotPasswordEmailService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'MailProvider' )
private mailProvider : IMailProvider ,
@ inject ( 'UserTokensRepository' )
private userTokensRepository : IUserTokensRepository ,
)
public async execute ( { email } : IRequest ) : Promise < void > {
const user = await this . usersRepository . findByEmail ( email ) ;
if ( ! user ) {
throw new AppError ( 'User does not exists.' ) ;
}
await this . userTokensRepository . generate ( user . id ) ;
await this . mailProvider . sendMail ( email , 'Password recovery request' ) ;
} ;
}
import AppError from '@ shared / errors / AppError' ;
import FakeMailProvider from '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ;
import FakeUserTokenRepository from '@ modules / users / repositories / fakes / FakeUserTokensRepository' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import SendForgotPasswordEmailService from './SendForgotPasswordEmailService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeUserTokensRepository : FakeUserTokenRepository ;
let fakeMailProvider : FakeMailProvider ;
let sendForgotPasswordEmail : SendForgotPasswordEmailService ;
describe ( 'SendForgotPasswordEmail' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeUserTokensRepository = new FakeUserTokenRepository ( ) ;
fakeMailProvider = new FakeMailProvider ( ) ;
sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
fakeUserTokensRepository ,
) ;
} ) ;
it ( 'should be able to recover the password using the email' , async ( ) => {
const sendEmail = jest . spyOn ( fakeMailProvider , 'sendMail' ) ;
await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ;
expect ( sendEmail ) . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should not be able to recover the password of a non-existing user' , async ( ) => {
await expect (
sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should generate a forgot password token' , async ( ) => {
const generateToken = jest . spyOn ( fakeUserTokensRepository , 'generate' ) ;
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( { email : 'test@example.com' } ) ;
expect ( generateToken ) . toHaveBeenCalledWith ( user . id ) ;
} ) ;
} ) ;
import UserToken from '../infra/typeorm/entities/UserToken' ;
export default interface IUserTokenRepository {
generate ( user_id : string ) : Promise < UserToken > ;
findByToken ( token : string ) : Promise < UserToken | undefined > ;
}
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import FakeUserTokensRepository from '../repositories/fakes/FakeUserTokensRepository' ;
import FakeHashProvider from '../provider/HashProvider/fakes/FakeHashProvider' ;
import ResetPasswordService from './ResetPasswordService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeUserTokensRepository : FakeUserTokensRepository ;
let resetPassword : ResetPasswordService ;
describe ( 'ResetPassword' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeUserTokensRepository = new FakeUserTokensRepository ( ) ;
resetPassword = new ResetPasswordService (
fakeUsersRepository ,
fakeUserTokensRepository ,
) ;
} )
it ( 'should be able to reset the password' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const generateHash = jest . spyOn ( fakeHashProvider , 'generateHash' ) ;
const { token } = await fakeUserTokensRepository . generate ( user . id ) ;
await resetPassword . execute ( {
token ,
password : '4444' ,
} ) ;
expect ( user . password ) . toBe ( '4444' ) ;
expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ;
} ) ;
} ) ;
import { injectable , inject } from 'tsyringe' ;
import AppError from '@ shared / errors / AppError' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IUserTokensRepository from '../repositories/IUserTokensRepository' ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
interface IRequest {
token : string ;
password : string ;
}
@ injectable ( )
class ResetPasswordService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'UserTokensRepository' )
private userTokensRepository : IUserTokensRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
)
public async execute ( { token , password } : IRequest ) : Promise < void > {
const userToken = await this . userTokensRepository . findByToken ( token ) ;
if ( ! userToken ) {
throw new AppError ( 'User Token does not exist.' ) ;
}
const user = await this . usersRepository . findById ( userToken . user_id ) ;
if ( ! user ) {
throw new AppError ( 'User does not exists.' ) ;
}
user . password = await this . hashProvider . generateHash ( password ) ;
await this . usersRepository . save ( user ) ;
}
}
export default ResetPasswordService ;
import AppError from '@ shared / errors / AppError' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import FakeUserTokensRepository from '../repositories/fakes/FakeUserTokensRepository' ;
import FakeHashProvider from '../provider/HashProvider/fakes/FakeHashProvider' ;
import ResetPasswordService from './ResetPasswordService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeUserTokensRepository : FakeUserTokensRepository ;
let resetPassword : ResetPasswordService ;
describe ( 'ResetPassword' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeUserTokensRepository = new FakeUserTokensRepository ( ) ;
resetPassword = new ResetPasswordService (
fakeUsersRepository ,
fakeUserTokensRepository ,
) ;
} )
it ( 'should be able to reset the password' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const generateHash = jest . spyOn ( fakeHashProvider , 'generateHash' ) ;
const { token } = await fakeUserTokensRepository . generate ( user . id ) ;
await resetPassword . execute ( {
token ,
password : '4444' ,
} ) ;
expect ( user . password ) . toBe ( '4444' ) ;
expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ;
} ) ;
it ( 'should not be able to reset the password with a non-existing user' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const { token } = await fakeUserTokensRepository . generate ( 'non-existing user' ) ;
await expect ( resetPassword . execute ( {
token ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should not be able to reset the password with a non-existing token' , ( ) => {
await expect ( resetPassword . execute ( {
token : 'non-existing-token' ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should not be able to reset the password if passed more than 2 hours' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
jest . spyOn ( Date , 'now' ) . mockImplementationOnce ( ( ) => {
const customDate = new Date ( ) ;
return customDate . setHours ( customDate . getHours ( ) + 3 ) ;
} ) ;
const { token } = await fakeUserTokensRepository . generate ( user . id ) ;
await expect ( resetPassword . execute ( {
token ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} )
} ) ;
import { uuid } from 'uuidv4' ;
import UserToken from '../../infra/typeorm/entities/UserToken' ;
import IUserTokenRepository from '../IUserTokenRepository' ;
class FakeUserTokensRepository implements IUserTokenRepository {
public async generate ( user_id : string ) : Promise < UserToken > {
const userToken = new UserToken ( ) ;
Object . assign ( userToken , {
id : uuid ( ) ,
token : uuid ( ) ,
user_id ,
created_at : new Date ( ) ,
updated_at : new Date ( ) ,
} )
return userToken ;
}
}
export default FakeUserTokensRepository ;
import { injectable , inject } from 'tsyringe' ;
import { isAfter , addHours } from 'date-fns' ;
import AppError from '@ shared / errors / AppError' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IUserTokensRepository from '../repositories/IUserTokensRepository' ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
interface IRequest {
token : string ;
password : string ;
}
@ injectable ( )
class ResetPasswordService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'UserTokensRepository' )
private userTokensRepository : IUserTokensRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
)
public async execute ( { token , password } : IRequest ) : Promise < void > {
const userToken = await this . userTokensRepository . findByToken ( token ) ;
if ( ! userToken ) {
throw new AppError ( 'User Token does not exist.' ) ;
}
const user = await this . usersRepository . findById ( userToken . user_id ) ;
if ( ! user ) {
throw new AppError ( 'User does not exists.' ) ;
}
const tokenCreateAt = userToken . created_at
const compareDate = addHours ( tokenCreateAt , 2 ) ;
if ( isAfter ( Date . now ( ) , compareDate ) ) {
throw new AppError ( 'Token expired.' ) ;
}
user . password = await this . hashProvider . generateHash ( password ) ;
await this . usersRepository . save ( user ) ;
}
}
export default ResetPasswordService ;
addHours adds hours on a specific date; isAfter will compare if the date the service was executed is greater than the date the token was created added to 2 hours. If this is true, it means that the tokenCreatedAt + 2 (hours) has passed the current time, that is, that the token has expired.
Remembering that the controllers of a Restful API must have a maximum of 5 methods:
Start by creating routes ( password.routes.ts ) and controllers ( ForgotPasswordController.ts , ResetPasswordControler.ts )
// ForgotPasswordController.ts
import { container } from 'tsyringe'
import { Request , Response } from 'express' ;
import ForgotPasswordEmailService from '@ modules / users / services / ForgotPasswordEmailService' ;
class ForgotPasswordController {
public async create ( request : Request , response : Response ) : Promise < Response > {
const { email } = request . body ;
const forgotPassword = container . resolve ( ForgotPasswordEmailService ) ;
await forgotPassword . execute ( {
email ,
} ) ;
return response . status ( 204 ) ;
}
}
export default ForgotPasswordController
// ResetPasswordController.ts
import { container } from 'tsyringe'
import { Request , Response } from 'express' ;
import ResetPasswordService from '@ modules / users / services / ResetPasswordService' ;
class ResetPasswordController {
public async create ( request : Request , response : Response ) : Promise < Response > {
const { token , password } = request . body ;
const resetPassword = container . resolve ( ResetPasswordService ) ;
await resetPassword . execute ( {
token ,
password ,
} ) ;
return response . status ( 400 ) . json ( ) ;
}
}
export default ResetPasswordController ;
// password.routes.ts
import { Router } from 'express' ;
import ResetPasswordController from '../controllers/ResetPasswordController' ;
import ForgotPasswordController from '../controllers/ForgotPasswordController' ;
const passwordRouter = Router ( ) ;
const forgotPasswordController = new ForgotPasswordController ( ) ;
const resetPasswordController = new ResetPasswordController ( ) ;
passwordRouter . post ( '/ reset' , resetPasswordController . create ) ;
passwordRouter . post ( '/ forgot' , forgotPasswordController . create ) ;
export default passwordRouter ;
Update the route index.ts
// @shared /infra/http/routes/index.ts
import { Router } from 'express' ;
import appointmentsRouter from '@ modules / appointments / infra / http / routes / appointments.routes' ;
import usersRouter from '@ modules / users / infra / http / routes / user.routes' ;
import sessionsRouter from '@ modules / users / infra / http / routes / sessions.routes' ;
import passwordRouter from '@ modules / users / infra / http / routes / password.routes' ;
const routes = Router ( ) ;
routes . use ( '/ appointments' , appointmentsRouter ) ;
routes . use ( '/ users' , usersRouter ) ;
routes . use ( '/ sessions' , sessionsRouter ) ;
routes . use ( '/ password' , passwordRouter ) ;
export default routes ;
Create the UserTokensRepository typeorm repository
// @modules /users/infra/typeorm/repositories/UserTokensRepository.ts
import { Repository , getRepository } from 'typeorm' ;
import UserToken from '../entities/UserToken' ;
import IUserTokensRepository from '@ modules / users / repositories / IUserTokensRepository' ;
class UserTokensRepository implements IUserTokensRepository {
private ormRepository : Repository < UserToken > ;
constructor ( ) {
this . ormRepository = getRepository ( UserToken ) ;
}
public async findByToken ( token : string ) : Promise < UserToken | undefined > {
const userToken = await this . ormRepository . findOne ( { where : { token } } ) ;
return userToken ;
}
public async generate ( user_id : string ) : Promise < UserToken > {
const userToken = this . ormRepository . create ( {
user_id ,
} ) ;
await this . ormRepository . save ( userToken ) ;
return userToken ;
}
}
export default UserTokensRepository ;
Create the migration
$ yarn typeorm migration: create -n CreateUserTokens
Go to @ shared / infra / typeorm / migrations / CreateUserTokens.ts
import { MigrationInterface , QueryRunner , Table } from 'typeorm' ;
export default class CreateUserTokens1597089880963
implements MigrationInterface {
public async up ( queryRunner : QueryRunner ) : Promise < void > {
await queryRunner . createTable ( new Table ( {
name : 'user_tokens' ,
columns : [
{
name : 'id' ,
type : 'uuid' ,
isPrimary :true ,
generatedStrategy : 'uuid' ,
default : 'uuid_generate_v4 ()' ,
} ,
{
name : 'token' ,
type : 'uuid' ,
generatedStrategy : 'uuid' ,
default : 'uuid_generate_v4 ()' ,
} ,
{
name : 'user_id' ,
type : 'uuid' ,
} ,
{
name : 'created_at' ,
type :'timestamp' ,
default : 'now ()' ,
} ,
{
name : 'updated_at' ,
type : 'timestamp' ,
default : 'now ()' ,
} ,
] ,
foreignKeys : [
{
name : 'TokenUser' ,
referencedTableName : 'users' ,
referencedColumnNames : [ 'id' ] ,
columnsName : [ 'user_id' ] ,
onDelete : 'CASCADE' ,
onUpdate : 'CASCADE' ,
}
] ,
} ) )
}
public async down ( queryRunner : QueryRunner ) : Promise < void > {
await queryRunner . dropTable ( 'user_tokens' ) ;
}
}
Go to @ shared / container / index.ts and add
import IUserTokensRepository from '@ modules / users / repositories / IUserTokensRepository' ;
import UserTokensRepository from '@ modules / users / infra / typeorm / repositories / UserTokensRepository' ;
container . registerSingleton < IUserTokensRepository > (
'UserTokensRepository' ,
UserTokensRepository ,
) ;
$ yarn add nodemailer
$ yarn add @ types / nodemailer -D
import nodemailer , { Transporter } from 'nodemailer' ;
import IMailProvider from '../models/IMailProvider' ;
class EtherealMailProvider implements {
private client : Transporter ;
constructor ( ){
nodemailer . createTestAccount ( ) . then ( account => {
const transporter = nodemailer . createTransport ( {
host : account . smtp . host ,
port : account . smtp . port ,
secure : account . smtp . secure ,
auth : {
user : account .user ,
pass : account . pass ,
} ,
} ) ;
this . client = transporter ;
} ) ;
}
public async sendMail ( to : string , body : string ) : Promise < void > {
const message = this . client . sendMail ( {
from : 'Team GoBarber <team@gobarber.com>' ,
to ,
subject : 'Recovery password' ,
text : body ,
} ) ;
console . log ( 'Message sent:% s' , message . messageId ) ;
console . log ( 'Preview URL:% s' , nodemailer . getTestMessageUrl ( message ) ) ;
}
}
export default EtherealMailProvider ;
import { container } from 'tsyringe' ;
import IMailProvider from './MailProvider/models/IMailProvider' ;
import EtherealMailProvider from './MailProvider/implementations/EtherealMailProvider' ;
container . registerInstance < IMailProvider > (
'MailProvider' ,
new EtherealMailProvider ( ) ,
)
I must use registerInstance , because with registerSingleton it was not working, as it is not instantiating EtherealMailProvider as it should happen.
...
const { token } = await this . userTokensRepository . generate ( user . id ) ;
await this . mailProvider . sendMail (
email ,
`Password recovery request: $ { token } ` ,
) ;
// dtos
interface ITemplateVariables {
[ key : string ] : string | number ;
}
export default interface IParseMailTemplateDTO {
template : string;
variables: ITemplateVariables ;
}
// models
import IParseMailTemplateDTO from '../dtos/IParseMailTemplateDTO' ;
export default interface IMailTemplateProvider {
parse ( data : IParseMailTemplateDTO ) : Promise < string > ;
}
// fakes
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class FakeMailTemplateProvider implements IMailTemplateProvider {
public async parse ( { template } : IMailTemplateProvider ) : Promise < string > {
return template ;
}
}
export default FakeMailTemplateProvider ;
// implementations
import handlebars from handlebars
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class HandlebarsMailTemplateProvider implements IMailTemplateProvider {
public async parse ( { template , variables } : IMailTemplateProvider ) : Promise < string > {
const parseTemplate = handlebars . compile ( template ) ;
return parseTemplate ( variables ) ;
}
}
export default HandlebarsMailTemplateProvider ;
import { container } from 'tsyringe' ;
import IMailTemplateProvider from './MailTemplateProvider/models/IMailTemplateProvider' ;
import HandlebarsMailTemplateProvider from './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;
container . registerSingleton < IMailTemplateProvider > (
'MailTemplateProvider' ,
HandlebarsMailTemplateProvider
) ;
MailTemplateProvider is directly associated with MailProvider , as MailTemplateProvider would not even exist if there were no MailProvider, so it will not be passed as a dependency injection of the SendForgotPasswordEmail service .
// @shared / container / providers / MailProvider / dtos / ISendMailDTO
import IParseMailTemplateDTO from '@ shared / container / providers / MailTemplateProvider / dtos / IParseMailTemplateDTO' ;
interface IMailContact {
name : string ;
email : string ;
}
export default interface ISendMailDTO {
to : IMailContact;
from ?: IMailContact ;
subject: string ;
templateData: IParseMailTemplateDTO ;
}
// fakes
import IMailProvider from '../models/IMailProvider' ;
import ISendMailDTO from '../dtos/ISendMailDTO' ;
class FakeMailProvider implements IMailProvider {
private messages : ISendMailDTO [ ] ;
public async sendMail ( message : ISendMailDTO ) : Promise < void > {
this . messages . push ( message ) ;
}
}
export default FakeMailProvider ;
Do the MailTemplateProvider dependency injection in MailProvider . One provider can depend on others, as MailTemplateProvider only exists if MailProvider also exists.
import { container } from 'tsyringe' ;
import IStorageProvider from './StorageProvider/models/IStorageProvider' ;
import DiskStorageProvider from './StorageProvider/implementations/DiskStorageProvider' ;
import IMailProvider from './MailProvider/models/IMailProvider' ;
import EtherealMailProvider from './MailProvider/implementations/EtherealMailProvider' ;
import IMailTemplateProvider from './MailTemplateProvider/models/IMailTemplateProvider' ;
import HandlebarsMailTemplateProvider from './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;
container . registerSingleton < IStorageProvider > (
'StorageProvider' ,
DiskStorageProvider ,
) ;
container . registerSingleton < IMailTemplateProvider > (
'MailTemplateProvider' ,
HandlebarsMailTemplateProvider ,
) ;
container . registerInstance < IMailProvider > (
'MailProvider' ,
container . resolve ( EtherealMailProvider ) ,
) ;
import nodemailer , { Transporter } from 'nodemailer' ;
import { injectable , inject } from 'tsyringe' ;
import IMailProvider from '../models/IMailProvider' ;
import ISendMailDTO from '../dtos/ISendMailDTO' ;
import IMailTemplateProvider from '@ shared / container / providers / MailTemplateProvider / models / IMailTemplateProvider' ;
@ injectable ( )
class EtherealMailProvider implements IMailProvider {
private client : Transporter ;
constructor (
@ inject ( 'MailTemplateProvider' )
private mailTemplateProvider : IMailTemplateProvider ,
) {
nodemailer . createTestAccount ( ) . then ( account => {
const transporter = nodemailer . createTransport ( {
host : account . smtp . host ,
port : account . smtp .port ,
secure : account . smtp . secure ,
auth : {
user : account . user ,
pass : account . pass ,
} ,
} ) ;
this . client = transporter ;
} ) ;
}
public async sendMail ( { to , from , subject , templateData } : ISendMailDTO ) : Promise < void > {
const message = await this . client . sendMail ( {
from : {
name : from ? . name || 'Team GoBarber' ,
address : from ? . email || 'team@gobarber.com.br' ,
} ,
to : {
name : to . name ,
address : to . email ,
} ,
subject ,
html : await this . mailTemplateProvider . parse ( templateData ) ,
} ) ;
console . log ( 'Message sent:% s' , message . messageId ) ;
console . log ( 'Preview URL:% s' , nodemailer . getTestMessageUrl ( message ) ) ;
}
}
export default EtherealMailProvider ;
Execute forgot password email and reset password on insomnia routes
<! - @ modules / users / views / forgot_password.hbs ->
< style >
.message-content { font-family : Arial , Helvetica , sans-serif ; max-width : 600 px ; font-size : 18 px ; line-height : 21 px ; }
</ style >
< div class = " message-content " >
< p > Hello, {{ name }} </ p >
< p > It looks like a password change for your account has been requested. </ p >
< p > If it was you, then click the link below to choose a new password </ p >
< p >
< A href = " {{ link }} " > Password Recovery </ a >
</ p >
< p > If it wasn't you, then discard that email </ p >
< p >
Thank you </ br >
< strong style = " color: ## FF9000 " > Team GoBarber </ strong >
</ p >
</ div >
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class FakeTemplateMailProvider implements IMailTemplateProvider {
public async parse ( ) : Promise < string > {
return 'Mail content' ;
}
}
export default FakeTemplateMailProvider ;
import handlebars from 'handlebars' ;
import fs from 'fs' ;
import IParseMailTemplateDTO from '../dtos/IParseMailTemplateDTO' ;
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class HandlebarsMailTemplateProvider implements IMailTemplateProvider {
public async parse ( {
file ,
variables ,
} : IParseMailTemplateDTO ) : Promise < string > {
const templateFileContent = await fs . promises . readFile ( file , {
encoding : 'utf-8' ,
} ) ;
const parseTemplate = handlebars . compile ( templateFileContent ) ;
return parseTemplate ( variables ) ;
}
}
import path from 'path' ;
const forgotPasswordTemplate = path . resolve (
__dirname ,
'..' ,
'views' ,
'forgot_password.hbs' ,
) ;
await this . mailProvider . sendMail ( {
to : {
name : user . name ,
email : user . email ,
} ,
subject : '[GoBarber] Password Recovery' ,
templateData : {
file : forgotPasswordTemplate ,
variables : {
name : user . name ,
link :`http: // localhost: 3000 / reset_password? token = $ { token } ` ,
} ,
} ,
Create the unit test very simple, as well as the service and gradually add the business rules.
Functional Requirements
Non-Functional Requirements
Business rules
Create UpdateProfileService.ts and UpdateProfileService.spec.ts .
Increase both the test and the Service so that the business rules are adequate.
Notes I make throughout my studies on:
Available at: Backend GoBarber
So far, the backend is separating files by type, for example: services, which is handling all business rules. Thus, the structure is disorganized, in case the structure grows.
Backend folder structure
From now on, they will be separated by Domain .
It is the area of knowledge of that module / file.
Example: User, all files related to this domain will be part of the same module.
Domain / Module Layer folder structure
Note: Previously, the User entity was part of the models folder , which basically has the same concept of entities : How we managed to represent information in the application.
Example: Database, errors, middlewares, routes and etc.
Shared folder structure
Example: Database, Automatic Email Service and so on …
Folder structure of the Infra Layer
Folder structure of the Domain Layer with the Infrastructure Layer
"./src"
{
"@modules/*": ["modules/*"],
"@config/*": ["config/*"],
"@shared/*": ["shared/*]
}
{
"build": "tsc",
"dev:server": "ts-node-dev -r tsconfig-paths/register --inspect --transpileOnly --ignore-watch node_modules src/shared/infra/http/server.ts",
"typeorm": "ts-node-dev -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
},
$ yarn add tsconfig-paths -D
// AppointmentsRepository that inherits methods from a standard TypeORM repository.
import { EntityRepository , Repository } from 'typeorm' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
@ EntityRepository ( Appointment )
class AppointmentsRepository extends Repository < Appointment > {
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = await this . findOne ( {
where : { date } ,
} ) ;
return findAppointment ;
}
}
export default AppointmentsRepository ;
import Appointment from '../infra/typeorm/entities/Appointment' ;
export default interface IAppointmentsRepository {
findByDate ( date : Date ) : Promise < Appointment | undefined > ;
}
import { EntityRepository , Repository } from 'typeorm' ;
import IAppointmentsRepository from '@ modules / appointments / repositories / IAppointmentsRepository' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
@ EntityRepository ( Appointment )
class AppointmentsRepository extends Repository < Appointment > implements IAppointmentsRepository {
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = await this . findOne ( {
where : { date } ,
}) ;
return findAppointment ;
}
}
export default AppointmentsRepository ;
When one class extends another, it means that it will inherit the methods. The implements, on the other hand, serve as a shape (format or rule) to be followed by the class, but not that it inherits its methods.
export default interface ICreateAppointmentDTO {
provider_id : string;
date: Date ;
}
import { getRepository , Repository } from 'typeorm' ;
import IAppointmentsRepository from '@ modules / appointments / repositories / IAppointmentsRepository' ;
import ICreateAppointmentDTO from '@ modules / appointments / dtos / ICreateAppointmentDTO' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
class AppointmentsRepository implements IAppointmentsRepository {
private ormRepository : Repository < Appointment > ;
constructor ( ) {
this . ormRepository = getRepository ( Appointment ) ;
}
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = await this . ormRepository . findOne ( {
where : { date } ,
} ) ;
return findAppointment ;
}
public async create ( { provider_id , date } : ICraeteAppointmentDTO ) : Promise < Appointment > {
const appointment = this . ormRepository . create ( {
provider_id ,
gives you
} ) ;
await this . ormRepository . save ( appointment ) ;
return appointment ;
}
}
export default AppointmentsRepository ;
This principle defines that:
That is, instead of my service depending directly on the typeorm repository , it now depends only on the interface of the repository, which will be passed through the route .
The route is part of the infra layer, which is the same as the TypeORM, so it is not necessary to apply the same principle to it. The service is part of the domain layer, so it must be decoupled from the infrastructure layer in such a way that it is not aware of its operation.
import { startOfHour } from 'date-fns' ;
import { getCustomRepository } from 'typeorm' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import AppointmentsRepository from '../repositories/AppointmentsRepository' ;
Request interface {
provider_id : string ;
date : Date ;
}
class CreateAppointmentService {
public async execute ( { provider_id , date } : Request ) : Promise < Appointment > {
// depending on the repository for the typeorm
const appointmentsRepository = getCustomRepository ( AppointmentsRepository ) ;
const appointmentDate = startOfHour ( date ) ;
const findAppointmentInSameDate = await appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await appointmentsRepository . create ( {
provider_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
import { startOfHour } from 'date-fns' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import IAppointmentsRepository from '../repositories/IAppointmentsRepository' ;
interface IRequest {
provider_id : string ;
date : Date ;
}
class CreateAppointmentService {
// depending only on the interface (abstraction) of the repository, which will be passed to the constructor when a new repository is instantiated on the route.
constructor ( private appointmentsRepository : IAppointmensRepository ) { }
public async execute ( { provider_id , date } : Request ) : Promise < Appointment > {
const appointmentDate = startOfHour ( date ) ;
const findAppointmentInSameDate = await this . appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await this . appointmentsRepository . create ( {
provider_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
Note: The Appointment entity is still provided by TypeORM, but in this case the Dependecy Inversion Principle will not be applied in order not to ‘hinder’ the understanding of this principle.
// modules / users / repositories / IUsersRepository
import User from '../infra/typeorm/entities/User' ;
import ICreateUserDTO from '../dtos/ICreateUserDTO' ;
export default interface IUsersRepository {
findByEmail ( email : string ) : Promise < User | undefined > ;
findById ( id : string ) : Promise < User | undefined > ;
create ( data : ICreateUserDTO ) : Promise < User > ;
save ( user : User ) : Promise < User > ;
}
// modules / users / dtos / ICreateUserDTO
export default interface ICreateUserDTO {
name : string;
email: string ;
password: string ;
}
// modules / users / infra / typeorm / repositories / UsersRepository
import { getRepository , Repository } from 'typeorm' ;
import User from '../entities/User' ;
import IUsersRepository from '@ modules / users / repositories / IUsersRepository' ;
class UsersRepository implements IUsersRepository {
private ormRepository : Repository < User > ;
constructor ( ) {
this . ormRepository = getRepository ( User ) ;
}
public async findByEmail ( email : string ) : Promise < User > {
const user = this . ormRepository . findOne ( { where : { email } } ) ;
return user ;
}
public async findById ( id : string ) : Promise < User > {
const user = this . ormRepository . findOne ( id ) ;
return user ;
}
public async create ( user : User ) : Promise < User > {
const user = this . ormRepository . create ( user ) ;
return this . ormRepository . save ( user ) ;
}
public async save ( user : User ) : Promise < User > {
return this . ormRepository . save ( user ) ;
}
}
import { hash } from 'bcryptjs' ;
import AppError from '@ shared / errors / AppError' ;
import User from '../infra/typeorm/entities/User' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
interface IRequest {
name : string ;
email : string ;
password : string ;
}
class CreateUserService {
constructor ( private usersRepository : IUsersRepository ) { }
public async execute ( { name , email , password } : IRequest ) : Promise < User > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( checkUserExist ) {
throw new AppError ( 'Email already used!' ) ;
}
const hashedPassword = await hash ( password , 8 ) ;
const user = await this . usersRepository . create ( {
name ,
email ,
password : hashedPassword ,
} ) ;
return user ;
}
}
export default CreateUserService ;
Make these changes to the other services.
import { container } from 'tsyringe' ;
import IAppointmentsRepository from '@ modules / appointsments / repositories / IAppointmentsRepository' ;
import IUsersRepository from '@ modules / users / repositories / IUsersRepository' ;
import AppointmentsRepository from '@ modules / appointments / infra / typeorm / repositories / AppointmentsRepository' ;
import UsersRepository from '@ modules / users / infra / typeorm / repositories / UsersRepository' ;
container . registerSingleton < IAppointmentsRepository > ( 'AppointmentsRepository' , AppointmentsRepository ) ;
container . registerSingleton < IUsersRepository > ( 'UsersRepository' , UsersRepository ) ;
import { startOfHour } from 'date-fns' ;
import { injectable , inject } from 'tsyringe' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import IAppointmentsRepository from '../repositories/IAppointmentsRepository' ;
interface IRequest {
provider_id : string ;
date : Date ;
}
// add the tsyringe decorator
@ injectable ( )
class CreateAppointmentService {
constructor (
// and will inject this dependency whenever the service is instantiated.
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository ,
) { }
public async execute ( { provider_id , date } : IRequest ) : Promise < Appointment > {
const appointmentDate = startOfHour ( date ) ;
const findAppointmentInSameDate = await this . appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await this . appointmentsRepository . create ( {
provider_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
import { Router } from 'express' ;
import { container } from 'tsyringe' ;
import CreateAppointmentService from '@ modules / appointments / services / CreateAppointmentService' ;
const appointmentsRouter = Router ( ) ;
appointmentsRouter . post ( '/' , async ( request , response ) => {
const { provider_id , date } = request . body ;
// const appointmentsRepository = new AppointmentsRepository ();
// const createAppointments = new CreateAppointmentService ();
// put it this way
const createAppointments = container . resolve ( CreateAppointmentService )
. . .
} ) ;
...
import '@ shared / container' ;
...
If more methods are needed, I must create a new controller.
// users / infra / http / controllers / SessionsController.ts
import { Request , Response } from 'express' ;
import { container } from 'tsyringe' ;
import AuthenticateUserService from '@ modules / users / services / AuthenticateUserService' ;
class SessionsController {
public async create ( request : Request , response : Response ) : Promise < Response > {
const { password , email } = request . body ;
const authenticateUser = container . resolve ( AuthenticateUserService ) ;
const { user , token } = await authenticateUser . execute ( { password , email } ) ;
delete user . password ;
return response . json ( { user , token } ) ;
}
}
export default SessionsController ;
// sessions.routes.ts
import { Router } from 'express' ;
import SessionsController from '../controllers/SessionsController' ;
const sessionsRouter = Router ( ) ;
const sessionsController = new SessionsController ( ) ;
sessionsRouter . post ( '/' , sessionsController . create ) ;
export default sessionsRouter ;
We have created tests to ensure that our application continues to function properly regardless of the number of functionality and the number of developers.
It does not depend on another part of the application or external service.
You will never own:
Side effect example: Trigger an email whenever a new user is created
It tests a complete functionality, passing through several layers of the application.
Rota -> Controller -> Service -> Repository -> Service -> …
Example: Creating a new user with sending email.
Example:
Example: When the user signs up for the application, they should receive a welcome email.
$ yarn add jest -D
$ yarn jest --init
$ yarn add @ types / jest ts-jest -D
test ( 'sum two numbers' , ( ) => {
expect ( 1 + 2 ) . toBe ( 3 ) ;
} ) ;
describe ( 'Create Appointment' , ( ) => {
it ( 'should be able to create a new appointment' , ( ) => {
} ) ;
} ) ;
// @modules / appointments / repositories / fakes / FakeAppointmentsRepository
import { uuid } from 'uuidv4' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointments' ;
import ICreateAppointmentDTO from '../dtos/ICreateAppointmentsDTO' ;
import IAppointmentsRepository from '../IAppointmentsRepository' ;
class FakeAppointmentsRepository implements IAppointmentsRepository {
private appointments : Appointments [ ] = [ ] ;
public async findByDate ( date : Date ) : Promise < Appointment | undefined > {
const findAppointment = this . appointments . find (
appointment => appointment . date === date ,
) ;
return findAppointment ;
}
public async create ( { provider_id , date } : ICreateAppointmentDTO ) : Promise < Appointment > {
const appointment = new Appointment ( ) ;
Object . assign ( appointment , { id : uuid ( ) , date , provider_id } ) ;
this . appointments . push ( appointment ) ;
return appointment ;
}
}
export default FakeAppointmentsRepository ;
import FakeAppointmentsRepository from '@ modules / appoinetments / repositories / fakes / FakeAppointmentsRepository' ;
import CreateAppointmentService from './CreateAppointmentService' ;
describe ( 'Create Appointment' , ( ) => {
it ( 'should be able to create a new appointment' , ( ) => {
const fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
const createAppointment = new CreateAppointmentService ( fakeAppointmentsRepository ) ;
const appointment = await createAppointment . execute ( {
date : new Date ( ) ,
provider_id : '4444'
} )
expect ( appointment ) . toHaveProperty ( 'id' ) ;
expect ( appointment . provider_id ) . toBe ( '4444' ) ;
} ) ;
} ) ;
When trying to execute, it will give error, because the test file does not understand the import @modules
const { pathsToModuleNameMapper } = require ( 'ts-jest / utils' ) ;
const { compilerOptions } = require ( './tsconfig.json' ) ;
module . exports = {
moduleNameMapper : pathsToModuleNameMapper ( compilerOptions . paths , { prefix : '<rootDir> / src /' } )
}
$ yarn test
collectCoverage : true ,
collectCoverageFrom : [
'<rootDir> /src/modules/**/*.ts'
] ,
coverageDir : ' coverage ' , // this folder will be created at the root that will hold the
coverageReporters reports : [
' text -summary ' ,
' lcov ' ,
] ,
$ yarn test
import AppError from '@ shared / errors / AppError' ;
import FakeAppointmentsRepository from '../repositories/fakes/FakeAppointmentsRepository' ;
import CreateAppointmentService from './CreateAppointmentService' ;
describe ( 'CreateAppointment' , ( ) => {
it ( 'should be able to create a new appointment' , async ( ) => {
const fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
const createAppointment = new CreateAppointmentService (
fakeAppointmentsRepository ,
) ;
const appointment = await createAppointment . execute ( {
date : new Date ( ) ,
provider_id : '4444' ,
} ) ;
expect ( appointment ) . toHaveProperty ( 'id' ) ;
expect ( appointment . provider_id ) . toBe ( '4444' ) ;
} ) ;
it ( 'should not be able to create two appointments at the same time' , async ( ) => {
const fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
const createAppointment = new CreateAppointmentService (
fakeAppointmentsRepository ,
) ;
const date = new Date ( 2020 , 5 , 10 , 14 ) ;
await createAppointment . execute ( {
date ,
provider_id : '4444' ,
} ) ;
expect (
createAppointment . execute ( {
date ,
provider_id : '4444' ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
} ) ;
// @modules /users/repositories/fakes/FakeUsersRepository.ts
import { uuid } from 'uuidv4' ;
import IUsersRepository from '../IUsersRepository' ;
import User from '../../infra/typeorm/entities/User' ;
import ICreateUserDTO from '../../dtos/ICreateUserDTO' ;
class FakeUsersRepository implements IUsersRepository {
private users : User [ ] = [ ] ;
public async findByEmail ( email : string ) : Promise < User | undefined > {
const findUser = this . users . find ( user => user . email === email ) ;
return findUser ;
}
public async findById ( id : string ) : Promise < User | undefined > {
const findUser = this . users . find ( user => user . id === id ) ;
return findUser ;
}
public async create ( userData : ICreateUserDTO ) : Promise < User > {
const user = new User ( ) ;
Object . assign ( user , { id : uuid ( ) } , userData ) ;
this . users . push ( user ) ;
return user ;
}
public async save ( user : User ) : Promise < User > {
const findIndex = this . users . findIndex ( findUser => findUser . id === user . id ) ;
this . users [ findIndex ] = user ;
return user ;
}
}
import AppError from '@ shared / errors / AppError' ;
import CreateUserService from './CreateUserService' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
describe ( 'CreateUser' , ( ) => {
it ( 'should be able to create a new user' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const createUser = new CreateUserService ( fakeUserRepository ) ;
const user = await createUser . execute ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456'
} ) ;
expect ( user ) . toHaveProperty ( 'id' ) ;
} ) ;
it ( 'should not be able to create two user with the same email' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const createUser = new CreateUserService ( fakeUserRepository ) ;
await createUser . execute ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456'
} ) ;
expect ( createUser . execute ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456'
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
} ) ;
$ yarn test
If there is an error related to reflect-metadata, go to jest.config.js and put
setupFiles : [
'reflect-metadata'
] ,
import CreateUserService from './CreateUserService' ;
import AuthenticateUserService from './AuthenticateUserService' ;
describe ( 'AuthenticateUser' , ( ) => {
it ( 'should be able to authenticate' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const createUser = new CreateUserService ( fakeUsersRepository ) ;
const authenticateUser = new AuthenticateUserService ( fakeUsersRepository) ) ;
const user = await createUser . execute ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} )
const authUser = await authenticateUser . execute ( {
email : 'testunit@example.com' ,
password : '123456' ,
} )
expect ( authUser ) . toHaveProperty ( 'token' ) ;
expect ( authUser . user ) . toEqual ( user ) ;
} ) ;
} ) ;
// CreateUserService
import { hash } from 'bcryptjs' ;
...
@ injectable ( )
class CreateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
) { }
public async execute ( { name , email , password } : IRequest ) : Promise < User > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( checkUserExist ) {
throw new AppError ( 'Email already used!' ) ;
}
const hashedPassword = await hash ( password , 8 ) ;
...
}
}
export default CreateUserService ;
import { compare } from 'bcryptjs' ;
...
@ injectable ( )
class AuthenticateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
) { }
public async execute ( { password , email } : IRequest ) : Promise < IResponse > {
const user = await this . usersRepository . findByEmail ( email ) ;
if ( ! user ) {
throw new AppError ( 'Incorrect email / password combination.' , 401 ) ;
}
const passwordMatched = await compare (
password ,
user . password ,
) ;
...
}
}
export default AuthenticateUserService ;
// @modules / users / providers / HashProvider / models
export default interface IHashProvider {
generateHash ( payload : string ) : Promise < string > ;
compareHash ( payload : string , hashed : string ) : Promise < boolean > ;
}
// @modules / users / provider / HashProvider / implementations
import { hash , compare } from 'bcrypjs' ;
import IHashProvider from '../models/IHashProvider' ;
class BCryptHashProvider implements IHashProvider {
public async generateHash ( payload : string ) : Promise < string > {
return hash ( payload , 8 ) ;
}
public async compareHash ( payload : string , hashed : string ) : Promise < string > {
return compare ( payload , hashed ) ;
}
}
export default BCryptHashProvider ;
// CreateUserService
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
@ injectable ( )
class CreateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
private hashProvider : IHashProvider ,
) { }
public async execute ( { name , email , password } : IRequest ) : Promise < User > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( checkUserExist ) {
throw new AppError ( 'Email already used!' ) ;
}
const hashedPassword = await this . hashProvider . generateHash ( password ) ;
const user = await this . usersRepository . create ( {
name ,
email ,
password : hashedPassword ,
} ) ;
return user ;
}
}
export default CreateUserService ;
// AuthenticateUserService
import { injectable , inject } from 'tsyringe' ;
import { sign } from 'jsonwebtoken' ;
import authConfig from '@ config / auth' ;
import AppError from '@ shared / errors / AppError' ;
import User from '../infra/typeorm/entities/User' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
interface IRequest {
email : string ;
password : string ;
}
IResponse interface {
user : User ;
token : string ;
}
@ injectable ( )
class AuthenticateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
private hashProvider : IHashProvider ,
) { }
public async execute ( { password , email } : IRequest ) : Promise < IResponse > {
const user = await this . usersRepository . findByEmail ( email ) ;
if ( ! user ) {
throw new AppError ( 'Incorrect email / password combination.' , 401 ) ;
}
const passwordMatched = await this . hashProvider . compareHash (
password ,
user . password ,
) ;
if ( ! passwordMatched ) {
throw new AppError ( 'Incorrect email / password combination.' , 401 ) ;
}
const { secret , expiresIn } = authConfig . jwt ;
const token = sign ( { } , secret , {
subject : user . id ,
expiresIn ,
} ) ;
return { user , token } ;
}
}
export default AuthenticateUserService ;
// @modules /users/provider/index.ts
import { container } from 'tsyringe' ;
import BCryptHashProvider from './HashProvider/implementations/BCryptHashProvider' ;
import IHashProvider from './HashProvider/models/IHashProvider' ;
container . registerSingleTon < IHashProvider > ( 'HashProvider' , BCryptHashProvider ) ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
@ injectable ( )
class CreateUserService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
) { }
...
}
}
export default CreateUserService ;
// @shared /container/index.ts
import '@ modules / users / providers' ;
...
import IHashProvider from '../models/IHashProvider' ;
class FakeHashProvider implements IHashProvider {
public async generateHash( payload : string ) : Promise < string > {
return payload ;
}
public async compareHash ( payload : string , hashed :string ) : Promise < boolean > {
return payload === hashed ;
}
} ;
export default FakeHashProvider ;
import CreateUserService from './CreateUserService' ;
import AuthenticateUserService from './AuthenticateUserService' ;
import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProviders' ;
describe ( 'AuthenticateUser' , ( ) => {
it ( 'should be able to authenticate' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeHashProvider = new FakeHashProvider ( ) ;
const createUser = new CreateUserService (
fakeUsersRepository ,
fakeHashProvider
) ;
const authenticateUser = new AuthenticateUserService (
fakeUsersRepository ,
fakeHashProvider
) ;
const user = await createUser . execute ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} )
const authUser = await authenticateUser . execute ( {
email : 'testunit@example.com' ,
password : '123456' ,
} )
expect ( authUser ) . toHaveProperty ( 'token' ) ;
expect ( authUser . user ) . toEqual ( user ) ;
} ) ;
} ) ;
class UpdateUserAvatarService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
) { }
public async execute ( { user_id , avatarFilename } : IRequest ) : Promise < User > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'Only authenticated user can change avatar' , 401 ) ;
}
// this direct dependency on the multer must be removed, as it ends up hurting liskov substitution and dependency inversion principle
if ( user . avatar ) {
const userAvatarFilePath = path . join ( uploadConfig . directory , user . avatar ) ;
const userAvatarFileExists = await fs . promises . stat ( userAvatarFilePath ) ;
if ( userAvatarFileExists ) {
await fs . promises . unlink ( userAvatarFilePath ) ;
}
user . avatar = avatarFilename ;
await this . usersRepository . save ( user ) ;
return user ;
}
}
}
export default UpdateUserAvatarService ;
//shared/container/providers/StorageProvider/models/IStorageProvider.ts
export default interface IStorageProvider {
saveFile ( file : string ) : Promise < string > ;
deleteFile ( file : string ) : Promise < string > ;
}
...
const tmpFolder = path . resolve ( __dirname , '..' , '..' , 'tmp' ) ;
export default {
tmpFolder ,
uploadsFolder : path . join ( tmpFolder , 'uploads' ) ;
...
}
//shared/container/providers/StorageProvider/implementations/DiskStorageProvider.ts
import fs from 'fs' ;
import path from 'path' ;
import uploadConfig from '@ config / upload' ;
import IStorageProvider from '../models/IStorageProvider' ;
class DiskStorageProvider implements IStorageProvider {
public async saveFile ( file : string ) : Promise < string > {
await fs . promises . rename (
path . resolve ( uploadConfig . tmpFolder , file ) ,
path . resolve ( uploadConfig . uploadsFolder , file ) ,
);
return file ;
}
public async deleteFile ( file : string ) : Promise < void > {
const filePath = path . resolves ( uploadConfig . uploadsFolder , file ) ;
try {
await fs . promises . stat ( filePath ) ;
} catch {
return ;
}
await fs . promises . unlink ( filePath ) ;
}
}
export default DiskStorageProvider ;
import { container } from 'tsyringe' ;
import IStorageProvider from './StorageProvider/models/IStorageProvider' ;
import DiskStorageProvider from './StorageProvider/implementations/DiskStorageProvider' ;
container . registerSingleton < IStorageProvider > (
'StorageProvider' ,
DiskStorageProvider ) ;
import path from 'path' ;
import fs from 'fs' ;
import uploadConfig from '@ config / upload' ;
import AppError from '@ shared / errors / AppError' ;
import { injectable , inject } from 'tsyringe' ;
import IStorageProvider from '@ shared / container / providers / StorageProvider / models / IStorageProvider' ;
import User from '../infra/typeorm/entities/User' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
interface IRequest {
user_id : string ;
avatarFilename : string ;
}
@ injectable ( )
class UpdateUserAvatarService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'StorageProvider' )
private storageProvider : IStorageProvider ,
) { }
public async execute ( { user_id , avatarFileName } : IRequest ) : Promise < User > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'Only authenticated user can change avatar' , 401 ) ;
}
if ( user . avatar ) {
await this . storageProvider . deleteFile ( user . avatar ) ;
}
const filePath = await this . storageProvider . saveFile ( avatarFileName ) ;
user . avatar = filePath ;
await this . usersRepository . save ( user ) ;
return user ;
}
}
export default UpdateUserAvatarService ;
// @shared /container/providers/StorageProvider/fakes/FakeStorageProvider.ts
import IStorageProvider from '../models/IStorageProvider' ;
class FakeStorageProvider implements IStorageProvider {
private storage : string [ ] = [ ] ;
public async saveFile ( file : string ) : Promise < string > {
this . storage . push ( file ) ;
return file ;
}
public async deleteFile ( file : string ) : Promise < void > {
const findIndex = this . storage . findIndex ( item => item === file ) ;
this . storage . splice ( findIndex , 1 ) ;
}
}
export default FakeStorageProvider ;
import AppError from '@ shared / errors / AppError' ;
import FakeUsersRepository from '../repositories/fakes/FakeUserRepository' ;
import FakeStorageProvider from '@ shared / container / providers / StorageProvider / fakes / FakeStorageProvider' ;
import UpdateUserAvatarService from './UpdateUserAvatarService' ;
describe ( 'UpdateUserAvatarService' , ( ) => {
it ( 'should be able to update user avatar' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeStorageProvider = new FakeStorageProvider ( ) ;
const updateUserAvatarService = new UpdateUserAvatarService (
fakeUsersRepository ,
fakeStorageProvider
) ;
const user = await fakeUsersRepository . create ( {
name : 'Test Unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await updateUserAvatarService . execute ( {
user_id : user . id ,
avatarFileName : 'avatar.jpg' ,
} )
expect ( user . avatar ) . toBe ( 'avatar.jpg' ) ;
} ) ;
it ( 'should not be able to update avatar from non existing user' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeStorageProvider = new FakeStorageProvider ( ) ;
const updateUserAvatarService = new UpdateUserAvatarService (
fakeUsersRepository ,
fakeStorageProvider
) ;
expect ( updateUserAvatarService . execute ( {
user_id : 'non-existing-id' ,
avatarFileName : 'avatar.jpg' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should delete old avatar when updating a new one' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeStorageProvider = new FakeStorageProvider ( ) ;
const updateUserAvatarService = new UpdateUserAvatarService (
fakeUsersRepository ,
fakeStorageProvider
) ;
const deleteFile = jest . spyOn ( fakeStorageProvider , 'deleteFile' ) ;
const user = await fakeUsersRepository . create ( {
name : 'Test Unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await updateUserAvatarService . execute ( {
user_id : user . id ,
avatarFileName : 'avatar.jpg' ,
} )
await updateUserAvatarService . execute ( {
user_id : user . id ,
avatarFileName : ' avatar2.jpg ' ,
} )
expect ( deleteFile ) . toHaveBeenCalledWithin ( 'avatar.jpg' ) ;
expect ( user . avatar ) . toBe ( ' avatar2.jpg ' ) ;
} ) ;
} ) ;
const deleteFile = jest.spyOn (fakeStorageProvider, ‘deleteFile’) checks whether the function was called or not, and in the end I hope it was called with the avatar.jpg parameter .
Made in Software Engineering.
I must :
- Mapear os Requisitos
- Conhecer as Regras de Negócio
In the real world, MANY MEETINGS must be held with the CUSTOMER so that MANY NOTES are made , to MAP FUNCTIONALITIES and BUSINESS RULES !
Is BASED on Feedback from the customer to be improved. I won’t always get it right the first time.
I must think of a project in a simple way . There is no point creating many features if the customer uses only 20% of them and wants them to be improved.
Create with Macro Functions in mind .
Inside they are well defined , but we can see them as an application screen .
And each macro functionality will be well described and documented, being divided into: Functional, Non-Functional Requirements and Business Rules.
Functional Requirements
Non-Functional Requirements
Example: if 400 users try to recover their password at the same time, it would be trying to send 400 emails at the same time , which could cause a slow service . Therefore, the approach adopted will be of queue (background job) , where the queue will be processed little by little by the tool.
Business rules
Functional Requirements
Non-Functional Requirements
Business rules
Functional Requirements
Non-Functional Requirements
It is possible that the provider is reloading the page many times throughout the day. Soon, the cache will be cleared and stored only when you have a new schedule.
Business rules
Functional Requirements
Non-Functional Requirements
This way, you will avoid processing costs of the machine.
Business rules
Problem
How am I going to write tests for a feature that doesn’t even exist yet?
Solution
For this, I will create a basic and minimal structure, so that it is possible to fully test a functionality even if it does not yet exist.
Create the MailProvider in the shared folder, since sending email is not necessarily just a password recovery request.
Create in @ shared / container / providers / MailProvider the models folders (rules that implementations must follow), implementations (which are the email service providers), fakes (for testing) .
// models / IMailProvider
export default interface IMailProvider {
sendMail ( to : string , body : string ) : Promise < void > ;
}
// fakes / FakeMailProvider
import IMailProvider from '../models/IMailProvider' ;
interface IMessage {
to : string ;
body : string ;
}
class FakeMailProvider implements IMailProvider {
private messages : IMessage [ ] = [ ] ;
public async sendMail ( to : string , body : string ) : Promise < void > {
this . messages . push ( {
to ,
body ,
} ) ;
}
}
export default FakeMailProvider ;
// SendForgotPasswordEmailService.ts
import { injectable , inject } from 'tsyringe' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IMailProvider from '@ shared / container / providers / MailProvider / models / IMailProvider' ;
interface IRequest {
email : string ;
}
@ injectable ( )
class SendForgotPasswordEmailService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'MailProvider' )
private mailProvider : IMailProvider
)
public async execute ( { email } : IRequest ) : Promise < void > {
this . mailProvider . sendMail ( email , 'Password recovery request' ) ;
} ;
}
export default SendForgotPasswordEmailService
// SendForgotPasswordEmailService.spec.ts
import SendForgotPasswordEmailService from './SendForgotPasswordEmailService' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import FakeMailProvider from '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ;
describe ( 'SendForgotPasswordEmail' , ( ) => {
it ( 'should be able to recover the password using email' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeMailProvider = new FakeMailProvider ( ) ;
const sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
) ;
const sendMail = jest . spyOn ( fakeMailProvider , 'sendMail' ) ;
await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ;
expect ( sendMail ) . toHaveBeenCalled ( ) ;
} )
} ) ;
describe ( 'SendForgotPasswordEmail' , ( ) => {
it ( 'should be able to recover the password using email' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeMailProvider = new FakeMailProvider ( ) ;
const sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
) ;
const sendMail = jest . spyOn ( fakeMailProvider , 'sendMail' ) ;
await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ;
expect ( sendMail ) . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should not be able to recover the password a non-existing user' , ( ) => {
const fakeUsersRepository = new FakeUsersRepository ( ) ;
const fakeMailProvider = new FakeMailProvider ( ) ;
const sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
) ;
await expect (
sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} )
} ) ;
If I run this new test ( should not be able to recover the password a non-existing user ), an ‘error’ will occur saying that: ‘an error was expected, but the promise has been resolved’. That is, instead of being reject , it was resolved .
If it is observed in the service SendForgotPasswordEmailService.ts , it is possible to see that there is no verification whether the user exists or not before sending the email, therefore, it must be added.
// SendForgotPasswordEmailService.ts
import { injectable , inject } from 'tsyringe' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IMailProvider from '@ shared / container / providers / MailProvider / models / IMailProvider' ;
interface IRequest {
email : string ;
}
@ injectable ( )
class SendForgotPasswordEmailService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'MailProvider' )
private mailProvider : IMailProvider
)
public async execute ( { email } : IRequest ) : Promise < void > {
const checkUserExist = await this . usersRepository . findByEmail ( email ) ;
if ( ! checkUserExist ) {
throw new AppError ( 'User does not exists.' ) ;
}
this . mailProvider . sendMail ( email , 'Password recovery request' ) ;
} ;
}
export default SendForgotPasswordEmailService
Thus, it was possible to notice that the service was being built based on the application tests.
Problem
It was observed that in the link that the user will receive by email, it must have an encryption (token) so that it is not possible to change the password of another user, and to ensure that the password recovery has been provided with a request.
Solution
// @modules /users/infra/typeorm/entities/UserToken.ts
import {
Entity ,
PrimaryGeneratedColumn ,
Column ,
CreateDateColumn ,
UpdateDateColumn ,
Generated ,
} from 'typeorm' ;
@ Entity ( 'user_tokens' )
class UserToken {
@ PrimaryGeneratedColumn ( 'uuid' )
id : string ;
@ Column ( )
@ Generated ( 'uuid' )
token : string ;
@ Column ( )
user_id : string ;
@ CreateDateColumn ( )
created_at : Date ;
@ UpdateDateColumn ( )
updated_at : Date ;
}
export default UserToken ;
import UserToken from '../infra/typeorm/entities/UserToken' ;
export default interface IUserTokenRepository {
generate ( user_id : string ) : Promise < UserToken > ;
}
import { uuid } from 'uuidv4' ;
import UserToken from '../../infra/typeorm/entities/UserToken' ;
import IUserTokenRepository from '../IUserTokenRepository' ;
class FakeUserTokensRepository implements IUserTokenRepository {
public async generate ( user_id : string ) : Promise < UserToken > {
const userToken = new UserToken ( ) ;
Object . assign ( userToken , {
id : uuid ( ) ,
token : uuid ( ) ,
user_id ,
} )
return userToken ;
}
}
export default FakeUserTokensRepository ;
import { injectable , inject } from 'tsyringe' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IUserTokensRepository from '../repositories/IUserTokensRepository' ;
import IMailProvider from '@ shared / container / providers / MailProvider / models / IMailProvider' ;
interface IRequest {
email : string ;
}
@ injectable ( )
class SendForgotPasswordEmailService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'MailProvider' )
private mailProvider : IMailProvider ,
@ inject ( 'UserTokensRepository' )
private userTokensRepository : IUserTokensRepository ,
)
public async execute ( { email } : IRequest ) : Promise < void > {
const user = await this . usersRepository . findByEmail ( email ) ;
if ( ! user ) {
throw new AppError ( 'User does not exists.' ) ;
}
await this . userTokensRepository . generate ( user . id ) ;
await this . mailProvider . sendMail ( email , 'Password recovery request' ) ;
} ;
}
import AppError from '@ shared / errors / AppError' ;
import FakeMailProvider from '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ;
import FakeUserTokenRepository from '@ modules / users / repositories / fakes / FakeUserTokensRepository' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import SendForgotPasswordEmailService from './SendForgotPasswordEmailService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeUserTokensRepository : FakeUserTokenRepository ;
let fakeMailProvider : FakeMailProvider ;
let sendForgotPasswordEmail : SendForgotPasswordEmailService ;
describe ( 'SendForgotPasswordEmail' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeUserTokensRepository = new FakeUserTokenRepository ( ) ;
fakeMailProvider = new FakeMailProvider ( ) ;
sendForgotPasswordEmail = new SendForgotPasswordEmailService (
fakeUsersRepository ,
fakeMailProvider ,
fakeUserTokensRepository ,
) ;
} ) ;
it ( 'should be able to recover the password using the email' , async ( ) => {
const sendEmail = jest . spyOn ( fakeMailProvider , 'sendMail' ) ;
await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ;
expect ( sendEmail ) . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should not be able to recover the password of a non-existing user' , async ( ) => {
await expect (
sendForgotPasswordEmail . execute ( {
email : 'test@example.com' ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should generate a forgot password token' , async ( ) => {
const generateToken = jest . spyOn ( fakeUserTokensRepository , 'generate' ) ;
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
await sendForgotPasswordEmail . execute ( { email : 'test@example.com' } ) ;
expect ( generateToken ) . toHaveBeenCalledWith ( user . id ) ;
} ) ;
} ) ;
import UserToken from '../infra/typeorm/entities/UserToken' ;
export default interface IUserTokenRepository {
generate ( user_id : string ) : Promise < UserToken > ;
findByToken ( token : string ) : Promise < UserToken | undefined > ;
}
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import FakeUserTokensRepository from '../repositories/fakes/FakeUserTokensRepository' ;
import FakeHashProvider from '../provider/HashProvider/fakes/FakeHashProvider' ;
import ResetPasswordService from './ResetPasswordService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeUserTokensRepository : FakeUserTokensRepository ;
let resetPassword : ResetPasswordService ;
describe ( 'ResetPassword' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeUserTokensRepository = new FakeUserTokensRepository ( ) ;
resetPassword = new ResetPasswordService (
fakeUsersRepository ,
fakeUserTokensRepository ,
) ;
} )
it ( 'should be able to reset the password' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const generateHash = jest . spyOn ( fakeHashProvider , 'generateHash' ) ;
const { token } = await fakeUserTokensRepository . generate ( user . id ) ;
await resetPassword . execute ( {
token ,
password : '4444' ,
} ) ;
expect ( user . password ) . toBe ( '4444' ) ;
expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ;
} ) ;
} ) ;
import { injectable , inject } from 'tsyringe' ;
import AppError from '@ shared / errors / AppError' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IUserTokensRepository from '../repositories/IUserTokensRepository' ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
interface IRequest {
token : string ;
password : string ;
}
@ injectable ( )
class ResetPasswordService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'UserTokensRepository' )
private userTokensRepository : IUserTokensRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
)
public async execute ( { token , password } : IRequest ) : Promise < void > {
const userToken = await this . userTokensRepository . findByToken ( token ) ;
if ( ! userToken ) {
throw new AppError ( 'User Token does not exist.' ) ;
}
const user = await this . usersRepository . findById ( userToken . user_id ) ;
if ( ! user ) {
throw new AppError ( 'User does not exists.' ) ;
}
user . password = await this . hashProvider . generateHash ( password ) ;
await this . usersRepository . save ( user ) ;
}
}
export default ResetPasswordService ;
import AppError from '@ shared / errors / AppError' ;
import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository' ;
import FakeUserTokensRepository from '../repositories/fakes/FakeUserTokensRepository' ;
import FakeHashProvider from '../provider/HashProvider/fakes/FakeHashProvider' ;
import ResetPasswordService from './ResetPasswordService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeUserTokensRepository : FakeUserTokensRepository ;
let resetPassword : ResetPasswordService ;
describe ( 'ResetPassword' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeUserTokensRepository = new FakeUserTokensRepository ( ) ;
resetPassword = new ResetPasswordService (
fakeUsersRepository ,
fakeUserTokensRepository ,
) ;
} )
it ( 'should be able to reset the password' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const generateHash = jest . spyOn ( fakeHashProvider , 'generateHash' ) ;
const { token } = await fakeUserTokensRepository . generate ( user . id ) ;
await resetPassword . execute ( {
token ,
password : '4444' ,
} ) ;
expect ( user . password ) . toBe ( '4444' ) ;
expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ;
} ) ;
it ( 'should not be able to reset the password with a non-existing user' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const { token } = await fakeUserTokensRepository . generate ( 'non-existing user' ) ;
await expect ( resetPassword . execute ( {
token ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should not be able to reset the password with a non-existing token' , ( ) => {
await expect ( resetPassword . execute ( {
token : 'non-existing-token' ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should not be able to reset the password if passed more than 2 hours' , ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
jest . spyOn ( Date , 'now' ) . mockImplementationOnce ( ( ) => {
const customDate = new Date ( ) ;
return customDate . setHours ( customDate . getHours ( ) + 3 ) ;
} ) ;
const { token } = await fakeUserTokensRepository . generate ( user . id ) ;
await expect ( resetPassword . execute ( {
token ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} )
} ) ;
import { uuid } from 'uuidv4' ;
import UserToken from '../../infra/typeorm/entities/UserToken' ;
import IUserTokenRepository from '../IUserTokenRepository' ;
class FakeUserTokensRepository implements IUserTokenRepository {
public async generate ( user_id : string ) : Promise < UserToken > {
const userToken = new UserToken ( ) ;
Object . assign ( userToken , {
id : uuid ( ) ,
token : uuid ( ) ,
user_id ,
created_at : new Date ( ) ,
updated_at : new Date ( ) ,
} )
return userToken ;
}
}
export default FakeUserTokensRepository ;
import { injectable , inject } from 'tsyringe' ;
import { isAfter , addHours } from 'date-fns' ;
import AppError from '@ shared / errors / AppError' ;
import IUsersRepository from '../repositories/IUsersRepository' ;
import IUserTokensRepository from '../repositories/IUserTokensRepository' ;
import IHashProvider from '../providers/HashProvider/models/IHashProvider' ;
interface IRequest {
token : string ;
password : string ;
}
@ injectable ( )
class ResetPasswordService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'UserTokensRepository' )
private userTokensRepository : IUserTokensRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
)
public async execute ( { token , password } : IRequest ) : Promise < void > {
const userToken = await this . userTokensRepository . findByToken ( token ) ;
if ( ! userToken ) {
throw new AppError ( 'User Token does not exist.' ) ;
}
const user = await this . usersRepository . findById ( userToken . user_id ) ;
if ( ! user ) {
throw new AppError ( 'User does not exists.' ) ;
}
const tokenCreateAt = userToken . created_at
const compareDate = addHours ( tokenCreateAt , 2 ) ;
if ( isAfter ( Date . now ( ) , compareDate ) ) {
throw new AppError ( 'Token expired.' ) ;
}
user . password = await this . hashProvider . generateHash ( password ) ;
await this . usersRepository . save ( user ) ;
}
}
export default ResetPasswordService ;
addHours adds hours on a specific date; isAfter will compare if the date the service was executed is greater than the date the token was created added to 2 hours. If this is true, it means that the tokenCreatedAt + 2 (hours) has passed the current time, that is, that the token has expired.
Remembering that the controllers of a Restful API must have a maximum of 5 methods:
Start by creating routes ( password.routes.ts ) and controllers ( ForgotPasswordController.ts , ResetPasswordControler.ts )
// ForgotPasswordController.ts
import { container } from 'tsyringe'
import { Request , Response } from 'express' ;
import ForgotPasswordEmailService from '@ modules / users / services / ForgotPasswordEmailService' ;
class ForgotPasswordController {
public async create ( request : Request , response : Response ) : Promise < Response > {
const { email } = request . body ;
const forgotPassword = container . resolve ( ForgotPasswordEmailService ) ;
await forgotPassword . execute ( {
email ,
} ) ;
return response . status ( 204 ) ;
}
}
export default ForgotPasswordController
// ResetPasswordController.ts
import { container } from 'tsyringe'
import { Request , Response } from 'express' ;
import ResetPasswordService from '@ modules / users / services / ResetPasswordService' ;
class ResetPasswordController {
public async create ( request : Request , response : Response ) : Promise < Response > {
const { token , password } = request . body ;
const resetPassword = container . resolve ( ResetPasswordService ) ;
await resetPassword . execute ( {
token ,
password ,
} ) ;
return response . status ( 400 ) . json ( ) ;
}
}
export default ResetPasswordController ;
// password.routes.ts
import { Router } from 'express' ;
import ResetPasswordController from '../controllers/ResetPasswordController' ;
import ForgotPasswordController from '../controllers/ForgotPasswordController' ;
const passwordRouter = Router ( ) ;
const forgotPasswordController = new ForgotPasswordController ( ) ;
const resetPasswordController = new ResetPasswordController ( ) ;
passwordRouter . post ( '/ reset' , resetPasswordController . create ) ;
passwordRouter . post ( '/ forgot' , forgotPasswordController . create ) ;
export default passwordRouter ;
Update the route index.ts
// @shared /infra/http/routes/index.ts
import { Router } from 'express' ;
import appointmentsRouter from '@ modules / appointments / infra / http / routes / appointments.routes' ;
import usersRouter from '@ modules / users / infra / http / routes / user.routes' ;
import sessionsRouter from '@ modules / users / infra / http / routes / sessions.routes' ;
import passwordRouter from '@ modules / users / infra / http / routes / password.routes' ;
const routes = Router ( ) ;
routes . use ( '/ appointments' , appointmentsRouter ) ;
routes . use ( '/ users' , usersRouter ) ;
routes . use ( '/ sessions' , sessionsRouter ) ;
routes . use ( '/ password' , passwordRouter ) ;
export default routes ;
Create the UserTokensRepository typeorm repository
// @modules /users/infra/typeorm/repositories/UserTokensRepository.ts
import { Repository , getRepository } from 'typeorm' ;
import UserToken from '../entities/UserToken' ;
import IUserTokensRepository from '@ modules / users / repositories / IUserTokensRepository' ;
class UserTokensRepository implements IUserTokensRepository {
private ormRepository : Repository < UserToken > ;
constructor ( ) {
this . ormRepository = getRepository ( UserToken ) ;
}
public async findByToken ( token : string ) : Promise < UserToken | undefined > {
const userToken = await this . ormRepository . findOne ( { where : { token } } ) ;
return userToken ;
}
public async generate ( user_id : string ) : Promise < UserToken > {
const userToken = this . ormRepository . create ( {
user_id ,
} ) ;
await this . ormRepository . save ( userToken ) ;
return userToken ;
}
}
export default UserTokensRepository ;
Create the migration
$ yarn typeorm migration: create -n CreateUserTokens
Go to @ shared / infra / typeorm / migrations / CreateUserTokens.ts
import { MigrationInterface , QueryRunner , Table } from 'typeorm' ;
export default class CreateUserTokens1597089880963
implements MigrationInterface {
public async up ( queryRunner : QueryRunner ) : Promise < void > {
await queryRunner . createTable ( new Table ( {
name : 'user_tokens' ,
columns : [
{
name : 'id' ,
type : 'uuid' ,
isPrimary :true ,
generatedStrategy : 'uuid' ,
default : 'uuid_generate_v4 ()' ,
} ,
{
name : 'token' ,
type : 'uuid' ,
generatedStrategy : 'uuid' ,
default : 'uuid_generate_v4 ()' ,
} ,
{
name : 'user_id' ,
type : 'uuid' ,
} ,
{
name : 'created_at' ,
type :'timestamp' ,
default : 'now ()' ,
} ,
{
name : 'updated_at' ,
type : 'timestamp' ,
default : 'now ()' ,
} ,
] ,
foreignKeys : [
{
name : 'TokenUser' ,
referencedTableName : 'users' ,
referencedColumnNames : [ 'id' ] ,
columnsName : [ 'user_id' ] ,
onDelete : 'CASCADE' ,
onUpdate : 'CASCADE' ,
}
] ,
} ) )
}
public async down ( queryRunner : QueryRunner ) : Promise < void > {
await queryRunner . dropTable ( 'user_tokens' ) ;
}
}
Go to @ shared / container / index.ts and add
import IUserTokensRepository from '@ modules / users / repositories / IUserTokensRepository' ;
import UserTokensRepository from '@ modules / users / infra / typeorm / repositories / UserTokensRepository' ;
container . registerSingleton < IUserTokensRepository > (
'UserTokensRepository' ,
UserTokensRepository ,
) ;
$ yarn add nodemailer
$ yarn add @ types / nodemailer -D
import nodemailer , { Transporter } from 'nodemailer' ;
import IMailProvider from '../models/IMailProvider' ;
class EtherealMailProvider implements {
private client : Transporter ;
constructor (){
nodemailer . createTestAccount ( ) . then ( account => {
const transporter = nodemailer . createTransport ( {
host : account . smtp . host ,
port : account . smtp . port ,
secure : account . smtp . secure ,
auth : {
user : account.user ,
pass : account . pass ,
} ,
} ) ;
this . client = transporter ;
} ) ;
}
public async sendMail ( to : string , body : string ) : Promise < void > {
const message = this . client . sendMail ( {
from : 'Team GoBarber <team@gobarber.com>' ,
to ,
subject : 'Recovery password' ,
text : body ,
} ) ;
console . log ( 'Message sent:% s' , message . messageId ) ;
console . log ( 'Preview URL:% s' , nodemailer . getTestMessageUrl ( message ) ) ;
}
}
export default EtherealMailProvider ;
import { container } from 'tsyringe' ;
import IMailProvider from './MailProvider/models/IMailProvider' ;
import EtherealMailProvider from './MailProvider/implementations/EtherealMailProvider' ;
container . registerInstance < IMailProvider > (
'MailProvider' ,
new EtherealMailProvider ( ) ,
)
I must use registerInstance , because with registerSingleton it was not working, as it is not instantiating EtherealMailProvider as it should happen.
...
const { token } = await this . userTokensRepository . generate ( user . id ) ;
await this . mailProvider . sendMail (
email ,
`Password recovery request: $ { token } ` ,
) ;
// dtos
interface ITemplateVariables {
[ key : string ] : string | number ;
}
export default interface IParseMailTemplateDTO {
template : string;
variables: ITemplateVariables ;
}
// models
import IParseMailTemplateDTO from '../dtos/IParseMailTemplateDTO' ;
export default interface IMailTemplateProvider {
parse ( data : IParseMailTemplateDTO ) : Promise < string > ;
}
// fakes
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class FakeMailTemplateProvider implements IMailTemplateProvider {
public async parse ( { template } : IMailTemplateProvider ) : Promise < string > {
return template ;
}
}
export default FakeMailTemplateProvider ;
// implementations
import handlebars from handlebars
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class HandlebarsMailTemplateProvider implements IMailTemplateProvider {
public async parse ( { template , variables } : IMailTemplateProvider ) : Promise < string > {
const parseTemplate = handlebars . compile ( template ) ;
return parseTemplate ( variables ) ;
}
}
export default HandlebarsMailTemplateProvider ;
import { container } from 'tsyringe' ;
import IMailTemplateProvider from './MailTemplateProvider/models/IMailTemplateProvider' ;
import HandlebarsMailTemplateProvider from './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;
container . registerSingleton < IMailTemplateProvider > (
'MailTemplateProvider' ,
HandlebarsMailTemplateProvider
) ;
MailTemplateProvider is directly associated with MailProvider , as MailTemplateProvider would not even exist if there were no MailProvider, so it will not be passed as a dependency injection of the SendForgotPasswordEmail service .
// @shared / container / providers / MailProvider / dtos / ISendMailDTO
import IParseMailTemplateDTO from '@ shared / container / providers / MailTemplateProvider / dtos / IParseMailTemplateDTO' ;
interface IMailContact {
name : string ;
email : string ;
}
export default interface ISendMailDTO {
to :IMailContact;
from ?: IMailContact ;
subject: string ;
templateData: IParseMailTemplateDTO ;
}
// fakes
import IMailProvider from '../models/IMailProvider' ;
import ISendMailDTO from '../dtos/ISendMailDTO' ;
class FakeMailProvider implements IMailProvider {
private messages : ISendMailDTO [ ] ;
public async sendMail ( message : ISendMailDTO ) : Promise < void > {
this . messages . push ( message ) ;
}
}
export default FakeMailProvider ;
Do the MailTemplateProvider dependency injection in MailProvider . One provider can depend on others, as MailTemplateProvider only exists if MailProvider also exists.
import { container } from 'tsyringe' ;
import IStorageProvider from './StorageProvider/models/IStorageProvider' ;
import DiskStorageProvider from './StorageProvider/implementations/DiskStorageProvider' ;
import IMailProvider from './MailProvider/models/IMailProvider' ;
import EtherealMailProvider from './MailProvider/implementations/EtherealMailProvider' ;
import IMailTemplateProvider from './MailTemplateProvider/models/IMailTemplateProvider' ;
import HandlebarsMailTemplateProvider from './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;
container . registerSingleton < IStorageProvider > (
'StorageProvider' ,
DiskStorageProvider ,
) ;
container . registerSingleton < IMailTemplateProvider > (
'MailTemplateProvider' ,
HandlebarsMailTemplateProvider ,
) ;
container . registerInstance < IMailProvider > (
'MailProvider' ,
container . resolve ( EtherealMailProvider ) ,
) ;
import nodemailer , { Transporter } from 'nodemailer' ;
import { injectable , inject } from 'tsyringe' ;
import IMailProvider from '../models/IMailProvider' ;
import ISendMailDTO from '../dtos/ISendMailDTO' ;
import IMailTemplateProvider from '@ shared / container / providers / MailTemplateProvider / models / IMailTemplateProvider' ;
@ injectable ( )
class EtherealMailProvider implements IMailProvider {
private client : Transporter ;
constructor (
@ inject ( 'MailTemplateProvider' )
private mailTemplateProvider : IMailTemplateProvider ,
) {
nodemailer . createTestAccount ( ) . then ( account => {
const transporter = nodemailer . createTransport ( {
host : account . smtp . host ,
port : account . smtp .port ,
secure : account . smtp . secure ,
auth : {
user : account . user ,
pass : account . pass ,
} ,
} ) ;
this . client = transporter ;
} ) ;
}
public async sendMail ( { to , from , subject , templateData } : ISendMailDTO ) : Promise < void > {
const message = await this . client . sendMail ( {
from : {
name : from ? . name || 'Team GoBarber' ,
address : from ? . email || 'team@gobarber.com.br' ,
} ,
to : {
name : to . name ,
address : to . email ,
} ,
subject ,
html : await this . mailTemplateProvider . parse ( templateData ) ,
} ) ;
console . log ( 'Message sent:% s' , message . messageId ) ;
console . log ( 'Preview URL:% s' , nodemailer . getTestMessageUrl ( message ) ) ;
}
}
export default EtherealMailProvider ;
Execute forgot password email and reset password on insomnia routes
<! - @ modules / users / views / forgot_password.hbs ->
< style >
.message-content { font-family : Arial , Helvetica , sans-serif ; max-width : 600 px ; font-size : 18 px ; line-height : 21 px ; }
</ style >
< div class = " message-content " >
< p > Hello, {{ name }} </ p >
< p > It looks like a password change for your account has been requested. </ p >
< p > If it was you, then click the link below to choose a new password </ p >
< p >
< A href = " {{ link }} " > Password Recovery </ a >
</ p >
< p > If it wasn't you, then discard that email </ p >
< p >
Thank you </ br >
< strong style = " color: ## FF9000 " > Team GoBarber </ strong >
</ p >
</ div >
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class FakeTemplateMailProvider implements IMailTemplateProvider {
public async parse ( ) : Promise < string > {
return 'Mail content' ;
}
}
export default FakeTemplateMailProvider ;
import handlebars from 'handlebars' ;
import fs from 'fs' ;
import IParseMailTemplateDTO from '../dtos/IParseMailTemplateDTO' ;
import IMailTemplateProvider from '../models/IMailTemplateProvider' ;
class HandlebarsMailTemplateProvider implements IMailTemplateProvider {
public async parse ( {
file ,
variables ,
} : IParseMailTemplateDTO ) : Promise < string > {
const templateFileContent = await fs . promises . readFile ( file , {
encoding : 'utf-8' ,
} ) ;
const parseTemplate = handlebars . compile ( templateFileContent ) ;
return parseTemplate ( variables ) ;
}
}
import path from 'path' ;
const forgotPasswordTemplate = path . resolve (
__dirname ,
'..' ,
'views' ,
'forgot_password.hbs' ,
) ;
await this . mailProvider . sendMail ( {
to : {
name : user . name ,
email : user . email ,
} ,
subject : '[GoBarber] Password Recovery' ,
templateData : {
file : forgotPasswordTemplate ,
variables : {
name : user . name ,
link :`http: // localhost: 3000 / reset_password? token = $ { token } ` ,
} ,
} ,
Create the unit test very simple, as well as the service and gradually add the business rules.
Functional Requirements
Non-Functional Requirements
Business rules
// @modules /users/services/UpdateProfileService.spec.ts;
import AppError from '@ shared / errors / AppError' ;
import FakeUsersRepository from '@ modules / users / repositories / fakes / FakeUsersRepository' ;
import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProvider' ;
import UpdateProfileService from './UpdateProfileService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeHashProvider : FakeHashProvider ;
let updateProfile : UpdateProfileService ;
describe ( 'UpdateProfile' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeHashProvider = new FakeHashProvider ( ) ;
updateProfile = new UpdateProfileService (
fakeUsersRepository ,
fakeHashProvider ,
) ;
} ) ;
it ( 'should be able to update the profile' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
const updateUser = await updateProfile . execute ( {
user_id : user . id ,
email : 'new-email' ,
name : 'new-name' ,
} ) ;
expect ( updateUser . name ) . toBe ( 'new-name' ) ;
expect ( updateUser . email ) . toBe ( 'new-email' ) ;
} ) ;
} ) ;
// @modules /users/services/UpdateProfileService.ts
interface IRequest {
user_id : string ;
name : string ;
email : string ;
old_password ?: string ;
password ?: string ;
}
class UpdateProfileService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
) { }
public async execute ( {
user_id ,
name ,
email ,
old_password ,
password ,
} : IRequest ) : Promise < void > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'User not found.' ) ;
}
user . email = email
user . name = name ;
return this . userRepository . save ( user ) ;
}
}
// @modules /users/services/UpdateProfileService.spec.ts;
import AppError from '@ shared / errors / AppError' ;
import FakeUsersRepository from '@ modules / users / repositories / fakes / FakeUsersRepository' ;
import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProvider' ;
import UpdateProfileService from './UpdateProfileService' ;
let fakeUsersRepository : FakeUsersRepository ;
let fakeHashProvider : FakeHashProvider ;
let updateProfile : UpdateProfileService ;
describe ( 'UpdateProfile' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
fakeHashProvider = new FakeHashProvider ( ) ;
updateProfile = new UpdateProfileService (
fakeUsersRepository ,
fakeHashProvider ,
) ;
} ) ;
it ( 'should be able to update the profile' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
const updateUser = await updateProfile . execute ( {
user_id : user . id ,
email : 'new-email' ,
name : 'new-name' ,
} ) ;
expect ( updateUser . name ) . toBe ( 'new-name' ) ;
expect ( updateUser . email ) . toBe ( 'new-email' ) ;
} ) ;
it ( 'should not be able to change the email using another user email' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit2@example.com' ,
password : '123456' ,
} ) ;
await expect ( await updateProfile . execute ( {
user_id : user . id ,
email : 'testunit2@example.com' ,
name : 'new-name' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should be able to update the password using old password' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : ' 123456 ' ,
} ) ;
const updateUser = await updateProfile . execute ( {
user_id : user . id ,
email : 'testunit2@example.com' ,
name : 'new-name' ,
old_password : '123456' ,
password : '4444' ,
} ) ;
expect ( updateUser . password ) . toBe ( '4444' ) ;
} ) ;
it ( 'should not be able to update the password using wrong old password' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await expect ( await updateProfile . execute ( {
user_id : user . id ,
email : 'testunit2@example.com' ,
name : 'new-name' ,
old_password : 'wrong-old-password' ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
it ( 'should not be able to update the password without old password' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test unit' ,
email : 'testunit@example.com' ,
password : '123456' ,
} ) ;
await expect ( await updateProfile . execute ( {
user_id : user . id ,
email : 'testunit2@example.com' ,
name : 'new-name' ,
password : '4444' ,
} ) ) . rejects . toBeInstanceOf ( AppError ) ;
} ) ;
} ) ;
// @modules /users/services/UpdateProfileService.ts
interface IRequest {
user_id : string ;
name : string ;
email : string ;
old_password ?: string ;
password ?: string ;
}
class UpdateProfileService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
@ inject ( 'HashProvider' )
private hashProvider : IHashProvider ,
) { }
public async execute ( {
user_id ,
name ,
email ,
old_password ,
password ,
} : IRequest ) : Promise < void > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'User not found.' ) ;
}
const checkEmailIsAlreadyUsed = await this . usersRepository . findByEmail (
email
) ;
if ( checkEmailIsAlreadyUsed && ckeckEmailIsAlreadyUsed . id ! == user . id ) {
throw new AppError ( 'E-mail already used.' ) ;
}
user . email = email
user . name = name ;
if ( password && ! old_password ) {
throw new AppError (
'You need to inform the old password to set a new password'
) ;
}
if ( password && old_password ) {
const checkPassword = await this . hashProvider . compare (
old_password ,
user . password
) ;
if ( ! checkPassword ) {
throw new AppError ( 'Old password does not match.' ) ;
}
user . password = await this . hashProvider . generateHash ( password ) ;
}
return this . userRepository . save ( user ) ;
}
}
// ShowProfileService.ts
interface IRequest {
user_id : string ;
}
class ShowProfileService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository
) { }
public async execute ( { user_id } : IRequest ) : Promise < User > {
const user = await this . usersRepository . findById ( user_id ) ;
if ( ! user ) {
throw new AppError ( 'User not found.' ) ;
}
return user ;
}
}
export default ShowProfileService ;
// ShowProfileService.spec.ts
describe ( 'ShowProfile' , ( ) => {
beforeEach ( ( ) => {
fakeUsersRepository = new FakeUsersRepository ( ) ;
showProfile = new ShowProfileService ( fakeUsersRepository ) ;
} ) .
it ( 'should be able to show the profile' , async ( ) => {
const user = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const profile = await showProfile . execute ( { user_id : user . id } ) ;
expect ( profile . name ) . toBe ( 'Test example' ) ;
expect ( profile . email ) . toBe ( 'test@example.com' ) ;
} ) ;
it ( 'should be able to show the profile from a non-existing user' , async ( ) => {
await expect ( showProfile . execute ( { user_id : user . id } ) )
. rejects
. toBeInstanceOf ( AppError ) ;
} ) ;
} )
class ProfileController {
public async show ( request : Request , response : Response ) : Promise < Response > {
const { user_id } = request . user . id ;
const showProfile = container . resolve ( ShowProfileService ) ;
const user = await showProfile . execute ( { user_id } ) ;
return response . json ( user ) ;
}
public async update ( request : Request , response : Response ) : Promise < Response > {
const { user_id } = request . user . id ;
const { email , password , old_password , name } = request . body ;
const updateProfile = container . resolve ( UpdateProfileService ) ;
const user = await updateProfileService . execute ( {
user_id ,
email ,
password ,
old_password ,
name
} ) ;
return response . json ( user ) ;
}
}
export default ProfileController ;
Users.routes.ts could be used, but it deals only with UsersController, which in turn deals with the methods of all users, not just the ones that are authenticated.
import { Router } from 'express' ;
import ProfileController from '../controller/ProfileController' ;
import ensureAuthenticated from '../middlewares/ensureAuthenticated' ;
const profileRouter = Router ( ) ;
const profileController = new ProfileController ( ) ;
profileRouter . use ( ensureAuthenticated ) ;
profileRouter . get ( '/' , profileController . show ) ;
profileRouter . put ( '/' , profileController . update ) ;
export default profileRouter ;
...
app . use ( '/ profile' , profileRouter ) ;
// listProvidersService.ts
interface IRequest {
user_id ?: string ;
}
class ListProvidersService {
constructor (
@ inject ( 'UsersRepository' )
private usersRepository : IUsersRepository ,
)
public async execute ( { user_id } : IRequest ) : Promise < User [ ] > {
const users = await this . usersRepository . findAllProviders ( {
except_user_id : user_id ,
} ) ;
return users ;
}
}
It will be more descriptive when I use the service and pass the user_id I’m receiving from the route as a parameter to the expect_user_id attribute .
export default interface IUsersRepository {
findAllProviders ( data : IFindAllProviders ) : Promise < User [ ] > ;
findById ( id : string ) : Promise < User | undefined > ;
findByEmail ( email : string ) : Promise < User | undefined > ;
create ( data: ICreateUserDTO ) : Promise < User > ;
save ( user : User ) : Promise < User > ;
}
export default interface IFindAllProviders {
except_user_id ?: string ;
}
class FakeUsersRepository implements IUsersRepository {
...
public async findAllProviders ( except_user_id ?: string ) : Promise < User [ ] > {
let { users } = this ;
if ( except_user_id ) {
users = this . users . filter ( user => user . id ! == except_user_id ) ;
}
return users ;
}
}
class UsersRepository implements IUsersRepository {
...
public async findAllProviders ( except_user_id ?: string ) : Promise < User [ ] > {
let users : User [ ] ;
if ( except_user_id ) {
users = await this . ormRepository . find ( {
where : {
id : Not ( except_user_id ) ,
}
} ) ;
} else {
users = await this . ormRepostiry . find ( ) ;
}
return users ;
}
}
describe ( 'ListProviders' , ( ) => {
beforeEach ( ( ) => { ... } ) ;
it ( 'should be able to list all providers' , async ( ) => {
const user1 = await fakeUsersRepository . create ( {
name : 'Test example' ,
email : 'test@example.com' ,
password : '123456' ,
} ) ;
const user2 = await fakeUsersRepository . create ( {
name : 'Test 2' ,
email : 'test2@example.com' ,
password : '123456' ,
} ) ;
const loggedUser = await fakeUsersRepository . create ( {
name : 'John Doe' ,
email : 'john@doe.com' ,
password : '123456' ,
} ) ;
const providers = await this . listProviders . execute ( {
user_id : loggedUser . id ,
} ) ;
expect ( providers ) . toBe ( [
user1 ,
user2 ,
] ) ;
} ) ;
} )
In simple cases like listing or something that does not involve a business rule, TDD does not necessarily have to be done before functionality !!
class ProvidersControllers {
public async index ( request : Request , response : Response ) : Promise < Response > {
const user_id = request . user . id ;
const listProviders = container . resolve ( ListProvidersService ) ;
const providers = await listProviders . execute ( { user_id } ) ;
return response . json ( providers ) ;
}
}
const providersRouter = Router ( ) ;
const providersController = new ProvidersController ( ) ;
providersRouter . use ( ensureAuthenticated ) ;
providersRouter . get ( '/' , providersControllers . index ) ;
export default providersRouter ;
interface IRequest {
provider_id : string ;
year : number ;
month : number ;
}
class ListProviderMonthAvailability {
constructor (
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository
)
public async execute ( { provider_id , year , month } : IRequest ) : Promise < void > {
const appointments = await this . appointmentsRepository . find ( ???? )
}
}
export default ListProviderMonthAvailability ;
There is still no method in the Repository interface to list all the schedules of a provider in a specific month
import { getMonth , getYear } from 'date-fns' ;
export default interface IFindAllInMonthFromProviderDTO {
provider_id : string;
year: number ;
month: number ;
}
class FakeAppointmentsRepository implements IAppointmentsRepository {
...
public async findAllInMonthFromProvider ( {
provider_id ,
year ,
month
} : IFindAllInMonthFromProviderDTO ) : Promise < Appointment [ ] > {
const appointments = this . appointments . filter ( appointment =>
appointment . provider_id === provider_id &&
getYear ( appointment . date ) === year &&
getMonth ( appointment . date ) + 1 === , ) month
return appointments ;
}
}
getMonth (appointment.date) + 1 because getMonth counts the months starting from 0. That is, 0 = January, 1 = February …
class AppointmentsRepository implements IAppointmentsRepository {
...
public async findAllInMonthFromProvider ( {
provider_id ,
year ,
month
} : IFindAllInMonthFromProviderDTO ) : Promise < Appointments [ ] > {
const parsedMonth = month . toString ( ) . padStart ( 2 , '0' ) ;
const appointments = await this . ormRepository . find ( {
where : {
provider_id ,
date : Raw ( dateFieldName =>
`to_char ( $ { dateFieldName } , 'MM-YYYY') = ' $ { parsedMonth } - $ { year } '`
)
}
} )
return appointments ;
}
}
Raw is a way of writing query sql in typeorm. This will make the ORM not even try to convert / interpret that query. You can receive a function or just the query. When a function is passed and I want to receive the name of that real column (because TypeORM changes the name of the columns and puts a nickname / alias), I passed the parameter dateFielName to_char (valor_que_quero_converter, forma_que_quero) converts a value to string. Just look in the documentation. When to_char converts to MM, it adds 0 in front of the value if it has only one decimal place. Example: 01 = January, 10 = October. So, I should take the month, convert it to a string and if it is not 2 in size, I would add ‘0’ at the beginning of it, using the padStart (2, ‘0’)
describe ( 'ListProviderMonthAvailability' , ( ) => {
beforeEach ( ( ) => {
fakeAppointmentsRepository = new FakeAppointmentsRepository ( ) ;
listProviderMonthAvailability = new ListProviderMonthAvailability (
fakeAppointmentsRepository ,
) ;
} ) ;
it ( 'should be able to list provider availability in month' , ( ) => {
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
date : new Date ( 2020 , 8 , 15 , 17 , 0 , 0 )) ,
} ) ;
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id',
date : new Date ( 2020 , 8 , 15 , 10 , 0 , 0 ) ,
} ) ;
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
date : new Date ( 2020 , 8 , 16 , 9 , 0 , 0 ) ,
} ) ;
const availability = await listProviderMonthAvailability . execute ( {
provider_id : 'provider-id' ,
month : 9 ,
year : 2020 ,
} ) ;
expect ( availability ) . toEqual ( expect . arrayContaining ( [
{
day : 14 ,
available : true ,
} ,
{
day : 15 ,
available : false ,
} ,
{
day : 16 ,
available : false ,
} ,
{
day : 17 ,
available : true ,
} ,
] ) )
} )
} )
interface IRequest {
provider_id : string ;
year : number ;
month : number ;
}
class ListProviderMonthAvailability {
constructor (
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository
)
public async execute ( { provider_id , year , month } : IRequest ) : Promise < void > {
const appointments = await this . appointmentsRepository . findAllInMonthFromProvider ( {
provider_id ,
year ,
month ,
} ) ;
console . log ( appointments ) ;
return [ { day : 1 , available : false } ] ;
}
}
export default ListProviderMonthAvailability ;
import { getDate , getDaysInMonth } from 'date-fns' ;
interface IRequest {
provider_id : string ;
year : number ;
month : number ;
}
class ListProviderMonthAvailability {
constructor (
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository
)
public async execute ( { provider_id , year , month } : IRequest ) : Promise < void > {
const appointments = await this . appointmentsRepository . findAllInMonthFromProvider ( {
provider_id ,
year ,
month ,
} ) ;
const numberOfDaysInMonth = getDaysInMonth ( new Date ( year , month - 1 ) ) ;
const eachDayArray = Array . from (
{ length : numberOfDaysInMonth } ,
( _ , index ) => index + 1 ,
) ;
const availability = eachDayArray . map ( day => {
const appointmentsInDay = appointments . filter ( appointment =>
getDay ( appointment . date ) === day
) ;
return {
day ,
available : appointmentsInDay . length < 10 ,
} ;
} )
return availability ;
}
}
export default ListProviderMonthAvailability ;
Array.from () creates an array from some options. As a first parameter, it receives an object where the length is the attribute. The second parameter is a function that contains (value, index).
export default interface IFindAllInDayFromProviderDTO {
provider_id : string;
year: number ;
month: number ;
day: number ;
}
export default interface IAppointmentsRepository {
...
findALlInDayFromProvider ( data : IFindAllInDayFromProviderDTO ) : Promise < Appointment [ ] > ;
}
class FakeAppointmentsRepository {
public async findAllInDayFromProvider ( {
provider_id ,
year ,
month ,
day
} : IFindAllInDayFromProviderDTO ) : Promise < Appointment [ ] > {
const appointmentsInDay = this . appointments . filter ( appointment =>
appointment . provider_id === provider_id &&
getDate ( appointment . date ) === day &&
getYear ( appointment . date ) === year &&
( getMonthappointment . date ) + 1 === month && ,
)
return appointmentsInDay ;
}
}
export default FakeAppointmentsRepository ;
class AppointmentsRepository {
public async findAllInDayFromProvider ( {
provider_id ,
year ,
month ,
day
} : IFindAllInDayFromProviderDTO ) : Promise < Appointment [ ] > {
const parsedDay = day . toString ( ) . padStart ( 2 , '0' ) ;
const parsedMonth = month . toString ( ) . padStart ( 2 , '0' ) ;
const appointments = await this . ormRepository . find ( {
where : { provider_id ,
date : Raw ( dateFieldName =>
`to_char ( $ { dateFieldName } , 'DD-MM-YYYY') = ' $ { parsedDay } - $ { parsedMonth } - $ { year } ' '
) ,
} ,
} ) ;
return appointments ;
}
}
class ListProviderDayAvailabilityService {
public async execute ( {
provider_id ,
year ,
month ,
day ,
} : IFindAllInDayFromProvider ) : Promise < Appointemnt [ ] > {
const appointmentsInDay = await this . appointmentsRepository . findAllInDayFromProvider ( {
provider_id ,
year ,
month ,
day ,
} ) ;
const hourStart = 8 ;
const eachHour = Array . from (
{ length : 10 } ,
( _ , index ) => index + 8 ) ,
) ;
const availability = eachHour ( hour => {
const hasAppointmentInHour = appointments . find ( appointment =>
getHours ( new Date ( appointment . date ) ) === hour ;
) ;
return {
hour ,
available : hasAppoinmentInHour ,
} ;
} ) ;
return availability ;
}
}
export default ListProviderDayAvailabilityService ;
describe ( 'ListProviderDayAvailability' , ( ) => {
beforeEach ( '' , ( ) =>) ;
it ( 'should be able to list day availability of a provider' , ( ) => {
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
date : new Date ( 2020 , 7 , 16 , 9 , 0 , 0 ) ,
} ) ;
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
date : new Date ( 2020 , 7 , 16 , 10 , 0 , 0 ) ,
} ) ;
const availability = await listProviderDayAvailability . execute ( {
provider_id : 'provider-id' ,
year : 2020 ,
month : 8 ,
day : 16 ,
} ) ;
expect ( availability ) . toEqual ( expect . arrayContaining ( [ {
{
hour : 8 ,
available : true ,
} ,
{
hour : 9 ,
available : false ,
} ,
{
hour : 10 ,
available : false ,
} ,
{
hour : 11 ,
available : true ,
} ,
} ] )
) ;
} ) ;
} )
When working to get the current time, use the new Date (Date.now ()) , as it is easier to apply a mock (as used in the test password reset) and change the date as necessary.
class ListProviderDayAvailabilityService {
public async execute ( {
provider_id ,
year ,
month ,
day ,
} : IFindAllInDayFromProvider ) : Promise < Appointemnt [ ] > {
const appointmentsInDay = await this . appointmentsRepository . findAllInDayFromProvider ( {
provider_id ,
year ,
month,
day ,
} ) ;
const hourStart = 8 ;
const eachHour = Array . from (
{ length : 10 } ,
( _ , index ) => index + 8 ) ,
) ;
const currentDate = new Date ( Date . now ( ) ) ;
const availability = eachHour ( hour => {
const hasAppointmentInHour = appointments . find ( appointment =>
getHours ( new Date ( appointment . date ) ) === hour ;
) ;
const compareDate = new Date ( year , month - 1 , day , hour ) ;
return {
hour ,
available : hasAppoinmentInHour && isAfter ( compareDate , currentDate ) ,
} ;
} ) ;
return availability ;
}
}
export default ListProviderDayAvailabilityService ;
it ( 'should be able to list the day availability of a provider' , async ( ) => {
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
date : new Date ( 2020 , 7 , 16 , 9 , 0 , 0 ) ,
} ) ;
await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
date : new Date ( 2020 , 7 , 16 , 11 , 0 , 0 ) ,
} ) ;
jest . spyOn ( Date , 'now' ) . mockImplementationOnce ( ( ) => {
return new Date ( 2020 , 7 , 16 , 10 ) . getTime ( ) ;
} )
const availability = await listProviderDayAvailability . execute ( {
provider_id : 'provider-id' ,
month : 8 ,
year : 2020 ,
day : 16 ,
} ) ;
expect ( availability ) . toEqual (
expect . arrayContaining ( [
{
hour : 8 ,
available : false ,
} ,
{
hour : 9 ,
available : false ,
} ,
{
hour : 10 ,
available : false ,
} ,
{
hour : 11 ,
available : false,
} ,
{
hour : 16 ,
available : true ,
} ,
{
hour : 17 ,
available : true ,
} ,
] ) ,
) ;
} ) ;
It must be refactored, because when creating a schedule, the user is able to make an appointment with himself.
$ yarn typeorm migration: create -n AddUserIdToAppointments
import {
MigrationInterface ,
QueryRunner ,
TableColumn ,
TableForeignKey ,
} from 'typeorm' ;
export default class AddUserIdToAppointments
implements MigrationInterface {
public async up ( queryRunner : QueryRunner ) : Promise < void > {
await queryRunner . addColumn ( 'appointments' ,
new TableColumn ( {
name : 'user_id' ,
type : 'uuid' ,
isNullable : true ,
} ) ,
);
await queryRunner . addForeignKey ( 'appointments' ,
new TableForeignKey ( {
name : 'ApppointmentUser' ,
columnNames : [ 'user_id' ] ,
referencedColumnNames : [ 'id' ] ,
referencedTableName : 'users' ,
onDelete : 'SET NULL' ,
onUpdate : 'CASCADE' ,
} ) ,
) ;
}
public async down ( queryRunner : QueryRunner ) : Promise < void > {
await queryRunner . dropForeignKey ( 'appointments' , 'AppointmentUser' ) ;
await queryRunner . dropColumn ( 'appointments' , 'user_id' ) ;
}
}
import {
Entity ,
Column ,
PrimaryGeneratedColumn ,
CreateDateColumn ,
UpdateDateColumn ,
ManyToOne ,
JoinColumn ,
} from 'typeorm' ;
import User from '@ modules / users / infra / typeorm / entities / User' ;
@ Entity ( 'appointments' )
class Appointment {
@ PrimaryGeneratedColumn ( 'uuid' )
id : string ;
@ Column ( )
provider_id : string ;
@ ManyToOne ( ( ) => User )
@ JoinColumn ( { name : 'provider_id' } )
provider : User ;
// map the new column created in the user_id database
// will be converted to a column in the database
@ Column ( )
user_id : string ;
// relationship that exists only here in javascript, but not in the bank
// multiple schedules for a user
@ ManyToOne ( ( ) => User )
// which column in this table makes the relationship
@ JoinColumn ( { name : 'user_id' } )
user : User ;
@ Column ( 'timestamp with time zone' )
date : Date ;
@ CreateDateColumn ( )
created_at : Date ;
@ UpdateDateColumn ( )
updated_at : Date ;
}
export default Appointment ;
jest
. spyOn ( Date , 'now' )
. mockImplementationOnce ( ( ) => new Date ( 2020 , 7 , 15 , 10 ) . getTime ( ) ) ;
await expect (
createAppointment . execute ( {
provider_id : 'provider-id' ,
user_id : 'user-id' ,
date : new Date ( 2020 , 7 , 15 , 9 ) ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
jest
. spyOn ( Date , 'now' )
. mockImplementationOnce ( ( ) => new Date ( 2020 , 7 , 15 , 10 ) . getTime ( ) ) ;
await expect (
createAppointment . execute ( {
provider_id : 'provider-id' ,
user_id : 'provider-id' ,
date : new Date ( 2020 , 7 , 15 , 11 ) ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
jest
. spyOn ( Date , 'now' )
. mockImplementationOnce ( ( ) => new Date ( 2020 , 7 , 15 , 10 ) . getTime ( ) ) ;
await expect (
createAppointment . execute ( {
provider_id : 'provider-id' ,
user_id : 'user-id' ,
date : new Date ( 2020 , 7 , 15 , 7 ) ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
await expect (
createAppointment . execute ( {
provider_id : 'provider-id' ,
user_id : 'user-id' ,
date : new Date ( 2020 , 7 , 15 , 18 ) ,
} ) ,
) . rejects . toBeInstanceOf ( AppError ) ;
import { startOfHour , isBefore , getHours } from 'date-fns' ;
import { injectable , inject } from 'tsyringe' ;
import Appointment from '@ modules / appointments / infra / typeorm / entities / Appointment' ;
import AppError from '@ shared / errors / AppError' ;
import IAppointmentsRepository from '../repositories/IAppointmentsRepository' ;
interface IRequest {
provider_id : string ;
user_id : string ;
date : Date ;
}
@ injectable ( )
class CreateAppointmentService {
constructor (
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository ,
) { }
public async execute ( {
provider_id ,
date ,
user_id ,
} : IRequest ) : Promise < Appointment > {
const appointmentDate = startOfHour ( date ) ;
if ( isBefore ( appointmentDate , Date . now ( ) ) ) {
throw new AppError ( "You can't create an appointment on a past date." ) ;
}
if ( provider_id === user_id ) {
throw new AppError ( "You can't create an appointment with yourself." ) ;
}
if ( getHours ( appointmentDate ) < 8 || getHours ( appointmentDate ) > 17 ) {
throw new AppError ( 'You can only create an appointment between 8am and 5pm.' ) ;
}
const findAppointmentInSameDate = await this . appointmentsRepository . findByDate (
appointmentDate ,
) ;
if ( findAppointmentInSameDate ) {
throw new AppError ( 'This Appointment is already booked!' ) ;
}
const appointment = await this . appointmentsRepository . create ( {
provider_id ,
user_id ,
date : appointmentDate ,
} ) ;
return appointment ;
}
}
export default CreateAppointmentService ;
Who should finish if the business rules are working are the tests
import { Request , Response } from 'express' ;
import { container } from 'tsyringe' ;
import ListProviderDayAvailabilityService from '@ modules / appointments / services / ListProviderDayAvailabilityService' ;
class ProviderDayAvailabilityController {
public async index ( request : Request , response : Response ) : Promise < Response > {
const { provider_id } = request . params ;
const { year , month , day } = request . body ;
const listProviderDayAvailability = container . resolve ( ListProviderDayAvailabilityService ) ;
const availability = await listProviderDayAvailabilityService . execute ( {
provider_id ,
year ,
month ,
day ,
} ) ;
return response . json ( availability ) ;
}
}
export default ProviderDayAvailabilityController ;
import { Router } from 'express' ;
import ensureAuthenticated from '@ modules / users / infra / http / middlewares / ensureAuthenticated' ;
import ProvidersController from '../controllers/ProvidersController' ;
import ProviderDayAvailabilityController from '../controllers/ProviderDayAvailabilityController' ;
import ProviderMonthAvailabilityController from '../controllers/ProviderMonthAvailabilityController' ;
const providersRouter = Router ( ) ;
const providersController = new ProvidersController ( ) ;
const providersDayAvailabilityController = new ProviderDayAvailabilityController ( ) ;
const providersMonthAvailabilityController = new ProviderMonthAvailabilityController ( ) ;
providersRouter . use ( ensureAuthenticated ) ;
providersRouter . get ( '/' , providersController . index ) ;
providersRouter . get (
'/: provider_id / day-availability' ,
providersDayAvailabilityController . index ,
) ;
providersRouter . get (
'/: provider_id / month-availability' ,
providersMonthAvailabilityController . index ,
) ;
export default providersRouter ;
class ListProviderAppointmentService {
constructor (
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository ,
) { }
public async execute ( {
provider_id ,
year ,
month ,
day ,
} : IRequest ) : Promise < Appointment [ ] > {
const appointments = await this . appointmentsRepository . findAllInDayFromProvider ( {
provider_id ,
year ,
month ,
day
} ) ;
return appointments ;
}
}
export default ListProviderAppointmentsService ;
describe ( 'ListProviderAppointments' , ( ) => {
beforeEach ( ( ) => { } ) ;
it ( 'should be able to list the appointments on a specific day' , ( ) => {
const appointment1 = await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
user_id : 'user-id' ,
date : new Date ( 2020 , 8 , 18 , 10 ) ,
} ) ;
const appointment2 = await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
user_id : 'user-id' ,
date : new Date ( 2020 , 8 , 18 , 11 ) ,
} ) ;
const appointment3 = await fakeAppointmentsRepository . create ( {
provider_id : 'provider-id' ,
user_id : 'user-id' ,
date :new Date ( 2020 , 8 , 18 , 12 ) ,
} ) ;
const appointments = await listProviderAppointments . execute ( {
provider_id : 'provider-id' ,
year : 2020 ,
month : 9 ,
day : 18 ,
} ) ;
expect ( appointments ) . toEqual ( [
appointments1 ,
appointments2 ,
appointments3 ,
] ) ;
} ) ;
} )
class ProviderAppointmentsController {
public async index ( request : Request , response : Response ) : Promise < Response > {
const provider_id = request . user . id ;
const { year , month , day } = request . body ;
const listProviderAppointments = container . resolve (
ListProviderAppointmentsService
) ;
const appointments = await listProviderAppointments . execute ( {
provider_id ,
year ,
month ,
day ,
} ) ;
return response . json ( appointments ) ;
}
}
export default ProviderAppointmentsController ;
import { Router } from 'express' ;
import ensureAuthenticated from '@ modules / users / infra / http / middlewares / ensureAuthenticated' ;
import AppointmentsController from '../controllers/AppointmentsController' ;
import ProviderAppointmentsController from '../controllers/ProviderAppointmentsController' ;
const appointmentsRouter = Router ( ) ;
const appointmentsController = new AppointmentsController ( ) ;
const providerAppointmentsController = new ProviderAppointmentsController ( ) ;
appointmentsRouter . use ( '/' , ensureAuthenticated ) ;
appointmentsRouter . post ( '/' , appointmentsController . create ) ;
appointmentsRouter . get ( '/ me' , providerAppointmentsController . index ) ;
export default appointmentsRouter ;
Why and when to use MongoDB?
For example, in Rocketseat PostgreSQL is used to store data about:
That is, it is used when I want to:
For example, in the case of notifications, I don’t need to keep a topic’s ID, I can simply store the topic’s name. If I need to relate something to the topic, just pass the name of the topic instead of a key or something.
We must save the raw data
Installing MongoDB
$ docker run --name mongodb -p 27017: 27017 -d -t mongo
Graphical Interface for MongoDB
Connect with bank
mongodb: // localhost: 27017
[
{
" name " : " default " ,
" type " : " postgres " ,
" host " : " localhost " ,
" port " : 5434 ,
" username " : " postgres " ,
" password " : " docker " ,
" database " :" gostack_gobarber ",
" entities " : [
" ./src/modules/**/infra/typeorm/entities/*.ts "
],
" migrations " : [
" ./src/shared/infra/typeorm/migrations/*.ts "
],
" cli " : {
" migrationsDir " : " ./src/shared/infra/typeorm/migrations "
}
},
{
" name " : " mongo " ,
" type " : " mongodb " ,
" host " : " localhost " ,
" port " : 27017 ,
" database " : " gobarber " ,
" useUnifiedTopology " : true ,
" entities " : [
" ./src/modules/**/infra/typeorm/schemas/*.ts "
]
}
]
$ yarn add mongodb
import {
ObjectID ,
ObjectIdColumn ,
Column ,
Entity ,
CreateDateColumn ,
UpdateDateColumn ,
} from 'typeorm' ;
@ Entity ( 'notifications' )
class Notification {
@ ObjectIdColumn ( )
id : ObjectID ;
@ Column ( )
content : string ;
@ Column ( 'uuid' )
recipient_id : string ;
@ Column ( { default : false } )
read : boolean ;
@ CreateDateColumn ( )
created_at : Date ;
@ UpdateDateColumn ( )
updated_at : Date ;
}
The column read value is defaulted, as we don’t have migrations to do this when using MongoDB.
export default interface ICreateNotificationDTO {
content : string;
recipient_id: string ;
}
import Notification from '../infra/typeorm/schemas/Notification' ;
import ICreateNotificationDTO from '../dtos/ICreateNotificationDTO' ;
export default interface INotificationsRepository {
create ( data : ICreateNotificationDTO ) : Promise < Notification > ;
}
import { MongoRepository , getMongoRepository } from 'typeorm' ;
import INotificationsRepository from '@ modules / notifications / repositories / INotificationsRepository' ;
import ICreateNotificationDTO from '@ modules / notifications / dtos / ICreateNotificationDTO' ;
import Notification from '@ modules / notifications / infra / typeorm / schemas / Notification' ;
class NotificationsRepository implements INotificationsRepository {
private ormRepository : MongoRepository < Notification >
constructor ( ) {
this . ormRepository = getMongoRepository ( Notification , 'mongo' ) ;
}
public async create ( {
content ,
recipient_id ,
} : ICreateNotificationDTO ) : Promise < Notification > {
const notification = await this . ormRepository . create ( {
content ,
recipient_id ,
} ) ;
await this . ormRepository . save ( notification ) ;
return notification ;
}
}
export default NotificationsRepository ;
The MongoDB (non-relational) repository has its own methods, so importing MongoRepository instead of Repository
Instead of using getRepository as it is done in a relational database, I must use getMongoRepository () which receives the repository as the first parameter and according to what the connection name is. Where postgres is default and MongoDB is mongo
container . registerSingleTon < INotificationsRepository > (
'NotificationsRepository' ,
NotificationsRepository ,
) ;
import { startOfHour , format } from 'date-fns' ;
@ injectable ( )
class CreateAppointmentService {
constructor (
@ inject ( 'AppointmentsRepository' )
private appointmentsRepository : IAppointmentsRepository ,
@ inject ( 'NotificationsRepository' )
private notificationsRepository : INotificationsRepository ,
)
public async execute ( {
provider_id ,
date ,
user_id ,
} : IRequest ) : Promise < Appointment > {
const appointmentDate = startOfHour ( date ) ;
...
const dateFormated = format ( appointmentDate , "dd / MM / yyyy 'at' HH: mm" ) ;
await this . notificationsRepository . run ( {
recipient_id : provider_id ,
content : 'New Schedule for the day $ { dateFormated } h` ,
} ) ;
return appointment ;
}
}
import { ObjectID } from 'mongodb' ;
class FakeNotificationsRepository implements INotificationsRepository {
private notifications : Notification [ ] = [ ] ;
public async create ( {
content ,
recipient_id ,
} : ICreateNotificationDTO ) : Promise < Notification > {
const notification = new Notification ( ) ;
Object . assign ( notification , { id : new ObjectID ( ) , content , recipient_id } ) ;
this . notifications . push ( notification ) ;
return notification ;
}
}
export default FakeNotificationsRepository ;
Use lib celebrate which is a validation middleware for express using lib Joi.
$ yarn add celebrate
import { celebrate , Joi , Segments } from 'celebrate'
appointmentsRouter . post (
'/' ,
celebrate ( {
[ Segments . BODY ] : {
provider_id : Joi . string ( ) . uuid ( ) . required ( ) ,
date : Joi . date ( ) ,
} ,
} )
, appointmentsController . create ,
) ;
I can pass ‘body’ or [Segments.BODY], because when the key of an object is a variable, I must use scale around.
providersRouter . get (
'/: provider_id / day-availability' ,
celebrate ( {
[ Segments . PARAMS ] : {
provider_id : Joi . string ( ) . uuid ( ) . required ( ) ,
}
} ) ,
providersDayAvailabilityController . index ,
) ;
providersRouter . get (
'/: provider_id / month-availability' ,
celebrate ( {
[ Segments . PARAMS ] : {
provider_id : Joi . string ( ) . uuid ( ) . required ( ) ,
}
} ) ,
providersMonthAvailabilityController . index ,
) ;
passwordRouter . post (
'/ forgot' ,
celebrate ( {
[ Segments . BODY ] : {
email : Joi . string ( ) . email ( ) . required ( ) ,
} ,
} ) ,
forgotPasswordControler . create ,
) ;
passwordRouter . post (
'/ reset' ,
celebrate ( {
[ Segments . BODY ] : {
token : Joi . string ( ) . uuid ( ) . required ( ) ,
password : Joi . string ( ) . required ( ) ,
password_confirmation : Joi . string ( ) .required ( ) . valid ( Joi . ref ( 'password' ) ) ,
}
} ) ,
resetPasswordController . create ,
) ;
Do this for the other routes! Thus, it will not be possible to call the controllers without all the fields being in agreement
Information that contains different values according to the environment (dev, production and the like) that our application is running.
Database access is another when I’m in production
The way to send email is another when I’m in production
$ yarn add dotenv
import 'dotenv / config'
Go to .tsconfig.json and add the attribute “allowJS”: true so that you can import js files.
APP_SECRET=insert_your_secret_here
APP_WEB_URL=http://localhost:3000
.env
ormconfig.json
$ git rm --cached ormconfig.json
Go to config / auth and replace the secret value for process.env.APP_SECRET
Go to SendForgotPasswordEmailService and insert the process.env.APP_WEB_URL
variables : {
name : user . name ,
link : ` $ { process . env . APP_WEB_URL } / reset_password? Token = $ { token } ` ,
} ,
Sometimes, we do not want to show any information when going to the frontend (such as deleting the user’s password when requested, for example), so transform the data / class before the data goes outside the API
Put the full avatar URL before you even go to the frontend
$ yarn add class-transformer
import { Exclude , Expose } from 'class-transformer' ;
@ Entity ( 'users' )
class User {
@ PrimaryGeneratedColumn ( 'uuid' )
id : string ;
@ Column ( )
name : string ;
@ Column ( )
email : string ;
@ Column ( )
@ Exclude ( )
password : string ;
@ Column ( )
avatar : string ;
@ CreateDateColumn ( )
created_at : Date ;
@ UpdateDateColumn ( )
updated_at : Date ;
@ Expose ( { name : 'avatar_url' } )
getAvatarUrl ( ) : string | null {
return this . avatar
? ` $ { process . env . APP_API_URL } / files / $ { this . avatar } `
: null ;
}
}
export default User ;
APP_API_URL = http : // localhost: 3333
// ProfileController.ts
import { classToClass } from 'class-transformer' ;
public async show ( request : Request , response : Response ) : Promise < Response > {
const user_id = request . user . id ;
const showProfile = container . resolve ( ShowProfileService ) ;
const user = await showProfile . execute ( { user_id } ) ;
return response . json ( classToClass ( user ) ) ;
}
Remover o delete user.password e envolver o retorno do usuário no método classToClass do class-transformer Dessa forma, irá modificar de acordo com os decorators que passamos na entidade/schema User
Author: danilobandeira29
Source Code: https://github.com/danilobandeira29/arquitetura-e-testes-nodejs
#nodejs #node #javascript