ravel
Version:
Ravel Rapid Application Development Framework
495 lines (431 loc) • 20.8 kB
JavaScript
'use strict';
const EventEmitter = require('events').EventEmitter;
const coreSymbols = require('./core/symbols');
const Rest = require('./util/rest');
const sInitialized = Symbol('isInitialized');
const sListening = Symbol('isListening');
const sLog = Symbol('log');
const sApplicationError = Symbol('ApplicationError');
const sCWD = Symbol('cwd');
const sServer = Symbol('server');
const sUncaughtRejections = Symbol('uncaughtRejections');
/**
* This class provides Ravel, a lightweight but powerful framework
* for the rapid creation of enterprise Node.js applications which
* scale horizontally with ease and support the latest in web technology.
*
* @example
* const Ravel = require('ravel');
* const app = new Ravel();
*/let
Ravel = class Ravel extends EventEmitter {
/**
* Creates a new Ravel app instance.
*
* @example
* const Ravel = require('ravel');
* const app = new Ravel();
*/
constructor() {
super();
this[sUncaughtRejections] = 0;
this[sInitialized] = false;
this[sListening] = false;
// how we store modules for dependency injection
this[coreSymbols.modules] = Object.create(null);
// current working directory of the app using the
// ravel library, so that client modules can be
// loaded with relative paths.
this[sCWD] = process.cwd();
this[coreSymbols.knownParameters] = Object.create(null);
this[coreSymbols.params] = Object.create(null);
// a list of registered module factories which haven't yet been instantiated
this[coreSymbols.moduleFactories] = Object.create(null);
this[coreSymbols.resourceFactories] = Object.create(null);
this[coreSymbols.routesFactories] = Object.create(null);
// a list of known modules, resources and routes so that metadata can be retrieved from them
this[coreSymbols.knownClasses] = Object.create(null);
// init errors
this[sApplicationError] = require('./util/application_error');
// init logging system
this[sLog] = new (require('./util/log'))(this);
// init dependency injection utility, which is used by most everything else
this[coreSymbols.injector] = new (require('./core/injector'))(this, module.parent);
// Register known ravel parameters
// redis parameters
this.registerParameter('redis host', true, '0.0.0.0');
this.registerParameter('redis port', true, 6379);
this.registerParameter('redis password');
this.registerParameter('redis max retries', true, 10);
this.registerParameter('redis keepalive interval', true, 1000);
// Node/koa parameters
this.registerParameter('port', true, 8080);
this.registerParameter('koa public directory');
this.registerParameter('koa view directory');
this.registerParameter('koa view engine');
this.registerParameter('koa favicon path');
this.registerParameter('keygrip keys', true);
// session parameters
this.registerParameter('session key', true, 'koa.sid');
this.registerParameter('session max age', true, null);
// Passport parameters
this.registerParameter('app route', false, '/');
this.registerParameter('login route', false, '/login');
}
/**
* The current working directory of the app using the ravel library, so that client modules can be
* loaded with relative paths.
*
* @type String
*/
get cwd() {
return this[sCWD];
}
/**
* Return the app instance of Logger.
* See [`Logger`](#logger) for more information.
*
* @type Logger
* @example
* app.log.error('Something terrible happened!');
*/
get log() {
return this[sLog];
}
/**
* A hash of all built-in error types.
*
* @type Object
*/
get ApplicationError() {
return this[sApplicationError];
}
/**
* Value is `true` iff init() or start() has been called on this app instance.
*
* @type boolean
*/
get initialized() {
return this[sInitialized];
}
/**
* Value is `true` iff listen() or start() has been called on this app instance.
*
* @type boolean
*/
get listening() {
return this[sListening];
}
/**
* The underlying HTTP server for this Ravel instance.
* Only available after listen() (i.e. use @postlisten).
*
* @type {http.Server}
*/
get server() {
return this[sServer];
}
/**
* Initializes the application, when the client is finished
* supplying parameters and registering modules, resources
* routes and rooms.
*/
init() {
// application configuration is completed in constructor
this.emit('pre init');
// log uncaught errors to prevent ES6 promise error swallowing
// https://www.hacksrus.net/blog/2015/08/a-solution-to-swallowed-exceptions-in-es6s-promises/
process.on('unhandledRejection', err => {
this[sUncaughtRejections] += 1;
const message = `Detected uncaught error in promise: \n ${err ? err.stack : err}`;
if (this[sUncaughtRejections] >= 10) {
this.log.error(message);
this.log.error(`Encountered ${this[sUncaughtRejections]} or more uncaught rejections. Re-run ` +
'with full logging output for more information: app.set(\'log level\', app.log.TRACE);');
} else {
this.log.debug(message);
}
});
// load parameters from .ravelrc.json file, if any
this.emit('pre load parameters');
this[coreSymbols.loadParameters]();
this.emit('post load parameters');
this[sInitialized] = true;
this.db = require('./db/database')(this);
this.kvstore = require('./util/kvstore')(this);
// App dependencies.
const http = require('http'); // https will be provided by reverse proxy
const upath = require('upath');
const Koa = require('koa');
const session = require('koa-session');
const compression = require('koa-compress');
const favicon = require('koa-favicon');
const router = new (require('koa-router'))();
// configure koa
const app = new Koa();
app.proxy = true;
// the first piece of middleware is the exception handler
// catch all errors and return appropriate error codes
// to the client
app.use(new Rest(this).errorHandler());
// enable gzip compression
app.use(compression());
// configure redis session store
app.keys = this.get('keygrip keys');
app.use(session({
store: new (require('./util/redis_session_store'))(this),
key: this.get('session key'),
maxAge: Number(this.get('session max age')),
overwrite: true, /** (boolean) can overwrite or not (default true) */
httpOnly: true, /** (boolean) httpOnly or not (default true) */
signed: true, /** (boolean) signed or not (default true) */
rolling: false /** (boolean) Force a session identifier cookie to be set on every response.
The expiration is reset to the original maxAge, resetting the expiration
countdown. default is false **/ },
app));
// configure view engine
if (this.get('koa view engine') && this.get('koa view directory')) {
const views = require('koa-views');
app.use(views(upath.join(this.cwd, this.get('koa view directory')), {
map: {
html: this.get('koa view engine') } }));
}
// favicon
if (this.get('koa favicon path')) {
app.use(favicon(upath.join(this.cwd, this.get('koa favicon path'))));
}
// static file serving
if (this.get('koa public directory')) {
const koaStatic = require('koa-static');
const root = upath.join(this.cwd, this.get('koa public directory'));
app.use(koaStatic(root, {
gzip: false // this should be handled by koa-compressor?
}));
}
// initialize authentication/authentication
require('./auth/passport_init.js')(this, router);
// basic koa configuration is completed
this.emit('post config koa', app);
// create registered modules using factories
this[coreSymbols.moduleInit]();
this.emit('post module init');
this.emit('pre routes init', app);
// create registered resources using factories
this[coreSymbols.resourceInit](router);
// create routes using factories
this[coreSymbols.routesInit](router);
// include routes as middleware
app.use(router.routes());
app.use(router.allowedMethods());
// Create koa server
this[sServer] = http.createServer(app.callback());
// application configuration is completed
this.emit('post init');
}
/**
* Starts the application. Must be called after `initialize()`.
*/
listen() {
return new Promise((resolve, reject) => {
if (!this[sInitialized]) {
reject(new this.ApplicationError.NotAllowed('Cannot call Ravel.listen() before Ravel.init()'));
} else {
this.emit('pre listen');
// validate parameters
this[coreSymbols.validateParameters]();
// Start Koa server
this[sServer].listen(this.get('port'), () => {
this.log.info('Application server listening on port ' + this.get('port'));
this[sListening] = true;
this.emit('post listen');
resolve();
});
}
});
}
/**
* Intializes and starts the application.
*/
start() {
this.init();
return this.listen();
}
/**
* Stops the application. A no op if the server isn't running.
*/
close() {
return new Promise(resolve => {
// console.log('closing')
this.emit('end');
if (!this[sServer] || !this[sListening]) {
resolve();
} else {
this[sServer].close(() => {
this.log.info('Application server terminated.');
this[sListening] = false;
resolve();
});
}
});
}};
/**
* The base class of Ravel `Error`s, which associate
* http status codes with your custom errors.
* @example
* const Ravel = require('ravel');
* class NotFoundError extends Ravel.Error {
* constructor (msg) {
* super(msg, Ravel.httpCodes.NOT_FOUND);
* }
* }
*/
Ravel.Error = require('./util/application_error').General;
/**
* The base class for Ravel `DatabaseProvider`s. See
* [`DatabaseProvider`](#databaseprovider) for more information.
* @example
* const DatabaseProvider = require('ravel').DatabaseProvider;
* class MySQLProvider extends DatabaseProvider {
* // ...
* }
*/
Ravel.DatabaseProvider = require('./db/database_provider').DatabaseProvider;
/**
* Return a list of all registered `DatabaseProvider`s. See
* [`DatabaseProvider`](#databaseprovider) for more information.
* @returns {Array} a list of `DatabaseProvider`s
* @private
*/
require('./db/database_provider')(Ravel);
/**
* The base class for Ravel `AuthenticationProvider`s. See
* [`AuthenticationProvider`](#authenticationprovider) for more information.
* @example
* const AuthenticationProvider = require('ravel').AuthenticationProvider;
* class GoogleOAuth2Provider extends AuthenticationProvider {
* // ...
* }
*/
Ravel.AuthenticationProvider = require('./auth/authentication_provider').AuthenticationProvider;
/**
* Return a list of all registered `AuthenticationProvider`s. See
* [`AuthenticationProvider`](#authenticationprovider) for more information.
* @returns {Array} a list of `AuthenticationProvider`s
* @private
*/
require('./auth/authentication_provider')(Ravel);
/*
* Makes the `@inject` decorator available as `Ravel.inject`
* @example
* const Ravel = require('ravel');
* const inject = Ravel.inject;
*/
Ravel.inject = require('./core/decorators/inject');
/**
* A dictionary of useful http status codes. See [HTTPCodes](#httpcodes) for more information.
* @example
* const Ravel = require('ravel');
* console.log(Ravel.httpCodes.NOT_FOUND);
*/
Ravel.httpCodes = require('./util/http_codes');
/**
* Requires Ravel's parameter system
* See `core/params` for more information.
* @example
* app.registerParameter('my parameter', true, 'default value');
* const value = app.get('my parameter');
* app.set('my parameter', 'another value');
* @private
*/
require('./core/params')(Ravel);
/**
* The base class for Ravel `Module`s. See
* [`Module`](#module) for more information.
* @example
* const Module = require('ravel').Module;
* class MyModule extends Module {
* // ...
* }
* module.exports = MyModule;
*/
Ravel.Module = require('./core/module').Module;
/**
* Requires Ravel's `Module` registration system
* See [`Module`](#module) for more information.
* @example
* app.module('./modules/mymodule', 'mymodule');
* @private
*/
require('./core/module')(Ravel);
/**
* Requires Ravel's recursive `Module` registration system.
* Names for modules are derived from their file names, and
* are namespaced by their directories.
* See [`Module`](#module) for more information.
* @example
* // recursively load all Modules in a directory
* app.modules('./modules');
* // a Module 'modules/test.js' in ./modules can be injected as `@inject('test')`
* // a Module 'modules/stuff/test.js' in ./modules can be injected as `@inject('stuff.test')`
* @private
*/
require('./core/modules')(Ravel);
/**
* The base class for Ravel `Routes`. See
* [`Routes`](#routes) for more information.
* @example
* const Routes = require('ravel').Routes;
* class MyRoutes extends Routes {
* // ...
* }
* module.exports = MyRoutes;
*/
Ravel.Routes = require('./core/routes').Routes;
/**
* Requires Ravel's `Routes` registration system
* See [`Routes`](#routes) for more information.
* @example
* app.routes('./routes/myroutes');
* @private
*/
require('./core/routes')(Ravel);
/**
* The base class for Ravel `Resource`. See
* [`Resource`](#resource) for more information.
* @example
* const Resource = require('ravel').Resource;
* class MyResource extends Resource {
* // ...
* }
* module.exports = MyResource;
*/
Ravel.Resource = require('./core/resource').Resource;
/**
* Requires Ravel's `Resource` registration system
* See [`Resource`](#resource) for more information.
* @example
* app.resource('./resources/myresource');
* @private
*/
require('./core/resource')(Ravel);
/**
* Requires Ravel's recursive `Resource` registration system.
*
* See [`Resource`](#resource) for more information.
* @example
* // recursively load all Resources in a directory
* app.resources('./resources');
* @private
*/
require('./core/resources')(Ravel);
/**
* Requires Ravel's lightweight reflection/metadata system.
*
* See `core/reflect` for more information.
* @example
* // examine a registered Module, Resource or Route by file path
* app.reflect('./modules/mymodule.js');
* @private
*/
require('./core/reflect')(Ravel);
module.exports = Ravel;