UNPKG

24.4 kBJavaScriptView Raw
1'use strict';
2
3const AsyncEventEmitter = require('./util/event_emitter');
4const coreSymbols = require('./core/symbols');
5const Rest = require('./util/rest');
6
7const sInitialized = Symbol('isInitialized');
8const sListening = Symbol('isListening');
9const sLog = Symbol('$log');
10const sErr = Symbol('$err');
11const sCWD = Symbol('cwd');
12const sCallback = Symbol('callback');
13const sServer = Symbol('server');
14const sUncaughtRejections = Symbol('uncaughtRejections');
15const sKeygripKeys = Symbol('keygripKeys');
16const sKeygrip = Symbol('keygrip');
17
18/**
19 * This class provides Ravel, a lightweight but powerful framework
20 * for the rapid creation of enterprise Node.js applications which
21 * scale horizontally with ease and support the latest in web technology.
22 *
23 * @example
24 * const Ravel = require('ravel');
25 * const app = new Ravel();
26 */let
27Ravel = class Ravel extends AsyncEventEmitter {
28 /**
29 * Creates a new Ravel app instance.
30 *
31 * @example
32 * const Ravel = require('ravel');
33 * const app = new Ravel();
34 */
35 constructor() {
36 super();
37
38 this[sUncaughtRejections] = 0;
39 this[sInitialized] = false;
40 this[sListening] = false;
41 this[sKeygripKeys] = [];
42
43 // how we store modules for dependency injection
44 this[coreSymbols.modules] = Object.create(null);
45 // how we store routes for later access
46 this[coreSymbols.routes] = Object.create(null);
47 // how we store resources for later access
48 this[coreSymbols.resource] = Object.create(null);
49 // how we store middleware for dependency injection
50 this[coreSymbols.middleware] = Object.create(null);
51
52 // current working directory of the app using the
53 // ravel library, so that client modules can be
54 // loaded with relative paths.
55 this[sCWD] = process.cwd();
56
57 this[coreSymbols.knownParameters] = Object.create(null);
58 this[coreSymbols.params] = Object.create(null);
59 // a list of registered module factories which haven't yet been instantiated
60 this[coreSymbols.moduleFactories] = Object.create(null);
61 this[coreSymbols.resourceFactories] = Object.create(null);
62 this[coreSymbols.routesFactories] = Object.create(null);
63 // Allows us to detect duplicate binds
64 this[coreSymbols.endpoints] = new Map();
65 // a list of known modules, resources and routes so that metadata can be retrieved from them
66 this[coreSymbols.knownComponents] = Object.create(null);
67
68 // init errors
69 this[sErr] = require('./util/application_error');
70
71 // init logging system
72 this[sLog] = new (require('./util/log'))(this);
73
74 // init dependency injection utility, which is used by most everything else
75 this[coreSymbols.injector] = new (require('./core/injector'))(this,
76 module.parent !== null ? module.parent : module);
77
78 // Register known ravel parameters
79 // websocket parameters
80 this.registerParameter('enable websockets', true, true);
81 this.registerParameter('max websocket payload bytes', true, 100 * 1024 * 1024);
82 this.registerParameter('redis websocket channel prefix', true, 'ravel.ws');
83 // redis parameters
84 this.registerParameter('redis host', false);
85 this.registerParameter('redis port', true, 6379);
86 this.registerParameter('redis password');
87 this.registerParameter('redis max retries', true, 10);
88 this.registerParameter('redis keepalive interval', true, 1000);
89 // Node/koa parameters
90 this.registerParameter('port', true, 8080);
91 this.registerParameter('https', true, false);
92 // supports any option from https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener
93 this.registerParameter('https options', true, {});
94 this.registerParameter('public directory');
95 this.registerParameter('favicon path');
96 this.registerParameter('keygrip keys', true);
97 // session parameters
98 this.registerParameter('session key', true, 'ravel.sid');
99 this.registerParameter('session max age', true, null);
100 this.registerParameter('session secure', true, true);
101 this.registerParameter('session rolling', true, false);
102 // Passport parameters
103 this.registerParameter('app route', false, '/');
104 this.registerParameter('login route', false, '/login');
105 }
106
107 /**
108 * The current working directory of the app using the `ravel` library.
109 *
110 * @type String
111 */
112 get cwd() {
113 return this[sCWD];
114 }
115
116 /**
117 * Return the app instance of Logger.
118 * See [`Logger`](#logger) for more information.
119 *
120 * @type Logger
121 * @example
122 * app.$log.error('Something terrible happened!');
123 */
124 get $log() {
125 return this[sLog];
126 }
127
128 /**
129 * A hash of all built-in error types. See [`$err`](#$err) for more information.
130 *
131 * @type Object
132 */
133 get $err() {
134 return this[sErr];
135 }
136
137 /**
138 * Value is `true` iff init() or start() has been called on this app instance.
139 *
140 * @type boolean
141 */
142 get initialized() {
143 return this[sInitialized];
144 }
145
146 /**
147 * Value is `true` iff `listen()` or `start()` has been called on this app instance.
148 *
149 * @type boolean
150 */
151 get listening() {
152 return this[sListening];
153 }
154
155 /**
156 * The underlying koa callback for this Ravel instance.
157 * Useful for [testing](#testing-ravel-applications) with `supertest`.
158 * Only available after `init()` (i.e. use `@postinit`).
159 *
160 * @type {Function}
161 */
162 get callback() {
163 return this[sCallback];
164 }
165
166 /**
167 * The underlying HTTP server for this Ravel instance.
168 * Only available after `init()` (i.e. use `@postinit`).
169 * Useful for [testing](#testing-ravel-applications) with `supertest`.
170 *
171 * @type {http.Server}
172 */
173 get server() {
174 return this[sServer];
175 }
176
177 /**
178 * The underlying Keygrip instance for cookie signing.
179 *
180 * @type {Keygrip}
181 */
182 get keys() {
183 return this[sKeygrip];
184 }
185
186 /**
187 * Register a provider, such as a DatabaseProvider or AuthenticationProvider.
188 *
189 * @param {Function} ProviderClass - The provider class.
190 * @param {...any} args - Arguments to be provided to the provider's constructor
191 * (prepended automatically with a reference to app).
192 */
193 registerProvider(ProviderClass, ...args) {
194 return new ProviderClass(this, ...args);
195 }
196
197 /**
198 * Initializes the application, when the client is finished
199 * supplying parameters and registering modules, resources
200 * routes and rooms.
201 *
202 * @example
203 * await app.init();
204 */
205 async init() {
206 // application configuration is completed in constructor
207 await this.emit('pre init');
208
209 // log uncaught errors to prevent ES6 promise error swallowing
210 // https://www.hacksrus.net/blog/2015/08/a-solution-to-swallowed-exceptions-in-es6s-promises/
211 process.removeAllListeners('unhandledRejection');
212 process.on('unhandledRejection', err => {
213 this[sUncaughtRejections] += 1;
214 const message = `Detected uncaught error in promise: \n ${err ? err.stack : err}`;
215 if (this[sUncaughtRejections] >= 10) {
216 this.$log.error(message);
217 this.$log.error(`Encountered ${this[sUncaughtRejections]} or more uncaught rejections. Re-run ` +
218 'node and ravel with full logging output for more information:' +
219 '\n- node --trace-warnings app.js' +
220 '\n- app.set(\'log level\', app.$log.TRACE);');
221 } else {
222 this.$log.debug(message);
223 }
224 });
225
226 // load parameters from .ravelrc.json file, if any
227 await this.emit('pre load parameters');
228 this[coreSymbols.loadParameters]();
229 await this.emit('post load parameters');
230
231 this[sInitialized] = true;
232
233 this.$db = require('./db/database')(this);
234 this.$kvstore = require('./util/kvstore')(this);
235
236 // App dependencies.
237 const http = require('http');
238 const https = require('https');
239 const upath = require('upath');
240 const Koa = require('koa');
241 const session = require('koa-session');
242 const compression = require('koa-compress');
243 const favicon = require('koa-favicon');
244 const router = new (require('koa-router'))();
245
246 // configure koa
247 const app = new Koa();
248 app.proxy = true;
249
250 // the first piece of middleware is the exception handler
251 // catch all errors and return appropriate error codes
252 // to the client
253 app.use(new Rest(this).errorHandler());
254
255 // enable gzip compression
256 app.use(compression());
257
258 // configure redis session store
259 this[sKeygripKeys] = [...this.get('keygrip keys')];
260 this[sKeygrip] = require('keygrip')(this[sKeygripKeys], 'sha256', 'base64');
261 app.keys = this[sKeygrip];
262
263 // configure session options
264 if (!this.get('https') && !this.get('session secure')) {
265 this.$log.warn("app.set('session secure', false) results in insecure sessions" +
266 ' over HTTP and introduces critical security vulnerabilities.' +
267 ' This is intended for development purposes only, or for use' +
268 ' behind a TLS termination proxy.');
269 }
270 const sessionOptions = {
271 store: new (require('./util/redis_session_store'))(this),
272 key: this.get('session key'),
273 maxAge: Number(this.get('session max age')),
274 overwrite: true, /* (boolean) can overwrite or not (default true) */
275 httpOnly: true, /* (boolean) httpOnly or not (default true) */
276 signed: true, /* (boolean) signed or not (default true) */
277 secure: this.get('https') || this.get('session secure'), /* (boolean) secure or not (default true) */
278 rolling: this.get('session rolling') /* (boolean) Force a session identifier cookie to be set on every response.
279 The expiration is reset to the original maxAge, resetting the expiration
280 countdown. default is false */ };
281
282 app.use(session(sessionOptions, app));
283
284 // favicon
285 if (this.get('favicon path')) {
286 app.use(favicon(upath.toUnix(upath.posix.join(this.cwd, this.get('favicon path')))));
287 }
288
289 // static file serving
290 if (this.get('public directory')) {
291 const koaStatic = require('koa-static');
292 const root = upath.toUnix(upath.posix.join(this.cwd, this.get('public directory')));
293 app.use(koaStatic(root, {
294 gzip: false // this should be handled by koa-compressor?
295 }));
296 }
297
298 // initialize authentication/authentication
299 require('./auth/passport_init.js')(this, router);
300
301 // create websocket broker
302 const WebsocketBroker = require('./core/websocket.js');
303 this[coreSymbols.websocketBroker] = new WebsocketBroker(this);
304
305 // basic koa configuration is completed
306 await this.emit('post config koa', app);
307
308 // create registered modules using factories
309 await this[coreSymbols.moduleInit]();
310
311 await this.emit('post module init');
312
313 await this.emit('pre routes init', app);
314
315 // create registered resources using factories
316 this[coreSymbols.resourceInit](router);
317
318 // create routes using factories
319 this[coreSymbols.routesInit](router);
320
321 // include routes as middleware
322 app.use(router.routes());
323 app.use(router.allowedMethods());
324
325 // Create koa callback and server
326 this[sCallback] = app.callback();
327 this[sServer] = this.get('https') ?
328 https.createServer(this.get('https options'), this[sCallback]) :
329 http.createServer(this[sCallback]);
330
331 // application configuration is completed
332 await this.emit('post init');
333 }
334
335 /**
336 * Rotate a new cookie signing key in, and the oldest key out.
337 *
338 * @param {string} newKey - The new key.
339 */
340 rotateKeygripKey(newKey) {
341 this[sKeygripKeys].unshift(newKey);
342 this[sKeygripKeys].pop();
343 }
344
345 /**
346 * Starts the application. Must be called after `initialize()`.
347 *
348 * @example
349 * await app.listen();
350 */
351 async listen() {
352 if (!this[sInitialized]) {
353 throw new this.$err.NotAllowed('Cannot call Ravel.listen() before Ravel.init()');
354 } else {
355 await this.emit('pre listen');
356 return new Promise((resolve, reject) => {
357 // validate parameters
358 this[coreSymbols.validateParameters]();
359 // Start Koa server
360 this[sServer].listen(this.get('port'), () => {
361 /* eslint-disable max-len */
362 this.$log.info(`Application server listening for ${this.get('https') ? 'https' : 'http'} traffic on port ${this.get('port')}`);
363 /* eslint-enable max-len */
364 this[sListening] = true;
365 this.emit('post listen');
366 resolve();
367 });
368 });
369 }
370 }
371
372 /**
373 * Intializes and starts the application.
374 *
375 * @example
376 * await app.start();
377 */
378 async start() {
379 await this.init();
380 return this.listen();
381 }
382
383 /**
384 * Stops the application. A no op if the server isn't running.
385 *
386 * @example
387 * await app.close();
388 */
389 async close() {
390 await this.emit('end');
391 if (this[sServer] && this[sListening]) {
392 return new Promise(resolve => {
393 this[sServer].close(() => {
394 this.$log.info('Application server terminated.');
395 this[sListening] = false;
396 resolve();
397 });
398 });
399 }
400 }};
401
402
403/**
404 * The base class of Ravel `Error`s, which associate
405 * HTTP status codes with your custom errors.
406 * @example
407 * const Ravel = require('ravel');
408 * class NotFoundError extends Ravel.Error {
409 * constructor (msg) {
410 * super(msg, Ravel.httpCodes.NOT_FOUND);
411 * }
412 * }
413 */
414Ravel.Error = require('./util/application_error').General;
415
416/**
417 * The base class for Ravel `DatabaseProvider`s. See
418 * [`DatabaseProvider`](#databaseprovider) for more information.
419 * @example
420 * const DatabaseProvider = require('ravel').DatabaseProvider;
421 * class MySQLProvider extends DatabaseProvider {
422 * // ...
423 * }
424 */
425Ravel.DatabaseProvider = require('./db/database_provider').DatabaseProvider;
426
427/**
428 * Return a list of all registered `DatabaseProvider`s. See
429 * [`DatabaseProvider`](#databaseprovider) for more information.
430 * @returns {Array} a list of `DatabaseProvider`s
431 * @private
432 */
433require('./db/database_provider')(Ravel);
434
435/**
436 * The base class for Ravel `AuthenticationProvider`s. See
437 * [`AuthenticationProvider`](#authenticationprovider) for more information.
438 * @example
439 * const AuthenticationProvider = require('ravel').AuthenticationProvider;
440 * class GoogleOAuth2Provider extends AuthenticationProvider {
441 * // ...
442 * }
443 */
444Ravel.AuthenticationProvider = require('./auth/authentication_provider').AuthenticationProvider;
445
446/**
447 * Return a list of all registered `AuthenticationProvider`s. See
448 * [`AuthenticationProvider`](#authenticationprovider) for more information.
449 * @returns {Array} a list of `AuthenticationProvider`s
450 * @private
451 */
452require('./auth/authentication_provider')(Ravel);
453
454/*
455 * Makes the `@inject` decorator available as `Ravel.inject`
456 * @example
457 * const Ravel = require('ravel');
458 * const inject = Ravel.inject;
459 */
460Ravel.inject = require('./core/decorators/inject');
461
462/*
463 * Makes the `@autoinject` decorator available as `Ravel.autoinject`
464 * @example
465 * const Ravel = require('ravel');
466 * const autoinject = Ravel.autoinject;
467 */
468Ravel.autoinject = require('./core/decorators/autoinject');
469
470/**
471 * A dictionary of useful http status codes. See [HTTPCodes](#httpcodes) for more information.
472 * @example
473 * const Ravel = require('ravel');
474 * console.log(Ravel.httpCodes.NOT_FOUND);
475 */
476Ravel.httpCodes = require('./util/http_codes');
477
478/**
479 * Requires Ravel's parameter system
480 * See `core/params` for more information.
481 * @example
482 * app.registerParameter('my parameter', true, 'default value');
483 * const value = app.get('my parameter');
484 * app.set('my parameter', 'another value');
485 * @private
486 */
487require('./core/params')(Ravel);
488
489/**
490 * The base decorator for Ravel `Module`s. See
491 * [`Module`](#module) for more information.
492 * @example
493 * const Module = require('ravel').Module;
494 * @Module
495 * class MyModule {
496 * // ...
497 * }
498 * module.exports = MyModule;
499 */
500Ravel.Module = require('./core/module').Module;
501
502/**
503 * Requires Ravel's `Module` retrieval and registration system
504 * See [`Module`](#module) for more information.
505 * @example
506 * app.scan('./modules/mymodule');
507 * @private
508 */
509require('./core/module')(Ravel);
510
511/**
512 * Requires Ravel's recursive component registration system.
513 *
514 * See [`Module`](#module), [`Resource`](#resource) and [`Routes`](#routes) for more information.
515 * @example
516 * // recursively load all Modules in a directory
517 * app.scan('./modules');
518 * // a Module 'modules/test.js' in ./modules can be injected as `@inject('test')`
519 * // a Module 'modules/stuff/test.js' in ./modules can be injected as `@inject('stuff.test')`
520 * @example
521 * // recursively load all Resources in a directory
522 * app.scan('./resources');
523 * // recursively load all Resources in a directory
524 * app.scan('./routes');
525 * @private
526 */
527require('./core/scan')(Ravel);
528
529/**
530 * The base class for Ravel `Routes`. See
531 * [`Routes`](#routes) for more information.
532 * @example
533 * const Routes = require('ravel').Routes;
534 * // @Routes('/')
535 * class MyRoutes {
536 * // ...
537 * }
538 * module.exports = MyRoutes;
539 */
540Ravel.Routes = require('./core/routes').Routes;
541
542/**
543 * Requires Ravel's `Routes` registration system
544 * See [`Routes`](#routes) for more information.
545 * @example
546 * app.routes('./routes/myroutes');
547 * @private
548 */
549require('./core/routes')(Ravel);
550
551/**
552 * The base class for Ravel `Resource`. See
553 * [`Resource`](#resource) for more information.
554 * @example
555 * const Resource = require('ravel').Resource;
556 * // @Resource('/')
557 * class MyResource {
558 * // ...
559 * }
560 * module.exports = MyResource;
561 */
562Ravel.Resource = require('./core/resource').Resource;
563
564/**
565 * Requires Ravel's `Resource` registration system
566 * See [`Resource`](#resource) for more information.
567 * @example
568 * app.resource('./resources/myresource');
569 * @private
570 */
571require('./core/resource')(Ravel);
572
573/**
574 * Requires Ravel's lightweight reflection/metadata system.
575 *
576 * See `core/reflect` for more information.
577 * @example
578 * // examine a registered Module, Resource or Route by file path
579 * app.reflect('./modules/mymodule.js');
580 * @private
581 */
582require('./core/reflect')(Ravel);
583
584module.exports = Ravel;
\No newline at end of file