-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Sergey Peshkov
committed
Jan 28, 2020
1 parent
b898ecf
commit f082945
Showing
14 changed files
with
1,786 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
Oops, something went wrong.