├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── preview-swagger.png ├── src ├── app.module.ts ├── app │ └── todo │ │ ├── dto │ │ ├── create-todo.dto.ts │ │ └── update-todo.dto.ts │ │ ├── entity │ │ └── todo.entity.ts │ │ ├── swagger │ │ ├── create-todo.swagger.ts │ │ ├── index-todo.swagger.ts │ │ ├── show-todo.swagger.ts │ │ └── update-todo.swagger.ts │ │ ├── todo.controller.spec.ts │ │ ├── todo.controller.ts │ │ ├── todo.module.ts │ │ ├── todo.service.spec.ts │ │ └── todo.service.ts ├── helpers │ └── swagger │ │ ├── bad-request.swagger.ts │ │ └── not-found.swagger.ts └── main.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | .env 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TODO App - Backend 2 | 3 | ## Description 4 | 5 | This is the API for the TODOAPP. The frontend interface was created using React, you can [take a look here](https://github.com/leobritob/youtube-todoapp-frontend). 6 | 7 | ## Preview 8 | ![TODOAPP SWAGGER API](./preview-swagger.png) 9 | 10 | ## Before Installation 11 | 12 | Create a .env file and fill with your database credentials. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ npm install 18 | ``` 19 | 20 | ## Running the app 21 | 22 | ```bash 23 | # development 24 | $ npm run start 25 | 26 | # watch mode 27 | $ npm run start:dev 28 | 29 | # production mode 30 | $ npm run start:prod 31 | ``` 32 | 33 | ## Test 34 | 35 | ```bash 36 | # unit tests 37 | $ npm run test 38 | 39 | # e2e tests 40 | $ npm run test:e2e 41 | 42 | # test coverage 43 | $ npm run test:cov 44 | ``` 45 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app-backend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "node dist/main", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^7.6.15", 24 | "@nestjs/config": "^0.6.3", 25 | "@nestjs/core": "^7.6.15", 26 | "@nestjs/platform-express": "^7.6.15", 27 | "@nestjs/swagger": "^4.8.0", 28 | "@nestjs/typeorm": "^7.1.5", 29 | "class-transformer": "^0.4.0", 30 | "class-validator": "^0.13.1", 31 | "mariadb": "^2.5.3", 32 | "mysql2": "^2.2.5", 33 | "pg": "^8.7.1", 34 | "reflect-metadata": "^0.1.13", 35 | "rimraf": "^3.0.2", 36 | "rxjs": "^6.6.6", 37 | "swagger-ui-express": "^4.1.6", 38 | "typeorm": "^0.2.34" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^7.6.0", 42 | "@nestjs/schematics": "^7.3.0", 43 | "@nestjs/testing": "^7.6.15", 44 | "@types/express": "^4.17.11", 45 | "@types/jest": "^26.0.22", 46 | "@types/node": "^14.14.36", 47 | "@types/supertest": "^2.0.10", 48 | "@typescript-eslint/eslint-plugin": "^4.19.0", 49 | "@typescript-eslint/parser": "^4.19.0", 50 | "eslint": "^7.22.0", 51 | "eslint-config-prettier": "^8.1.0", 52 | "eslint-plugin-prettier": "^3.3.1", 53 | "jest": "^26.6.3", 54 | "prettier": "^2.2.1", 55 | "supertest": "^6.1.3", 56 | "ts-jest": "^26.5.4", 57 | "ts-loader": "^8.0.18", 58 | "ts-node": "^9.1.1", 59 | "tsconfig-paths": "^3.9.0", 60 | "typescript": "^4.2.3" 61 | }, 62 | "jest": { 63 | "moduleFileExtensions": [ 64 | "js", 65 | "json", 66 | "ts" 67 | ], 68 | "rootDir": "src", 69 | "testRegex": ".*\\.spec\\.ts$", 70 | "transform": { 71 | "^.+\\.(t|j)s$": "ts-jest" 72 | }, 73 | "collectCoverageFrom": [ 74 | "**/*.(t|j)s" 75 | ], 76 | "coverageDirectory": "../coverage", 77 | "testEnvironment": "node" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /preview-swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leobritob/youtube-todoapp-backend/0e891cd4f0164599d5d5cb0af8c7b756cce89ce1/preview-swagger.png -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { TodoModule } from './app/todo/todo.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | ConfigModule.forRoot(), 9 | TypeOrmModule.forRootAsync({ 10 | imports: [ConfigModule], 11 | inject: [ConfigService], 12 | useFactory: (configService: ConfigService) => ({ 13 | type: 'mysql', 14 | host: configService.get('DB_HOST', 'localhost'), 15 | port: Number(configService.get('DB_PORT', 3306)), 16 | username: configService.get('DB_USERNAME', 'root'), 17 | password: configService.get('DB_PASSWORD', '123'), 18 | database: configService.get('DB_DATABASE', 'todo'), 19 | entities: [__dirname + '/**/*.entity{.js,.ts}'], 20 | synchronize: true, 21 | }), 22 | }), 23 | TodoModule, 24 | ], 25 | controllers: [], 26 | providers: [], 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /src/app/todo/dto/create-todo.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { IsIn, IsNotEmpty } from 'class-validator'; 3 | 4 | export class CreateTodoDto { 5 | @IsNotEmpty() 6 | @ApiProperty() 7 | task: string; 8 | 9 | @IsNotEmpty() 10 | @IsIn([0, 1]) 11 | @ApiPropertyOptional() 12 | isDone: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/todo/dto/update-todo.dto.ts: -------------------------------------------------------------------------------- 1 | import { CreateTodoDto } from './create-todo.dto'; 2 | 3 | export class UpdateTodoDto extends CreateTodoDto {} 4 | -------------------------------------------------------------------------------- /src/app/todo/entity/todo.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | @Entity({ name: 'todos' }) 12 | export class TodoEntity { 13 | @PrimaryGeneratedColumn('uuid') 14 | @ApiProperty() 15 | id: string; 16 | 17 | @Column() 18 | @ApiProperty() 19 | task: string; 20 | 21 | @Column({ name: 'is_done', type: 'int', width: 1 }) 22 | @ApiProperty() 23 | isDone: number; 24 | 25 | @CreateDateColumn({ name: 'created_at' }) 26 | @ApiProperty() 27 | createdAt: string; 28 | 29 | @UpdateDateColumn({ name: 'updated_at' }) 30 | @ApiProperty() 31 | updatedAt: string; 32 | 33 | @DeleteDateColumn({ name: 'deleted_at' }) 34 | @ApiProperty() 35 | deletedAt: string; 36 | 37 | constructor(todo?: Partial) { 38 | this.id = todo?.id; 39 | this.task = todo?.task; 40 | this.isDone = todo?.isDone; 41 | this.createdAt = todo?.createdAt; 42 | this.updatedAt = todo?.updatedAt; 43 | this.deletedAt = todo?.deletedAt; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/todo/swagger/create-todo.swagger.ts: -------------------------------------------------------------------------------- 1 | import { TodoEntity } from '../entity/todo.entity'; 2 | 3 | export class CreateTodoSwagger extends TodoEntity {} 4 | -------------------------------------------------------------------------------- /src/app/todo/swagger/index-todo.swagger.ts: -------------------------------------------------------------------------------- 1 | import { TodoEntity } from '../entity/todo.entity'; 2 | 3 | export class IndexTodoSwagger extends TodoEntity {} 4 | -------------------------------------------------------------------------------- /src/app/todo/swagger/show-todo.swagger.ts: -------------------------------------------------------------------------------- 1 | import { TodoEntity } from '../entity/todo.entity'; 2 | 3 | export class ShowTodoSwagger extends TodoEntity {} 4 | -------------------------------------------------------------------------------- /src/app/todo/swagger/update-todo.swagger.ts: -------------------------------------------------------------------------------- 1 | import { TodoEntity } from '../entity/todo.entity'; 2 | 3 | export class UpdateTodoSwagger extends TodoEntity {} 4 | -------------------------------------------------------------------------------- /src/app/todo/todo.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CreateTodoDto } from './dto/create-todo.dto'; 3 | import { UpdateTodoDto } from './dto/update-todo.dto'; 4 | import { TodoEntity } from './entity/todo.entity'; 5 | import { TodoController } from './todo.controller'; 6 | import { TodoService } from './todo.service'; 7 | 8 | const todoEntityList: TodoEntity[] = [ 9 | new TodoEntity({ id: '1', task: 'task-1', isDone: 0 }), 10 | new TodoEntity({ id: '2', task: 'task-2', isDone: 0 }), 11 | new TodoEntity({ id: '3', task: 'task-3', isDone: 0 }), 12 | ]; 13 | 14 | const newTodoEntity = new TodoEntity({ task: 'new-task', isDone: 0 }); 15 | 16 | const updatedTodoEntity = new TodoEntity({ task: 'task-1', isDone: 1 }); 17 | 18 | describe('TodoController', () => { 19 | let todoController: TodoController; 20 | let todoService: TodoService; 21 | 22 | beforeEach(async () => { 23 | const module: TestingModule = await Test.createTestingModule({ 24 | controllers: [TodoController], 25 | providers: [ 26 | { 27 | provide: TodoService, 28 | useValue: { 29 | findAll: jest.fn().mockResolvedValue(todoEntityList), 30 | create: jest.fn().mockResolvedValue(newTodoEntity), 31 | findOneOrFail: jest.fn().mockResolvedValue(todoEntityList[0]), 32 | update: jest.fn().mockResolvedValue(updatedTodoEntity), 33 | deleteById: jest.fn().mockResolvedValue(undefined), 34 | }, 35 | }, 36 | ], 37 | }).compile(); 38 | 39 | todoController = module.get(TodoController); 40 | todoService = module.get(TodoService); 41 | }); 42 | 43 | it('should be defined', () => { 44 | expect(todoController).toBeDefined(); 45 | expect(todoService).toBeDefined(); 46 | }); 47 | 48 | describe('index', () => { 49 | it('should return a todo list entity successfully', async () => { 50 | // Act 51 | const result = await todoController.index(); 52 | 53 | // Assert 54 | expect(result).toEqual(todoEntityList); 55 | expect(typeof result).toEqual('object'); 56 | expect(todoService.findAll).toHaveBeenCalledTimes(1); 57 | }); 58 | 59 | it('should throw an exception', () => { 60 | // Arrange 61 | jest.spyOn(todoService, 'findAll').mockRejectedValueOnce(new Error()); 62 | 63 | // Assert 64 | expect(todoController.index()).rejects.toThrowError(); 65 | }); 66 | }); 67 | 68 | describe('create', () => { 69 | it('should create a new todo item successfully', async () => { 70 | // Arrange 71 | const body: CreateTodoDto = { 72 | task: 'new-task', 73 | isDone: 0, 74 | }; 75 | 76 | // Act 77 | const result = await todoController.create(body); 78 | 79 | // Assert 80 | expect(result).toEqual(newTodoEntity); 81 | expect(todoService.create).toHaveBeenCalledTimes(1); 82 | expect(todoService.create).toHaveBeenCalledWith(body); 83 | }); 84 | 85 | it('should throw an exception', () => { 86 | // Arrange 87 | const body: CreateTodoDto = { 88 | task: 'new-task', 89 | isDone: 0, 90 | }; 91 | 92 | jest.spyOn(todoService, 'create').mockRejectedValueOnce(new Error()); 93 | 94 | // Assert 95 | expect(todoController.create(body)).rejects.toThrowError(); 96 | }); 97 | }); 98 | 99 | describe('show', () => { 100 | it('should get a todo item successfully', async () => { 101 | // Act 102 | const result = await todoController.show('1'); 103 | 104 | // Assert 105 | expect(result).toEqual(todoEntityList[0]); 106 | expect(todoService.findOneOrFail).toHaveBeenCalledTimes(1); 107 | expect(todoService.findOneOrFail).toHaveBeenCalledWith('1'); 108 | }); 109 | 110 | it('should throw an exception', () => { 111 | // Arrange 112 | jest 113 | .spyOn(todoService, 'findOneOrFail') 114 | .mockRejectedValueOnce(new Error()); 115 | 116 | // Assert 117 | expect(todoController.show('1')).rejects.toThrowError(); 118 | }); 119 | }); 120 | 121 | describe('update', () => { 122 | it('should update a todo item successfully', async () => { 123 | // Arrange 124 | const body: UpdateTodoDto = { 125 | task: 'task-1', 126 | isDone: 1, 127 | }; 128 | 129 | // Act 130 | const result = await todoController.update('1', body); 131 | 132 | // Assert 133 | expect(result).toEqual(updatedTodoEntity); 134 | expect(todoService.update).toHaveBeenCalledTimes(1); 135 | expect(todoService.update).toHaveBeenCalledWith('1', body); 136 | }); 137 | 138 | it('should throw an exception', () => { 139 | // Arrange 140 | const body: UpdateTodoDto = { 141 | task: 'task-1', 142 | isDone: 1, 143 | }; 144 | 145 | jest.spyOn(todoService, 'update').mockRejectedValueOnce(new Error()); 146 | 147 | // Assert 148 | expect(todoController.update('1', body)).rejects.toThrowError(); 149 | }); 150 | }); 151 | 152 | describe('destroy', () => { 153 | it('should remove a todo item successfully', async () => { 154 | // Act 155 | const result = await todoController.destroy('1'); 156 | 157 | // Assert 158 | expect(result).toBeUndefined(); 159 | }); 160 | 161 | it('should throw an exception', () => { 162 | // Arrange 163 | jest.spyOn(todoService, 'deleteById').mockRejectedValueOnce(new Error()); 164 | 165 | // Assert 166 | expect(todoController.destroy('1')).rejects.toThrowError(); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /src/app/todo/todo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | HttpCode, 7 | HttpStatus, 8 | Param, 9 | ParseUUIDPipe, 10 | Post, 11 | Put, 12 | } from '@nestjs/common'; 13 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 14 | import { BadRequestSwagger } from '../../helpers/swagger/bad-request.swagger'; 15 | import { NotFoundSwagger } from '../../helpers/swagger/not-found.swagger'; 16 | import { CreateTodoDto } from './dto/create-todo.dto'; 17 | import { UpdateTodoDto } from './dto/update-todo.dto'; 18 | import { CreateTodoSwagger } from './swagger/create-todo.swagger'; 19 | import { IndexTodoSwagger } from './swagger/index-todo.swagger'; 20 | import { ShowTodoSwagger } from './swagger/show-todo.swagger'; 21 | import { UpdateTodoSwagger } from './swagger/update-todo.swagger'; 22 | import { TodoService } from './todo.service'; 23 | 24 | @Controller('api/v1/todos') 25 | @ApiTags('todos') 26 | export class TodoController { 27 | constructor(private readonly todoService: TodoService) {} 28 | 29 | @Get() 30 | @ApiOperation({ summary: 'Listar todas as tarefas' }) 31 | @ApiResponse({ 32 | status: 200, 33 | description: 'Lista de tarefas retornada com sucesso', 34 | type: IndexTodoSwagger, 35 | isArray: true, 36 | }) 37 | async index() { 38 | return await this.todoService.findAll(); 39 | } 40 | 41 | @Post() 42 | @ApiOperation({ summary: 'Adicionar uma nova tarefa' }) 43 | @ApiResponse({ 44 | status: 201, 45 | description: 'Nova tarefa criada com sucesso', 46 | type: CreateTodoSwagger, 47 | }) 48 | @ApiResponse({ 49 | status: 400, 50 | description: 'Parâmetros inválidos', 51 | type: BadRequestSwagger, 52 | }) 53 | async create(@Body() body: CreateTodoDto) { 54 | return await this.todoService.create(body); 55 | } 56 | 57 | @Get(':id') 58 | @ApiOperation({ summary: 'Exibir os dados de uma tarefa' }) 59 | @ApiResponse({ 60 | status: 200, 61 | description: 'Dados de uma tarefa retornado com sucesso', 62 | type: ShowTodoSwagger, 63 | }) 64 | @ApiResponse({ 65 | status: 404, 66 | description: 'Task não foi encontrada', 67 | type: NotFoundSwagger, 68 | }) 69 | async show(@Param('id', new ParseUUIDPipe()) id: string) { 70 | return await this.todoService.findOneOrFail(id); 71 | } 72 | 73 | @Put(':id') 74 | @ApiOperation({ summary: 'Atualizar os dados de uma tarefa' }) 75 | @ApiResponse({ 76 | status: 200, 77 | description: 'Tarefa atualizada com sucesso', 78 | type: UpdateTodoSwagger, 79 | }) 80 | @ApiResponse({ 81 | status: 400, 82 | description: 'Dados inválidos', 83 | type: BadRequestSwagger, 84 | }) 85 | @ApiResponse({ 86 | status: 404, 87 | description: 'Task não foi encontrada', 88 | type: NotFoundSwagger, 89 | }) 90 | async update( 91 | @Param('id', new ParseUUIDPipe()) id: string, 92 | @Body() body: UpdateTodoDto, 93 | ) { 94 | return await this.todoService.update(id, body); 95 | } 96 | 97 | @Delete(':id') 98 | @HttpCode(HttpStatus.NO_CONTENT) 99 | @ApiOperation({ summary: 'Remover uma tarefa' }) 100 | @ApiResponse({ status: 204, description: 'Tarefa removida com sucesso' }) 101 | @ApiResponse({ 102 | status: 404, 103 | description: 'Task não foi encontrada', 104 | type: NotFoundSwagger, 105 | }) 106 | async destroy(@Param('id', new ParseUUIDPipe()) id: string) { 107 | await this.todoService.deleteById(id); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/todo/todo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { TodoEntity } from './entity/todo.entity'; 4 | import { TodoController } from './todo.controller'; 5 | import { TodoService } from './todo.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([TodoEntity])], 9 | controllers: [TodoController], 10 | providers: [TodoService], 11 | exports: [TodoService], 12 | }) 13 | export class TodoModule {} 14 | -------------------------------------------------------------------------------- /src/app/todo/todo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { CreateTodoDto } from './dto/create-todo.dto'; 6 | import { UpdateTodoDto } from './dto/update-todo.dto'; 7 | import { TodoEntity } from './entity/todo.entity'; 8 | import { TodoService } from './todo.service'; 9 | 10 | const todoEntityList: TodoEntity[] = [ 11 | new TodoEntity({ task: 'task-1', isDone: 0 }), 12 | new TodoEntity({ task: 'task-2', isDone: 0 }), 13 | new TodoEntity({ task: 'task-3', isDone: 0 }), 14 | ]; 15 | 16 | const updatedTodoEntityItem = new TodoEntity({ task: 'task-1', isDone: 1 }); 17 | 18 | describe('TodoService', () => { 19 | let todoService: TodoService; 20 | let todoRepository: Repository; 21 | 22 | beforeEach(async () => { 23 | const module: TestingModule = await Test.createTestingModule({ 24 | providers: [ 25 | TodoService, 26 | { 27 | provide: getRepositoryToken(TodoEntity), 28 | useValue: { 29 | find: jest.fn().mockResolvedValue(todoEntityList), 30 | findOneOrFail: jest.fn().mockResolvedValue(todoEntityList[0]), 31 | create: jest.fn().mockReturnValue(todoEntityList[0]), 32 | merge: jest.fn().mockReturnValue(updatedTodoEntityItem), 33 | save: jest.fn().mockResolvedValue(todoEntityList[0]), 34 | softDelete: jest.fn().mockReturnValue(undefined), 35 | }, 36 | }, 37 | ], 38 | }).compile(); 39 | 40 | todoService = module.get(TodoService); 41 | todoRepository = module.get>( 42 | getRepositoryToken(TodoEntity), 43 | ); 44 | }); 45 | 46 | it('should be defined', () => { 47 | expect(todoService).toBeDefined(); 48 | expect(todoRepository).toBeDefined(); 49 | }); 50 | 51 | describe('findAll', () => { 52 | it('should return a todo entity list successfully', async () => { 53 | // Act 54 | const result = await todoService.findAll(); 55 | 56 | // Assert 57 | expect(result).toEqual(todoEntityList); 58 | expect(todoRepository.find).toHaveBeenCalledTimes(1); 59 | }); 60 | 61 | it('should throw an exception', () => { 62 | // Arrange 63 | jest.spyOn(todoRepository, 'find').mockRejectedValueOnce(new Error()); 64 | 65 | // Assert 66 | expect(todoService.findAll()).rejects.toThrowError(); 67 | }); 68 | }); 69 | 70 | describe('findOneOrFail', () => { 71 | it('should return a todo entity item successfully', async () => { 72 | // Act 73 | const result = await todoService.findOneOrFail('1'); 74 | 75 | // Assert 76 | expect(result).toEqual(todoEntityList[0]); 77 | expect(todoRepository.findOneOrFail).toHaveBeenCalledTimes(1); 78 | }); 79 | 80 | it('should throw a not found exception', () => { 81 | // Arrange 82 | jest 83 | .spyOn(todoRepository, 'findOneOrFail') 84 | .mockRejectedValueOnce(new Error()); 85 | 86 | // Assert 87 | expect(todoService.findOneOrFail('1')).rejects.toThrowError( 88 | NotFoundException, 89 | ); 90 | }); 91 | }); 92 | 93 | describe('create', () => { 94 | it('should create a new todo entity item successfully', async () => { 95 | // Arrange 96 | const data: CreateTodoDto = { 97 | task: 'task-1', 98 | isDone: 0, 99 | }; 100 | 101 | // Act 102 | const result = await todoService.create(data); 103 | 104 | // Assert 105 | expect(result).toEqual(todoEntityList[0]); 106 | expect(todoRepository.create).toHaveBeenCalledTimes(1); 107 | expect(todoRepository.save).toHaveBeenCalledTimes(1); 108 | }); 109 | 110 | it('should throw an exception', () => { 111 | // Arrange 112 | const data: CreateTodoDto = { 113 | task: 'task-1', 114 | isDone: 0, 115 | }; 116 | 117 | jest.spyOn(todoRepository, 'save').mockRejectedValueOnce(new Error()); 118 | 119 | // Assert 120 | expect(todoService.create(data)).rejects.toThrowError(); 121 | }); 122 | }); 123 | 124 | describe('update', () => { 125 | it('should update a todo entity item successfully', async () => { 126 | // Arrange 127 | const data: UpdateTodoDto = { 128 | task: 'task-1', 129 | isDone: 1, 130 | }; 131 | 132 | jest 133 | .spyOn(todoRepository, 'save') 134 | .mockResolvedValueOnce(updatedTodoEntityItem); 135 | 136 | // Act 137 | const result = await todoService.update('1', data); 138 | 139 | // Assert 140 | expect(result).toEqual(updatedTodoEntityItem); 141 | }); 142 | 143 | it('should throw a not found exception', () => { 144 | // Arrange 145 | jest 146 | .spyOn(todoRepository, 'findOneOrFail') 147 | .mockRejectedValueOnce(new Error()); 148 | 149 | const data: UpdateTodoDto = { 150 | task: 'task-1', 151 | isDone: 1, 152 | }; 153 | 154 | // Assert 155 | expect(todoService.update('1', data)).rejects.toThrowError( 156 | NotFoundException, 157 | ); 158 | }); 159 | 160 | it('should throw an exception', () => { 161 | // Arrange 162 | jest.spyOn(todoRepository, 'save').mockRejectedValueOnce(new Error()); 163 | 164 | const data: UpdateTodoDto = { 165 | task: 'task-1', 166 | isDone: 1, 167 | }; 168 | 169 | // Assert 170 | expect(todoService.update('1', data)).rejects.toThrowError(); 171 | }); 172 | }); 173 | 174 | describe('deleteById', () => { 175 | it('should delete a todo entity item successfully', async () => { 176 | // Act 177 | const result = await todoService.deleteById('1'); 178 | 179 | // Assert 180 | expect(result).toBeUndefined(); 181 | expect(todoRepository.findOneOrFail).toHaveBeenCalledTimes(1); 182 | expect(todoRepository.softDelete).toHaveBeenCalledTimes(1); 183 | }); 184 | 185 | it('should throw a not found exception', () => { 186 | // Arrange 187 | jest 188 | .spyOn(todoRepository, 'findOneOrFail') 189 | .mockRejectedValueOnce(new Error()); 190 | 191 | // Assert 192 | expect(todoService.deleteById('1')).rejects.toThrowError( 193 | NotFoundException, 194 | ); 195 | }); 196 | 197 | it('should throw an exception', () => { 198 | // Arrange 199 | jest 200 | .spyOn(todoRepository, 'softDelete') 201 | .mockRejectedValueOnce(new Error()); 202 | 203 | // Assert 204 | expect(todoService.deleteById('1')).rejects.toThrowError(); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /src/app/todo/todo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CreateTodoDto } from './dto/create-todo.dto'; 5 | import { UpdateTodoDto } from './dto/update-todo.dto'; 6 | import { TodoEntity } from './entity/todo.entity'; 7 | 8 | @Injectable() 9 | export class TodoService { 10 | constructor( 11 | @InjectRepository(TodoEntity) 12 | private readonly todoRepository: Repository, 13 | ) {} 14 | 15 | async findAll() { 16 | return await this.todoRepository.find({ order: { createdAt: 'DESC' } }); 17 | } 18 | 19 | async findOneOrFail(id: string) { 20 | try { 21 | return await this.todoRepository.findOneOrFail(id); 22 | } catch (error) { 23 | throw new NotFoundException(error.message); 24 | } 25 | } 26 | 27 | async create(data: CreateTodoDto) { 28 | return await this.todoRepository.save(this.todoRepository.create(data)); 29 | } 30 | 31 | async update(id: string, data: UpdateTodoDto) { 32 | const todo = await this.findOneOrFail(id); 33 | 34 | this.todoRepository.merge(todo, data); 35 | return await this.todoRepository.save(todo); 36 | } 37 | 38 | async deleteById(id: string) { 39 | await this.findOneOrFail(id); 40 | await this.todoRepository.softDelete(id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/helpers/swagger/bad-request.swagger.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class BadRequestSwagger { 4 | @ApiProperty() 5 | statusCode: number; 6 | 7 | @ApiProperty() 8 | message: string[]; 9 | 10 | @ApiProperty() 11 | error: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/swagger/not-found.swagger.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class NotFoundSwagger { 4 | @ApiProperty() 5 | statusCode: number; 6 | 7 | @ApiProperty() 8 | message: string; 9 | 10 | @ApiProperty() 11 | error: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule, { cors: true }); 8 | 9 | const config = new DocumentBuilder() 10 | .setTitle('TODOApp API') 11 | .setVersion('0.0.1') 12 | .build(); 13 | 14 | const document = SwaggerModule.createDocument(app, config); 15 | 16 | SwaggerModule.setup('swagger', app, document); 17 | 18 | app.useGlobalPipes( 19 | new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }), 20 | ); 21 | await app.listen(process.env.PORT); 22 | } 23 | bootstrap(); 24 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------