Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chalenge Bravo - NodeJS #308

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
dist
1,883 changes: 1,883 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "challenge-bravo",
"version": "1.0.0",
"description": "[[English](README.md) | [Português](README.pt.md)]",
"main": "index.js",
"scripts": {
"dev": "nodemon --exec ts-node ./src/index.ts",
"build": "rm -rf ./dist && tsc",
"start": "node ./dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^16.4.5",
"env-var": "^7.4.1",
"express": "^4.19.2",
"express-async-errors": "^3.1.1",
"joi": "^17.12.2",
"knex": "^3.1.0",
"pg": "^8.11.3",
"redis": "^4.6.13"
},
"devDependencies": {
"@types/express": "^4.17.21",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.3"
}
}
6 changes: 3 additions & 3 deletions pull-request.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Your name: ___
Your Github homepage: ___
Original challenge URL: http://github.com/hurbcom/challenge-___
Your name: Patrick do Carmo
Your Github homepage: https://github.com/patrick-carmo
Original challenge URL: https://github.com/hurbcom/challenge-bravo
12 changes: 12 additions & 0 deletions src/config/envConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import env_var from "env-var";

const env = {
REDIS_HOST: env_var.get("REDIS_HOST").required().asString(),
REDIS_PASSWORD: env_var.get("REDIS_PASSWORD").required().asString(),
REDIS_PORT: env_var.get("REDIS_PORT").required().asInt(),
DATABASE_URL: env_var.get("DATABASE_URL").required().asUrlString(),
PORT: env_var.get("PORT").required().asInt(),
KEY: env_var.get("KEY").required().asString(),
};

export default env;
101 changes: 101 additions & 0 deletions src/controllers/currencyController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Request, Response } from "express";
import { currencyService } from "../services/currencyService";
import { Conversion, Currency } from "../types/currency";
import { BadRequestError } from "../utils/apiError";

const listCurrencies = async (_: Request, res: Response) => {
const currencies = await currencyService.getCurrencies();
return res.status(200).json(currencies);
};

const getCurrency = async (req: Request, res: Response) => {
const code = (req.params.code as string).toUpperCase();

const supportedCurrencies = await currencyService.getSupportedCurrencies();

if (!supportedCurrencies.includes(code)) {
throw new BadRequestError("The currency is not supported");
}

const currency = await currencyService.getCurrency(code);

return res.status(200).json(currency);
};

const convertCurrency = async (req: Request, res: Response) => {
const { from, to, amount } = req.query as unknown as {
from: string;
to: string;
amount: number;
};

const uppercaseFrom = from.toUpperCase();
const uppercaseTo = to.toUpperCase();

const supportedCurrencies = await currencyService.getSupportedCurrencies();

if (!supportedCurrencies.includes(uppercaseFrom)) {
throw new BadRequestError(
`The currency ${uppercaseFrom} is not supported`
);
}

if (!supportedCurrencies.includes(uppercaseTo)) {
throw new BadRequestError(
`The currency ${uppercaseTo} is not supported`
);
}

const data: Conversion = {
from: uppercaseFrom,
to: uppercaseTo,
amount,
};

const conversion: Conversion = await currencyService.getConversion(data);

return res.status(200).json(conversion);
};

const createCurrency = async (req: Request, res: Response) => {
const { code, name, value }: Currency = req.body;

const currency: Currency = {
code: code.toUpperCase(),
name,
value,
};

await currencyService.createCurrency(currency);
return res.status(201).json(currency);
};

const updateCurrency = async (req: Request, res: Response) => {
const { code, name, value }: Currency = req.body;
const codeParams = (req.params.code as string).toUpperCase();

const currency: Currency = {
code: code.toUpperCase(),
name,
value,
};

await currencyService.updateCurrency(codeParams, currency);
return res.status(204).send();
};

const deleteCurrency = async (req: Request, res: Response) => {
const code = (req.params.code as string).toUpperCase();

await currencyService.deleteCurrency(code);
return res.status(204).send();
};

export const currencyController = {
listCurrencies,
getCurrency,
convertCurrency,
createCurrency,
updateCurrency,
deleteCurrency,
};
7 changes: 7 additions & 0 deletions src/database/postgres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import knex from "knex";
import env from "../config/envConfig";

export default knex({
client: "pg",
connection: env.DATABASE_URL,
});
12 changes: 12 additions & 0 deletions src/database/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createClient } from "redis";
import env from "../config/envConfig";

const redis = createClient({
password: env.REDIS_PASSWORD,
socket: {
host: env.REDIS_HOST,
port: env.REDIS_PORT,
},
});

export default redis;
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import "dotenv/config";
import 'express-async-errors'
import env from "./config/envConfig";
import express from "express";
import redis from "./database/redis";

import currencyRoutes from "./routes/currencyRoute";
import errorMiddleware from "./middlewares/error";

redis.connect();
const app = express();

app.use(express.json());

app.use(currencyRoutes);
app.use(errorMiddleware)

app.listen(env.PORT, () => console.log(`Server running on port ${env.PORT}`));
14 changes: 14 additions & 0 deletions src/middlewares/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Request, Response, NextFunction } from "express";
import { ApiError } from "../utils/apiError";

export default function errorMiddleware(
error: Error & Partial<ApiError>,
req: Request,
res: Response,
next: NextFunction
) {
const statusCode = error.statusCode ?? 500;
const message =
statusCode === 500 ? "Internal server error" : error.message;
res.status(statusCode).json({ message });
}
31 changes: 31 additions & 0 deletions src/middlewares/requestValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Request, Response, NextFunction } from "express";
import { ObjectSchema } from "joi";
import { BadRequestError } from "../utils/apiError";

const requestValidation =
(schema: ObjectSchema) =>
async (req: Request, res: Response, next: NextFunction) => {
const body = Object.keys(req.body).length;
const params = Object.keys(req.params).length;
const query = Object.keys(req.query).length;
if (req.method !== "GET" && !body && !params && !query) {
throw new BadRequestError("Please fill out all the fields");
}
try {
if (body) {
await schema.validateAsync({ body: req.body });
}
if (params) {
await schema.validateAsync({ params: req.params });
}
if (query) {
await schema.validateAsync({ query: req.query });
}

return next();
} catch (error: any) {
throw new BadRequestError(error.message);
}
};

export default requestValidation;
13 changes: 13 additions & 0 deletions src/models/dump.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
create table currency (
id serial primary key,
name varchar(70) not null,
code varchar(6) not null unique,
value numeric(20, 4) not null
);

insert into currency (code, name, value) values
('USD', 'Dólar Americano', 0),
('BRL', 'Real Brasileiro', 0),
('EUR', 'Euro', 0),
('BTC', 'Bitcoin', 0);
('ETH', 'Ethereum', 0),
59 changes: 59 additions & 0 deletions src/models/joiCurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import joi from "joi";

const currency = joi
.object({
params: joi.object({
code: joi.string().uppercase().min(2).max(6).required().messages({
"string.base": "The code must be a string",
"string.empty": "The code cannot be empty",
"any.required": "The code is required",
"string.min": "The code must be at least 2 characters long",
"string.max": "The code must be at most 6 characters long",
}),
}),
query: joi.object({
from: joi.string().uppercase().min(2).max(6).messages({
"string.base": "The from must be a string",
"string.empty": "The from cannot be empty",
"string.min": "The from must be at least 2 characters long",
"string.max": "The from must be at most 6 characters long",
}),
to: joi.string().uppercase().min(2).max(6).messages({
"string.base": "The to must be a string",
"string.empty": "The to cannot be empty",
"string.min": "The to must be at least 2 characters long",
"string.max": "The to must be at most 6 characters long",
}),
amount: joi.number().positive().required().messages({
"number.base": "The amount must be a number",
"number.empty": "The amount cannot be empty",
"number.positive": "The amount must be positive",
"any.required": "The amount is required",
}),
}),
body: joi.object({
name: joi.string().required().messages({
"string.base": "The name must be a string",
"string.empty": "The name cannot be empty",
"any.required": "The name is required",
}),
code: joi.string().uppercase().min(2).max(6).required().messages({
"string.base": "The code must be a string",
"string.empty": "The code cannot be empty",
"string.min": "The code must be at least 2 characters long",
"string.max": "The code must be at most 6 characters long",
"any.required": "The code is required",
}),
value: joi.number().positive().required().messages({
"number.base": "The value must be a number",
"number.empty": "The value cannot be empty",
"number.positive": "The value must be positive",
"any.required": "The value is required",
}),
}),
})
.unknown();

export const schema = {
currency,
};
32 changes: 32 additions & 0 deletions src/routes/currencyRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Router } from "express";
import { currencyController } from "../controllers/currencyController";
import joi from "../middlewares/requestValidation";
import { schema } from "../models/joiCurrency";

const route = Router();

route.get("/", currencyController.listCurrencies);
route.get("/convert", joi(schema.currency), currencyController.convertCurrency);

route.get(
"/currency/:code",
joi(schema.currency),
currencyController.getCurrency
);
route.post(
"/currency",
joi(schema.currency),
currencyController.createCurrency
);
route.put(
"/currency/:code",
joi(schema.currency),
currencyController.updateCurrency
);
route.delete(
"/currency/:code",
joi(schema.currency),
currencyController.deleteCurrency
);

export default route;
Loading