From 34b876c826f1b123592ec8a6206a8cba274c3012 Mon Sep 17 00:00:00 2001 From: rafamagalhas Date: Thu, 10 Aug 2023 22:33:25 -0300 Subject: [PATCH 1/4] chore: add husky pre-push --- .husky/pre-push | 4 ++++ package.json | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100755 .husky/pre-push diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..610c2a5 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm test diff --git a/package.json b/package.json index df723b1..37d52d0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "rm -rf ./dist/ && tsc", "start": "nodemon src/index.ts", - "test": "jest --maxWorkers 2" + "test": "jest --maxWorkers 2", + "prepare": "husky install" }, "repository": { "type": "git", From 0e20fbd97c1ea640bda106d5c3f90915e8031925 Mon Sep 17 00:00:00 2001 From: rafamagalhas Date: Thu, 10 Aug 2023 22:44:23 -0300 Subject: [PATCH 2/4] feat: add docker-compose to upsert app --- Dockerfile | 14 ++++++++++++++ docker-compose.yml | 15 +++++++++++++++ package.json | 5 +++-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2278e68 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:18-alpine AS server +WORKDIR /app +COPY package* ./ +RUN npm ci +COPY --from=builder ./app/dist ./dist +EXPOSE 3000 +CMD ["npm", "run", "start-prod"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5e46721 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3.1" + +services: + app: + build: + context: . + dockerfile: Dockerfile + env_file: + - ./.env + container_name: beleza-na-web + ports: + - "3000:3000" + environment: + - PORT=3000 + restart: always diff --git a/package.json b/package.json index 37d52d0..3d7dca3 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "main": "index.js", "scripts": { "build": "rm -rf ./dist/ && tsc", + "prepare": "husky install", "start": "nodemon src/index.ts", - "test": "jest --maxWorkers 2", - "prepare": "husky install" + "start-prod": "node dist/index.js", + "test": "jest --maxWorkers 2" }, "repository": { "type": "git", From 450ced4ad5c9b74959f52b13ccf48a80628a7f4a Mon Sep 17 00:00:00 2001 From: rafamagalhas Date: Fri, 11 Aug 2023 09:28:21 -0300 Subject: [PATCH 3/4] feat: add products error message enum --- src/business/product.ts | 9 ++++--- src/controllers/product.ts | 9 ++++--- src/utils/errorMessages.ts | 4 +++ tests/integration/products/fail.test.ts | 9 +++++++ .../products/mocks/products-mock.ts | 25 +++++++++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 src/utils/errorMessages.ts diff --git a/src/business/product.ts b/src/business/product.ts index 580f46f..610abb0 100644 --- a/src/business/product.ts +++ b/src/business/product.ts @@ -1,10 +1,11 @@ import { product } from "./../models/product"; import { IProduct } from "../interfaces/product"; import { productListParser, productParser } from "../parsers/product"; +import { productErrorMessages } from "../utils/errorMessages"; export async function createBusiness(data: IProduct): Promise { if (product.checkExists(data.sku)) { - throw new Error("Product already exists"); + throw new Error(productErrorMessages.CONFLICT); } return product.create(data); @@ -18,7 +19,7 @@ export async function findAll(): Promise { export async function findBySku(sku: number): Promise { const response = product.findBySku(sku); if (response.length === 0) { - throw new Error("Product not found"); + throw new Error(productErrorMessages.NOT_FOUND); } return productParser(response[0]); @@ -28,7 +29,7 @@ export async function update(sku: number, payload: IProduct): Promise const productIndex: number = product.getProductIndex(sku); if (productIndex === -1) { - throw new Error("Product not found"); + throw new Error(productErrorMessages.NOT_FOUND); } payload.sku = sku; const response = product.update(productIndex, payload); @@ -37,7 +38,7 @@ export async function update(sku: number, payload: IProduct): Promise export async function remove(sku: number): Promise { if (!product.checkExists(sku)) { - throw new Error("Product not found"); + throw new Error(productErrorMessages.NOT_FOUND); } product.remove(sku); } diff --git a/src/controllers/product.ts b/src/controllers/product.ts index 21f9ff4..adc8f68 100644 --- a/src/controllers/product.ts +++ b/src/controllers/product.ts @@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { createBusiness, findAll, findBySku, remove, update } from "../business/product"; import { StatusCodes } from "http-status-codes"; import { logger } from "../utils/winston"; +import { productErrorMessages } from "../utils/errorMessages"; export async function createController( req: Request, @@ -12,7 +13,7 @@ export async function createController( const response = await createBusiness(req.body); return res.status(StatusCodes.CREATED).json(response); } catch (error) { - if (error.message === "Product already exists") { + if (error.message === productErrorMessages.CONFLICT) { logger.error(error.message); return res.status(StatusCodes.CONFLICT).json({ message: error.message }); } @@ -42,7 +43,7 @@ export async function findBySkuController( const response = await findBySku(parseInt(req.params.sku)); return res.status(StatusCodes.OK).json(response); } catch (error) { - if (error.message === "Product not found") { + if (error.message === productErrorMessages.NOT_FOUND) { logger.info(error.message); return res.status(StatusCodes.NOT_FOUND).json({ message: error.message }); } @@ -59,7 +60,7 @@ export async function updateController( const response = await update(parseInt(req.params.sku), req.body); return res.status(StatusCodes.OK).json(response); } catch (error) { - if (error.message === "Product not found") { + if (error.message === productErrorMessages.NOT_FOUND) { logger.error(error); return res.status(StatusCodes.NOT_FOUND).json({ message: error.message }); } @@ -76,7 +77,7 @@ export async function removeController( await remove(parseInt(req.params.sku)); return res.status(StatusCodes.NO_CONTENT).send(); } catch (error) { - if (error.message === "Product not found") { + if (error.message === productErrorMessages.NOT_FOUND) { logger.error(error.message); return res.status(StatusCodes.NOT_FOUND).json({ message: error.message }); } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts new file mode 100644 index 0000000..5538413 --- /dev/null +++ b/src/utils/errorMessages.ts @@ -0,0 +1,4 @@ +export enum productErrorMessages { + NOT_FOUND = 'Product not found', + CONFLICT = 'Product already exists' +} diff --git a/tests/integration/products/fail.test.ts b/tests/integration/products/fail.test.ts index 1395536..9d63927 100644 --- a/tests/integration/products/fail.test.ts +++ b/tests/integration/products/fail.test.ts @@ -4,6 +4,15 @@ import * as mocks from "./mocks/products-mock"; import * as business from "../../../src/business/product"; describe("/api/v1/product endpoint fail test", () => { + test("[POST] should return 400 when try create a product with inventory quantity", async () => { + const res = await request(app) + .post("/api/v1/products") + .send(mocks.productWithQuantity); + + expect(res.statusCode).toEqual(400); + expect(res.body).toBeTruthy(); + }); + test("[POST] should return 500 when try create a product", async () => { jest.spyOn(business, "createBusiness").mockRejectedValue(new Error("Erro test")) diff --git a/tests/integration/products/mocks/products-mock.ts b/tests/integration/products/mocks/products-mock.ts index 45cc405..aa975b1 100644 --- a/tests/integration/products/mocks/products-mock.ts +++ b/tests/integration/products/mocks/products-mock.ts @@ -84,3 +84,28 @@ export const productFour = { }, }; +export const productWithQuantity = { + sku: 44989, + name: "Produto Teste", + inventory: { + quantity: 10, + warehouses: [ + { + locality: "SP", + quantity: 0, + type: "ECOMMERCE", + }, + { + locality: "MOEMA", + quantity: 0, + type: "PHYSICAL_STORE", + }, + { + locality: "FRANCA", + quantity: 0, + type: "PHYSICAL_STORE", + }, + ], + }, +}; + From 6d059ce1a36ee6e0d4e7773af2259b75ac66a009 Mon Sep 17 00:00:00 2001 From: rafamagalhas Date: Fri, 11 Aug 2023 09:28:49 -0300 Subject: [PATCH 4/4] chore: include how to execute app into readme --- .docs/belezanaweb.json | 131 +++++++++++++++++++++++++++++++++ README.md | 163 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 .docs/belezanaweb.json diff --git a/.docs/belezanaweb.json b/.docs/belezanaweb.json new file mode 100644 index 0000000..cf034f3 --- /dev/null +++ b/.docs/belezanaweb.json @@ -0,0 +1,131 @@ +{ + "info": { + "_postman_id": "6f2c9214-ee6d-401a-89f5-59be18f877c4", + "name": "belezanaweb", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "3097218" + }, + "item": [ + { + "name": "products", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"sku\": 43261,\n \"name\": \"L'Oréal Professionnel Expert Absolut Repair Cortex Lipidium - Máscara de Reconstrução 500g\",\n \"inventory\": {\n \"warehouses\": [\n {\n \"locality\": \"SP\",\n \"quantity\": 10,\n \"type\": \"ECOMMERCE\"\n },\n {\n \"locality\": \"MOEMA\",\n \"quantity\": 4,\n \"type\": \"PHYSICAL_STORE\"\n },\n {\n \"locality\": \"FRANCA\",\n \"quantity\": 0,\n \"type\": \"PHYSICAL_STORE\"\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:3000/api/v1/products", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "products" + ] + } + }, + "response": [] + }, + { + "name": "products - findAll", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:3000/api/v1/products", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "products" + ] + } + }, + "response": [] + }, + { + "name": "products - find by sku", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:3000/api/v1/products/43261", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "products", + "43261" + ] + } + }, + "response": [] + }, + { + "name": "products", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Produto Teste\",\n \"inventory\": {\n \"warehouses\": [\n {\n \"locality\": \"SP\",\n \"quantity\": 0,\n \"type\": \"ECOMMERCE\"\n },\n {\n \"locality\": \"MOEMA\",\n \"quantity\": 0,\n \"type\": \"PHYSICAL_STORE\"\n },\n {\n \"locality\": \"FRANCA\",\n \"quantity\": 0,\n \"type\": \"PHYSICAL_STORE\"\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:3000/api/v1/products/43261", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "products", + "43261" + ] + } + }, + "response": [] + }, + { + "name": "products", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "localhost:3000/api/v1/products/43261", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "products", + "43261" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 0857d70..b5fca89 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,166 @@ Crie endpoints para as seguintes ações: - Os produtos podem ficar em memória, não é necessário persistir os dados - Testes são sempre bem-vindos :smiley: + +### Executando a aplicação + +### Conteúdo + +- [Dependências](#dependências) +- [Instalação](#instalação) +- [Modo de usar](#modo-de-usar) +- [Testes](#testes) +- [Suporte](#suporte) +- [Contribuição](#contribuição) +- [Observação](#observação) + +### Dependências + +Tecnologias que o projeto utiliza. + +- [NodeJS](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/get-docker/) +- [Docker Compose](https://docs.docker.com/compose/install/) +- [VSCode](https://code.visualstudio.com/download) +- [Postman](https://www.postman.com/downloads/) + +### Instalação + +Após clonar o projeto, execute os passos abaixo: + +### Execução + +Para executar a aplicação utilizando docker: + +```sh +docker-compose up -d --build +``` + +### Execução no ambiente de desenvolvimento + +#### Criar .env contendo as configurações do projeto: + +Criar o arquivo `.env` baseado no `.env.example`. Esse arquivo contém as informações para execução da aplicação, como: porta e host; + +#### Instalar dependências do projeto: +```sh +npm install +``` + +#### Executando o projeto: +```sh +npm start +``` + +#### A aplicação estará rodando em: +```sh +http://localhost:3000 +``` + +## Modo de usar + +Com a aplicação em execução, há duas maneiras de consumir seus recursos: + +### 1. Acessando os recursos da api manualmente: + +#### Inserir produtos +``` +curl --location 'localhost:3000/api/v1/products' \ +--header 'Content-Type: application/json' \ +--data '{ + "sku": 43261, + "name": "L'\''Oréal Professionnel Expert Absolut Repair Cortex Lipidium - Máscara de Reconstrução 500g", + "inventory": { + "warehouses": [ + { + "locality": "SP", + "quantity": 10, + "type": "ECOMMERCE" + }, + { + "locality": "MOEMA", + "quantity": 4, + "type": "PHYSICAL_STORE" + }, + { + "locality": "FRANCA", + "quantity": 0, + "type": "PHYSICAL_STORE" + } + ] + } +}' +``` + +#### Listar todos os produtos +``` +curl --location 'localhost:3000/api/v1/products' +``` + +#### Listar produto pelo sku (código identificador do produto) +``` +curl --location 'localhost:3000/api/v1/products/43261' +``` + +#### Atualizar produto pelo sku +``` +curl --location --request PUT 'localhost:3000/api/v1/products/43261' \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "Produto Teste", + "inventory": { + "warehouses": [ + { + "locality": "SP", + "quantity": 0, + "type": "ECOMMERCE" + }, + { + "locality": "MOEMA", + "quantity": 0, + "type": "PHYSICAL_STORE" + }, + { + "locality": "FRANCA", + "quantity": 0, + "type": "PHYSICAL_STORE" + } + ] + } +}' +``` + +#### Remover produto pelo sku +``` +curl --location --request DELETE 'localhost:3000/api/v1/products/43261' +``` + +### 2. Utilizando a coleção do postman: + +É possível importar o arquivo `belezanaweb.json` que se encontra no diretório `/.docs` via `postman`. Após importar o mesmo, as rotas listadas acima, serão importadas no seu postman. + +### Testes + +Os testes do projeto encontram-se dentro da pasta `./test`, na qual utiliza as dependências `jest`. Para executar a suíte de teste execute o comando: + + +### Executar os testes: +```sh +$ npm test +``` + +### Suporte + +Por favor [abra uma issue](https://github.com/rafamagalhas/desafio-api-menu/issues/new) para suporte. + +### Contribuição + +1. Faça um fork do projeto. +2. Crie sua feature branch (`git checkout -b my-new-feature`). +3. Commit suas alterações (`git commit -am 'Add some feature'`). +4. Faça um push de sua branch (`git push origin my-new-feature`). +5. Crie uma nova [pull request](https://github.com/rafamagalhas/test-nodejs/pulls). + +### Observação + +Esse projeto utiliza o `husky` para executar algumas ações, uma delas é o `pre-push`. Ou seja, antes que o git push seja executado, os testes serão rodados.