1 | ;
|
2 |
|
3 | const AsyncEventEmitter = require('./util/event_emitter');
|
4 | const coreSymbols = require('./core/symbols');
|
5 | const Rest = require('./util/rest');
|
6 |
|
7 | const sInitialized = Symbol('isInitialized');
|
8 | const sListening = Symbol('isListening');
|
9 | const sLog = Symbol('$log');
|
10 | const sErr = Symbol('$err');
|
11 | const sCWD = Symbol('cwd');
|
12 | const sCallback = Symbol('callback');
|
13 | const sServer = Symbol('server');
|
14 | const sUncaughtRejections = Symbol('uncaughtRejections');
|
15 | const sKeygripKeys = Symbol('keygripKeys');
|
16 | const 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
|
27 | Ravel = 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 | */
|
414 | Ravel.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 | */
|
425 | Ravel.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 | */
|
433 | require('./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 | */
|
444 | Ravel.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 | */
|
452 | require('./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 | */
|
460 | Ravel.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 | */
|
468 | Ravel.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 | */
|
476 | Ravel.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 | */
|
487 | require('./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 | */
|
500 | Ravel.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 | */
|
509 | require('./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 | */
|
527 | require('./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 | */
|
540 | Ravel.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 | */
|
549 | require('./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 | */
|
562 | Ravel.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 | */
|
571 | require('./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 | */
|
582 | require('./core/reflect')(Ravel);
|
583 |
|
584 | module.exports = Ravel; |
\ | No newline at end of file |