Out of preview

Finally 😅

After almost five months from the last release and over one year from the initial preview release, I am happy to announce that we are going out of preview, and v5 will be the main version of the framework starting today.

If you are using v4 of the framework, we do not recommend upgrading existing applications since there is no easy upgrade path from v4 to v5.

The documentation for v4 has been moved to https://legacy.adonisjs.com . We will continue pushing security updates and minor patches to v4 for the entire 2021

To ensure we have a smooth journey moving forward, I have to come up with a few breaking changes in this release. Most of the changes are small, and TypeScript static type checking will also help you along the way.

But first, let's celebrate new additions to the framework.

Social authentication

You can implement social authentication in your applications using the @adonisjs/ally package. Just like everything else, the API for ally is boilerplate-free and straightforward.

Route.get('/github/redirect', async ({ ally }) => {
return ally.use('github').redirect()
})
Route.get('/github/callback', async ({ ally }) => {
const github = ally.use('github')
const user = await github.user()
})

Also, we make sure to provide IntelliSense for the available scopes for a given OAuth provider.

Authorization

The @adonisjs/bouncer packages add support for authorizing user actions. The main goal of the bouncer package is to help you extract the authorization logic to the Bouncer actions or policies vs. writing it everywhere in your codebase.

Following is an example of expressing the authorization checks as bouncer actions.

export const { actions } = Bouncer
.define('viewPost', (user: User, post: Post) => {
return post.userId === user.id
})
.define('editPost', (user: User, post: Post) => {
return post.userId === user.id
})
.define('deletePost', (user: User, post: Post) => {
return post.userId === user.id && post.status !== 'published'
})

You can authorize the currently logged in user against the defined actions as follows:

import Route from '@ioc:Adonis/Core/Route'
import Post from 'App/Models/Post'
Route.get('posts/:id', async ({ bouncer, request }) => {
const post = await Post.findOrFail(request.param('id'))
await bouncer.authorize('viewPost', post)
})

Assets manager

You can optionally configure webpack encore to bundle your frontend assets.

We use package detection to check if @symfony/webpack-encore is installed and start the webpack dev server from the node ace serve --watch command. Meaning, you can start both the AdonisJS development server and the webpack dev server from a single command.

Also, we have added new tags and helper methods to reference the compiled assets inside Edge templates. Following is a standard template to reference the frontend assets inside the edge templates.

<!DOCTYPE html>
<html lang="en">
<head>
@entryPointScripts('app')
@entryPointStyles('app')
</head>
<body>
</body>
</html>

Ace command aliases

You can now define the aliases for the ace commands inside the .adonisrc.json file. The goal is to help you create short and memorizable aliases.

Following is an example of defining the migrate alias for the migration:run command.

{
"migrate": "migration:run"
}

And now run the migrations as follows.

node ace migrate

Helpers module

We have collected all the small utilities used by the framework and the ecosystem packages to a helpers module and make it available to your apps.

Since these utilities are already installed and used by the framework, the helpers module does not add any additional bloat to your node_modules.

import { string } from '@ioc:Adonis/Core/Helpers'
string.camelCase('hello-world') // helloWorld

New documentation website

Improving documentation was a long pending task. With this release, I have almost rewritten the complete documentation from scratch and covered most topics.

To begin with, we have divided the documentation into multiple sub-groups, each trying to solve a specific use case.

Since the documentation website is now decoupled with the main marketing website, individuals interested in translating the docs can fork the repo of the docs website and create a translated version of it.

Upgrading to the latest versions

You can upgrade to the latest versions of all the packages using the npm update command or manually installing packages with the @latest tag.

Make sure to double-check the package.json file of your application and then only re-install the required packages.

npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
npm i @adonisjs/[email protected]
# Development only
npm i -D @adonisjs/[email protected]

Breaking changes

You will start receiving some errors right after upgrading the packages. It is alright, as we will walk through the breaking changes together and fix them.

As the first step, do check your Node.js version and ensure you are running Node >= 14.15.4. The preview release worked with Node 12 as well, but now AdonisJS needs 14.15.4 and above.


Making middleware type-safe

Open the pre-existing start/kernel.ts file and remove the string based middleware references with the import statement as follows:

Server.middleware.register([
'Adonis/Core/BodyParserMiddleware',
'Adonis/Addons/ShieldMiddleware',
'App/Middleware/SilentAuth'
() => import('@ioc:Adonis/Core/BodyParser'),
() => import('@ioc:Adonis/Addons/Shield'),
() => import('App/Middleware/SilentAuth')
])

Similarly, update the auth middleware also to use import statements.

Server.middleware.registerNamed({
auth: 'App/Middleware/Auth',
auth: () => import('App/Middleware/Auth')
})

Import-based middleware ensures that the TypeScript compiler can report the invalid references in advance.


Rename forceContentNegotiationToJSON config property

Open the config/app.ts file and replace the forceContentNegotiationToJSON with the following code.

The change is only applicable if the forceContentNegotiationToJSON property was existing inside the config file. Feel free to configure the change if there is no property in the first place.

{
http: {
forceContentNegotiationToJSON: true
forceContentNegotiationTo: 'application/json'
}
}

The old flag forceContentNegotiationToJSON forced the request Accept header to always be application/json. With the recent change, you can force it to any value to want.


Async views rendering

The view.render method now returns a promise that resolves to a string value. Earlier, this method was synchronous.

The change was required because of the following reasons:

You have a couple of options to make this change.

If you decide to opt into async rendering, then you will have to also update the components using slots to await them. For example:

{{{ $slots.main() }}}
{{{ await $slots.main() }}}

Removing the orm config property

The orm config property inside the config/database.ts file has been removed in favor of Naming strategy

You can define a custom naming strategy inside a preload file and assign it to the BaseModel to have the same impact as the orm config property.

import { BaseModel, NamingStrategyContract } from '@ioc:Adonis/Lucid/Orm'
class CamelCaseStrategy implements NamingStrategyContract {
// implementation
}
BaseModel.namingStrategy = new CamelCaseStrategy()

Make sure to read the Naming strategy doc to view the available methods.


Application bootstrap process

The application bootstrap process has been changed to be completely async. This change will not impact you unless you manually booted the AdonisJS app for a specific use case. Make sure to read the github release notes to understand the change and its impact.

Along with this change, the service providers now receive an instance of the Application class and not the IoC container. So make sure to update your service provider to reference the container as follows.

import { IocContract } from '@ioc:Adonis/Core/Application'
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
constructor(protected container: IocContract) {}
constructor(protected app: ApplicationContract) {}
public register() {
this.container.bind('Binding', () => {})
this.app.container.bind('Binding', () => {})
}
}

Validator

We have renamed a few validation options and the blacklist rule to notIn. The changes are made to not use color for defining what is allowed and not allowed.

The blacklist rule has been renamed to notIn

{
username: schema.string({}, [
rules.blacklist(['admin', 'super'])
rules.notIn(['admin', 'super'])
])
}

The hostWhitelist and hostBlacklist properties of the url validation rules have been renamed to allowedHosts and bannedHosts.

{
twitterHandle: schema.string({}, [
rules.url({
hostWhitelist: ['twitter']
allowedHosts: ['twitter']
})
])
}

Auth

Open the config/auth.ts file and rename the list property to guards. The guards keyword is more specific than the generic list keyword.

{
guard: 'web',
list: {
guards: {
}
}

Route.makeUrl

The route.makeUrl was overloaded with too many options. We tried improving the API and also introduce a new URL builder API.

For the most common use case, there are no breaking changes. But a few, if you are generating URLs for a specific domain.

The following examples will continue to work

// Will continue to work
Route.makeUrl('PostsController.show', { id: 1 })
Route.makeUrl('PostsController.show', {
id: 1,
qs: { published: true }
})
// Will continue to work
Route.makeUrl('PostsController.show', { params: { id: 1 } })
Route.makeUrl('PostsController.show', {
params: { id: 1 },
qs: { published: true }
})

If you were creating the routes for a specific domain, you would have to make the following adjustments.

Route.makeUrl(
'PostsController.show',
{ id: 1 },
'blog.adonisjs.com'
{
'blog.adonisjs.com'
}
)

Also, the prefixDomain and the domainParams have been removed, and you should use the prefixUrl option. Following is the new ideal API.

// Pass params as an array
Route.makeUrl('/posts/:id', [1])
// Pass params as an object
Route.makeUrl('/posts/:id', { id: 1 })
// Options as 3rd argument
Route.makeUrl(
'/posts/:id',
{ id: 1 },
{ qs: { published: true } }
)

Remove X-Download-Options and X-XSS-Protection headers support

Both the headers are deprecated by the HTTP standards. One must use CSP to protect against XSS attacks. AdonisJS already supports CSP


Change on Model.query().count() behaviour

The model query builder now always returns an array of models instances or directly a model instance and not plain objects. The following code will not work anymore since it needs extra properties to access the count.

const count = await SomeModel.query().count('id')
console.log(count[0].count) // undefined

To make it work, you need to use the new pojo method.

const count = await SomeModel.query().pojo<{ total: number }>().count('id as total')
console.log(count[0].total) // X

Additions

Following are the new additions to the existing packages.


Convert empty strings to null

You can now configure bodyparser to convert empty strings to null. The goal of the addition is to handle the native behavior of the browsers.

Open the config/bodyparser.ts file and set the following option inside the multipart and form blocks.

{
form: {
convertEmptyStringsToNull: true
},
multipart: {
convertEmptyStringsToNull: true
}
}

Pivot table timestamps and custom attributes

You can now enable timestamps for the pivot table using the timestamps property on the relationship. Read the documentation for complete reference.

@manyToMany(() => Skill, {
pivotTimestamps: true
})
public skills: ManyToMany<typeof Skill>

Also, during the create and save calls for a many to many relationship, you can also define pivot columns to insert. Learn more


Http

Add request.matchesRoute method to check if the current request URL matches a route or not.

if (request.matchesRoute('PostsController.show')) {
}

Add route.redirect method to register a route that redirects to another route or a URL.

Route.get('guides/:doc', 'GuidesController.show')
Route.on('docs/:doc').redirect('GuidesController.show')

To redirect to an exact URL, you can make use of the redirectToPath method.

Route.on('blog/:slug').redirectToPath('https://medium.com/my-blog')

Bug fixes and other small improvements