├── 01-Preface ├── 01-about.adoc ├── 02-upgrade-guide.adoc └── 03-Contribution-Guide.adoc ├── 02-Concept ├── 01-Request-Lifecycle.adoc ├── 02-ioc-container.adoc ├── 03-service-providers.adoc └── 05-ignitor.adoc ├── 03-getting-started ├── 01-installation.adoc ├── 02-Configuration.adoc └── 03-Directory-Structure.adoc ├── 04-Basics ├── 01-Routing.adoc ├── 02-Middleware.adoc ├── 03-Controllers.adoc ├── 04-Request.adoc ├── 05-Response.adoc ├── 06-Views.adoc ├── 07-Sessions.adoc ├── 08-Validation.adoc ├── 09-Error-Handling.adoc └── 10-Logger.adoc ├── 05-Security ├── 01-Getting-Started.adoc ├── 02-Authentication.adoc ├── 04-CORS.adoc ├── 05-CSRF-Protection.adoc ├── 06-Encryption.adoc └── 08-Shield-Middleware.adoc ├── 06-Digging-Deeper ├── 01-Ace-Commands.adoc ├── 02-Events.adoc ├── 03-Extending-the-Core.adoc ├── 03-File-Uploads.adoc ├── 04-File-Storage.adoc ├── 05-Helpers.adoc ├── 06-Internationalization.adoc ├── 07-Mails.adoc └── 08-Social-Authentication.adoc ├── 07-Database ├── 01-Getting-Started.adoc ├── 02-Query-Builder.adoc ├── 03-Migrations.adoc ├── 04-Seeding.adoc └── 05-Redis.adoc ├── 08-Lucid-ORM ├── 01-Getting-Started.adoc ├── 02-Hooks.adoc ├── 03-Traits.adoc ├── 04-Mutators.adoc ├── 05-Relationships.adoc └── 06-Serialization.adoc ├── 09-WebSockets ├── 01-Getting-Started.adoc ├── 02-Philosophy.adoc ├── 03-Server-API.adoc └── 04-Client-API.adoc ├── 10-testing ├── 01-Getting-Started.adoc ├── 02-HTTP-Tests.adoc ├── 03-browser-tests.adoc └── 05-Fakes.adoc ├── README.md └── recipes ├── 01-nginx-proxy.adoc ├── 02-dev-domains.adoc ├── 03-frontend-assets.adoc ├── 04-why-adonis-install.adoc ├── 05-https.adoc └── 06-number-guessing-game.adoc /01-Preface/01-about.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: About AdonisJs 3 | permalink: about 4 | category: Preface 5 | --- 6 | 7 | = About AdonisJs 8 | 9 | toc::[] 10 | 11 | AdonisJs is a Node.js MVC framework that runs on all major operating systems. It offers a stable ecosystem to write server-side web applications so you can focus on business needs over finalizing which package to choose or not. 12 | 13 | AdonisJs favors developer joy with a consistent and expressive API to build full-stack web applications or micro API servers. 14 | 15 | == Getting started 16 | There are no hard prerequisites for using AdonisJs, but having a conventional understanding of JavaScript, Async programming and Node.js is very helpful. 17 | 18 | Also, if you are new to JavaScript or unfamiliar with its recent progress in ES6, we recommend watching link:https://goo.gl/ox3uSc[Wes Bos's ES6 course, window="_blank"]. 19 | 20 | Finally, make sure to read through our link:installation[installation] guide, especially if this is your first time using AdonisJs. 21 | 22 | == Providers 23 | AdonisJs is a modular framework that consists of multiple service providers, the building blocks of AdonisJs applications. 24 | 25 | In theory, they are like any other npm module with some code on top to work smoothly with AdonisJs applications (for example, link:https://github.com/adonisjs/adonis-bodyparser[BodyParser] to parse the HTTP request body, or link:https://github.com/adonisjs/adonis-lucid[Lucid] which is a SQL ORM). 26 | 27 | == FAQ's 28 | Below is the list of frequently asked questions. If you think a common question is missing from the list, please create an issue link:https://github.com/adonisjs/docs[here]. 29 | 30 | [ol-spaced] 31 | 1. *How is AdonisJs different to Express or Koa?* 32 | + 33 | Express and Koa are routing libraries with a thin layer of middleware on top. They are great for several use cases but fall apart when projects start to grow. 34 | + 35 | Since your projects have their own standards and conventions, it may become harder to hire developers to work on them. As AdonisJs follows a set of standardized conventions, it should be easier to hire someone to work on existing AdonisJs apps. 36 | 37 | 2. *Is AdonisJs for monolithic apps?* 38 | + 39 | No. The AdonisJs Framework is a combination of multiple packages that gracefully integrate with the rest of your application. 40 | + 41 | The framework provides a robust link:ioc-container[dependency injection] layer leveraged by all official and 3rd party packages to offer functionality without manually wiring up each part of your app. 42 | -------------------------------------------------------------------------------- /01-Preface/02-upgrade-guide.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Upgrading from 4.0 3 | permalink: upgrade-guide 4 | category: Preface 5 | --- 6 | 7 | = Upgrading from 4.0 8 | 9 | toc::[] 10 | 11 | The 4.1 release contains a number of *bug fixes* and *API improvements* to keep the code base simple and less magical. Breaking changes were kept to a minimum, however, they could not be eliminated entirely. 12 | 13 | == Getting started 14 | 15 | The first step is to update all dependencies. 16 | 17 | We use link:https://www.npmjs.com/package/npm-check[npm-check] to pull the latest versions of packages: 18 | 19 | [source, bash] 20 | ---- 21 | > npm install -g npm-check 22 | ---- 23 | 24 | Run the following command to update dependencies interactively: 25 | 26 | [source, bash] 27 | ---- 28 | > npm-check -u 29 | ---- 30 | 31 | == Exception handling 32 | One of the most significant changes has been to the link:https://github.com/adonisjs/adonis-framework/issues/718[global exception handler, window="_blank"]. 33 | 34 | NOTE: If you never created the global exception handler, feel free to ignore this section. 35 | 36 | Make the following changes to the `app/Exceptions/Handler.js` file. 37 | 38 | 1. Ensure your exception handler extends the `BaseExceptionHandler`: 39 | + 40 | [source, js] 41 | ---- 42 | const BaseExceptionHandler = use('BaseExceptionHandler') 43 | 44 | class ExceptionHandler extends BaseExceptionHandler { 45 | } 46 | ---- 47 | 48 | 2. Call `super.handle` for exceptions you don't want to handle: 49 | + 50 | [source, js] 51 | ---- 52 | class ExceptionHandler extends BaseExceptionHandler { 53 | async handle (error, { response }) { 54 | if (error.name === 'UserNotFoundException') { 55 | // handle it yourself 56 | return 57 | } 58 | 59 | super.handle(...arguments) 60 | } 61 | } 62 | ---- 63 | 64 | 3. Lastly, you can remove `Exception.bind` calls from your codebase, since all exceptions will be routed to the global exception handler. 65 | 66 | == Routing 67 | 68 | ==== Route.url 69 | 70 | `Route.url` generates a fully qualified URL to a pre-registered route. 71 | 72 | Previously, `domain` was passed as a string literal. 73 | 74 | `domain` is now accepted as an object. 75 | 76 | Previously: 77 | [source, js] 78 | ---- 79 | Route.url('posts/:id', { id: 1 }, 'blog.adonisjs.com') 80 | ---- 81 | 82 | Now: 83 | [source, js] 84 | ---- 85 | Route.url('posts/:id', { id: 1 }, { domain: 'blog.adonisjs.com' }) 86 | ---- 87 | 88 | == Validator 89 | The validator provider now uses the latest version of link:https://indicative.adonisjs.com[Indicative, window="_blank"], causing the following breaking changes. 90 | 91 | ==== formatters 92 | There concept of named formatters no longer exists. 93 | 94 | If you want to use a pre-existing formatter, instead of passing by name, you must now pass by reference. 95 | 96 | Previously: 97 | [source, js] 98 | ---- 99 | const { validate } = use('Validator') 100 | validate(data, rules, messages, 'jsonapi') 101 | ---- 102 | 103 | Now: 104 | [source, js] 105 | ---- 106 | const { validate, formatters } = use('Validator') 107 | validate(data, rules, messages, formatters.JsonApi) 108 | ---- 109 | 110 | The same applies to route validators too. 111 | 112 | Previously: 113 | [source, js] 114 | ---- 115 | class StoreUser { 116 | get formatter () { 117 | return 'jsonapi' 118 | } 119 | } 120 | ---- 121 | 122 | Now: 123 | [source, js] 124 | ---- 125 | const { formatters } = use('Validator') 126 | 127 | class StoreUser { 128 | get formatter () { 129 | return formatters.JsonApi 130 | } 131 | } 132 | ---- 133 | 134 | ==== configure 135 | The new version of Indicative exposes the link:http://indicative.adonisjs.com/docs/api/configure[configure, window="_blank"] method to define library-wide defaults: 136 | 137 | [source, js] 138 | ---- 139 | const { formatters, configure } = use('Validator') 140 | 141 | configure({ 142 | FORMATTER: formatters.JsonApi 143 | }) 144 | ---- 145 | 146 | == Views 147 | 148 | ==== css 149 | 150 | The `css` global has been changed to `style`. The `css` global is no longer supported 151 | 152 | Earlier 153 | [source, edge] 154 | ---- 155 | {{ css('style') }} 156 | ---- 157 | 158 | Now 159 | [source, edge] 160 | ---- 161 | {{ style('style') }} 162 | ---- 163 | 164 | == Lucid 165 | Previously, date formatting was inconsistent with newly created records and existing records. 166 | 167 | This has been fixed in the newer release with a *small breaking change* (make sure to read the link:https://github.com/adonisjs/adonis-lucid/issues/245[related issue]). 168 | 169 | ==== dates 170 | The date fields no longer cast to `moment` instances on the model instance. 171 | 172 | Previously: 173 | [source, js] 174 | ---- 175 | const user = await User.find(1) 176 | user.created_at instanceof moment // true 177 | ---- 178 | 179 | Now: 180 | [source, js] 181 | ---- 182 | const user = await User.find(1) 183 | user.created_at instanceof moment // false 184 | ---- 185 | 186 | This change prevents you from mutating the date on the model instance directly and instead use the `castDates` hook to mutate the date when you serialize model properties. 187 | 188 | The `castDates` hook works as it did previously: 189 | 190 | [source, js] 191 | ---- 192 | class User extends Model { 193 | static castDates (field, value) { 194 | if (field === 'dob') { 195 | return `${value.fromNow(true)} old` 196 | } 197 | return super.formatDates(field, value) 198 | } 199 | } 200 | ---- 201 | 202 | == Goodies 203 | A number of bug fixes have been applied to keep the codebase reliable. 204 | 205 | Also, a handful of performance improvements have been implemented. 206 | 207 | ==== Validator 208 | Since Indicative is rewritten from the ground up, the new version is *2x faster* than it was previously. 209 | 210 | ==== Middleware 211 | Middleware is now resolved by the middleware parsing layer at the time of **booting** the app, instantiating a new instance of them for each request (previously, the **resolve** process was used for each request). 212 | 213 | ==== Better errors 214 | Errors will now appear nicely formatted in your terminal as shown below: 215 | 216 | image:https://pbs.twimg.com/media/DTHfXErU8AADIyQ.png[] 217 | -------------------------------------------------------------------------------- /01-Preface/03-Contribution-Guide.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contribution Guide 3 | category: Preface 4 | permalink: contribution-guide 5 | --- 6 | 7 | = Contribution Guide 8 | 9 | toc::[] 10 | 11 | Open Source projects are maintained and backed by a **vibrant community** of users and collaborators. 12 | 13 | We encourage you to participate actively in the development and the future of AdonisJs either by contributing to the source code, improving documentation, reporting potential bugs and/or testing new features. 14 | 15 | == Channels 16 | 17 | There are many ways to communicate with the AdonisJs team. 18 | 19 | 1. link:https://github.com/adonisjs[Github Repositories, window="_blank"]: Share bugs or create feature requests against the dedicated AdonisJs repositories. 20 | 2. link:https://forum.adonisjs.com[Forum, window="_blank"]: Ask questions, showcase your project and participate in the life of the AdonisJs Framework. 21 | 3. link:https://discord.gg/vDcEjq6[Discord, window="_blank"]: Join our Discord Server to chat instantly with others in the community. 22 | 4. link:https://twitter.com/adonisframework[Twitter, window="_blank"]: Stay in touch with the progress we make each day and be informed about awesome projects provided by the community. 23 | 24 | == Bug Reports 25 | 26 | Providing a great bug report can seem simple at first glance. 27 | 28 | Always try to be descriptive and provide enough context and information to reproduce the issue. 29 | 30 | Bug reports may also be sent in the form of a pull request containing a failing test. 31 | 32 | 1. Provide a clear title and description of the issue. 33 | 2. Share the version of the framework you are on. 34 | 3. Add as much code samples as you can to demonstrate the issue. You can also provide a complete repository to reproduce the issue quickly. 35 | 36 | Remember, bug reports don't mean the bug will be fixed within an hour! 37 | 38 | It serves yourself and the broader community to start on the path of fixing the problem before reporting it. 39 | 40 | == Coding Style 41 | 42 | Unfortunately, JavaScript doesn’t have any official coding style. 43 | 44 | For this reason, AdonisJs uses link:https://standardjs.com/[StandardJS, window="_blank"] to help maintain a readable and consistent codebase. 45 | 46 | Please ensure you lint your code before sending pull requests to any AdonisJs repositories: 47 | 48 | [source, shell] 49 | ---- 50 | > npm run lint 51 | ---- 52 | 53 | == Documentation 54 | 55 | When adding a new feature to the core of the framework, be sure to create a parallel pull request in the link:https://github.com/adonisjs/docs[documentation repository] and link them. 56 | 57 | This will help the AdonisJs team understand your feature and keep the documentation updated. 58 | 59 | == Testing 60 | 61 | Before providing a pull request be sure to test the feature you are adding or create a regression test to show how a piece of code fails under specific circumstance while providing a bug fix. 62 | -------------------------------------------------------------------------------- /02-Concept/01-Request-Lifecycle.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request Lifecycle 3 | category: concept 4 | permalink: request-lifecycle 5 | --- 6 | 7 | = Request Lifecycle 8 | 9 | toc::[] 10 | 11 | == Introduction 12 | 13 | The Node.js platform is asynchronous. For beginners, it can be difficult to understand how it works, and how to handle its non-blocking approach to application flow. 14 | 15 | It can also be confusing differentiating the JavaScript you write for your front-end verses the JavaScript you write for your backend. They are the same in terms of syntax, but don’t run in the same run-time and context. 16 | 17 | Having an excellent high-level overview of the request lifecycle is a must-have. AdonisJs will feel less “magical”, and you will be more confident about building your applications. 18 | 19 | == Request Flow 20 | 21 | HTTP requests sent from a client are handled by the AdonisJs `Server` module, executing all **server level middleware** (for example, the `StaticFileMiddleware` that serves static files from the `public` directory). 22 | 23 | If the request isn’t terminated by server level middleware, the AdonisJs `Router` comes into play. It attempts to find a route that matches the URL requested. If `Router` cannot find a match, a `RouteNotFound` exception will be thrown. 24 | 25 | After finding a matching route, all **global middleware** are executed followed by any **named middleware** defined for the matched route. If no global or named middleware terminate the request, the matched route handler is called. 26 | 27 | You must terminate the request in your route handler. Once terminated, AdonisJs executes all **downstream middleware** and sends the response back to the client. 28 | 29 | ## HTTP Context 30 | 31 | AdonisJs provides a **HTTP Context** object to each route handler. 32 | 33 | This object contains everything you need to handle the request, like the `request` or `response` class, and can be easily extended via link:service-providers[Providers] or link:middleware[Middleware]: 34 | 35 | .start/routes.js 36 | [source, js] 37 | ---- 38 | Route.get('/', ({ request, response, view }) => { 39 | // request 40 | // response 41 | // view 42 | }) 43 | ---- 44 | 45 | Alternatively, you can use it directly instead of unpacking it: 46 | 47 | .start/routes.js 48 | [source, js] 49 | ---- 50 | Route.get('/', (ctx) => { 51 | // ctx.request 52 | // ctx.response 53 | // ctx.view 54 | // etc 55 | }) 56 | ---- 57 | -------------------------------------------------------------------------------- /02-Concept/02-ioc-container.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: ioc-container 3 | title: IoC Container 4 | category: concept 5 | --- 6 | 7 | = IoC Container 8 | 9 | toc::[] 10 | 11 | == Introduction 12 | Before understanding the **Inversion of Control (IoC)** container usage and benefits, we need to step back and understand the dependency management issues faced by large codebases. 13 | 14 | === Useless abstractions 15 | Quite often you run into a situation where you have to create useless abstractions for a library to manage its lifecycle. 16 | 17 | For example, to make sure the database is only connected once, you might move all database setup code into its own file (e.g. `lib/database.js`) and then `require` it everywhere inside your application: 18 | 19 | .lib/database.js 20 | [source, js] 21 | ---- 22 | const knex = require('knex') 23 | 24 | const connection = knex({ 25 | client: 'mysql', 26 | connection: {} 27 | }) 28 | 29 | module.exports = connection 30 | ---- 31 | 32 | Now, instead of requiring `knex` directly, you can require `lib/database.js` wherever it is needed. 33 | 34 | This is fine for a single dependency, but as the application grows, you'll find a number of these files growing inside your codebase, which is not ideal. 35 | 36 | === Dependency management 37 | One of the biggest problems large codebases suffer is the management of dependencies. 38 | 39 | Since dependencies don't know about each other, the developer has to wire them together somehow. 40 | 41 | Let's take the example of *sessions* stored in a *redis* database: 42 | 43 | [source, js] 44 | ---- 45 | class Session { 46 | constructor (redis) { 47 | // needs Redis instance 48 | } 49 | } 50 | 51 | class Redis { 52 | constructor (config) { 53 | // needs Config instance 54 | } 55 | } 56 | 57 | class Config { 58 | constructor (configDirectory) { 59 | // needs config directory path 60 | } 61 | } 62 | ---- 63 | 64 | As you can see, the `Session` class is dependant on the `Redis` class, the `Redis` class is dependant on the `Config` class, and so on. 65 | 66 | When using the `Session` class, we have to build its dependencies correctly: 67 | 68 | [source, js] 69 | ---- 70 | const config = new Config(configDirectory) 71 | const redis = new Redis(config) 72 | const session = new Session(redis) 73 | ---- 74 | 75 | As the dependency list may increase based on the requirements of the project, you can quickly imagine how this sequential instantiation process could begin to spin out of control! 76 | 77 | This is where the IoC container comes to the rescue, taking the responsibility of resolving your dependencies for you. 78 | 79 | === Painful testing 80 | When not using an IoC container, you have to come up with different ways to mock dependencies, or rely on libraries like link:http://sinonjs.org/[sinonjs, window="_blank"]. 81 | 82 | When using the IoC container, it is simple to link:testing-fakes#_self_implementing_fakes[create fakes], since all dependencies are resolved from the IoC container and not the file-system directly. 83 | 84 | == Binding dependencies 85 | Let's say we want to bind the `Redis` library inside the IoC container, making sure it knows how to compose itself. 86 | 87 | NOTE: There is no secret sauce to the IoC container. It is a relatively simple idea that controls the composition and resolution of modules, opening up a whole new world of possibilities. 88 | 89 | The first step is to create the actual `Redis` implementation and define all dependencies as `constructor` parameters: 90 | 91 | [source, js] 92 | ---- 93 | class Redis { 94 | constructor (Config) { 95 | const redisConfig = Config.get('redis') 96 | // connect to redis server 97 | } 98 | } 99 | 100 | module.exports = Redis 101 | ---- 102 | 103 | Note that `Config` is a constructor dependency and not a hardcoded `require` statement. 104 | 105 | Next, let's bind our `Redis` class to the IoC container as `My/Redis`: 106 | [source, js] 107 | ---- 108 | const { ioc } = require('@adonisjs/fold') 109 | const Redis = require('./Redis') 110 | 111 | ioc.bind('My/Redis', function (app) { 112 | const Config = app.use('Adonis/Src/Config') 113 | return new Redis(Config) 114 | }) 115 | ---- 116 | 117 | We can then use our `My/Redis` binding like so: 118 | 119 | [source, js] 120 | ---- 121 | const redis = ioc.use('My/Redis') 122 | ---- 123 | 124 | [ol-spaced] 125 | 1. The `ioc.bind` method takes two parameters: + 126 | - The name of the binding (e.g. `My/Redis`) 127 | - A *factory* function executed every time you access the binding, returning the final value for the binding 128 | 2. Since we're using the IoC container, we pull any existing bindings (e.g. `Config`) and pass it to the `Redis` class. 129 | 3. Finally, we return a new instance of `Redis`, configured and ready for use. 130 | 131 | === Singletons 132 | There's a problem with the `My/Redis` binding we just created. 133 | 134 | Each time we fetch it from the IoC container it returns a new `Redis` instance, in turn creating a new connection to the Redis server. 135 | 136 | To overcome this problem, the IoC container lets you define **singletons**: 137 | 138 | [source, js] 139 | ---- 140 | ioc.singleton('My/Redis', function (app) { 141 | const Config = app.use('Adonis/Src/Config') 142 | return new Redis(Config) 143 | }) 144 | ---- 145 | 146 | Instead of using `ioc.bind`, we use `ioc.singleton` instead which caches its first return value and re-uses it for future returns. 147 | 148 | == Resolving dependencies 149 | Simply call the `ioc.use` method and give it a namespace to resolve: 150 | 151 | [source, js] 152 | ---- 153 | const redis = ioc.use('My/Redis') 154 | ---- 155 | 156 | The global `use` method can also be used: 157 | 158 | [source, js] 159 | ---- 160 | const redis = use('My/Redis') 161 | ---- 162 | 163 | The steps performed when resolving a dependency from the IoC container are: 164 | 165 | 1. Look for a registered fake. 166 | 2. Next, find the actual binding. 167 | 3. Look for an alias, and if found, repeat the process using the actual binding name. 168 | 4. Resolve as an autoloaded path. 169 | 5. Fallback to the Node.js native `require` method. 170 | 171 | === Aliases 172 | Since IoC container bindings must be unique, we use the following pattern for binding names: `Project/Scope/Module`. 173 | 174 | Breaking it down, using `Adonis/Src/Config` as an example: 175 | 176 | [ul-spaced] 177 | - `Adonis` is the **Project** name (could be your company name instead) 178 | - `Src` is the **Scope**, since this binding is part of the core (for 1st party packages, we use the `Addon` keyword) 179 | - `Config` is the actual **Module** name 180 | 181 | As it's sometimes difficult to remember and type full namespaces, the IoC container allows you to define *aliases* for them. 182 | 183 | Aliases are defined inside the `start/app.js` file's `aliases` object. 184 | 185 | NOTE: AdonisJs pre-registers aliases for inbuilt modules like `Route`, `View`, `Model` and so on. However, you can always override them as shown below. 186 | 187 | .start/app.js 188 | [source, js] 189 | ---- 190 | aliases: { 191 | MyRoute: 'Adonis/Src/Route' 192 | } 193 | ---- 194 | 195 | [source, js] 196 | ---- 197 | const Route = use('MyRoute') 198 | ---- 199 | 200 | === Autoloading 201 | Instead of only binding dependencies to the IoC container, you can also define a directory to be autoloaded by the IoC container. 202 | 203 | *Don't worry*, it does not load all the files from the directory, but instead considers the directory paths as part of the dependency resolution process. 204 | 205 | For example, the `app` directory of AdonisJs is autoloaded under the `App` namespace, which means you can require all files from the `app` directory without typing relative paths. 206 | 207 | For example: 208 | 209 | .app/Services/Foo.js 210 | [source, js] 211 | ---- 212 | class FooService { 213 | } 214 | 215 | module.exports = FooService 216 | ---- 217 | 218 | Can be required as: 219 | 220 | .app/Controllers/Http/UserController.js 221 | [source, js] 222 | ---- 223 | const Foo = use('App/Services/Foo') 224 | ---- 225 | 226 | Without autloading, it would have to be required as `require('../../Services/Foo')`. 227 | 228 | So think of autoloading as a more readable and consistent way to require files. 229 | 230 | Also, you can easily define link:testing-fakes[fakes] for them too. 231 | 232 | == FAQ's 233 | 234 | [ol-spaced] 235 | 1. *Do I have to bind everything inside the IoC container?* + 236 | No. IoC container bindings should only be used when you want to abstract the setup of a library/module to its own thing. Also, consider using link:service-providers[service providers] when you want to distribute dependencies and want them to play nice with the AdonisJs ecosystem. 237 | 238 | 2. *How do I mock bindings?* + 239 | There's no need to mock bindings since AdonisJs allows you to implement *fakes*. Learn more about fakes link:testing-fakes[here]. 240 | 241 | 3. *How do I wrap an npm module as a service provider?* + 242 | link:service-providers[Here's] the complete guide for that. 243 | -------------------------------------------------------------------------------- /02-Concept/03-service-providers.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: service-providers 3 | title: Service Providers 4 | category: concept 5 | --- 6 | 7 | = Service Providers 8 | 9 | toc::[] 10 | 11 | So far we learned link:ioc-container#_binding_dependencies[how to bind] dependencies to the IoC container. 12 | 13 | In this guide, we take a step further to learn about *service providers* and how to distribute packages that play nicely with the AdonisJs ecosystem. 14 | 15 | == Introduction 16 | We know that `ioc.bind` method can be used to register bindings. However, we're yet to define where to call this method. 17 | 18 | This is where *service providers* come into play. Service providers are pure ES6 classes with lifecycle methods that are used to register and bootstrap bindings. 19 | 20 | For example: 21 | 22 | [source, js] 23 | ---- 24 | const { ServiceProvider } = require('@adonisjs/fold') 25 | 26 | class MyProvider extends ServiceProvider { 27 | register () { 28 | // register bindings 29 | } 30 | 31 | boot () { 32 | // optionally do some initial setup 33 | } 34 | } 35 | 36 | module.exports = MyProvider 37 | ---- 38 | 39 | 1. The `register` method is used to register bindings, and you should never try to use any other binding inside this method. 40 | 2. The `boot` method is called when all providers have been registered, and is the right place to use existing bindings to bootstrap application state. 41 | 42 | For example, adding a view global: 43 | 44 | [source, js] 45 | ---- 46 | boot () { 47 | const View = this.app.use('Adonis/Src/View') 48 | View.global('time', () => new Date().getTime()) 49 | } 50 | ---- 51 | 52 | == npm package as a service provider 53 | Let's see how we can wrap an existing npm package in a service provider. 54 | 55 | NOTE: Do not wrap packages like `lodash` in a servive provider since it can be used directly and doesn't require any setup process. 56 | 57 | All application specific providers live inside the `providers` directory in the root of your app: 58 | 59 | === Directory structure 60 | [source, bash] 61 | ---- 62 | ├── app 63 | └── providers 64 | └── Queue 65 | └── index.js 66 | └── Provider.js 67 | └── start 68 | ---- 69 | 70 | === Principles 71 | We are going to wrap link:https://github.com/bee-queue/bee-queue[bee-queue, window="_blank"] as a provider. 72 | 73 | Here is a set of principles we want to follow: 74 | 75 | 1. The end-user should not have to worry about configuring the queue provider. 76 | 2. All configuration should live inside the `config/queue.js` file. 77 | 3. It should be simple enough to create new queues with a different configuration. 78 | 79 | === Implementation 80 | Let's implement the wrapper inside the `providers/Queue/index.js` file: 81 | 82 | .providers/Queue/index.js 83 | [source, js] 84 | ---- 85 | 'use strict' 86 | 87 | const BeeQueue = require('bee-queue') 88 | 89 | class Queue { 90 | constructor (Config) { 91 | this.Config = Config 92 | this._queuesPool = {} 93 | } 94 | 95 | get (name) { 96 | /** 97 | * If there is an instance of queue already, then return it 98 | */ 99 | if (this._queuesPool[name]) { 100 | return this._queuesPool[name] 101 | } 102 | 103 | /** 104 | * Read configuration using Config 105 | * provider 106 | */ 107 | const config = this.Config.get(`queue.${name}`) 108 | 109 | /** 110 | * Create a new queue instance and save it's 111 | * reference 112 | */ 113 | this._queuesPool[name] = new BeeQueue(name, config) 114 | 115 | /** 116 | * Return the instance back 117 | */ 118 | return this._queuesPool[name] 119 | } 120 | } 121 | 122 | module.exports = Queue 123 | ---- 124 | 125 | The above class has only one method called `get`, which returns an instance of the queue for a given *queue name*. 126 | 127 | The steps performed by the `get` method are: 128 | 129 | 1. Look for an instance of a given queue name. 130 | 2. If an instance does not exist, read the configuration using the *Config Provider*. 131 | 3. Create a new `bee-queue` instance and store inside an object for future use. 132 | 4. Finally, return the instance. 133 | 134 | The `Queue` class is pure since it does not have any hard dependencies on the framework and instead relies on *Dependency Injection* to provide the *Config Provider*. 135 | 136 | === Service provider 137 | Now let's create a service provider that does the instantiation of this class and binds it to the IoC container. 138 | 139 | The code lives inside `providers/Queue/Provider.js`: 140 | 141 | .providers/Queue/Provider.js 142 | [source, js] 143 | ---- 144 | const { ServiceProvider } = require('@adonisjs/fold') 145 | 146 | class QueueProvider extends ServiceProvider { 147 | register () { 148 | this.app.singleton('Bee/Queue', () => { 149 | const Config = this.app.use('Adonis/Src/Config') 150 | return new (require('.'))(Config) 151 | }) 152 | } 153 | } 154 | 155 | module.exports = QueueProvider 156 | ---- 157 | 158 | Note that `this.app` is a reference to the `ioc` object, which means instead of calling `ioc.singleton`, we call `this.app.singleton`. 159 | 160 | Finally, we need to register this provider like any other provider inside the `start/app.js` file: 161 | 162 | .start/app.js 163 | [source, js] 164 | ---- 165 | const providers = [ 166 | path.join(__dirname, '..', 'providers', 'Queue/Provider') 167 | ] 168 | ---- 169 | 170 | Now, we can call `use('Bee/Queue')` inside any file in your application to use it: 171 | 172 | [source, js] 173 | ---- 174 | const Queue = use('Bee/Queue') 175 | 176 | Queue 177 | .get('addition') 178 | .createJob({ x: 2, y: 3 }) 179 | .save() 180 | ---- 181 | 182 | == Distributing as a package 183 | The xref:_npm_package_as_a_service_provider[bee queue] provider we created lives in the same project structure. However, we can extract it into its own package. 184 | 185 | Let's create a new directory with the following directory structure: 186 | 187 | [source, bash] 188 | ---- 189 | └── providers 190 | └── QueueProvider.js 191 | ├── src 192 | └── Queue 193 | └── index.js 194 | └── package.json 195 | ---- 196 | 197 | All we did is move the actual `Queue` implementation to the `src` directory and rename the provider file to `QueueProvider.js`. 198 | 199 | Also, we have to make the following changes: 200 | 201 | 1. Since `Queue/index.js` is in a different directory, we need to tweak the reference to this file inside our service provider. 202 | 2. Rename the `Bee/Queue` namespace to a more suited namespace, which has fewer chances of collision. For example, when creating this provider for AdonisJs, we will name it as `Adonis/Addons/Queue`. 203 | 204 | .providers/QueueProvider.js 205 | [source, js] 206 | ---- 207 | const { ServiceProvider } = require('@adonisjs/fold') 208 | 209 | class QueueProvider extends ServiceProvider { 210 | register () { 211 | this.app.singleton('Adonis/Addons/Queue', () => { 212 | const Config = this.app.use('Adonis/Src/Config') 213 | return new (require('../src/Queue'))(Config) 214 | }) 215 | } 216 | } 217 | 218 | module.exports = QueueProvider 219 | ---- 220 | 221 | NOTE: Do not include `@adonisjs/fold` as a dependency for your provider, as this should be installed by the main application only. For testing, you can install it as a *dev dependency*. 222 | 223 | === Writing provider tests 224 | AdonisJs officially uses link:https://github.com/thetutlage/japa[japa, window="_blank"] to write provider tests, though you can use any testing engine you want. 225 | 226 | Setting up japa is straightforward: 227 | 228 | [source, bash] 229 | ---- 230 | > npm i --save-dev japa 231 | ---- 232 | 233 | Create the tests inside the `test` directory: 234 | 235 | [source, bash] 236 | ---- 237 | > mkdir test 238 | ---- 239 | 240 | The tests can be executed by running the test file using the `node` command: 241 | 242 | [source, bash] 243 | ---- 244 | > node test/example.spec.js 245 | ---- 246 | 247 | To run all your tests together, you can use `japa-cli`: 248 | 249 | [source, js] 250 | ---- 251 | > npm i --save-dev japa-cli 252 | ---- 253 | 254 | And run all tests via: 255 | 256 | [source, bash] 257 | ---- 258 | > ./node_modules/.bin/japa 259 | ---- 260 | 261 | == FAQ's 262 | [ol-spaced] 263 | 1. *Why not install `@adonisjs/fold` as a dependency?* + 264 | This requirement is so the main application version of `@adonisjs/fold` is always installed for your provider to use. Otherwise, each provider will end up shipping its own version of the AdonisJS IoC container. If you have ever worked with gulp, they also link:https://github.com/gulpjs/gulp/blob/master/docs/writing-a-plugin/guidelines.md[recommend] (p:14) not to install gulp as a dependency when creating plugins. 265 | -------------------------------------------------------------------------------- /02-Concept/05-ignitor.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: ignitor 3 | title: Ignitor 4 | category: concept 5 | --- 6 | 7 | = Ignitor 8 | 9 | toc::[] 10 | 11 | link:https://github.com/adonisjs/adonis-ignitor[Ignitor, window="_blank"] powers the bootstrapping of an AdonisJs application. 12 | 13 | In this guide, we learn about some of the features and functionality offered by the Ignitor package to manage our code. 14 | 15 | == Hooks 16 | Ignitor exposes a number of hooks to customize your application behavior. 17 | 18 | These hooks are registered inside the `start/hooks.js` file. Feel free to create this file if it does not already exist. 19 | 20 | Here's an example of how to use `hooks.after` to register a view global *after* all providers have booted: 21 | 22 | .start/hooks.js 23 | [source, js] 24 | ---- 25 | const { hooks } = require('@adonisjs/ignitor') 26 | 27 | hooks.after.providersBooted(() => { 28 | const View = use('View') 29 | View.global('time', () => new Date().getTime()) 30 | }) 31 | ---- 32 | 33 | Similar to `hooks.after`, you can also use `hooks.before` to register application logic *before* a hook occurs. 34 | 35 | Below is the list of available hooks: 36 | 37 | [options="header", cols="10%, 90%"] 38 | |==== 39 | | Hook Event | Description 40 | | providersRegistered | Before/after all providers have registered 41 | | providersBooted | Before/after all providers have booted 42 | | preloading | Before/after preloading registered files 43 | | httpServer | Before/after HTTP server has started 44 | | aceCommand | Before/after ace command is executed 45 | |==== 46 | 47 | == Preloading files 48 | Ignitor makes it easy to preload files after the HTTP server has started. 49 | 50 | To do so, modify the `server.js` file and add the `preLoad` method: 51 | 52 | [source, js] 53 | ---- 54 | new Ignitor(require('@adonisjs/fold')) 55 | .appRoot(__dirname) 56 | .preLoad('start/fire-zombies') 57 | .fireHttpServer() 58 | .catch(console.error) 59 | ---- 60 | 61 | NOTE: The `preLoad` method accepts a relative application root path, or an absolute path to any JavaScript file. 62 | 63 | To load multiple files, call the `preLoad` method multiple times: 64 | 65 | [source, js] 66 | ---- 67 | new Ignitor(require('@adonisjs/fold')) 68 | .preLoad('') 69 | .preLoad('') 70 | // etc 71 | ---- 72 | 73 | == Ignitor methods 74 | Below is the list of methods available on the `ignitor` instance. 75 | 76 | ==== appRoot(location) 77 | Define the absolute path to the application root: 78 | 79 | [source, js] 80 | ---- 81 | ignitor 82 | .appRoot(__dirname) 83 | ---- 84 | 85 | ==== modulesRoot(location) 86 | Define the absolute path to the application's `node_modules` parent directory. 87 | 88 | By default, the path set in `appRoot()` is used: 89 | 90 | [source, js] 91 | ---- 92 | ignitor 93 | .modulesRoot(path.join(__dirname, '..')) 94 | ---- 95 | 96 | ==== appFile(location) 97 | Define the relative path to the app file. 98 | 99 | By default, the `start/app.js` file is used: 100 | 101 | [source, js] 102 | ---- 103 | ignitor 104 | .appFile('start/app.js') 105 | ---- 106 | 107 | ==== loadCommands() 108 | Instruct Ignitor to load ace providers and commands. 109 | 110 | This is done when running an ace command, however, you can also load commands when starting the HTTP server: 111 | 112 | [source, js] 113 | ---- 114 | ignitor 115 | .loadCommands() 116 | ---- 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /03-getting-started/01-installation.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | category: getting-started 4 | permalink: installation 5 | --- 6 | 7 | = Installation 8 | 9 | toc::[] 10 | 11 | Installing AdonisJs is a simple process and will only take a few minutes. 12 | 13 | == System Requirements 14 | 15 | The only dependencies of the framework are `Node.js` and `npm`. 16 | 17 | Ensure your versions of those tools match the following criteria: 18 | 19 | - Node.js >= 8.0.0 20 | - npm >= 3.0.0 21 | - git 22 | 23 | TIP: You can use tools like link:https://github.com/creationix/nvm[nvm, window="_blank"] to help manage multiple versions of Node.js and npm at the same time. 24 | 25 | == Installing AdonisJs 26 | 27 | === Via AdonisJs CLI 28 | 29 | AdonisJs CLI is a command line tool to help you install AdonisJs. 30 | 31 | Install it globally via `npm` like so: 32 | [source, bash] 33 | ---- 34 | > npm i -g @adonisjs/cli 35 | ---- 36 | 37 | TIP: You can also use `npx` to avoid installing the CLI globally. 38 | 39 | Make sure to add the `npm` system-wide `node_modules/.bin` directory to your `$PATH` to be able to access the installed binary. 40 | 41 | Once installed, you can use the `adonis new` command to create fresh installations of AdonisJs. 42 | 43 | For example, to create a new application called `yardstick`, simply: 44 | 45 | [source, bash] 46 | ---- 47 | > adonis new yardstick 48 | ---- 49 | 50 | [NOTE] 51 | ====== 52 | By default, the link:https://github.com/adonisjs/adonis-fullstack-app[fullstack blueprint, window="_blank"] is cloned from Github. You can customize this by using the options `--api-only` or `--slim`. 53 | 54 | You can also specify your own blueprint by using the option `--blueprint=`. 55 | ====== 56 | 57 | === Via Git 58 | 59 | Alternatively, you can use `git` directly to fetch our boilerplates: 60 | 61 | [source, bash] 62 | ---- 63 | # Fullstack 64 | > git clone --dissociate https://github.com/adonisjs/adonis-fullstack-app 65 | 66 | # API 67 | > git clone --dissociate https://github.com/adonisjs/adonis-api-app 68 | 69 | # Slim 70 | > git clone --dissociate https://github.com/adonisjs/adonis-slim-app 71 | ---- 72 | 73 | After cloning a boilerplate, install all dependencies by running `npm install`. 74 | 75 | == Serving the application 76 | 77 | Once the installation process has completed, you can `cd` into your new application directory and run the following command to start the HTTP Server: 78 | 79 | [source, bash] 80 | ---- 81 | > adonis serve --dev 82 | ---- 83 | 84 | This command starts the server on the port defined inside the root `.env` file. 85 | -------------------------------------------------------------------------------- /03-getting-started/02-Configuration.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | category: getting-started 4 | permalink: configuration-and-env 5 | --- 6 | 7 | = Configuration 8 | 9 | toc::[] 10 | 11 | == Config Provider 12 | 13 | The first step to having a maintainable codebase is to find a dedicated location for storing application configuration. 14 | 15 | AdonisJs uses the `config` directory, where all files are loaded at boot time. 16 | 17 | You can access configuration values via the **Config Provider**: 18 | 19 | [source, js] 20 | ---- 21 | const Config = use('Config') 22 | const appSecret = Config.get('app.appSecret') 23 | ---- 24 | 25 | Config values are fetched using `Config.get` which accepts a string argument referencing the key you want in the form `fileName.key`. 26 | 27 | You can fetch nested config values using dot notation: 28 | 29 | [source, js] 30 | ---- 31 | // Example of a configuration file e.g. database.js 32 | { 33 | mysql: { 34 | host: '127.0.0.1', 35 | }, 36 | } 37 | 38 | // You can retrieve it like so... 39 | Config.get('database.mysql.host') 40 | ---- 41 | 42 | If you aren't sure that a key is defined in your configuration, you can supply a second argument that will be returned as the default value: 43 | 44 | [source, js] 45 | ---- 46 | Config.get('database.mysql.host', '127.0.0.1') 47 | ---- 48 | 49 | If you want to change the in-memory configuration values, use `Config.set`: 50 | 51 | [source, js] 52 | ---- 53 | Config.set('database.mysql.host', 'db.example.com') 54 | ---- 55 | 56 | NOTE: `Config.set` will only change the value **in-memory**. It will not write the new value to your config file. 57 | 58 | == Env Provider 59 | 60 | When building an application, you may want different configuration based upon the environment your code is running in. 61 | 62 | To fulfill this requirement, AdonisJs uses the link:https://github.com/motdotla/dotenv[dotenv, window="_blank"] library. 63 | 64 | Inside the root of every new AdonisJs project, you'll find an `.env.example` file. 65 | If you used AdonisJs CLI to install your application, this file will be automatically duplicated as `.env`. Otherwise, you should copy it manually. 66 | 67 | WARNING: The `.env` file should never be committed to your source control or shared with other people. 68 | 69 | The `.env` file has a simple `key=value` syntax: 70 | 71 | ..env 72 | [source, env] 73 | ---- 74 | APP_SECRET=F7op5n9vx1nAkno0DsNgZm5vjNXpOLIq 75 | DB_HOST=127.0.0.1 76 | DB_USER=root 77 | ---- 78 | 79 | You can access env values using the **Env Provider**: 80 | 81 | [source, js] 82 | ---- 83 | const Env = use('Env') 84 | const appSecret = Env.get('APP_SECRET') 85 | ---- 86 | 87 | Like the **Config Provider**, you can provide a default value as the second argument: 88 | 89 | [source, js] 90 | ---- 91 | Env.get('DB_USER', 'root') 92 | ---- 93 | 94 | `Env.get` always return a `string`. If you want an `Env` value to act as boolean, you will need to check it via a conditional equality statement, like so: 95 | 96 | [source, js] 97 | ---- 98 | const myBoolean = Env.get('MY_BOOLEAN') === 'true' 99 | ---- 100 | 101 | === Raising errors if a required environment variable does not exist 102 | 103 | When you have an environment variable that is required for running your application, you may use `Env.getOrFail()` to throw an error if the required variable is not set. 104 | 105 | TIP: If you want your application to fail quickly at boot time when an environment variable is missing, **only limit access to environment variables from inside your configuration files**, and do not to use the Env Provider anywhere else in the app. 106 | 107 | [source, js] 108 | ---- 109 | const Env = use('Env') 110 | // Throw "Make sure to define APP_SECRET inside .env file." 111 | Env.getOrFail('APP_SECRET') 112 | ---- 113 | 114 | === Location of the .env file 115 | 116 | You may want to load a different `.env` file. 117 | 118 | This can be done by using the `ENV_PATH` environment variable: 119 | 120 | [source, bash] 121 | ---- 122 | > ENV_PATH=/user/.env adonis serve 123 | ---- 124 | 125 | === Disabling the .env file 126 | 127 | You may want to directly use the environment variables on your server instead of relaying to a file. 128 | 129 | This can be done by using the `ENV_SILENT` environment variable: 130 | 131 | [source, bash] 132 | ---- 133 | > ENV_SILENT=true adonis serve 134 | ---- 135 | 136 | === Testing Environment 137 | 138 | If you are starting your application with `NODE_ENV` set to `testing`, AdonisJs will load your `.env.testing` file and merge its values over your `.env` file. 139 | 140 | This is useful to set different credentials when testing your codebase. 141 | -------------------------------------------------------------------------------- /03-getting-started/03-Directory-Structure.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Directory Structure 3 | category: getting-started 4 | permalink: folder-structure 5 | --- 6 | 7 | = Directory Structure 8 | 9 | toc::[] 10 | 11 | The AdonisJs directory structure may feel overwhelming at first glance since there are a handful of pre-configured directories. 12 | 13 | Gradually you'll understand the benefit of separating your entities into multiple directories, keeping your code maintainable and easy to search. 14 | 15 | A standard AdonisJs installation looks something like so: 16 | ++++ 17 |
 18 | .
 19 | ├── app/
 20 |   ├── ...
 21 | ├── config/
 22 |   ├── app.js
 23 |   ├── auth.js
 24 |   └── ...
 25 | ├── database/
 26 |   ├── migrations/
 27 |   ├── seeds/
 28 |   └── factory.js
 29 | ├── public/
 30 | ├── resources/
 31 |   ├── ...
 32 |   └── views/
 33 | ├── storage/
 34 | ├── start/
 35 |   ├── app.js
 36 |   ├── kernel.js
 37 |   └── routes.js
 38 | ├── test/
 39 | ├── ace
 40 | ├── server.js
 41 | └── package.json
 42 | 
 43 | 
44 | ++++ 45 | 46 | == Root Directories 47 | 48 | === app 49 | 50 | The `app` directory is the home of your application logic. 51 | 52 | It's autoloaded under the namespace `App`. 53 | 54 | === config 55 | 56 | The `config` directory is used to define the configuration of your application. 57 | 58 | AdonisJs ships with a number of config files, but feel free to create your own. 59 | 60 | link:configuration-and-env[Read more about configuration]. 61 | 62 | === database 63 | 64 | The `database` directory is used to store all database related files. 65 | 66 | link:database[Read more about databases]. 67 | 68 | === public 69 | 70 | The `public` directory is used to serve static assets over HTTP. 71 | 72 | This directory is mapped to the root of your website: 73 | 74 | [source, html] 75 | ---- 76 | 77 | 78 | ---- 79 | 80 | === resources 81 | 82 | The `resources` directory is used to store presentational files for your application like view templates, LESS/SASS files, uncompiled JavaScript, or even images. 83 | 84 | === start 85 | 86 | The `start` directory is used to store files that are loaded at the boot of your application. 87 | By default, you'll find `app.js`, `kernel.js` and `routes.js`. 88 | 89 | === test 90 | 91 | The `test` directory is used to store all your application tests. 92 | The testing package is not included by default – you can install it following the instructions defined link:testing[here]. 93 | 94 | == app Directories 95 | 96 | === app/Commands 97 | 98 | The `app/Commands` directory is used to store all your CLI commands. 99 | This directory is automatically created when you run `adonis make:command `. 100 | 101 | === app/Controllers 102 | 103 | The `app/Controllers` directory is used to store all your `Http` and `WebSocket` controllers. 104 | This directory is automatically created when you run `adonis make:controller `. 105 | 106 | === app/Exceptions 107 | 108 | The `app/Exceptions` directory is used to store the global exception handler and all of your custom exceptions. 109 | This directory is automatically created when you run `adonis make:ehandler` or `adonis make:exception `. 110 | 111 | === app/Listeners 112 | 113 | The `app/Listeners` directory is used to store all event listeners. 114 | This directory is automatically created when you run `adonis make:listener `. 115 | 116 | === app/Middleware 117 | 118 | The `app/Middleware` directory is used to store all your middleware. 119 | This directory is automatically created when you run `adonis make:middleware `. 120 | 121 | === app/Models 122 | 123 | The `app/Models` directory is used to store all your models. 124 | This directory is automatically created when you run `adonis make:model `. 125 | 126 | === app/Validators 127 | 128 | The `app/Validators` directory is used to store all your route validators. 129 | This directory is automatically created when you run `adonis make:validator ` (you need to have installed the link:validator[Validator Provider] to use this command). 130 | -------------------------------------------------------------------------------- /04-Basics/02-Middleware.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middleware 3 | category: basics 4 | permalink: middleware 5 | --- 6 | 7 | = Middleware 8 | 9 | toc::[] 10 | 11 | Middleware hook into the request lifecycle of your application. 12 | 13 | They are a set of functions executed in sequence and let you transform the request and/or the response. 14 | 15 | As an example, AdonisJs provides an `auth` middleware that verifies if the user of your application is authenticated. If the user is not authenticated an exception will be thrown and the request will never reach your route handler. 16 | 17 | == Creating Middleware 18 | 19 | To create a new middleware, use the `make:middleware` command: 20 | 21 | [source, bash] 22 | ---- 23 | > adonis make:middleware CountryDetector 24 | ---- 25 | 26 | This command will create a file in the `app/Middleware` folder with some boilerplate code. 27 | 28 | In our example `CountryDetector` middleware, we want to detect the country of the user from their ip address: 29 | 30 | .app/Middleware/CountryDetector.js 31 | [source, js] 32 | ---- 33 | 'use strict' 34 | 35 | const geoip = require('geoip-lite') 36 | 37 | class CountryDetector { 38 | async handle ({ request }, next) { 39 | const ip = request.ip() 40 | request.country = geoip.lookup(ip).country 41 | await next() 42 | } 43 | } 44 | 45 | module.exports = CountryDetector 46 | ---- 47 | 48 | In this example, we are using the library `geoip-lite` and adding the country of the user inside the `request` object of the link:request-lifecycle#_http_context[HTTP Context]. 49 | 50 | === Upstream & Downstream Middleware 51 | 52 | When creating your middleware, you'll need to decide if it runs before or after the request hits your route handler. 53 | 54 | This is done by defining the code before or after the middleware `handle` method's `await next()` call: 55 | 56 | .app/Middleware/UpstreamExample.js 57 | [source, js] 58 | ---- 59 | 'use strict' 60 | 61 | class UpstreamExample { 62 | async handle ({ request }, next) { 63 | // Code... 64 | await next() 65 | } 66 | } 67 | 68 | module.exports = UpstreamExample 69 | ---- 70 | 71 | To access the `response` object for downstream middleware, you'll need to unpack it from the passed link:request-lifecycle#_http_context[HTTP Context]: 72 | 73 | .app/Middleware/DownstreamExample.js 74 | [source, js] 75 | ---- 76 | 'use strict' 77 | 78 | class DownstreamExample { 79 | async handle ({ response }, next) { 80 | await next() 81 | // Code... 82 | } 83 | } 84 | 85 | module.exports = DownstreamExample 86 | ---- 87 | 88 | If you want, your middleware code can also run before **and** after the request hits your route handler: 89 | 90 | .app/Middleware/BeforeAndAfterExample.js 91 | [source, js] 92 | ---- 93 | 'use strict' 94 | 95 | class BeforeAndAfterExample { 96 | async handle ({ response }, next) { 97 | // Upstream code... 98 | await next() 99 | // Downstream code... 100 | } 101 | } 102 | 103 | module.exports = BeforeAndAfterExample 104 | ---- 105 | 106 | == Registering Middleware 107 | 108 | All middleware is registered inside the `start/kernel.js` file. 109 | 110 | Middleware is separated into 3 categories: **Server**, **Global** and **Named**. 111 | 112 | === Server Middleware 113 | 114 | Server middleware executes before the request reaches the AdonisJs routing system. This means if the requested route isn't registered, AdonisJs will still execute all middleware defined here: 115 | 116 | .start/kernel.js 117 | [source, js] 118 | ---- 119 | const serverMiddleware = [ 120 | 'Adonis/Middleware/Static', 121 | 'Adonis/Middleware/Cors', 122 | ] 123 | ---- 124 | 125 | Server middleware is generally used to serve static assets or handle CORS. 126 | 127 | === Global Middleware 128 | 129 | Global middleware executes after the requested route has been found: 130 | 131 | .start/kernel.js 132 | [source, js] 133 | ---- 134 | const globalMiddleware = [ 135 | 'Adonis/Middleware/BodyParser', 136 | ] 137 | ---- 138 | 139 | Global middleware executes in the sequence they were defined, so you must be careful when one middleware requires another. 140 | 141 | === Named Middleware 142 | 143 | Named middleware are assigned to a specific route or route group: 144 | 145 | .start/kernel.js 146 | [source, js] 147 | ---- 148 | const namedMiddleware = { 149 | auth: 'Adonis/Middleware/Auth', 150 | } 151 | ---- 152 | 153 | .start/routes.js 154 | [source, js] 155 | ---- 156 | Route.get(url, closure).middleware(['auth']) 157 | ---- 158 | 159 | Named middleware executes in the sequence they were defined against their assigned route. 160 | 161 | == Middleware Properties 162 | 163 | AdonisJs uses the link:https://www.npmjs.com/package/haye#pipe-expression[pipe expression, window="_blank"] to define middleware properties. 164 | 165 | For example, the `auth` middleware optionally accepts an authentication scheme as a middleware property: 166 | 167 | .start/routes.js 168 | [source, js] 169 | ---- 170 | // Use the Session Scheme for this route 171 | Route.post(url, closure).middleware(['auth:session']) 172 | 173 | // Use the JWT Scheme for this route 174 | Route.post(url, closure).middleware(['auth:jwt']) 175 | ---- 176 | 177 | You can also pass multiple props by chaining them with a comma: 178 | 179 | .start/routes.js 180 | [source, js] 181 | ---- 182 | Route.post(url, closure).middleware(['auth:session,jwt']) 183 | ---- 184 | 185 | Those properties are available as the third argument in your middleware `handle` method: 186 | 187 | [source, js] 188 | ---- 189 | async handle (context, next, properties) { 190 | // 191 | } 192 | ---- 193 | -------------------------------------------------------------------------------- /04-Basics/03-Controllers.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Controllers 3 | category: basics 4 | permalink: controllers 5 | --- 6 | 7 | = Controllers 8 | 9 | toc::[] 10 | 11 | While closures might be enough to handle route logic for small applications, when your application starts to grow, it becomes useful to organize application logic elsewhere. 12 | 13 | This is where Controllers come into play. 14 | 15 | Controllers attach to one or many routes, grouping related request handling logic into single files, and are the common point of interaction between your models, views and any other services you may need. 16 | 17 | NOTE: A controller's only job is to respond to a HTTP request. Do not use them internally by requiring them inside different files. 18 | 19 | == Creating Controllers 20 | 21 | To create a new controller, use the `make:controller` command: 22 | 23 | [source, bash] 24 | ---- 25 | # HTTP Controller 26 | > adonis make:controller User --type http 27 | 28 | # WS Controller 29 | > adonis make:controller User --type ws 30 | 31 | # Will use an Admin subfolder 32 | > adonis make:controller Admin/User 33 | ---- 34 | 35 | This command creates a boilerplate file in the `App/Controllers/{TYPE}` folder: 36 | 37 | .app/Controllers/Http/UserController.js 38 | [source, js] 39 | ---- 40 | 'use strict' 41 | 42 | class UserController { 43 | // 44 | } 45 | 46 | module.exports = UserController 47 | ---- 48 | 49 | TIP: Use the `--resource` flag to create a resourceful controller. 50 | 51 | == Using a Controller 52 | 53 | A controller can only be accessed from a route. 54 | 55 | This is done by referencing the controller as a **string** in your route definition: 56 | 57 | .app/routes.js 58 | [source, js] 59 | ---- 60 | Route.get(url, 'UserController.index') 61 | ---- 62 | 63 | The part before the dot is a reference to the controller file (e.g. `UserController`), and is by default namespaced to `App/Controllers/Http`. 64 | 65 | The part after the dot is the name of the method you want to call inside this controller (e.g. `index`). 66 | 67 | For example: 68 | 69 | .app/routes.js 70 | [source, js] 71 | ---- 72 | // app/Controllers/Http/UserController -> index() 73 | Route.get(url, 'UserController.index') 74 | 75 | // app/Controllers/Http/Admin/UserController -> store() 76 | Route.post(url, 'Admin/UserController.store') 77 | 78 | // app/MyOwnControllers/UserController -> index() 79 | Route.post(url, 'App/MyOwnControllers/UserController.index') 80 | ---- 81 | 82 | As your defined controller methods are route handlers, they will receive the link:request-lifecycle#_http_context[HTTP Context] as an argument: 83 | 84 | .app/Controllers/Http/UserController.js 85 | [source, js] 86 | ---- 87 | 'use strict' 88 | 89 | class UserController { 90 | index ({ request, response }) { 91 | // 92 | } 93 | } 94 | 95 | module.exports = UserController 96 | ---- 97 | -------------------------------------------------------------------------------- /04-Basics/07-Sessions.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: sessions 3 | title: Sessions 4 | category: basics 5 | --- 6 | 7 | = Sessions 8 | 9 | toc::[] 10 | 11 | AdonisJs has first-class session support with a variety of inbuilt drivers to efficiently manage and store sessions. 12 | 13 | In this guide, we learn how to configure and use these different session drivers. 14 | 15 | == Setup 16 | If the session provider is not already set up, follow the instructions below. 17 | 18 | First, run the `adonis` command to download the session provider: 19 | 20 | [source, bash] 21 | ---- 22 | > adonis install @adonisjs/session 23 | ---- 24 | 25 | The above command also creates the `config/session.js` file and displays a small set of instructions to help complete your set up. 26 | 27 | Next, register the session provider inside the `start/app.js` file: 28 | 29 | .start/app.js 30 | [source, js] 31 | ---- 32 | const providers = [ 33 | '@adonisjs/session/providers/SessionProvider' 34 | ] 35 | ---- 36 | 37 | Finally, register the session middleware inside the `start/kernel.js` file: 38 | 39 | .start/kernel.js 40 | [source, js] 41 | ---- 42 | const globalMiddleware = [ 43 | 'Adonis/Middleware/Session' 44 | ] 45 | ---- 46 | 47 | == Supported drivers 48 | Below is the list of drivers supported by the session provider. You can change the current driver inside the `config/session.js` file. 49 | 50 | NOTE: The Redis driver requires the `@adonisjs/redis` package (see the link:redis[Redis] section for installation instructions). 51 | 52 | [options="header", cols="10, 20, 70"] 53 | |==== 54 | | Name | Config key | Description 55 | | Cookie | cookie | Saves session values in encrypted cookies. 56 | | File | file | Saves session values in a file on a server (should not be used if you are running AdonisJs on multiple servers and behind a load balancer). 57 | | Redis | redis | Save in link:https://redis.io[Redis, window="_blank"] (ideal for scaling horizontally). 58 | |==== 59 | 60 | == Basic example 61 | The `session` object is passed as part of the link:request-lifecycle#_http_context[HTTP Context], just like the `request` and `response` objects. 62 | 63 | Here's a quick example of how to use sessions during the HTTP lifecycle: 64 | 65 | .start/routes.js 66 | [source, js] 67 | ---- 68 | Route.get('/', ({ session, response }) => { 69 | session.put('username', 'virk') 70 | response.redirect('/username') 71 | }) 72 | 73 | Route.get('/username', ({ session }) => { 74 | return session.get('username') // 'virk' 75 | }) 76 | ---- 77 | 78 | == Session methods 79 | Below is a list of all session methods and their example usages. 80 | 81 | ==== put(key, value) 82 | Add a key/value pair to the session store: 83 | 84 | [source, js] 85 | ---- 86 | session.put('username', 'virk') 87 | ---- 88 | 89 | ==== get(key, [defaultValue]) 90 | Return the value for a given key (accepts an optional default value): 91 | 92 | [source, js] 93 | ---- 94 | session.get('username') 95 | 96 | // default value 97 | session.get('username', 'defaultName') 98 | ---- 99 | 100 | ==== all 101 | Get everything back as an object from the session store: 102 | 103 | [source, js] 104 | ---- 105 | session.all() 106 | ---- 107 | 108 | ==== increment(key, [steps]) 109 | Increment the value for a given key (ensure the previous value is a number): 110 | 111 | [source, js] 112 | ---- 113 | session.increment('counter') 114 | 115 | // increment by 5 116 | session.increment('counter', 5) 117 | ---- 118 | 119 | ==== decrement(key, [steps]) 120 | Decrement the value for a given key (ensure the previous value is a number): 121 | 122 | [source, js] 123 | ---- 124 | session.decrement('counter') 125 | 126 | // decrement by 2 127 | session.decrement('counter', 2) 128 | ---- 129 | 130 | ==== forget(key) 131 | Remove a key/value pair from the session store: 132 | [source, js] 133 | ---- 134 | session.forget('username') 135 | ---- 136 | 137 | ==== pull(key, [defaultValue]) 138 | Return (and then remove) a key/value pair from the session store: 139 | 140 | [source, js] 141 | ---- 142 | const username = session.pull('username') // returns username 143 | 144 | session.get('username') // null 145 | ---- 146 | 147 | ==== clear 148 | Empty the session store: 149 | 150 | [source, js] 151 | ---- 152 | session.clear() 153 | ---- 154 | 155 | == Flash messages 156 | Flash messages are short-lived session values for a single request only. They are mainly used to *flash form errors*, but can be used for any other purpose. 157 | 158 | === HTML form example 159 | 160 | Let's say we want to validate submitted user data and redirect back to our form if there are validation errors. 161 | 162 | Start with the following HTML form: 163 | 164 | [source, edge] 165 | ---- 166 |
167 | {{ csrfField() }} 168 | 169 | 170 |
171 | ---- 172 | 173 | Then, register the `/users` route to validate form data: 174 | 175 | .app/routes.js 176 | [source, js] 177 | ---- 178 | const { validate } = use('Validator') 179 | 180 | Route.post('users', ({ request, session, response }) => { 181 | const rules = { username: 'required' } 182 | const validation = await validate(request.all(), rules) 183 | 184 | if (validation.fails()) { 185 | session.withErrors(validation.messages()).flashAll() 186 | return response.redirect('back') 187 | } 188 | 189 | return 'Validation passed' 190 | }) 191 | ---- 192 | 193 | Finally, rewrite the HTML form to retrieve flash data using link:sessions#_view_helpers[view helpers]: 194 | 195 | [source, edge] 196 | ---- 197 |
198 | {{ csrfField() }} 199 | 200 | {{ getErrorFor('username') }} 201 | 202 |
203 | ---- 204 | 205 | === Flash methods 206 | Below is a list of all session flash methods and their example usages. 207 | 208 | ==== flashAll 209 | Flash the request form data: 210 | 211 | [source, js] 212 | ---- 213 | session.flashAll() 214 | ---- 215 | 216 | ==== flashOnly 217 | Flash only the selected fields: 218 | 219 | [source, js] 220 | ---- 221 | session.flashOnly(['username', 'email']) 222 | ---- 223 | 224 | ==== flashExcept 225 | Flash the request form data except the selected fields: 226 | 227 | [source, js] 228 | ---- 229 | session.flashExcept(['password', 'csrf_token']) 230 | ---- 231 | 232 | ==== withErrors 233 | Flash with an array of errors: 234 | 235 | [source, js] 236 | ---- 237 | session 238 | .withErrors([{ field: 'username', message: 'Error message' }]) 239 | .flashAll() 240 | ---- 241 | 242 | ==== flash 243 | Flash a custom object: 244 | 245 | [source, js] 246 | ---- 247 | session.flash({ notification: 'You have been redirected back' }) 248 | ---- 249 | 250 | === View helpers 251 | When using flash messages, you can use the following view helpers to read values from the flash session store. 252 | 253 | ==== old(key, defaultValue) 254 | Returns the value for a given key from the flash store: 255 | 256 | [source, js] 257 | ---- 258 | session.flashOnly(['username']) 259 | ---- 260 | 261 | [source, edge] 262 | ---- 263 | 264 | ---- 265 | 266 | ==== hasErrorFor(key) 267 | Returns `true` if there is an error for a given field inside the flash store: 268 | 269 | [source, js] 270 | ---- 271 | session 272 | .withErrors({ username: 'Username is required' }) 273 | .flashAll() 274 | ---- 275 | 276 | [source, edge] 277 | ---- 278 | @if(hasErrorFor('username')) 279 | // display error 280 | @endif 281 | ---- 282 | 283 | ==== getErrorFor(key) 284 | Returns the error message for a given field: 285 | 286 | [source, js] 287 | ---- 288 | session 289 | .withErrors({ username: 'Username is required' }) 290 | .flashAll() 291 | ---- 292 | 293 | ==== flashMessage(key, defaultValue) 294 | Returns the flash message for a given key: 295 | 296 | [source, js] 297 | ---- 298 | session.flash({ notification: 'Update successful!' }) 299 | ---- 300 | 301 | [source, edge] 302 | ---- 303 | @if(flashMessage('notification')) 304 | {{ flashMessage('notification') }} 305 | @endif 306 | ---- 307 | 308 | == Session persistence 309 | Session values are persisted in bulk when the request ends. This keeps the request/response performant since you can mutate the session store as many times as you want and a bulk update is only performed at the end. 310 | 311 | It is achieved via AdonisJs middleware (see the implementation link:https://github.com/adonisjs/adonis-session/blob/develop/src/Session/Middleware.js#L89[here, window="_blank"]). 312 | 313 | However, there is a caveat. If an exception is thrown, the middleware layer breaks and session values are never committed. 314 | 315 | AdonisJs first-party packages handle this gracefully, but you should commit the session manually if you are handling exceptions of your own: 316 | 317 | [source, js] 318 | ---- 319 | const GE = require('@adonisjs/generic-exceptions') 320 | 321 | class MyCustomException extends GE.LogicalException { 322 | handle (error, { session }) { 323 | await session.commit() 324 | // handle exception 325 | } 326 | } 327 | ---- 328 | -------------------------------------------------------------------------------- /04-Basics/09-Error-Handling.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: exceptions 3 | title: Error Handling 4 | category: basics 5 | --- 6 | 7 | = Handling Exceptions 8 | 9 | toc::[] 10 | 11 | AdonisJs not only treats exceptions as a way to guide the developer on what went wrong, but also as a way to build application flow around them. 12 | 13 | In this guide, we learn how exceptions are raised, how to write logic around them and finally creating your own custom exceptions. 14 | 15 | == Introduction 16 | Exceptions are great since they halt the program at a certain stage and make sure everything is correct before proceeding. 17 | 18 | Exceptions are usually just treated as guides to tell the developer what went wrong, but if handled carefully, they can help you build application flow around them. 19 | 20 | By default, AdonisJs handles all exceptions for you and displays them in a link:http://res.cloudinary.com/adonisjs/image/upload/v1485520687/Screen_Shot_2017-01-27_at_6.07.28_PM_blcaau.png[nice format, window="_blank"] during development. However, you are free to handle exceptions however you want. 21 | 22 | == Handling exceptions 23 | Exceptions can be handled by binding a wildcard exception handler, or handling individual exceptions using their names. 24 | 25 | === Wildcard handler 26 | Let's create a wildcard exception handler using the `adonis` command: 27 | 28 | [source, bash] 29 | ---- 30 | > adonis make:ehandler 31 | ---- 32 | 33 | .make:ehandler output 34 | [source, bash] 35 | ---- 36 | ✔ create app/Exceptions/Handler.js 37 | ---- 38 | 39 | Once created, the wildcard exception handler is passed all exceptions that occurred during the HTTP lifecycle: 40 | 41 | .app/Exceptions/Handler.js 42 | [source, js] 43 | ---- 44 | const BaseExceptionHandler = use('BaseExceptionHandler') 45 | 46 | class ExceptionHandler extends BaseExceptionHandler { 47 | async handle (error, { response, session }) { 48 | if (error.name === 'ValidationException') { 49 | session.withErrors(error.messages).flashAll() 50 | await session.commit() 51 | response.redirect('back') 52 | return 53 | } 54 | 55 | return super.handle(...arguments) 56 | } 57 | } 58 | 59 | module.exports = ExceptionHandler 60 | ---- 61 | 62 | In the example above, the `handle` method handles the `ValidationException` by flashing validation errors back to the form. 63 | 64 | === Individual exceptions 65 | You can hook into individual exceptions by defining an inline handler for them. 66 | 67 | This can be done inside the `start/hooks.js` file: 68 | 69 | .start/hooks.js 70 | [source, js] 71 | ---- 72 | const { hooks } = require('@adonisjs/ignitor') 73 | 74 | hooks.after.providersBooted(() => { 75 | const Exception = use('Exception') 76 | 77 | Exception.handle('ValidationException', async (error, { response, session }) => { 78 | session.withErrors(error.messages).flashAll() 79 | await session.commit() 80 | response.redirect('back') 81 | return 82 | }) 83 | }) 84 | ---- 85 | 86 | == Custom exceptions 87 | AdonisJs makes it simple to build your own custom exceptions and define handlers for them. 88 | 89 | Let's use the `adonis` command to create a custom exception: 90 | 91 | [source, bash] 92 | ---- 93 | > adonis make:exception Custom 94 | ---- 95 | 96 | .make:exception output 97 | [source, bash] 98 | ---- 99 | ✔ create app/Exceptions/CustomException.js 100 | ---- 101 | 102 | .app/Exceptions/CustomException.js 103 | [source, js] 104 | ---- 105 | const { LogicalException } = require('@adonisjs/generic-exceptions') 106 | 107 | class CustomException extends LogicalException {} 108 | 109 | module.exports = CustomException 110 | ---- 111 | 112 | You can throw this exception by importing its source file (the `status` and `code` values are optional): 113 | 114 | [source, js] 115 | ---- 116 | const CustomException = use('App/Exceptions/CustomException') 117 | 118 | throw new CustomException(message, status, code) 119 | ---- 120 | 121 | You can set default messages, status and codes in a custom exception: 122 | 123 | .app/Exceptions/NotEditableException.js 124 | [source, js] 125 | ---- 126 | const { LogicalException } = require('@adonisjs/generic-exceptions') 127 | const message = 'The item is in an status where modifications are disallowed' 128 | const status = 403 129 | const code = 'E_NOT_EDITABLE' 130 | 131 | class NotEditableException extends LogicalException { 132 | constructor () { 133 | super(message, status, code) 134 | } 135 | } 136 | 137 | module.exports = NotEditableException 138 | ---- 139 | 140 | [source, js] 141 | ---- 142 | const NotEditableException = use('App/Exceptions/NotEditableException') 143 | 144 | throw new NotEditableException() 145 | ---- 146 | 147 | The beauty of this approach is that you can give a unique name to your exceptions as the class name, and then catch and respond to them appropriately. 148 | 149 | 150 | === A step further 151 | We can take custom exception handling a step further by defining `handle` and `report` methods on our custom exception class: 152 | 153 | .app/Exceptions/CustomException.js 154 | [source, js] 155 | ---- 156 | const { LogicalException } = require('@adonisjs/generic-exceptions') 157 | 158 | class CustomException extends LogicalException { 159 | handle (error, { response }) { 160 | response 161 | .status(500) 162 | .send('Custom exception handled!') 163 | } 164 | } 165 | 166 | module.exports = CustomException 167 | ---- 168 | 169 | If set, AdonisJs calls the custom exception's `handle` method to create and return the exception response. 170 | -------------------------------------------------------------------------------- /04-Basics/10-Logger.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logger 3 | permalink: logger 4 | category: basics 5 | --- 6 | 7 | = Logger 8 | 9 | toc::[] 10 | 11 | AdonisJs comes with a fully featured logger built on top of link:https://github.com/winstonjs/winston[winston, window="_blank"], using link:https://tools.ietf.org/html/rfc5424#page-11[RFC5424] logging levels. 12 | 13 | Logger ships with the following drivers: 14 | 15 | [ol-shrinked] 16 | 1. Console (`console`) 17 | 2. File (`file`) 18 | 19 | You are free to add your own drivers built on top of link:https://github.com/winstonjs/winston#transports[winston transports]. 20 | 21 | == Configuration 22 | The configuration for Logger is saved inside the `config/app.js` file under the `logger` object: 23 | 24 | .config/app.js 25 | [source, js] 26 | ---- 27 | logger: { 28 | transport: 'console', 29 | console: { 30 | driver: 'console' 31 | }, 32 | file: { 33 | driver: 'file', 34 | filename: 'adonis.log' 35 | } 36 | } 37 | ---- 38 | 39 | The `file` driver saves your log file inside the application root `tmp` directory. 40 | 41 | NOTE: You can define an absolute `filename` path to a different log file location if you wish. 42 | 43 | == Basic example 44 | Let's start with a basic example of logging data within your app: 45 | 46 | [source, js] 47 | ---- 48 | const Logger = use('Logger') 49 | 50 | Logger.info('request url is %s', request.url()) 51 | 52 | Logger.info('request details %j', { 53 | url: request.url(), 54 | user: auth.user.username() 55 | }) 56 | ---- 57 | 58 | TIP: All logging methods support link:http://www.diveintojavascript.com/projects/javascript-sprintf[sprintf] syntax. 59 | 60 | The logger uses link:https://tools.ietf.org/html/rfc5424#page-11[RFC5424] log levels, exposing simple methods for each level: 61 | 62 | [options="header", cols="20%,20%,60%"] 63 | |==== 64 | | Level | Method | Usage 65 | | 0 | emerg | `Logger.emerg(msg, ...data)` 66 | | 1 | alert | `Logger.alert(msg, ...data)` 67 | | 2 | crit | `Logger.crit(msg, ...data)` 68 | | 3 | error | `Logger.error(msg, ...data)` 69 | | 4 | warning | `Logger.warning(msg, ...data)` 70 | | 5 | notice | `Logger.notice(msg, ...data)` 71 | | 6 | info | `Logger.info(msg, ...data)` 72 | | 7 | debug | `Logger.debug(msg, ...data)` 73 | |==== 74 | 75 | == Switching transports 76 | You can switch transports on the fly using the `transport` method: 77 | 78 | [source, js] 79 | ---- 80 | Logger 81 | .transport('file') 82 | .info('request url is %s', request.url()) 83 | ---- 84 | 85 | == Logging level 86 | Logger has a default config logging `level` which can be updated at runtime. 87 | 88 | Any messages above the defined logging level are not logged. For example: 89 | 90 | [source, js] 91 | ---- 92 | const Logger = use('Logger') 93 | Logger.level = 'info' 94 | 95 | // not logged 96 | Logger.debug('Some debugging info') 97 | 98 | Logger.level = 'debug' 99 | 100 | // now logged 101 | Logger.debug('Some debugging info') 102 | ---- 103 | 104 | This approach could make it easier to turn off debugging messages when your server is under high load. 105 | 106 | -------------------------------------------------------------------------------- /05-Security/01-Getting-Started.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: security-introduction 3 | title: Introduction 4 | category: security 5 | --- 6 | 7 | = Getting Started 8 | 9 | toc::[] 10 | 11 | AdonisJs provides a handful of tools to keep your websites secure from common web attacks. 12 | 13 | In this guide, we learn about the best practices to keep your AdonisJs applications secure. 14 | 15 | NOTE: If you discover any security bugs, mailto:virk@adonisjs.com[please inform us immediately via email]. Do not create GitHub issues, as this may impact applications running in production. Found issues will be disclosed once patches have been pushed to the codebase. 16 | 17 | == Session Security 18 | Sessions can leak important information if not handled with care. 19 | 20 | AdonisJs encrypts and signs all cookies using the `appKey` defined in the `config/app.js` file. 21 | 22 | Keep your `appKey` secret – don't share it with anyone, and never push it to version control systems like Github. 23 | 24 | === Session Config 25 | Session configuration is saved inside the `config/session.js` file. 26 | 27 | When updating your session configuration, considering the following suggestions: 28 | 29 | [ul-spaced] 30 | * The `httpOnly` value should be set to `true`, as setting it to `false` will make your cookies accessible via JavaScript using `document.cookie`. 31 | * The `sameSite` value should also be set to `true`, ensuring your session cookie is not visible/accessible via different domains. 32 | 33 | == Form Method Spoofing 34 | As HTML forms are only capable of making `GET` and `POST` requests, you cannot use HTTP verbs like `PUT` or `DELETE` to perform resourceful operations via a form's `method` attribute. 35 | 36 | To work around this, AdonisJs implements link:request#_method_spoofing[form method spoofing], enabling you to send your intended HTTP method via the request URL's `_method` query string parameter: 37 | 38 | .Route 39 | [source, javascript] 40 | ---- 41 | Route.put('/users/:id', 'UserController.update') 42 | ---- 43 | 44 | .View 45 | [source, html] 46 | ---- 47 |
48 |
49 | ---- 50 | 51 | In the example above, appending `?_method=PUT` to the form's `action` URL converts the request HTTP method from `POST` to `PUT`. 52 | 53 | Here are a couple of things you should know about method spoofing: 54 | 55 | [ul-spaced] 56 | * AdonisJs only spoofs methods where the source HTTP method is `POST`, meaning `GET` requests passing an intented HTTP `_method` are not spoofed. 57 | * Method spoofing can be disabled by setting `allowMethodSpoofing` to `false` inside the `config/app.js` file: 58 | + 59 | .config/app.js 60 | [source, javascript] 61 | ---- 62 | http: { 63 | allowMethodSpoofing: false 64 | } 65 | ---- 66 | 67 | == File Uploads 68 | Attackers often try to upload malicious files to servers to later execute and gain access to servers to perform some kind of destructive activity. 69 | 70 | Besides uploading malicious files, attackers may also try to upload *huge* files so you server stays busy uploading and starts throwing *TIMEOUT* errors for subsequent requests. 71 | 72 | To combat this scenario, AdonisJs lets you define the *maximum upload size* processable by your server. This means any file larger than the specified `maxSize` is denied, keeping your server in a healthy state. 73 | 74 | Set your `maxSize` value inside the `config/bodyParser.js` file: 75 | 76 | .config/bodyParser.js 77 | [source, javascript] 78 | ---- 79 | uploads: { 80 | maxSize: '2mb' 81 | } 82 | ---- 83 | 84 | Here are a few tips to consider when handling file uploads: 85 | 86 | [ul-spaced] 87 | * Rename user files before uploading/storing. 88 | * Don't store uploaded files inside the `public` directory, since `public` files can be accessed directly. 89 | * Don't share the actual location of uploaded files with your users. Instead, consider saving a reference to uploaded file paths in your database (each file having a *unique id*), and set up a route to serve those uploaded files via `id`, like so: 90 | + 91 | .start/routes.js 92 | [source, javascript] 93 | ---- 94 | const Helpers = use('Helpers') 95 | 96 | Route.get('download/:fileId', async ({ params, response }) => { 97 | const file = await Files.findorFail(params.fileId) 98 | response.download(Helpers.tmpPath('uploads/${file.path}')) 99 | }) 100 | ---- 101 | -------------------------------------------------------------------------------- /05-Security/04-CORS.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: cors 3 | title: CORS 4 | category: security 5 | --- 6 | 7 | = CORS 8 | 9 | toc::[] 10 | 11 | Cross-Origin Resource Sharing (CORS) is a way to allow incoming HTTP requests from different domains. 12 | 13 | It is very common in AJAX applications where the browser blocks all cross-domain requests if the server does not authorize them. 14 | 15 | Read more about CORS link:https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS[here, window="_blank"]. 16 | 17 | == Setup 18 | Install the middleware provider via npm by executing the following command: 19 | 20 | [source, bash] 21 | ---- 22 | > adonis install @adonisjs/cors 23 | ---- 24 | 25 | Next, register the provider inside the `start/app.js` file: 26 | 27 | .start/app.js 28 | [source, javascript] 29 | ---- 30 | const providers = [ 31 | '@adonisjs/cors/providers/CorsProvider' 32 | ] 33 | ---- 34 | 35 | Finally, register the middleware inside the `start/kernel.js` file: 36 | 37 | .start/kernel.js 38 | [source, js] 39 | ---- 40 | Server 41 | .use(['Adonis/Middleware/Cors']) 42 | ---- 43 | 44 | == Config 45 | The configuration for CORS is defined inside the `config/cors.js` file and accepts the following options. 46 | 47 | ==== origin 48 | The origin(s) to be allowed for making cross-domain requests. 49 | 50 | You can return one of the following values: 51 | 52 | [ul-spaced] 53 | - A boolean `true` or `false` to deny the current request origin. 54 | - A comma-separated strings of domains to be allowed. 55 | - An array of domains to be allowed. 56 | - A function, which receives the current request origin. Here you can compute whether or not the origin is allowed by returning true or false: 57 | + 58 | .config/cors.js 59 | [source, js] 60 | ---- 61 | origin: function (currentOrigin) { 62 | return currentOrigin === 'mywebsite.com' 63 | } 64 | ---- 65 | 66 | For all other options, please inspect the comments inside the link:https://github.com/adonisjs/adonis-cors/blob/develop/config/cors.js#L3[config file, window="_blank"]. 67 | -------------------------------------------------------------------------------- /05-Security/05-CSRF-Protection.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: csrf 3 | title: CSRF Protection 4 | category: security 5 | --- 6 | 7 | = CSRF Protection 8 | 9 | toc::[] 10 | 11 | link:https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)[Cross-Site Request Forgery (CSRF)] allows an attacker to perform actions on behalf of another user without their knowledge or permission. 12 | 13 | AdonisJs protects your application from CSRF attacks by denying unidentified requests. HTTP requests with *POST, PUT and DELETE* methods are checked to make sure that the right people from the right place invoke these requests. 14 | 15 | == Setup 16 | Install the `shield` provider via npm by executing the following command: 17 | [source, bash] 18 | ---- 19 | > adonis install @adonisjs/shield 20 | ---- 21 | 22 | Next, register the provider inside the `start/app.js` file: 23 | 24 | .start/app.js 25 | [source, js] 26 | ---- 27 | const providers = [ 28 | '@adonisjs/shield/providers/ShieldProvider' 29 | ] 30 | ---- 31 | 32 | Finally, register the global middleware inside the `start/kernel.js` file: 33 | 34 | .start/kernel.js 35 | [source, js] 36 | ---- 37 | const globalMiddleware = [ 38 | 'Adonis/Middleware/Shield' 39 | ] 40 | ---- 41 | 42 | NOTE: Shield middleware relies on link:sessions[sessions], so make sure they are set up correctly. 43 | 44 | == Config 45 | The configuration for CSRF is saved inside the `config/shield.js` file: 46 | 47 | .config/shield.js 48 | [source, javascript] 49 | ---- 50 | csrf: { 51 | enable: true, 52 | methods: ['POST', 'PUT', 'DELETE'], 53 | filterUris: ['/user/:id'], 54 | cookieOptions: {} 55 | } 56 | ---- 57 | 58 | [options="header"] 59 | |==== 60 | | Key | Value | Description 61 | | enable | Boolean | A boolean to turn on/off CSRF for the entire application. 62 | | methods | Array | HTTP verbs to be protected by CSRF. Consider adding all verbs which allow the end user to add or modify data. 63 | | filterUris | Array | A list of URLs/Routes to ignore. You can pass actual route definitions or a regular expression to match. 64 | | cookieOptions | Object | An object of link:https://www.npmjs.com/package/cookie#options-1[cookie options, window="_blank"]. 65 | |==== 66 | 67 | == How It Works 68 | 69 | [ol-spaced] 70 | 1. AdonisJs creates a *CSRF secret* for each user visiting your website. 71 | 2. A corresponding token for the secret is generated for each request and passed to all views as `csrfToken` and `csrfField()` globals. 72 | 3. Also, the same token is set to a cookie with key `XSRF-TOKEN`. Frontend Frameworks like *AngularJs* automatically read this cookie and send it along with each Ajax request. 73 | 4. Whenever a *POST*, *PUT* or *DELETE* requests comes, the middleware verifies the token with the secret to make sure it is valid. 74 | 75 | NOTE: If you are using the `XSRF-TOKEN` cookie value, ensure the header key is `X-XSRF-TOKEN`. 76 | 77 | == View Helpers 78 | You can access the CSRF token using one of the following view helpers to ensure it gets set inside your forms. 79 | 80 | To send the token along with each request, you need access to it. There are a few ways to get access to the CSRF token. 81 | 82 | ==== csrfField 83 | [source, edge] 84 | ---- 85 | {{ csrfField() }} 86 | ---- 87 | 88 | .Output 89 | [source, html] 90 | ---- 91 | 92 | ---- 93 | 94 | ==== csrfToken 95 | The `csrfToken` helper returns the raw value of the token: 96 | 97 | [source, edge] 98 | ---- 99 | {{ csrfToken }} 100 | ---- 101 | 102 | == Exception handling 103 | On validation failure, an exception is thrown with the code *EBADCSRFTOKEN*. 104 | 105 | Ensure you listen for this exception and return an appropriate response, like so: 106 | 107 | .app/Exceptions/Handler.js 108 | [source, javascript] 109 | ---- 110 | class ExceptionHandler { 111 | async handle (error, { response }) { 112 | if (error.code === 'EBADCSRFTOKEN') { 113 | response.forbidden('Cannot process your request.') 114 | return 115 | } 116 | } 117 | } 118 | ---- 119 | -------------------------------------------------------------------------------- /05-Security/06-Encryption.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Encryption and Hashing 3 | permalink: encryption-and-hashing 4 | category: security 5 | --- 6 | 7 | = Encryption and Hashing 8 | 9 | toc::[] 10 | 11 | AdonisJs ships with providers for *hashing values* and *encrypting data*. 12 | 13 | *Hashing values* is different to *encrypting data*, since hashed values cannot be decrypted once encrypted. 14 | 15 | == Encrypting Data 16 | 17 | The AdonisJs encryption provider uses the link:https://nodejs.org/api/crypto.html[Node.js crypto module, window="_blank"] to encrypt and decrypt values. 18 | 19 | NOTE: Your *appKey* must be defined inside the `config/app.js` file before you can encrypt values. 20 | 21 | ==== encrypt(value) 22 | [source, javascript] 23 | ---- 24 | const Encryption = use('Encryption') 25 | const encrypted = Encryption.encrypt('hello world') 26 | ---- 27 | 28 | ==== decrypt 29 | [source, javascript] 30 | ---- 31 | const Encryption = use('Encryption') 32 | const decrypted = Encryption.decrypt('encrypted value') 33 | ---- 34 | 35 | == Hashing Values 36 | The AdonisJs hash provider comes with multiple drivers to hash user data. 37 | 38 | By default it uses link:https://en.wikipedia.org/wiki/Bcrypt[bcrypt, window="_blank"], however there is Argon support via the link:https://npm.im/argon2[argon2 npm package, window="_blank"]. 39 | 40 | NOTE: Multiple drivers are supported by `@adonisjs/framework` version `>=5.0.8`. 41 | 42 | === Config 43 | The config is defined inside the `config/hash.js` file: 44 | 45 | .config/hash.js 46 | [source, js] 47 | ---- 48 | module.exports = { 49 | driver: 'bcrypt', 50 | bcrypt: { 51 | rounds: 10 52 | }, 53 | argon: { 54 | type: 1 55 | } 56 | } 57 | ---- 58 | 59 | NOTE: If using the `argon` driver, you will have to install the link:https://npm.im/argon2[argon2 npm package, window="_blank"] package via npm. 60 | 61 | ==== make(value, [config]) 62 | Hash a plain string value: 63 | 64 | [source, javascript] 65 | ---- 66 | const Hash = use('Hash') 67 | const safePassword = await Hash.make(request.input('password')) 68 | ---- 69 | 70 | Optionally, inline config can be passed to override config file defaults: 71 | 72 | [source, javascript] 73 | ---- 74 | const Hash = use('Hash') 75 | const safeExample = await Hash.make('example', config) 76 | ---- 77 | 78 | ==== verify(value, hashedValue) 79 | Since you cannot decrypt a hash, you can verify the user input against the previously hashed value. 80 | 81 | [source, javascript] 82 | ---- 83 | const Hash = use('Hash') 84 | const isSame = await Hash.verify('plain-value', 'hashed-value') 85 | 86 | if (isSame) { 87 | // ... 88 | } 89 | ---- 90 | 91 | -------------------------------------------------------------------------------- /05-Security/08-Shield-Middleware.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: shield 3 | title: Shield Middleware 4 | category: security 5 | --- 6 | 7 | = Shield Middleware 8 | 9 | toc::[] 10 | 11 | Apart from link:cors[CORS] and link:csrf[CSRF], AdonisJs also prevents your web applications from other malware attacks like *XSS*, *Content Sniffing*, *Script Injection* and so on. 12 | 13 | NOTE: There is no silver bullet to secure your websites completely. AdonisJs as a framework gives you a handful of ways to prevent common web attacks. 14 | 15 | == Setup 16 | Install the `shield` provider and register the appropriate middleware: 17 | 18 | [source, bash] 19 | ---- 20 | > adonis install @adonisjs/shield 21 | ---- 22 | 23 | Next, register the provider inside the `start/app.js` file: 24 | 25 | .start/app.js 26 | [source, js] 27 | ---- 28 | const providers = [ 29 | '@adonisjs/shield/providers/ShieldProvider' 30 | ] 31 | ---- 32 | 33 | Finally, register the global middleware inside the `start/kernel.js` file: 34 | 35 | .start/kernel.js 36 | [source, js] 37 | ---- 38 | const globalMiddleware = [ 39 | 'Adonis/Middleware/Shield' 40 | ] 41 | ---- 42 | 43 | NOTE: Shield middleware relies on link:sessions[sessions], so make sure they are set up correctly. 44 | 45 | == Content Security Policy 46 | 47 | Content Security Policy (CSP) helps you define the trusted sources for loading and executing *scripts*, *styles*, *fonts* and various other resources. 48 | 49 | It's good practice to be strict when allowing the execution of scripts from different sources. 50 | 51 | For more information, read this interesting article by link:http://www.html5rocks.com/en/tutorials/security/content-security-policy[HTML5 rocks, window="_blank"]. 52 | 53 | === Configuration 54 | The configuration for CSP is saved inside the `config/shield.js` file: 55 | 56 | .config/shield.js 57 | [source, javascript] 58 | ---- 59 | csp: { 60 | directives: { 61 | defaultSrc: ['self', 'http://getcdn.com'], 62 | scriptSrc: ['self', '@nonce'], 63 | styleSrc: ['http://getbootstrap.com'], 64 | imgSrc: ['http://dropbox.com'] 65 | }, 66 | reportOnly: false, 67 | setAllHeaders: false, 68 | disableAndroid: true 69 | } 70 | ---- 71 | 72 | [options="header", cols="15,20,65"] 73 | |==== 74 | | Key | Value | Description 75 | | directives | Object | Directives help you define policies to be applied to different resource types. You can get the list of all directives from link:http://content-security-policy.com[http://content-security-policy.com, window="_blank"]. 76 | | reportOnly | Boolean | Set the value to `true` to log warnings that some rules are violated instead of stopping the execution of the page. 77 | | setAllHeaders | Boolean | Shield sets different HTTP headers for different browsers. Set the value to `true` to set all fallback headers, regardless of the browser. 78 | | disableAndroid | Boolean | As Android is known to be buggy with CSP, set the value to `true` to disable CSP for Android. 79 | |==== 80 | 81 | === Browser support 82 | Almost all modern browsers support CSP. 83 | 84 | Here is the most accurate list of link:http://caniuse.com/#feat=contentsecuritypolicy[supported browsers, window="_blank"]. 85 | 86 | === CSP policy via meta tags 87 | The `shield` middleware automatically sets the required HTTP headers for CSP to work, but also provides a view helper to set the meta tag if required: 88 | 89 | [source, edge] 90 | ---- 91 | {{ cspMeta() }} 92 | ---- 93 | 94 | .Output 95 | [source, html] 96 | ---- 97 | 98 | ---- 99 | 100 | === CSP Nonce 101 | Script tags with inline JavaScript code are automatically trusted and executed by the browser. 102 | 103 | This behavior can be stopped by adding `@nonce` to your configuration `scriptSrc` array: 104 | 105 | .config/shield.js 106 | [source, js] 107 | ---- 108 | csp: { 109 | directives: { 110 | scriptSrc: ['self', '@nonce'] 111 | }, 112 | // ... 113 | } 114 | ---- 115 | 116 | To tell the browser which inline script blocks should still execute, append a `nonce` attribute using the `cspNonce` view global in your templates like so: 117 | 118 | [source, edge] 119 | ---- 120 | 123 | ---- 124 | 125 | == Malware Protection 126 | Malware protection helps in protecting your website from *XSS* attacks, unwanted *iframe embeds*, *content-type sniffing* and stopping IE from executing unsolicited scripts in the context of your web page. 127 | 128 | === XSS 129 | Edit the `xss` configuration object to enable/disable XSS protection (sets the header `X-XSS-Protection=1; mode=block`): 130 | 131 | .config/shield.js 132 | [source, javascript] 133 | ---- 134 | xss: { 135 | enabled: true, 136 | enableOnOldIE: false 137 | } 138 | ---- 139 | 140 | === No Sniff 141 | The majority of modern browsers attempts to detect the *Content-Type* of a request by sniffing its content, meaning a file ending in *.txt* could be executed as JavaScript if it contains JavaScript code. 142 | 143 | To disable this behavior set `nosniff` to `false`: 144 | 145 | .config/shield.js 146 | [source, javascript] 147 | ---- 148 | { 149 | nosniff: true 150 | } 151 | ---- 152 | 153 | === No Open 154 | IE users can execute webpages in the context of your website, which is a serious security risk. 155 | 156 | To stop IE from executing unknown scripts in the context of your website, ensure `noopen` is set to `true` (sets the header `X-Download-Options: noopen`): 157 | 158 | .config/shield.js 159 | [source, javascript] 160 | ---- 161 | { 162 | noopen: true 163 | } 164 | ---- 165 | 166 | === XFrame 167 | The `xframe` option within the `config/shield.js` file makes it easy for you to control the embed behavior of your website inside an iframe. 168 | 169 | Available options are `DENY`, `SAMEORIGIN` or `ALLOW-FROM http://example.com`: 170 | 171 | .config/shield.js 172 | [source, javascript] 173 | ---- 174 | { 175 | xframe: 'DENY' 176 | } 177 | ---- 178 | -------------------------------------------------------------------------------- /06-Digging-Deeper/02-Events.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events 3 | permalink: events 4 | category: digging-deeper 5 | --- 6 | 7 | = Events 8 | 9 | toc::[] 10 | 11 | AdonisJs ships with a dedicated *Event Provider*. 12 | 13 | Internally, it uses the link:https://github.com/asyncly/EventEmitter2[EventEmitter2, window="_blank"] package, with other convenient functionality added on top of it. 14 | 15 | The *Event Provider* has a link:testing-fakes#_events_fake[fake] implementation, which can be used for assertions when writing tests. 16 | 17 | == Events Overview 18 | 1. Event listeners are defined inside the `start/events.js` file. 19 | 2. Events listeners can be defined as *closures*, or you can bind an IoC container *namespace* instead: 20 | + 21 | [source, javascript] 22 | ---- 23 | Event.on('new::user', async (user) => { 24 | }) 25 | 26 | // OR 27 | Event.on('new::user', 'User.registered') 28 | ---- 29 | 30 | 3. Namespaced event listeners are stored inside the `app/Listeners` directory. 31 | 4. When binding listeners to events, you are not required to enter the entire namespace. For example, A listener stored as `app/Listeners/User.js` is referenced as `User.`. 32 | 5. The `make:listener` command can be used to create new event listeners: 33 | + 34 | [source, bash] 35 | ---- 36 | > adonis make:listener User 37 | ---- 38 | + 39 | .Output 40 | [source, bash] 41 | ---- 42 | ✔ create app/Listeners/User.js 43 | ---- 44 | 45 | == Basic Example 46 | Let's say we want to emit an event every time a user registers on our website, and inside an *event listener*, send an email to the registered user. 47 | 48 | First, we need to create the relevant route and controller: 49 | 50 | .start/routes.js 51 | [source, js] 52 | ---- 53 | Route.post('register', 'UserController.register') 54 | ---- 55 | 56 | .app/Controllers/Http/UserController.js 57 | [source, js] 58 | ---- 59 | const Event = use('Event') 60 | 61 | class UserController { 62 | register () { 63 | // register the user 64 | 65 | Event.fire('new::user', user) 66 | } 67 | } 68 | ---- 69 | 70 | Next, we need to a listener for the `new::user` event so we can send the email. 71 | 72 | To do so, create an `events.js` file inside the `start` directory: 73 | 74 | [source, bash] 75 | ---- 76 | # Mac / Linux 77 | > touch start/events.js 78 | 79 | # Windows 80 | > type NUL > start/events.js 81 | ---- 82 | 83 | Finally, write our event handling code inside the `start/events.js` file: 84 | 85 | [source, js] 86 | ---- 87 | const Event = use('Event') 88 | const Mail = use('Mail') 89 | 90 | Event.on('new::user', async (user) => { 91 | await Mail.send('new.user', user, (message) => { 92 | message.to(user.email) 93 | message.from('from@email') 94 | }) 95 | }) 96 | ---- 97 | 98 | As you can see, AdonisJs makes it easy to use the `await` keyword inside the event listener callback. 99 | 100 | == API 101 | Below is the list of methods that can be used to interact with the *Event Provider*. 102 | 103 | ==== on(event, listener) 104 | Bind single or multiple listeners for a given event. The `listener` can be a closure function or reference to one (or many) IoC container bindings: 105 | 106 | [source, js] 107 | ---- 108 | Event.on('new::user', async () => { 109 | 110 | }) 111 | 112 | // IoC container binding 113 | Event.on('new::user', 'User.registered') 114 | 115 | // Array of listeners 116 | Event.on('new::user', ['Mailer.sendEmail', 'SalesForce.trackLead']) 117 | ---- 118 | 119 | ==== when(event, listener) 120 | The `when` method aliases the xref:_onevent_listener[on] method. 121 | 122 | ==== once(event, listener) 123 | Same as xref:_onevent_listener[on], but only called one time: 124 | 125 | [source, js] 126 | ---- 127 | Event.once('new::user', () => { 128 | console.log('executed once') 129 | }) 130 | ---- 131 | 132 | ==== onAny(listener) 133 | Bind listener for any event: 134 | 135 | [source, js] 136 | ---- 137 | Event.onAny(function () { 138 | 139 | }) 140 | 141 | // Ioc container binding 142 | Event.onAny('EventsLogger.track') 143 | ---- 144 | 145 | ==== times(number) 146 | The `times` method is chained with `on` or `when` to limit the number of times the listener should be fired: 147 | 148 | [source, js] 149 | ---- 150 | Event 151 | .times(3) 152 | .on('new::user', () => { 153 | console.log('fired 3 times') 154 | }) 155 | ---- 156 | 157 | ==== emit(event, data) 158 | Emit an event with optional data: 159 | 160 | [source, js] 161 | ---- 162 | Event.emit('new::user', user) 163 | ---- 164 | 165 | ==== fire(event, data) 166 | The `fire` method aliases the xref:_emitevent_data[emit] method. 167 | 168 | ==== removeListener(event, listener) 169 | Remove listener(s) for a given event: 170 | 171 | [source, js] 172 | ---- 173 | Event.on('new::user', 'User.registered') 174 | 175 | // later remove it 176 | Event.removeListener('new::user', 'User.registered') 177 | ---- 178 | 179 | NOTE: You must bind an IoC container reference to remove it later. 180 | 181 | ==== off(event, listener) 182 | The `off` method aliases the xref:_removelistenerevent_listener[removeListener] method. 183 | 184 | ==== removeAllListeners(event) 185 | Remove all listeners for a given event: 186 | 187 | [source, js] 188 | ---- 189 | Event.removeAllListeners() 190 | ---- 191 | 192 | ==== listenersCount(event) 193 | Return the number of listeners for a given event: 194 | 195 | [source, js] 196 | ---- 197 | Event.listenersCount('new::user') 198 | ---- 199 | 200 | ==== getListeners(event) 201 | Return an array of listeners for a given event: 202 | 203 | [source, js] 204 | ---- 205 | Event.getListeners('new::user') 206 | ---- 207 | 208 | ==== hasListeners(event) 209 | Return a `boolean` indicating whether there are any listeners for a given event: 210 | 211 | [source, js] 212 | ---- 213 | Event.hasListeners('new::user') 214 | ---- 215 | -------------------------------------------------------------------------------- /06-Digging-Deeper/03-Extending-the-Core.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: extending-adonisjs 3 | title: Extending the Core 4 | category: digging-deeper 5 | --- 6 | 7 | = Extending the Core 8 | 9 | toc::[] 10 | 11 | AdonisJs is fully extendible to the core. 12 | 13 | In this guide, we learn how to extend parts of the framework. 14 | 15 | == Where to Write Code 16 | The easiest way to get started is to use link:ignitor#_hooks[applications hooks], and only later move code inside a provider if you want to share your code as a package. 17 | 18 | Hooks live inside the `start/hooks.js` file and can be used to execute code at a specific time in the application lifecycle: 19 | 20 | .start/hooks.js 21 | [source, js] 22 | ---- 23 | const { hooks } = require('@adonisjs/ignitor') 24 | 25 | hooks.after.providersRegistered(() => { 26 | // execute your code 27 | }) 28 | ---- 29 | 30 | NOTE: Hook callbacks are synchronous. You must create a provider and use the `boot` method to write asynchronous code. 31 | 32 | Providers live inside the `providers` directory in the project root: 33 | 34 | [source, bash] 35 | ---- 36 | ├── providers 37 | ├── AppProvider.js 38 | ---- 39 | 40 | Your providers must be registered inside the `start/app.js` file: 41 | 42 | .start/app.js 43 | [source, js] 44 | ---- 45 | const path = require('path') 46 | 47 | const providers = [ 48 | path.join(__dirname, '..', 'providers/AppProvider') 49 | ] 50 | ---- 51 | 52 | Providers are generally used to add functionality to your application by binding namespaces to the IoC container, however you can also use providers to run custom code when booted: 53 | 54 | [source, js] 55 | ---- 56 | const { ServiceProvider } = require('@adonisjs/fold') 57 | 58 | class AppProvider extends ServiceProvider { 59 | async boot () { 60 | // execute code 61 | } 62 | } 63 | ---- 64 | 65 | == Adding Macros/Getters 66 | Macros let you add methods to existing classes. 67 | 68 | A class must extend the link:https://www.npmjs.com/package/macroable[Macroable] class to be extended via macros. 69 | 70 | TIP: Use link:ignitor#_hooks[hooks] or a provider's `boot` method to add macros. 71 | 72 | For example, if a macro was defined like so: 73 | [source, js] 74 | ---- 75 | const Response = use('Adonis/Src/Response') 76 | const Request = use('Adonis/Src/Request') 77 | 78 | Response.macro('sendStatus', function (status) { 79 | this.status(status).send(status) 80 | }) 81 | ---- 82 | 83 | It could then be used as follows: 84 | 85 | [source, js] 86 | ---- 87 | Route.get('/', ({ response }) => { 88 | response.sendStatus(200) 89 | }) 90 | ---- 91 | 92 | In the same way, you can also add `getters` to your macroable classes: 93 | 94 | [source, js] 95 | ---- 96 | Request.getter('time', function () { 97 | return new Date().getTime() 98 | }) 99 | 100 | // Or add a singleton getter 101 | Request.getter('id', function () { 102 | return uuid.v4() 103 | }, true) 104 | ---- 105 | 106 | Below is the list of classes you can add getters/macros to: 107 | 108 | [ol-shrinked] 109 | 1. link:https://github.com/adonisjs/adonis-framework/blob/develop/src/Context/index.js[Adonis/Src/HttpContext, window="_blank"] 110 | 2. link:https://github.com/adonisjs/adonis-framework/blob/develop/src/Request/index.js[Adonis/Src/Request, window="_blank"] 111 | 3. link:https://github.com/adonisjs/adonis-framework/blob/develop/src/Response/index.js[Adonis/Src/Response, window="_blank"] 112 | 4. link:https://github.com/adonisjs/adonis-framework/blob/develop/src/Route/index.js[Adonis/Src/Route, window="_blank"] 113 | 114 | == Extending Providers 115 | Some existing providers let you extend them by adding new functionality. 116 | 117 | For example, the **Session Provider** allows new drivers to be added, while the **Auth Provider** allows new serializers and schemes. 118 | 119 | NOTE: Refer to the documentation of individual providers to understand their extending capabilities. 120 | 121 | To keep the extend interface unified and simple, use the `Ioc.extend` method to add new drivers or serializers: 122 | 123 | [source, js] 124 | ---- 125 | const { ioc } = require('@adonisjs/fold') 126 | const { hooks } = require('@adonisjs/ignitor') 127 | 128 | hooks.after.providersRegistered(() => { 129 | ioc.extend('Adonis/Src/Session', 'mongo', function () { 130 | return class MongoDriver { 131 | } 132 | }) 133 | }) 134 | ---- 135 | 136 | If you are developing a provider and want to use the same interface for exposing extending capabilities, make sure to bind a `Manager` object as follows: 137 | 138 | [source, js] 139 | ---- 140 | const { ServiceProvider } = require('@adonisjs/fold') 141 | 142 | class MyProvider extends ServiceProvider { 143 | register () { 144 | this.app.manager('MyApp/Provider', { 145 | extend: function () { 146 | } 147 | }) 148 | } 149 | } 150 | ---- 151 | 152 | 1. The manager object must have an `extend` method. The values passed to `ioc.extend` will be forwarded to this method. 153 | 2. The `namespace` must be same as the binding namespace. 154 | 3. You must manage the registration/lifecycle of your drivers. 155 | -------------------------------------------------------------------------------- /06-Digging-Deeper/04-File-Storage.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: File Storage 3 | permalink: file-system 4 | category: digging-deeper 5 | --- 6 | 7 | = File Storage 8 | 9 | toc::[] 10 | 11 | AdonisJs has a dedicated *Drive Provider* built on top of link:https://github.com/Slynova-Org/node-flydrive[Flydrive] to interact with local and remote file systems like *Amazon S3*. 12 | 13 | In this guide, we learn how to set up and use the *Drive Provider*. 14 | 15 | == Setup 16 | As the *Drive Provider* is not installed by default, we need to pull it from `npm`: 17 | 18 | [source, bash] 19 | ---- 20 | > adonis install @adonisjs/drive 21 | ---- 22 | 23 | Next, we need to register the provider inside the `start/app.js` file: 24 | 25 | .start/app.js 26 | [source, js] 27 | ---- 28 | const providers = [ 29 | '@adonisjs/drive/providers/DriveProvider' 30 | ] 31 | ---- 32 | 33 | NOTE: Driver configuration is saved inside the `config/drive.js` file, which is created by the `adonis install` command when installing the *Drive Provider*. 34 | 35 | == Available Drivers 36 | The default drivers shipped with the *Drive Provider* are: 37 | 38 | [ol-shrinked] 39 | 1. Amazon S3 (`s3`), which requires the link:https://www.npmjs.com/package/aws-sdk[aws-sdk, window="_blank"] package 40 | 2. DigitalOcean Spaces (`spaces`), which requires the link:https://www.npmjs.com/package/aws-sdk[aws-sdk, window="_blank"] package 41 | 2. Local file system (`local`) 42 | 43 | == Basic Example 44 | Here's a basic example of how to interact with the local disk via `adonis repl`: 45 | 46 | image:http://res.cloudinary.com/adonisjs/image/upload/q_100/v1505719793/Drive_dlcc3v.gif[] 47 | 48 | == Drive API 49 | While common operations like reading and writing remain the same across drivers, the API of a drive is mainly based upon the driver you're using to interact with that drive's file system. 50 | 51 | ==== exists(relativePath) 52 | Find if a file/directory exists or not: 53 | 54 | [source, js] 55 | ---- 56 | const exists = await Drive.exists('unicorn.jpg') 57 | ---- 58 | 59 | ==== get(relativePath, encoding = utf-8) 60 | Get file contents as a buffer or string: 61 | 62 | [source, js] 63 | ---- 64 | const unicorn = await Drive.get('unicorn.jpg') 65 | ---- 66 | 67 | ==== getStream(relativePath) 68 | Get file as a stream: 69 | 70 | [source, js] 71 | ---- 72 | Drive.getStream('hello.txt') 73 | ---- 74 | 75 | ==== put(relativePath, content, options = {}) 76 | Create a new file with given contents (creates any missing directories): 77 | 78 | [source, js] 79 | ---- 80 | await Drive.put('hello.txt', Buffer.from('Hello world!')) 81 | ---- 82 | 83 | ==== prepend(relativePath, content, options = {}) 84 | Prepend content to a file (creates a new file if path doesn't exist): 85 | 86 | [source, js] 87 | ---- 88 | await Drive.prepend('hello.txt', Buffer.from('Prepended!')) 89 | ---- 90 | 91 | NOTE: The `prepend` method only works with the local driver. 92 | 93 | ==== append(relativePath, content, options = {}) 94 | Append content to a file (creates a new file if path doesn't exist): 95 | 96 | [source, js] 97 | ---- 98 | await Drive.append('hello.txt', Buffer.from('Appended!')) 99 | ---- 100 | 101 | NOTE: The `append` method only works with the local driver. 102 | 103 | ==== delete(relativePath) 104 | Remove existing file: 105 | 106 | [source, js] 107 | ---- 108 | await Drive.delete('hello.txt') 109 | ---- 110 | 111 | ==== move(src, dest, options = {}) 112 | Move file from one directory to another: 113 | 114 | [source, js] 115 | ---- 116 | await Drive.move('hello.txt', 'hi.txt') 117 | ---- 118 | 119 | ==== copy(src, dest, options = {}) 120 | Copy file from one directory to another: 121 | 122 | [source, js] 123 | ---- 124 | await Drive.copy('hi.txt', 'hello.txt') 125 | ---- 126 | 127 | == S3/Spaces API 128 | The following methods work for the `s3` and `spaces` drivers only. 129 | 130 | ==== getObject(location, params) 131 | Get S3 object for a given file (for `params` info, see link:http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getObject-property[S3 params]): 132 | 133 | [source, js] 134 | ---- 135 | await Drive.disk('s3').getObject('unicorn.jpg') 136 | ---- 137 | 138 | ==== getUrl(location, [bucket]) 139 | Get url for a given file (accepts optional alternative `bucket` param): 140 | 141 | [source, js] 142 | ---- 143 | const url = Drive.disk('s3').getUrl('unicorn.jpg') 144 | ---- 145 | 146 | ==== getSignedUrl(location, expiry = 900, params) 147 | Get signed url for a given file (expiry set to `15mins` by default): 148 | 149 | [source, js] 150 | ---- 151 | const url = await Drive.disk('s3').getSignedUrl('unicorn.jpg') 152 | ---- 153 | -------------------------------------------------------------------------------- /06-Digging-Deeper/05-Helpers.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Helpers 3 | permalink: helpers 4 | category: digging-deeper 5 | --- 6 | 7 | = Helpers 8 | 9 | toc::[] 10 | 11 | The AdonisJs *Helpers Provider* enables a number of convenient methods to supercharge your application. 12 | 13 | Many of these methods can be used to retrieve *absolute paths* to specific directories within your application. 14 | 15 | == Basic Example 16 | From anywhere within your application, simply pull in the *Helpers Provider* and use it to retrieve paths to your different directories: 17 | 18 | [source, js] 19 | ---- 20 | const Helpers = use('Helpers') 21 | const welcomeView = Helpers.viewsPath('welcome.edge') 22 | ---- 23 | 24 | == Path Helpers 25 | Below is the list of path related helpers available via the *Helpers Provider*. 26 | 27 | ==== appRoot 28 | Returns path to the application root: 29 | 30 | [source, js] 31 | ---- 32 | Helpers.appRoot() 33 | ---- 34 | 35 | ==== publicPath([toFile]) 36 | Returns path to the public directory or file inside the directory: 37 | 38 | [source, js] 39 | ---- 40 | const publicPath = Helpers.publicPath() 41 | // or 42 | const cssFile = Helpers.publicPath('style.css') 43 | ---- 44 | 45 | ==== configPath([toFile]) 46 | Returns path to the config directory or file inside the directory: 47 | 48 | [source, js] 49 | ---- 50 | const configPath = Helpers.configPath() 51 | // or 52 | const appConfig = Helpers.configPath('app.js') 53 | ---- 54 | 55 | TIP: Use the link:configuration-and-env[Config Provider] to read config file values. 56 | 57 | ==== resourcesPath([toFile]) 58 | Returns path to the resources directory or file inside the directory: 59 | 60 | [source, js] 61 | ---- 62 | const resourcesPath = Helpers.resourcesPath() 63 | // or 64 | const appSass = Helpers.resourcesPath('assets/sass/app.scss') 65 | ---- 66 | 67 | ==== migrationsPath([toFile]) 68 | Returns path to the migrations directory or file inside the directory: 69 | 70 | [source, js] 71 | ---- 72 | const migrationsPath = Helpers.migrationsPath() 73 | // or 74 | const UserSchema = Helpers.migrationsPath('UserSchema.js') 75 | ---- 76 | 77 | ==== seedsPath([toFile]) 78 | Returns path to the seeds directory or file inside the directory: 79 | 80 | [source, js] 81 | ---- 82 | const seedsPath = Helpers.seedsPath() 83 | // or 84 | const DatabaseSeed = Helpers.seedsPath('Database.js') 85 | ---- 86 | 87 | ==== databasePath([toFile]) 88 | Returns path to the database directory or file inside the directory: 89 | 90 | [source, js] 91 | ---- 92 | const databasePath = Helpers.databasePath() 93 | // or 94 | const factoryFile = Helpers.databasePath('factory.js') 95 | ---- 96 | 97 | ==== viewsPath([toFile]) 98 | Returns path to the views directory or file inside the directory: 99 | 100 | [source, js] 101 | ---- 102 | const viewsPath = Helpers.viewsPath() 103 | // or 104 | const welcomeView = Helpers.viewsPath('welcome.edge') 105 | ---- 106 | 107 | ==== tmpPath([toFile]) 108 | Returns path to the tmp directory or file inside the directory: 109 | 110 | [source, js] 111 | ---- 112 | const tmpPath = Helpers.tmpPath() 113 | // or 114 | const resized = Helpers.tmpPath('resized.jpg') 115 | ---- 116 | 117 | == Other Helpers 118 | Below is the list of other helpers available via the *Helpers Provider*. 119 | 120 | ==== promisify 121 | Returns link:https://www.npmjs.com/package/pify[promisified, window="_blank"] callback functions: 122 | 123 | [source, js] 124 | ---- 125 | const exists = Helpers.promisify(require('fs').exists) 126 | const isExist = await exists(Helpers.tmpPath('image.jpg')) 127 | // or 128 | const fs = Helpers.promisify(require('fs')) 129 | await fs.unlink(Helpers.tmpPath('image.jpg')) 130 | ---- 131 | 132 | ==== isAceCommand 133 | Returns whether the process was started as the ace command or not: 134 | 135 | [source, js] 136 | ---- 137 | Helpers.isAceCommand() 138 | ---- 139 | -------------------------------------------------------------------------------- /06-Digging-Deeper/08-Social-Authentication.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Social Authentication 3 | permalink: social-auth 4 | category: digging-deeper 5 | --- 6 | 7 | = Social Authentication 8 | 9 | toc::[] 10 | 11 | *Ally* is a 1st party social authentication provider for AdonisJs. 12 | 13 | Using *Ally* makes it trivial to authenticate users via 3rd party websites like *Google*, *Twitter*, and *Facebook*. 14 | 15 | The *Ally Provider* supports the following drivers: 16 | 17 | [ul-shrinked] 18 | - Facebook (`facebook`) 19 | - Github (`github`) 20 | - Google (`google`) 21 | - Instagram (`instagram`) 22 | - Linkedin (`linkedin`) 23 | - Twitter (`twitter`) 24 | - Foursquare (`foursquare`) 25 | 26 | == Setup 27 | As the *Ally Provider* is not installed by default, we need to pull it from npm: 28 | 29 | [source, bash] 30 | ---- 31 | > adonis install @adonisjs/ally 32 | ---- 33 | 34 | Next, register the provider inside the `start/app.js` file: 35 | 36 | .start/app.js 37 | [source, js] 38 | ---- 39 | const providers = [ 40 | '@adonisjs/ally/providers/AllyProvider' 41 | ] 42 | ---- 43 | 44 | NOTE: Social authentication configuration is saved inside the `config/services.js` file, which is created by the `adonis install` command when installing the *Ally Provider*. 45 | 46 | == Config 47 | 48 | Your config must be stored inside the `config/services.js` file's `ally` object: 49 | 50 | .config/services.js 51 | [source, js] 52 | ---- 53 | module.exports = { 54 | ally: { 55 | facebook: {} 56 | } 57 | } 58 | ---- 59 | 60 | TIP: You can always access the latest config source file on link:https://github.com/adonisjs/adonis-ally/blob/master/templates/config.js[Github, window="_blank"]. 61 | 62 | == Basic Example 63 | Let's start with a basic example of logging in using *Facebook*. 64 | 65 | First, we need to register routes to redirect the user to Facebook then handle the response when the user is redirected back from Facebook: 66 | 67 | .start/routes.js 68 | [source, js] 69 | ---- 70 | Route.get('login/facebook', 'LoginController.redirect') 71 | Route.get('facebook/callback', 'LoginController.callback') 72 | ---- 73 | 74 | NOTE: Make sure the *Auth Provider* and auth-related middleware is link:authentication#_setup[configured correctly]. 75 | 76 | Next, we need to create the controller to implement our route methods: 77 | 78 | [source, bash] 79 | ---- 80 | > adonis make:controller Login 81 | ---- 82 | 83 | .app/Controllers/Http/LoginController.js 84 | [source, js] 85 | ---- 86 | const User = use('App/Models/User') 87 | 88 | class LoginController { 89 | async redirect ({ ally }) { 90 | await ally.driver('facebook').redirect() 91 | } 92 | 93 | async callback ({ ally, auth }) { 94 | try { 95 | const fbUser = await ally.driver('facebook').getUser() 96 | 97 | // user details to be saved 98 | const userDetails = { 99 | email: fbUser.getEmail(), 100 | token: fbUser.getAccessToken(), 101 | login_source: 'facebook' 102 | } 103 | 104 | // search for existing user 105 | const whereClause = { 106 | email: fbUser.getEmail() 107 | } 108 | 109 | const user = await User.findOrCreate(whereClause, userDetails) 110 | await auth.login(user) 111 | 112 | return 'Logged in' 113 | } catch (error) { 114 | return 'Unable to authenticate. Try again later' 115 | } 116 | } 117 | } 118 | ---- 119 | 120 | We now have a fully working login system in a few lines of code! 121 | 122 | *Ally's API* is consistent across drivers, so it's easy to swap `facebook` with `google` or any other driver required by your application. 123 | 124 | == Ally API 125 | Below is the list of available functions. 126 | 127 | ==== redirect 128 | Redirect user to the 3rd party website: 129 | 130 | [source, js] 131 | ---- 132 | await ally.driver('facebook').redirect() 133 | ---- 134 | 135 | ==== getRedirectUrl 136 | Get redirect URL back as a string: 137 | 138 | [source, js] 139 | ---- 140 | const url = await ally.driver('facebook').getRedirectUrl() 141 | 142 | return view.render('login', { url }) 143 | ---- 144 | 145 | ==== scope(scopesArray) 146 | Define runtime scopes before redirecting the user: 147 | 148 | [source, js] 149 | ---- 150 | await ally 151 | .driver('facebook') 152 | .scope(['email', 'birthday']) 153 | .redirect() 154 | ---- 155 | 156 | NOTE: Check the relevant provider's official OAuth documentation for a list of their available scopes. 157 | 158 | ==== fields(fieldsArray) 159 | Fields to be fetched when getting the authenticated user profile: 160 | 161 | [source, js] 162 | ---- 163 | await ally 164 | .driver('facebook') 165 | .fields(['username', 'email', 'profile_pic']) 166 | .getUser() 167 | ---- 168 | 169 | ==== getUser 170 | Get the user profile of an authenticated user (returns an link:https://github.com/adonisjs/adonis-ally/blob/develop/src/AllyUser.js[AllyUser, window="_blank"] instance): 171 | 172 | [source, js] 173 | ---- 174 | await ally 175 | .driver('facebook') 176 | .fields(['email']) 177 | .getUser() 178 | ---- 179 | 180 | ==== getUserByToken(accessToken, [accessSecret]) 181 | Returns the user details using the `accessToken`: 182 | 183 | [source, js] 184 | ---- 185 | await ally.getUserByToken(accessToken) 186 | ---- 187 | 188 | This is helpful when using client-side code to perform the OAuth action and you have access to the `accessToken`. 189 | 190 | NOTE: The `accessSecret` parameter is required when the *OAuth 1* protocol is used (e.g. Twitter relies on OAuth 1). 191 | 192 | == User API 193 | Below is the list of available methods on an link:https://github.com/adonisjs/adonis-ally/blob/develop/src/AllyUser.js[AllyUser, window="_blank"] instance. 194 | 195 | ==== getId 196 | Returns the user id: 197 | 198 | [source, js] 199 | ---- 200 | const user = await ally.driver('facebook').getUser() 201 | 202 | user.getId() 203 | ---- 204 | 205 | ==== getName 206 | Returns the user name: 207 | 208 | [source, js] 209 | ---- 210 | user.getName() 211 | ---- 212 | 213 | ==== getEmail 214 | Returns the user email: 215 | 216 | [source, js] 217 | ---- 218 | user.getEmail() 219 | ---- 220 | 221 | NOTE: Some 3rd party providers do not share email, in which case this method returns `null`. 222 | 223 | ==== getNickname 224 | Returns the nickname / display name of the user: 225 | 226 | [source, js] 227 | ---- 228 | user.getNickname() 229 | ---- 230 | 231 | ==== getAvatar 232 | Returns public URL to the user's profile picture: 233 | 234 | [source, js] 235 | ---- 236 | user.getAvatar() 237 | ---- 238 | 239 | ==== getAccessToken 240 | Returns the access token which may be used later to update the user profile: 241 | 242 | [source, js] 243 | ---- 244 | user.getAccessToken() 245 | ---- 246 | 247 | ==== getRefreshToken 248 | Refresh token to be used when access token expires: 249 | 250 | [source, js] 251 | ---- 252 | user.getRefreshToken() 253 | ---- 254 | 255 | NOTE: Available only when 3rd party provider implements *OAuth 2*. 256 | 257 | ==== getExpires 258 | Access token expiry data: 259 | 260 | [source, js] 261 | ---- 262 | user.getExpires() 263 | ---- 264 | 265 | NOTE: Available only when 3rd party provider implements *OAuth 2*. 266 | 267 | ==== getTokenSecret 268 | Returns token secret: 269 | 270 | [source, js] 271 | ---- 272 | user.getTokenSecret() 273 | ---- 274 | 275 | NOTE: Available only when 3rd party provider implements *OAuth 1*. 276 | 277 | ==== getOriginal 278 | Original payload returned by the 3rd party provider: 279 | 280 | [source, js] 281 | ---- 282 | user.getOriginal() 283 | ---- 284 | -------------------------------------------------------------------------------- /07-Database/01-Getting-Started.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | permalink: database 4 | category: database 5 | --- 6 | 7 | = Getting Started 8 | 9 | toc::[] 10 | 11 | Creating AdonisJs data-driven apps is greatly simplified via its powerful link:query-builder[Query Builder], link:lucid[Lucid ORM], link:migrations[Migrations], link:seeds-and-factories[Factories], and link:seeds-and-factories[Seeds]. 12 | 13 | In this guide we'll learn to setup and use the *Database Provider*. 14 | 15 | NOTE: The Data Provider uses link:https://knexjs.org[Knex.js, window="_blank"] internally, so browse the Knex documentation whenever further information is required. 16 | 17 | == Supported Databases 18 | The list of supported databases and their equivalent drivers are as follows: 19 | 20 | [options="header", cols="25, 75"] 21 | |==== 22 | | Database | NPM Driver 23 | | MariaDB | `npm i mysql` or `npm i mysql2` 24 | | MSSQL | `npm i mssql` 25 | | MySQL | `npm i mysql` or `npm i mysql2` 26 | | Oracle | `npm i oracledb` 27 | | PostgreSQL | `npm i pg` 28 | | SQLite3 | `npm i sqlite3` 29 | |==== 30 | 31 | == Setup 32 | 33 | === Installation 34 | If the *Database Provider* (*Lucid*) is not installed, pull it from `npm`: 35 | 36 | [source, bash] 37 | ---- 38 | > adonis install @adonisjs/lucid 39 | ---- 40 | 41 | Next, register the following providers inside the `start/app.js` file: 42 | 43 | .start/app.js 44 | [source, js] 45 | ---- 46 | const providers = [ 47 | '@adonisjs/lucid/providers/LucidProvider' 48 | ] 49 | 50 | const aceProviders = [ 51 | '@adonisjs/lucid/providers/MigrationsProvider' 52 | ] 53 | ---- 54 | 55 | NOTE: Many AdonisJs boilerplates have *Lucid* installed by default. 56 | 57 | === Configuration 58 | The *Database Provider* uses the `sqlite` connection by default. 59 | 60 | The default connection can be set via the `config/database.js` file: 61 | 62 | .config/database.js 63 | [source, js] 64 | ---- 65 | module.exports = { 66 | connection: 'mysql', 67 | } 68 | ---- 69 | 70 | All of the Knex link:http://knexjs.org/#Installation-client[configuration options, window="_blank"] are supported as is. 71 | 72 | == Basic Example 73 | The AdonisJs link:query-builder[Query Builder] has a *fluent* API, meaning you can chain/append JavaScript methods to create your SQL queries. 74 | 75 | For example, to select and return all users as JSON: 76 | [source, js] 77 | ---- 78 | const Database = use('Database') 79 | 80 | Route.get('/', async () => { 81 | return await Database.table('users').select('*') 82 | }) 83 | ---- 84 | 85 | === Where Clause 86 | To add a where clause to a query, chain a `where` method: 87 | 88 | [source, js] 89 | ---- 90 | Database 91 | .table('users') 92 | .where('age', '>', 18) 93 | ---- 94 | 95 | To add another where clause, chain an `orWhere` method: 96 | 97 | [source, js] 98 | ---- 99 | Database 100 | .table('users') 101 | .where('age', '>', 18) 102 | .orWhere('vip', true) 103 | ---- 104 | 105 | See the link:query-builder[Query Builder] documentation for the complete API reference. 106 | 107 | == Multiple Connections 108 | By default, AdonisJs uses the `connection` value defined inside the `config/database.js` file when making database queries. 109 | 110 | You can select any of the connections defined inside the `config/database.js` file at runtime to make your queries: 111 | 112 | [source, js] 113 | ---- 114 | Database 115 | .connection('mysql') 116 | .table('users') 117 | ---- 118 | 119 | NOTE: Since AdonisJs pools connections for reuse, all used connections are maintained unless the process dies. 120 | 121 | To close a connection, call the `close` method passing any connection names: 122 | 123 | [source, js] 124 | ---- 125 | const users = await Database 126 | .connection('mysql') 127 | .table('users') 128 | 129 | // later close the connection 130 | Database.close(['mysql']) 131 | ---- 132 | 133 | == Table Prefixing 134 | The *Database Provider* can automatically prefix table names by defining a `prefix` value inside the `config/database.js` file: 135 | 136 | .config/database.js 137 | [source, js] 138 | ---- 139 | module.exports = { 140 | connection: 'sqlite', 141 | 142 | sqlite: { 143 | client: 'sqlite3', 144 | prefix: 'my_' 145 | } 146 | } 147 | ---- 148 | 149 | Now, all queries on the `sqlite` connection will have `my_` as their table prefix: 150 | 151 | [source, js] 152 | ---- 153 | await Database 154 | .table('users') 155 | .select('*') 156 | ---- 157 | 158 | .SQL Output 159 | [source, sql] 160 | ---- 161 | select * from `my_users` 162 | ---- 163 | 164 | ==== withOutPrefix 165 | If a `prefix` value is defined you can ignore it by calling `withOutPrefix`: 166 | 167 | [source, js] 168 | ---- 169 | await Database 170 | .withOutPrefix() 171 | .table('users') 172 | ---- 173 | 174 | == Debugging 175 | Debugging database queries can be handy in both development and production. 176 | 177 | Let's go through the available strategies to debug queries. 178 | 179 | === Globally 180 | Setting `debug: true` inside the `database/config.js` file enables debugging for all queries globally: 181 | 182 | .config/database.js 183 | [source, js] 184 | ---- 185 | module.exports = { 186 | connection: 'sqlite', 187 | 188 | sqlite: { 189 | client: 'sqlite3', 190 | connection: {}, 191 | debug: true 192 | } 193 | } 194 | ---- 195 | 196 | You can also debug queries via the *Database Provider* `query` event. 197 | 198 | Listen for the `query` event by defining a hook inside the `start/hooks.js` file: 199 | 200 | .start/hooks.js 201 | [source, js] 202 | ---- 203 | const { hooks } = require('@adonisjs/ignitor') 204 | 205 | hooks.after.providersBooted(() => { 206 | const Database = use('Database') 207 | Database.on('query', console.log) 208 | }) 209 | ---- 210 | 211 | NOTE: Create the `start/hooks.js` file if it does not exist. 212 | 213 | === Locally 214 | You can listen for the `query` event per query at runtime: 215 | 216 | [source, js] 217 | ---- 218 | await Database 219 | .table('users') 220 | .select('*') 221 | .on('query', console.log) 222 | ---- 223 | 224 | //// 225 | === Slow query logs 226 | Tracking slow SQL queries is helpful to keep your app running smoothly. 227 | 228 | AdonisJs makes it easy to track slow SQL queries by listening for the `slow:query` event: 229 | 230 | [source, js] 231 | ---- 232 | Database.on('slow:query', (sql, time) => { 233 | console.log(`${time}: ${sql.query}`) 234 | }) 235 | ---- 236 | 237 | The configuration for slow queries is saved next to the connection settings in the `config/database.js` file: 238 | 239 | [source, js] 240 | ---- 241 | module.exports = { 242 | connection: 'sqlite', 243 | 244 | sqlite: { 245 | client: 'sqlite3', 246 | slowQuery: { 247 | enabled: true, 248 | threshold: 5000 249 | } 250 | } 251 | } 252 | ---- 253 | //// 254 | -------------------------------------------------------------------------------- /07-Database/04-Seeding.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Seeds & Factories 3 | permalink: seeds-and-factories 4 | category: database 5 | --- 6 | 7 | = Seeds & Factories 8 | 9 | toc::[] 10 | 11 | Once you've prepared your database schema with link:migrations[migrations], the next step is to add some data. This is where database *seeds* and *factories* come into the picture. 12 | 13 | == Seeds 14 | Seeds are JavaScript classes containing a `run` method. Within the `run` method you are free to write any database related operations your seed requires. 15 | 16 | Like migrations, a seed file is created using the `adonis make` command: 17 | 18 | [source, bash] 19 | ---- 20 | > adonis make:seed User 21 | ---- 22 | 23 | .Output 24 | [source, bash] 25 | ---- 26 | ✔ create database/seeds/UserSeeder.js 27 | ---- 28 | 29 | Now open this file and type the following code inside it: 30 | 31 | .database/seeds/UserSeeder.js 32 | [source, js] 33 | ---- 34 | const Factory = use('Factory') 35 | const Database = use('Database') 36 | 37 | class UserSeeder { 38 | async run () { 39 | const users = await Database.table('users') 40 | console.log(users) 41 | } 42 | } 43 | 44 | module.exports = UserSeeder 45 | ---- 46 | 47 | Run the seed file by calling the `adonis seed` command, which will execute the `run` method on all existing seed files. 48 | 49 | Since you can write any database related code inside your seed files and execute them from the command line, they are helpful in offloading some tasks from your actual application code. 50 | 51 | However, the real power of seeds is unlocked when combined with *Factories*. 52 | 53 | == Factories 54 | Factories define data structures (blueprints) used to generate dummy data. 55 | 56 | Factory blueprints are set inside the `database/factory.js` file: 57 | 58 | [source, js] 59 | ---- 60 | const Factory = use('Factory') 61 | const Hash = use('Hash') 62 | 63 | Factory.blueprint('App/Models/User', async (faker) => { 64 | return { 65 | username: faker.username(), 66 | email: faker.email(), 67 | password: await Hash.make(faker.password()) 68 | } 69 | }) 70 | ---- 71 | 72 | When a model instance is generated from a factory blueprint, the model's attributes are prefilled using the keys defined inside the blueprint: 73 | 74 | [source, js] 75 | ---- 76 | const user = await Factory 77 | .model('App/Models/User') 78 | .create() 79 | ---- 80 | 81 | Many model instances can be generated at the same time: 82 | 83 | [source, js] 84 | ---- 85 | const usersArray = await Factory 86 | .model('App/Models/User') 87 | .createMany(5) 88 | ---- 89 | 90 | === Creating Relationships 91 | Say we want to create a `User` model and relate a `Post` to it. 92 | 93 | NOTE: For the example below, a `posts` relationship must first be defined on the User model. Learn more about relationships link:relationships[here]. 94 | 95 | First, create blueprints for both models in the `database/factory.js` file: 96 | 97 | .database/factory.js 98 | [source, js] 99 | ---- 100 | // User blueprint 101 | Factory.blueprint('App/Models/User', (faker) => { 102 | return { 103 | username: faker.username(), 104 | password: faker.password() 105 | } 106 | }) 107 | 108 | // Post blueprint 109 | Factory.blueprint('App/Models/Post', (faker) => { 110 | return { 111 | title: faker.sentence(), 112 | body: faker.paragraph() 113 | } 114 | }) 115 | ---- 116 | 117 | Then, create a `User`, make a `Post`, and associate both models to each other: 118 | 119 | [source, js] 120 | ---- 121 | const user = await Factory.model('App/Models/User').create() 122 | const post = await Factory.model('App/Models/Post').make() 123 | 124 | await user.posts().save(post) 125 | ---- 126 | 127 | You may have noticed that we used the `make` method on the `Post` blueprint. 128 | 129 | Unlike the `create` method, the `make` method does not persist the `Post` model to the database, instead returning an unsaved instance of the `Post` model pre-filled with dummy data (the `Post` model is saved when the `.posts().save()` method is called). 130 | 131 | == Seed Commands 132 | Below is the list of available seed commands. 133 | 134 | [options="header", cols="30, 20, 50"] 135 | |==== 136 | | Command | Options | Description 137 | | `adonis make:seed` | None | Make a new seed file. 138 | | `adonis seed` | `--files` | Execute seed files (you can optionally pass a comma-separated list of `--files` to be executed, otherwise, all files get executed). 139 | |==== 140 | 141 | == Model Factory API 142 | Below is the list of available methods when using link:lucid[Lucid model] factories. 143 | 144 | ==== create 145 | Persist and return model instance: 146 | 147 | [source, js] 148 | ---- 149 | await Factory 150 | .model('App/Models/User') 151 | .create() 152 | ---- 153 | 154 | ==== createMany 155 | Persist and return many model instances: 156 | 157 | [source, js] 158 | ---- 159 | await Factory 160 | .model('App/Models/User') 161 | .createMany(3) 162 | ---- 163 | 164 | ==== make 165 | Return model instance but do not persist it to the database: 166 | 167 | [source, js] 168 | ---- 169 | await Factory 170 | .model('App/Models/User') 171 | .make() 172 | ---- 173 | 174 | ==== makeMany 175 | Return array of model instances but do not persist them to the database: 176 | 177 | [source, js] 178 | ---- 179 | await Factory 180 | .model('App/Models/User') 181 | .makeMany(3) 182 | ---- 183 | 184 | == Usage Without Lucid 185 | If your application doesn't use link:lucid[Lucid models] you can still use the link:query-builder[Database Provider] to generate factory database records. 186 | 187 | ==== blueprint 188 | 189 | To define your factory blueprint without Lucid, pass a table name as the first parameter instead of a model name (e.g. `users` instead of `App/Models/User`): 190 | 191 | [source, js] 192 | ---- 193 | Factory.blueprint('users', (faker) => { 194 | return { 195 | username: faker.username(), 196 | password: faker.password() 197 | } 198 | }) 199 | ---- 200 | 201 | ==== create 202 | Created a table record: 203 | 204 | [source, js] 205 | ---- 206 | run () { 207 | await Factory.get('users').create() 208 | } 209 | ---- 210 | 211 | ==== table 212 | Define a different table name at runtime: 213 | 214 | [source, js] 215 | ---- 216 | await Factory 217 | .get('users') 218 | .table('my_users') 219 | .create() 220 | ---- 221 | 222 | ==== returning 223 | For PostgreSQL, define a returning column: 224 | 225 | [source, js] 226 | ---- 227 | await Factory 228 | .get('users') 229 | .returning('id') 230 | .create() 231 | ---- 232 | 233 | ==== connection 234 | Choose a different connection at runtime: 235 | 236 | [source, js] 237 | ---- 238 | await Factory 239 | .get('users') 240 | .connection('mysql') 241 | .returning('id') 242 | .create() 243 | ---- 244 | 245 | ==== createMany 246 | Create multiple records: 247 | 248 | [source, js] 249 | ---- 250 | await Factory 251 | .get('users') 252 | .createMany(3) 253 | ---- 254 | 255 | == Custom Data 256 | The methods `make`, `makeMany`, `create` and `createMany` accept a custom data object which is passed directly to your blueprints. 257 | 258 | For example: 259 | 260 | [source, js] 261 | ---- 262 | const user = await Factory 263 | .model('App/Models/User') 264 | .create({ status: 'admin' }) 265 | ---- 266 | 267 | Inside your blueprint, your custom data object is consumed like so: 268 | 269 | [source, js] 270 | ---- 271 | Factory.blueprint('App/Models/User', async (faker, i, data) => { 272 | return { 273 | username: faker.username(), 274 | status: data.status 275 | } 276 | }) 277 | ---- 278 | 279 | == Faker API 280 | The `faker` object passed to a factory blueprint is a reference to the link:http://chancejs.com[Chance, window="_blank"] random generator JavaScript library. 281 | 282 | Make sure to read Chance's link:http://chancejs.com[documentation, window="_blank"] for the full list of available `faker` methods and properties. 283 | 284 | == FAQ's 285 | Since factories and seeds fit many different use cases you might be confused how/when to use them, so here is a list of frequently asked questions. 286 | 287 | [ol-spaced] 288 | 1. *Do factories and seeds have to be used together?* + 289 | No. Factories and seeds are not dependent upon each other and can be used independently. For example, you could just use seed files to import data into an AdonisJs app from a completely different app. 290 | 291 | 2. *Can I use factories when writing tests?* + 292 | Yes. Import the factory provider (`Factory`) into your test and use as required. 293 | 294 | 3. *Can I run only selected seed files?* + 295 | Yes. Passing `--files` with a list of comma-separated filenames to the `adonis seed` command ensures only those files are run, for example: 296 | + 297 | [source, bash] 298 | ---- 299 | > adonis seed --files='UsersSeeder.js, PostsSeeder.js' 300 | ---- 301 | -------------------------------------------------------------------------------- /07-Database/05-Redis.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Redis 3 | permalink: redis 4 | category: database 5 | --- 6 | = Redis 7 | 8 | toc::[] 9 | 10 | AdonisJs has first class support for link:https://redis.io/[Redis, window="blank"] built on top of link:https://github.com/luin/ioredis[ioredis, window="_blank"] with a better pub/sub API. 11 | 12 | NOTE: Configuration, events API and all *ioredis* methods are 100% supported. See the link:https://github.com/luin/ioredis[ioredis, window="_blank"] repository for full documentation. 13 | 14 | == Setup 15 | As the *Redis Provider* is not installed by default, we need to pull it from `npm`: 16 | 17 | [source, bash] 18 | ---- 19 | > adonis install @adonisjs/redis 20 | ---- 21 | 22 | Next, register the provider inside the `start/app.js` file: 23 | 24 | .start/app.js 25 | [source, js] 26 | ---- 27 | const providers = [ 28 | '@adonisjs/redis/providers/RedisProvider' 29 | ] 30 | ---- 31 | 32 | NOTE: Redis configuration is saved inside the `config/redis.js` file, which is created by the `adonis install` command when installing the *Redis Provider*. 33 | 34 | == Basic Example 35 | Let’s start with a basic example of caching users inside Redis: 36 | 37 | .app/Controllers/Http/UserController.js 38 | [source, js] 39 | ---- 40 | 41 | 'use strict' 42 | 43 | const Redis = use('Redis') 44 | const User = use('App/Models/User') 45 | 46 | class UserController { 47 | 48 | async index () { 49 | const cachedUsers = await Redis.get('users') 50 | if (cachedUsers) { 51 | return JSON.parse(cachedUsers) 52 | } 53 | 54 | const users = await User.all() 55 | await Redis.set('users', JSON.stringify(users)) 56 | return users 57 | } 58 | } 59 | ---- 60 | 61 | NOTE: The above example may not be the best way to cache data – it simply provides an idea on how to use Redis. 62 | 63 | == Commands 64 | All link:http://redis.io/commands[Redis commands, window="_blank"] are supported as JavaScript functions, for example: 65 | 66 | [source, js] 67 | ---- 68 | const Redis = use('Redis') 69 | 70 | const user = { 71 | username: 'foo', 72 | email: 'foo@bar.com' 73 | } 74 | 75 | // set user 76 | await Redis.hmset('users', user.username, JSON.stringify(user)) 77 | 78 | // get user 79 | const user = await Redis.hmget('users', user.username) 80 | ---- 81 | 82 | == Pub/Sub 83 | Redis has built-in support for publish/subscribe (pub/sub) to share messages on the same server or across multiple servers. 84 | 85 | AdonisJs offers a clean API on top of Redis pub/sub to subscribe to different events and act upon them. 86 | 87 | Set your Redis subscribers in the `start/redis.js` file: 88 | 89 | .start/redis.js 90 | [source, js] 91 | ---- 92 | 'use strict' 93 | 94 | const Redis = use('Redis') 95 | 96 | Redis.subscribe('music', async (track) => { 97 | console.log('received track', track) 98 | }) 99 | ---- 100 | 101 | NOTE: Create the `start/redis.js` file if it does not exist and load it inside your `server.js`: `.preLoad('start/redis')`. 102 | 103 | Once a subscriber has been registered, you can publish data to this channel from the same or different server: 104 | 105 | [source, js] 106 | ---- 107 | const Redis = use('Redis') 108 | 109 | Redis.publish('music', track) 110 | ---- 111 | 112 | === Available Methods 113 | Below is the list of methods to interact with the pub/sub layer of Redis. 114 | 115 | NOTE: You can only have one subscriber for a given channel. 116 | 117 | ==== subscribe(channel, listener) 118 | [source, js] 119 | ---- 120 | Redis.subscribe('music', (track) { 121 | console.log(track) 122 | }) 123 | ---- 124 | 125 | You can also pass a `file.method` reference from the `app/Listeners` directory: 126 | 127 | [source, js] 128 | ---- 129 | Redis.subscribe('music', 'Music.newTrack') 130 | ---- 131 | 132 | .app/Listeners/Music.js 133 | [source, js] 134 | ---- 135 | 'use strict' 136 | 137 | const Music = exports = module.exports = {} 138 | 139 | Music.newTrack = (track) => { 140 | console.log(track) 141 | } 142 | ---- 143 | 144 | ==== psubscribe(pattern, listener) 145 | Subscribe to a pattern: 146 | 147 | [source, js] 148 | ---- 149 | Redis.psubscribe('h?llo', function (pattern, message, channel) { 150 | }) 151 | 152 | Redis.publish('hello') 153 | Redis.publish('hallo') 154 | ---- 155 | 156 | ==== publish(channel, message) 157 | Publish message to a given channel: 158 | 159 | [source, js] 160 | ---- 161 | Redis.publish('music', JSON.stringify({ 162 | id: 1, 163 | title: 'Love me like you do', 164 | artist: 'Ellie goulding' 165 | })) 166 | ---- 167 | 168 | ==== unsubscribe(channel) 169 | Unsubscribe from a given channel: 170 | 171 | [source, js] 172 | ---- 173 | Redis.unsubscribe('music') 174 | ---- 175 | 176 | ==== punsubscribe(channel) 177 | Unsubscribe from a given pattern: 178 | 179 | [source, js] 180 | ---- 181 | Redis.punsubscribe('h?llo') 182 | ---- 183 | 184 | == Multiple connections 185 | You can define the configuration for multiple connections inside the `config/redis.js` file, and you can use those connections by calling the `connection` method: 186 | 187 | .config/redis.js 188 | [source, js] 189 | ---- 190 | module.exports = { 191 | connection: 'local', 192 | 193 | local: { 194 | ... 195 | }, 196 | 197 | secondary: { 198 | host: 'myhost.com', 199 | port: 6379 200 | } 201 | } 202 | ---- 203 | 204 | ==== connection(name) 205 | Use a different connection to make Redis queries: 206 | 207 | [source, js] 208 | ---- 209 | await Redis 210 | .connection('secondary') 211 | .get('users') 212 | 213 | // hold reference to connection 214 | const secondaryConnection = Redis.connection('secondary') 215 | await secondaryConnection.get('users') 216 | ---- 217 | 218 | ==== quit(name) 219 | The Redis Provider creates a connection pool to reuse existing connections. 220 | 221 | You can quit a connection by calling the `quit` method passing a single connection or array of connections: 222 | 223 | [source, js] 224 | ---- 225 | await Redis.quit('primary') 226 | await Redis.quit(['primary', 'secondary']) 227 | ---- 228 | 229 | -------------------------------------------------------------------------------- /08-Lucid-ORM/02-Hooks.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks 3 | permalink: database-hooks 4 | category: lucid-orm 5 | --- 6 | 7 | = Hooks 8 | 9 | toc::[] 10 | 11 | *Hooks* are actions performed before or after database link:#_lifecycle_events[lifecycle events]. 12 | 13 | Using model hooks helps keep your codebase link:https://en.wikipedia.org/wiki/Don%27t_repeat_yourself[DRY, window="_blank"], providing convenient lifecycle code injection from wherever your hooks are defined. 14 | 15 | A classic hook example is link:encryption-and-hashing#_hashing_values[hashing] the user password before saving the user to a database. 16 | 17 | == Defining Hooks 18 | Hooks can be defined in a model class file via a closure, or by referencing any `file.method` handler in the `app/Models/Hooks` directory. 19 | 20 | === Binding Closure 21 | [source, js] 22 | ---- 23 | const Model = use('Model') 24 | const Hash = use('Hash') 25 | 26 | class User extends Model { 27 | static boot () { 28 | super.boot() 29 | 30 | this.addHook('beforeCreate', async (userInstance) => { 31 | userInstance.password = await Hash.make(userInstance.password) 32 | }) 33 | } 34 | } 35 | 36 | module.exports = User 37 | ---- 38 | 39 | === Hook Within Transactions 40 | To use hooks within link:lucid#_transactions[transactions] just add second param `trx` into hook closure 41 | 42 | [source, js] 43 | ---- 44 | const Model = use('Model') 45 | const Hash = use('Hash') 46 | 47 | class User extends Model { 48 | static boot () { 49 | super.boot() 50 | 51 | this.addHook('beforeCreate', async (userInstance, trx) => { 52 | // here you can use transaction object `trx` 53 | }) 54 | } 55 | } 56 | 57 | module.exports = User 58 | ---- 59 | 60 | In the example above, the `beforeCreate` closure is executed when creating a `User` model to ensure the user's password is hashed before it's saved. 61 | 62 | === Hook File 63 | AdonisJs has a dedicated `app/Models/Hooks` directory to store model hooks. 64 | 65 | Use the `make:hook` command to create a hook file: 66 | 67 | [source, bash] 68 | ---- 69 | > adonis make:hook User 70 | ---- 71 | 72 | .Output 73 | [source, bash] 74 | ---- 75 | ✔ create app/Models/Hooks/UserHook.js 76 | ---- 77 | 78 | Open the new `UserHook.js` file and paste in the code below: 79 | 80 | .app/Models/Hooks/UserHook.js 81 | [source, js] 82 | ---- 83 | 'use strict' 84 | 85 | const Hash = use('Hash') 86 | 87 | const UserHook = exports = module.exports = {} 88 | 89 | UserHook.hashPassword = async (user) => { 90 | user.password = await Hash.make(user.password) 91 | } 92 | ---- 93 | 94 | With a hook `file.method` defined, we can remove the inline closure from our link:#_binding_closure[previous example] and instead reference the hook file and method like so: 95 | 96 | [source, js] 97 | ---- 98 | const Model = use('Model') 99 | 100 | class User extends Model { 101 | static boot () { 102 | super.boot() 103 | this.addHook('beforeCreate', 'UserHook.hashPassword') 104 | } 105 | } 106 | 107 | module.exports = User 108 | ---- 109 | 110 | == Aborting Database Operations 111 | Hooks can abort database operations by throwing exceptions: 112 | 113 | .app/Models/Hooks/UserHook.js 114 | [source, javascript] 115 | ---- 116 | UserHook.validate = async (user) => { 117 | if (!user.username) { 118 | throw new Error('Username is required') 119 | } 120 | } 121 | ---- 122 | 123 | == Lifecycle Events 124 | Below is the list of available database lifecycle events to hook into: 125 | 126 | [options="header", cols="35, 65"] 127 | |==== 128 | | Event | Description 129 | | `beforeCreate` | Before creating a new record. 130 | | `afterCreate` | After a new record is created. 131 | | `beforeUpdate` | Before updating a record. 132 | | `afterUpdate` | After a record has been updated. 133 | | `beforeSave` | Before *creating or updating* a new record. 134 | | `afterSave` | After a new record has been *created or updated*. 135 | | `beforeDelete` | Before removing a record. 136 | | `afterDelete` | After a record is removed. 137 | | `afterFind` | After a single record is fetched from the database. 138 | | `afterFetch` | After the `fetch` method is executed. The hook method receives an array of model instances. 139 | | `afterPaginate` | After the `paginate` method is executed. The hook method receives two arguments: an array of model instances and the pagination metadata. 140 | |==== 141 | -------------------------------------------------------------------------------- /08-Lucid-ORM/03-Traits.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Traits 3 | permalink: traits 4 | category: lucid-orm 5 | --- 6 | = Traits 7 | 8 | toc::[] 9 | 10 | *Traits* make it possible to add functionality to models *from the outside in*. 11 | 12 | Using model traits, you can: 13 | 14 | [ol-shrinked] 15 | 1. Add new methods to your model class. 16 | 2. Listen for model link:database-hooks[hooks]. 17 | 3. Add methods to the link:lucid#_query_builder[Query Builder] instance for a given model. 18 | 19 | == Creating a Trait 20 | Traits are stored in the `app/Models/Traits` directory. 21 | 22 | Use the `make:trait` command to generate a trait file: 23 | 24 | [source, bash] 25 | ---- 26 | > adonis make:trait Slugify 27 | ---- 28 | 29 | .Output 30 | [source, bash] 31 | ---- 32 | ✔ create app/Models/Traits/Slugify.js 33 | ---- 34 | 35 | Traits require a `register` method receiving the `Model` class and an `customOptions` object as its parameters: 36 | 37 | .app/Models/Traits/Slugify.js 38 | [source, js] 39 | ---- 40 | 'use strict' 41 | 42 | class Slugify { 43 | register (Model, customOptions = {}) { 44 | const defaultOptions = {} 45 | const options = Object.assign(defaultOptions, customOptions) 46 | } 47 | } 48 | 49 | module.exports = Slugify 50 | ---- 51 | 52 | == Registering a Trait 53 | Add a trait to a Lucid model like so: 54 | 55 | [source, js] 56 | ---- 57 | const Model = use('Model') 58 | 59 | class User extends Model { 60 | static boot () { 61 | super.boot() 62 | this.addTrait('Slugify') 63 | } 64 | } 65 | ---- 66 | 67 | == Registering a Trait with Options 68 | If required, you can pass initialization options when adding a trait: 69 | 70 | [source, js] 71 | ---- 72 | const Model = use('Model') 73 | 74 | class User extends Model { 75 | static boot () { 76 | super.boot() 77 | this.addTrait('Slugify', {useCamelCase: true}) 78 | } 79 | } 80 | ---- 81 | 82 | The options you pass will be forwarded to the trait's `register()` method. 83 | 84 | When passing options, it's recommended you define *default trait options* like so: 85 | 86 | .app/Models/Traits/Slugify.js 87 | [source, js] 88 | ---- 89 | const _ = require('lodash') 90 | 91 | class Slugify { 92 | 93 | register (Model, customOptions) { 94 | const defaultOptions = {useCamelCase: false} 95 | const options = _.extend({}, defaultOptions, customOptions) 96 | } 97 | } 98 | 99 | module.exports = Slugify 100 | ---- 101 | 102 | == Extending Model Methods 103 | Use traits to add static and instance model methods: 104 | 105 | .app/Models/Traits/Slugify.js 106 | [source, js] 107 | ---- 108 | class Slugify { 109 | 110 | register (Model, options) { 111 | // Add a static method 112 | Model.newAdminUser = function () { 113 | let m = new Model() 114 | m.isAdmin = true 115 | return m 116 | } 117 | 118 | // Add an instance method 119 | Model.prototype.printUsername = function () { 120 | console.log(this.username) 121 | } 122 | } 123 | } 124 | 125 | module.exports = Slugify 126 | ---- 127 | 128 | == Adding Model Hooks 129 | Use traits to link:database-hooks[hook] into database lifecycle events: 130 | 131 | [source, js] 132 | ---- 133 | class Slugify { 134 | 135 | register (Model, options) { 136 | Model.addHook('beforeCreate', function (modelInstance) { 137 | // create slug 138 | }) 139 | } 140 | } 141 | 142 | module.exports = Slugify 143 | ---- 144 | 145 | == Extending Query Builder 146 | Use traits to add macros to a model's link:lucid#_query_builder[Query Builder]: 147 | 148 | [source, js] 149 | ---- 150 | class Slugify { 151 | 152 | register (Model, options) { 153 | Model.queryMacro('whereSlug', function (value) { 154 | this.where('slug', value) 155 | return this 156 | }) 157 | } 158 | } 159 | 160 | module.exports = Slugify 161 | ---- 162 | 163 | .Usage 164 | [source, js] 165 | ---- 166 | await User.query().whereSlug('some value') 167 | ---- 168 | -------------------------------------------------------------------------------- /08-Lucid-ORM/04-Mutators.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: database-getters-setters 3 | title: Mutators 4 | category: lucid-orm 5 | --- 6 | 7 | = Mutators 8 | 9 | toc::[] 10 | 11 | *Getters and setters* provide link:https://stackoverflow.com/a/1568230/1210490[many benefits, window="_blank"], including the ability to transform your data before saving and retrieving from a database. 12 | 13 | In this guide, we learn when and where to use getters, setters and computed properties (also known as *accessors and mutators*). 14 | 15 | == Getters 16 | *Getters* are called when retrieving a value from a model instance. 17 | 18 | They are often used to transform model data for display. 19 | 20 | For example, converting a `Post` title to title case: 21 | 22 | .app/Models/Post.js 23 | [source, js] 24 | ---- 25 | 'use strict' 26 | 27 | const Model = use('Model') 28 | 29 | class Post extends Model { 30 | getTitle (title) { 31 | return title.replace(/^(.)|\s(.)/g, ($1) => { 32 | return $1.toUpperCase() 33 | }) 34 | } 35 | } 36 | ---- 37 | 38 | [source, js] 39 | ---- 40 | const post = await Post.find(postId) 41 | 42 | // getters are called automatically 43 | return post.toJSON() 44 | ---- 45 | 46 | In the example above, assuming the `Post` title is saved as a `title` field in the database, AdonisJs executes the `getTitle` method and uses the returned value when `post.title` is referenced. 47 | 48 | [ul-spaced] 49 | - Getters always start with the `get` keyword followed by the *camel case* version of the field name (e.g. `field_name` → `getFieldName`). 50 | - A getter's return value is used instead of the actual database field name value when that field is referenced on a model instance. 51 | - Getters are automatically evaluated when you call `toJSON` on a model instance or link:serializers[serializer] instance. 52 | - As getters are synchronous, you cannot run asynchronous code inside them (for asynchronous functionality, use link:database-hooks[hooks]). 53 | 54 | == Setters 55 | *Setters* are called when assigning a value to a model instance. 56 | 57 | They are often used to normalize data before saving to a database: 58 | 59 | .app/Models/User.js 60 | [source, js] 61 | ---- 62 | 'use strict' 63 | 64 | const Model = use('Model') 65 | 66 | class User extends Model { 67 | setAccess (access) { 68 | return access === 'admin' ? 1 : 0 69 | } 70 | } 71 | ---- 72 | 73 | [source, js] 74 | ---- 75 | const user = new User() 76 | user.access = 'admin' 77 | 78 | console.log(user.access) // will return 1 79 | await user.save() 80 | ---- 81 | 82 | [ul-spaced] 83 | - Setters always starts with the `set` keyword followed by the *camel case* version of the field name. 84 | - A setter executes when you set/update the value of the given field on the model instance. 85 | - Setters receive the current value of a given field to parse before assignment. 86 | - As setters are synchronous, you cannot run asynchronous code inside them (for asynchronous functionality, use link:database-hooks[hooks]). 87 | 88 | == Computed Properties 89 | Computed properties are virtual values which only exist in a model instance's JSON representation. 90 | 91 | To create a computed `fullname` property from a `User` first/last name: 92 | 93 | .app/Models/User.js 94 | [source, js] 95 | ---- 96 | 'use strict' 97 | 98 | const Model = use('Model') 99 | 100 | class User extends Model { 101 | static get computed () { 102 | return ['fullname'] 103 | } 104 | 105 | getFullname ({ firstname, lastname }) { 106 | return `${firstname} ${lastname}` 107 | } 108 | } 109 | ---- 110 | 111 | In the example above, when `toJSON` is called on the `User` instance, a `fullname` property gets added to the return value: 112 | 113 | [source, js] 114 | ---- 115 | const user = await User.find(1) 116 | 117 | const json = user.toJSON() 118 | console.log(json.fullname) // firstname + lastname 119 | ---- 120 | 121 | [ul-spaced] 122 | - All computed property names (e.g. `fullname`) must be returned in an array from the model class static `computed` getter. 123 | - Computed property method definitions are prefixed with `get`, the same as getter method definitions (e.g. `getFullname`). 124 | - Computed properties receive an object of existing model attributes for use in their method definitions. 125 | -------------------------------------------------------------------------------- /08-Lucid-ORM/06-Serialization.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Serialization 3 | permalink: serializers 4 | category: lucid-orm 5 | --- 6 | 7 | = Serialization 8 | 9 | toc::[] 10 | 11 | *Serializers* provide clean abstractions to transform database results. 12 | 13 | AdonisJs ships with the default link:https://github.com/adonisjs/adonis-lucid/blob/develop/src/Lucid/Serializers/Vanilla.js[Vanilla Serializer, window="_blank"], but you are free to create and use any serializer your application requires. 14 | 15 | A common serializer usage is to format data as per the link:http://jsonapi.org/[JSON:API, window="_blank"] spec. 16 | 17 | == Introduction 18 | Database queries made via link:lucid[Lucid models] return serializable instances: 19 | 20 | [source, js] 21 | ---- 22 | const User = use('App/Models/User') 23 | 24 | const users = await User.all() 25 | 26 | // users -> Vanilla Serializer instance 27 | ---- 28 | 29 | To convert a serializable instance to a plain array/object, call its `toJSON` method: 30 | 31 | [source, js] 32 | ---- 33 | const json = users.toJSON() 34 | ---- 35 | 36 | Calling `toJSON` on any serializable instance returns data ready for JSON output. 37 | 38 | === Why Use Serializers? 39 | When writing an API server, it's unlikely you'll want to return unserialized model instance data to your users. 40 | 41 | *Serializers* solve this problem by formatting model data when required. 42 | 43 | Assuming a `User` can have many `Post` relations: 44 | 45 | [source, js] 46 | ---- 47 | const User = use('App/Models/User') 48 | 49 | const users = await User 50 | .query() 51 | .with('posts') 52 | .fetch() 53 | ---- 54 | 55 | In the example above, Lucid loads all `User` models and their `Post` relations, but doesn't format the loaded data for JSON at this point in time. 56 | 57 | When `toJSON` is finally called on `users`, the responsibility of formatting the data is delegated to the Vanilla Serializer: 58 | 59 | [source, js] 60 | ---- 61 | // serialize the data 62 | users.toJSON() 63 | ---- 64 | 65 | .Output 66 | [source, js] 67 | ---- 68 | [ 69 | { 70 | id: 1, 71 | username: 'virk', 72 | posts: [ 73 | { 74 | id: 1, 75 | user_id: 1, 76 | title: 'Adonis 101' 77 | } 78 | ] 79 | } 80 | ] 81 | ---- 82 | 83 | A serializer executes all `getters`, `setters` and `computed properties` before returning formatted model data. 84 | 85 | == Using Serializer 86 | Serializers can be defined per model by overriding the `Serializer` getter: 87 | 88 | .app/Models/User.js 89 | [source, js] 90 | ---- 91 | class User extends Model { 92 | static get Serializer () { 93 | return // your own implementation 94 | } 95 | } 96 | ---- 97 | 98 | == Vanilla Serializer 99 | The link:https://github.com/adonisjs/adonis-lucid/blob/develop/src/Lucid/Serializers/Vanilla.js[Vanilla Serializer, window="_blank"] performs the following operations: 100 | 101 | 1. Attach all relations next to each model record as a property. 102 | 2. Attach all `sideloaded` data to the root `\___meta___` key, for example, *posts counts* for a given user are represented like so: 103 | + 104 | [source, js] 105 | ---- 106 | { 107 | id: 1, 108 | username: 'virk', 109 | __meta__: { 110 | posts_count: 2 111 | } 112 | } 113 | ---- 114 | 3. Format pagination results: 115 | + 116 | [source, js] 117 | ---- 118 | { 119 | total: 10, 120 | perPage: 20, 121 | lastPage: 1, 122 | currentPage: 1, 123 | data: [] 124 | } 125 | ---- 126 | 127 | == Creating Serializer 128 | Create your own serializer to return data in a format not provided by AdonisJs. 129 | 130 | The serializer API is intentionally small to make it easy to add new serializers. 131 | 132 | NOTE: Avoid custom serializers for small amends to JSON output. Instead, use `getters` and `computed properties`. 133 | 134 | === API Overview 135 | Below is an example template for a custom serializer: 136 | 137 | .app/Serializers/CustomSerializer.js 138 | [source, js] 139 | ---- 140 | class CustomSerializer { 141 | constructor (rows, pages = null, isOne = false) { 142 | this.rows = rows 143 | this.pages = pages 144 | this.isOne = isOne 145 | } 146 | 147 | first () { 148 | return this.rows[0] 149 | } 150 | 151 | last () { 152 | return this.rows[this.rows.length - 1] 153 | } 154 | 155 | size () { 156 | return this.isOne ? 1 : this.rows.length 157 | } 158 | 159 | toJSON () { 160 | // return formatted data 161 | } 162 | } 163 | 164 | module.exports = CustomSerializer 165 | ---- 166 | 167 | Once your custom serializer is created, bind it to the link:ioc-container[IoC container]: 168 | 169 | .start/hooks.js 170 | [source, js] 171 | ---- 172 | const { ioc } = require('@adonisjs/fold') 173 | 174 | ioc.bind('MyApp/CustomSerializer', () => { 175 | return require('./app/Serializers/CustomSerializer') 176 | }) 177 | ---- 178 | 179 | Once bound to the container, define your custom serializer per model: 180 | 181 | .app/Models/User.js 182 | [source, js] 183 | ---- 184 | class User extends Model { 185 | static get Serializer () { 186 | return 'MyApp/CustomSerializer' 187 | } 188 | } 189 | ---- 190 | -------------------------------------------------------------------------------- /09-WebSockets/01-Getting-Started.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | permalink: websocket 4 | category: websockets 5 | --- 6 | 7 | = Getting Started 8 | 9 | toc::[] 10 | 11 | AdonisJs offers a robust *WebSocket Provider* to serve real-time apps. 12 | 13 | The server works on pure link:https://developer.mozilla.org/en-US/docs/Glossary/WebSockets[WebSocket, window="_blank"] connections (supported by all major browsers) and scales naturally within a cluster of Node.js processes. 14 | 15 | == Setup 16 | As the *WebSocket Provider* is not installed by default, pull it from `npm`: 17 | 18 | [source, bash] 19 | ---- 20 | > adonis install @adonisjs/websocket 21 | ---- 22 | 23 | Installing the provider adds the following files to your project: 24 | 25 | [ol-shrinked] 26 | 1. `config/socket.js` contains WebSocket server *configuration*. 27 | 2. `start/socket.js` boots the WebSocket server and registers *channels*. 28 | 3. `start/wsKernel.js` *registers middleware* to execute on channel subscription. 29 | 30 | Next, register the provider inside the `start/app.js` file: 31 | 32 | .start/app.js 33 | [source, js] 34 | ---- 35 | const providers = [ 36 | '@adonisjs/websocket/providers/WsProvider' 37 | ] 38 | ---- 39 | 40 | Finally, instruct link:ignitor[Ignitor] to boot the WebSocket server in the root `server.js` file: 41 | 42 | .server.js 43 | [source, js] 44 | ---- 45 | const { Ignitor } = require('@adonisjs/ignitor') 46 | 47 | new Ignitor(require('@adonisjs/fold')) 48 | .appRoot(__dirname) 49 | .wsServer() // boot the WebSocket server 50 | .fireHttpServer() 51 | .catch(console.error) 52 | ---- 53 | 54 | === Cluster Support 55 | When running a link:https://nodejs.org/api/cluster.html[Node.js cluster, window="_blank"], the master node needs to connect the pub/sub communication between worker nodes. 56 | 57 | To do so, add the following code to the top of the root `server.js` file: 58 | 59 | .server.js 60 | [source, js] 61 | ---- 62 | const cluster = require('cluster') 63 | 64 | if (cluster.isMaster) { 65 | for (let i=0; i < 4; i ++) { 66 | cluster.fork() 67 | } 68 | require('@adonisjs/websocket/clusterPubSub')() 69 | return 70 | } 71 | 72 | // ... 73 | ---- 74 | 75 | == Basic Example 76 | Let's build a single room *chat server* for user messaging. 77 | 78 | To keep things simple we won't store user messages, just deliver them. 79 | 80 | Open the `start/socket.js` file and paste the following code: 81 | 82 | .start/socket.js 83 | [source, js] 84 | ---- 85 | const Ws = use('Ws') 86 | 87 | Ws.channel('chat', 'ChatController') 88 | ---- 89 | 90 | NOTE: We can also bind a closure to the `Ws.channel` method, but having a dedicated controller is the recommended practice. 91 | 92 | Next, create the `ChatController` using the `make:controller` command: 93 | 94 | [source, bash] 95 | ---- 96 | > adonis make:controller Chat --type=ws 97 | ---- 98 | 99 | .Output 100 | [source, bash] 101 | ---- 102 | ✔ create app/Controllers/Ws/ChatController.js 103 | ---- 104 | 105 | .app/Controllers/Ws/ChatController.js 106 | [source, js] 107 | ---- 108 | 'use strict' 109 | 110 | class ChatController { 111 | constructor ({ socket, request }) { 112 | this.socket = socket 113 | this.request = request 114 | } 115 | } 116 | 117 | module.exports = ChatController 118 | ---- 119 | 120 | === Client Code 121 | Let's switch from server to client and subscribe to the `chat` channel. 122 | 123 | First, copy the CSS and HTML template from link:https://gist.github.com/thetutlage/7f0f2252b4d22dad13753ced890051e2[this gist, window="_blank"] to the following locations: 124 | 125 | [ol-shrinked] 126 | 1. *CSS* → `public/style.css` 127 | 2. *HTML template* → `resources/views/chat.edge` 128 | 129 | NOTE: Make sure to link:routing[define a route] to serve the HTML template. 130 | 131 | Next, create a `public/chat.js` file and paste the code below to connect the client to the server (to keep things simple we're using link:https://jquery.com[jQuery, window="_blank"]): 132 | 133 | .public/chat.js 134 | [source, js] 135 | ---- 136 | let ws = null 137 | 138 | $(function () { 139 | // Only connect when username is available 140 | if (window.username) { 141 | startChat() 142 | } 143 | }) 144 | 145 | function startChat () { 146 | ws = adonis.Ws().connect() 147 | 148 | ws.on('open', () => { 149 | $('.connection-status').addClass('connected') 150 | subscribeToChannel() 151 | }) 152 | 153 | ws.on('error', () => { 154 | $('.connection-status').removeClass('connected') 155 | }) 156 | } 157 | ---- 158 | 159 | Then, add the channel subscription method, binding listeners to handle messages: 160 | 161 | .public/chat.js 162 | [source, js] 163 | ---- 164 | // ... 165 | 166 | function subscribeToChannel () { 167 | const chat = ws.subscribe('chat') 168 | 169 | chat.on('error', () => { 170 | $('.connection-status').removeClass('connected') 171 | }) 172 | 173 | chat.on('message', (message) => { 174 | $('.messages').append(` 175 |

${message.username}

${message.body}

176 | `) 177 | }) 178 | } 179 | ---- 180 | 181 | Finally, add the event handler to send a message when the kbd:[Enter] key is released: 182 | 183 | .public/chat.js 184 | [source, js] 185 | ---- 186 | // ... 187 | 188 | $('#message').keyup(function (e) { 189 | if (e.which === 13) { 190 | e.preventDefault() 191 | 192 | const message = $(this).val() 193 | $(this).val('') 194 | 195 | ws.getSubscription('chat').emit('message', { 196 | username: window.username, 197 | body: message 198 | }) 199 | return 200 | } 201 | }) 202 | ---- 203 | 204 | === Server Code 205 | Now finished with the client, let's switch back to the server. 206 | 207 | Add the `onMessage` link:#_event_methods[event method] to the `ChatController` file: 208 | 209 | .app/Controllers/Ws/ChatController.js 210 | [source, js] 211 | ---- 212 | class ChatController { 213 | constructor ({ socket, request }) { 214 | this.socket = socket 215 | this.request = request 216 | } 217 | 218 | onMessage (message) { 219 | this.socket.broadcastToAll('message', message) 220 | } 221 | } 222 | ---- 223 | 224 | In the example above, the `onMessage` method sends the same message to all connected clients via the socket `broadcastToAll` method. 225 | 226 | == Controllers 227 | Controllers keep your code organised by defining separate classes per channel. 228 | 229 | WebSocket controllers are stored in the `app/Controllers/Ws` directory. 230 | 231 | A new controller instance is created per subscription with a `context` object passed to its constructor, enabling the `socket` instance to be unpacked like so: 232 | 233 | [source, js] 234 | ---- 235 | class ChatController { 236 | constructor ({ socket }) { 237 | this.socket = socket 238 | } 239 | } 240 | ---- 241 | 242 | === Event Methods 243 | 244 | Bind to WebSocket events by creating controller methods with the same name: 245 | 246 | [source, js] 247 | ---- 248 | class ChatController { 249 | onMessage () { 250 | // same as: socket.on('message') 251 | } 252 | 253 | onClose () { 254 | // same as: socket.on('close') 255 | } 256 | 257 | onError () { 258 | // same as: socket.on('error') 259 | } 260 | } 261 | ---- 262 | 263 | NOTE: Event methods must be prefixed with the `on` keyword. 264 | -------------------------------------------------------------------------------- /09-WebSockets/02-Philosophy.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Philosophy 3 | permalink: websocket-philosophy 4 | category: websockets 5 | --- 6 | = Philosophy 7 | 8 | toc::[] 9 | 10 | This guide covers the philosophy of the WebSocket server. 11 | 12 | By the end of this guide, you will know about *channels*, *topic subscriptions* and *multiplexing*. 13 | 14 | == Pure WebSockets 15 | AdonisJs uses pure link:https://developer.mozilla.org/en-US/docs/Glossary/WebSockets[WebSockets, window="_blank"] and doesn't rely on polling. 16 | 17 | Using pure WebSockets (supported by all major browsers), AdonisJs makes it easier to *scale apps horizontally within a Node.js cluster* without any 3rd party dependencies (e.g. Redis) and without the need for sticky sessions. 18 | 19 | == Multiplexing 20 | Multiplexing reuses the same TCP connection while separating *event* and *authentication* layers between channels. 21 | 22 | Multiplexing maintains a single connection from client to server, with AdonisJS providing a clean abstraction layer to manage channel subscriptions and exchange your application messages. 23 | 24 | == Channels & Topics 25 | Once a client makes a WebSocket connection, they are required to subscribe to a *topic* in order to exchange messages. 26 | 27 | Channels and topics are interrelated, so let's write some code to better understand them. 28 | 29 | Register a channel like so: 30 | 31 | [source, js] 32 | ---- 33 | Ws.channel('chat', ({ socket }) => { 34 | console.log(socket.topic) 35 | }) 36 | ---- 37 | 38 | In the example above, all subscriptions to the `chat` channel have a static topic called `chat` (i.e. the channel and the topic name are same). 39 | 40 | To register a channel with dynamic/wildcard topics: 41 | 42 | [source, js] 43 | ---- 44 | Ws.channel('chat:*', ({ socket }) => { 45 | console.log(socket.topic) 46 | }) 47 | ---- 48 | 49 | In the example above, the `chat` channel accepts dynamic topics, so a user could subscribe to the channel `chat:watercooler`, `chat:intro`, `chat:news`, etc. 50 | 51 | This dynamic/wildcard approach opens up an exciting world of creative possibilities (e.g. dynamic topics for private chats between two users). 52 | 53 | == Data Encoders 54 | The WebSocket server uses data encoders when passing messages. 55 | 56 | By default, the WebSocket server uses the JSON encoder, which has limitations when passing binary data. 57 | 58 | If required, the AdonisJs link:https://www.npmjs.com/package/@adonisjs/msgpack-encoder[@adonisjs/msgpack-encoder, window="_blank"] package can be used to handle *ArrayBuffers* and *Blobs*. 59 | 60 | == Message Packets 61 | Multiplexing requires a standard to define data packet structure. 62 | 63 | As a consumer of the WebSocket server package you don't have to worry about packet types, but when writing a client for the server, it's critically important to understand them. 64 | 65 | Your WebSocket data encoder decodes network data as an *object* (or equivalent data type for your programming language) containing a structure similar to: 66 | 67 | [source, js] 68 | ---- 69 | { 70 | t: 7, 71 | d: { 72 | topic: 'chat', 73 | data: 'hello world' 74 | } 75 | } 76 | ---- 77 | 78 | 1. The property `t` is the type of the packet (we use numbers over strings, since numbers are less data to transfer). 79 | 2. The property `d` is the data associated with that packet. 80 | 81 | TIP: Learn more about AdonisJs WebSocket packets link:https://github.com/adonisjs/adonis-websocket-protocol[here, window="_blank"]. 82 | -------------------------------------------------------------------------------- /09-WebSockets/03-Server-API.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server API 3 | permalink: websocket-server 4 | category: websockets 5 | --- 6 | 7 | = Server API 8 | 9 | toc::[] 10 | 11 | In this guide we dive deeper into *channels*, *authentication* and exchanging *real time messages*. 12 | 13 | == Registering Channels 14 | WebSocket channels are registered in the `start/socket.js` file: 15 | 16 | .start/socket.js 17 | [source, js] 18 | ---- 19 | const Ws = use('Ws') 20 | 21 | Ws.channel('news', ({ socket }) => { 22 | console.log('a new subscription for news topic') 23 | }) 24 | ---- 25 | 26 | Channel handlers receive a `context` object, similar to HTTP link:request-lifecycle#_http_context[route handlers]. 27 | 28 | By default, channel `context` objects contain `socket` and `request` properties (with more added by optional middleware like `Auth`, `Session`, etc). 29 | 30 | Once a subscription is made, use the `socket` instance to exchange messages: 31 | 32 | [source, js] 33 | ---- 34 | socket.on('message', (data) => { 35 | }) 36 | 37 | // emit events 38 | socket.emit('message', 'Hello world') 39 | socket.emit('typing', true) 40 | ---- 41 | 42 | === Dynamic Topics 43 | Channels can be registered to accept dynamic topic subscriptions: 44 | 45 | [source, js] 46 | ---- 47 | Ws.channel('chat:*', ({ socket }) => { 48 | console.log(socket.topic) 49 | }) 50 | ---- 51 | 52 | In the example above, `*` sets the channel to accept any subscriptions to topics beginning with `chat:` (e.g. `chat:watercooler`, `chat:intro`, etc). 53 | 54 | Subscriptions to dynamic topics are made via the link:websocket-client#_subscribetopic[Client API]: 55 | 56 | [source, js] 57 | ---- 58 | const watercooler = ws.subscribe('chat:watercooler') 59 | const intro = ws.subscribe('chat:intro') 60 | const news = ws.subscribe('chat:news') 61 | ---- 62 | 63 | In the example above, our different topic subscriptions all point to the same channel, but when topic specific events are emitted, they will be delivered to their specific topic subscribers only. 64 | 65 | == Registering Middleware 66 | Middleware is registered inside the `start/wsKernel.js` file: 67 | 68 | .start/wsKernel.js 69 | [source, js] 70 | ---- 71 | const globalMiddleware = [ 72 | 'Adonis/Middleware/Session', 73 | 'Adonis/Middleware/AuthInit' 74 | ] 75 | 76 | const namedMiddleware = { 77 | auth: 'Adonis/Middleware/Auth' 78 | } 79 | ---- 80 | 81 | Named middleware is applied per channel in the `start/socket.js` file: 82 | 83 | .start/socket.js 84 | [source, js] 85 | ---- 86 | Ws 87 | .channel('chat', 'ChatController') 88 | .middleware(['auth']) 89 | ---- 90 | 91 | == Creating Middleware 92 | WebSocket middleware require a `wsHandle` method. 93 | 94 | You can share HTTP and WebSocket middleware by ensuring both `handle` (for HTTP requests) and `wsHandle` methods are defined on your middleware class: 95 | 96 | .app/Middleware/CustomMiddleware.js 97 | [source, js] 98 | ---- 99 | 'use strict' 100 | 101 | class CustomMiddleware { 102 | // for HTTP 103 | async handle (ctx, next) { 104 | } 105 | 106 | // for WebSocket 107 | async wsHandle (ctx, next) { 108 | } 109 | } 110 | 111 | module.exports = CustomMiddleware 112 | ---- 113 | 114 | == Broadcast Anywhere 115 | As pre-registered WebSocket channels can be accessed from anywhere inside your application, WebSocket communication isn't limited to the socket lifecycle. 116 | 117 | Emit WebSocket events during the HTTP lifecycle like so: 118 | 119 | .app/Controllers/Http/UserController.js 120 | [source, js] 121 | ---- 122 | const Ws = use('Ws') 123 | 124 | class UserController { 125 | async register () { 126 | // ... 127 | 128 | const topic = Ws.getChannel('subscriptions').topic('subscriptions') 129 | // if no one is listening, so the `topic('subscriptions')` method will return `null` 130 | if(topic){ 131 | topic.broadcast('new:user') 132 | } 133 | } 134 | } 135 | ---- 136 | 137 | In the example above, we: 138 | 139 | [ol-shrinked] 140 | 1. Select the channel via the `getChannel(name)` method 141 | 2. Select the channel topic via the `topic(name)` method 142 | 3. Broadcast to topic subscribers via the `broadcast(event)` message 143 | 144 | `topic()` returns an object containing the following methods: 145 | 146 | [source, js] 147 | ---- 148 | const chat = Ws.getChannel('chat:*') 149 | const { broadcast, emitTo } = chat.topic('chat:watercooler') 150 | 151 | // broadcast: send to everyone (except the caller) 152 | // emitTo: send to selected socket ids 153 | ---- 154 | 155 | NOTE: For more info, see the list of link:#_methods[socket methods] below. 156 | 157 | == Socket API 158 | 159 | === Events 160 | 161 | The following events are reserved and *must not be emitted*. 162 | 163 | ==== error 164 | Invoked when an error is received: 165 | 166 | [source, js] 167 | ---- 168 | socket.on('error', () => { 169 | }) 170 | ---- 171 | 172 | ==== close 173 | Invoked when a subscription is closed: 174 | 175 | [source, js] 176 | ---- 177 | socket.on('close', () => { 178 | }) 179 | ---- 180 | 181 | === Methods 182 | The following methods can be called on the socket instance. 183 | 184 | ==== emit(event, data, [ackCallback]) 185 | Emit event to the connected client: 186 | 187 | [source, js] 188 | ---- 189 | socket.emit('id', socket.id) 190 | ---- 191 | 192 | NOTE: This method only sends a message to your own connection. 193 | 194 | ==== emitTo(event, data, socketIds[]) 195 | Emit event to an array of socket ids: 196 | 197 | [source, js] 198 | ---- 199 | socket.emitTo('greeting', 'hello', [someIds]) 200 | ---- 201 | 202 | ==== broadcast(event, data) 203 | Emit event to everyone *except* yourself: 204 | 205 | [source, js] 206 | ---- 207 | socket.broadcast('message', 'hello everyone!') 208 | ---- 209 | 210 | ==== broadcastToAll(event, data) 211 | Emit event to everyone *including* yourself: 212 | 213 | [source, js] 214 | ---- 215 | socket.broadcastToAll('message', 'hello everyone!') 216 | ---- 217 | 218 | ==== close() 219 | Forcefully close a subscription from the server: 220 | 221 | [source, js] 222 | ---- 223 | socket.close() 224 | ---- 225 | 226 | === Properties 227 | The following *read-only* properties can be accessed on the socket instance. 228 | 229 | ==== id 230 | Socket unique id: 231 | 232 | [source, js] 233 | ---- 234 | socket.id 235 | ---- 236 | 237 | ==== topic 238 | Topic under which the subscription socket was created: 239 | 240 | [source, js] 241 | ---- 242 | socket.topic 243 | ---- 244 | 245 | ==== connection 246 | Reference to the TCP connection (shared across multiple sockets for a single client for multiplexing): 247 | 248 | [source, js] 249 | ---- 250 | socket.connection 251 | ---- 252 | -------------------------------------------------------------------------------- /09-WebSockets/04-Client-API.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client API 3 | permalink: websocket-client 4 | category: websockets 5 | --- 6 | = Client API 7 | 8 | toc::[] 9 | 10 | This guide covers the JavaScript *WebSocket client* used to connect to the link:websocket-server[WebSocket server]. 11 | 12 | == Installation 13 | 14 | === NPM 15 | As the *WebSocket client* is not installed by default, we need to pull it from `npm`: 16 | 17 | [source, bash] 18 | ---- 19 | > npm i @adonisjs/websocket-client 20 | ---- 21 | 22 | TIP: Once installed, bundle the package using Webpack, Rollup, etc. 23 | 24 | Then, import the WebSocket client like so: 25 | 26 | [source, js] 27 | ---- 28 | import Ws from '@adonisjs/websocket-client' 29 | const ws = Ws('ws://localhost:3333') 30 | ---- 31 | 32 | === UNPKG 33 | 34 | Alternatively, source the UMD bundle directly from link:https://unpkg.com[unpkg, window="_blank"]: 35 | [source, html] 36 | ---- 37 | 38 | 41 | ---- 42 | 43 | == Polyfill 44 | The module build requires the link:https://babeljs.io/docs/plugins/transform-regenerator[regenerator-runtime, window="_blank"] polyfill (add it via link:https://babeljs.io[Babel, window="_blank"]). 45 | 46 | == Production Builds 47 | As the development build contains a number of log statements, we recommend defining `NODE_ENV` via link:https://webpack.js.org/plugins/define-plugin/[Webpack DefinePlugin, window="_blank"] or link:https://github.com/rollup/rollup-plugin-replace[rollup-plugin-replace, window="_blank"]. 48 | 49 | == Getting Started 50 | Connect to a WebSocket server via the client like so: 51 | 52 | [source, js] 53 | ---- 54 | const ws = Ws(url, options) 55 | 56 | // connect to the server 57 | ws.connect() 58 | ---- 59 | 60 | NOTE: The `url` parameter will fallback to the current hostname if a full `ws://` url value is omitted. 61 | 62 | ==== options 63 | 64 | [role="resource-table", options="header", cols="30%, 10%, 60%"] 65 | |=== 66 | | Key | Default Value | Description 67 | | `path` | `adonis-ws` | The path used to make the connection (only change if you changed it on the server). 68 | | `reconnection` | `true` | Whether to reconnect automatically after disconnect. 69 | | `reconnectionAttempts` | `10` | Number of reconnection attempts before abandoning. 70 | | `reconnectionDelay` | `1000` | How long to wait before reconnecting. The value will be used as `n x delay`, where `n` is the current value of reconnection attempts. 71 | | `query` | `null` | Query string to pass to the connection URL (also accepts an object). 72 | | `encoder` | `JsonEncoder` | The encoder to use (the same encoder will be required on the server). 73 | |=== 74 | 75 | To manage your application state, listen for the `open`/`close` events: 76 | 77 | [source, js] 78 | ---- 79 | let isConnected = false 80 | 81 | ws.on('open', () => { 82 | isConnected = true 83 | }) 84 | 85 | ws.on('close', () => { 86 | isConnected = false 87 | }) 88 | ---- 89 | 90 | Once connected, subscribe to different/multiple topics: 91 | 92 | [source, js] 93 | ---- 94 | const chat = ws.subscribe('chat') 95 | 96 | chat.on('ready', () => { 97 | chat.emit('message', 'hello') 98 | }) 99 | 100 | chat.on('error', (error) => { 101 | console.log(error) 102 | }) 103 | 104 | chat.on('close', () => { 105 | }) 106 | ---- 107 | 108 | == Subscription API 109 | The following methods are used to send/receive messages. 110 | 111 | ==== emit(event, data) 112 | Send event to the server: 113 | 114 | [source, js] 115 | ---- 116 | chat.emit('message', { 117 | body: 'hello', 118 | user: 'virk' 119 | }) 120 | ---- 121 | 122 | ==== on(event, callback) 123 | Bind event listener: 124 | 125 | [source, js] 126 | ---- 127 | chat.on('message', () => {}) 128 | chat.on('new:user', () => {}) 129 | ---- 130 | 131 | ==== off(event, callback) 132 | Remove event listener: 133 | 134 | [source, js] 135 | ---- 136 | const messageHandler = function () {} 137 | 138 | chat.on('message', messageHandler) 139 | chat.off('message', messageHandler) 140 | ---- 141 | 142 | ==== close() 143 | Initiate request to close the subscription: 144 | 145 | [source, js] 146 | ---- 147 | chat.on('close', () => { 148 | // server acknowledged close 149 | }) 150 | 151 | chat.close() 152 | ---- 153 | 154 | NOTE: Listen for the link:#_close_2[close event] to confirm the subscription closed. 155 | 156 | ==== leaveError 157 | Emitted when the server refuses to close the subscription: 158 | 159 | [source, js] 160 | ---- 161 | chat.on('leaveError', (response) => { 162 | console.log(response) 163 | }) 164 | ---- 165 | 166 | ==== error 167 | Emitted when an error occurs on the TCP connection: 168 | 169 | [source, js] 170 | ---- 171 | chat.on('error', (event) => { 172 | }) 173 | ---- 174 | 175 | NOTE: Preferably, listen for the `ws.on('error')` event instead. 176 | 177 | ==== close 178 | Emitted when the subscription is closed: 179 | 180 | [source, js] 181 | ---- 182 | chat.on('close', () => { 183 | }) 184 | ---- 185 | 186 | == Ws API 187 | The following methods are available on a single `ws` connection. 188 | 189 | ==== connect 190 | Initiate the connection: 191 | 192 | [source, js] 193 | ---- 194 | ws.connect() 195 | ---- 196 | 197 | ==== close 198 | Forcefully close the connection: 199 | 200 | [source, js] 201 | ---- 202 | ws.close() 203 | ---- 204 | 205 | NOTE: Removes all subscriptions and does not trigger a reconnection. 206 | 207 | ==== getSubscription(topic) 208 | Returns the subscription instance for a given topic: 209 | 210 | [source, js] 211 | ---- 212 | ws.subscribe('chat') 213 | 214 | ws.getSubscription('chat').on('message', () => { 215 | }) 216 | ---- 217 | 218 | NOTE: If no subscriptions for the given topic, returns `null`. 219 | 220 | ==== subscribe(topic) 221 | Subscribe to a topic: 222 | 223 | [source, js] 224 | ---- 225 | const chat = ws.subscribe('chat') 226 | ---- 227 | 228 | NOTE: Subscribing to the same topic twice raises an exception. 229 | 230 | == Authentication 231 | The AdonisJs WebSocket client makes it simple to authenticate users. 232 | 233 | Auth credentials are only passed once to the server during the initial connection, so the same information can be reused to allow/disallow channel subscriptions. 234 | 235 | NOTE: If your application uses sessions, users will be authenticated automatically providing they have a valid session. 236 | 237 | ==== withBasicAuth(username, password) 238 | Authenticate via basic auth: 239 | 240 | [source, js] 241 | ---- 242 | const ws = Ws(url, options) 243 | 244 | ws 245 | .withBasicAuth(username, password) 246 | .connect() 247 | ---- 248 | 249 | ==== withApiToken(token) 250 | Authenticate via api token: 251 | 252 | [source, js] 253 | ---- 254 | const ws = Ws(url, options) 255 | 256 | ws 257 | .withApiToken(token) 258 | .connect() 259 | ---- 260 | 261 | ==== withJwtToken(token) 262 | Authenticate via JWT token: 263 | 264 | [source, js] 265 | ---- 266 | const ws = Ws(url, options) 267 | 268 | ws 269 | .withJwtToken(token) 270 | .connect() 271 | ---- 272 | 273 | === User Information 274 | 275 | On the server, access user information via the `auth` object: 276 | 277 | .start/socket.js 278 | [source, js] 279 | ---- 280 | Ws.channel('chat', ({ auth }) => { 281 | console.log(auth.user) 282 | }) 283 | ---- 284 | 285 | NOTE: link:websocket-server#_registering_middleware[Required middleware] must be set up to access the `auth` object. 286 | 287 | === Channel Middleware 288 | 289 | To authenticate connections, ensure the `auth` named middleware is applied: 290 | 291 | .start/socket.js 292 | [source, js] 293 | ---- 294 | Ws.channel('chat', ({ auth }) => { 295 | console.log(auth.user) 296 | }).middleware(['auth']) 297 | ---- 298 | -------------------------------------------------------------------------------- /10-testing/01-Getting-Started.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: testing 3 | title: Getting Started 4 | category: testing 5 | --- 6 | 7 | = Getting Started 8 | 9 | toc::[] 10 | 11 | Manually testing your application by visiting each webpage or API endpoint can be tedious, and sometimes even impossible. 12 | 13 | Automated testing is the preferred strategy to confirm your application continues to behave as expected as you make changes to your codebase. 14 | 15 | In this guide, we learn about the benefits of testing and different ways to test your application's code. 16 | 17 | == Test Cases 18 | If you are new to testing, you may find it hard to understand the benefits. 19 | 20 | Once you get into the habit of writing tests, your code quality and confidence about your code's behavior should improve drastically. 21 | 22 | === Test Categories 23 | Testing is divided into multiple categories, encouraging you to write different types of test cases with clear boundaries. 24 | 25 | These test categories include: 26 | 27 | [ul-shrinked] 28 | - link:#_unit_tests[Unit Tests] 29 | - link:#_functional_tests[Functional Tests] 30 | 31 | ==== Unit Tests 32 | Unit tests are written to test small pieces of code in isolation. 33 | 34 | For example, you might test a class directly without worrying how that class is used in the real world: 35 | 36 | .Example 37 | [source, js] 38 | ---- 39 | const { test } = use('Test/Suite')('Example unit test') 40 | const UserValidator = use('App/Services/UserValidator') 41 | 42 | test('validate user details', async ({ assert }) => { 43 | const validation = await UserValidator.validate({ 44 | email: 'wrong email' 45 | }) 46 | 47 | assert.isTrue(validation.fails()) 48 | assert.deepEqual(validation.messages(), [ 49 | { 50 | field: 'email', 51 | message: 'Invalid user email address' 52 | } 53 | ]) 54 | }) 55 | ---- 56 | 57 | ==== Functional Tests 58 | Functional tests are written to test your application like an end-user. 59 | 60 | For example, you might programmatically open a browser and interact with various webpages to ensure they work as intended: 61 | 62 | .Example 63 | [source, js] 64 | ---- 65 | const { test, trait } = use('Test/Suite')('Example functional test') 66 | trait('Test/Browser') 67 | trait('Test/Session') 68 | 69 | test('validate user details', async ({ browser }) => { 70 | const page = await browser.visit('/') 71 | 72 | await page 73 | .type('email', 'wrong email') 74 | .submitForm('form') 75 | .waitForNavigation() 76 | 77 | page.session.assertError('email', 'Invalid user email address') 78 | }) 79 | ---- 80 | 81 | Both the above test examples validate the email address for a given user, but the approach is different based on the type of test you are writing. 82 | 83 | == Setup 84 | As the *Vow Provider* is not installed by default, we need to pull it from `npm`: 85 | 86 | [source, bash] 87 | ---- 88 | > adonis install @adonisjs/vow 89 | ---- 90 | 91 | Next, register the provider in the `start/app.js` file `aceProviders` array: 92 | 93 | .start/app.js 94 | [source, js] 95 | ---- 96 | const aceProviders = [ 97 | '@adonisjs/vow/providers/VowProvider' 98 | ] 99 | ---- 100 | 101 | NOTE: The provider is registered inside the `aceProviders` array since we do not want to boot the testing engine when running your app in production. 102 | 103 | Installing `@adonisjs/vow` creates the following files and directory: 104 | 105 | ==== vowfile.js 106 | `vowfiles.js` is loaded before your tests are executed, and is used to define tasks that should occur before/after running all tests. 107 | 108 | ==== .env.testing 109 | `env.testing` contains the environment variables used when running tests. This file gets merged with `.env`, so you only need to define values you want to override from the `.env` file. 110 | 111 | ==== test 112 | All application tests are stored inside subfolders of the `test` directory. An example *unit test* is added to this directory when `@adonisjs/vow` is installed: 113 | 114 | .test/unit/example.spec.js 115 | [source, js] 116 | ---- 117 | 'use strict' 118 | 119 | const { test } = use('Test/Suite')('Example') 120 | 121 | test('make sure 2 + 2 is 4', async ({ assert }) => { 122 | assert.equal(2 + 2, 4) 123 | }) 124 | ---- 125 | 126 | == Running Tests 127 | Installing the *Vow Provider* creates an example *unit test* for you, which can be executed by running the following command: 128 | 129 | [source, bash] 130 | ---- 131 | > adonis test 132 | ---- 133 | 134 | .Output 135 | [source, bash] 136 | ---- 137 | Example 138 | ✓ make sure 2 + 2 is 4 (2ms) 139 | 140 | PASSED 141 | total : 1 142 | passed : 1 143 | time : 6ms 144 | ---- 145 | 146 | == Testing Suite & Traits 147 | Before we dive into writing tests, let's understand some fundamentals which are important to understanding the flow of tests. 148 | 149 | === Suite 150 | Each file is a test suite, defining a group of tests with similar behavior. 151 | 152 | For example, we can have a suite of tests for *user registration*: 153 | 154 | [source, js] 155 | ---- 156 | const Suite = use('Test/Suite')('User registration') 157 | 158 | // or destructuring 159 | const { test } = use('Test/Suite')('User registration') 160 | ---- 161 | 162 | The `test` function obtained from the `Suite` instance is used to define tests: 163 | 164 | [source, js] 165 | ---- 166 | test('return error when credentials are wrong', async (ctx) => { 167 | // implementation 168 | }) 169 | ---- 170 | 171 | === Traits 172 | To avoid bloating the test runner with unnecessary functionality, AdonisJs ships different pieces of code as *traits* (the building blocks for your test suite). 173 | 174 | For example, we call the `Test/Browser` trait so we can test via web browser: 175 | 176 | [source, js] 177 | ---- 178 | const { test, trait } = use('Test/Suite')('User registration') 179 | 180 | trait('Test/Browser') 181 | 182 | test('return error when credentials are wrong', async ({ browser }) => { 183 | const page = await browser.visit('/user') 184 | }) 185 | ---- 186 | 187 | NOTE: In the example above, if we were to remove the `Test/Browser` trait, the `browser` object would be `undefined` inside our tests. 188 | 189 | You can define custom traits with a closure or IoC container binding: 190 | 191 | [source, js] 192 | ---- 193 | const { test, trait } = use('Test/Suite')('User registration') 194 | 195 | trait(function (suite) { 196 | suite.Context.getter('foo', () => { 197 | return 'bar' 198 | }) 199 | }) 200 | 201 | test('foo must be bar', async ({ foo, assert }) => { 202 | assert.equal(foo, 'bar') 203 | }) 204 | ---- 205 | 206 | NOTE: Traits are helpful when you want to bundle a package to be used by others, though for most situations, you could simply use xref:_lifecycle_hooks[Lifecycle Hooks] instead. 207 | 208 | === Context 209 | Each test has an isolated context. 210 | 211 | By default, the context has only one property called `assert` which is an instance of link:http://chaijs.com/api/assert/[chaijs/assert, window="_blank"] to run assertions. 212 | 213 | You can pass custom values to each test context by defining *getters* or *macros* to be accessed inside the `test` callback closure (see the link:#_traits[Traits] closure example). 214 | 215 | == Lifecycle Hooks 216 | Each suite has lifecycle hooks which can be used to perform repetitive tasks (for example, cleaning the database after each test): 217 | 218 | [source, js] 219 | ---- 220 | const Suite = use('Test/Suite')('User registration') 221 | 222 | const { before, beforeEach, after, afterEach } = Suite 223 | 224 | before(async () => { 225 | // executed before all the tests for a given suite 226 | }) 227 | 228 | beforeEach(async () => { 229 | // executed before each test inside a given suite 230 | }) 231 | 232 | after(async () => { 233 | // executed after all the tests for a given suite 234 | }) 235 | 236 | afterEach(async () => { 237 | // executed after each test inside a given suite 238 | }) 239 | ---- 240 | 241 | == Assertions 242 | The `assert` object is an instance of link:http://chaijs.com/api/assert/[chaijs/assert, window="_blank"], passed to each test as a property of the `test` callback context. 243 | 244 | To make your tests more reliable, you can also plan assertions to be executed for a given test. Let's consider this example: 245 | 246 | [source, js] 247 | ---- 248 | test('must throw exception', async ({ assert }) => { 249 | try { 250 | await badOperation() 251 | } catch ({ message }) { 252 | assert.equal(message, 'Some error message') 253 | } 254 | }) 255 | ---- 256 | 257 | The above test passes even if an exception was never thrown and no assertions were run. This is a bad test, passing only because we structured it poorly. 258 | 259 | To overcome this scenario, `plan` for your expected number of assertions: 260 | 261 | [source, js] 262 | ---- 263 | test('must throw exception', async ({ assert }) => { 264 | assert.plan(1) 265 | 266 | try { 267 | await badOperation() 268 | } catch ({ message }) { 269 | assert.equal(message, 'Some error message') 270 | } 271 | }) 272 | ---- 273 | 274 | In the above example, if `badOperation` doesn't throw an exception, the test still fails since we planned for `1` assertion and `0` were made. 275 | 276 | 277 | -------------------------------------------------------------------------------- /10-testing/02-HTTP-Tests.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: api-tests 3 | title: HTTP Tests 4 | category: testing 5 | --- 6 | 7 | = HTTP Tests 8 | 9 | toc::[] 10 | 11 | In this guide, we learn how to write HTTP tests against an API server. 12 | 13 | If you are new to testing with AdonisJs, or just testing in general, consider reading the link:testing[Getting Started] guide before continuing further. 14 | 15 | == Basic Example 16 | Let's start with a basic example to test a HTTP endpoint returns a list of posts in JSON format. 17 | 18 | NOTE: The following example assumes you've created a `Post` model with related database table, and defined a `GET /posts` route that returns all `Post` models. 19 | 20 | First, make a new *functional test* (since we'll test the API like an end-user): 21 | 22 | [source, bash] 23 | ---- 24 | > adonis make:test Post 25 | ---- 26 | 27 | .make:test Menu 28 | [source, bash] 29 | ---- 30 | > Select the type of test to create 31 | Unit test 32 | ❯ Functional test 33 | ---- 34 | 35 | .Output 36 | [source, bash] 37 | ---- 38 | create: test/functional/post.spec.js 39 | ---- 40 | 41 | Next, open the test file and paste in the following code: 42 | 43 | .test/functional/post.spec.js 44 | [source, js] 45 | ---- 46 | const { test, trait } = use('Test/Suite')('Post') 47 | const Post = use('App/Models/Post') 48 | 49 | trait('Test/ApiClient') 50 | 51 | test('get list of posts', async ({ client }) => { 52 | await Post.create({ 53 | title: 'Adonis 101', 54 | body: 'Blog post content' 55 | }) 56 | 57 | const response = await client.get('/posts').end() 58 | 59 | response.assertStatus(200) 60 | response.assertJSONSubset([{ 61 | title: 'Adonis 101', 62 | body: 'Blog post content' 63 | }]) 64 | }) 65 | ---- 66 | 67 | Examining our test file… 68 | 69 | 1. We register the `Test/ApiClient` trait, providing us an HTTP `client` to make requests with 70 | 2. We create a dummy `Post` instance 71 | 3. We request the `/posts` URL and capture the response 72 | 4. We run assertions against the response to ensure the HTTP status is `200` 73 | and at least one returned post has the same `title` and `body` as our dummy `Post` instance 74 | 75 | Finally, run all your functional tests via the following command: 76 | 77 | [source, bash] 78 | ---- 79 | > adonis test functional 80 | ---- 81 | 82 | .Output 83 | [source, bash] 84 | ---- 85 | Post 86 | ✓ get list of posts (286ms) 87 | 88 | PASSED 89 | 90 | total : 1 91 | passed : 1 92 | time : 289ms 93 | ---- 94 | 95 | ++++ 96 | Your first HTTP test PASSED. Congratulations! 🎉 97 | ++++ 98 | 99 | == Client Methods 100 | The following methods can be called when making HTTP requests. 101 | 102 | ==== get(url) 103 | Make an HTTP `GET` request to a given url: 104 | 105 | [source, js] 106 | ---- 107 | client.get('posts') 108 | ---- 109 | 110 | TIP: The `post`, `patch`, `put`, `delete`, and `head` methods can also be used to make HTTP requests. 111 | 112 | ==== header(key, value) 113 | Set a header `key/value` pair when making the HTTP request: 114 | 115 | [source, js] 116 | ---- 117 | client 118 | .get('posts') 119 | .header('accept', 'application/json') 120 | ---- 121 | 122 | ==== send(body) 123 | Send request body when making the HTTP request: 124 | 125 | [source, js] 126 | ---- 127 | client 128 | .post('posts') 129 | .send({ 130 | title: 'Adonis 101', 131 | body: 'Post content' 132 | }) 133 | ---- 134 | 135 | ==== query(queryObject) 136 | Set query string parameters: 137 | 138 | [source, js] 139 | ---- 140 | client 141 | .get('posts') 142 | .query({ order: 'desc', page: 1 }) // ?order=desc&page=1 143 | ---- 144 | 145 | ==== type(type) 146 | Set the request content type: 147 | 148 | [source, js] 149 | ---- 150 | client 151 | .get('posts') 152 | .type('json') 153 | ---- 154 | 155 | ==== accept(type) 156 | Set the data type you want to accept from the server: 157 | 158 | [source, js] 159 | ---- 160 | client 161 | .get('posts') 162 | .accept('json') 163 | ---- 164 | 165 | ==== cookie(key, value) 166 | Set request cookies: 167 | 168 | [source, js] 169 | ---- 170 | client 171 | .get('posts') 172 | .cookie('name', 'virk') 173 | ---- 174 | 175 | NOTE: As all cookies are encrypted in AdonisJs, this method makes sure to encrypt the values properly so that AdonisJs server can parse them. 176 | 177 | ==== plainCookie(key, value) 178 | Set a cookie which doesn't get encrypted: 179 | 180 | [source, js] 181 | ---- 182 | client 183 | .get('posts') 184 | .plainCookie('name', 'virk') 185 | ---- 186 | 187 | ==== end 188 | End the HTTP request chain, execute the request and return the response: 189 | 190 | [source, js] 191 | ---- 192 | const response = await client.get('posts').end() 193 | ---- 194 | 195 | NOTE: You must call `end` to execute HTTP `client` requests. 196 | 197 | == Multipart Requests 198 | To make multipart requests and send files with the request body: 199 | 200 | [source, js] 201 | ---- 202 | await client 203 | .post('posts') 204 | .field('title', 'Adonis 101') 205 | .attach('cover_image', Helpers.tmpPath('cover-image.jpg')) 206 | .end() 207 | ---- 208 | 209 | You can also set HTML form style field names to send an array of data: 210 | 211 | [source, js] 212 | ---- 213 | await client 214 | .post('user') 215 | .field('user[name]', 'Virk') 216 | .field('user[email]', 'virk@adonisjs.com') 217 | .end() 218 | ---- 219 | 220 | == Sessions 221 | When writing tests, you may want to set sessions beforehand. 222 | 223 | This can be done by using the `Session/Client` trait: 224 | 225 | [source, js] 226 | ---- 227 | const { test, trait } = use('Test/Suite')('Post') 228 | 229 | trait('Test/ApiClient') 230 | trait('Session/Client') 231 | 232 | test('get list of posts', async ({ client }) => { 233 | const response = await client 234 | .get('posts') 235 | .session('adonis-auth', 1) 236 | .end() 237 | }) 238 | ---- 239 | 240 | NOTE: The AdonisJs link:sessions#_setup[Session Provider] must be installed before you can take advantage of the `Session/Client` trait. 241 | 242 | == Authentication 243 | You can authenticate users beforehand by using the `Auth/Client` trait: 244 | 245 | [source, js] 246 | ---- 247 | const { test, trait } = use('Test/Suite')('Post') 248 | 249 | trait('Test/ApiClient') 250 | trait('Auth/Client') 251 | trait('Session/Client') 252 | 253 | test('get list of posts', async ({ client }) => { 254 | const user = await User.find(1) 255 | 256 | const response = await client 257 | .get('posts') 258 | .loginVia(user) 259 | .end() 260 | }) 261 | ---- 262 | 263 | To authenticate with a custom scheme: 264 | 265 | [source, js] 266 | ---- 267 | client 268 | .get('posts') 269 | .loginVia(user, 'jwt') 270 | ---- 271 | 272 | For basic auth, pass the user's `username` and the `password`: 273 | 274 | [source, js] 275 | ---- 276 | client 277 | .get('posts') 278 | .loginVia(username, password, 'basic') 279 | ---- 280 | 281 | == Assertions 282 | The following assertions can be called on HTTP `client` responses. 283 | 284 | ==== assertStatus 285 | Assert the response status: 286 | 287 | [source, js] 288 | ---- 289 | response.assertStatus(200) 290 | ---- 291 | 292 | ==== assertJSON 293 | Assert the response body should `deepEqual` the expected value: 294 | 295 | [source, js] 296 | ---- 297 | response.assertJSON({ 298 | }) 299 | ---- 300 | 301 | ==== assertJSONSubset 302 | Assert a subset of JSON: 303 | 304 | [source, js] 305 | ---- 306 | response.assertJSONSubset({ 307 | title: 'Adonis 101', 308 | body: 'Some content' 309 | }) 310 | ---- 311 | 312 | TIP: This assertion tests a subset of objects, which is helpful when values inside an object are not determinable (e.g. timestamps). 313 | 314 | ==== assertText 315 | Assert plain text returned by the server: 316 | 317 | [source, js] 318 | ---- 319 | response.assertText('Hello world') 320 | ---- 321 | 322 | ==== assertError 323 | Assert the response error: 324 | 325 | [source, js] 326 | ---- 327 | response.assertError([ 328 | { 329 | message: 'username is required', 330 | field: 'username', 331 | validation: 'required' 332 | } 333 | ]) 334 | ---- 335 | 336 | ==== assertCookie 337 | Assert the server set a cookie with value: 338 | 339 | [source, js] 340 | ---- 341 | response.assertCookie('key', 'value') 342 | ---- 343 | 344 | ==== assertPlainCookie 345 | Assert a plain cookie value: 346 | 347 | [source, js] 348 | ---- 349 | response.assertPlainCookie('key', 'value') 350 | ---- 351 | 352 | ==== assertCookieExists 353 | Assert the server set a cookie with the given name: 354 | 355 | [source, js] 356 | ---- 357 | response.assertCookieExists('key') 358 | ---- 359 | 360 | ==== assertPlainCookieExists 361 | Assert a plain cookie exists: 362 | 363 | [source, js] 364 | ---- 365 | response.assertPlainCookieExists('key') 366 | ---- 367 | 368 | ==== assertHeader 369 | Assert the server sent a header: 370 | 371 | [source, js] 372 | ---- 373 | response.assertHeader('content-type', 'application/json') 374 | ---- 375 | 376 | ==== assertRedirect 377 | Assert the request was redirected to a given URL: 378 | 379 | [source, js] 380 | ---- 381 | response.assertRedirect('/there') 382 | ---- 383 | -------------------------------------------------------------------------------- /10-testing/05-Fakes.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: testing-fakes 3 | title: Fakes 4 | category: testing 5 | --- 6 | 7 | = Fakes 8 | 9 | toc::[] 10 | Quite often, you'll want to swap out the original implementation of certain parts of your application with a fake implementation when writing tests. 11 | 12 | Since AdonisJs leverages an IoC container to manage dependencies, it's incredibly easy to create fake implementations when writing tests. 13 | 14 | == Self-Implementing Fakes 15 | Let's start with a basic example of faking a service that normally sends emails. 16 | 17 | NOTE: Creating too many test fakes may lead to false tests, where all you are testing is the syntax and not the implementation. As a rule, try to keep fakes as the last option when writing your tests. 18 | 19 | .app/Services/UserRegistration.js 20 | [source, js] 21 | ---- 22 | class UserRegistration { 23 | 24 | async sendVerificationEmail (user) { 25 | await Mail.send('emails.verify', user, (message) => { 26 | message.to(user.email) 27 | message.subject('Verify account') 28 | }) 29 | } 30 | } 31 | ---- 32 | 33 | Without implementing a fake, each time we test our application's user registration logic, a verification email would be sent to the passed email address! 34 | 35 | To avoid this behavior, it makes sense to fake the `UserRegistration` service: 36 | 37 | [source, js] 38 | ---- 39 | const { ioc } = use('@adonisjs/fold') 40 | const { test } = use('Test/Suite')('User registration') 41 | 42 | test('register user', async () => { 43 | ioc.fake('App/Services/UserRegistration', () => { 44 | return { 45 | sendVerificationEmail () {} 46 | } 47 | }) 48 | 49 | // code to test user registration 50 | // .... 51 | 52 | ioc.restore('App/Services/UserRegistration') 53 | }) 54 | ---- 55 | 56 | The `ioc.fake` method lets you bind a fake value to the IoC container, and when any part of the application tries to resolve the bound namespace, the fake value is returned instead of the actual value. 57 | 58 | Once finished with a fake, `ioc.restore` can be called to remove it. 59 | 60 | This approach works great for a majority of use cases until you can create a fake which is similar to the actual implementation. For greater control, you can use external libraries like link:http://sinonjs.org/[Sinon.JS, window="_blank"]. 61 | 62 | == Mail Fake 63 | The AdonisJs link:mail[Mail Provider] comes with a built-in `fake` method: 64 | 65 | [source, js] 66 | ---- 67 | const Mail = use('Mail') 68 | const { test } = use('Test/Suite')('User registration') 69 | 70 | test('register user', async ({ assert }) => { 71 | Mail.fake() 72 | 73 | // write your test 74 | 75 | const recentEmail = Mail.pullRecent() 76 | assert.equal(recentEmail.message.to[0].address, 'joe@example.com') 77 | assert.equal(recentEmail.message.to[0].name, 'Joe') 78 | 79 | Mail.restore() 80 | }) 81 | ---- 82 | 83 | Calling `Mail.fake` binds a fake mailer to the IoC container. Once faked, all emails are stored in memory as an array of objects which can be later run assertions against. 84 | 85 | The following methods are available on the fake mailer. 86 | 87 | ==== recent() 88 | Returns the most recent email object: 89 | 90 | [source, js] 91 | ---- 92 | Mail.recent() 93 | ---- 94 | 95 | ==== pullRecent() 96 | Returns the recent email object and removes it from the in-memory array: 97 | 98 | [source, js] 99 | ---- 100 | Mail.pullRecent() 101 | ---- 102 | 103 | ==== all() 104 | Returns all emails: 105 | 106 | [source, js] 107 | ---- 108 | const mails = Mail.all() 109 | assert.lengthof(mails, 1) 110 | ---- 111 | 112 | ==== clear() 113 | Clears the in-memory emails array: 114 | 115 | [source, js] 116 | ---- 117 | Mail.clear() 118 | ---- 119 | 120 | ==== restore() 121 | Restore the original emailer class: 122 | 123 | [source, js] 124 | ---- 125 | Mail.restore() 126 | ---- 127 | 128 | == Events Fake 129 | The AdonisJs link:events[Event Provider] also comes with a built-in `fake` method: 130 | 131 | [source, js] 132 | ---- 133 | const Event = use('Event') 134 | const { test } = use('Test/Suite')('User registration') 135 | 136 | test('register user', async ({ assert }) => { 137 | Event.fake() 138 | 139 | // write your test 140 | .... 141 | 142 | const recentEvent = Event.pullRecent() 143 | assert.equal(recentEvent.event, 'register:user') 144 | 145 | Event.restore() 146 | }) 147 | ---- 148 | 149 | Calling `Event.fake` binds a fake event emitter to the IoC container. Once faked, all emitted events are stored in memory as an array of objects which can be later run assertions against. 150 | 151 | You can also `trap` an event inline and run assertions inside the passed callback: 152 | 153 | [source, js] 154 | ---- 155 | test('register user', async ({ assert }) => { 156 | assert.plan(2) 157 | Event.fake() 158 | 159 | Event.trap('register:user', function (data) { 160 | assert.equal(data.username, 'joe') 161 | assert.equal(data.email, 'joe@example.com') 162 | }) 163 | 164 | // write your test 165 | .... 166 | 167 | Event.restore() 168 | }) 169 | ---- 170 | 171 | The following methods are available on the fake event emitter. 172 | 173 | ==== recent() 174 | Returns the most recent event object: 175 | 176 | [source, js] 177 | ---- 178 | Event.recent() 179 | ---- 180 | 181 | ==== pullRecent() 182 | Returns the recent event object and removes it from the in-memory array: 183 | 184 | [source, js] 185 | ---- 186 | Event.pullRecent() 187 | ---- 188 | 189 | ==== all() 190 | Returns all events: 191 | 192 | [source, js] 193 | ---- 194 | const events = Event.all() 195 | assert.lengthof(events, 1) 196 | ---- 197 | 198 | ==== clear() 199 | Clears the in-memory array of events: 200 | 201 | [source, js] 202 | ---- 203 | Event.clear() 204 | ---- 205 | 206 | ==== restore() 207 | Restore the original event class: 208 | 209 | [source, js] 210 | ---- 211 | Event.restore() 212 | ---- 213 | 214 | == Database Transactions 215 | Keeping your database clean for each test can be difficult. 216 | 217 | AdonisJs ships with a `DatabaseTransactions` trait that wraps your databases queries inside a transaction then rolls them back after each test: 218 | 219 | [source, js] 220 | ---- 221 | const { test, trait } = use('Test/Suite')('User registration') 222 | 223 | trait('DatabaseTransactions') 224 | ---- 225 | 226 | Alternatively, you could set a link:testing#_lifecycle_hooks[Lifecycle Hook] to truncate your database tables after each test, but using the `DatabaseTransactions` trait would be far simpler. 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adonis Documentation 📚 2 | 3 | 4 | 5 | This repo contains the documentation for AdonisJs framework written in [Asciidoc](https://asciidoctor.org/) 6 | 7 | The docs are compiled and render via [adonisjs.com](https://github.com/adonisjs/adonisjs.com) repo which itself is a Adonisjs app. 8 | 9 | Make sure to read the [README of adonisjs.com repo](https://github.com/adonisjs/adonisjs.com) to understand, how to run documentation on your machine. 10 | 11 | [![Site][site-image]][site-url] 12 | 13 | 14 | [site-image]: https://img.shields.io/badge/read-docs-green.svg?style=flat-square 15 | [site-url]: http://adonisjs.com/docs 16 | -------------------------------------------------------------------------------- /recipes/01-nginx-proxy.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: nginx-proxy 3 | title: Nginx proxy 4 | category: recipes 5 | --- 6 | = Nginx proxy 7 | 8 | toc::[] 9 | 10 | This recipe shares the minimally required steps to serve AdonisJs app using `nginx` proxy. 11 | 12 | == First steps 13 | Before getting started, make sure you can run your application on the defined port. Also, it is recommended to make use of a process manager like `pm2` to start your Node.js server. 14 | 15 | [source, bash] 16 | ---- 17 | pm2 start server.js 18 | ---- 19 | 20 | Verify it is working 21 | 22 | [source, js] 23 | ---- 24 | pm2 list 25 | ---- 26 | 27 | To check application logs, you can run the following command 28 | 29 | [source, js] 30 | ---- 31 | pm2 logs 32 | ---- 33 | 34 | == Nginx proxy 35 | 36 | Open the `default` server configuration file. 37 | 38 | [source, bash] 39 | ---- 40 | # empty the file 41 | echo > /etc/nginx/sites-available/default 42 | 43 | # open in editor 44 | vi /etc/nginx/sites-available/default 45 | ---- 46 | 47 | Moreover, paste the following code inside it. 48 | 49 | [source, nginx] 50 | ---- 51 | server { 52 | listen 80; 53 | 54 | server_name myapp.com; 55 | 56 | location / { 57 | proxy_pass http://localhost:3333; 58 | proxy_http_version 1.1; 59 | proxy_set_header Upgrade $http_upgrade; 60 | proxy_set_header Connection 'upgrade'; 61 | proxy_set_header Host $host; 62 | proxy_set_header X-Real-IP $remote_addr; 63 | proxy_set_header X-Forwarded-Proto $scheme; 64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 65 | proxy_cache_bypass $http_upgrade; 66 | } 67 | } 68 | ---- 69 | 70 | === Points to note 71 | 72 | 1. It is assumed that `nginx` is installed and working as expected. 73 | 2. Your app is running on `PORT 3333`. If not, then do change `proxy_pass` block inside the nginx file and define the appropriate port. 74 | 3. Do replace `myapp.com` with the actual domain of your app. 75 | 4. Change value of `trustProxy` to *true* inside link:https://github.com/adonisjs/adonis-slim-app/blob/develop/config/app.js#L43[config/app.js] file. 76 | 77 | Now visiting `myapp.com` shows your Adonisjs application, since `nginx` is proxying all the requests to the application running on a specified port. 78 | -------------------------------------------------------------------------------- /recipes/02-dev-domains.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: dev-domains 3 | title: Using .dev domains 4 | category: recipes 5 | --- 6 | = Using .dev domains 7 | 8 | toc::[] 9 | 10 | We all love to use pretty `.dev` domains when developing our apps in development. In this recipe, we learn how to bind custom domains to your app instead of accessing them via `localhost`. 11 | 12 | NOTE: This technique has no technical advantage or disadvantage and instead used as a personal preference towards pretty domains. 13 | 14 | 15 | == Setup hotel 16 | The first step is to set up an external tool called link:https://www.npmjs.com/package/hotel[hotel, window="_blank"]. It allows you to register domains for an app or a URL. 17 | 18 | [source, bash] 19 | ---- 20 | npm install -g hotel 21 | ---- 22 | 23 | Next, we need to start it as a daemon on `port=2000` using the following command. 24 | 25 | [source, bash] 26 | ---- 27 | hotel start 28 | ---- 29 | 30 | Once it is running, you can run `hotel ls` command to see the list of registered apps/domains. Which is empty with the new installation. 31 | 32 | == Setup proxy 33 | Let's understand how this works in theory. We need to tell our *browser* or *system network* to pass through a proxy, which serves our `.dev` apps or pass the request to the actual URL. 34 | 35 | The entire process of proxy is very lightweight and doesn't impact your system performance or speed. 36 | 37 | Now as we know, that all magic is done via the proxy, let's update the settings of the browser/system to pass via link:https://github.com/typicode/hotel/blob/master/docs/README.md#browser-configuration[hotel proxy, window="_blank"]. 38 | 39 | === System configuration 40 | We need to point the network to link:http://localhost:2000/proxy.pac[http://localhost:2000/proxy.pac] file. 41 | 42 | ==== Osx 43 | 44 | [source, bash] 45 | ---- 46 | Network Preferences > Advanced > Proxies > Automatic Proxy Configuration 47 | ---- 48 | 49 | 50 | ==== Windows 51 | 52 | [source, bash] 53 | ---- 54 | Settings > Network and Internet > Proxy > Use setup script 55 | ---- 56 | 57 | 58 | ==== Linux ( ubuntu ) 59 | [source, bash] 60 | ---- 61 | System Settings > Network > Network Proxy > Automatic 62 | ---- 63 | 64 | === Browser configuration 65 | Browser configuration just proxy the request for that browser only and not for the entire system. 66 | 67 | ==== Chrome ( exit chrome first ) 68 | [source, bash] 69 | ---- 70 | # Linux 71 | google-chrome --proxy-pac-url=http://localhost:2000/proxy.pac 72 | 73 | # OS X 74 | open -a "Google Chrome" --args --proxy-pac-url=http://localhost:2000/proxy.pac 75 | ---- 76 | 77 | ==== Firefox 78 | [source, bash] 79 | ---- 80 | Preferences > Advanced > Network > Connection > Settings > Automatic proxy URL configuration 81 | ---- 82 | 83 | == Integrate with AdonisJs 84 | Now that hotel is configured, we can use it independently of AdonisJs for any application. However, the problem is all apps registered with `hotel` are mapped forever unless you remove them manually. 85 | 86 | This behavior can cause issues, where you want your *disposable domains* which lives till your app is running. 87 | 88 | Adonis cli `adonis serve` command accepts a flag which registers a disposable domain with the *hotel* and removes it when you stop your app. 89 | 90 | [source, bash] 91 | ---- 92 | adonis serve --domain=yardstick@http://localhost:3333 93 | ---- 94 | 95 | The `--domain` flag takes the *domain* and the *URL*. In this case 96 | 97 | - `domain=yardstick` 98 | - `url=http://localhost:3333` 99 | 100 | -------------------------------------------------------------------------------- /recipes/03-frontend-assets.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: frontend-assets 3 | title: Managing frontend assets 4 | category: recipes 5 | --- 6 | = Managing frontend assets 7 | 8 | toc::[] 9 | 10 | AdonisJs does not make any assumptions, neither provide tools on how to bundle your frontend assets. The goal of the framework is to provide a productive workflow for backend applications only. 11 | 12 | Whereas, in this recipe, we discuss some ways on how you can go about managing and bundling your frontend code. 13 | 14 | == Webpack 15 | There are so many build tools in the frontend eco-system that it is quite easy to feel overwhelming. However, link:https://webpack.js.org/concepts/[webpack, window="_blank"] *(as of now)* does manage everything gracefully and is the popular choice for many devs. 16 | 17 | Let's see how to go about storing your assets and then bundling them. 18 | 19 | === Directory structure 20 | [source, bash] 21 | ---- 22 | └── resources 23 | └── assets 24 | └── sass 25 | └── scripts 26 | └── images 27 | ---- 28 | 29 | We should keep all `source assets` inside the `resources` directory. This directory is already used by Adonis to store the views. 30 | 31 | All compiled assets from this directory are placed inside the `public` directory. 32 | 33 | === Webpack base config 34 | First, make sure to install webpack as a dev dependency and create the config file. 35 | 36 | [source, bash] 37 | ---- 38 | npm i --save-dev webpack webpack-cli 39 | 40 | touch webpack.config.js 41 | ---- 42 | 43 | .webpack.config.js 44 | [source, js] 45 | ---- 46 | module.exports = { 47 | } 48 | ---- 49 | 50 | Run `./node_modules/.bin/webpack` to build your files. 51 | 52 | - Use the flag `--mode` to choose between production and development. 53 | - To start the watcher, make use of `--watch` flag. 54 | 55 | example 56 | [source, bash] 57 | ---- 58 | ./node_modules/.bin/webpack --mode development 59 | ---- 60 | 61 | == Sass setup 62 | 63 | [source, bash] 64 | ---- 65 | npm i --save-dev style-loader css-loader extract-text-webpack-plugin@next node-sass sass-loader 66 | ---- 67 | 68 | Add following code to your webpack.config.js file. 69 | 70 | [source, js] 71 | ---- 72 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 73 | 74 | const extractSass = new ExtractTextPlugin({ 75 | filename: 'public/app.css' 76 | }) 77 | 78 | function sassRules () { 79 | return [ 80 | { 81 | test: /\.(sass|scss)$/, 82 | use: ExtractTextPlugin.extract( 83 | { 84 | fallback: 'style-loader', 85 | use: ['css-loader', 'sass-loader'] 86 | }) 87 | } 88 | ] 89 | } 90 | 91 | module.exports = { 92 | entry: [ 93 | './resources/assets/sass/app.scss' 94 | ], 95 | output: { 96 | filename: 'public/app.js' 97 | }, 98 | module: { 99 | rules: sassRules() 100 | }, 101 | plugins: [ 102 | extractSass 103 | ] 104 | } 105 | ---- 106 | 107 | Here we make use of `sass-loader` and some related dependencies to compile `resources/assets/sass/app.scss -> public/app.css`. 108 | 109 | Require css file from `edge` templates. 110 | 111 | [source, edge] 112 | ---- 113 | 114 | {{ style('/public/app') }} 115 | 116 | ---- 117 | 118 | 119 | == Scripts setup 120 | The scripts setup is done to bundle your frontend JavaScript into a single file. I assume that you want to compile code to ES5 to target all major browsers. 121 | 122 | NOTE: We use babel for ES6 to ES5 transpilation. Also *AdonisJs itself does not need babel*, it is just for the JavaScript you are writing for the browser. 123 | 124 | [source, bash] 125 | ---- 126 | npm i --save-dev babel-loader @babel/core @babel/preset-env 127 | ---- 128 | 129 | [source, js] 130 | ---- 131 | function scriptRules () { 132 | return [ 133 | { 134 | test: /\.js$/, 135 | exclude: [/node_modules/], 136 | loader: 'babel-loader', 137 | options: { presets: ['env'] } 138 | } 139 | ] 140 | } 141 | 142 | module.exports = { 143 | entry: [ 144 | './resources/assets/sass/app.scss', 145 | './resources/assets/scripts/app.js' 146 | ], 147 | output: { 148 | filename: 'public/app.js' 149 | }, 150 | module: { 151 | rules: sassRules().concat(scriptRules()) 152 | }, 153 | plugins: [ 154 | extractSass 155 | ] 156 | } 157 | ---- 158 | 159 | This time we compile `resources/assets/scripts/app.js -> public/app.js` 160 | 161 | Require js file from `edge` templates. 162 | 163 | [source, edge] 164 | ---- 165 | 166 | {{ script('/public/app') }} 167 | 168 | ---- 169 | -------------------------------------------------------------------------------- /recipes/04-why-adonis-install.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: why-adonis-install 3 | title: Why Adonis install? 4 | category: recipes 5 | --- 6 | = Why Adonis install? 7 | 8 | toc::[] 9 | 10 | AdonisJs ships with a command called `adonis install `, which is same as `npm install` or `yarn add`. 11 | 12 | In fact behind the scenes `adonis install` uses npm or yarn to install the package, but also performs an extra step after the install. 13 | 14 | == Command execution 15 | 16 | link:http://res.cloudinary.com/adonisjs/image/upload/q_100/v1509020167/adonis-install-flow.png[image:http://res.cloudinary.com/adonisjs/image/upload/q_100/v1509020167/adonis-install-flow.png[width="400px"]] 17 | 18 | Every provider can have two files link:https://github.com/adonisjs/adonis-lucid/blob/develop/instructions.js[instructions.js, window="_blank"] and link:https://github.com/adonisjs/adonis-lucid/blob/develop/instructions.md[instructions.md, window="_blank"], which are used by adonis to perform `post install` steps. 19 | 20 | 21 | === instructions.js 22 | The `.js` file exports a function. This function can be used to perform any steps since you can write functional code inside it. 23 | 24 | Most commonly performed steps are 25 | 26 | [ul-shrinked] 27 | - Copy config files. 28 | - Create entities like controllers, models and so on. 29 | 30 | === instructions.md 31 | The `.md` files are the small set of instructions to be followed by hand from the user of the provider. So it is a nice place to drop some usage and setup instructions written in Github flavored markdown. 32 | -------------------------------------------------------------------------------- /recipes/05-https.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: using-https 3 | title: Using Https 4 | category: recipes 5 | --- 6 | toc::[] 7 | 8 | The HTTPs server of Node.js can be used with AdonisJs as follows. The idea is use the Node.js `https` module inside `server.js` file. + 9 |   10 | [source, js] 11 | ---- 12 | const { Ignitor } = require('@adonisjs/ignitor') 13 | const path = require('path') 14 | const https = require('https') 15 | const fs = require('fs') 16 | 17 | // Certificate 18 | const options = { 19 | key: fs.readFileSync(path.join(__dirname, './server.key')), 20 | cert: fs.readFileSync(path.join(__dirname, './server.crt')) 21 | } 22 | 23 | new Ignitor(require('@adonisjs/fold')) 24 | .appRoot(__dirname) 25 | .fireHttpServer((handler) => { 26 | return https.createServer(options, handler) 27 | }) 28 | .catch(console.error) 29 | ---- 30 | 31 | The real work happens inside `fireHttpServer` method. This function takes a single argument as the callback, and the return value must be the instance of Node.js server. 32 | 33 | == Self-signed certificate 34 | In development, you can also make use of the self-signed certificate. It requires an additional dependency from npm. 35 | 36 | [source, bash] 37 | ---- 38 | npm i pem 39 | ---- 40 | 41 | [source, js] 42 | ---- 43 | const { Ignitor } = require('@adonisjs/ignitor') 44 | const https = require('https') 45 | const pem = require('pem') 46 | 47 | pem.createCertificate({ days: 1, selfSigned: true }, (error, keys) => { 48 | if (error) { 49 | return console.log(error) 50 | } 51 | 52 | const options = { 53 | key: keys.serviceKey, 54 | cert: keys.certificate 55 | } 56 | 57 | new Ignitor(require('@adonisjs/fold')) 58 | .appRoot(__dirname) 59 | .fireHttpServer((handler) => { 60 | return https.createServer(options, handler) 61 | }) 62 | .catch(console.error) 63 | }) 64 | ---- 65 | -------------------------------------------------------------------------------- /recipes/06-number-guessing-game.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: number-guessing-game 3 | title: Number guessing game 4 | category: recipes 5 | --- 6 | 7 | = Number guessing game 8 | 9 | toc::[] 10 | 11 | In this guide, we create a simple number guessing game as a way to learn more about the framework. By the end of this guide, you will know how to make use of *views*, *create new routes* and *bind controllers* to them. 12 | 13 | TIP: You can see the final working version on link:https://adonis-number-guessing-game.glitch.me/?number=5[glitch, window="_blank"]. Also, you can checkout the code using the following remix button. + 14 | link:https://glitch.com/edit/#!/remix/adonis-number-guessing-game[image:https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg[], window="_blank"] 15 | 16 | == Game story 17 | The number guessing takes input from the user and matches it against a random number. If the number matches, it is called a match, otherwise it is a pass. 18 | 19 | To keep it simple, we accept the user guessed number as the query string in the URL. 20 | 21 | == Setup 22 | Let's create a new project using `adonis` command. We create a slim project since we do not need a database or models for this app. 23 | 24 | [source, bash] 25 | ---- 26 | adonis new number-game --slim 27 | ---- 28 | 29 | Now, `cd` into the created directory and make sure you can run the application using `adonis serve` command. 30 | 31 | == Routing 32 | Let's start by removing everything from the `start/routes.js` file and paste the following code inside it. 33 | 34 | [source, js] 35 | ---- 36 | const Route = use('Route') 37 | 38 | Route.get('/', ({ request }) => { 39 | /** get number from the url query string */ 40 | const guessedNumber = Number(request.input('number')) 41 | 42 | /** if number is not specified, let the user know about it */ 43 | if (!guessedNumber) { 44 | return 'Please specify a number by passing ?number= to the url' 45 | } 46 | 47 | /** generate a random number */ 48 | const randomNumber = Math.floor(Math.random() * 20) + 1 49 | 50 | /** let the user know about the match */ 51 | return randomNumber === guessedNumber 52 | ? 'Matched' 53 | : `Match failed. The actual number is ${randomNumber}` 54 | }) 55 | ---- 56 | 57 | Now if we visit link:http://127.0.0.1:3333?number=5[127.0.0.1:3333?number=5] and keep on changing the `number` between 1-20, we see *Matched* or the *Match failed* statement. 58 | 59 | The logic of number guessing game remains the same, but we start extracting this code into a controller and also create a view for it. 60 | 61 | == Http controller 62 | Let's create a new controller by running the following command. 63 | 64 | [source, bash] 65 | ---- 66 | adonis make:controller Game 67 | ---- 68 | 69 | .Output 70 | [source, bash] 71 | ---- 72 | ✔ create app/Controllers/Http/GameController.js 73 | ---- 74 | 75 | Now open the `GameController.js` file and paste the following code into it. 76 | 77 | [source, js] 78 | ---- 79 | 'use strict' 80 | 81 | class GameController { 82 | 83 | render ({ request }) { 84 | /** get number from the url query string */ 85 | const guessedNumber = Number(request.input('number')) 86 | 87 | /** if number is not specified, let the user know about it */ 88 | if (!guessedNumber) { 89 | return 'Please specify a number by passing ?number= to the url' 90 | } 91 | 92 | /** generate a random number */ 93 | const randomNumber = Math.floor(Math.random() * 20) + 1 94 | 95 | /** let the user know about the match */ 96 | return randomNumber === guessedNumber 97 | ? 'Matched' 98 | : `Match failed. The actual number is ${randomNumber}` 99 | } 100 | } 101 | 102 | module.exports = GameController 103 | ---- 104 | 105 | All we have done is moved the code from the route closure to the controller file. Now, we can remove all the code from `start/routes.js` file and instead bind controller to it. 106 | 107 | [source, js] 108 | ---- 109 | Route.get('/', 'GameController.render') 110 | ---- 111 | 112 | Now refresh the page, and the game works as expected. 113 | 114 | == Views 115 | AdonisJs makes use of link:http://edge.adonisjs.com/[edge.js, window="_blank"] as the templating engine to power views. Let's learn how to register the view provider and render it from the controller method. 116 | 117 | === Setup 118 | All of the providers are registered inside `start/app.js` file. So will the *ViewProvider*. 119 | 120 | [source, js] 121 | ---- 122 | const providers = [ 123 | '@adonisjs/framework/providers/ViewProvider' 124 | ] 125 | ---- 126 | 127 | Once the provider has been registered, you can access the `view` instance inside your controller methods as follows. 128 | 129 | [source, js] 130 | ---- 131 | render ({ request, view }) { 132 | } 133 | ---- 134 | 135 | Now, all we need to do is, create the `game.edge` template file and write the logic inside it. 136 | 137 | === Creating template file 138 | 139 | Just like the controller, we can use the `make:view` command to create a new view for us. 140 | 141 | [source, bash] 142 | ---- 143 | adonis make:view game 144 | ---- 145 | 146 | .Output 147 | [source, bash] 148 | ---- 149 | ✔ create resources/views/game.edge 150 | ---- 151 | 152 | === Extracting logic from controller 153 | Let's remove all the logic from the controller method and instead render a view with required data. 154 | 155 | [source, js] 156 | ---- 157 | 'use strict' 158 | 159 | class GameController { 160 | 161 | render ({ request, view }) { 162 | const guessedNumber = Number(request.input('number')) 163 | const randomNumber = Math.floor(Math.random() * 20) + 1 164 | 165 | /** rendering view */ 166 | return view.render('game', { guessedNumber, randomNumber }) 167 | } 168 | } 169 | 170 | module.exports = GameController 171 | ---- 172 | 173 | .resources/views/game.edge 174 | [source, edge] 175 | ---- 176 | @if(!guessedNumber) 177 |

Please specify a number by passing ?number to the url

178 | @elseif(guessedNumber === randomNumber) 179 |

Matched

180 | @else 181 |

Match failed. The actual number is {{ randomNumber }}

182 | @endif 183 | ---- 184 | 185 | As you can see, Edge makes it so simple to write logic in the template files. We are easily able to output the statement we want. 186 | 187 | TIP: If you have any questions or concerns, please join us on link:https://forum.adonisjs.com/c/help/view[discourse, window="_blank"] to be a part of our small and helpful community. 188 | 189 | == Next Steps 190 | This tutorial was the easiest attempt to make use of different pieces and build a simple application in AdonisJs. Moving forward consider learning more about the following topics. 191 | 192 | [ol-shrinked] 193 | 1. link:routing[Routing] 194 | 2. link:database[Database] 195 | 3. and link:authentication[Authentication] 196 | --------------------------------------------------------------------------------