├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── nodemon.json ├── package-lock.json ├── package.json ├── requirements ├── ecommerce-arquitetura.drawio ├── ecommerce-caso-uso.drawio ├── ecommerce-database.drawio └── visao-geral-dominio.md ├── src ├── index.ts ├── main │ ├── index.ts │ ├── infra │ │ └── database │ │ │ └── orm │ │ │ └── prisma │ │ │ ├── client.ts │ │ │ ├── migrations │ │ │ ├── 20230925052754_initial │ │ │ │ └── migration.sql │ │ │ ├── 20230925053534_alter_produto_categoria │ │ │ │ └── migration.sql │ │ │ ├── 20231008004737_alter_produto_soft_delete │ │ │ │ └── migration.sql │ │ │ ├── 20231009081732_alter_produto_status │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ │ └── schema.prisma │ └── presentation │ │ └── http │ │ ├── app.express.ts │ │ ├── middlewares │ │ ├── content-type.middleware.ts │ │ ├── custom-morgan.middleware.ts │ │ ├── error-logger.middleware.ts │ │ ├── error-responser.middleware.ts │ │ └── invalid-path.middleware.ts │ │ ├── rest │ │ └── api.v1.ts │ │ └── server.ts ├── modules │ └── catalogo │ │ ├── application │ │ ├── exceptions │ │ │ ├── categoria.application.exception.ts │ │ │ └── produto.application.exception.ts │ │ └── use-cases │ │ │ ├── atualizar-categoria │ │ │ └── atualizar-categoria.use-case.ts │ │ │ ├── deletar-categoria │ │ │ └── deletar-categoria.use-case.ts │ │ │ ├── index.ts │ │ │ ├── inserir-categoria │ │ │ ├── inserir-categoria.use-case.spec.ts │ │ │ └── inserir-categoria.use-case.ts │ │ │ ├── recuperar-categoria-por-id │ │ │ ├── recuperar-categoria-por-id.use-case.spec.ts │ │ │ └── recuperar-categoria-por-id.use-case.ts │ │ │ ├── recuperar-produto-por-id │ │ │ └── recuperar-produto-por-id.use-case.ts │ │ │ └── recuperar-todas-categorias │ │ │ └── recuperar-todas-categorias.use-case.ts │ │ ├── domain │ │ ├── categoria │ │ │ ├── categoria.entity.ts │ │ │ ├── categoria.exception.ts │ │ │ ├── categoria.repository.interface.ts │ │ │ ├── categoria.spec.ts │ │ │ └── categoria.types.ts │ │ └── produto │ │ │ ├── produto.entity.ts │ │ │ ├── produto.exception.ts │ │ │ ├── produto.repository.interface.ts │ │ │ ├── produto.spec.ts │ │ │ └── produto.types.ts │ │ ├── infra │ │ ├── database │ │ │ ├── categoria.prisma.repository.spec.ts │ │ │ ├── categoria.prisma.repository.ts │ │ │ ├── index.ts │ │ │ └── produto.prisma.repository.ts │ │ └── mappers │ │ │ ├── categoria.map.ts │ │ │ └── produto.map.ts │ │ ├── presentation │ │ └── http │ │ │ ├── middlewares │ │ │ ├── valida-input-atualizar-categoria.middleware.ts │ │ │ └── valida-input-inserir-categoria.middleware.ts │ │ │ └── rest │ │ │ ├── categoria.http │ │ │ ├── categoria.routes.spec.ts │ │ │ ├── categoria.routes.ts │ │ │ └── controllers │ │ │ ├── atualizar-categoria.express.controller.ts │ │ │ ├── deletar-categoria.express.controller.ts │ │ │ ├── index.ts │ │ │ ├── inserir-categoria.express.controller.spec.ts │ │ │ ├── inserir-categoria.express.controller.ts │ │ │ ├── recuperar-categoria-por-id.express.controller.spec.ts │ │ │ ├── recuperar-categoria-por-id.express.controller.ts │ │ │ └── recuperar-todas-categorias.express.controller.ts │ │ └── requirements │ │ ├── adicionar-categoria-produto-caso-uso.feature │ │ ├── criar-categoria-caso-uso.feature │ │ ├── criar-produto-caso-uso.feature │ │ ├── recuperar-categoria-caso-uso.feature │ │ └── remover-categoria-produto-caso-uso.feature └── shared │ ├── application │ ├── application.exception.ts │ └── use-case.interface.ts │ ├── domain │ ├── datas.types.ts │ ├── domain.exception.ts │ ├── entity.ts │ └── repository.interface.ts │ ├── helpers │ └── logger.winston.ts │ ├── infra │ └── database │ │ ├── prisma.repository.ts │ │ └── prisma.types.ts │ └── presentation │ └── http │ ├── express.controller.ts │ └── http.error.ts ├── tsconfig.json └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}\\src\\index.ts", 15 | "preLaunchTask": "tsc: build - tsconfig.json", 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/**/*.js" 18 | ], 19 | "runtimeArgs": [ 20 | "-r", 21 | "tsconfig-paths/register", 22 | "-r", 23 | "sucrase/register", 24 | "src" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "code-runner.clearPreviousOutput": true, 3 | "code-runner.executorMap": { 4 | "javascript": "node", 5 | "typescript": "npm run dev:console", 6 | } 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Diego Armando O. Meneses 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecommerce-node-api 2 | [Software Development] Este é um repositório de um projeto acadêmico para prática de desenvolvimento de software mais especificamente a aplicação do paradigma orientado objeto (POO) e seus conceitos, modelagem de domínios ricos, implementação de aplicações WEB e APIs, integração com banco de dados, utilização de testes automatizados, tudo em conformidade com princípios e padrões de arquitetura e projeto além de observância às boas práticas de desenvolvimento. Uso do ambiente de execução node e linguagem de programação TypeScript. 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"], 3 | "watch": ["src"], 4 | "exec": "node -r tsconfig-paths/register -r sucrase/register ./src/main/index.ts", 5 | "ext": "ts, js" 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-node-api", 3 | "version": "1.0.0", 4 | "description": "Este é um projeto acadêmico para prática de desenvolvimento de software mais especificamente a aplicação do paradigma orientado objeto (POO) e seus conceitos, modelagem de domínios ricos, implementação de aplicações WEB e APIs, integração com banco de dados, utilização de testes automatizados, tudo em conformidade com princípios e padrões de arquitetura e projeto além de observância às boas práticas de desenvolvimento. Uso do ambiente de execução node e linguagem de programação TypeScript.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev:server": "nodemon", 8 | "dev:console": "node -r tsconfig-paths/register -r sucrase/register src/index.ts", 9 | "test": "vitest", 10 | "test:run": "vitest run", 11 | "test:ui": "vitest --ui", 12 | "test:verbose": "vitest --reporter verbose" 13 | }, 14 | "prisma": { 15 | "schema": "./src/main/infra/database/orm/prisma/schema.prisma" 16 | }, 17 | "keywords": [ 18 | "processo de desenvolvimento software", 19 | "aplicações WEB", 20 | "API", 21 | "principios e padroes de arquitetura", 22 | "principios e padroes de projeto", 23 | "testes automatizados", 24 | "boas práticas", 25 | "typescript", 26 | "node", 27 | "acadêmico", 28 | "ifs", 29 | "programação II", 30 | "programação III" 31 | ], 32 | "author": "Diego Armando", 33 | "license": "ISC", 34 | "devDependencies": { 35 | "@faker-js/faker": "^8.0.2", 36 | "@types/compression": "^1.7.5", 37 | "@types/express": "^4.17.21", 38 | "@types/morgan": "^1.9.9", 39 | "@types/node": "^20.3.3", 40 | "@types/supertest": "^2.0.16", 41 | "@types/winston": "^2.4.4", 42 | "@vitest/ui": "^0.33.0", 43 | "nodemon": "^3.0.1", 44 | "prisma": "^5.3.1", 45 | "sucrase": "^3.32.0", 46 | "supertest": "^6.3.3", 47 | "tsconfig-paths": "^4.2.0", 48 | "typescript": "^5.1.6", 49 | "vitest": "^0.33.0", 50 | "vitest-mock-extended": "^1.3.1" 51 | }, 52 | "dependencies": { 53 | "@prisma/client": "^5.3.1", 54 | "compression": "^1.7.4", 55 | "dotenv": "^16.3.1", 56 | "express": "^4.18.2", 57 | "helmet": "^7.1.0", 58 | "morgan": "^1.10.0", 59 | "winston": "^3.11.0", 60 | "zod": "^3.22.4", 61 | "zod-validation-error": "^2.1.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /requirements/ecommerce-caso-uso.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /requirements/ecommerce-database.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /requirements/visao-geral-dominio.md: -------------------------------------------------------------------------------- 1 | A fábrica ABC aumentou a sua produção de produtos nas categorias de cama, mesa e banho. Com esse aumento houve a necessidade de expandir as vendas dos produtos através da internet, objetivando atender clientes de outras localidades. O comércio eletrônico desses produtos vai ser feito na modalidade B2C (Business-to-consumer) onde a compra/venda (transação) é efetuada diretamente entre a empresa produtora/vendedora (ABC) e o consumidor final/cliente, ou seja, a forma de comercialização online será o varejo. 2 | 3 | Os administradores do comércio eletrônico da fábrica ABC poderão gerenciar os produtos e categorias e acompanhar informações sobre as transações feitas no sistema. 4 | 5 | Os clientes finais poderão visualizar os produtos, gerenciar o carrinho de compra e comprar os produtos diretamente ou comprar os itens inseridos no carrinho de compra. As compras/vendas (transações) devem possuir informações sobre o status da transação, o pagamento e a entrega. 6 | 7 | Administradores e Clientes são usuários do sistema e-commerce. Os clientes vão poder gerenciar somente suas próprias informações de usuário e os administradores podem gerenciar suas próprias informações e informações de cliente, se necessário. Os clientes devem possuir status no sistema para fins de controle de acesso e relatórios. -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from '@modules/catalogo/domain/categoria/categoria.entity'; 2 | import { Produto } from '@modules/catalogo/domain/produto/produto.entity'; 3 | import { StatusProduto } from '@modules/catalogo/domain/produto/produto.types'; 4 | import { CategoriaPrismaRepository } from '@modules/catalogo/infra/database/categoria.prisma.repository'; 5 | import { ProdutoPrismaRepository } from '@modules/catalogo/infra/database/produto.prisma.repository'; 6 | import { DomainException } from '@shared/domain/domain.exception'; 7 | import { prisma } from '@main/infra/database/orm/prisma/client'; 8 | import { categoriaRepositorio as categoriaRepo } from '@modules/catalogo/infra/database'; 9 | import { produtoRepositorio as produtoRepo } from '@modules/catalogo/infra/database'; 10 | import { atualizarCategoriaUseCase, deletarCategoriaUseCase, inserirCategoriaUseCase, recuperarCategoriaPorIdUseCase, recuperarProdutoPorIdUseCase, recuperarTodasCategoriasUseCase } from '@modules/catalogo/application/use-cases'; 11 | 12 | 13 | async function main() { 14 | 15 | prisma.$connect().then( 16 | async () => { 17 | console.log('Postgres Conectado'); 18 | } 19 | ); 20 | 21 | //////////////////////////////// 22 | //Recuperar Categoria por UUID// 23 | //////////////////////////////// 24 | 25 | //console.log(await recuperarCategoriaPorIdUseCase.execute("80830927-8c3e-4db9-9ddf-30ea191f139b")); 26 | 27 | ///////////////////////////////// 28 | //Recuperar Todas as Categorias// 29 | ///////////////////////////////// 30 | 31 | //console.log(await recuperarTodasCategoriasUseCase.execute()); 32 | 33 | //////////////////////////////// 34 | //Verifica se Existe Categoria// 35 | //////////////////////////////// 36 | 37 | //const existeCategoria: boolean = await categoriaRepo.existe("7061d559-ab25-4182-98ce-170afdf2acd2"); 38 | 39 | //console.log(existeCategoria); 40 | 41 | ///////////////////// 42 | //Inserir Categoria// 43 | ///////////////////// 44 | 45 | //console.log(await inserirCategoriaUseCase.execute({nome:'Cozinha Francesa'})); 46 | 47 | /////////////////////// 48 | //Atualizar Categoria// 49 | /////////////////////// 50 | 51 | /* 52 | console.log( 53 | await atualizarCategoriaUseCase.execute({ 54 | id: "3ef71a0e-752a-4d0e-9dc5-cb40eeb55e21", 55 | nome: "Co" 56 | }) 57 | ); 58 | */ 59 | 60 | ///////////////////// 61 | //Deletar Categoria// 62 | ///////////////////// 63 | 64 | //console.log(await deletarCategoriaUseCase.execute("1f2c7f0d-d074-46f6-b835-ec1fed480363")); 65 | 66 | //////////////////////////////// 67 | //Recuperar Produto por UUID// 68 | //////////////////////////////// 69 | 70 | console.log(await recuperarProdutoPorIdUseCase.execute("738f111b-eba1-457f-9552-5b5f28511d5d")); 71 | 72 | /////////////////// 73 | //Inserir Produto// 74 | /////////////////// 75 | /* 76 | 77 | const categoria01: Categoria = Categoria.recuperar({ 78 | id: "03f890b0-684a-44ba-a887-170e26bb2cd2", 79 | nome: 'Cozinha' 80 | }); 81 | 82 | const categoria02: Categoria = Categoria.recuperar({ 83 | id: "fc762da1-8d2c-4ffa-9559-901db94cb92e", 84 | nome: 'Banho' 85 | }) 86 | 87 | const produto: Produto = Produto.criar({ 88 | nome:'Pano de Pratro', 89 | descricao:'Algodão fio 60', 90 | valor:30, 91 | categorias:[categoria01] 92 | }); 93 | 94 | const produtoInserido = await produtoRepo.inserir(produto); 95 | 96 | console.log(produtoInserido); 97 | */ 98 | 99 | 100 | 101 | ///////////////////////////////////////////////// 102 | //Recuperar Todos os Produtos e Suas Categorias// 103 | ///////////////////////////////////////////////// 104 | 105 | //const todosProdutos: Array = await produtoRepo.recuperarTodos(); 106 | 107 | //console.log(todosProdutos); 108 | 109 | /////////////////////////////////////////////// 110 | //Atualizar Produto - Sem Atulizar Categorias// 111 | /////////////////////////////////////////////// 112 | 113 | /* 114 | const produto = { 115 | id: "7d6a14d5-02f3-4b6d-8cb8-8601ff151f10", 116 | nome: "Toalha de Cozinha", 117 | descricao: "toalha de algodão", 118 | valor: 200 119 | }; 120 | 121 | const atualizouProduto: boolean = await produtoRepo.atualizar(produto.id,produto); 122 | 123 | */ 124 | /////////////////// 125 | //Deletar Produto// 126 | /////////////////// 127 | 128 | //const produtoDeletado: boolean = await produtoRepo.deletar("7d6a14d5-02f3-4b6d-8cb8-8601ff151f10"); 129 | 130 | //console.log(produtoDeletado); 131 | 132 | //////////////////////////////////////////// 133 | //Adicionar e Remover Categoria ao Produto// 134 | //////////////////////////////////////////// 135 | 136 | //const produtoRecuperado: Produto | null = await produtoRepo.recuperarPorUuid("737f111b-eba1-457f-9552-5b5f28511d5d"); 137 | 138 | //const categoriaRecuperada: Categoria | null = await categoriaRepo.recuperarPorUuid("03f890b0-684a-44ba-a887-170e26bb2cd2"); 139 | 140 | //if (produtoRecuperado && categoriaRecuperada){ 141 | 142 | //if (produtoRecuperado.adicionarCategoria(categoriaRecuperada)) { 143 | // await produtoRepo.adicionarCategoria(produtoRecuperado,categoriaRecuperada); 144 | //} 145 | 146 | //if (produtoRecuperado.removerCategoria(categoriaRecuperada)) { 147 | // await produtoRepo.removerCategoria(produtoRecuperado,categoriaRecuperada); 148 | //} 149 | 150 | //} 151 | 152 | ////////////////////////// 153 | //Alterar Status Produto// 154 | ////////////////////////// 155 | 156 | //const produtoRecuperado: Produto | null = await produtoRepo.recuperarPorUuid("ace8780f-1aac-4219-9b36-e13b60159e4b"); 157 | 158 | //if (produtoRecuperado) { 159 | // const alterouStatusProduto: boolean = await produtoRepo.alterarStatus(produtoRecuperado,StatusProduto.ATIVO) 160 | // console.log(alterouStatusProduto); 161 | //} 162 | 163 | //////////////////////////////////// 164 | //Recuperar Produtos por Categoria// 165 | //////////////////////////////////// 166 | 167 | //const todosProdutosPorCategoria: Array = await produtoRepo.recuperarPorCategoria("03f890b0-684a-44ba-a887-170e26bb2cd2"); 168 | 169 | //console.log(todosProdutosPorCategoria); 170 | 171 | 172 | } 173 | 174 | main() 175 | .then(async () => { 176 | await prisma.$disconnect() 177 | }) 178 | .catch(async (error) => { 179 | if (error instanceof DomainException) { 180 | console.log('Execeção de Dóminio'); 181 | console.log(error.message); 182 | } 183 | else { 184 | console.log('Outras Exceções'); 185 | console.log(error.message); 186 | } 187 | await prisma.$disconnect() 188 | process.exit(1) 189 | }) -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { createHTTPServer } from './presentation/http/server'; 3 | import { prisma } from '@main/infra/database/orm/prisma/client'; 4 | import { Application } from 'express'; 5 | import { createExpressApplication } from './presentation/http/app.express'; 6 | import { logger } from '@shared/helpers/logger.winston'; 7 | 8 | async function bootstrap() { 9 | 10 | logger.info(`Inicializando a API....🚀`); 11 | 12 | //Carrega variáveis de ambiente do arquivo .env 13 | dotenv.config(); 14 | const api_name = process.env.API_NAME; 15 | const host_name = process.env.HOST_NAME; 16 | const port = process.env.PORT; 17 | logger.ok(`Carregando variáveis de ambiente do arquivo .env`); 18 | 19 | const app: Application = await createExpressApplication(); 20 | logger.ok(`Aplicação Express Instanciada e Configurada`); 21 | 22 | const httpServer = await createHTTPServer(app); 23 | logger.ok('Servidor HTTP Instanciado e Configurado'); 24 | 25 | httpServer.listen({ port: port }, async () => { 26 | logger.ok(`Servidor HTTP Pronto e Ouvindo em http://${host_name}:${port}`); 27 | }); 28 | 29 | prisma.$connect().then( 30 | async () => { 31 | logger.ok(`Banco de Dados Conectado`); 32 | } 33 | ); 34 | 35 | } 36 | 37 | bootstrap() 38 | .catch((error) => { 39 | logger.error(error.message); 40 | }); -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { logger } from '@shared/helpers/logger.winston'; 3 | 4 | //Adiciona o prisma aos tipos globais do NodeJS 5 | declare global { 6 | var prisma: PrismaClient 7 | } 8 | 9 | //Evita múltiplas instâncias do cliente prisma 10 | const prisma = global.prisma || new PrismaClient({ 11 | log: [ 12 | {emit: 'event', level: 'query'}, 13 | {emit: 'event', level: 'info'}, 14 | {emit: 'event', level: 'error'}, 15 | {emit: 'event', level: 'warn'} 16 | ], 17 | errorFormat: 'minimal' 18 | }); 19 | 20 | export type QueryEvent = { 21 | timestamp: Date 22 | query: string //Consulta enviada ao banco de dados 23 | params: string //Parâmetros de consulta 24 | duration: number //Tempo decorrido (em milissegundos) entre a emissão da consulta pelo cliente e a resposta do banco de dados - não apenas o tempo necessário para executar a consulta 25 | target: string 26 | } 27 | 28 | prisma.$on('query' as never, (event:QueryEvent) => {logger.sql(event);}) 29 | 30 | prisma.$on('info' as never, (event) => {logger.info(event);}) 31 | 32 | prisma.$on('error' as never, (event) => {logger.error(event);}) 33 | 34 | prisma.$on('warn' as never, (event) => {logger.warn(event);}) 35 | 36 | //Em desenvolvimento é criado por hot-reloading (recarga automática) 37 | if (process.env.NODE_ENV === 'development') { 38 | global.prisma = prisma 39 | } 40 | 41 | export { prisma } -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/migrations/20230925052754_initial/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "categorias" ( 3 | "id" UUID NOT NULL, 4 | "nome" VARCHAR(50) NOT NULL, 5 | "data_criacao" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "data_atualizaco" TIMESTAMP(3) NOT NULL, 7 | 8 | CONSTRAINT "categorias_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "produtos" ( 13 | "id" UUID NOT NULL, 14 | "nome" VARCHAR(50) NOT NULL, 15 | "descricao" VARCHAR(200) NOT NULL, 16 | "valor" INTEGER NOT NULL, 17 | "data_criacao" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "data_atualizaco" TIMESTAMP(3) NOT NULL, 19 | 20 | CONSTRAINT "produtos_pkey" PRIMARY KEY ("id") 21 | ); 22 | 23 | -- CreateTable 24 | CREATE TABLE "_CategoriaToProduto" ( 25 | "A" UUID NOT NULL, 26 | "B" UUID NOT NULL 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "_CategoriaToProduto_AB_unique" ON "_CategoriaToProduto"("A", "B"); 31 | 32 | -- CreateIndex 33 | CREATE INDEX "_CategoriaToProduto_B_index" ON "_CategoriaToProduto"("B"); 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "_CategoriaToProduto" ADD CONSTRAINT "_CategoriaToProduto_A_fkey" FOREIGN KEY ("A") REFERENCES "categorias"("id") ON DELETE CASCADE ON UPDATE CASCADE; 37 | 38 | -- AddForeignKey 39 | ALTER TABLE "_CategoriaToProduto" ADD CONSTRAINT "_CategoriaToProduto_B_fkey" FOREIGN KEY ("B") REFERENCES "produtos"("id") ON DELETE CASCADE ON UPDATE CASCADE; 40 | -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/migrations/20230925053534_alter_produto_categoria/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `_CategoriaToProduto` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "_CategoriaToProduto" DROP CONSTRAINT "_CategoriaToProduto_A_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "_CategoriaToProduto" DROP CONSTRAINT "_CategoriaToProduto_B_fkey"; 12 | 13 | -- DropTable 14 | DROP TABLE "_CategoriaToProduto"; 15 | 16 | -- CreateTable 17 | CREATE TABLE "produtos_categorias" ( 18 | "produto_id" UUID NOT NULL, 19 | "categoria_id" UUID NOT NULL, 20 | "data_criacao" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | "data_atualizaco" TIMESTAMP(3) NOT NULL, 22 | 23 | CONSTRAINT "produtos_categorias_pkey" PRIMARY KEY ("produto_id","categoria_id") 24 | ); 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "produtos_categorias" ADD CONSTRAINT "produtos_categorias_produto_id_fkey" FOREIGN KEY ("produto_id") REFERENCES "produtos"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 28 | 29 | -- AddForeignKey 30 | ALTER TABLE "produtos_categorias" ADD CONSTRAINT "produtos_categorias_categoria_id_fkey" FOREIGN KEY ("categoria_id") REFERENCES "categorias"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 31 | -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/migrations/20231008004737_alter_produto_soft_delete/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `data_atualizaco` on the `produtos` table. All the data in the column will be lost. 5 | - Added the required column `data_atualizacao` to the `produtos` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "produtos" DROP COLUMN "data_atualizaco", 10 | ADD COLUMN "data_atualizacao" TIMESTAMP(3) NOT NULL, 11 | ADD COLUMN "data_exclusao" TIMESTAMP(3); 12 | -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/migrations/20231009081732_alter_produto_status/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "StatusProdutoPrisma" AS ENUM ('ATIVO', 'DESATIVO'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "produtos" ADD COLUMN "status_produto" "StatusProdutoPrisma" NOT NULL DEFAULT 'ATIVO'; 6 | -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /src/main/infra/database/orm/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Categoria { 14 | id String @id @db.Uuid 15 | nome String @db.VarChar(50) 16 | dataCriacao DateTime @default(now()) @map("data_criacao") 17 | dataAtualizacao DateTime @updatedAt @map("data_atualizaco") 18 | produtos ProdutosCategorias[] 19 | 20 | @@map("categorias") 21 | } 22 | 23 | model ProdutosCategorias { 24 | produto Produto @relation(fields: [produtoId], references: [id]) 25 | produtoId String @db.Uuid @map("produto_id") 26 | categoria Categoria @relation(fields: [categoriaId], references: [id]) 27 | categoriaId String @db.Uuid @map("categoria_id") 28 | dataCriacao DateTime @default(now()) @map("data_criacao") 29 | dataAtualizacao DateTime @updatedAt @map("data_atualizaco") 30 | 31 | @@id([produtoId, categoriaId]) 32 | @@map("produtos_categorias") 33 | } 34 | 35 | model Produto { 36 | id String @id @db.Uuid 37 | nome String @db.VarChar(50) 38 | descricao String @db.VarChar(200) 39 | valor Int 40 | dataCriacao DateTime @default(now()) @map("data_criacao") 41 | dataAtualizacao DateTime @updatedAt @map("data_atualizacao") 42 | dataExclusao DateTime? @map("data_exclusao") 43 | status StatusProdutoPrisma @default(ATIVO) @map("status_produto") 44 | categorias ProdutosCategorias[] 45 | 46 | @@map("produtos") 47 | } 48 | 49 | enum StatusProdutoPrisma { 50 | ATIVO 51 | DESATIVO 52 | } -------------------------------------------------------------------------------- /src/main/presentation/http/app.express.ts: -------------------------------------------------------------------------------- 1 | import express, { Application } from "express"; 2 | import { apiv1Router } from "./rest/api.v1"; 3 | import morgan from "morgan"; 4 | import helmet from "helmet"; 5 | import compression from "compression"; 6 | import { customMorgan } from "./middlewares/custom-morgan.middleware"; 7 | import { logger } from "@shared/helpers/logger.winston"; 8 | import { errorLogger } from "./middlewares/error-logger.middleware"; 9 | import { errorResponder } from "./middlewares/error-responser.middleware"; 10 | import { invalidPath } from "./middlewares/invalid-path.middleware"; 11 | 12 | const createExpressApplication = async (): Promise => { 13 | const app: Application = express(); 14 | app.disable('x-powered-by'); 15 | 16 | //Middlewares Integrados (Built-in) 17 | app.use(express.json()); 18 | app.use(express.urlencoded({ extended: true })); 19 | 20 | //Middlewares de Terceiros 21 | app.use(helmet()); 22 | app.use(compression()); 23 | 24 | //Middleware Customizados 25 | app.use(customMorgan); 26 | 27 | //Middlewares de Rotas 28 | app.use('/api/v1', apiv1Router); 29 | 30 | //Middleware de Tratamento de Erros (Error Handling) 31 | app.use(invalidPath); 32 | app.use(errorLogger); 33 | app.use(errorResponder); 34 | 35 | return app; 36 | } 37 | 38 | export { createExpressApplication } -------------------------------------------------------------------------------- /src/main/presentation/http/middlewares/content-type.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrors } from "@shared/presentation/http/http.error"; 2 | import { NextFunction, Request, Response } from "express"; 3 | 4 | const allowedContentTypes = ['application/json']; 5 | 6 | const contentTypeMiddleware = (request: Request, response: Response, next: NextFunction) => { 7 | 8 | const contentType = request.headers['content-type']; 9 | 10 | if (!contentType || !allowedContentTypes.includes(contentType)) { 11 | next(new HttpErrors.UnsupportedMediaTypeError()); 12 | } 13 | 14 | next(); 15 | } 16 | 17 | export { contentTypeMiddleware as contentType } -------------------------------------------------------------------------------- /src/main/presentation/http/middlewares/custom-morgan.middleware.ts: -------------------------------------------------------------------------------- 1 | import morgan from "morgan"; 2 | import { logger } from "@shared/helpers/logger.winston"; 3 | 4 | const stream = { 5 | write: (message:string) => logger.http(message.trim()), 6 | }; 7 | 8 | const skip = () => { 9 | const env = process.env.NODE_ENV || "development"; 10 | return env !== "development"; 11 | }; 12 | 13 | // Define a string do formato da mensagem (este é o padrão). 14 | const formatDefault = ":remote-addr :method :url :status :res[content-length] - :response-time ms"; 15 | 16 | const customMorganMiddleware = morgan( 17 | formatDefault, 18 | { stream, skip } 19 | ); 20 | 21 | 22 | export { customMorganMiddleware as customMorgan } -------------------------------------------------------------------------------- /src/main/presentation/http/middlewares/error-logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@shared/helpers/logger.winston"; 2 | import { HttpError } from "@shared/presentation/http/http.error"; 3 | import { NextFunction, Request, Response } from "express"; 4 | 5 | const errorLoggerMiddleware = (error: HttpError, request: Request, response: Response, next: NextFunction) => { 6 | let statusCode = error.statusCode || 500; 7 | 8 | const logErro = JSON.stringify({ 9 | name: error.name, 10 | statusCode: statusCode, 11 | message: error.message, 12 | stack: process.env.NODE_ENV === 'development' ? error.stack : {} 13 | }, null, 2); 14 | 15 | logger.error(logErro); 16 | 17 | next(error); 18 | } 19 | 20 | export { errorLoggerMiddleware as errorLogger } -------------------------------------------------------------------------------- /src/main/presentation/http/middlewares/error-responser.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@shared/presentation/http/http.error"; 2 | import { NextFunction, Request, Response } from "express"; 3 | 4 | const errorResponderMiddleware = (error: HttpError, request: Request, response: Response, next: NextFunction) => { 5 | let statusCode = error.statusCode || 500; 6 | response.status(statusCode).json({ 7 | name: error.name, 8 | statusCode: statusCode, 9 | message: error.message, 10 | stack: process.env.NODE_ENV === 'development' ? error.stack : {} 11 | }); 12 | } 13 | 14 | export { errorResponderMiddleware as errorResponder } -------------------------------------------------------------------------------- /src/main/presentation/http/middlewares/invalid-path.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrors } from "@shared/presentation/http/http.error"; 2 | import { NextFunction, Request, Response } from "express"; 3 | 4 | //Lança um erro 404 para caminhos indefinidos que vai ser tratado pelos middlewares de erros (log de erro e o responder de erro) 5 | const invalidPathMiddleware = (request: Request, response: Response, next: NextFunction) => { 6 | const error = new HttpErrors.NotFoundError(); 7 | next(error); 8 | } 9 | 10 | export { invalidPathMiddleware as invalidPath } -------------------------------------------------------------------------------- /src/main/presentation/http/rest/api.v1.ts: -------------------------------------------------------------------------------- 1 | import { categoriaRouter } from '@modules/catalogo/presentation/http/rest/categoria.routes'; 2 | import express, { Router } from 'express'; 3 | 4 | const apiv1Router: Router = express.Router(); 5 | 6 | apiv1Router.use( 7 | '/categorias', 8 | categoriaRouter 9 | ); 10 | 11 | apiv1Router.use( 12 | '/produtos', 13 | function (request, response, next) { 14 | response.json({"entidade":"Produtos"}); 15 | } 16 | ); 17 | 18 | apiv1Router.use( 19 | '/usuarios', 20 | function (request, response, next) { 21 | response.json({"entidade":"Usuários"}); 22 | } 23 | ); 24 | 25 | apiv1Router.use( 26 | '/pedidos', 27 | function (request, response, next) { 28 | response.json({"entidade":"Pedidos"}); 29 | } 30 | ); 31 | 32 | export { apiv1Router } -------------------------------------------------------------------------------- /src/main/presentation/http/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | 3 | const createHTTPServer = async (app: object): Promise => { 4 | const httpServer: http.Server = http.createServer(app); 5 | return httpServer; 6 | }; 7 | 8 | export { createHTTPServer } -------------------------------------------------------------------------------- /src/modules/catalogo/application/exceptions/categoria.application.exception.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationException } from "@shared/application/application.exception"; 2 | 3 | class CategoriaApplicationException extends ApplicationException { 4 | constructor(message:string = '⚠️ Exceção de Aplicação Genérica da Entidade Categoria') { 5 | super(message); 6 | this.name = 'CategoriaApplicationException' 7 | this.message = message; 8 | } 9 | } 10 | 11 | class CategoriaNaoEncontrada extends CategoriaApplicationException { 12 | public constructor(message:string = '⚠️ A categoria não foi encontrada na base de dados.') { 13 | super(message); 14 | this.name = 'CategoriaNaoEncontrada' 15 | this.message = message; 16 | } 17 | } 18 | 19 | const CategoriaApplicationExceptions = { 20 | CategoriaNaoEncontrada: CategoriaNaoEncontrada 21 | } 22 | 23 | export { CategoriaApplicationExceptions } -------------------------------------------------------------------------------- /src/modules/catalogo/application/exceptions/produto.application.exception.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationException } from "@shared/application/application.exception"; 2 | import { produtoIncludeCategoriaPrisma } from "@shared/infra/database/prisma.types"; 3 | 4 | class ProdutoApplicationException extends ApplicationException { 5 | constructor(message:string = '⚠️ Exceção de Aplicação Genérica da Entidade Produto') { 6 | super(message); 7 | this.name = 'ProdutoApplicationException' 8 | this.message = message; 9 | } 10 | } 11 | 12 | class ProdutoNaoEncontrado extends ProdutoApplicationException { 13 | public constructor(message:string = '⚠️ o produto não foi encontrado na base de dados.') { 14 | super(message); 15 | this.name = 'ProdutoNaoEncontrado' 16 | this.message = message; 17 | } 18 | } 19 | 20 | const ProdutoApplicationExceptions = { 21 | ProdutoNaoEncontrado: ProdutoNaoEncontrado 22 | } 23 | 24 | export { ProdutoApplicationExceptions } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/atualizar-categoria/atualizar-categoria.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { RecuperarCategoriaProps } from "@modules/catalogo/domain/categoria/categoria.types"; 4 | import { IUseCase } from "@shared/application/use-case.interface"; 5 | import { CategoriaApplicationExceptions } from "../../exceptions/categoria.application.exception"; 6 | 7 | class AtualizarCategoriaUseCase implements IUseCase { 8 | private _categoriaRepositorio: ICategoriaRepository; 9 | 10 | constructor(repositorio: ICategoriaRepository){ 11 | this._categoriaRepositorio = repositorio; 12 | } 13 | 14 | async execute(categoriaProps: RecuperarCategoriaProps): Promise { 15 | 16 | const existeCategoria: boolean = await this._categoriaRepositorio.existe(categoriaProps.id); 17 | 18 | if (!existeCategoria){ 19 | throw new CategoriaApplicationExceptions.CategoriaNaoEncontrada(); 20 | } 21 | 22 | const categoria: Categoria = Categoria.recuperar(categoriaProps); 23 | 24 | const atualizouCategoria: boolean = await this._categoriaRepositorio.atualizar(categoria.id, categoria); 25 | 26 | return atualizouCategoria; 27 | 28 | } 29 | 30 | } 31 | 32 | export { AtualizarCategoriaUseCase } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/deletar-categoria/deletar-categoria.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { IUseCase } from "@shared/application/use-case.interface"; 4 | import { CategoriaApplicationExceptions } from "../../exceptions/categoria.application.exception"; 5 | 6 | class DeletarCategoriaUseCase implements IUseCase { 7 | private _categoriaRepositorio: ICategoriaRepository; 8 | 9 | constructor(repositorio: ICategoriaRepository){ 10 | this._categoriaRepositorio = repositorio; 11 | } 12 | 13 | async execute(uuid: string): Promise { 14 | 15 | const existeCategoria: boolean = await this._categoriaRepositorio.existe(uuid); 16 | 17 | if (!existeCategoria){ 18 | throw new CategoriaApplicationExceptions.CategoriaNaoEncontrada(); 19 | } 20 | 21 | const deletouCategoria:boolean = await this._categoriaRepositorio.deletar(uuid); 22 | 23 | return deletouCategoria; 24 | 25 | } 26 | 27 | } 28 | 29 | export { DeletarCategoriaUseCase } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/index.ts: -------------------------------------------------------------------------------- 1 | import { categoriaRepositorio, produtoRepositorio } from "@modules/catalogo/infra/database"; 2 | import { RecuperarCategoriaPorIdUseCase } from "./recuperar-categoria-por-id/recuperar-categoria-por-id.use-case"; 3 | import { RecuperarTodasCategoriasUseCase } from "./recuperar-todas-categorias/recuperar-todas-categorias.use-case"; 4 | import { InserirCategoriaUseCase } from "./inserir-categoria/inserir-categoria.use-case"; 5 | import { AtualizarCategoriaUseCase } from "./atualizar-categoria/atualizar-categoria.use-case"; 6 | import { DeletarCategoriaUseCase } from "./deletar-categoria/deletar-categoria.use-case"; 7 | import { RecuperarProdutoPorIdUseCase } from "./recuperar-produto-por-id/recuperar-produto-por-id.use-case"; 8 | 9 | const recuperarCategoriaPorIdUseCase = new RecuperarCategoriaPorIdUseCase(categoriaRepositorio); 10 | const recuperarTodasCategoriasUseCase = new RecuperarTodasCategoriasUseCase(categoriaRepositorio); 11 | const inserirCategoriaUseCase = new InserirCategoriaUseCase(categoriaRepositorio); 12 | const atualizarCategoriaUseCase = new AtualizarCategoriaUseCase(categoriaRepositorio); 13 | const deletarCategoriaUseCase = new DeletarCategoriaUseCase(categoriaRepositorio); 14 | const recuperarProdutoPorIdUseCase = new RecuperarProdutoPorIdUseCase(produtoRepositorio); 15 | 16 | export { 17 | recuperarCategoriaPorIdUseCase, 18 | recuperarTodasCategoriasUseCase, 19 | inserirCategoriaUseCase, 20 | atualizarCategoriaUseCase, 21 | deletarCategoriaUseCase, 22 | recuperarProdutoPorIdUseCase 23 | } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/inserir-categoria/inserir-categoria.use-case.spec.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { afterEach, beforeAll, describe, expect, test, vi } from "vitest"; 4 | import { MockProxy, mock, mockReset } from "vitest-mock-extended"; 5 | import { InserirCategoriaUseCase } from "./inserir-categoria.use-case"; 6 | import { CriarCategoriaProps, ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 7 | 8 | let categoriaRepositorioMock: MockProxy>;; 9 | let inserirCategoriaUseCase: InserirCategoriaUseCase; 10 | 11 | describe('Caso de Uso: Inserir Categoria', () => { 12 | 13 | beforeAll(async () => { 14 | categoriaRepositorioMock = mock>(); 15 | inserirCategoriaUseCase = new InserirCategoriaUseCase(categoriaRepositorioMock); 16 | }); 17 | 18 | afterEach(() => { 19 | vi.restoreAllMocks(); 20 | mockReset(categoriaRepositorioMock); 21 | }); 22 | 23 | test('Deve Inserir Uma Categoria', async () => { 24 | 25 | //Dado (Given) 26 | const categoriaInputDTO: CriarCategoriaProps = { 27 | nome: "Cama" 28 | }; 29 | 30 | const categoria: Categoria = Categoria.criar(categoriaInputDTO); 31 | 32 | categoriaRepositorioMock.inserir.mockResolvedValue(categoria); 33 | 34 | //Quando (When) 35 | const categoriaOutputDTO: ICategoria = await inserirCategoriaUseCase.execute(categoria); 36 | 37 | //Então (Then) 38 | expect(categoriaOutputDTO).toBeDefined(); 39 | expect(categoriaOutputDTO).toMatchObject( 40 | expect.objectContaining({ 41 | id:expect.any(String), 42 | nome:expect.any(String) 43 | }) 44 | ); 45 | expect(categoriaRepositorioMock.inserir).toHaveBeenCalledTimes(1); 46 | 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/inserir-categoria/inserir-categoria.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { CriarCategoriaProps, ICategoria, RecuperarCategoriaProps } from "@modules/catalogo/domain/categoria/categoria.types"; 4 | import { CategoriaMap } from "@modules/catalogo/infra/mappers/categoria.map"; 5 | import { IUseCase } from "@shared/application/use-case.interface"; 6 | 7 | class InserirCategoriaUseCase implements IUseCase { 8 | private _categoriaRepositorio: ICategoriaRepository; 9 | 10 | constructor(repositorio: ICategoriaRepository){ 11 | this._categoriaRepositorio = repositorio; 12 | } 13 | 14 | async execute(categoriaProps: CriarCategoriaProps): Promise { 15 | 16 | const categoria: Categoria = Categoria.criar(categoriaProps); 17 | 18 | const categoriaInserida = await this._categoriaRepositorio.inserir(categoria); 19 | 20 | return CategoriaMap.toDTO(categoriaInserida); 21 | } 22 | 23 | } 24 | 25 | export { InserirCategoriaUseCase } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/recuperar-categoria-por-id/recuperar-categoria-por-id.use-case.spec.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { afterEach, beforeAll, describe, expect, test, vi } from "vitest"; 4 | import { MockProxy, mock, mockReset } from "vitest-mock-extended"; 5 | import { RecuperarCategoriaPorIdUseCase } from "./recuperar-categoria-por-id.use-case"; 6 | import { ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 7 | import { CategoriaApplicationExceptions } from "../../exceptions/categoria.application.exception"; 8 | 9 | let categoriaRepositorioMock: MockProxy>;; 10 | let recuperarCategoriaPorIdUseCase: RecuperarCategoriaPorIdUseCase; 11 | 12 | describe('Caso de Uso: Recuperar Categoria por ID', () => { 13 | 14 | beforeAll(async () => { 15 | categoriaRepositorioMock = mock>(); 16 | recuperarCategoriaPorIdUseCase = new RecuperarCategoriaPorIdUseCase(categoriaRepositorioMock); 17 | }); 18 | 19 | afterEach(() => { 20 | vi.restoreAllMocks(); 21 | mockReset(categoriaRepositorioMock); 22 | }); 23 | 24 | test('Deve Recuperar Uma Categoria por UUID', async () => { 25 | 26 | //Dado (Given) 27 | const categoriaInputDTO = { 28 | id: "80830927-8c3e-4db9-9ddf-30ea191f139b", 29 | nome: "Cama" 30 | }; 31 | 32 | categoriaRepositorioMock.existe.mockResolvedValue(true); 33 | 34 | categoriaRepositorioMock.recuperarPorUuid.mockResolvedValue(Categoria.recuperar(categoriaInputDTO)); 35 | 36 | //Quando (When) 37 | const categoriaOutputDTO: ICategoria = await recuperarCategoriaPorIdUseCase.execute(categoriaInputDTO.id); 38 | 39 | //Então (Then) 40 | expect(categoriaOutputDTO).toEqual(categoriaInputDTO); 41 | expect(categoriaRepositorioMock.existe).toHaveBeenCalledTimes(1); 42 | expect(categoriaRepositorioMock.recuperarPorUuid).toHaveBeenCalledTimes(1); 43 | 44 | }); 45 | 46 | test('Deve Lançar uma Exceção ao Tentar Recuperar uma Categoria que Não Existe', async () => { 47 | 48 | //Dado (Given) 49 | const categoriaInputDTO = { 50 | id: "80830927-8c3e-4db9-9ddf-30ea191f139b", 51 | nome: "Cama" 52 | }; 53 | 54 | categoriaRepositorioMock.existe.mockResolvedValue(false); 55 | 56 | //Quando (When) e Então (Then) 57 | await expect(() => recuperarCategoriaPorIdUseCase.execute(categoriaInputDTO.id)) 58 | .rejects 59 | .toThrowError(CategoriaApplicationExceptions.CategoriaNaoEncontrada); 60 | 61 | }); 62 | 63 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/recuperar-categoria-por-id/recuperar-categoria-por-id.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 4 | import { IUseCase } from "@shared/application/use-case.interface"; 5 | import { CategoriaApplicationExceptions } from "../../exceptions/categoria.application.exception"; 6 | import { CategoriaMap } from "@modules/catalogo/infra/mappers/categoria.map"; 7 | 8 | class RecuperarCategoriaPorIdUseCase implements IUseCase { 9 | 10 | private _categoriaRepositorio: ICategoriaRepository; 11 | 12 | constructor(categoriaRepositorio:ICategoriaRepository){ 13 | this._categoriaRepositorio = categoriaRepositorio; 14 | } 15 | 16 | async execute(uuid: string): Promise { 17 | 18 | const existeCategoria: boolean = await this._categoriaRepositorio.existe(uuid); 19 | 20 | if (!existeCategoria){ 21 | throw new CategoriaApplicationExceptions.CategoriaNaoEncontrada(); 22 | } 23 | 24 | const categoria = await this._categoriaRepositorio.recuperarPorUuid(uuid); 25 | 26 | return CategoriaMap.toDTO(categoria as Categoria); 27 | 28 | } 29 | 30 | } 31 | 32 | export { RecuperarCategoriaPorIdUseCase } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/recuperar-produto-por-id/recuperar-produto-por-id.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { Produto } from "@modules/catalogo/domain/produto/produto.entity"; 4 | import { IProdutoRepository } from "@modules/catalogo/domain/produto/produto.repository.interface"; 5 | import { IProduto } from "@modules/catalogo/domain/produto/produto.types"; 6 | import { IUseCase } from "@shared/application/use-case.interface"; 7 | import { ProdutoApplicationExceptions } from "../../exceptions/produto.application.exception"; 8 | import { ProdutoMap } from "@modules/catalogo/infra/mappers/produto.map"; 9 | 10 | class RecuperarProdutoPorIdUseCase implements IUseCase { 11 | private _produtoRepositorio: IProdutoRepository; 12 | 13 | constructor(repositorioProduto: IProdutoRepository){ 14 | this._produtoRepositorio = repositorioProduto; 15 | } 16 | 17 | async execute(uuid: string): Promise { 18 | 19 | const existeProduto: boolean = await this._produtoRepositorio.existe(uuid); 20 | 21 | if (!existeProduto){ 22 | throw new ProdutoApplicationExceptions.ProdutoNaoEncontrado(); 23 | } 24 | 25 | const produto = await this._produtoRepositorio.recuperarPorUuid(uuid); 26 | 27 | return ProdutoMap.toDTO(produto as Produto); 28 | 29 | } 30 | 31 | 32 | } 33 | 34 | export { RecuperarProdutoPorIdUseCase } -------------------------------------------------------------------------------- /src/modules/catalogo/application/use-cases/recuperar-todas-categorias/recuperar-todas-categorias.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 4 | import { CategoriaMap } from "@modules/catalogo/infra/mappers/categoria.map"; 5 | import { IUseCase } from "@shared/application/use-case.interface"; 6 | 7 | class RecuperarTodasCategoriasUseCase implements IUseCase> { 8 | private _categoriaRepositorio: ICategoriaRepository; 9 | 10 | constructor(repositorio: ICategoriaRepository){ 11 | this._categoriaRepositorio = repositorio; 12 | } 13 | 14 | async execute(): Promise { 15 | 16 | const todasCategorias: Array = await this._categoriaRepositorio.recuperarTodos(); 17 | 18 | const todasCategoriasDTO = todasCategorias.map( 19 | (categoria) => CategoriaMap.toDTO(categoria) 20 | ); 21 | 22 | return todasCategoriasDTO; 23 | } 24 | 25 | } 26 | 27 | export { RecuperarTodasCategoriasUseCase } -------------------------------------------------------------------------------- /src/modules/catalogo/domain/categoria/categoria.entity.ts: -------------------------------------------------------------------------------- 1 | import { CategoriaMap } from "@modules/catalogo/infra/mappers/categoria.map"; 2 | import { Entity } from "@shared/domain/entity"; 3 | import { CategoriaExceptions } from "./categoria.exception"; 4 | import { CriarCategoriaProps, ICategoria, RecuperarCategoriaProps } from "./categoria.types"; 5 | 6 | class Categoria extends Entity implements ICategoria { 7 | 8 | /////////////////////// 9 | //Atributos de Classe// 10 | /////////////////////// 11 | 12 | private _nome: string; 13 | private _dataCriacao?: Date | undefined; 14 | private _dataAtualizacao?: Date | undefined; 15 | 16 | ////////////// 17 | //Constantes// 18 | ////////////// 19 | 20 | public static readonly TAMANHO_MINIMO_NOME = 3; 21 | public static readonly TAMANHO_MAXIMO_NOME = 50; 22 | 23 | /////////////// 24 | //Gets e Sets// 25 | /////////////// 26 | 27 | public get nome(): string { 28 | return this._nome; 29 | } 30 | 31 | private set nome(nome: string) { 32 | 33 | const tamanhoNome = nome.trim().length; 34 | 35 | if (nome === null || nome === undefined) { 36 | throw new CategoriaExceptions.NomeCategoriaNuloOuIndefinido(); 37 | } 38 | 39 | if (tamanhoNome < Categoria.TAMANHO_MINIMO_NOME) { 40 | throw new CategoriaExceptions.NomeCategoriaTamanhoMinimoInvalido(); 41 | } 42 | 43 | if (tamanhoNome > Categoria.TAMANHO_MAXIMO_NOME) { 44 | throw new CategoriaExceptions.NomeCategoriaTamanhoMaximoInvalido(); 45 | } 46 | 47 | this._nome = nome; 48 | } 49 | 50 | public get dataCriacao(): Date | undefined { 51 | return this._dataCriacao; 52 | } 53 | 54 | private set dataCriacao(dataCriacao: Date | undefined) { 55 | this._dataCriacao = dataCriacao; 56 | } 57 | 58 | public get dataAtualizacao(): Date | undefined { 59 | return this._dataAtualizacao; 60 | } 61 | 62 | private set dataAtualizacao(dataAtualizacao: Date | undefined) { 63 | this._dataAtualizacao = dataAtualizacao; 64 | } 65 | 66 | ////////////// 67 | //Construtor// 68 | ////////////// 69 | 70 | private constructor(categoria:ICategoria){ 71 | super(categoria.id); 72 | this.nome = categoria.nome; 73 | this.dataCriacao = categoria.dataCriacao; 74 | this.dataAtualizacao = categoria.dataAtualizacao; 75 | } 76 | 77 | ///////////////////////// 78 | //Static Factory Method// 79 | ///////////////////////// 80 | 81 | public static criar(props: CriarCategoriaProps): Categoria { 82 | return new Categoria(props); 83 | } 84 | 85 | public static recuperar(props: RecuperarCategoriaProps): Categoria { 86 | return new Categoria(props); 87 | } 88 | 89 | /////////// 90 | //Métodos// 91 | /////////// 92 | 93 | public toDTO(): ICategoria { 94 | return CategoriaMap.toDTO(this); 95 | } 96 | 97 | } 98 | 99 | export { Categoria }; 100 | -------------------------------------------------------------------------------- /src/modules/catalogo/domain/categoria/categoria.exception.ts: -------------------------------------------------------------------------------- 1 | import { DomainException } from "@shared/domain/domain.exception"; 2 | 3 | class CategoriaException extends DomainException { 4 | constructor(message:string = '⚠️ Exceção de Domínio Genérica da Entidade Categoria') { 5 | super(message); 6 | this.name = 'CategoriaException' 7 | this.message = message; 8 | } 9 | } 10 | 11 | class NomeCategoriaNuloOuIndefinido extends CategoriaException { 12 | public constructor(message:string = '⚠️ O nome da categoria é nulo ou indefinido.') { 13 | super(message); 14 | this.name = 'NomeCategoriaNuloOuIndefinido' 15 | this.message = message; 16 | } 17 | } 18 | 19 | class NomeCategoriaTamanhoMinimoInvalido extends CategoriaException { 20 | public constructor(message:string = '⚠️ O nome da categoria não possui um tamanho mínimo válido.') { 21 | super(message); 22 | this.name = 'NomeCategoriaTamanhoMinimoInvalido' 23 | this.message = message; 24 | } 25 | } 26 | 27 | class NomeCategoriaTamanhoMaximoInvalido extends CategoriaException { 28 | public constructor(message:string = '⚠️ O nome da categoria não possui um tamanho máximo válido.') { 29 | super(message); 30 | this.name = 'NomeCategoriaTamanhoMaximoInvalido' 31 | this.message = message; 32 | } 33 | } 34 | 35 | const CategoriaExceptions = { 36 | CategoriaException: CategoriaException, 37 | NomeCategoriaNuloOuIndefinido: NomeCategoriaNuloOuIndefinido, 38 | NomeCategoriaTamanhoMinimoInvalido: NomeCategoriaTamanhoMinimoInvalido, 39 | NomeCategoriaTamanhoMaximoInvalido: NomeCategoriaTamanhoMaximoInvalido 40 | } 41 | 42 | export { 43 | CategoriaExceptions 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/catalogo/domain/categoria/categoria.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from "@shared/domain/repository.interface"; 2 | 3 | interface ICategoriaRepository extends IRepository {} 4 | 5 | export { ICategoriaRepository } -------------------------------------------------------------------------------- /src/modules/catalogo/domain/categoria/categoria.spec.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { IDEntityUUIDInvalid } from '@shared/domain/domain.exception'; 3 | import { beforeAll, describe, expect, test } from 'vitest'; 4 | import { Categoria } from './categoria.entity'; 5 | import { CategoriaExceptions } from './categoria.exception'; 6 | import { CriarCategoriaProps, RecuperarCategoriaProps } from './categoria.types'; 7 | 8 | let nomeCategoriaValido: string; 9 | let nomeCategoriaTamanhoMinInvalido: string; 10 | let nomeCategoriaTamanhoMaxInvalido: string; 11 | let UUIDValido: string; 12 | let UUIDInvalido: string; 13 | 14 | //Chamado uma vez antes de iniciar a execução de todos os testes no contexto atual. 15 | beforeAll(async () => { 16 | 17 | //Preencendo as variáveis com dados em conformidade com as restrições da regra de negócio 18 | nomeCategoriaValido = faker.string.alpha({length:{min:3,max:50}}); 19 | nomeCategoriaTamanhoMinInvalido = faker.string.alpha({length:{min:0,max:2}}); 20 | nomeCategoriaTamanhoMaxInvalido = faker.string.alpha({length:{min:51,max:51}}); 21 | UUIDValido = faker.string.uuid(); // Retorna um UUID v4 22 | UUIDInvalido = faker.string.alpha({length:{min:1,max:20}}); 23 | 24 | }); 25 | 26 | //Suite de Testes de Unidade - Entidade de Domínio 27 | //Usando a descrição, você pode definir como um conjunto de testes ou benchmarks relacionados 28 | describe('Entidade de Domínio: Categoria', () => { 29 | 30 | describe('Criar Categoria', () => { 31 | 32 | //Teste define um conjunto de expectativas relacionadas. 33 | test('Deve Criar Uma Categoria Válida', async () => { 34 | 35 | //Dado (Given) 36 | const categoriaValida: CriarCategoriaProps = { 37 | nome: nomeCategoriaValido 38 | }; 39 | 40 | //Quando (When) e Então (Then) 41 | expect(Categoria.criar(categoriaValida)) 42 | .to.be.instanceof(Categoria); 43 | 44 | }); 45 | 46 | test('Não Deve Criar Categoria Com Nome Inválido (Tamanho Mínimo)', async () => { 47 | 48 | //Dado (Given) 49 | //Nome menor que três caracteres 50 | const categoriaNomeInvalido: CriarCategoriaProps = { 51 | nome: nomeCategoriaTamanhoMinInvalido 52 | }; 53 | 54 | //Quando (When) e Então (Then) 55 | expect(() => Categoria.criar(categoriaNomeInvalido)) 56 | .toThrowError(CategoriaExceptions.NomeCategoriaTamanhoMinimoInvalido); 57 | 58 | }); 59 | 60 | test('Não Deve Criar Categoria Com Nome Inválido (Tamanho Máximo)', async () => { 61 | 62 | //Dado (Given) 63 | //Nome maior que 50 caracteres 64 | const categoriaNomeInvalido: CriarCategoriaProps = { 65 | nome: nomeCategoriaTamanhoMaxInvalido 66 | }; 67 | 68 | //Quando (When) e Então (Then) 69 | expect(() => Categoria.criar(categoriaNomeInvalido)) 70 | .toThrowError(CategoriaExceptions.NomeCategoriaTamanhoMaximoInvalido); 71 | 72 | }); 73 | 74 | }); 75 | 76 | describe('Recuperar Categoria', () => { 77 | 78 | test('Deve Recuperar Uma Categoria Válida', async () => { 79 | 80 | //Dado (Given) 81 | const categoriaValida: RecuperarCategoriaProps = { 82 | id: UUIDValido, 83 | nome: nomeCategoriaValido 84 | }; 85 | 86 | //Quando (When) e Então (Then) 87 | expect(Categoria.recuperar(categoriaValida)) 88 | .to.be.instanceof(Categoria); 89 | 90 | }); 91 | 92 | test('Não Deve Recuperar Categoria Com ID Inválido (UUID Inválido)', async () => { 93 | 94 | //Dado (Given) 95 | //Nome menor que três caracteres 96 | const categoriaIdInvalido: RecuperarCategoriaProps = { 97 | id: UUIDInvalido, 98 | nome: nomeCategoriaValido 99 | }; 100 | 101 | //Quando (When) e Então (Then) 102 | expect(() => Categoria.recuperar(categoriaIdInvalido)) 103 | .toThrowError(IDEntityUUIDInvalid); 104 | 105 | }); 106 | 107 | test('Não Deve Recuperar Categoria Com Nome Inválido (Tamanho Mínimo)', async () => { 108 | 109 | //Dado (Given) 110 | //Nome menor que três caracteres 111 | const categoriaNomeInvalido: RecuperarCategoriaProps = { 112 | id: UUIDValido, 113 | nome: nomeCategoriaTamanhoMinInvalido 114 | }; 115 | 116 | //Quando (When) e Então (Then) 117 | expect(() => Categoria.recuperar(categoriaNomeInvalido)) 118 | .toThrowError(CategoriaExceptions.NomeCategoriaTamanhoMinimoInvalido); 119 | 120 | }); 121 | 122 | test('Não Deve Recuperar Categoria Com Nome Inválido (Tamanho Máximo)', async () => { 123 | 124 | //Dado (Given) 125 | //Nome maior que 50 caracteres 126 | const categoriaNomeInvalido: RecuperarCategoriaProps = { 127 | id: UUIDValido, 128 | nome: nomeCategoriaTamanhoMaxInvalido 129 | }; 130 | 131 | //Quando (When) e Então (Then) 132 | expect(() => Categoria.recuperar(categoriaNomeInvalido)) 133 | .toThrowError(CategoriaExceptions.NomeCategoriaTamanhoMaximoInvalido); 134 | 135 | }); 136 | 137 | }); 138 | 139 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/domain/categoria/categoria.types.ts: -------------------------------------------------------------------------------- 1 | import { IDatasControle, KeysDatasControle } from "@shared/domain/datas.types"; 2 | 3 | //Todos os atributos/propriedades que uma categoria deve ter no sistema 4 | //Auxilia na criação de invariantes e modelos ricos 5 | interface ICategoria extends IDatasControle { 6 | id?: string; 7 | nome:string; 8 | } 9 | 10 | //Atributos que são necessários para criar uma categoria 11 | //Garantir a integridade dos dados de um objeto 12 | type CriarCategoriaProps = Omit; 13 | 14 | //Atributos que são necessários para recuperar uma categoria 15 | type RecuperarCategoriaProps = ICategoria & { 16 | id: NonNullable 17 | } 18 | 19 | export { 20 | ICategoria , 21 | CriarCategoriaProps, 22 | RecuperarCategoriaProps 23 | } -------------------------------------------------------------------------------- /src/modules/catalogo/domain/produto/produto.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "@shared/domain/entity"; 2 | import { ProdutoMap } from "../../infra/mappers/produto.map"; 3 | import { Categoria } from "../categoria/categoria.entity"; 4 | import { ProdutoExceptions } from "./produto.exception"; 5 | import { CriarProdutoProps, IProduto, RecuperarProdutoProps, StatusProduto } from "./produto.types"; 6 | import { RecuperarCategoriaProps } from "../categoria/categoria.types"; 7 | 8 | class Produto extends Entity implements IProduto { 9 | 10 | /////////////////////// 11 | //Atributos de Classe// 12 | /////////////////////// 13 | 14 | private _nome: string; 15 | private _descricao: string; 16 | private _valor: number; 17 | private _categorias: Array; 18 | private _dataCriacao?: Date | undefined; 19 | private _dataAtualizacao?: Date | undefined; 20 | private _dataExclusao?: Date | null | undefined; 21 | private _status?: StatusProduto | undefined; 22 | 23 | ////////////// 24 | //Constantes// 25 | ////////////// 26 | 27 | public static readonly TAMANHO_MINIMO_NOME = 5; 28 | public static readonly TAMANHO_MAXIMO_NOME = 50; 29 | public static readonly TAMANHO_MINIMO_DESCRICAO = 10; 30 | public static readonly TAMANHO_MAXIMO_DESCRICAO = 200; 31 | public static readonly VALOR_MINIMO = 0; 32 | public static readonly QTD_MINIMA_CATEGORIAS = 1; 33 | public static readonly QTD_MAXIMA_CATEGORIAS = 3; 34 | 35 | /////////////// 36 | //Gets e Sets// 37 | /////////////// 38 | 39 | public get nome(): string { 40 | return this._nome; 41 | } 42 | 43 | private set nome(nome: string) { 44 | 45 | const tamanhoNome = nome.trim().length; 46 | 47 | if (tamanhoNome < Produto.TAMANHO_MINIMO_NOME) { 48 | throw new ProdutoExceptions.NomeProdutoTamanhoMinimoInvalido(); 49 | } 50 | 51 | if (tamanhoNome > Produto.TAMANHO_MAXIMO_NOME) { 52 | throw new ProdutoExceptions.NomeProdutoTamanhoMaximoInvalido(); 53 | } 54 | 55 | this._nome = nome; 56 | } 57 | 58 | public get descricao(): string { 59 | return this._descricao; 60 | } 61 | 62 | private set descricao(descricao: string) { 63 | 64 | const tamanhoDescricao = descricao.trim().length; 65 | 66 | if (tamanhoDescricao < Produto.TAMANHO_MINIMO_DESCRICAO) { 67 | throw new ProdutoExceptions.DescricaoProdutoTamanhoMinimoInvalido(); 68 | } 69 | 70 | if (tamanhoDescricao > Produto.TAMANHO_MAXIMO_DESCRICAO) { 71 | throw new ProdutoExceptions.DescricaoProdutoTamanhoMaximoInvalido(); 72 | } 73 | 74 | this._descricao = descricao; 75 | } 76 | 77 | public get valor(): number { 78 | return this._valor; 79 | } 80 | 81 | private set valor(valor: number) { 82 | 83 | if (valor < Produto.VALOR_MINIMO) { 84 | throw new ProdutoExceptions.ValorMinimoProdutoInvalido(); 85 | } 86 | 87 | this._valor = valor; 88 | } 89 | 90 | public get categorias(): Array { 91 | return this._categorias; 92 | } 93 | 94 | private set categorias(categorias: Array) { 95 | 96 | const qtdCategorias = categorias.length; 97 | 98 | if (qtdCategorias < Produto.QTD_MINIMA_CATEGORIAS){ 99 | throw new ProdutoExceptions.QtdMinimaCategoriasProdutoInvalida(); 100 | } 101 | 102 | if (qtdCategorias > Produto.QTD_MAXIMA_CATEGORIAS){ 103 | throw new ProdutoExceptions.QtdMaximaCategoriasProdutoInvalida(); 104 | } 105 | 106 | this._categorias = categorias; 107 | } 108 | 109 | public get dataCriacao(): Date | undefined { 110 | return this._dataCriacao; 111 | } 112 | 113 | private set dataCriacao(value: Date | undefined) { 114 | this._dataCriacao = value; 115 | } 116 | 117 | public get dataAtualizacao(): Date | undefined { 118 | return this._dataAtualizacao; 119 | } 120 | 121 | private set dataAtualizacao(value: Date | undefined) { 122 | this._dataAtualizacao = value; 123 | } 124 | 125 | public get dataExclusao(): Date | null | undefined { 126 | return this._dataExclusao; 127 | } 128 | 129 | private set dataExclusao(value: Date | null | undefined) { 130 | this._dataExclusao = value; 131 | } 132 | 133 | public get status(): StatusProduto | undefined { 134 | return this._status; 135 | } 136 | 137 | private set status(value: StatusProduto | undefined) { 138 | this._status = value; 139 | } 140 | 141 | ////////////// 142 | //Construtor// 143 | ////////////// 144 | 145 | private constructor(produto:IProduto){ 146 | super(produto.id); 147 | this.nome = produto.nome; 148 | this.descricao = produto.descricao; 149 | this.valor = produto.valor; 150 | this.categorias = produto.categorias.map((categoria) => { return Categoria.recuperar(categoria as RecuperarCategoriaProps)}); 151 | this.dataCriacao = produto.dataCriacao; 152 | this.dataAtualizacao = produto.dataAtualizacao; 153 | this.dataExclusao = produto.dataExclusao; 154 | this.status = produto.status; 155 | } 156 | 157 | ///////////////////////// 158 | //Static Factory Method// 159 | ///////////////////////// 160 | 161 | public static criar(props: CriarProdutoProps): Produto { 162 | return new Produto(props); 163 | } 164 | 165 | public static recuperar(props: RecuperarProdutoProps): Produto { 166 | return new Produto(props); 167 | } 168 | 169 | /////////// 170 | //Métodos// 171 | /////////// 172 | 173 | public toDTO(): IProduto { 174 | return ProdutoMap.toDTO(this); 175 | } 176 | 177 | public estaDeletado(): boolean { 178 | return this.dataExclusao !== null ? true : false; 179 | } 180 | 181 | public quantidadeCategorias(): number { 182 | return this.categorias.length; 183 | } 184 | 185 | public possuiCategoria(categoria: Categoria): boolean { 186 | 187 | const categoriaExistente = this.categorias.find((categoriaExistente) => categoriaExistente.id === categoria.id); 188 | 189 | if (categoriaExistente) { 190 | return true; 191 | } 192 | return false; 193 | } 194 | 195 | public adicionarCategoria(categoria: Categoria): Categoria { 196 | if (this.quantidadeCategorias() >= Produto.QTD_MAXIMA_CATEGORIAS){ 197 | throw new ProdutoExceptions.ProdutoJaPossuiQtdMaximaCategorias(); 198 | } 199 | 200 | if (this.possuiCategoria(categoria)) { 201 | throw new ProdutoExceptions.ProdutoJaPossuiCategoriaInformada(); 202 | } 203 | 204 | this.categorias.push(categoria); 205 | return categoria; 206 | } 207 | 208 | public removerCategoria(categoria: Categoria): Categoria { 209 | 210 | const qtdCategoriasDoProduto: number = this.quantidadeCategorias(); 211 | 212 | if (qtdCategoriasDoProduto <= Produto.QTD_MINIMA_CATEGORIAS) { 213 | throw new ProdutoExceptions.ProdutoJaPossuiQtdMinimaCategorias(); 214 | } 215 | 216 | const produtoNaoPossuiCategoria: boolean = !this.possuiCategoria(categoria); 217 | 218 | if (produtoNaoPossuiCategoria) { 219 | throw new ProdutoExceptions.ProdutoNaoPossuiCategoriaInformada(); 220 | } 221 | 222 | //Removendo uma categoria do array 223 | this.categorias.filter((categoriaExistente, index, arrayCategorias) => { 224 | if (categoriaExistente.id === categoria.id) { 225 | arrayCategorias.splice(index, 1) 226 | } 227 | }); 228 | return categoria; 229 | } 230 | 231 | 232 | } 233 | 234 | export { Produto }; 235 | 236 | -------------------------------------------------------------------------------- /src/modules/catalogo/domain/produto/produto.exception.ts: -------------------------------------------------------------------------------- 1 | import { DomainException } from "@shared/domain/domain.exception"; 2 | 3 | class ProdutoException extends DomainException { 4 | constructor(message:string = '⚠️ Exceção de Domínio Genérica da Entidade Produto') { 5 | super(message); 6 | this.name = 'ProdutoException' 7 | this.message = message; 8 | } 9 | } 10 | 11 | class NomeProdutoTamanhoMinimoInvalido extends ProdutoException { 12 | public constructor(message:string = '⚠️ O nome do produto não possui um tamanho mínimo válido.') { 13 | super(message); 14 | this.name = 'NomeProdutoTamanhoMinimoInvalido' 15 | this.message = message; 16 | } 17 | } 18 | 19 | class NomeProdutoTamanhoMaximoInvalido extends ProdutoException { 20 | public constructor(message:string = '⚠️ O nome do produto não possui um tamanho máximo válido.') { 21 | super(message); 22 | this.name = 'NomeProdutoTamanhoMaximoInvalido' 23 | this.message = message; 24 | } 25 | } 26 | 27 | class DescricaoProdutoTamanhoMinimoInvalido extends ProdutoException { 28 | public constructor(message:string = '⚠️ A descrição do produto não possui um tamanho mínimo válido.') { 29 | super(message); 30 | this.name = 'DescricaoProdutoTamanhoMinimoInvalido' 31 | this.message = message; 32 | } 33 | } 34 | 35 | class DescricaoProdutoTamanhoMaximoInvalido extends ProdutoException { 36 | public constructor(message:string = '⚠️ A descrição do produto não possui um tamanho máximo válido.') { 37 | super(message); 38 | this.name = 'DescricaoProdutoTamanhoMaximoInvalido' 39 | this.message = message; 40 | } 41 | } 42 | 43 | class ValorMinimoProdutoInvalido extends ProdutoException { 44 | public constructor(message:string = '⚠️ O valor mínimo do produto é inválido.') { 45 | super(message); 46 | this.name = 'ValorMinimoProdutoInvalido' 47 | this.message = message; 48 | } 49 | } 50 | 51 | class QtdMinimaCategoriasProdutoInvalida extends ProdutoException { 52 | public constructor(message:string = '⚠️ A quantidade mínima de categorias produto é inválida.') { 53 | super(message); 54 | this.name = 'QtdMinimaCategoriasProdutoInvalida' 55 | this.message = message; 56 | } 57 | } 58 | 59 | class QtdMaximaCategoriasProdutoInvalida extends ProdutoException { 60 | public constructor(message:string = '⚠️ A quantidade máxima de categorias do produto é inválida.') { 61 | super(message); 62 | this.name = 'QtdMaximaCategoriasProdutoInvalida' 63 | this.message = message; 64 | } 65 | } 66 | 67 | class ProdutoJaPossuiQtdMaximaCategorias extends ProdutoException { 68 | public constructor(message:string = '⚠️ O produto já possui a quantidade máxima de categorias.') { 69 | super(message); 70 | this.name = 'ProdutoJaPossuiQtdMaximaCategorias' 71 | this.message = message; 72 | } 73 | } 74 | 75 | class ProdutoJaPossuiCategoriaInformada extends ProdutoException { 76 | public constructor(message:string = '⚠️ O produto já possui a categoria informada.') { 77 | super(message); 78 | this.name = 'ProdutoJaPossuiCategoriaInformada' 79 | this.message = message; 80 | } 81 | } 82 | 83 | class ProdutoJaPossuiQtdMinimaCategorias extends ProdutoException { 84 | public constructor(message:string = '⚠️ O produto já possui a quantidade mínima de categorias.') { 85 | super(message); 86 | this.name = 'ProdutoJaPossuiQtdMinimaCategorias' 87 | this.message = message; 88 | } 89 | } 90 | 91 | class ProdutoNaoPossuiCategoriaInformada extends ProdutoException { 92 | public constructor(message:string = '⚠️ O produto não possui a categoria informada.') { 93 | super(message); 94 | this.name = 'ProdutoNaoPossuiCategoriaInformada' 95 | this.message = message; 96 | } 97 | } 98 | 99 | const ProdutoExceptions = { 100 | ProdutoException: ProdutoException, 101 | NomeProdutoTamanhoMinimoInvalido: NomeProdutoTamanhoMinimoInvalido, 102 | NomeProdutoTamanhoMaximoInvalido: NomeProdutoTamanhoMaximoInvalido, 103 | DescricaoProdutoTamanhoMinimoInvalido: DescricaoProdutoTamanhoMinimoInvalido, 104 | DescricaoProdutoTamanhoMaximoInvalido: DescricaoProdutoTamanhoMaximoInvalido, 105 | ValorMinimoProdutoInvalido: ValorMinimoProdutoInvalido, 106 | QtdMinimaCategoriasProdutoInvalida:QtdMinimaCategoriasProdutoInvalida, 107 | QtdMaximaCategoriasProdutoInvalida: QtdMaximaCategoriasProdutoInvalida, 108 | ProdutoJaPossuiQtdMaximaCategorias: ProdutoJaPossuiQtdMaximaCategorias, 109 | ProdutoJaPossuiCategoriaInformada: ProdutoJaPossuiCategoriaInformada, 110 | ProdutoJaPossuiQtdMinimaCategorias:ProdutoJaPossuiQtdMinimaCategorias, 111 | ProdutoNaoPossuiCategoriaInformada:ProdutoNaoPossuiCategoriaInformada 112 | } 113 | 114 | 115 | export { 116 | ProdutoExceptions 117 | } 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/modules/catalogo/domain/produto/produto.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from "@shared/domain/repository.interface"; 2 | import { Produto } from "./produto.entity"; 3 | import { Categoria } from "../categoria/categoria.entity"; 4 | import { StatusProduto } from "./produto.types"; 5 | 6 | interface IProdutoRepository extends IRepository { 7 | 8 | adicionarCategoria(produto:Produto, categoria: Categoria): Promise; 9 | removerCategoria(produto:Produto, categoria: Categoria): Promise; 10 | alterarStatus(produto: Produto, status: StatusProduto): Promise; 11 | recuperarPorCategoria(idCategoria: string): Promise; 12 | 13 | } 14 | 15 | export { IProdutoRepository } -------------------------------------------------------------------------------- /src/modules/catalogo/domain/produto/produto.spec.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { beforeAll, describe, expect, test } from "vitest"; 3 | import { Categoria } from "../categoria/categoria.entity"; 4 | import { Produto } from "./produto.entity"; 5 | import { ProdutoExceptions } from "./produto.exception"; 6 | import { CriarProdutoProps } from "./produto.types"; 7 | 8 | let nomeProdutoValido: string; 9 | let nomeProdutoTamanhoMinInvalido: string; 10 | let nomeProdutoTamanhoMaxInvalido: string; 11 | let descricaoProdutoValido: string; 12 | let descricaoProdutoTamanhoMinInvalido: string; 13 | let descricaoProdutoTamanhoMaxInvalido: string; 14 | let valorProdutoValido: number; 15 | let valorMinProdutoInvalido: number; 16 | let categoriasValidas: Array; 17 | let categoriasQtdMinInvalidas: Array; 18 | let categoriasQtdMaxInvalidas: Array; 19 | let UUIDValido: string; 20 | let categoriasQtdValidaAptaAdicao: Array; 21 | let categoriasQtdMaxValidaInaptaAdicao: Array; 22 | let categoriasQtdValidaInaptaAdicaoDuplicacao: Array; 23 | let categoriasQtdValidaAptaRemocao: Array; 24 | let categoriasQtdMinValidaInaptaRemocao: Array; 25 | let categoriasQtdValidaInaptaRemocaoNaoAssociada: Array; 26 | 27 | //Chamado uma vez antes de iniciar a execução de todos os testes no contexto atual. 28 | beforeAll(async () => { 29 | 30 | //Preencendo as variáveis com dados em conformidade com as restrições da regra de negócio para o nome do produto 31 | nomeProdutoValido = faker.string.alpha({length:{min:5,max:50}}); 32 | nomeProdutoTamanhoMinInvalido = faker.string.alpha({length:{min:0,max:4}}); 33 | nomeProdutoTamanhoMaxInvalido = faker.string.alpha({length:{min:51,max:51}}); 34 | 35 | //Preencendo as variáveis com dados em conformidade com as restrições da regra de negócio para a descrição do produto 36 | descricaoProdutoValido = faker.string.alpha({length:{min:10,max:200}}); 37 | descricaoProdutoTamanhoMinInvalido = faker.string.alpha({length:{min:0,max:9}}); 38 | descricaoProdutoTamanhoMaxInvalido = faker.string.alpha({length:{min:201,max:201}}); 39 | 40 | //Preencendo as variáveis com dados em conformidade com as restrições da regra de negócio para o valor do produto 41 | valorProdutoValido = faker.number.int({min:1,max:2000 }); 42 | valorMinProdutoInvalido = faker.number.int({min:-10,max: -1}); 43 | 44 | //Preencendo um array de categorias válido com dados simulados 45 | const categoriaValida01 = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 46 | const categoriaValida02 = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 47 | const categoriaValida03 = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 48 | const categoriaValida04 = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 49 | categoriasValidas = faker.helpers.arrayElements([categoriaValida01,categoriaValida02,categoriaValida03], {min:1,max:3}); 50 | categoriasQtdMinInvalidas = []; 51 | categoriasQtdMaxInvalidas = faker.helpers.arrayElements([categoriaValida01,categoriaValida02,categoriaValida03,categoriaValida04], { min: 4, max: 4}); 52 | categoriasQtdValidaAptaAdicao = faker.helpers.arrayElements([categoriaValida01,categoriaValida02], { min: 1, max: 2}); 53 | categoriasQtdMaxValidaInaptaAdicao = faker.helpers.arrayElements([categoriaValida01,categoriaValida02,categoriaValida03], { min: 3, max: 3}); 54 | categoriasQtdValidaInaptaAdicaoDuplicacao = faker.helpers.arrayElements([categoriaValida01,categoriaValida02], { min: 1, max: 2}); 55 | categoriasQtdValidaAptaRemocao = faker.helpers.arrayElements([categoriaValida01,categoriaValida02,categoriaValida03], { min: 2, max: 3}); 56 | categoriasQtdMinValidaInaptaRemocao = faker.helpers.arrayElements([categoriaValida01], { min: 1, max: 1}); 57 | categoriasQtdValidaInaptaRemocaoNaoAssociada = faker.helpers.arrayElements([categoriaValida01,categoriaValida02,categoriaValida03], { min: 2, max: 3}); 58 | 59 | //Preenche UUID Válido para Produto 60 | UUIDValido = faker.string.uuid(); // Retorna um UUID v4 61 | 62 | }); 63 | 64 | //Suite de Testes de Unidade - Entidade de Domínio 65 | //Usando o 'describe', você pode definir como um conjunto de testes ou benchmarks relacionados 66 | describe('Entidade de Domínio: Produto', () => { 67 | 68 | describe('Criar Produto', () => { 69 | 70 | //Teste define um conjunto de expectativas relacionadas. 71 | test('Deve Criar Um Produto Válido', async () => { 72 | 73 | //Dado (Given) 74 | const produtoValido: CriarProdutoProps = { 75 | nome: nomeProdutoValido, 76 | descricao: descricaoProdutoValido, 77 | valor: valorProdutoValido, 78 | categorias: categoriasValidas 79 | }; 80 | 81 | //Quando (When) e Então (Then) 82 | expect(Produto.criar(produtoValido)) 83 | .to.be.instanceof(Produto); 84 | 85 | }); 86 | 87 | //Teste define um conjunto de expectativas relacionadas. 88 | test('Não Deve Criar Produto Com Nome Inválido (Tamanho Mínimo)', async () => { 89 | 90 | //Dado (Given) 91 | //Nome menor que cinco caracteres 92 | const produtoNomeInvalido: CriarProdutoProps = { 93 | nome: nomeProdutoTamanhoMinInvalido, 94 | descricao: descricaoProdutoValido, 95 | valor: valorProdutoValido, 96 | categorias: categoriasValidas 97 | }; 98 | 99 | //Quando (When) e Então (Then) 100 | expect(() => Produto.criar(produtoNomeInvalido)) 101 | .toThrowError(ProdutoExceptions.NomeProdutoTamanhoMinimoInvalido); 102 | 103 | }); 104 | 105 | //Teste define um conjunto de expectativas relacionadas. 106 | test('Não Deve Criar Produto Com Nome Inválido (Tamanho Máximo)', async () => { 107 | 108 | //Dado (Given) 109 | //Nome maior que cinquenta caracteres 110 | const produtoNomeInvalido: CriarProdutoProps = { 111 | nome: nomeProdutoTamanhoMaxInvalido, 112 | descricao: descricaoProdutoValido, 113 | valor: valorProdutoValido, 114 | categorias: categoriasValidas 115 | }; 116 | 117 | //Quando (When) e Então (Then) 118 | expect(() => Produto.criar(produtoNomeInvalido)) 119 | .toThrowError(ProdutoExceptions.NomeProdutoTamanhoMaximoInvalido); 120 | 121 | }); 122 | 123 | //Teste define um conjunto de expectativas relacionadas. 124 | test('Não Deve Criar Produto Com Descrição Inválida (Tamanho Mínimo)', async () => { 125 | 126 | //Dado (Given) 127 | //Descrição menor que dez caracteres 128 | const produtoNomeInvalido: CriarProdutoProps = { 129 | nome: nomeProdutoValido, 130 | descricao: descricaoProdutoTamanhoMinInvalido, 131 | valor: valorProdutoValido, 132 | categorias: categoriasValidas 133 | }; 134 | 135 | //Quando (When) e Então (Then) 136 | expect(() => Produto.criar(produtoNomeInvalido)) 137 | .toThrowError(ProdutoExceptions.DescricaoProdutoTamanhoMinimoInvalido); 138 | 139 | }); 140 | 141 | //Teste define um conjunto de expectativas relacionadas. 142 | test('Não Deve Criar Produto Com Descrição Inválida (Tamanho Máximo)', async () => { 143 | 144 | //Dado (Given) 145 | //Descrição maior que duzentos caracteres 146 | const produtoNomeInvalido: CriarProdutoProps = { 147 | nome: nomeProdutoValido, 148 | descricao: descricaoProdutoTamanhoMaxInvalido, 149 | valor: valorProdutoValido, 150 | categorias: categoriasValidas 151 | }; 152 | 153 | //Quando (When) e Então (Then) 154 | expect(() => Produto.criar(produtoNomeInvalido)) 155 | .toThrowError(ProdutoExceptions.DescricaoProdutoTamanhoMaximoInvalido); 156 | 157 | }); 158 | 159 | //Teste define um conjunto de expectativas relacionadas. 160 | test('Não Deve Criar Produto Com Valor Mínimo Inválido', async () => { 161 | 162 | //Dado (Given) 163 | //Valor mínimo menor que 0 164 | const produtoNomeInvalido: CriarProdutoProps = { 165 | nome: nomeProdutoValido, 166 | descricao: descricaoProdutoValido, 167 | valor: valorMinProdutoInvalido, 168 | categorias: categoriasValidas 169 | }; 170 | 171 | //Quando (When) e Então (Then) 172 | expect(() => Produto.criar(produtoNomeInvalido)) 173 | .toThrowError(ProdutoExceptions.ValorMinimoProdutoInvalido); 174 | 175 | }); 176 | 177 | //Teste define um conjunto de expectativas relacionadas. 178 | test('Não Deve Criar Produto Com Número Mínimo de Categorias Inválido', async () => { 179 | 180 | //Dado (Given) 181 | //Nenhuma categoria é atribuida - menor que 1 182 | const produtoNomeInvalido: CriarProdutoProps = { 183 | nome: nomeProdutoValido, 184 | descricao: descricaoProdutoValido, 185 | valor: valorProdutoValido, 186 | categorias: categoriasQtdMinInvalidas 187 | }; 188 | 189 | //Quando (When) e Então (Then) 190 | expect(() => Produto.criar(produtoNomeInvalido)) 191 | .toThrowError(ProdutoExceptions.QtdMinimaCategoriasProdutoInvalida); 192 | 193 | }); 194 | 195 | //Teste define um conjunto de expectativas relacionadas. 196 | test('Não Deve Criar Produto Com Número Máximo de Categorias Inválido', async () => { 197 | 198 | //Dado (Given) 199 | //4 categorias é atribuidas - maior que 3 200 | const produtoNomeInvalido: CriarProdutoProps = { 201 | nome: nomeProdutoValido, 202 | descricao: descricaoProdutoValido, 203 | valor: valorProdutoValido, 204 | categorias: categoriasQtdMaxInvalidas 205 | }; 206 | 207 | //Quando (When) e Então (Then) 208 | expect(() => Produto.criar(produtoNomeInvalido)) 209 | .toThrowError(ProdutoExceptions.QtdMaximaCategoriasProdutoInvalida); 210 | 211 | }); 212 | 213 | }); 214 | 215 | describe('Adicionar Categoria ao Produto', () => { 216 | 217 | test('Deve Adicionar Uma Categoria Válida a Um Produto Válido Apto a Ter Uma Nova Categoria', async () => { 218 | 219 | //Dado (Given) 220 | const produtoValidoAptoNovaCategoria: Produto = Produto.recuperar({ 221 | id: UUIDValido, 222 | nome: nomeProdutoValido, 223 | descricao: descricaoProdutoValido, 224 | valor: valorProdutoValido, 225 | categorias: categoriasQtdValidaAptaAdicao 226 | }); 227 | 228 | //Categoria válida que não seja uma das categorias já adicionadas 229 | const categoriaValida = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 230 | 231 | //Quando (When) e Então (Then) 232 | expect(produtoValidoAptoNovaCategoria.adicionarCategoria(categoriaValida)) 233 | .toBe(categoriaValida); 234 | 235 | expect(produtoValidoAptoNovaCategoria.categorias) 236 | .toContain(categoriaValida); 237 | 238 | }); 239 | 240 | test('Não Deve Adicionar Uma Categoria Válida a Um Produto Válido Inapto a Ter Uma Nova Categoria - Quantidade Máxima de Categorias', async () => { 241 | 242 | //Dado (Given) 243 | const produtoValidoInaptoNovaCategoria: Produto = Produto.recuperar({ 244 | id: UUIDValido, 245 | nome: nomeProdutoValido, 246 | descricao: descricaoProdutoValido, 247 | valor: valorProdutoValido, 248 | categorias: categoriasQtdMaxValidaInaptaAdicao 249 | }); 250 | 251 | //Categoria válida que não seja uma das categorias já adicionadas 252 | const categoriaValida = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 253 | 254 | //Quando (When) e Então (Then) 255 | expect(() => produtoValidoInaptoNovaCategoria.adicionarCategoria(categoriaValida)) 256 | .toThrowError(ProdutoExceptions.ProdutoJaPossuiQtdMaximaCategorias); 257 | 258 | }); 259 | 260 | test('Não Deve Adicionar Uma Categoria Válida a Um Produto Válido Inapto a Ter Uma Nova Categoria - Categoria Já Adicionada', async () => { 261 | 262 | //Dado (Given) 263 | const produtoValidoInaptoNovaCategoria: Produto = Produto.recuperar({ 264 | id: UUIDValido, 265 | nome: nomeProdutoValido, 266 | descricao: descricaoProdutoValido, 267 | valor: valorProdutoValido, 268 | categorias: categoriasQtdValidaInaptaAdicaoDuplicacao 269 | }); 270 | 271 | //Categoria válida já adicionada - recupera do array passado no produto anteriormete - garente que é um elemento que já existe 272 | const categoriaValida = categoriasQtdValidaInaptaAdicaoDuplicacao[0]; 273 | 274 | //Quando (When) e Então (Then) 275 | expect(() => produtoValidoInaptoNovaCategoria.adicionarCategoria(categoriaValida)) 276 | .toThrowError(ProdutoExceptions.ProdutoJaPossuiCategoriaInformada); 277 | 278 | }); 279 | 280 | }); 281 | 282 | describe('Remover Categoria do Produto', () => { 283 | 284 | test('Deve Remover Uma Categoria Válida de Um Produto Válido Apto a Ter Uma Categoria Removida', async () => { 285 | 286 | //Dado (Given) 287 | const produtoValidoAptoRemoverCategoria: Produto = Produto.recuperar({ 288 | id: UUIDValido, 289 | nome: nomeProdutoValido, 290 | descricao: descricaoProdutoValido, 291 | valor: valorProdutoValido, 292 | categorias: categoriasQtdValidaAptaRemocao 293 | }); 294 | 295 | //Categoria válida que já esteja adicionada 296 | const categoriaValida = categoriasQtdValidaAptaRemocao[0]; 297 | 298 | //Quando (When) e Então (Then) 299 | expect(produtoValidoAptoRemoverCategoria.removerCategoria(categoriaValida)) 300 | .toBe(categoriaValida); 301 | 302 | expect(produtoValidoAptoRemoverCategoria.categorias) 303 | .not.toContain(categoriaValida); 304 | 305 | }); 306 | 307 | test('Não Deve Remover Uma Categoria Válida de Um Produto Válido Inapto a Ter Uma Categoria Removida - Quantidade Mínima de Categorias', async () => { 308 | 309 | //Dado (Given) 310 | const produtoValidoInaptoRemoverCategoria: Produto = Produto.recuperar({ 311 | id: UUIDValido, 312 | nome: nomeProdutoValido, 313 | descricao: descricaoProdutoValido, 314 | valor: valorProdutoValido, 315 | categorias: categoriasQtdMinValidaInaptaRemocao 316 | }); 317 | 318 | //Categoria válida que já esteja adicionada 319 | const categoriaValida = categoriasQtdMinValidaInaptaRemocao[0]; 320 | 321 | //Quando (When) e Então (Then) 322 | expect(() => produtoValidoInaptoRemoverCategoria.removerCategoria(categoriaValida)) 323 | .toThrowError(ProdutoExceptions.ProdutoJaPossuiQtdMinimaCategorias); 324 | 325 | }); 326 | 327 | test('Não Deve Remover Uma Categoria Válida de Um Produto Válido Inapto a Ter Uma Categoria Removida - Categoria Não Associada ao Produto', async () => { 328 | 329 | //Dado (Given) 330 | const produtoValidoInaptoRemoverCategoria: Produto = Produto.recuperar({ 331 | id: UUIDValido, 332 | nome: nomeProdutoValido, 333 | descricao: descricaoProdutoValido, 334 | valor: valorProdutoValido, 335 | categorias: categoriasQtdValidaInaptaRemocaoNaoAssociada 336 | }); 337 | 338 | //Categoria válida que não seja uma das categorias já adicionadas 339 | const categoriaValida = Categoria.criar({nome:faker.string.alpha({length:{min:3,max:50}})}); 340 | 341 | //Quando (When) e Então (Then) 342 | expect(() => produtoValidoInaptoRemoverCategoria.removerCategoria(categoriaValida)) 343 | .toThrowError(ProdutoExceptions.ProdutoNaoPossuiCategoriaInformada); 344 | 345 | }); 346 | 347 | }); 348 | 349 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/domain/produto/produto.types.ts: -------------------------------------------------------------------------------- 1 | import { IDatasControle, KeysDatasControle } from "@shared/domain/datas.types"; 2 | import { ICategoria } from "../categoria/categoria.types"; 3 | 4 | enum StatusProduto { 5 | ATIVO = "ATIVO", 6 | DESATIVO = "DESATIVO" 7 | } 8 | 9 | //Todos os atributos/propriedades que um produto deve ter no sistema 10 | //Auxilia na criação de invariantes e modelos ricos 11 | interface IProduto extends IDatasControle{ 12 | id?: string; 13 | nome:string; 14 | descricao:string; 15 | valor: number; 16 | categorias: Array; 17 | status?: StatusProduto 18 | } 19 | 20 | //Atributos que são necessários para criar um produto 21 | //Tipo representa um dos estados do ciclo de vida da entidade 22 | //Garantir a integridade dos dados de um objeto 23 | type CriarProdutoProps = Omit; 24 | 25 | //Atributos que são necessários para recuperar uma categoria 26 | //Tipo representa um dos estados do ciclo de vida da entidade 27 | type RecuperarProdutoProps = IProduto & { 28 | id: NonNullable 29 | }; 30 | 31 | export { 32 | IProduto, 33 | CriarProdutoProps, 34 | RecuperarProdutoProps, 35 | StatusProduto 36 | } -------------------------------------------------------------------------------- /src/modules/catalogo/infra/database/categoria.prisma.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { afterEach, beforeAll, describe, expect, test, vi } from "vitest"; 3 | import { DeepMockProxy, mockDeep, mockReset } from 'vitest-mock-extended'; 4 | import { CategoriaPrismaRepository } from "./categoria.prisma.repository"; 5 | import { faker } from "@faker-js/faker"; 6 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 7 | import { CategoriaMap } from "../mappers/categoria.map"; 8 | 9 | const prismaMock: DeepMockProxy = mockDeep(); 10 | let categoriaRepositorio: CategoriaPrismaRepository; 11 | let UUIDValido: string; 12 | let nomeCategoriaValido: string; 13 | let dataCriacaoCategoria: Date; 14 | let dataAtualizacaoCategoria: Date; 15 | 16 | describe('Repositório Prisma: Categoria', () => { 17 | 18 | beforeAll(async () => { 19 | 20 | categoriaRepositorio = new CategoriaPrismaRepository(prismaMock); 21 | 22 | //Preencendo as variáveis com dados em conformidade com as restrições da regra de negócio 23 | UUIDValido = faker.string.uuid(); // Retorna um UUID v4 24 | nomeCategoriaValido = faker.string.alpha({length:{min:Categoria.TAMANHO_MINIMO_NOME,max:Categoria.TAMANHO_MAXIMO_NOME}}); 25 | dataCriacaoCategoria = faker.date.anytime(); 26 | dataAtualizacaoCategoria = faker.date.anytime(); 27 | }); 28 | 29 | afterEach(() => { 30 | vi.restoreAllMocks(); 31 | mockReset(prismaMock); 32 | }); 33 | 34 | describe('Recuperar Categoria por ID', () => { 35 | 36 | test('Deve Recuperar Uma Categoria por UUID', async () => { 37 | 38 | const categoriaPrisma = { 39 | id: UUIDValido, 40 | nome: nomeCategoriaValido, 41 | dataCriacao: dataCriacaoCategoria, 42 | dataAtualizacao: dataAtualizacaoCategoria 43 | }; 44 | 45 | prismaMock.categoria.findUnique.mockResolvedValue(categoriaPrisma); 46 | 47 | const categoria: Categoria = CategoriaMap.toDomain(categoriaPrisma); 48 | 49 | const categoriaRecuperada = await categoriaRepositorio.recuperarPorUuid(categoria.id); 50 | 51 | expect(categoriaRecuperada).toEqual(categoria); 52 | expect(prismaMock.categoria.findUnique).toHaveBeenCalledTimes(1); 53 | expect(prismaMock.categoria.findUnique).toBeCalledWith({ 54 | where: { 55 | id: categoria.id 56 | } 57 | }); 58 | 59 | }); 60 | 61 | }); 62 | 63 | describe('Recuperar Todas as Categorias', () => { 64 | 65 | test('Deve Recuperar Todas as Categorias Sem Execeção', async () => { 66 | 67 | const listaCategoriasPrisma = [{ 68 | id: UUIDValido, 69 | nome: nomeCategoriaValido, 70 | dataCriacao: dataCriacaoCategoria, 71 | dataAtualizacao: dataAtualizacaoCategoria 72 | },{ 73 | id: UUIDValido, 74 | nome: nomeCategoriaValido, 75 | dataCriacao: dataCriacaoCategoria, 76 | dataAtualizacao: dataAtualizacaoCategoria 77 | }]; 78 | 79 | prismaMock.categoria.findMany.mockResolvedValue(listaCategoriasPrisma); 80 | 81 | const categorias:Array = listaCategoriasPrisma.map( 82 | (categoria) => CategoriaMap.fromPrismaModelToDomain(categoria) 83 | ); 84 | 85 | const todasCategoriasRecuperadas = await categoriaRepositorio.recuperarTodos(); 86 | 87 | expect(todasCategoriasRecuperadas).toStrictEqual(categorias); 88 | expect(prismaMock.categoria.findMany).toHaveBeenCalledTimes(1); 89 | 90 | 91 | }); 92 | 93 | }); 94 | 95 | describe('Existe Categoria', () => { 96 | 97 | test('Deve Verificar se Existe Uma Determinada Categoria por UUID', async () => { 98 | 99 | const categoriaPrisma = { 100 | id: UUIDValido, 101 | nome: nomeCategoriaValido, 102 | dataCriacao: dataCriacaoCategoria, 103 | dataAtualizacao: dataAtualizacaoCategoria 104 | }; 105 | 106 | prismaMock.categoria.findUnique.mockResolvedValue(categoriaPrisma); 107 | 108 | const existeCategoria = await categoriaRepositorio.existe(categoriaPrisma.id); 109 | 110 | expect(existeCategoria).toBeTruthy(); 111 | 112 | }); 113 | 114 | }); 115 | 116 | describe('Inserir Categoria', () => { 117 | 118 | test('Deve Inserir Uma Categoria', async () => { 119 | 120 | const categoriaPrisma = { 121 | id: UUIDValido, 122 | nome: nomeCategoriaValido, 123 | dataCriacao:dataCriacaoCategoria, 124 | dataAtualizacao: dataAtualizacaoCategoria 125 | }; 126 | 127 | prismaMock.categoria.create.mockResolvedValue(categoriaPrisma); 128 | 129 | const categoria: Categoria = CategoriaMap.toDomain(categoriaPrisma); 130 | 131 | const categoriaInserida = await categoriaRepositorio.inserir(categoria); 132 | 133 | expect(categoriaInserida).toStrictEqual(categoria) 134 | expect(prismaMock.categoria.create).toHaveBeenCalledTimes(1); 135 | expect(prismaMock.categoria.create).toBeCalledWith( { 136 | data: { 137 | id: categoria.id, 138 | nome: categoria.nome 139 | } 140 | }); 141 | 142 | 143 | }); 144 | 145 | }); 146 | 147 | describe('Alterar Categoria', () => { 148 | 149 | test('Deve Atualizar Uma Categoria', async () => { 150 | 151 | const categoriaPrisma = { 152 | id: UUIDValido, 153 | nome: nomeCategoriaValido, 154 | dataCriacao:dataCriacaoCategoria, 155 | dataAtualizacao: dataAtualizacaoCategoria 156 | }; 157 | 158 | prismaMock.categoria.update.mockResolvedValue(categoriaPrisma); 159 | 160 | const categoria: Categoria = CategoriaMap.toDomain(categoriaPrisma); 161 | 162 | const categoriaAtualizada = await categoriaRepositorio.atualizar(categoria.id,categoria); 163 | 164 | expect(categoriaAtualizada).toBeTruthy() 165 | expect(prismaMock.categoria.update).toHaveBeenCalledTimes(1); 166 | expect(prismaMock.categoria.update).toBeCalledWith({ 167 | where: {id : categoria.id}, 168 | data: categoriaPrisma 169 | }); 170 | 171 | 172 | }); 173 | 174 | }); 175 | 176 | describe('Deletar Categoria', () => { 177 | 178 | test('Deve Deletar Uma Categoria por UUID', async () => { 179 | 180 | const categoriaPrisma = { 181 | id: UUIDValido, 182 | nome: nomeCategoriaValido, 183 | dataCriacao:dataCriacaoCategoria, 184 | dataAtualizacao: dataAtualizacaoCategoria 185 | }; 186 | 187 | prismaMock.categoria.delete.mockResolvedValue(categoriaPrisma); 188 | 189 | const categoria: Categoria = CategoriaMap.toDomain(categoriaPrisma); 190 | 191 | const categoriaDeletada = await categoriaRepositorio.deletar(categoria.id); 192 | 193 | expect(categoriaDeletada).toBeTruthy(); 194 | expect(prismaMock.categoria.delete).toHaveBeenCalledTimes(1); 195 | expect(prismaMock.categoria.delete).toBeCalledWith({ 196 | where: {id : categoria.id} 197 | }); 198 | 199 | 200 | }); 201 | 202 | }); 203 | 204 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/infra/database/categoria.prisma.repository.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 3 | import { CategoriaMap } from "@modules/catalogo/infra/mappers/categoria.map"; 4 | import { PrismaRepository } from "@shared/infra/database/prisma.repository"; 5 | 6 | class CategoriaPrismaRepository extends PrismaRepository implements ICategoriaRepository { 7 | 8 | async recuperarPorUuid(uuid: string): Promise { 9 | const categoriaRecuperada = await this._datasource.categoria.findUnique( 10 | { 11 | where: { 12 | id: uuid 13 | } 14 | } 15 | ) 16 | if (categoriaRecuperada) { 17 | return CategoriaMap.fromPrismaModelToDomain(categoriaRecuperada); 18 | } 19 | return null; 20 | } 21 | 22 | async recuperarTodos(): Promise> { 23 | const categoriasRecuperadas = await this._datasource.categoria.findMany(); 24 | const categorias = categoriasRecuperadas.map( 25 | (categoria) => CategoriaMap.fromPrismaModelToDomain(categoria) 26 | ); 27 | return categorias; 28 | } 29 | 30 | async existe(uuid: string): Promise { 31 | const categoriaExistente = await this.recuperarPorUuid(uuid); 32 | if (categoriaExistente) {return true;} 33 | return false; 34 | } 35 | 36 | async inserir(categoria: Categoria): Promise { 37 | const categoriaInserida = await this._datasource.categoria.create( 38 | { 39 | data: { 40 | id: categoria.id, 41 | nome: categoria.nome 42 | } 43 | } 44 | ); 45 | return CategoriaMap.fromPrismaModelToDomain(categoriaInserida);; 46 | } 47 | 48 | async atualizar(uuid: string, categoria: Categoria): Promise { 49 | const categoriaAtualizada = await this._datasource.categoria.update( 50 | { 51 | where: {id : uuid}, 52 | data: CategoriaMap.toDTO(categoria) 53 | } 54 | ); 55 | if (categoriaAtualizada) {return true}; 56 | return false; 57 | } 58 | 59 | async deletar(uuid: string): Promise { 60 | const categoriaDeletada = await this._datasource.categoria.delete( 61 | { 62 | where: { 63 | id: uuid 64 | } 65 | } 66 | ); 67 | if (categoriaDeletada.id) {return true;} 68 | return false; 69 | } 70 | } 71 | 72 | export { CategoriaPrismaRepository } -------------------------------------------------------------------------------- /src/modules/catalogo/infra/database/index.ts: -------------------------------------------------------------------------------- 1 | import { IProdutoRepository } from "@modules/catalogo/domain/produto/produto.repository.interface"; 2 | import { CategoriaPrismaRepository } from "./categoria.prisma.repository"; 3 | import { ProdutoPrismaRepository } from "./produto.prisma.repository"; 4 | import { prisma } from "@main/infra/database/orm/prisma/client"; 5 | import { Produto } from "@modules/catalogo/domain/produto/produto.entity"; 6 | import { ICategoriaRepository } from "@modules/catalogo/domain/categoria/categoria.repository.interface"; 7 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 8 | 9 | const categoriaRepositorio: ICategoriaRepository = new CategoriaPrismaRepository(prisma); 10 | const produtoRepositorio: IProdutoRepository = new ProdutoPrismaRepository(prisma); 11 | 12 | export { 13 | categoriaRepositorio, 14 | produtoRepositorio 15 | } -------------------------------------------------------------------------------- /src/modules/catalogo/infra/database/produto.prisma.repository.ts: -------------------------------------------------------------------------------- 1 | import { Categoria } from "@modules/catalogo/domain/categoria/categoria.entity"; 2 | import { Produto } from "@modules/catalogo/domain/produto/produto.entity"; 3 | import { IProdutoRepository } from "@modules/catalogo/domain/produto/produto.repository.interface"; 4 | import { StatusProduto } from "@modules/catalogo/domain/produto/produto.types"; 5 | import { ProdutoMap } from "@modules/catalogo/infra/mappers/produto.map"; 6 | import { Prisma } from "@prisma/client"; 7 | import { PrismaRepository } from "@shared/infra/database/prisma.repository"; 8 | import { produtoIncludeCategoriaPrisma } from "@shared/infra/database/prisma.types"; 9 | 10 | class ProdutoPrismaRepository extends PrismaRepository implements IProdutoRepository { 11 | 12 | 13 | async recuperarPorUuid(uuid: string): Promise { 14 | const produtoRecuperado = await this._datasource.produto.findUnique({ 15 | where: { 16 | id: uuid, 17 | }, 18 | include: produtoIncludeCategoriaPrisma 19 | }); 20 | if (produtoRecuperado){ 21 | return ProdutoMap.fromPrismaModelToDomain(produtoRecuperado); 22 | } 23 | return null; 24 | } 25 | 26 | async recuperarTodos(): Promise { 27 | const produtosRecuperados = await this._datasource.produto.findMany({ 28 | where: { 29 | dataExclusao: null, 30 | status: StatusProduto.ATIVO 31 | }, 32 | include: produtoIncludeCategoriaPrisma 33 | }); 34 | 35 | const produtos: Array = []; 36 | 37 | if (produtosRecuperados.length > 0) { 38 | produtosRecuperados.map((produto) => { 39 | produtos.push(ProdutoMap.fromPrismaModelToDomain(produto)); 40 | }); 41 | } 42 | return produtos; 43 | } 44 | 45 | async existe(uuid: string): Promise { 46 | const produto = await this.recuperarPorUuid(uuid); 47 | if (produto) {return true;} 48 | return false; 49 | } 50 | 51 | async inserir(produto: Produto): Promise { 52 | const produtoInserido = await this._datasource.produto.create({ 53 | data: { 54 | id: produto.id, 55 | nome: produto.nome, 56 | descricao: produto.descricao, 57 | valor: produto.valor, 58 | categorias: { 59 | create: produto.categorias.map((categoria) => { return {categoriaId: categoria.id} }) 60 | } 61 | } 62 | }); 63 | return produto; 64 | } 65 | 66 | async atualizar(uuid: string, produto: Partial): Promise { 67 | const produtoAtualizado = await this._datasource.produto.update( 68 | { 69 | where: {id : uuid}, 70 | data: { 71 | nome: produto.nome, 72 | descricao: produto.descricao, 73 | valor: produto.valor 74 | } 75 | } 76 | ); 77 | if (produtoAtualizado) {return true;} 78 | return false; 79 | } 80 | 81 | async deletar(uuid: string): Promise { 82 | const produtoDeletado = await this._datasource.produto.update( 83 | { 84 | where: { 85 | id: uuid 86 | }, 87 | data: { 88 | dataExclusao: new Date() 89 | } 90 | } 91 | ); 92 | if (produtoDeletado.id) {return true;} 93 | return false; 94 | } 95 | 96 | async adicionarCategoria(produto: Produto, categoria: Categoria): Promise { 97 | const categoriaProdutoAdicionada = await this._datasource.produtosCategorias.create( 98 | { 99 | data:{ 100 | produtoId: produto.id, 101 | categoriaId: categoria.id 102 | } 103 | } 104 | ); 105 | if (categoriaProdutoAdicionada) {return true;} 106 | return false; 107 | } 108 | 109 | async removerCategoria(produto: Produto, categoria: Categoria): Promise { 110 | const categoriaProdutoRemovida = await this._datasource.produtosCategorias.delete( 111 | { 112 | where: { 113 | produtoId_categoriaId: { 114 | produtoId: produto.id, 115 | categoriaId:categoria.id 116 | } 117 | } 118 | 119 | } 120 | ); 121 | if (categoriaProdutoRemovida) {return true;} 122 | return false; 123 | } 124 | 125 | async alterarStatus(produto: Produto, status: StatusProduto): Promise { 126 | const produtoStatusAlterado = await this._datasource.produto.update( 127 | { 128 | where: { 129 | id: produto.id 130 | }, 131 | data: { 132 | status: ProdutoMap.toStatusProdutoPrisma(status) 133 | } 134 | } 135 | ); 136 | if (produtoStatusAlterado.id) {return true;} 137 | return false; 138 | } 139 | 140 | async recuperarPorCategoria(idCategoria: string): Promise { 141 | const produtosPorCategoriaRecuperados = await this._datasource.produto.findMany({ 142 | where: { 143 | dataExclusao: null, 144 | status: StatusProduto.ATIVO, 145 | AND: [ 146 | { 147 | categorias: { 148 | some: { 149 | categoriaId: idCategoria 150 | } 151 | } 152 | } 153 | ] 154 | }, 155 | include: produtoIncludeCategoriaPrisma 156 | }); 157 | const produtos: Array = []; 158 | 159 | if (produtosPorCategoriaRecuperados.length > 0) { 160 | produtosPorCategoriaRecuperados.map((produto) => { 161 | produtos.push(ProdutoMap.fromPrismaModelToDomain(produto)); 162 | }); 163 | } 164 | return produtos; 165 | } 166 | 167 | } 168 | 169 | export { ProdutoPrismaRepository } -------------------------------------------------------------------------------- /src/modules/catalogo/infra/mappers/categoria.map.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { Categoria } from "../../domain/categoria/categoria.entity"; 3 | import { ICategoria, RecuperarCategoriaProps } from "../../domain/categoria/categoria.types"; 4 | 5 | class CategoriaMap { 6 | 7 | public static toDTO(categoria: Categoria): ICategoria { 8 | return { 9 | id: categoria.id, 10 | nome: categoria.nome, 11 | dataCriacao: categoria.dataCriacao, 12 | dataAtualizacao: categoria.dataAtualizacao 13 | } 14 | } 15 | 16 | public static toDomain(categoria: RecuperarCategoriaProps): Categoria { 17 | return Categoria.recuperar(categoria); 18 | } 19 | 20 | public static fromPrismaModelToDomain(categoriaPrisma: Prisma.CategoriaCreateInput): Categoria{ 21 | return CategoriaMap.toDomain({ 22 | id: categoriaPrisma.id, 23 | nome: categoriaPrisma.nome, 24 | dataCriacao: categoriaPrisma.dataCriacao as Date, 25 | dataAtualizacao: categoriaPrisma.dataAtualizacao as Date 26 | }); 27 | } 28 | 29 | } 30 | 31 | export { CategoriaMap }; 32 | -------------------------------------------------------------------------------- /src/modules/catalogo/infra/mappers/produto.map.ts: -------------------------------------------------------------------------------- 1 | import { ProdutoComCategoriaPrisma } from "@shared/infra/database/prisma.types"; 2 | import { Produto } from "../../domain/produto/produto.entity"; 3 | import { IProduto, RecuperarProdutoProps, StatusProduto } from "../../domain/produto/produto.types"; 4 | import { Categoria } from "../../domain/categoria/categoria.entity"; 5 | import { CategoriaMap } from "./categoria.map"; 6 | import { StatusProdutoPrisma } from "@prisma/client"; 7 | 8 | class ProdutoMap { 9 | 10 | public static toDTO(produto: Produto): IProduto { 11 | return { 12 | id: produto.id, 13 | nome: produto.nome, 14 | descricao: produto.descricao, 15 | valor: produto.valor, 16 | categorias: produto.categorias.map((categoria) => { return CategoriaMap.toDTO(categoria)}), 17 | dataCriacao: produto.dataCriacao, 18 | dataAtualizacao: produto.dataAtualizacao, 19 | dataExclusao: produto.dataExclusao, 20 | status: produto.status 21 | } 22 | } 23 | 24 | public static toDomain(produto: RecuperarProdutoProps): Produto { 25 | return Produto.recuperar(produto); 26 | } 27 | 28 | public static fromPrismaModelToDomain(produtoPrisma: ProdutoComCategoriaPrisma): Produto { 29 | 30 | //Define e inicializa um array de entidades de domínios categoria 31 | const categorias: Array = []; 32 | 33 | //Transforma as categorias obtidas com o prisma em entidades de domínio categoria 34 | produtoPrisma.categorias.map( 35 | (categoria) => { 36 | categorias.push( 37 | CategoriaMap.fromPrismaModelToDomain(categoria.categoria) 38 | ) 39 | } 40 | ); 41 | 42 | //Retorna um produto como uma entidade de domínio 43 | return this.toDomain({ 44 | id: produtoPrisma.id, 45 | nome: produtoPrisma.nome, 46 | descricao: produtoPrisma.descricao, 47 | valor: produtoPrisma.valor, 48 | categorias: categorias, 49 | dataCriacao: produtoPrisma.dataCriacao, 50 | dataAtualizacao: produtoPrisma.dataAtualizacao, 51 | dataExclusao: produtoPrisma.dataExclusao, 52 | status: StatusProduto[produtoPrisma.status] 53 | }); 54 | 55 | } 56 | 57 | public static toStatusProdutoPrisma(status: StatusProduto): StatusProdutoPrisma{ 58 | return StatusProdutoPrisma[status.toString() as keyof typeof StatusProdutoPrisma]; 59 | } 60 | 61 | 62 | } 63 | 64 | export { ProdutoMap } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/middlewares/valida-input-atualizar-categoria.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrors } from "@shared/presentation/http/http.error"; 2 | import { NextFunction, Request, Response } from "express"; 3 | import { z, ZodSchema } from "zod"; 4 | import { fromZodError } from 'zod-validation-error'; 5 | 6 | const atualizarCategoriaSchema = z.object( 7 | { 8 | id: z.string().uuid(), 9 | nome: z.string().min(3).max(50) 10 | } 11 | ).strict(); 12 | 13 | const validaInputAtualizarCategoriaMiddleware = ( 14 | request: Request, 15 | response: Response, 16 | next: NextFunction) => { 17 | try { 18 | atualizarCategoriaSchema.parse(request.body); 19 | next(); 20 | } catch (error: any) { 21 | const validationError = fromZodError(error); 22 | error = new HttpErrors.BadRequestError({message: validationError.message }); 23 | next(error); 24 | } 25 | } 26 | 27 | export { validaInputAtualizarCategoriaMiddleware as validaInputAtualizarCategoria } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/middlewares/valida-input-inserir-categoria.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrors } from "@shared/presentation/http/http.error"; 2 | import { NextFunction, Request, Response } from "express"; 3 | import { z, ZodSchema } from "zod"; 4 | import { fromZodError } from 'zod-validation-error'; 5 | 6 | const inserirCategoriaSchema = z.object( 7 | { 8 | nome: z.string().min(3).max(50) 9 | } 10 | ).strict(); 11 | 12 | const validaInputInserirCategoriaMiddleware = ( 13 | request: Request, 14 | response: Response, 15 | next: NextFunction) => { 16 | try { 17 | inserirCategoriaSchema.parse(request.body); 18 | next(); 19 | } catch (error: any) { 20 | const validationError = fromZodError(error); 21 | error = new HttpErrors.BadRequestError({message: validationError.message }); 22 | next(error); 23 | } 24 | } 25 | 26 | export { validaInputInserirCategoriaMiddleware as validaInputInserirCategoria } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/categoria.http: -------------------------------------------------------------------------------- 1 | @host = localhost 2 | @port = 3000 3 | @apiversion = api/v1 4 | 5 | ### Recuperar Um Categoria por ID 6 | GET http://{{host}}:{{port}}/{{apiversion}}/categorias/ac6c99e0-0759-47c1-89cb-5c6dc6e64852 7 | 8 | ### Recuperar Todas as Categorias 9 | GET http://{{host}}:{{port}}/{{apiversion}}/categorias 10 | 11 | ### Inserir Uma Categoria 12 | POST http://{{host}}:{{port}}/{{apiversion}}/categorias 13 | Content-type: application/json 14 | 15 | { 16 | "nome":"Sala" 17 | } 18 | 19 | ### Atualizar Categoria 20 | PUT http://{{host}}:{{port}}/{{apiversion}}/categorias/ac6c99e0-0759-47c1-89cb-5c6dc6e64853 21 | Content-type: application/json 22 | 23 | { 24 | "id": "ac6c99e0-0759-47c1-89cb-5c6dc6e64852", 25 | "nome":"Sala de Estar" 26 | } 27 | 28 | ### Deletar Categoria 29 | DELETE http://{{host}}:{{port}}/{{apiversion}}/categorias/ac6c99e0-0759-47c1-89cb-5c6dc6e64853 -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/categoria.routes.spec.ts: -------------------------------------------------------------------------------- 1 | import express, { Application } from "express"; 2 | import { afterEach, beforeAll, describe, expect, test, vi } from "vitest"; 3 | import { MockProxy, mock, mockReset } from "vitest-mock-extended"; 4 | import { RecuperarCategoriaPorIdExpressController } from "./controllers/recuperar-categoria-por-id.express.controller"; 5 | import { InserirCategoriaExpressController } from "./controllers/inserir-categoria.express.controller"; 6 | import { CriarCategoriaProps, ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 7 | import request from 'supertest'; 8 | 9 | let appMock: Application; 10 | let recuperarCategoriaPorIdControllerMock: MockProxy; 11 | let inserirCategoriaControllerMock: MockProxy; 12 | 13 | describe('[REST] Rotas Express: Categoria', () => { 14 | 15 | beforeAll(async () => { 16 | appMock = express(); 17 | recuperarCategoriaPorIdControllerMock = mock(); 18 | inserirCategoriaControllerMock = mock(); 19 | }); 20 | 21 | afterEach(() => { 22 | vi.restoreAllMocks(); 23 | mockReset(recuperarCategoriaPorIdControllerMock); 24 | mockReset(inserirCategoriaControllerMock); 25 | }); 26 | 27 | describe('GET api/v1/categorias/:id', () => { 28 | 29 | test('Deve Retornar Status 200 e um Objeto do Tipo ICategoria no formato JSON', async () => { 30 | 31 | //Dado (Given) 32 | const categoriaInputDTO: ICategoria = { 33 | id: "80830927-8c3e-4db9-9ddf-30ea191f139b", 34 | nome: "Cama" 35 | } 36 | 37 | recuperarCategoriaPorIdControllerMock.recuperar.mockImplementation(async (request, response, next) => { 38 | response.status(200).json(categoriaInputDTO); 39 | }); 40 | 41 | appMock.use('/api/v1/categorias/:id', recuperarCategoriaPorIdControllerMock.recuperar); 42 | 43 | //Quando (When) 44 | const response = await request(appMock) 45 | .get('/api/v1/categorias/80830927-8c3e-4db9-9ddf-30ea191f139b') 46 | 47 | //Então (Then 48 | expect(response.status).toEqual(200); 49 | expect(response.headers["content-type"]).toMatch(/json/); 50 | expect(response.body).toEqual(categoriaInputDTO); 51 | 52 | }); 53 | 54 | }); 55 | 56 | describe('POST api/v1/categorias', () => { 57 | 58 | test('Deve Retornar Status 200 e um Objeto do Tipo ICategoria no formato JSON', async () => { 59 | 60 | //Dado (Given) 61 | const categoriaInputDTO: CriarCategoriaProps = { 62 | nome: "Cama" 63 | }; 64 | 65 | const categoriaOutputDTO: ICategoria = { 66 | id: "80830927-8c3e-4db9-9ddf-30ea191f139b", 67 | nome: "Cama" 68 | } 69 | 70 | inserirCategoriaControllerMock.inserir.mockImplementation(async (request, response, next) => { 71 | response.status(200).json(categoriaOutputDTO); 72 | }); 73 | 74 | appMock.use('/api/v1/categorias', inserirCategoriaControllerMock.inserir); // Cast to any for mocking purposes 75 | 76 | //Quando (When) 77 | const response = await request(appMock) 78 | .post('/api/v1/categorias') 79 | .send(categoriaInputDTO) 80 | 81 | //Então (Then 82 | expect(response.status).toEqual(200); 83 | expect(response.headers["content-type"]).toMatch(/json/); 84 | expect(response.body).toEqual(categoriaOutputDTO); 85 | 86 | }); 87 | 88 | }); 89 | 90 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/categoria.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { atualizarCategoriaController, deletarCategoriaController, inserirCategoriaController, recuperarCategoriaPorIdController, recuperarTodasCategoriasController } from './controllers'; 3 | import { contentType } from '@main/presentation/http/middlewares/content-type.middleware'; 4 | import { validaInputInserirCategoria } from '../middlewares/valida-input-inserir-categoria.middleware'; 5 | import { validaInputAtualizarCategoria } from '../middlewares/valida-input-atualizar-categoria.middleware'; 6 | 7 | const categoriaRouter = express.Router(); 8 | 9 | categoriaRouter.get( 10 | '/:id', 11 | (request, response, next) => recuperarCategoriaPorIdController.recuperar(request, response, next) 12 | ) 13 | 14 | categoriaRouter.get( 15 | '/', 16 | (request, response, next) => recuperarTodasCategoriasController.recuperar(request, response, next) 17 | ) 18 | 19 | categoriaRouter.post( 20 | '/', 21 | contentType, 22 | validaInputInserirCategoria, 23 | (request, response, next) => inserirCategoriaController.inserir(request, response, next) 24 | ) 25 | 26 | categoriaRouter.put( 27 | '/:id', 28 | contentType, 29 | validaInputAtualizarCategoria, 30 | (request, response, next) => atualizarCategoriaController.atualizar(request, response, next) 31 | ) 32 | 33 | categoriaRouter.delete( 34 | '/:id', 35 | (request, response, next) => deletarCategoriaController.deletar(request, response, next) 36 | ) 37 | 38 | export { categoriaRouter }; -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/atualizar-categoria.express.controller.ts: -------------------------------------------------------------------------------- 1 | import { CategoriaApplicationExceptions } from "@modules/catalogo/application/exceptions/categoria.application.exception"; 2 | import { AtualizarCategoriaUseCase } from "@modules/catalogo/application/use-cases/atualizar-categoria/atualizar-categoria.use-case"; 3 | import { RecuperarCategoriaProps } from "@modules/catalogo/domain/categoria/categoria.types"; 4 | import { ExpressController } from "@shared/presentation/http/express.controller"; 5 | import { HttpErrors } from "@shared/presentation/http/http.error"; 6 | import { NextFunction, Request, Response } from "express"; 7 | 8 | class AtualizarCategoriaExpressController extends ExpressController { 9 | 10 | private _atualizarCategoriaUseCase: AtualizarCategoriaUseCase; 11 | 12 | constructor(atualizarCategoriaUseCase: AtualizarCategoriaUseCase) { 13 | super(); 14 | this._atualizarCategoriaUseCase = atualizarCategoriaUseCase; 15 | } 16 | 17 | async atualizar(request: Request, response: Response, next: NextFunction) { 18 | try { 19 | const categoriaInputDTO: RecuperarCategoriaProps = request.body as RecuperarCategoriaProps; 20 | const categoriaAtualizada: boolean = await this._atualizarCategoriaUseCase.execute(categoriaInputDTO); 21 | this.sendSuccessResponse(response,categoriaAtualizada); 22 | } catch (error) { 23 | if (error instanceof CategoriaApplicationExceptions.CategoriaNaoEncontrada){ 24 | error = new HttpErrors.NotFoundError({ message: error.message }); 25 | } 26 | next(error); 27 | } 28 | } 29 | 30 | } 31 | 32 | export { AtualizarCategoriaExpressController } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/deletar-categoria.express.controller.ts: -------------------------------------------------------------------------------- 1 | import { CategoriaApplicationExceptions } from "@modules/catalogo/application/exceptions/categoria.application.exception"; 2 | import { DeletarCategoriaUseCase } from "@modules/catalogo/application/use-cases/deletar-categoria/deletar-categoria.use-case"; 3 | import { ExpressController } from "@shared/presentation/http/express.controller"; 4 | import { HttpErrors } from "@shared/presentation/http/http.error"; 5 | import { NextFunction, Request, Response } from "express"; 6 | 7 | class DeletarCategoriaExpressController extends ExpressController { 8 | 9 | private _deletarCategoriaUseCase: DeletarCategoriaUseCase; 10 | 11 | constructor(deletarCategoriaUseCase: DeletarCategoriaUseCase) { 12 | super(); 13 | this._deletarCategoriaUseCase = deletarCategoriaUseCase; 14 | } 15 | 16 | async deletar(request: Request, response: Response, next: NextFunction) { 17 | try { 18 | const uuid:string = request.params.id; 19 | const categoriaDeletada: boolean = await this._deletarCategoriaUseCase.execute(uuid); 20 | this.sendSuccessResponse(response,categoriaDeletada); 21 | } catch (error) { 22 | if (error instanceof CategoriaApplicationExceptions.CategoriaNaoEncontrada){ 23 | error = new HttpErrors.NotFoundError({ message: error.message }); 24 | } 25 | next(error); 26 | } 27 | } 28 | 29 | } 30 | 31 | export { DeletarCategoriaExpressController } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import { atualizarCategoriaUseCase, deletarCategoriaUseCase, inserirCategoriaUseCase, recuperarCategoriaPorIdUseCase, recuperarTodasCategoriasUseCase } from "@modules/catalogo/application/use-cases"; 2 | import { RecuperarCategoriaPorIdExpressController } from "./recuperar-categoria-por-id.express.controller"; 3 | import { RecuperarTodasCategoriaExpressController } from "./recuperar-todas-categorias.express.controller"; 4 | import { InserirCategoriaExpressController } from "./inserir-categoria.express.controller"; 5 | import { AtualizarCategoriaExpressController } from "./atualizar-categoria.express.controller"; 6 | import { DeletarCategoriaExpressController } from "./deletar-categoria.express.controller"; 7 | 8 | const recuperarCategoriaPorIdController = new RecuperarCategoriaPorIdExpressController(recuperarCategoriaPorIdUseCase); 9 | const recuperarTodasCategoriasController = new RecuperarTodasCategoriaExpressController(recuperarTodasCategoriasUseCase); 10 | const inserirCategoriaController = new InserirCategoriaExpressController(inserirCategoriaUseCase); 11 | const atualizarCategoriaController = new AtualizarCategoriaExpressController(atualizarCategoriaUseCase); 12 | const deletarCategoriaController = new DeletarCategoriaExpressController(deletarCategoriaUseCase); 13 | 14 | export { 15 | recuperarCategoriaPorIdController, 16 | recuperarTodasCategoriasController, 17 | inserirCategoriaController, 18 | atualizarCategoriaController, 19 | deletarCategoriaController 20 | } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/inserir-categoria.express.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mock, afterEach, beforeAll, describe, expect, test, vi, vitest } from "vitest"; 2 | import { MockProxy, mock, mockReset } from "vitest-mock-extended"; 3 | import { Request, Response } from "express"; 4 | import { CriarCategoriaProps, ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 5 | import { InserirCategoriaExpressController } from "./inserir-categoria.express.controller"; 6 | import { InserirCategoriaUseCase } from "@modules/catalogo/application/use-cases/inserir-categoria/inserir-categoria.use-case"; 7 | 8 | 9 | let inserirCategoriaUseCaseMock: MockProxy; 10 | let requestMock: MockProxy; 11 | let responseMock: MockProxy; 12 | let nextMock: Mock; 13 | let inserirCategoriaController: InserirCategoriaExpressController; 14 | 15 | describe('Controller Express: Inserir Categoria por ID', () => { 16 | 17 | beforeAll(async () => { 18 | inserirCategoriaUseCaseMock = mock(); 19 | requestMock = mock(); 20 | responseMock = mock(); 21 | nextMock = vitest.fn(); 22 | inserirCategoriaController = new InserirCategoriaExpressController(inserirCategoriaUseCaseMock); 23 | }); 24 | 25 | afterEach(() => { 26 | vi.restoreAllMocks(); 27 | mockReset(requestMock); 28 | mockReset(responseMock); 29 | mockReset(inserirCategoriaUseCaseMock); 30 | }); 31 | 32 | test('Deve Inserir Uma Categoria por UUID', async () => { 33 | 34 | //Dado (Given) 35 | const categoriaInputDTO: CriarCategoriaProps = { 36 | nome: "Cama" 37 | }; 38 | 39 | requestMock.body = categoriaInputDTO; 40 | inserirCategoriaUseCaseMock.execute.mockResolvedValue(categoriaInputDTO); 41 | responseMock.status.mockReturnThis(); 42 | 43 | //Quando (When) 44 | await inserirCategoriaController.inserir(requestMock, responseMock, nextMock); 45 | 46 | //Então (Then) 47 | expect(inserirCategoriaUseCaseMock.execute).toHaveBeenCalledWith(categoriaInputDTO); 48 | expect(responseMock.status).toHaveBeenCalledWith(200); 49 | expect(responseMock.json).toHaveBeenCalledWith(categoriaInputDTO); 50 | expect(nextMock).not.toHaveBeenCalled(); 51 | 52 | }); 53 | 54 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/inserir-categoria.express.controller.ts: -------------------------------------------------------------------------------- 1 | import { InserirCategoriaUseCase } from "@modules/catalogo/application/use-cases/inserir-categoria/inserir-categoria.use-case"; 2 | import { CriarCategoriaProps, ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 3 | import { ExpressController } from "@shared/presentation/http/express.controller"; 4 | import { NextFunction, Request, Response } from "express"; 5 | 6 | class InserirCategoriaExpressController extends ExpressController { 7 | 8 | private _inserirCategoriaUseCase: InserirCategoriaUseCase; 9 | 10 | constructor(inserirCategoriaUseCase: InserirCategoriaUseCase) { 11 | super(); 12 | this._inserirCategoriaUseCase = inserirCategoriaUseCase; 13 | } 14 | 15 | async inserir(request: Request, response: Response, next: NextFunction) { 16 | try { 17 | const categoriaInputDTO: CriarCategoriaProps = request.body as CriarCategoriaProps; 18 | const categoriaOutputDTO: ICategoria = await this._inserirCategoriaUseCase.execute(categoriaInputDTO); 19 | this.sendSuccessResponse(response,categoriaOutputDTO); 20 | } 21 | catch (error){ 22 | next(error); 23 | } 24 | } 25 | 26 | } 27 | 28 | export { InserirCategoriaExpressController } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/recuperar-categoria-por-id.express.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { RecuperarCategoriaPorIdUseCase } from "@modules/catalogo/application/use-cases/recuperar-categoria-por-id/recuperar-categoria-por-id.use-case"; 2 | import { Request, Response } from "express"; 3 | import { Mock, afterEach, beforeAll, describe, expect, test, vi, vitest } from "vitest"; 4 | import { MockProxy, mock, mockReset } from "vitest-mock-extended"; 5 | import { RecuperarCategoriaPorIdExpressController } from "./recuperar-categoria-por-id.express.controller"; 6 | import { ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 7 | import { CategoriaApplicationExceptions } from "@modules/catalogo/application/exceptions/categoria.application.exception"; 8 | import { HttpError, HttpErrors } from "@shared/presentation/http/http.error"; 9 | 10 | 11 | let requestMock: MockProxy; 12 | let responseMock: MockProxy; 13 | let nextMock: Mock; 14 | let recuperarCategoriaPorIdUseCaseMock: MockProxy; 15 | let recuperarCategoriaPorIdController: RecuperarCategoriaPorIdExpressController; 16 | 17 | describe('Controller Express: Recuperar Categoria por ID', () => { 18 | 19 | beforeAll(async () => { 20 | requestMock = mock(); 21 | responseMock = mock(); 22 | nextMock = vitest.fn(); 23 | recuperarCategoriaPorIdUseCaseMock = mock(); 24 | recuperarCategoriaPorIdController = new RecuperarCategoriaPorIdExpressController(recuperarCategoriaPorIdUseCaseMock); 25 | }); 26 | 27 | afterEach(() => { 28 | vi.restoreAllMocks(); 29 | mockReset(requestMock); 30 | mockReset(responseMock); 31 | mockReset(recuperarCategoriaPorIdUseCaseMock); 32 | }); 33 | 34 | test('Deve Recuperar Uma Categoria por UUID', async () => { 35 | 36 | //Dado (Given) 37 | const categoriaInputDTO: ICategoria = { 38 | id: "80830927-8c3e-4db9-9ddf-30ea191f139b", 39 | nome: "Cama" 40 | } 41 | 42 | requestMock.params.id = categoriaInputDTO.id as string; 43 | recuperarCategoriaPorIdUseCaseMock.execute.mockResolvedValue(categoriaInputDTO); 44 | responseMock.status.mockReturnThis(); 45 | 46 | //Quando (When) 47 | await recuperarCategoriaPorIdController.recuperar(requestMock, responseMock, nextMock); 48 | 49 | //Então (Then 50 | expect(recuperarCategoriaPorIdUseCaseMock.execute).toHaveBeenCalledWith(categoriaInputDTO.id); 51 | expect(responseMock.status).toHaveBeenCalledWith(200); 52 | expect(responseMock.json).toHaveBeenCalledWith(categoriaInputDTO); 53 | expect(nextMock).not.toHaveBeenCalled(); 54 | 55 | }); 56 | 57 | test('Deve Tratar uma Exceção de Categoria Não Encontrada', async () => { 58 | 59 | //Dado (Given) 60 | const categoriaInputDTO: ICategoria = { 61 | id: "80830927-8c3e-4db9-9ddf-30ea191f139b", 62 | nome: "Cama" 63 | } 64 | 65 | requestMock.params.id = categoriaInputDTO.id as string; 66 | recuperarCategoriaPorIdUseCaseMock.execute.mockRejectedValue(new CategoriaApplicationExceptions.CategoriaNaoEncontrada()); 67 | responseMock.status.mockReturnThis(); 68 | 69 | //Quando (When) 70 | await recuperarCategoriaPorIdController.recuperar(requestMock, responseMock, nextMock); 71 | 72 | expect(recuperarCategoriaPorIdUseCaseMock.execute).toHaveBeenCalledWith(categoriaInputDTO.id); 73 | expect(nextMock).toHaveBeenCalled(); 74 | expect(nextMock.mock.lastCall[0].name).toBe(HttpErrors.NotFoundError.name); 75 | 76 | }); 77 | 78 | }); -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/recuperar-categoria-por-id.express.controller.ts: -------------------------------------------------------------------------------- 1 | import { CategoriaApplicationExceptions } from "@modules/catalogo/application/exceptions/categoria.application.exception"; 2 | import { RecuperarCategoriaPorIdUseCase } from "@modules/catalogo/application/use-cases/recuperar-categoria-por-id/recuperar-categoria-por-id.use-case"; 3 | import { ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 4 | import { ExpressController } from "@shared/presentation/http/express.controller"; 5 | import { HttpErrors } from "@shared/presentation/http/http.error"; 6 | import { NextFunction, Request, Response } from "express"; 7 | 8 | class RecuperarCategoriaPorIdExpressController extends ExpressController { 9 | 10 | private _recuperarCategoriaPorIdUseCase: RecuperarCategoriaPorIdUseCase; 11 | 12 | constructor(recuperarCategoriaPorIdUseCase: RecuperarCategoriaPorIdUseCase) { 13 | super(); 14 | this._recuperarCategoriaPorIdUseCase = recuperarCategoriaPorIdUseCase; 15 | } 16 | 17 | async recuperar(request: Request, response: Response, next: NextFunction) { 18 | try { 19 | const uuid:string = request.params.id; 20 | const categoriaOutputDTO: ICategoria = await this._recuperarCategoriaPorIdUseCase.execute(uuid); 21 | this.sendSuccessResponse(response,categoriaOutputDTO); 22 | } 23 | catch (error) { 24 | if (error instanceof CategoriaApplicationExceptions.CategoriaNaoEncontrada){ 25 | error = new HttpErrors.NotFoundError({ message: error.message }); 26 | } 27 | next(error); 28 | } 29 | } 30 | 31 | } 32 | 33 | export { RecuperarCategoriaPorIdExpressController } -------------------------------------------------------------------------------- /src/modules/catalogo/presentation/http/rest/controllers/recuperar-todas-categorias.express.controller.ts: -------------------------------------------------------------------------------- 1 | import { RecuperarTodasCategoriasUseCase } from "@modules/catalogo/application/use-cases/recuperar-todas-categorias/recuperar-todas-categorias.use-case"; 2 | import { ICategoria } from "@modules/catalogo/domain/categoria/categoria.types"; 3 | import { ExpressController } from "@shared/presentation/http/express.controller"; 4 | import { NextFunction, Request, Response } from "express"; 5 | 6 | class RecuperarTodasCategoriaExpressController extends ExpressController { 7 | 8 | private _recuperarTodasCategoriaUseCase: RecuperarTodasCategoriasUseCase; 9 | 10 | constructor(recuperarTodasCategoriaUseCase: RecuperarTodasCategoriasUseCase) { 11 | super(); 12 | this._recuperarTodasCategoriaUseCase = recuperarTodasCategoriaUseCase; 13 | } 14 | 15 | async recuperar(request: Request, response: Response, next: NextFunction) { 16 | try { 17 | const listaCategoriasDTO: Array = await this._recuperarTodasCategoriaUseCase.execute(); 18 | this.sendSuccessResponse(response,listaCategoriasDTO); 19 | } catch (error) { 20 | next(error); 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | export { RecuperarTodasCategoriaExpressController } -------------------------------------------------------------------------------- /src/modules/catalogo/requirements/adicionar-categoria-produto-caso-uso.feature: -------------------------------------------------------------------------------- 1 | Feature: Adicionar categoria ao produto 2 | Como um 3 | Eu quero 4 | De modo que 5 | 6 | Scenario: Categoria válida e produto válido apto a ter uma nova categoria adicionada (Padrão) 7 | Dado (Given) [ 8 | Um produto válido apto a ter uma nova categoria adicionada - Ter no mínimo (1) e no máximo (2) categoria(s) já adicionada(s) 9 | Uma categoria válida 10 | ] 11 | Quando (When) [Solicitar a adição da categoria ao produto] 12 | Então (Then) [A categoria deve ser adicionada e retornada] 13 | 14 | Scenario: Categoria válida e produto válido inapto a ter uma nova categoria adicionada - quantidade máxima de categorias 15 | Dado (Given) [ 16 | Um produto válido inapto a ter uma nova categoria adicionada - Ter (3) categorias já adicionadas 17 | Uma categoria válida 18 | ] 19 | Quando (When) [Solicitar a adição da categoria ao produto] 20 | Então (Then) [Um erro informando que o produto já possui número máximo de categorias adicionadas] 21 | 22 | Scenario: Categoria válida e produto válido inapto a ter uma categoria adicionada - categoria já adicionada 23 | Dado (Given) [ 24 | Um produto válido inapto a ter uma categoria adicionada - categoria já adicionada 25 | Uma categoria válida 26 | ] 27 | Quando (When) [Solicitar a adição da categoria ao produto] 28 | Então (Then) [Um erro informando que o produto já possui possui a categoria adicionada] -------------------------------------------------------------------------------- /src/modules/catalogo/requirements/criar-categoria-caso-uso.feature: -------------------------------------------------------------------------------- 1 | Feature: Criar Categoria 2 | Como um 3 | Eu quero 4 | De modo que 5 | 6 | Scenario: Categoria válida (Padrão) 7 | Dado (Given) [Categoria válida] 8 | Quando (When) [Solicitar a Criação de uma Categoria] 9 | Então (Then) [A categoria deve ser criada corretamente] 10 | 11 | Scenario: Categoria inválida - Nome da Categoria é nulo ou indefinido 12 | Dado [Uma categoria com nome nulo ou indefinido] 13 | Quando [Solicitar a Criação de uma Categoria] 14 | Então [Um erro informando que o nome da categoria é nulo ou indefinido deve ser apresentado] 15 | 16 | Scenario: Categoria inválida - Nome da Categoria não atende o tamanho mínino (Execeção) 17 | Dado [Uma categoria com nome que não atende ao tamanho mínimo] 18 | Quando [Solicitar a Criação de uma Categoria] 19 | Então [Um erro informando que o nome da categoria não possui um tamanho mínimo válido deve ser apresentado] 20 | 21 | Scenario: Categoria inválida - Nome da Categoria não atende o tamanho máximo (Execeção) 22 | Dado [Uma categoria com nome que não atende ao tamanho máximo] 23 | Quando [Solicitar a Criação de uma Categoria] 24 | Então [Um erro informando que o nome da categoria não possui um tamanho máximo válido deve ser apresentado] -------------------------------------------------------------------------------- /src/modules/catalogo/requirements/criar-produto-caso-uso.feature: -------------------------------------------------------------------------------- 1 | Feature: Criar Produto 2 | Como um 3 | Eu quero 4 | De modo que 5 | 6 | Scenario: Produto válido (Padrão) 7 | Dado (Given) [Produto válido] 8 | Quando (When) [Solicitar a criação de um produto] 9 | Então (Then) [O Produto deve ser criado corretamente] 10 | 11 | Scenario: Produto inválido - Nome do produto não atende o tamanho mínimo (5) (Execeção) 12 | Dado [Um produto com nome que não atende ao tamanho mínimo] 13 | Quando [Solicitar a criação de um produto] 14 | Então [Um erro informando que o nome do produto não possui um tamanho mínimo válido deve ser apresentado] 15 | 16 | Scenario: Produto inválido - Nome do produto não atende o tamanho máximo (50) (Execeção) 17 | Dado [Um produto com nome que não atende ao tamanho máximo] 18 | Quando [Solicitar a criação de um produto] 19 | Então [Um erro informando que o nome do produto não possui um tamanho máximo válido deve ser apresentado] 20 | 21 | Scenario: Produto inválido - Descrição do produto não atende o tamanho mínimo (10) (Execeção) 22 | Dado [Um produto com descrição que não atende ao tamanho mínimo] 23 | Quando [Solicitar a criação de um produto] 24 | Então [Um erro informando que o descrição do produto não possui um tamanho mínimo válido deve ser apresentado] 25 | 26 | Scenario: Produto inválido - Descrição do produto não atende o tamanho máximo (200) (Execeção) 27 | Dado [Um produto com descrição que não atende ao tamanho máximo] 28 | Quando [Solicitar a criação de um produto] 29 | Então [Um erro informando que o descrição do produto não possui um tamanho máximo válido deve ser apresentado] 30 | 31 | Scenario: Produto inválido - Valor do produto não atende ao valor mínimo (0) (Execeção) 32 | Dado [Um produto com valor que não atende ao valor mínimo] 33 | Quando [Solicitar a criação de um produto] 34 | Então [Um erro informando que o valor do produto não possui um valor mínimo válido deve ser apresentado] 35 | 36 | Scenario: Produto inválido - Produto não tem um número mínimo válido de categorias (1) (Execeção) 37 | Dado [Um produto com um número mínimo inválido de categorias] 38 | Quando [Solicitar a criação de um produto] 39 | Então [Um erro informando que o produto não tem um número mínimo válido de categorias] 40 | 41 | Scenario: Produto inválido - Produto não tem um número máximo válido de categorias (3) (Execeção) 42 | Dado [Um produto com um número máximo inválido de categorias] 43 | Quando [Solicitar a criação de um produto] 44 | Então [Um erro informando que o produto não tem um número máximo válido de categorias] -------------------------------------------------------------------------------- /src/modules/catalogo/requirements/recuperar-categoria-caso-uso.feature: -------------------------------------------------------------------------------- 1 | Feature: Recuperar Categoria 2 | Como um 3 | Eu quero 4 | De modo que 5 | 6 | Scenario: Categoria válida (Padrão) 7 | Dado (Given) [Categoria válida] 8 | Quando (When) [Solicitar a Recuperação de uma Categoria] 9 | Então (Then) [A categoria deve ser recuperada corretamente] 10 | 11 | Scenario: Categoria inválida - ID da Categoria é um UUID inválido 12 | Dado [Uma categoria com UUID inválido ] 13 | Quando [Solicitar a Recuperação de uma Categoria] 14 | Então [Um erro informando que o ID da categoria é um UUID inválido] 15 | 16 | Scenario: Categoria inválida - Nome da Categoria não atende o tamanho mínino (Execeção) 17 | Dado [Uma categoria com nome que não atende ao tamanho mínimo] 18 | Quando [Solicitar a Recuperação de uma Categoria] 19 | Então [Um erro informando que o nome da categoria não possui um tamanho mínimo válido deve ser apresentado] 20 | 21 | Scenario: Categoria inválida - Nome da Categoria não atende o tamanho máximo (Execeção) 22 | Dado [Uma categoria com nome que não atende ao tamanho máximo] 23 | Quando [Solicitar a Recuperação de uma Categoria] 24 | Então [Um erro informando que o nome da categoria não possui um tamanho máximo válido deve ser apresentado] -------------------------------------------------------------------------------- /src/modules/catalogo/requirements/remover-categoria-produto-caso-uso.feature: -------------------------------------------------------------------------------- 1 | Feature: Remover Categoria do Produto 2 | Como um 3 | Eu quero 4 | De modo que 5 | 6 | Scenario: Categoria válida e produto válido apto a ter uma categoria removida (Padrão) 7 | Dado (Given) [ 8 | Um produto válido apto a ter uma categoria removida - Ter no mínimo (2) e no máximo (3) categorias já adicionadas 9 | Uma categoria válida 10 | ] 11 | Quando (When) [Solicitar a remoção da categoria do produto] 12 | Então (Then) [A categoria deve ser removida corretamente e retornada] 13 | 14 | Scenario: Categoria válida e produto válido inapto a ter uma categoria removida - quantidade mínima de categorias 15 | Dado (Given) [ 16 | Um produto válido inapto a ter uma categoria removida - Ter apenas (1) categoria adicionada 17 | Uma categoria válida 18 | ] 19 | Quando (When) [Solicitar a remoção da categoria do produto] 20 | Então (Then) [Um erro informando que o produto já possui número mínimo de categorias] 21 | 22 | Scenario: Categoria válida e produto válido inapto a ter uma categoria removida - categoria não associada ao produto 23 | Dado (Given) [ 24 | Um produto válido inapto a ter uma categoria removida - categoria não associada ao produto 25 | Uma categoria válida 26 | ] 27 | Quando (When) [Solicitar a remoção da categoria do produto] 28 | Então (Then) [Um erro informando que o produto não possui a categoria informada a ser removida] -------------------------------------------------------------------------------- /src/shared/application/application.exception.ts: -------------------------------------------------------------------------------- 1 | class ApplicationException extends Error { 2 | constructor(message:string = '⚠️ Exceção de aplicação genérica') { 3 | super(message); 4 | this.name = 'ApplicationException'; 5 | this.message = message; 6 | Error.captureStackTrace(this, this.constructor) 7 | } 8 | } 9 | 10 | export { 11 | ApplicationException 12 | } -------------------------------------------------------------------------------- /src/shared/application/use-case.interface.ts: -------------------------------------------------------------------------------- 1 | interface IUseCase { 2 | execute(input?: InputDTO): Promise; 3 | } 4 | 5 | export { IUseCase } -------------------------------------------------------------------------------- /src/shared/domain/datas.types.ts: -------------------------------------------------------------------------------- 1 | interface IDatasControle { 2 | dataCriacao?: Date; 3 | dataAtualizacao?: Date; 4 | dataExclusao?: Date | null; 5 | } 6 | 7 | type KeysDatasControle = keyof IDatasControle; 8 | 9 | export { IDatasControle, KeysDatasControle } -------------------------------------------------------------------------------- /src/shared/domain/domain.exception.ts: -------------------------------------------------------------------------------- 1 | class DomainException extends Error { 2 | constructor(message:string = '⚠️ Exceção de domínio genérica') { 3 | super(message); 4 | this.name = 'DomainException'; 5 | this.message = message; 6 | Error.captureStackTrace(this, this.constructor) 7 | } 8 | } 9 | 10 | class IDEntityUUIDInvalid extends DomainException { 11 | public constructor(message:string = '⚠️ O ID da entidade é um UUID inválido.') { 12 | super(message); 13 | this.name = 'IDEntityUUIDInvalid' 14 | this.message = message; 15 | } 16 | } 17 | 18 | export { 19 | DomainException, 20 | IDEntityUUIDInvalid 21 | } -------------------------------------------------------------------------------- /src/shared/domain/entity.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from "crypto"; 2 | import { IDEntityUUIDInvalid } from "./domain.exception"; 3 | 4 | //Método que verifica se é a instância de uma entidade 5 | const isEntity = (v: any): v is Entity => { 6 | return v instanceof Entity; 7 | }; 8 | 9 | abstract class Entity { 10 | 11 | ///////////// 12 | //Atributos// 13 | ///////////// 14 | 15 | private _id: string; 16 | 17 | /////////////// 18 | //Gets e Sets// 19 | /////////////// 20 | 21 | public get id(): string { 22 | return this._id; 23 | } 24 | 25 | private set id(value: string) { 26 | 27 | if (!Entity.validUUID(value)){ 28 | throw new IDEntityUUIDInvalid(); 29 | } 30 | 31 | this._id = value; 32 | } 33 | 34 | ////////////// 35 | //Construtor// 36 | ////////////// 37 | 38 | constructor(id?: string) { 39 | this.id = id ? id : randomUUID(); 40 | } 41 | 42 | /////////// 43 | //Métodos// 44 | /////////// 45 | 46 | public equals(object?: Entity): boolean { 47 | if (object == null || object == undefined) { 48 | return false 49 | } 50 | 51 | if (this === object) { 52 | return true 53 | } 54 | 55 | if (!isEntity(object)) { 56 | return false 57 | } 58 | 59 | return this._id == object._id 60 | } 61 | 62 | public static validUUID(UUID: string): boolean { 63 | let padraoUUID: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 64 | return padraoUUID.test(UUID); 65 | } 66 | 67 | } 68 | 69 | export { Entity } -------------------------------------------------------------------------------- /src/shared/domain/repository.interface.ts: -------------------------------------------------------------------------------- 1 | interface IQuery { 2 | recuperarPorUuid(uuid: string): Promise; 3 | recuperarTodos(): Promise>; 4 | existe(uuid: string): Promise; 5 | } 6 | 7 | interface ICommand { 8 | inserir(entity: T): Promise; 9 | atualizar(uuid: string, entity: Partial): Promise; 10 | deletar(uuid: string): Promise; 11 | } 12 | 13 | interface IRepository extends IQuery, ICommand {}; 14 | 15 | export { IRepository } -------------------------------------------------------------------------------- /src/shared/helpers/logger.winston.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | 3 | //Tipos de Severidade 4 | type LevelName = "error" | "warn" | "ok" | "info" | "http" | "sql" | "debug"; 5 | 6 | interface ILevel { 7 | id:number; 8 | color:string; 9 | emoji:string; 10 | } 11 | 12 | interface CustomLevels extends winston.Logger { 13 | error: winston.LeveledLogMethod; 14 | warn: winston.LeveledLogMethod; 15 | ok: winston.LeveledLogMethod; 16 | info: winston.LeveledLogMethod; 17 | http: winston.LeveledLogMethod; 18 | sql: winston.LeveledLogMethod; 19 | debug: winston.LeveledLogMethod; 20 | } 21 | 22 | const LevelsRecord: Record = { 23 | error: {id:0 , color: 'red', emoji:'🔴 '}, 24 | warn: {id:1, color: 'yellow', emoji:'🟡'}, 25 | ok: {id:2, color: 'green', emoji:'🟢'}, 26 | info: {id:3, color: 'blue', emoji:'🔵 '}, 27 | http: {id:4, color: 'italic white magentaBG' , emoji:'🌐 '}, 28 | sql: {id:5, color: 'italic gray whiteBG', emoji:'🔎 '}, 29 | debug: {id:6, color: 'italic white redBG', emoji:'🐞 '}, 30 | }; 31 | 32 | const levels: winston.config.AbstractConfigSetLevels = Object.fromEntries(Object.keys(LevelsRecord).map(key => [key, LevelsRecord[key as LevelName].id])); 33 | const colors = Object.fromEntries(Object.keys(LevelsRecord).map(key => [key, LevelsRecord[key as LevelName].color])); 34 | const emojis = Object.fromEntries(Object.keys(LevelsRecord).map(key => [key, LevelsRecord[key as LevelName].emoji])); 35 | 36 | //Este método define a gravidade atual com base no NODE_ENV atual 37 | //Mostra todos os níveis de log se o servidor estiver executando em modo de desenvolvimento; 38 | //Se está sendo executado em produção, mostra apenas mensagens de advertência e erro. 39 | const level = () => { 40 | const env = process.env.NODE_ENV || 'development' 41 | const isDevelopment = env === 'development' 42 | return isDevelopment ? 'debug' : 'warn' 43 | }; 44 | 45 | //Configura as cores no winston 46 | //Definido acima para os níveis de severidade/levels existentes 47 | winston.addColors(colors); 48 | 49 | //Cria um timestamp da mensagem com o formato preferido 50 | const timeStampDefault = winston.format.timestamp({ format: 'YYYY.MM.DD HH:mm:ss A' }); 51 | 52 | //Indica ao Winston que os logs devem ser coloridos 53 | const colorizaDefault = winston.format.colorize({ all: true }); 54 | 55 | //Define o formato da mensagem para o console mostrando o timestamp de data/hora, nome da api, emoji que indica o level e a mensagem 56 | const printfConsole = winston.format.printf( 57 | ({ level, message, label, timestamp }) => { 58 | const cleanLevel = level.replace(/\u001b\[.*?m/g, ''); 59 | const emoji = emojis[cleanLevel as keyof typeof emojis]; 60 | const cleanEmoji = emoji.replace(/\u001b\[.*?m/g, ''); 61 | return `[${process.env.API_NAME}] ${cleanEmoji} ${message}`; 62 | }, 63 | ); 64 | 65 | //Define o formato da mensagem para o arquivo de log mostrando o timestamp de data/hora, nome da api, emoji que indica o level e a mensagem 66 | const printfFileLog = winston.format.printf( 67 | ({ level, message, label, timestamp }) => { 68 | const cleanLevel = level.replace(/\u001b\[.*?m/g, ''); 69 | const cleanMessage = message.replace(/\u001b\[.*?m/g, ''); 70 | const emoji = emojis[cleanLevel as keyof typeof emojis]; 71 | return `[${timestamp}][${process.env.API_NAME}] ${emoji.trim()} ${cleanMessage}`; 72 | }, 73 | ); 74 | 75 | //Personalizando o formato padrão usado do log no console 76 | const formatDefault = winston.format.combine( 77 | timeStampDefault, 78 | colorizaDefault, 79 | printfConsole 80 | ); 81 | 82 | //Personalizando o formato padrão usado no arquivo de log 83 | const formatFileLog = winston.format.combine( 84 | timeStampDefault, 85 | colorizaDefault, 86 | printfFileLog 87 | ); 88 | 89 | // Define quais transportes o logger deve utilizar para imprimir mensagens. 90 | // Neste exemplo, estamos usando três transportes diferentes 91 | const transports = [ 92 | //Permitir usar o console para imprimir as mensagens 93 | new winston.transports.Console(), 94 | //Permitir imprimir todas as mensagens de nível de erro dentro do arquivo error.log 95 | new winston.transports.File({ 96 | filename: 'logs/error.log', 97 | level: 'error', 98 | format: formatFileLog 99 | }), 100 | //Permite imprimir todas as mensagens dentro do arquivo api.log 101 | new winston.transports.File({ 102 | filename: 'logs/api.log', 103 | format: formatFileLog 104 | }), 105 | ]; 106 | 107 | //Cria a instância do logger que deve ser exportada e usado para registrar mensagens. 108 | const logger: CustomLevels = winston.createLogger({ 109 | level: level(), 110 | levels: levels, 111 | format: formatDefault, 112 | transports: transports 113 | }); 114 | 115 | export { logger } -------------------------------------------------------------------------------- /src/shared/infra/database/prisma.repository.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | abstract class PrismaRepository { 4 | 5 | protected _datasource: PrismaClient; 6 | 7 | constructor(prisma: PrismaClient){ 8 | this._datasource = prisma; 9 | } 10 | 11 | } 12 | 13 | export { PrismaRepository } -------------------------------------------------------------------------------- /src/shared/infra/database/prisma.types.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | 3 | const produtoIncludeCategoriaPrisma = Prisma.validator()( 4 | { 5 | categorias: { 6 | include: { 7 | categoria: true 8 | } 9 | } 10 | } 11 | ); 12 | 13 | type ProdutoComCategoriaPrisma = Prisma.ProdutoGetPayload< 14 | {include: typeof produtoIncludeCategoriaPrisma;} 15 | >; 16 | 17 | export { produtoIncludeCategoriaPrisma, ProdutoComCategoriaPrisma } -------------------------------------------------------------------------------- /src/shared/presentation/http/express.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | 3 | abstract class ExpressController { 4 | 5 | protected sendSuccessResponse(response: Response, data: any, status = 200) { 6 | response.status(status).json(data); 7 | } 8 | 9 | protected sendErrorResponse(response: Response, error: Error, status = 500) { 10 | response.status(status).json({ error: error.message }); 11 | } 12 | 13 | protected sendNotFoundResponse(response: Response, message = 'Not found') { 14 | response.status(404).json({ error: message }); 15 | } 16 | 17 | } 18 | 19 | export { ExpressController } -------------------------------------------------------------------------------- /src/shared/presentation/http/http.error.ts: -------------------------------------------------------------------------------- 1 | class HttpError extends Error { 2 | statusCode: number; 3 | 4 | constructor(statusCode:number, message: string = '⚠️ Erro HTTP genérico') { 5 | super(message); 6 | this.name = 'HttpError'; 7 | this.statusCode = statusCode; 8 | this.message = message; 9 | Object.setPrototypeOf(this, HttpError.prototype); 10 | Error.captureStackTrace(this, this.constructor); 11 | } 12 | 13 | } 14 | 15 | class NotFoundError extends HttpError { 16 | constructor( params?: {statusCode?: number, message?: string}) { 17 | const { statusCode, message} = params || {}; 18 | super(statusCode || 404, message || '⚠️ Servidor Não Conseguiu Encontrar o Recurso Solicitado.'); 19 | this.name = 'NotFoundError'; 20 | } 21 | } 22 | 23 | class UnsupportedMediaTypeError extends HttpError { 24 | constructor( params?: {statusCode?: number, message?: string}) { 25 | const { statusCode, message} = params || {}; 26 | super(statusCode || 415, message || '⚠️ Servidor se Recusou a Aceitar a Requisição Porque o Formato do Payload Não é Um Formato Suportado.'); 27 | this.name = 'UnsupportedMediaTypeError'; 28 | } 29 | } 30 | 31 | class BadRequestError extends HttpError { 32 | constructor( params?: {statusCode?: number, message?: string}) { 33 | const { statusCode, message} = params || {}; 34 | super(statusCode || 400, message || '⚠️ Servidor Não Pode ou Não Irá Processar a Requisição Devido a Algum Erro do Cliente (ex.: sintaxe de requisição mal formada, enquadramento de mensagem de requisição inválida ou requisição de roteamento enganosa.'); 35 | this.name = 'BadRequestError'; 36 | } 37 | } 38 | 39 | const HttpErrors = { 40 | NotFoundError: NotFoundError, 41 | UnsupportedMediaTypeError: UnsupportedMediaTypeError, 42 | BadRequestError: BadRequestError 43 | } 44 | 45 | export { HttpError, HttpErrors } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | "module": "commonjs", /* Specify what module code is generated. */ 5 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 6 | "paths": { 7 | "@modules/*": ["./modules/*"], 8 | "@shared/*": ["./shared/*"], 9 | "@main/*": ["./main/*"] 10 | }, /* Specify a set of entries that re-map imports to additional lookup locations. */ 11 | "rootDirs": ["src"], /* Allow multiple folders to be treated as one when resolving modules. */ 12 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 13 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | "strict": true, /* Enable all strict type-checking options. */ 17 | "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ 18 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | /* for example, use global to avoid globals imports (describe, test, expect): */ 7 | globals: true, 8 | }, 9 | resolve: { 10 | alias: [ 11 | { 12 | find: "@modules", 13 | replacement: path.resolve(__dirname, "src/modules"), 14 | }, 15 | { 16 | find: "@shared", 17 | replacement: path.resolve(__dirname, "src/shared"), 18 | }, 19 | { 20 | find: "@main", 21 | replacement: path.resolve(__dirname, "src/main"), 22 | } 23 | ] 24 | } 25 | }) --------------------------------------------------------------------------------