Skip to content
This repository has been archived by the owner on Nov 22, 2023. It is now read-only.

Giuliano Bortoloni - Teste Boticario #94

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"rules": {
"max-len": ["warn", 120],
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off"
},
"env": {
"node": true,
"es2022": true
},
"ignorePatterns": ["/node_modules/", "/dist/", "/.vscode/", "/coverage/"]
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ typings/

# DynamoDB Local files
.dynamodb/

dist
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
package-lock.json
coverage/
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
91 changes: 27 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,37 @@
### Backend Test
[![Build Status](https://travis-ci.org/belezanaweb/test-nodejs.svg?branch=master)](https://travis-ci.org/belezanaweb/test-nodejs)
# Product API

Esta é uma avaliação básica de código.
API responsible for managing all products.

O objetivo é conhecer um pouco do seu conhecimento/prática de RESTful e NodeJS.
| Table of Contents |
| :-------------------------------: |
| [Requirements](#requirements) |
| [Run Project](#run-project) |
| [Commands](#commands) |

Recomendamos que você não gaste mais do que 4 - 6 horas.
## Requirements / Prerequisites

Faça um fork deste repositório.
- [Node 18.16.0](https://nodejs.org);
- [Yarn 1.22.19](https://classic.yarnpkg.com/en/);
- [Docker 23.0.4](https://www.docker.com/)

Ao finalizar o teste, submeta um pull request para o repositório que nosso time será notificado.
## Run Project

### Tarefas

Com a seguinte representação de produto:

```json
{
"sku": 43264,
"name": "L'Oréal Professionnel Expert Absolut Repair Cortex Lipidium - Máscara de Reconstrução 500g",
"inventory": {
"quantity": 15,
"warehouses": [
{
"locality": "SP",
"quantity": 12,
"type": "ECOMMERCE"
},
{
"locality": "MOEMA",
"quantity": 3,
"type": "PHYSICAL_STORE"
}
]
},
"isMarketable": true
}
```
# create env file
cp sample.env .env (change the values with yout need)
# execute yarn local:up to run docker compose
```

Crie endpoints para as seguintes ações:

- [ ] Criação de produto onde o payload será o json informado acima (exceto as propriedades **isMarketable** e **inventory.quantity**)

- [ ] Edição de produto por **sku**

- [ ] Recuperação de produto por **sku**

- [ ] Deleção de produto por **sku**

### Requisitos


- [ ] Toda vez que um produto for recuperado por **sku** deverá ser calculado a propriedade: **inventory.quantity**

A propriedade inventory.quantity é a soma da quantity dos warehouses

- [ ] Toda vez que um produto for recuperado por **sku** deverá ser calculado a propriedade: **isMarketable**

Um produto é marketable sempre que seu inventory.quantity for maior que 0

- [ ] Caso um produto já existente em memória tente ser criado com o mesmo **sku** uma exceção deverá ser lançada

Dois produtos são considerados iguais se os seus skus forem iguais


- [ ] Ao atualizar um produto, o antigo deve ser sobrescrito com o que esta sendo enviado na requisição

A requisição deve receber o sku e atualizar com o produto que tbm esta vindo na requisição
## Commands

### Dicas
- `yarn lint` to see possible lint errors;
- `yarn format` to fix possible lint errors;
- `yarn build` to application build;
- `yarn dev` to run local application;
- `yarn local:up` to up docker compose application
- `yarn local:down` to down docker compose application
- `yarn test` to run all test (unit and integration)
- `yarn test:unit` to run all unit test
- `yarn test:integration` to run all integration test
- `yarn migration` to run prisma migration (its runs in docker compose)

- Os produtos podem ficar em memória, não é necessário persistir os dados
- Testes são sempre bem-vindos :smiley:
25 changes: 25 additions & 0 deletions development/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: '3.7'
services:
database:
image: postgres
ports:
- 5432:5432
environment:
- POSTGRES_USER=test
- POSTGRES_PASSWORD=testBoticario
boticario-test_api:
image: node:18.16.0-alpine
volumes:
- ../:/boticario-test_api
working_dir: /boticario-test_api
ports:
- 3000:3000
- 9229:9229
env_file: ../.env
links:
- database
depends_on:
- database
environment:
- DATABASE_URL=postgresql://test:testBoticario@database:5432/postgres
command: ash -c "yarn install --frozen-lockfile && sleep 10 && yarn migrations && yarn dev"
16 changes: 16 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
moduleFileExtensions: ['ts', 'tsx', 'js'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testMatch: ['**/tests/**/*.test.(ts|js|tsx)'],
testEnvironment: 'node',
collectCoverageFrom: ['**/*.{ts,tsx}'],
coveragePathIgnorePatterns: [
'./node_modules/',
'./tests/',
'./dist/',
'src/index.ts',
'./development'
],
}
6 changes: 6 additions & 0 deletions nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": ".ts",
"ignore": [],
"exec": "ts-node-dev -r tsconfig-paths/register --inspect=0.0.0.0:9229"
}
51 changes: 49 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"dev": "nodemon src/index.ts",
"start": "node dist/index.js",
"build": "tsc --project tsconfig.json",
"local:up": "docker compose -f development/docker-compose.yml up",
"local:down": "docker compose -f development/docker-compose.yml down",
"test": "jest --bail --forceExit",
"migrations": "npx prisma migrate dev --name boticario-test",
"test:integration": "jest --bail --forceExit --passWithNoTests \"tests/integration/.*\\.test\\.ts\"",
"test:unit": "jest --bail --forceExit \"tests/unit/.*\\.test\\.ts\"",
"lint:tsc": "tsc --project tsconfig.json --noEmit",
"lint": "yarn lint:tsc && eslint . --ext .ts,.js && prettier --check \"src/**/*.ts\"",
"format": "eslint --fix . && prettier --write ."
},
"repository": {
"type": "git",
Expand All @@ -16,5 +27,41 @@
"bugs": {
"url": "https://github.com/belezanaweb/test-nodejs/issues"
},
"homepage": "https://github.com/belezanaweb/test-nodejs#readme"
"homepage": "https://github.com/belezanaweb/test-nodejs#readme",
"dependencies": {
"@prisma/client": "^4.13.0",
"@prisma/generator-helper": "^4.13.0",
"body-parser": "^1.20.2",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"http-status": "^1.6.2",
"joi": "^17.9.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.1",
"@types/node": "^18.16.2",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.59.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0",
"jest": "^29.5.0",
"jest-mock-extended": "^3.0.4",
"nodemon": "^2.0.22",
"prettier": "^2.8.8",
"prisma": "^4.13.0",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-node-dev": "^2.0.0",
"typescript": "*"
}
}
39 changes: 39 additions & 0 deletions prisma/migrations/20230501025306_boticario_test/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- CreateTable
CREATE TABLE "Warehouse" (
"warehouseId" TEXT NOT NULL,
"locality" TEXT NOT NULL,
"quantity" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"inventoryId" TEXT NOT NULL,

CONSTRAINT "Warehouse_pkey" PRIMARY KEY ("warehouseId")
);

-- CreateTable
CREATE TABLE "Inventory" (
"inventoryId" TEXT NOT NULL,
"productId" TEXT NOT NULL,

CONSTRAINT "Inventory_pkey" PRIMARY KEY ("inventoryId")
);

-- CreateTable
CREATE TABLE "Product" (
"productId" TEXT NOT NULL,
"sku" INTEGER NOT NULL,
"name" TEXT NOT NULL,

CONSTRAINT "Product_pkey" PRIMARY KEY ("productId")
);

-- CreateIndex
CREATE UNIQUE INDEX "Inventory_productId_key" ON "Inventory"("productId");

-- CreateIndex
CREATE UNIQUE INDEX "Product_sku_key" ON "Product"("sku");

-- AddForeignKey
ALTER TABLE "Warehouse" ADD CONSTRAINT "Warehouse_inventoryId_fkey" FOREIGN KEY ("inventoryId") REFERENCES "Inventory"("inventoryId") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Inventory" ADD CONSTRAINT "Inventory_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("productId") ON DELETE CASCADE ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
31 changes: 31 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}

model Warehouse {
warehouseId String @id @default(uuid())
locality String
quantity Int
type String
inventoryId String
inventory Inventory @relation(fields: [inventoryId], references: [inventoryId], onDelete: Cascade)
}

model Inventory {
inventoryId String @id @default(uuid())
productId String @unique
product Product @relation(fields: [productId], references: [productId], onDelete: Cascade)
warehouse Warehouse[]
}

model Product {
productId String @id @default(uuid())
sku Int @unique
name String
inventory Inventory?
}
5 changes: 5 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
APPLICATION_NAME=beleza-na-web-test
APP_ENVIROMENT=development
APP_PORT=3000

DATABASE_URL=postgresql://test:testBoticario@localhost:5432/postgres
62 changes: 62 additions & 0 deletions src/application/App.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import express, { RequestHandler } from "express";
import {NOT_FOUND} from 'http-status';
import * as http from 'http';

import { Router } from "./Controller";

interface AppOptions {
port: number;
name: string;
middlewares?: RequestHandler[];
routers: Router[];
environment: string;
}

export default class App {
app: express.Express;
port: number;
name: string;
middlewares: RequestHandler[];
routers: Router[];
environment: string;

constructor(options: AppOptions) {
this.app = express();
this.port = options.port;
this.name = options.name;
this.middlewares = options.middlewares || [];
this.routers = options.routers;
this.environment = options.environment || '';

this.handleMiddlewares();
this.handleRoutes();
this.handleNotFound();
}

private handleMiddlewares(): void {
this.middlewares.forEach(middleware => this.app.use(middleware));
}

private handleRoutes(): void {
this.routers.forEach((router) => {
const expressRouter = express.Router();
router.controllers.forEach((controller) => {
expressRouter[controller.method](controller.path, controller.handler);
});
this.app.use(router.prefix, expressRouter);
})
}

private handleNotFound(): void {
this.app.use((_request, response) => {
response.status(NOT_FOUND).json({error: 'not found'});
})
}

async listen(): Promise<http.Server> {
return this.app.listen(this.port, () => {
console.log(`Aplication ${this.name} is running. Listen on http://localhost:${this.port}`);
console.log('Press CTRL+C to exit');
})
}
}
Loading