UNPKG

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