Commit 00a87dec authored by João Victor Risso's avatar João Victor Risso

Change API to use ECMAScript6

Major modifications:
- Source code is now in the src directory
- All source files were adapted to ECMAScript6
- Add Gulpfile and Babel to transcompile the project from ES6 to ES5 (until Node.js fully supports ES6)
	- By running gulp one generates the build directory with the files transcompiled, from which the API can be run
- Add ESLint to check for syntax errors and enforce code standards
	- Javascript coding standard currently adopted is AirBnB with 4 spaces
- Replace monetdb package with monetdb-pool, which allows for concurrent queries over a pool of connections while maintaining the same interface

TODO (in order of priority, from high to low):
- Add Gulp tasks to handle automatic building, tests and running the server in order to deprecate using npm
- Implement decorator to execute the SQL queries and reduce code duplication.
- Implement SQL query builder (e.g. squel.js) to erradicate the need for embedding SQL directly into the code (which is error-prone).
- Change enrollments route not to use route chaining in order to decide which SQL query is appropriate to respond the user's request.
- Implement decorator for API responses and also reduce code duplication.
- Split up tests into and add more test cases.
Signed-off-by: João Victor Risso's avatarJoão Victor Risso <jvtr12@c3sl.ufpr.br>
parent 8d77d014
Pipeline #6487 failed with stage
in 59 seconds
{ 'presets': ['es2015'] }
{
"extends": "airbnb",
"plugins": [
"react",
"jsx-a11y",
"import"
],
"rules": {
"indent": [ "error", 4 ],
"no-unused-vars": [ "error", { "args": "none" }],
"no-param-reassign": [ "off" ]
}
}
......@@ -10,7 +10,6 @@ lib-cov
*.gz
pids
logs
results
npm-debug.log
......
......@@ -7,6 +7,6 @@ before_script:
run_tests:
stage: test
script:
- npm test
- gulp && cd build/ && mocha
tags:
- node
{
"port": 3000,
"monetdb": {
"host": "localhost",
"port": 50000,
"dbname": "simcaq_dev",
"user": "monetdb",
"password":"monetdb",
"nrConnections": 16
},
"default": {
"api": {
"version" : "v1"
}
}
}
'use strict';
var express = require('express');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var cors = require('cors');
var log = require('./log')(module);
var api = require('./routes/api');
var states = require('./routes/states');
var regions = require('./routes/regions');
var cities = require('./routes/cities');
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(cors());
app.use(methodOverride());
app.use('/v1/', api);
app.use('/v1/states', states);
app.use('/v1/regions', regions);
app.use('/v1/cities', cities);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
res.status(404);
log.debug('%s %d %s', req.method, res.statusCode, req.url);
res.json({ error: 'Not found' }).end();
});
// error handlers
app.use(function (err, req, res, next) {
res.status(err.status || 500);
log.error('%s %d %s', req.method, res.statusCode, err.message);
res.json({ error: err.message }).end();
});
module.exports = app;
\ No newline at end of file
'use strict';
var nconf = require('nconf');
nconf.argv().env().file({ file: process.cwd() + '/config.json' });
module.exports = nconf;
\ No newline at end of file
var mdb = require('monetdb')()
'use strict';
var libs = process.cwd() + '/libs/'
var MonetDBPool = require('monetdb-pool');
var log = require(libs + 'log')(module)
var config = require(libs + 'config')
var libs = process.cwd() + '/libs';
var config = require(libs + '/config');
var poolOptions = {
nrConnections: config.get('monetdb:nrConnections')
};
var options = {
host: config.get('monetdb:host'),
......@@ -11,9 +16,9 @@ var options = {
dbname: config.get('monetdb:dbname'),
user: config.get('monetdb:user'),
password: config.get('monetdb:password')
}
};
var conn = new mdb(options)
conn.connect()
var conn = new MonetDBPool(poolOptions, options);
conn.connect();
module.exports = conn
module.exports = conn;
\ No newline at end of file
"use strict";
var libs = process.cwd() + "/libs";
var log = require(libs + "/log")(module);
var conn = require(libs + "/db/monet");
/**
* Basic decorator to wrap SQL query strings
*/
exports.execQuery = function (sqlQuery, sqlQueryParams) {
log.debug("Executing SQL query '" + sqlQuery + "' with params '" + sqlQueryParams + "'");
conn.prepare(sqlQuery, true).then(function (dbQuery) {
dbQuery.exec(sqlQueryParams).then(function (dbResult) {
log.debug(dbResult.data);
log.debug("Query result: " + dbResult.data);
return dbResult;
}, function (error) {
log.error("SQL query execution error: " + error.message);
return error;
});
});
};
\ No newline at end of file
"use strict";
\ No newline at end of file
"use strict";
\ No newline at end of file
'use strict';
var libs = process.cwd() + '/libs';
var log = require(libs + '/log')(module);
var sqlDecorator = require(libs + '/query_decorator');
var yearRange = function yearRange() {
var yearSql = 'SELECT MIN(t.ano_censo) AS start_year, MAX(t.ano_censo)' + 'AS end_year FROM turmas AS t';
log.debug('Generated SQL query for enrollments\' year range');
return sqlDecorator.execQuery(yearSql, []);
};
module.exports = yearRange;
\ No newline at end of file
"use strict";
\ No newline at end of file
"use strict";
\ No newline at end of file
"use strict";
\ No newline at end of file
'use strict';
var winston = require('winston');
winston.emitErrs = true;
function getFilePath(module) {
// using filename in log statements
return module.filename.split('/').slice(-2).join('/');
}
function logger(module) {
return new winston.Logger({
transports: [new winston.transports.File({
level: 'info',
filename: process.cwd() + '/logs/all.log',
handleException: true,
json: false,
maxSize: 5242880, // 5MB
maxFiles: 2,
colorize: false
}), new winston.transports.Console({
level: 'debug',
label: getFilePath(module),
handleException: true,
json: true,
colorize: true
})],
exitOnError: false
});
}
module.exports = logger;
\ No newline at end of file
"use strict";
var libs = process.cwd() + "/libs";
var log = require(libs + "/log")(module);
var conn = require(libs + "/db/monet");
/**
* Basic decorator to wrap SQL query strings
*/
exports.execQuery = function (sqlQuery, sqlQueryParams) {
log.debug("Executing SQL query '" + sqlQuery + "' with params '" + sqlQueryParams + "'");
conn.prepare(sqlQuery, true).then(function (dbQuery) {
dbQuery.exec(sqlQueryParams).then(function (dbResult) {
log.debug(dbResult.data);
log.debug("Query result: " + dbResult.data);
return dbResult;
}, function (error) {
log.error("SQL query execution error: " + error.message);
return error;
});
});
};
\ No newline at end of file
This diff is collapsed.
'use strict';
var express = require('express');
var xml = require('js2xmlparser');
var cityApp = express();
var libs = process.cwd() + '/libs';
var log = require(libs + '/log')(module);
var conn = require(libs + '/db/monet');
function response(req, res) {
if (req.query.format === 'csv') {
res.csv(req.result.data);
} else if (req.query.format === 'xml') {
res.send(xml('result', JSON.stringify({ city: req.result.data })));
} else {
res.json({ result: req.result.data });
}
}
cityApp.get('/', function (req, res) {
conn.query('SELECT * FROM municipios', true).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
cityApp.get('/:id', function (req, res) {
var citySql = 'SELECT * FROM municipios WHERE pk_municipio_id = ?';
var cityId = parseInt(req.params.id, 10);
conn.prepare(citySql, true).then(function (dbQuery) {
dbQuery.exec([cityId]).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
});
cityApp.get('/ibge/:id', function (req, res) {
var citySql = 'SELECT * FROM municipios WHERE codigo_ibge = ?';
var cityId = parseInt(req.params.id, 10);
conn.prepare(citySql, true).then(function (dbQuery) {
dbQuery.exec([cityId]).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
});
cityApp.get('/state/:id', function (req, res) {
var citySql = 'SELECT * FROM municipios WHERE fk_estado_id = ?';
var stateId = parseInt(req.params.id, 10);
conn.prepare(citySql, true).then(function (dbQuery) {
dbQuery.exec([stateId]).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
});
module.exports = cityApp;
\ No newline at end of file
'use strict';
var express = require('express');
var xml = require('js2xmlparser');
var regionApp = express();
var libs = process.cwd() + '/libs';
var log = require(libs + '/log')(module);
var conn = require(libs + '/db/monet');
function response(req, res) {
if (req.query.format === 'csv') {
res.csv(req.result.data);
} else if (req.query.format === 'xml') {
res.send(xml('result', JSON.stringify({ state: req.result.data })));
} else {
res.json({ result: req.result.data });
}
}
regionApp.get('/', function (req, res) {
var regionSql = 'SELECT * FROM regioes';
conn.query(regionSql, true).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
regionApp.get('/:id', function (req, res) {
var regionSql = 'SELECT * FROM regioes WHERE pk_regiao_id = ?';
var regionId = parseInt(req.params.id, 10);
conn.prepare(regionSql, true).then(function (dbQuery) {
dbQuery.exec([regionId]).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
});
module.exports = regionApp;
\ No newline at end of file
'use strict';
var express = require('express');
var xml = require('js2xmlparser');
var stateApp = express();
var libs = process.cwd() + '/libs';
var log = require(libs + '/log')(module);
var conn = require(libs + '/db/monet');
function response(req, res) {
if (req.query.format === 'csv') {
res.csv(req.result.data);
} else if (req.query.format === 'xml') {
res.send(xml('result', JSON.stringify({ state: req.result.data })));
} else {
res.json({ result: req.result.data });
}
}
stateApp.get('/', function (req, res, next) {
var stateSql = 'SELECT * FROM estados';
conn.query(stateSql, true).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
stateApp.get('/:id', function (req, res, next) {
var stateSql = 'SELECT * FROM estados WHERE pk_estado_id = ?';
var stateId = parseInt(req.params.id, 10);
conn.prepare(stateSql, true).then(function (dbQuery) {
dbQuery.exec([stateId]).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
});
stateApp.get('/region/:id', function (req, res, next) {
var stateSql = 'SELECT * FROM estados WHERE fk_regiao_id = ?';
var regionId = parseInt(req.params.id, 10);
conn.prepare(stateSql, true).then(function (dbQuery) {
dbQuery.exec([regionId]).then(function (dbResult) {
log.debug(dbResult);
req.result = dbResult;
response(req, res);
}, function (dbError) {
log.error('SQL query execution error: ' + dbError.message);
// FIXME: change response to HTTP 501 status
res.json({ error: 'An internal error has occurred' }).end();
});
});
});
module.exports = stateApp;
\ No newline at end of file
'use strict';
var debug = require('debug')('node-express-base');
var libs = process.cwd() + '/libs';
var config = require(libs + '/config');
var log = require(libs + '/log')(module);
var app = require(libs + '/app');
app.set('port', config.get('port') || 3000);
var server = app.listen(app.get('port'), function () {
debug('Express server listening on port ' + server.address().port);
log.info('Express server listening on port ' + config.get('port'));
});
\ No newline at end of file
'use strict';
var chai = require('chai');
var chaiHttp = require('chai-http');
var assert = chai.assert;
var expect = chai.expect;
var should = chai.should(); // actually call the function
var server = require('../libs/app');
chai.use(chaiHttp);
describe('request enrollments', function () {
it('should list enrollments', function (done) {
chai.request(server).get('/v1/enrollments').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('name');
res.body.result[0].should.have.property('total');
done();
});
});
});
describe('request regions', function () {
it('should list all regions', function (done) {
chai.request(server).get('/v1/regions').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('pk_regiao_id');
res.body.result[0].should.have.property('nome');
done();
});
});
it('should list region by id', function (done) {
chai.request(server).get('/v1/regions/1').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result.should.have.length(1);
res.body.result[0].should.have.property('pk_regiao_id');
res.body.result[0].should.have.property('nome');
done();
});
});
});
describe('request states', function () {
it('should list all states', function (done) {
chai.request(server).get('/v1/states').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('pk_estado_id');
res.body.result[0].should.have.property('fk_regiao_id');
res.body.result[0].should.have.property('nome');
done();
});
});
it('should list a state by id', function (done) {
chai.request(server).get('/v1/states/11').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result.should.have.length(1);
res.body.result[0].should.have.property('pk_estado_id');
res.body.result[0].should.have.property('fk_regiao_id');
res.body.result[0].should.have.property('nome');
done();
});
});
it('should list states by region id', function (done) {
chai.request(server).get('/v1/states/region/1').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('pk_estado_id');
res.body.result[0].should.have.property('fk_regiao_id');
res.body.result[0].should.have.property('nome');
done();
});
});
});
describe('request cities', function () {
it('should list all cities', function (done) {
chai.request(server).get('/v1/cities').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('pk_municipio_id');
res.body.result[0].should.have.property('fk_estado_id');
res.body.result[0].should.have.property('nome');
res.body.result[0].should.have.property('codigo_ibge');
done();
});
});
it('should list a city by id', function (done) {
chai.request(server).get('/v1/cities/1').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('pk_municipio_id');
res.body.result[0].should.have.property('fk_estado_id');
res.body.result[0].should.have.property('nome');
res.body.result[0].should.have.property('codigo_ibge');
done();
});
});
it('should list a city by codigo_ibge', function (done) {
chai.request(server).get('/v1/cities/ibge/1200013').end(function (err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('result');
res.body.result.should.be.a('array');
res.body.result[0].should.have.property('pk_municipio_id');
res.body.result[0].should.have.property('fk_estado_id');
res.body.result[0].should.have.property('nome');
res.body.result[0].should.have.property('codigo_ibge');
done();
});
});
});
\ No newline at end of file
{
"port": 3000,
"monetdb": {
"host": "simcaqdb1",
"host": "localhost",
"port": 50000,
"dbname": "simcaq_dev",
"user": "monetdb",
......
const gulp = require('gulp');
const babel = require('gulp-babel');
const eslint = require('gulp-eslint');
gulp.task('default', () => {
// run ESLint
gulp.src('src/**/*.js')
.pipe(eslint())
.pipe(eslint.format());
// compile source to ES5
gulp.src('src/**/*.js')
.pipe(babel())
.pipe(gulp.dest('build'));
});
var express = require('express')
var path = require('path')
var cookieParser = require('cookie-parser')
var bodyParser = require('body-parser')
var csv = require('csv-express')
var xml = require('js2xmlparser')
var methodOverride = require('method-override')
var cors = require('cors')
var libs = process.cwd() + '/libs/'
var config = require('./config')
var log = require('./log')(module)
var api = require('./routes/api')
var states = require('./routes/states')
var regions = require('./routes/regions')
var cities = require('./routes/cities')
var app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(cors())
app.use(methodOverride())
app.use('/v1/', api)
app.use('/v1/states', states)
app.use('/v1/regions', regions)
<