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 |
|
6 | Ravel 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 |
|
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 |
|
42 |
|
43 | ## Introduction
|
44 |
|
45 | Ravel 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 |
|
52 | And 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 |
|
58 | Ravel 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 |
|
60 | Ravel 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 |
|
78 | Ravel 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 |
|
86 | If 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 |
|
92 | For more information about `Module`s, look at [Ravel.Module](#ravelmodule) below.
|
93 |
|
94 | *modules/cities.js*
|
95 | ```javascript
|
96 | const Ravel = require('ravel');
|
97 | const Error = Ravel.Error;
|
98 | const Module = Ravel.Module;
|
99 | const 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 | */
|
106 | class 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')
|
117 | class 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
|
143 | module.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
|
152 | const Ravel = require('ravel');
|
153 | const Module = Ravel.Module;
|
154 | const middleware = Module.middleware;
|
155 | class 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 |
|
170 | For more information about `Routes`, look at [Ravel.Routes](#ravelroutes) below.
|
171 |
|
172 | *routes/index.js*
|
173 | ```javascript
|
174 | const Ravel = require('ravel');
|
175 | const Routes = Ravel.Routes;
|
176 | const inject = Ravel.inject;
|
177 | const before = Routes.before; // decorator to chain middleware before an endpoint
|
178 | const 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.
|
181 | class 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
|
194 | module.exports = ExampleRoutes;
|
195 | ```
|
196 |
|
197 | ### Resources
|
198 |
|
199 | What 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 |
|
201 | For 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.
|
207 | const Ravel = require('ravel');
|
208 | const Resource = Ravel.Resource;
|
209 | const inject = Ravel.inject;
|
210 | const 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
|
215 | class 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
|
239 | module.exports = CitiesResource;
|
240 | ```
|
241 |
|
242 | ### Bringing it all together
|
243 |
|
244 | *app.js*
|
245 | ```javascript
|
246 | const app = new require('ravel')();
|
247 |
|
248 | // parameters like this can be supplied via a .ravelrc.json file
|
249 | app.set('keygrip keys', ['mysecret', 'anothersecret']);
|
250 |
|
251 | app.scan('./modules'); //import all Modules from a directory
|
252 | app.scan('./resources'); //import all Resources from a directory
|
253 | app.scan('./routes/index.js'); //import all Routes from a file
|
254 |
|
255 | // start it up!
|
256 | app.start();
|
257 | ```
|
258 |
|
259 | ### Decorator Transpilation
|
260 |
|
261 | Since 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
|
270 | const babelConfig = {
|
271 | 'retainLines': true,
|
272 | 'plugins': ['transform-decorators-legacy'] // add 'transform-async-to-generator' if you are using Node v6 instead of v7
|
273 | };
|
274 | gulp.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 |
|
283 | Check 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 🕮</small>](http://raveljs.github.io/docs/latest/)
|
293 |
|
294 | ### Ravel App
|
295 | > [<small>View API docs 🕮</small>](http://raveljs.github.io/docs/latest/index.html#ravel)
|
296 |
|
297 | A 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
|
301 | const Ravel = require('ravel');
|
302 | const 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 |
|
316 | Traditional `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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#registerParameter)
|
320 |
|
321 | Create managed parameters with `app.registerParameter()`:
|
322 |
|
323 | *app.js*
|
324 | ```js
|
325 | const Ravel = require('ravel');
|
326 | const app = new Ravel();
|
327 |
|
328 | // register a new optional parameter
|
329 | app.registerParameter('my optional parameter');
|
330 | // register a new required parameter
|
331 | app.registerParameter('my required parameter', true);
|
332 | // register a required parameter with a default value
|
333 | app.registerParameter('my third parameter', true, 'some value');
|
334 |
|
335 | (async () => {
|
336 | await app.init();
|
337 | await app.listen();
|
338 | })();
|
339 | ```
|
340 |
|
341 | Many 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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#set)
|
345 |
|
346 | Provide values via `app.set()`. Setting an unknown parameter will result in an `Error`.
|
347 |
|
348 | *app.js*
|
349 | ```js
|
350 | const Ravel = require('ravel');
|
351 | const app = new Ravel();
|
352 |
|
353 | // register a new optional parameter
|
354 | app.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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#get)
|
370 |
|
371 | Retrieve values via `app.get()`. Retrieving an unknown parameter will result in an `Error`.
|
372 |
|
373 | *app.js*
|
374 | ```js
|
375 | const Ravel = require('ravel');
|
376 | const app = new Ravel();
|
377 |
|
378 | // register a new parameter
|
379 | app.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 |
|
397 | Ravel has several core parameters:
|
398 |
|
399 | ```js
|
400 | // set initial keygrip keys for cookie signing (recommend at least 5):
|
401 | app.set('keygrip keys', ['my super secret key', 'another super secret key']);
|
402 |
|
403 | // these are optional (default values are shown):
|
404 | app.set('enable websockets', true); // set to false to disable websocket server
|
405 | app.set('redis host', undefined); // set to point to an external redis server (required for horizontal scaling).
|
406 | app.set('redis port', 6379);
|
407 | app.set('redis password', undefined);
|
408 | app.set('redis max retries', 10); // connection retries
|
409 | app.set('port', 8080); // port the app will run on
|
410 | app.set('session key', 'ravel.sid'); // the cookie name to use for sessions
|
411 | app.set('session max age', null); // session maxAge (default never expires)
|
412 | app.set('app route', '/'); // if you have a UI, this is the path users will be sent to when they are logged in
|
413 | app.set('login route', '/login'); // if users aren't logged in and you redirect them, this is where they'll be sent
|
414 | app.set('public directory', undefined); // if you want to statically serve a directory
|
415 | app.set('favicon path', undefined); // favicon middleware configuration
|
416 | ```
|
417 |
|
418 | #### .ravelrc.json
|
419 |
|
420 | To 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 |
|
429 | You 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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#Ravel.Error)
|
441 |
|
442 | This 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
|
446 | const Ravel = require('ravel');
|
447 | /**
|
448 | * Thrown when a user tries to POST something unexpected to /upload
|
449 | */
|
450 | class 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 🕮</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 |
|
462 | Here's a simple module:
|
463 |
|
464 | *modules/my-module.js*
|
465 | ```js
|
466 | const Ravel = require('ravel');
|
467 | const inject = Ravel.inject; // Ravel's dependency injection decorator
|
468 | const 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')
|
473 | class 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 |
|
491 | module.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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#inject)
|
496 |
|
497 | Ravel'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 |
|
502 | Ravel addresses this with the the [`@inject`](http://raveljs.github.io/docs/latest/index.html#inject) decorator:
|
503 |
|
504 | *modules/my-module.js*
|
505 | ```js
|
506 | const Ravel = require('ravel');
|
507 | const inject = Ravel.inject;
|
508 | const Module = Ravel.Module;
|
509 |
|
510 | @Module('mymodule')
|
511 | @inject('another-module') // inject another Module from your project without require()!
|
512 | class MyModule {
|
513 | constructor (another) { // @inject'd modules are available here as parameters
|
514 | this.another = another;
|
515 | }
|
516 | }
|
517 | module.exports = MyModule;
|
518 | ```
|
519 |
|
520 | The injection name of `another-module` is inferred from its filename, but can be overriden via the `@Module('custom-name')` decorator.
|
521 |
|
522 | If 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 |
|
529 | To 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
|
532 | const Ravel = require('ravel');
|
533 | const inject = Ravel.inject;
|
534 | const Module = Ravel.Module;
|
535 |
|
536 | @Module('mymodule')
|
537 | @inject('another-module', 'fs', 'moment', '$err') // anything that can be require()d can be @injected
|
538 | class MyModule {
|
539 | constructor (another, fs, moment, $err) {
|
540 | // ...
|
541 | }
|
542 | }
|
543 | module.exports = MyModule;
|
544 | ```
|
545 |
|
546 | To 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
|
550 | const Ravel = require('ravel');
|
551 | const inject = Ravel.inject;
|
552 | const Module = Ravel.Module;
|
553 |
|
554 | @Module('mymodule')
|
555 | @inject('another') // you can still mix using @inject!
|
556 | @autoinject('fs', 'moment', '$err')
|
557 | class 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 | }
|
567 | module.exports = MyModule;
|
568 | ```
|
569 |
|
570 | #### Core Services
|
571 |
|
572 | Several 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 |
|
583 | In 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 | ```
|
586 | app.js
|
587 | .ravelrc.json
|
588 | modules/
|
589 | core/
|
590 | my-module.js
|
591 | util/
|
592 | my-module.js
|
593 | ```
|
594 |
|
595 | Then, import the `Module` directory as before, using `app.scan()`:
|
596 |
|
597 | *app.js*
|
598 | ```js
|
599 | // ...
|
600 | const app = new Ravel();
|
601 | app.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 🕮</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
|
614 | const Ravel = require('ravel');
|
615 | const Module = Ravel.Module;
|
616 | const prelisten = Module.prelisten;
|
617 |
|
618 | @Module('init-module')
|
619 | class MyInitModule {
|
620 | // ...
|
621 | @prelisten
|
622 | initDBTables () {
|
623 | // ...
|
624 | }
|
625 | }
|
626 | module.exports = MyInitModule;
|
627 | ```
|
628 |
|
629 | There 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 🕮</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 |
|
643 | Like `Module`s, `Routes` classes support dependency injection, allowing easy connection of application logic and web layers.
|
644 |
|
645 | Endpoints 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
|
649 | const inject = require('ravel').inject;
|
650 | const Routes = require('ravel').Routes;
|
651 | const mapping = Routes.mapping; // Ravel decorator for mapping a method to an endpoint
|
652 | const 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
|
657 | class 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 |
|
679 | module.exports = MyRoutes;
|
680 | ```
|
681 |
|
682 | #### Registering Routes
|
683 |
|
684 | Much like `Module`s, `Routes` can be added to your Ravel application via `app.scan('path/to/routes')`:
|
685 |
|
686 | *app.js*
|
687 | ```js
|
688 | // ...
|
689 | const app = new Ravel();
|
690 | app.scan('./routes');
|
691 | ```
|
692 |
|
693 | ### Ravel.Resource
|
694 | > [<small>View API docs 🕮</small>](http://raveljs.github.io/docs/latest/index.html#resource)
|
695 |
|
696 | What 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 |
|
698 | As 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
|
702 | const inject = require('ravel').inject;
|
703 | const Resource = require('ravel').Resource;
|
704 | const 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
|
709 | class 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 |
|
746 | module.exports = PersonResource;
|
747 | ```
|
748 |
|
749 | #### Registering Resources
|
750 | > [<small>View API docs 🕮</small>](http://raveljs.github.io/docs/latest/index.html#Ravel#resources)
|
751 |
|
752 | Much 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 | // ...
|
757 | const app = new Ravel();
|
758 | // directory scanning!
|
759 | app.scan('./resources');
|
760 | ```
|
761 |
|
762 | ### Response Caching
|
763 |
|
764 | Ravel 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
|
768 | const Routes = require('ravel').Routes;
|
769 | const mapping = Routes.mapping;
|
770 | const cache = Routes.cache;
|
771 |
|
772 | @Routes('/')
|
773 | class 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
|
787 | const Resource = require('ravel').Resource;
|
788 | const 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('/')
|
794 | class 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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#databaseprovider)
|
809 |
|
810 | A `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
|
816 | const app = new require('ravel')();
|
817 | const MySQLProvider = require('ravel-mysql-provider');
|
818 | new 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 |
|
845 | Ravel 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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#transaction)
|
855 |
|
856 | The `@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 |
|
858 | When 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 |
|
860 | Connections 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
|
864 | const Resource = require('ravel').Resource;
|
865 | const transaction = Resource.transaction;
|
866 |
|
867 | @Resource('/person')
|
868 | class 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 | }
|
884 | module.exports = PersonResource;
|
885 | ```
|
886 |
|
887 | ### Scoped Transactions
|
888 | > [<small>View API docs 🕮</small>](http://raveljs.github.io/docs/latest/index.html#Module#db)
|
889 |
|
890 | Sometimes, 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
|
894 | const Module = require('ravel').Module;
|
895 | const autoinject = require('ravel').autoinject;
|
896 | const prelisten = Module.prelisten;
|
897 |
|
898 | @Module('db-init')
|
899 | @autoinject('$db','$log')
|
900 | class 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 |
|
931 | module.exports = DatabaseInitializer;
|
932 | ```
|
933 |
|
934 | ### Authentication Providers
|
935 | > [<small>View API docs 🕮</small>](http://raveljs.github.io/docs/latest/index.html#authenticationprovider)
|
936 |
|
937 | An `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
|
943 | const app = new require('ravel')();
|
944 | const GitHubProvider = require('ravel-github-oauth2-provider');
|
945 | new 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 |
|
966 | You'll also need to implement an `@authconfig` module like this:
|
967 |
|
968 | *modules/authconfig.js*
|
969 | ```js
|
970 | 'use strict';
|
971 |
|
972 | const Ravel = require('ravel');
|
973 | const inject = Ravel.inject;
|
974 | const Module = Ravel.Module;
|
975 | const authconfig = Module.authconfig;
|
976 |
|
977 | @authconfig
|
978 | @Module('authconfig')
|
979 | @inject('user-profiles')
|
980 | class 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 |
|
1003 | module.exports = AuthConfig;
|
1004 | ```
|
1005 |
|
1006 | #### List of Ravel `AuthenticationProvider`s
|
1007 |
|
1008 | Ravel 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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#authenticated)
|
1017 |
|
1018 | Once 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
|
1022 | const Routes = require('ravel').Routes;
|
1023 | const mapping = Routes.mapping;
|
1024 | const authenticated = Routes.authenticated;
|
1025 |
|
1026 | @authenticated // protect all endpoints in this Routes class
|
1027 | @Routes('/')
|
1028 | class 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 🕮</small>](http://raveljs.github.io/docs/latest/index.html#websockets)
|
1039 |
|
1040 | ## Deployment and Scaling
|
1041 |
|
1042 | Ravel 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 |
|
1049 | It 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 |
|
1051 | Ravel 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 |
|
1053 | If 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.
|