Skip to content

Commit

Permalink
feat(general): more middlewares
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Peshkov committed Jan 28, 2020
1 parent b898ecf commit f082945
Show file tree
Hide file tree
Showing 14 changed files with 1,786 additions and 64 deletions.
8 changes: 8 additions & 0 deletions .sequelizerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const path = require('path');

module.exports = {
'config': path.join(__dirname, 'config', 'database.js'),
'models-path': path.join(__dirname, 'models'),
'seeders-path': path.join(__dirname, 'seeders'),
'migrations-path': path.join(__dirname, 'migrations')
};
10 changes: 10 additions & 0 deletions config/database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const config = require('./index');

// Workaround for running Sequelize migrations
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const env = process.env.NODE_ENV;

const object = config.postgres;
object.dialect = 'postgres';

module.exports[env] = object;
71 changes: 71 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const merge = require('../lib/merge');

const config = {
default: {
port: 8084,
postgres: {
host: process.env.DB_HOST || 'postgres',
port: parseInt(process.env.DB_PORT, 10) || 5432,
username: process.env.USERNAME || 'postgres',
password: process.env.PG_PASSWORD || '5ecr3t',
database: process.env.DB_DATABASE || 'core'
},
logger: {
silent: false,
level: process.env.LOGLEVEL || 'debug'
},
host: process.env.HOST || 'localhost',
media_dir: '/usr/app/media',
media_url: '/frontend/media',
salt_rounds: 12,
ttl: {
access_token: 10 * 60 // 10 minutes
},
filter_fields: [
'token',
'password'
],
bugsnagKey: process.env.BUGSNAG_KEY_CORE || 'CHANGEME'
},
development: {

},
test: {
port: 8085,
postgres: {
host: 'localhost',
database: 'core-testing'
},
logger: {
silent: (typeof process.env.ENABLE_LOGGING !== 'undefined') ? (!process.env.ENABLE_LOGGING) : true
},
media_dir: './tmp_upload',
salt_rounds: 4
}
};


// Assuming by default that we run in 'development' environment, if no
// NODE_ENV is specified.
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const env = process.env.NODE_ENV;

// The config.json file can contain a 'default' field and some environment
// fields. (e.g. 'development'). The 'default' field is loaded first, if exists,
// and then its fields are overwritten by the environment field, if exists.
// If both 'default' and environment fields are missing, than there's no config
// and we throw an error.
if (!config[env] && !config.default) {
throw new Error(`Both 'default' and '${process.env.NODE_ENV}' are not set in config/index.js; \
cannot run without config.`);
}

// If we have the default config, set it first.
let appConfig = config.default || {};

// If we have the environment config, overwrite the config's fields with its fields
if (config[env]) {
appConfig = merge(appConfig, config[env]);
}

module.exports = appConfig;
15 changes: 15 additions & 0 deletions lib/bugsnag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const bugsnag = require('@bugsnag/js');

const config = require('../config');
const logger = require('./logger');
const packageInfo = require('../package.json');

const bugsnagClient = bugsnag({
apiKey: config.bugsnagKey,
logger,
appVersion: packageInfo.version,
hostname: config.host,
releaseStage: process.env.NODE_ENV
});

module.exports = bugsnagClient;
44 changes: 44 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
exports.makeError = (res, statusCode, err) => {
// 4 cases:
// 1) 'err' is a string
// 2) 'err' is a SequelizeValidationError
// 3) 'err' is a SequelizeUniqueConstraintError
// 4) 'err' is Error

// If the error is a string, just forward it to user.
if (typeof err === 'string') {
return res.status(statusCode).json({
success: false,
message: err
});
}

// If the error is SequelizeValidationError or SequelizeUniqueConstraintError, pass the errors details to the user.
if (err.name && ['SequelizeValidationError', 'SequelizeUniqueConstraintError'].includes(err.name)) {
// Reformat errors.
return res.status(statusCode).json({
success: false,
errors: err.errors.reduce((acc, val) => {
if (val.path in acc) {
acc[val.path].push(val.message);
} else {
acc[val.path] = [val.message];
}
return acc;
}, {})
});
}

// Otherwise, just pass the error message.
return res.status(statusCode).json({
success: false,
message: err.message
});
};

exports.makeUnauthorizedError = (res, err) => exports.makeError(res, 401, err);
exports.makeValidationError = (res, err) => exports.makeError(res, 422, err);
exports.makeForbiddenError = (res, err) => exports.makeError(res, 403, err);
exports.makeNotFoundError = (res, err) => exports.makeError(res, 404, err);
exports.makeInternalError = (res, err) => exports.makeError(res, 500, err);
exports.makeBadRequestError = (res, err) => exports.makeError(res, 400, err);
28 changes: 28 additions & 0 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const winston = require('winston');

const config = require('../config');

const logger = winston.createLogger({
level: config.logger.level,
silent: config.logger.silent,
format: winston.format.json(),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.align(),
winston.format.splat(),
winston.format.printf((info) => `${info.timestamp} [${info.level}]: ${info.message}`),
)
})
]
});

logger.stream = {
write(message) {
logger.info(message.substring(0, message.lastIndexOf('\n')));
}
};

module.exports = logger;
26 changes: 26 additions & 0 deletions lib/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// A helper to deep-merge 2 objects.
// This is here and not in helpers.js to avoid circular modules
// loading breaking stuff.
const mergeDeep = (target, source) => {
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item);

const keys = [
...Object.keys(source),
...Object.getOwnPropertySymbols(source)
];

for (const key of keys) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}

return target;
};

module.exports = mergeDeep;
20 changes: 20 additions & 0 deletions lib/morgan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const morgan = require('morgan');

const log = require('./logger');

module.exports = morgan((tokens, req, res) => {
let result = [
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
tokens.res(req, res, 'content-length'), '-',
tokens['response-time'](req, res), 'ms,',
req.user ? ('user ' + req.user.user.name + ' with id ' + req.user.id) : 'unauthorized'
].join(' ');

if (['PUT', 'POST'].includes(tokens.method(req, res))) {
result += ', request body: ' + JSON.stringify(req.body, null, ' ');
}

return result;
}, { stream: log.stream });
7 changes: 7 additions & 0 deletions lib/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* istanbul ignore next */
const { startServer } = require('./server');

/* istanbul ignore next */
(async () => {
await startServer();
})();
53 changes: 53 additions & 0 deletions lib/sequelize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require('pg').defaults.parseInt8 = true; // to return count() as int, not string

const Sequelize = require('sequelize');

const logger = require('./logger');
const config = require('../config');

const requiredFields = ['database', 'username', 'password', 'host', 'port'];
for (const field of requiredFields) {
if (typeof config.postgres[field] === 'undefined') { // if var is set
logger.error('Missing config field: config.postgres.%s', field);
process.exit(1);
}
}

Sequelize.postgres.DECIMAL.parse = (value) => parseFloat(value);

const getSequelize = () => new Sequelize(config.postgres.database, config.postgres.username, config.postgres.password, {
host: config.postgres.host,
port: config.postgres.port,
dialect: 'postgres',
logging: (sql) => logger.debug(sql),
});

let sequelize = getSequelize();

exports.sequelize = sequelize;
exports.Sequelize = Sequelize;

exports.authenticate = async () => {
if (!sequelize) {
sequelize = getSequelize();
}

try {
await sequelize.authenticate();
logger.info(
'Connected to PostgreSQL at postgres://%s:%s/%s',
config.postgres.host,
config.postgres.port,
config.postgres.database
);
} catch (err) {
logger.error('Unable to connect to the database: %s', err);
process.exit(1);
}
};

exports.close = async () => {
logger.info('Closing PostgreSQL connection...');
await sequelize.close();
sequelize = null;
};
64 changes: 64 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const express = require('express');
const router = require('express-promise-router');
const bodyParser = require('body-parser');
const boolParser = require('express-query-boolean');

const morgan = require('./morgan');
const db = require('./sequelize');
const log = require('./logger');
const middlewares = require('../middlewares/generic');
const config = require('../config');
const bugsnag = require('./bugsnag');

const GeneralRouter = router({ mergeParams: true });

const server = express();
server.use(bodyParser.json());
server.use(morgan);
server.use(boolParser());

/* istanbul ignore next */
process.on('unhandledRejection', (err) => {
log.error('Unhandled rejection: ', err);

if (process.env.NODE_ENV !== 'test') {
bugsnag.notify(err);
}
});

GeneralRouter.get('/healthcheck', middlewares.healthcheck);

server.use('/', GeneralRouter);

server.use(middlewares.notFound);
server.use(middlewares.errorHandler);

let app;
async function startServer() {
return new Promise((res, rej) => {
log.info('Starting server with the following config: %o', config);
const localApp = server.listen(config.port, async () => {
app = localApp;
log.info('Up and running, listening on http://localhost:%d', config.port);
await db.authenticate();
return res();
});
/* istanbul ignore next */
localApp.on('error', (err) => rej(new Error('Error starting server: ' + err.stack)));
});
}

async function stopServer() {
log.info('Stopping server...');
app.close();
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'test') await db.close();
app = null;
}

module.exports = {
app,
server,
stopServer,
startServer
};
Loading

0 comments on commit f082945

Please sign in to comment.