Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
k33rs committed Apr 1, 2020
0 parents commit 33d2684
Show file tree
Hide file tree
Showing 24 changed files with 5,330 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.docker*
.git*
.vscode/
coverage/
node_modules/
test/
.eslintrc.js
*.yml
Dockerfile*
*.md
19 changes: 19 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
env: {
commonjs: true,
es6: true,
node: true,
jest: true
},
extends: [
'standard'
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 2018
},
rules: {}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.docker/
coverage/
node_modules/
31 changes: 31 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "app",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/app",
"protocol": "inspector"
},
{
"type": "node",
"request": "launch",
"name": "test",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
}
}
]
}
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:10

WORKDIR /usr/src/app

COPY ["package.json", "yarn.lock", "./"]

RUN yarn install --production

COPY . .

ENTRYPOINT [ "yarn", "start" ]
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Instructions

## Run

Run with docker compose:

```bash
$ docker-compose up -d
```

## API call examples

```bash
$ curl "localhost:8080/convert?amount=100&src_currency=GBP&dest_currency=USD&reference_date=2020-03-25"
$ curl "localhost:8080/convert?amount=200&src_currency=AUD&dest_currency=CAD"
```

## Run the tests

Install dependencies then run with npm:

```bash
$ npm i
$ npm test
````

or yarn:

```bash
$ yarn
$ yarn test
```
35 changes: 35 additions & 0 deletions docker-compose.debug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: "3.7"
services:
app:
build: .
depends_on:
- redis
entrypoint: yarn debug
environment:
- CRONTZ=Europe/Rome
- RATES_API=https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml
- REDIS_HOST=redis
- REDIS_TTL=120
- REF_CURRENCY=EUR
ports:
- 8080:8080
- 9229:9229
restart: always

redis:
image: redis:5
container_name: redis
command: redis-server --appendonly yes
restart: always
volumes:
- $PWD/.docker/redis-debug:/data

redis-commander:
image: rediscommander/redis-commander:latest
container_name: redis-commander
hostname: redis-commander
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- 8081:8081
restart: always
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: "3.7"
services:
app:
build: .
depends_on:
- redis
environment:
- "CRON=5 16 * * 1-5"
- CRONTZ=Europe/Rome
- RATES_API=https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml
- REDIS_HOST=redis
- REDIS_TTL=90000
- REF_CURRENCY=EUR
ports:
- 8080:8080
restart: always

redis:
image: redis:5
container_name: redis
command: redis-server --appendonly yes
restart: always
volumes:
- $PWD/.docker/redis:/data
24 changes: 24 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const cron = require('node-cron')
const Redis = require('ioredis')

const getRedisClient = require('./src/lib/redis')
const apiFactory = require('./src/services/api')
const parserFactory = require('./src/services/parser')

const redis = getRedisClient(Redis)
const api = apiFactory(redis)
const parse = parserFactory(redis)

const HOST = '0.0.0.0'
const PORT = 8080

parse()
.then(() => cron.schedule(
process.env.CRON || '* * * * *',
parse,
{ timezone: process.env.CRONTZ || 'Europe/London' }
))

api.listen(PORT, HOST)

console.log(`${new Date().toISOString()} INFO listening on http://${HOST}:${PORT}`)
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "currency-converter",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"debug": "node --inspect-brk=0.0.0.0 index.js",
"test": "jest --coverage --verbose=true test"
},
"repository": {
"type": "git",
"url": "https://github.com/k33rs/currency-converter.git"
},
"author": "Michele Chersich",
"license": "ISC",
"bugs": {
"url": "https://github.com/k33rs/currency-converter/issues"
},
"homepage": "https://github.com/k33rs/currency-converter#readme",
"dependencies": {
"@hapi/joi": "^17.1.1",
"@hapi/joi-date": "^2.0.1",
"axios": "^0.19.2",
"bignumber.js": "^9.0.0",
"celebrate": "^12.0.1",
"express": "^4.17.1",
"ioredis": "^4.16.0",
"node-cron": "^2.0.3",
"xml2json": "^0.12.0"
},
"devDependencies": {
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"jest": "^25.1.0"
}
}
44 changes: 44 additions & 0 deletions src/controllers/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable camelcase */

const convertFactory = (convertLib) => async (req, res) => {
try {
const {
query: {
amount: src_amount,
src_currency,
dest_currency: currency,
reference_date
}
} = req

const amount = await convertLib(
src_amount,
src_currency,
currency,
reference_date
)

res.status(200).json({ amount, currency })
return Promise.resolve()
} catch (error) {
console.log(`${new Date().toISOString()} ERROR ${error}`)

switch (error.constructor.name) {
case 'NotFoundError':
res.status(404).json({
statusCode: 404,
error: error.message
})
break
default:
res.status(500).json({
statusCode: 500,
error: 'Internal Server Error'
})
}

return Promise.reject(error)
}
}

module.exports = convertFactory
14 changes: 14 additions & 0 deletions src/controllers/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { celebrate } = require('celebrate')
const Joi = require('@hapi/joi')
.extend(require('@hapi/joi-date'))

const validate = celebrate({
query: Joi.object({
amount: Joi.number().required(),
src_currency: Joi.string().pattern(/^[A-Z]{3}$/).required(),
dest_currency: Joi.string().pattern(/^[A-Z]{3}$/).required(),
reference_date: Joi.date().format('YYYY-MM-DD').raw()
})
})

module.exports = validate
20 changes: 20 additions & 0 deletions src/lib/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const BigNumber = require('bignumber.js')

const convertLibFactory = (redis) => async (
amount,
srcCurrency,
dstCurrency,
date = new Date().toISOString().replace(/T.*Z/, '')
) => {
const srcRate = await redis.getRate(date, srcCurrency)
const dstRate = await redis.getRate(date, dstCurrency)

const amountBN = BigNumber(amount)
.div(srcRate)
.times(dstRate)
.dp(2, BigNumber.ROUND_HALF_DOWN)

return amountBN.toNumber()
}

module.exports = convertLibFactory
3 changes: 3 additions & 0 deletions src/lib/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class NotFoundError extends Error {}

module.exports = { NotFoundError }
35 changes: 35 additions & 0 deletions src/lib/rates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const parser = require('xml2json')

const getRatesFactory = (client) => async () => {
const response = await client.get(
process.env.RATES_API || 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'
)

const { data } = response
const dataStr = parser.toJson(data)
const dataJson = JSON.parse(dataStr)

const {
'gesmes:Envelope': {
Cube: {
Cube: rates
}
}
} = dataJson

return rates
}

const compactRates = (rates) => {
return rates.map(rate => {
const kvpairs = rate.Cube.map(elem => ({ [elem.currency]: elem.rate }))

rate.Cube = { [process.env.REF_CURRENCY || 'EUR']: '1' }
const [rate1, ...rate2toN] = kvpairs
Object.assign(rate.Cube, rate1, ...rate2toN)

return rate
})
}

module.exports = { getRatesFactory, compactRates }
Loading

0 comments on commit 33d2684

Please sign in to comment.