UNPKG

45.5 kBMarkdownView Raw
1# Ravel
2> Forge past a tangle of modules. Make a cool app.
3
4[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/raveljs/ravel/master/LICENSE) [![npm version](https://badge.fury.io/js/ravel.svg)](http://badge.fury.io/js/ravel) [![Dependency Status](https://david-dm.org/raveljs/ravel.svg)](https://david-dm.org/raveljs/ravel) [![npm](https://img.shields.io/npm/dm/ravel.svg?maxAge=2592000)](https://www.npmjs.com/package/ravel) [![Build Status](https://travis-ci.org/raveljs/ravel.svg?branch=master)](https://travis-ci.org/raveljs/ravel) [![Build status](https://ci.appveyor.com/api/projects/status/5kx5j2d1fhyn9yn3/branch/master?svg=true)](https://ci.appveyor.com/project/Ghnuberath/ravel/branch/master) [![Test Coverage](https://codeclimate.com/github/raveljs/ravel/badges/coverage.svg)](https://codeclimate.com/github/raveljs/ravel/coverage) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard)
5
6Ravel is a [tiny](https://npm.anvaka.com/#/view/2d/ravel), sometimes-opinionated foundation for creating organized, maintainable, and scalable web applications in [node.js](https://github.com/joyent/node) with [ES2016/2017](http://kangax.github.io/compat-table/esnext/).
7
8**Note:** The `master` branch may be in an unstable or even broken state during development. Please use [releases](https://github.com/raveljs/ravel/releases) instead of the `master` branch to explore stable code.
9
10## Table of Contents
11
12<!-- TOC depthFrom:2 depthTo:3 withLinks:1 updateOnSave:1 orderedList:0 -->
13
14- [Table of Contents](#table-of-contents)
15- [Introduction](#introduction)
16- [Installation](#installation)
17- [Architecture](#architecture)
18 - [Modules (and Errors)](#modules-and-errors)
19 - [Middleware](#middleware)
20 - [Routes](#routes)
21 - [Resources](#resources)
22 - [Bringing it all together](#bringing-it-all-together)
23 - [Decorator Transpilation](#decorator-transpilation)
24 - [Running the Application](#running-the-application)
25- [API Documentation](#api-documentation)
26 - [Ravel App](#ravel-app)
27 - [Managed Configuration System](#managed-configuration-system)
28 - [Ravel.Error](#ravelerror)
29 - [Ravel.Module](#ravelmodule)
30 - [Ravel.Routes](#ravelroutes)
31 - [Ravel.Resource](#ravelresource)
32 - [Response Caching](#response-caching)
33 - [Database Providers](#database-providers)
34 - [Transaction-per-request](#transaction-per-request)
35 - [Scoped Transactions](#scoped-transactions)
36 - [Authentication Providers](#authentication-providers)
37 - [Authentication](#authentication)
38 - [WebSockets](#websockets)
39- [Deployment and Scaling](#deployment-and-scaling)
40
41<!-- /TOC -->
42
43## Introduction
44
45Ravel is inspired by the simplicity of [koa](http://koajs.com/) and [express](http://expressjs.com), but aims to provide a pre-baked, well-tested and highly modular solution for creating enterprise web applications by providing:
46
47- A standard set of well-defined architectural components so that your code stays **organized**
48- Rapid **REST API** definition
49- Easy **bootstrapping** via an enforced, reference configuration of [koa](http://koajs.com/) with critical middleware
50- Dependency injection (instead of relative `require`s)
51
52And a few other features, plucked from popular back-end frameworks:
53
54- Transaction-per-request
55- Simple authentication and authentication configuration (no complex [passport](https://github.com/jaredhanson/passport) setup)
56- (Optional) externalized session storage for horizontal scalability
57
58Ravel is deliberately designed to [minimize unnecessary dependencies](https://npm.anvaka.com/#/view/2d/ravel) and have a small, well-documented codebase, making it easier to create secure and robust applications you and your users can trust.
59
60Ravel is layered on top of, and designed to be used with, awesome technologies, including:
61- [koa](http://koajs.com/)
62- [Passport](https://github.com/jaredhanson/passport)
63- [Intel](https://github.com/seanmonstar/intel)
64- [Redis](https://github.com/antirez/redis)
65- [docker](http://docker.com)
66
67
68## Installation
69
70> As Ravel uses async/await and several other ES2015/2016 features, you will need to use a 8.0.x+ distribution of node
71
72```bash
73$ npm install ravel
74```
75
76## Architecture
77
78Ravel applications consist of a few basic parts:
79
80- **Modules:** plain old classes which offer a great place to define modular application logic, middleware, authentication logic, etc.
81- **Middleware** a familiar concept from `express` or `koa`-like frameworks, middleware are chained functions which run in sequence against a request to a specific route.
82- **Routes:** a low-level place for general routing logic
83- **Resources:** built on top of `Routes`, `Resource`s are REST-focused
84- **Errors:** Node.js `Error`s which are associated with an HTTP response code. `throw` them in your code and `Routes` and `Resource`s will automatically produce responses with a matching status.
85
86If you're doing it right, your applications will consist largely of `Module`s, with a thin layer of `Routes` and `Resource`s on top.
87
88### Modules (and Errors)
89
90`Module`s are plain old node.js modules exporting a single class which encapsulates application logic. `Module`s support dependency injection of core Ravel services and other Modules alongside npm dependencies *(no relative `require`'s!)*. `Module`s are instantiated safely in dependency-order, and cyclical dependencies are detected automatically.
91
92For more information about `Module`s, look at [Ravel.Module](#ravelmodule) below.
93
94*modules/cities.js*
95```javascript
96const Ravel = require('ravel');
97const Error = Ravel.Error;
98const Module = Ravel.Module;
99const inject = Ravel.inject;
100
101/**
102 * First, we'll define an Error we will throw when a requested
103 * city is not found. This Error will be associated with the
104 * HTTP error code 404.
105 */
106class MissingCityError extends Error {
107 constructor (name) {
108 super(`City ${name} does not exist.`, Ravel.httpCodes.NOT_FOUND);
109 }
110}
111
112/**
113 * Our main Module, defining logic for working with Cities
114 */
115@inject('moment', '$log')
116@Module('cities')
117class Cities {
118 constructor (moment, $log) {
119 this.moment = moment;
120 this.$log = $log
121 this.cities = ['Toronto', 'New York', 'Chicago']; // our fake 'database'
122 }
123
124 getAllCities () {
125 return Promise.resolve(this.cities);
126 }
127
128 getCity (name) {
129 return new Promise((resolve, reject) => {
130 const index = this.cities.indexOf(name);
131 if (index !== -1) {
132 resolve(this.cities[index]);
133 } else {
134 // Ravel will automatically respond with the appropriate HTTP status code!
135 this.$log.warn(`User requested unknown city ${name}`);
136 reject(new MissingCityError(name));
137 }
138 });
139 }
140}
141
142// Export Module class
143module.exports = Cities;
144```
145
146### Middleware
147
148`Ravel` middleware takes the form of an `async funtion` and is defined within `Modules`:
149
150*modules/cities.js*
151```js
152const Ravel = require('ravel');
153const Module = Ravel.Module;
154const middleware = Module.middleware;
155class MyMiddleware {
156 // this middleware will be available by name elsewhere in the application
157 @middleware('custom-middleware')
158 async doSomething(ctx, next) {
159 // ... do something before the next middleware runs
160 await next();
161 // ... do something after the next middlware runs
162 }
163}
164```
165
166### Routes
167
168`Routes` are `Ravel`'s lower-level wrapper for `koa` (`Resource`s are the higher-level one). They support GET, POST, PUT and DELETE requests, and middleware, via decorators. Like `Module`s, they also support dependency injection. Though `Routes` can do everything `Resources` can do, they are most useful for implementing non-REST things, such as static content serving, proxying, etc. If you want to build a REST API, use `Resource`s instead (they're up next!).
169
170For more information about `Routes`, look at [Ravel.Routes](#ravelroutes) below.
171
172*routes/index.js*
173```javascript
174const Ravel = require('ravel');
175const Routes = Ravel.Routes;
176const inject = Ravel.inject;
177const before = Routes.before; // decorator to chain middleware before an endpoint
178const mapping = Routes.mapping; // decorator to associate a handler method with an endpoint
179
180@Routes('/') // base path for all routes in this class. Will be prepended to the @mapping.
181class ExampleRoutes {
182 constructor (middleware1) {
183 // bind this method to an endpoint and verb with @mapping. This one will become GET /app
184 @mapping(Routes.GET, 'app')
185 @before('custom-middleware') // use @before to place multiple middleware (comma-separated names) before appHandler - these could be npm modules, functions on this scope, or defined via @middleware
186 async appHandler (ctx) {
187 // ctx is just a koa context! Have a look at the koa docs to see what methods and properties are available.
188 ctx.body = '<!DOCTYPE html><html><body>Hello World!</body></html>';
189 ctx.status = 200;
190 }
191}
192
193// Export Routes class
194module.exports = ExampleRoutes;
195```
196
197### Resources
198
199What might be referred to as a *controller* in other frameworks, a `Resource` module defines HTTP methods on an endpoint, supporting the session-per-request transaction pattern via Ravel middleware. `Resource`s also support dependency injection, allowing for the easy creation of RESTful interfaces to your `Module`-based application logic. Resources are really just a thin wrapper around `Routes`, using specially-named handler functions (`get`, `getAll`, `post`, `put`, `putAll`, `delete`, `deleteAll`) instead of `@mapping`. This convention-over-configuration approach makes it easier to write proper REST APIs with less code, and is recommended over "carefully chosen" `@mapping`s in a `Routes` class.
200
201For more information about `Resource`s, look at [Ravel.Resource](#ravelresouce) below.
202
203*resources/city.js*
204```javascript
205// Resources support dependency injection too!
206// Notice that we have injected our cities Module by name.
207const Ravel = require('ravel');
208const Resource = Ravel.Resource;
209const inject = Ravel.inject;
210const before = Resource.before; // decorator to add middleware to an endpoint within the Resource
211
212// using @before at the class level decorates all endpoint methods with middleware
213@inject('cities')
214@Resource('/cities') // base path for all routes in this Resource
215class CitiesResource {
216 constructor (cities) {
217 this.cities = cities;
218 }
219
220 // no need to use @mapping here. Routes methods are automatically mapped using their names.
221 async getAll (ctx) { // just like in Routes, ctx is a koa context.
222 ctx.body = await this.cities.getAllCities();
223 }
224
225 @before('custom-middleware') // using @before at the method level decorates this method with middleware
226 async get (ctx) { // get routes automatically receive an endpoint of /cities/:id (in this case).
227 ctx.body = await this.cities.getCity(ctx.params.id);
228 }
229
230 // post, put, putAll, delete and deleteAll are
231 // also supported. Not specifying them for
232 // this resource will result in calls using
233 // those verbs returning HTTP 501 NOT IMPLEMENTED
234
235 // postAll is not supported, because it makes no sense
236}
237
238// Export Resource class
239module.exports = CitiesResource;
240```
241
242### Bringing it all together
243
244*app.js*
245```javascript
246const app = new require('ravel')();
247
248// parameters like this can be supplied via a .ravelrc.json file
249app.set('keygrip keys', ['mysecret', 'anothersecret']);
250
251app.scan('./modules'); //import all Modules from a directory
252app.scan('./resources'); //import all Resources from a directory
253app.scan('./routes/index.js'); //import all Routes from a file
254
255// start it up!
256app.start();
257```
258
259### Decorator Transpilation
260
261Since decorators are not yet available in Node, you will need to use a transpiler to convert them into ES2016-compliant code. We have chosen [Babel](https://babeljs.io/) as our recommended transpiler.
262
263```bash
264$ npm install gulp-sourcemaps@1.6.0 babel-core@6.18.2 babel-plugin-transform-decorators-legacy@1.3.4 gulp-babel@6.1.2
265# Note, please add babel-plugin-transform-async-to-generator@6.16.0 if you are using Node v6 instead of v7.
266```
267
268*gulpfile.js*
269```js
270const babelConfig = {
271 'retainLines': true,
272 'plugins': ['transform-decorators-legacy'] // add 'transform-async-to-generator' if you are using Node v6 instead of v7
273};
274gulp.task('transpile', function () {
275 return gulp.src('src/**/*.js') // point it at your source directory, containing Modules, Resources and Routes
276 .pipe(plugins.sourcemaps.init())
277 .pipe(plugins.babel(babelConfig))
278 .pipe(plugins.sourcemaps.write('.'))
279 .pipe(gulp.dest('dist')); // your transpiled Ravel app will appear here!
280});
281```
282
283Check out the [starter project](https://github.com/raveljs/ravel-github-mariadb-starter) to see a working example of this build process.
284
285### Running the Application
286
287```bash
288$ node dist/app.js
289```
290
291## API Documentation
292> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/)
293
294### Ravel App
295> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#ravel)
296
297A Ravel application is a root application file (such as `app.js`), coupled with a collection of files exporting `Module`s, `Resource`s and `Routes` (see [Architecture](#architecture) for more information). Getting started is usually as simple as creating `app.js`:
298
299*app.js*
300```js
301const Ravel = require('ravel');
302const app = new Ravel();
303
304(async () => {
305 // you'll register managed parameters, and connect Modules, Resources and Routes here
306 await app.init();
307 // you'll set managed parameters here
308 // ...
309 // then start the server
310 await app.listen();
311})();
312```
313
314### Managed Configuration System
315
316Traditional `node` appliations often rely on `process.env` for configuration. This can lead to headaches when an expected value is not declared in the environment, a value is supplied but doesn't match any expected ones, or the name of an environment variable changes and refactoring mistakes are made. To help mitigate this common issue, Ravel features a simple configuration system which relies on three methods:
317
318#### app.registerParameter
319> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#registerParameter)
320
321Create managed parameters with `app.registerParameter()`:
322
323*app.js*
324```js
325const Ravel = require('ravel');
326const app = new Ravel();
327
328// register a new optional parameter
329app.registerParameter('my optional parameter');
330// register a new required parameter
331app.registerParameter('my required parameter', true);
332// register a required parameter with a default value
333app.registerParameter('my third parameter', true, 'some value');
334
335(async () => {
336 await app.init();
337 await app.listen();
338})();
339```
340
341Many Ravel plugin libraries will automatically create parameters which you will have to supply values for. These parameters will be documented in their `README.md`.
342
343#### app.set
344> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#set)
345
346Provide values via `app.set()`. Setting an unknown parameter will result in an `Error`.
347
348*app.js*
349```js
350const Ravel = require('ravel');
351const app = new Ravel();
352
353// register a new optional parameter
354app.registerParameter('my optional parameter');
355
356(async () => {
357 await app.init();
358
359 // set a value
360 app.set('my optional parameter', 'some value');
361 // this won't work:
362 app.set('an unknown parameter', 'some value');
363
364 await app.listen();
365})();
366```
367
368#### app.get
369> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#get)
370
371Retrieve values via `app.get()`. Retrieving an unknown parameter will result in an `Error`.
372
373*app.js*
374```js
375const Ravel = require('ravel');
376const app = new Ravel();
377
378// register a new parameter
379app.registerParameter('my required parameter', true, 'default value');
380
381(async () => {
382 await app.init();
383
384 // set a value
385 app.set('my required parameter', 'some value');
386 // get a value
387 app.get('my required parameter') === 'some value';
388 // this won't work:
389 // app.get('an unknown parameter');
390
391 await app.listen();
392})();
393```
394
395#### Core parameters
396
397Ravel has several core parameters:
398
399```js
400// set initial keygrip keys for cookie signing (recommend at least 5):
401app.set('keygrip keys', ['my super secret key', 'another super secret key']);
402
403// these are optional (default values are shown):
404app.set('enable websockets', true); // set to false to disable websocket server
405app.set('redis host', undefined); // set to point to an external redis server (required for horizontal scaling).
406app.set('redis port', 6379);
407app.set('redis password', undefined);
408app.set('redis max retries', 10); // connection retries
409app.set('port', 8080); // port the app will run on
410app.set('session key', 'ravel.sid'); // the cookie name to use for sessions
411app.set('session max age', null); // session maxAge (default never expires)
412app.set('app route', '/'); // if you have a UI, this is the path users will be sent to when they are logged in
413app.set('login route', '/login'); // if users aren't logged in and you redirect them, this is where they'll be sent
414app.set('public directory', undefined); // if you want to statically serve a directory
415app.set('favicon path', undefined); // favicon middleware configuration
416```
417
418#### .ravelrc.json
419
420To make it easier to supply configuration values to Ravel, a `.ravelrc.json` file can be placed beside `app.js` (or in any parent directory of `app.js`). This is the recommended method of setting parameters.
421
422*.ravelrc.json*
423```
424{
425 "keygrip keys": ["my super secret key", "another super secret key"]
426}
427```
428
429You can also use environment variables in `.ravelrc.json` file which will be interpolated using `process.env` at startup time:
430
431```
432{
433 "keygrip keys": "$MY_SUPER_SECRET_KEY",
434 "mysql connection string":
435 "mysql:///$MYSQL_USER:$MYSQL_PASSWORD@$MYSQL_HOST:$MYSQL_PORT/$MYSQL_DB"
436}
437```
438
439### Ravel.Error
440> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Ravel.Error)
441
442This is the base `Error` type for Ravel, meant to be extended into semantic errors which can be used within your applications. When you create a custom `Ravel.Error`, you **must** provide an associated HTTP status code, which Ravel will automatically respond with if an HTTP request results in that particular `Error` being thrown. This helps create meaningful status codes for your REST APIs while working within traditional `node` error-handling paradigms (`throw/try/catch` and `Promise.reject()`). Errors are generally best-declared within `Module`, `Resource` or `Routes` files (and not exported), closest to where they are used. If necessary, create a `Module` to group and export them.
443
444*at the top of some `Module`, `Resource` or `Routes` file (we'll get to this next)*
445```js
446const Ravel = require('ravel');
447/**
448 * Thrown when a user tries to POST something unexpected to /upload
449 */
450class UploadError extends Ravel.Error {
451 constructor (msg) {
452 super(msg, Ravel.httpCodes.BAD_REQUEST);
453 }
454}
455```
456
457### Ravel.Module
458> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#module)
459
460`Module`s are meant to contain the bulk of your application logic, either to support endpoints defined in `Resource`s and `Routes`, or to perform tasks at specific points during the Ravel lifecycle (see [Lifecycle Decorators](#lifecycle-decorators) below).
461
462Here's a simple module:
463
464*modules/my-module.js*
465```js
466const Ravel = require('ravel');
467const inject = Ravel.inject; // Ravel's dependency injection decorator
468const Module = Ravel.Module; // base class for Ravel Modules
469
470// inject a custom ravel Module (or your plain classes) beside npm dependencies!
471@Module('mymodule')
472@inject('path', 'fs', 'custom-module', 'plain-class')
473class MyModule {
474 constructor (path, fs, custom, plain) { // @inject'd modules are available here as parameters
475 this.path = path;
476 this.fs = fs;
477 this.custom = custom;
478 this.plain = plain;
479 }
480
481 // implement any methods you like :)
482 aMethod () {
483 // ...
484 }
485
486 async anAsyncMethod () {
487 // ...
488 }
489}
490
491module.exports = MyModule; // you must export your Module so that Ravel can require() it.
492```
493
494#### Dependency Injection and Module Registration
495> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#inject)
496
497Ravel's *dependency injection* system is meant to address several issues with traditional `require()`s:
498
499- Using `require()` with one's own modules in a complex project often results in statements like this: `require('../../../../my/module');`. This issue is especially pronounced when `require()`ing source modules in test files.
500- Cyclical dependencies between modules are not always obvious in a large codebase, and can result in unexpected behaviour.
501
502Ravel addresses this with the the [`@inject`](http://raveljs.github.io/docs/latest/index.html#inject) decorator:
503
504*modules/my-module.js*
505```js
506const Ravel = require('ravel');
507const inject = Ravel.inject;
508const Module = Ravel.Module;
509
510@Module('mymodule')
511@inject('another-module') // inject another Module from your project without require()!
512class MyModule {
513 constructor (another) { // @inject'd modules are available here as parameters
514 this.another = another;
515 }
516}
517module.exports = MyModule;
518```
519
520The injection name of `another-module` is inferred from its filename, but can be overriden via the `@Module('custom-name')` decorator.
521
522If runnning `app.scan('./modules')`:
523- `'./modules/my-module'` will be injectable as `'my-module'`
524- `'./modules/another-module'` will be injectable as `'another-module'`
525- `'./modules/package/another-module'` will be injectable as `'package.another-module'`
526
527`Module`s are singletons which are instantiated in *dependency-order* (i.e. if `A` depends on `B`, `B` is guaranteed to be constructed first). Cyclical dependencies are detected automatically and result in an `Error`.
528
529To further simplify working with imports in Ravel, you can `@inject` Ravel services, the core node API, and `npm` dependencies (installed in your local `node_modules` or globally) alongside your own `Module`s:
530
531```js
532const Ravel = require('ravel');
533const inject = Ravel.inject;
534const Module = Ravel.Module;
535
536@Module('mymodule')
537@inject('another-module', 'fs', 'moment', '$err') // anything that can be require()d can be @injected
538class MyModule {
539 constructor (another, fs, moment, $err) {
540 // ...
541 }
542}
543module.exports = MyModule;
544```
545
546To avoid constructors which simply perform assignments, Ravel includes the `@autoinject` decorator which can perform assignments for you:
547
548*modules/my-module.js*
549```js
550const Ravel = require('ravel');
551const inject = Ravel.inject;
552const Module = Ravel.Module;
553
554@Module('mymodule')
555@inject('another') // you can still mix using @inject!
556@autoinject('fs', 'moment', '$err')
557class MyModule {
558 constructor (another) { // @inject'd modules are available here as parameters
559 this.another = another;
560 // @autoinjection takes place AFTER construction, so fs,
561 // moment and $err are not available here.
562 }
563 method () {
564 // this.fs, this.moment and this.$err are available here
565 }
566}
567module.exports = MyModule;
568```
569
570#### Core Services
571
572Several core `Ravel` services are available for injection within your `Module`s, `Resource`s and `Routes`:
573
574- `@inject('$app')` - A reference to the ravel app object itself
575- `@inject('$err')` - Built-in error types
576- `@inject('$log')` - A logger scoped to the target module
577- `@inject('$kvstore')` - A reference to the internal redis connection (or mock, in the case where no external redis is supplied)
578- `@inject('$params')` - A read-only reference to the parameter system, to retrieve parameter values
579- `@inject('$db')` - A mechanism for creating scoped transactions. See [Scoped Transactions](#scoped-transactions) below for more information.
580
581#### Module Namespacing
582
583In a large project, it may become desirable to namespace your `Module`s to avoid naming conflicts. This is easily accomplished with Ravel by separating source files for `Module`s into different directories. Let's assume the following project structure:
584
585```
586app.js
587.ravelrc.json
588modules/
589 core/
590 my-module.js
591 util/
592 my-module.js
593```
594
595Then, import the `Module` directory as before, using `app.scan()`:
596
597*app.js*
598```js
599// ...
600const app = new Ravel();
601app.scan('./modules');
602// core/my-module can now be injected using @inject(core.my-module)!
603// util/my-module can now be injected using @inject(util.my-module)!
604```
605
606> Essentially, Ravel ignores the path you pass to `app.scan()` and uses any remaining path components to namespace `Module`s.
607
608#### Lifecycle Decorators
609> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Module.postinit)
610
611`Module`s are also a great place to define logic which should run at particular points during the Ravel lifecycle. Decorating a `Module` method with a lifecycle decorator appropriately results in that method firing exactly once at the specified time (with the exception of `@interval`, of course):
612
613```js
614const Ravel = require('ravel');
615const Module = Ravel.Module;
616const prelisten = Module.prelisten;
617
618@Module('init-module')
619class MyInitModule {
620 // ...
621 @prelisten
622 initDBTables () {
623 // ...
624 }
625}
626module.exports = MyInitModule;
627```
628
629There are currently six lifecycle decorators:
630
631- `@postinit` fires at the end of `Ravel.init()`
632- `@prelisten` fires at the beginning of `Ravel.listen()`
633- `@postlisten` fires at the end of `Ravel.listen()`
634- `@preclose` fires at the beginning of `Ravel.close()`
635- `@interval(1000)` fires at the end of `Ravel.listen()` and then repeatedly at the specified interval until `Ravel.close()`
636- `@koaconfig` fires during `Ravel.init()`, after Ravel is finished configuring the underlying `koa` app object with global middleware. Methods decorated with `@koaconfig` receive a reference to the underlying `koa` app object for customization. This decorator is meant for exceptional circumstances, since (unnecessarily) global middleware constitutes a hot path and can lead to inefficiency.
637
638### Ravel.Routes
639> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#routes)
640
641`Routes` are Ravel's abstraction of `koa`. They provide Ravel with a simple mechanism for registering `koa` routes, which should (generally) only be used for serving templated pages or static content (not for building RESTful APIs, for which `Ravel.Resource` is more applicable). Extend this abstract superclass to create a `Routes` module.
642
643Like `Module`s, `Routes` classes support dependency injection, allowing easy connection of application logic and web layers.
644
645Endpoints are created within a `Routes` class by creating an `async` method and then decorating it with [`@mapping`](http://raveljs.github.io/docs/latest/index.html#Routes.mapping). The `@mapping` decorator indicates the subpath for the route (concatenated with the base path passed to `super()` in the `constructor`), as well as the HTTP verb. The method handler accepts a single argument `ctx` which is a [koa context](http://koajs.com/#context). Savvy readers with `koa` experience will note that, within the handler, `this` refers to the instance of the Routes class (to make it easy to access injected `Module`s), and the passed `ctx` argument is a reference to the `koa` context.
646
647*routes/my-routes.js*
648```js
649const inject = require('ravel').inject;
650const Routes = require('ravel').Routes;
651const mapping = Routes.mapping; // Ravel decorator for mapping a method to an endpoint
652const before = Routes.before; // Ravel decorator for conneting middleware to an endpoint
653
654// you can inject your own Modules and npm dependencies into Routes
655@inject('koa-bodyparser', 'fs', 'custom-module')
656@Routes('/') // base path for all routes in this class
657class MyRoutes {
658 // The constructor for a `Routes` class must call `super()` with the base
659 // path for all routes within that class. Koa path parameters such as
660 // :something are supported.
661 constructor (bodyParser, fs, custom) {
662 this.bodyParser = bodyParser(); // make bodyParser middleware available
663 this.fs = fs;
664 this.custom = custom;
665 }
666
667 // will map to GET /app
668 @mapping(Routes.GET, 'app'); // Koa path parameters such as :something are supported
669 @before('bodyParser') // use bodyParser middleware before handler. Matches this.bodyParser created in the constructor.
670 async appHandler (ctx) {
671 ctx.status = 200;
672 ctx.body = '<!doctype html><html></html>';
673 // ctx is a koa context object.
674 // await on Promises and use ctx to create a body/status code for response
675 // throw a Ravel.Error to automatically set an error status code
676 }
677}
678
679module.exports = MyRoutes;
680```
681
682#### Registering Routes
683
684Much like `Module`s, `Routes` can be added to your Ravel application via `app.scan('path/to/routes')`:
685
686*app.js*
687```js
688// ...
689const app = new Ravel();
690app.scan('./routes');
691```
692
693### Ravel.Resource
694> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#resource)
695
696What might be referred to as a *controller* in other frameworks, a `Resource` module defines HTTP methods on an endpoint. `Resource`s also support dependency injection, allowing for the easy creation of RESTful interfaces to your `Module`-based application logic. Resources are really just a thin wrapper around `Routes`, using specially-named handler methods (`get`, `getAll`, `post`, `put`, `putAll`, `delete`, `deleteAll`) instead of `@mapping`. This convention-over-configuration approach makes it easier to write proper REST APIs with less code, and is recommended over ~~carefully chosen~~ `@mapping`s in a `Routes` class. Omitting any or all of the specially-named handler functions is fine, and will result in a `501 NOT IMPLEMENTED` status when that particular method/endpoint is requested. `Resource`s inherit all the properties, methods and decorators of `Routes`. See [core/routes](http://raveljs.github.io/docs/latest/index.html#routes) for more information. Note that `@mapping` does not apply to `Resources`.
697
698As with `Routes` classes, `Resource` handler methods are `async` functions which receive a [koa context](http://koajs.com/#context) as their only argument.
699
700*resources/person-resource.js*
701```js
702const inject = require('ravel').inject;
703const Resource = require('ravel').Resource;
704const before = Routes.before;
705
706// you can inject your own Modules and npm dependencies into Resources
707@inject('koa-bodyparser', 'fs', 'custom-module')
708@Resource('/person') // base path for all routes in this class
709class PersonResource {
710 constructor(convert, bodyParser, fs, custom) {
711 this.bodyParser = bodyParser(); // make bodyParser middleware available to @before within this class
712 this.fs = fs;
713 this.custom = custom;
714 }
715
716 // will map to GET /person
717 @before('bodyParser') // use bodyParser middleware before handler
718 async getAll (ctx) {
719 // ctx is a koa context object.
720 // await on Promises, and set ctx.body to create a body for response
721 // "OK" status code will be chosen automatically unless configured via ctx.status
722 // Extend and throw a Ravel.Error to send an error status code
723 }
724
725 // will map to GET /person/:id
726 async get (ctx) {
727 // can use ctx.params.id in here automatically
728 }
729
730 // will map to POST /person
731 async post (ctx) {}
732
733 // will map to PUT /person
734 async putAll (ctx) {}
735
736 // will map to PUT /person/:id
737 async put (ctx) {}
738
739 // will map to DELETE /person
740 async deleteAll (ctx) {}
741
742 // will map to DELETE /person/:id
743 async delete (ctx) {}
744}
745
746module.exports = PersonResource;
747```
748
749#### Registering Resources
750> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#resources)
751
752Much like `Module`s, `Resource`s can be added to your Ravel application via `app.scan('path/to/resources/directory')`:
753
754*app.js*
755```js
756// ...
757const app = new Ravel();
758// directory scanning!
759app.scan('./resources');
760```
761
762### Response Caching
763
764Ravel supports transparent response caching via the `@cache` decorator, which can be applied at both the class and method-level of `Resource`s and `Routes`. Method-level applications of `@cache` override class-level ones.
765
766*Method-level example*
767```js
768const Routes = require('ravel').Routes;
769const mapping = Routes.mapping;
770const cache = Routes.cache;
771
772@Routes('/')
773class MyRoutes {
774 @cache // method-level version only applies to this route
775 @mapping(Routes.GET, '/projects/:id')
776 async handler (ctx) {
777 // The response will automatically be cached when this handler is run
778 // for the first time, and then will be served instead of running the
779 // handler for as long as the cached response is available.
780 // If this handler throws an exception, then that response will not be cached.
781 }
782}
783```
784
785*Class-level example, with options*
786```js
787const Resource = require('ravel').Resource;
788const cache = Resource.cache;
789
790// class-level version applies to all routes in class, overriding any
791// method-level instances of the decorator.
792@cache({expire:60, maxLength: 100}) // expire is measured in seconds. maxLength in bytes.
793@Resource('/')
794class MyResource {
795 constructor (bodyParser) {
796 this.bodyParser = bodyParser();
797 }
798
799 async get(ctx) {
800 // The response will automatically be cached when this handler is run
801 // for the first time, and then will be served instead of running the
802 // handler for as long as the cached response is available (60 seconds).
803 }
804}
805```
806
807### Database Providers
808> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#databaseprovider)
809
810A `DatabaseProvider` is a lightweight wrapper for a `node` database library (such as [node-mysql](https://github.com/felixge/node-mysql)) which performs all the complex set-up and configuration of the library automatically, and registers simple parameters which you must `app.set` (such as the database host ip). The true purpose of `DatabaseProvider`s is to reduce boilerplate code between applications, as well as facilitate Ravel's transaction-per-request system (coming up [next](#transaction-per-request)). You may use as many different `DatbaseProvider`s as you wish in your application. Here's an example pulled from [`ravel-mysql-provider`](https://github.com/raveljs/ravel-mysql-provider):
811
812#### Example Setup
813
814*app.js*
815```javascript
816const app = new require('ravel')();
817const MySQLProvider = require('ravel-mysql-provider');
818new MySQLProvider(app, 'mysql');
819// ... other providers and parameters
820(async () => {
821 await app.init();
822})();
823// ... the rest of your Ravel app
824```
825
826#### Example Configuration
827
828*.ravelrc.json*
829```json
830{
831 "mysql options": {
832 "host": "localhost",
833 "port": 3306,
834 "user": "root",
835 "password": "a password",
836 "database": "mydatabase",
837 "idleTimeoutMillis": 5000,
838 "connectionLimit": 10
839 }
840}
841```
842
843#### List of Ravel `DatabaseProvider`s
844
845Ravel currently supports several `DatabaseProvider`s via external libraries.
846
847 - [`ravel-mysql-provider`](https://github.com/raveljs/ravel-mysql-provider)
848 - [`ravel-rethinkdb-provider`](https://github.com/raveljs/ravel-rethinkdb-provider)
849 - [`ravel-neo4j-provider`](https://github.com/raveljs/ravel-neo4j-provider)
850
851> If you've written a `DatabaseProvider` and would like to see it on this list, contact us or open an issue/PR against this README!
852
853### Transaction-per-request
854> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#transaction)
855
856The `@transaction` decorator is Ravel's way of automatically opening (and managing) database connections for a `Routes` or `Resource` handler method. It is available for import as `Routes.transaction` or `Resource.transaction`.
857
858When used at the method-level, `@transaction` opens connections for that specific handler method. When used at the class-level, it open connections for all handler methods in that `Route` or `Resource` class.
859
860Connections are available within the handler method as an object `ctx.transaction`, which contains connections as values and `DatabaseProvider` names as keys. Connections will be closed automatically when the endpoint responds (**do not close them yourself**), and will automatically roll-back changes if a `DatabaseProvider` supports it (generally a SQL-only feature).
861
862*resources/person-resource.js*
863```js
864const Resource = require('ravel').Resource;
865const transaction = Resource.transaction;
866
867@Resource('/person')
868class PersonResource {
869 // maps to GET /person/:id
870 @transaction('mysql') // this is the name exposed by ravel-mysql-provider
871 async get (ctx) {
872 // TIP: Don't write complex logic here. Pass ctx.transaction into
873 // a Module function which returns a Promise! This example is
874 // just for demonstration purposes.
875 ctx.body = await new Promise((resolve, reject) => {
876 // ctx.transaction.mysql is a https://github.com/felixge/node-mysql connection
877 ctx.transaction.mysql.query('SELECT 1', (err, rows) => {
878 if (err) return reject(err);
879 resolve(rows);
880 });
881 });
882 }
883}
884module.exports = PersonResource;
885```
886
887### Scoped Transactions
888> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#Module#db)
889
890Sometimes, you may need to open a transaction outside of a code path triggered by an HTTP request. Good examples of this might include database initialization at application start-time, or logic triggered by a websocket connection. In these cases, a `Module` class can open a `scoped` transaction using the names of the DatabaseProviders you are interested in, and an `async` function (scope) in which to use the connections. Scoped transactions only exist for the scope of the `async` function and are automatically cleaned up at the end of the function. It is best to view `Module.$db.scoped()` as an identical mechanism to `@transaction`, behaving in exactly the same way, with a slightly different API:
891
892*modules/database-initializer.js*
893```js
894const Module = require('ravel').Module;
895const autoinject = require('ravel').autoinject;
896const prelisten = Module.prelisten;
897
898@Module('db-init')
899@autoinject('$db','$log')
900class DatabaseInitializer {
901 @prelisten // trigger db init on application startup
902 doDbInit (ctx) {
903 const self = this;
904 // specify one or more providers to open connections to, or none
905 // to open connections to all known DatabaseProviders.
906 this.$db.scoped('mysql', async function (ctx) {
907 // this async function behaves like koa middleware,
908 // so feel free to await on promises!
909 await self.createTables(ctx.transaction.mysql);
910 await self.insertRows(ctx.transaction.mysql);
911 // notice that this.transaction is identical to ctx.transaction
912 // from @transaction! It's just a hash of open, named connections
913 // to the DatabaseProviders specified.
914 }).catch((err) => {
915 self.$log.error(err.stack);
916 process.exit(1); // in this case, we might want to kill our app if db init fails!
917 });
918 }
919
920 /**
921 * @returns {Promise}
922 */
923 createTables (mysqlConnection) { /* ... */ }
924
925 /**
926 * @returns {Promise}
927 */
928 insertRows (mysqlConnection) { /* ... */ }
929}
930
931module.exports = DatabaseInitializer;
932```
933
934### Authentication Providers
935> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#authenticationprovider)
936
937An `AuthenticationProvider` is a lightweight wrapper for a [Passport](https://github.com/jaredhanson/passport) provider library (such as [passport-github](https://github.com/jaredhanson/passport-github)) which performs all the complex set-up and configuration of the library automatically, and registers simple parameters which you must `app.set` (such as OAuth client ids and secrets). The purpose of `AuthenticationProvider`s is to reduce boilerplate code between applications, and simplify often complex `Passport` configuration code. You may use as many different `AuthenticationProvider`s as you wish in your application. Here's an example pulled from [`ravel-github-oauth2-provider`](https://github.com/raveljs/ravel-github-oauth2-provider):
938
939#### Example Setup
940
941*app.js*
942```javascript
943const app = new require('ravel')();
944const GitHubProvider = require('ravel-github-oauth2-provider');
945new GitHubProvider(app);
946// ... other providers and parameters
947(async () => {
948 await app.init();
949});
950// ... the rest of your Ravel app
951```
952
953#### Example Configuration
954
955*.ravelrc.json*
956```json
957{
958 "github auth callback url" : "http://localhost:8080",
959 "github auth path": "/auth/github",
960 "github auth callback path": "/auth/github/callback",
961 "github client id": "YOUR_CLIENT_ID",
962 "github client secret" : "YOUR_CLIENT_SECRET"
963}
964```
965
966You'll also need to implement an `@authconfig` module like this:
967
968*modules/authconfig.js*
969```js
970'use strict';
971
972const Ravel = require('ravel');
973const inject = Ravel.inject;
974const Module = Ravel.Module;
975const authconfig = Module.authconfig;
976
977@authconfig
978@Module('authconfig')
979@inject('user-profiles')
980class AuthConfig {
981 constructor (userProfiles) {
982 this.userProfiles = userProfiles;
983 }
984 serializeUser (profile) {
985 // serialize profile to session using the id field
986 return Promise.resolve(profile.id);
987 }
988 deserializeUser (id) {
989 // retrieve profile from database using id from session
990 return this.userProfiles.getProfile(id); // a Promise
991 }
992 verify (providerName, ...args) {
993 // this method is roughly equivalent to the Passport verify callback, but
994 // supports multiple simultaneous AuthenticationProviders.
995 // providerName is the name of the provider which needs credentials verified
996 // args is an array containing credentials, such as username/password for
997 // verification against your database, or a profile and OAuth tokens. See
998 // specific AuthenticationProvider library READMEs for more information about
999 // how to implement this method.
1000 }
1001}
1002
1003module.exports = AuthConfig;
1004```
1005
1006#### List of Ravel `AuthenticationProvider`s
1007
1008Ravel currently supports several `AuthenticationProvider`s via external libraries.
1009
1010 - [`ravel-github-oauth2-provider`](https://github.com/raveljs/ravel-github-oauth2-provider)
1011 - [`ravel-google-oauth2-provider`](https://github.com/raveljs/ravel-google-oauth2-provider)
1012
1013> If you've written an `AuthenticationProvider` and would like to see it on this list, contact us or open an issue/PR against this README!
1014
1015### Authentication
1016> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#authenticated)
1017
1018Once you've registered an `AuthenticationProvider`, requiring users to have an authenticated session to access a `Routes` or `Resource` endpoint is accomplished via the `@authenticated` decorator, which can be used at the class or method level:
1019
1020*Note: the @authenticated decorator works the same way on `Routes` and `Resource` classes/methods*
1021```js
1022const Routes = require('ravel').Routes;
1023const mapping = Routes.mapping;
1024const authenticated = Routes.authenticated;
1025
1026@authenticated // protect all endpoints in this Routes class
1027@Routes('/')
1028class MyRoutes {
1029 @authenticated({redirect: true}) // protect one endpoint specifically
1030 @mapping(Routes.GET, 'app')
1031 async handler (ctx) {
1032 // will redirect to app.get('login route') if not signed in
1033 }
1034}
1035```
1036
1037### WebSockets
1038> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/index.html#websockets)
1039
1040## Deployment and Scaling
1041
1042Ravel is designed for horizontal scaling, and helps you avoid common pitfalls when designing your node.js backend application. In particular:
1043
1044 - Session storage in [Redis](https://github.com/antirez/redis) is highly recommended. Without it, you cannot safely replicate your Ravel app. When deploying multiple replicas of your Ravel app, be sure to `app.set('redis host')` to point to an external, shared `redis` server.
1045 - The internal [koa](http://koajs.com/) application's `app.proxy` flag is set to `true`.
1046 - All Ravel dependencies are strictly locked (i.e. no use of `~` or `^` in `package.json`). This helps foster repeatability between members of your team, as well as between development/testing/production environments. Adherence to semver in the node ecosystem is unfortunately varied at best, so it is recommended that you follow the same practice in your app as well.
1047 - While it is possible to color outside the lines, Ravel provides a framework for developing **stateless** backend applications, where all stateful data is stored in external caches or databases.
1048
1049It is strongly encouraged that you containerize your Ravel app using an [Alpine-based docker container](https://hub.docker.com/r/mhart/alpine-node/), and then explore technologies such as [docker-compose](https://www.docker.com/products/docker-compose) or [kubernetes](http://kubernetes.io/) to appropriately scale out and link to (at least) the [official redis container](https://hub.docker.com/_/redis/). An example project with a reference `docker-compose` environment for Ravel can be found in the [starter project](https://github.com/raveljs/ravel-github-mariadb-starter).
1050
1051Ravel does not explicitly require [hiredis](https://github.com/redis/hiredis-node), but is is highly recommended that you install it alongside Ravel for improved redis performance.
1052
1053If you are looking for a good way to share `.ravelrc.json` configuration between multiple replicas of the same Ravel app, have a look at [ravel-etcd-config](https://github.com/raveljs/ravel-etcd-config) for easy distributed configuration.