May release - 2022

This release improves Lucid model factories and the ability to switch Drive S3 and GCS buckets at runtime. In addition, the make:suite command to create new test suites and many bug fixes and minor improvements.

You must update all the packages under @adonisjs scope to their latest version. You can run the npm update command or use the following command to upgrade packages manually.

npx npm-check-updates -i

Once done, make sure to update the ace commands index by running following ace command.

node ace generate:manifest

Infer TypeScript types from config

If you open contracts/drive.ts, contracts/mail.ts, or contracts/hash.ts, you will notice we have manually defined the mappings of the drivers we want to use within our application.

Back story

Before moving forward, let's understand why we have to define these mappings on an interface, and we will use Drive as an example for the same.

In the following import statement, the Drive object is a singleton created using the config defined inside the config/drive.ts file and you switch between the disks using the Drive.use method.

import Drive from '@ioc:Adonis/Core/Drive'
Drive.use('<use-any-mapping-from-config-file>')

All this works as expected at runtime. However, TypeScript has no way to make a relation between the Drive import and its config file. Therefore the TypeScript static compiler cannot provide any IntelliSense or type safety.

To combat that, we use an interface called DisksList and explicitly inform the TypeScript compiler about the mappings/disks we want to use inside our application.

declare module '@ioc:Adonis/Core/Drive' {
interface DisksList {
local: {
config: LocalDriverConfig
implementation: LocalDriverContract
}
s3: {
config: S3DriverConfig
implementation: S3DriverContract
}
}
}

In a nutshell, we have disk mappings in two places.

  • First is the contracts file for the TypeScript compiler.
  • Another is the config file for runtime JavaScript.

We can do better here and reduce the mental fatigue of defining mappings at multiple places. Technically it is possible by inferring the TypeScript types directly from the config.

In this and the upcoming releases, we will incrementally remove the manually defined mappings from the interface and use config inference. This release focuses on Drive, Mailer, and the Hash module.

Updating Drive config and contract

Open the config/drive.ts file and wrap the config object within the driveConfig method below.

config/drive.ts
import { DriveConfig } from '@ioc:Adonis/Core/Drive'
import { driveConfig } from '@adonisjs/core/build/config'
const driveConfig: DriveConfig = {
// ...configObject
export default driveConfig({
})
export default driveConfig

The next step is to replace the entire contents of the contracts/drive.ts file with the following code block.

contracts/drive.ts
import { InferDisksFromConfig } from '@adonisjs/core/build/config'
import driveConfig from '../config/drive'
declare module '@ioc:Adonis/Core/Drive' {
interface DisksList extends InferDisksFromConfig<typeof driveConfig> {}
}

Updating Mail config and contract

Open the config/mail.ts file and wrap the config object within the mailConfig method below.

config/mail.ts
import { MailConfig } from '@ioc:Adonis/Addons/Mail'
import { mailConfig } from '@adonisjs/mail/build/config'
const mailConfig: MailConfig = {
// ...configObject
export default mailConfig({
})
export default mailConfig

The next step is to replace the entire contents of the contracts/mail.ts file with the following code block.

contracts/mail.ts
import { InferMailersFromConfig } from '@adonisjs/mail/build/config'
import mailConfig from '../config/mail'
declare module '@ioc:Adonis/Addons/Mail' {
interface MailersList extends InferMailersFromConfig<typeof mailConfig> {}
}

Updating Hash config and contract

Open the config/hash.ts file and wrap the config object within the hashConfig method below.

config/hash.ts
import { HashConfig } from '@ioc:Adonis/Core/Hash'
import { hashConfig } from '@adonisjs/core/build/config'
const hashConfig: HashConfig = {
// ...configObject
export default hashConfig({
})
export default hashConfig

The next step is to replace the entire contents of the contracts/hash.ts file with the following code block.

contracts/hash.ts
import { InferListFromConfig } from '@adonisjs/core/build/config'
import hashConfig from '../config/hash'
declare module '@ioc:Adonis/Core/Hash' {
interface HashersList extends InferListFromConfig<typeof hashConfig> {}
}

That is all. We will also introduce config type inference for other packages in future releases.

Lucid model factory improvements and new commands

The following new methods/properties have been added to Lucid model factories.

tap

The tap method allows you to access the underlying model instance before it is persisted using the factory. You can use this method to define additional attributes on the model.

UserFactory
.tap((user) => user.isAdmin => true)
.create()

parent

You can access the parent of a relationship inside the with callback. This is usually helpful when you want to infer some attributes from the parent model.

We infer the tenantId property from the User model instance in the following example.

await TenantFactory
.with('users', 1, (user) => {
user
.with('posts', 2, (post) => {
post.merge({ tenantId: post.parent.tenantId })
})
})
.create()

pivotAttributes

You can define pivot attributes for a many-to-many relationship using the pivotAttributes method.

await Team.with('users', 2, (user) => {
user.pivotAttributes({ role: 'admin' })
})

mergeRecursive

You often want to merge attributes with all the children's relationships.

Let's take the previous example of defining the tenant id on the Post model. Instead of accessing the Post factory builder and then calling the merge method, we can recursively pass the tenantId to all the children relationships from the User factory.

await TenantFactory
.with('users', 1, (user) => {
user
.mergeRecursive({ tenantId: user.parent.id })
.with('posts', 2)
.with('posts', 2, (post) => {
post.merge({ tenantId: post.parent.tenantId })
})
})
.create()

make:factory command

You can now make a new factory by running the node ace make:factory ModelName command. Also, you can pass the -f flag to the make:model command to create a factory alongside a model.

node ace make:factory User
# create a factory with model
node ace make:model User -f
# create factory + migration with model
node ace make:model User -cf

Upgrading to Knex 2.0

We have upgraded the Knex version to 2.0, resulting in a small breaking change for SQLite users. Earlier, knex used the @vscode/sqlite3 package for the SQLite connection. However, now it relies on the sqlite3 package.

If you are using SQLite, uninstall the old dependency and favor the new one.

npm uninstall @vscode/sqlite3
npm install sqlite3

Lucid now also ships with the following query builder methods. They were introduced in Knex 1.0.

  • whereLike and whereILike
  • whereJson, whereJsonSuperset, whereJsonSubset, and whereJsonPath.

Drive improvements

You can now switch buckets at runtime before performing a Drive operation. The drive-s3 and the drive-gcs ship with a new bucket method.

await Drive
.use('s3')
.bucket('bucket-name')
.put(path, contents)

Prevention for path traversal

In earlier versions of Drive using local driver, the absolute paths were working with drive methods without prefixing them with disk root. To prevent Path Traversal it has been changed to always resolve paths relative to the root of the defined disk and not from the entire filesystem. If you are using absolute paths with Drive like in the example below, please change it to relative locations. This applies to all Drive methods not only put.

const filename = 'somefile.txt'
const contents = '...'
await Drive.put(
// DO NOT resolve absolute path for filename
Application.tmpPath('uploads', filename),
contents
)
// instead just let Drive to prefix it with
// defined root for given disk inside config
await Drive.put(filename, contents)

Experimental list method

The local driver of Drive now ships with an expiremental list method to list all files inside a given directory. We will implement the same for the S3 and the GCS drivers in the coming days.

Since implementing the list method is experimental, we might change the final APIs.

Testing improvements

  • A new make:suite command has been added to create a new test suite and register it inside the .adonisrc.json file.
  • Add support to filter tests by the test title. For example, you can now pass the --tests="the title of the test"to run tests matching the mentioned title.
  • The migrations and database seeders now runs in compact mode during tests.

Commits

  • feat: filter tests by title - 09d2b81
  • fix: array filters passed to TestProcess were not processed properly - f7a3069
  • feat: add make:suite command 018da9d
  • feat: add support to find routes by identifier 7920caf
  • feat: add route lookup methods on the router class 9211cfd
  • fix: do not use a magic method to compute file name when the file type is not supported 0740a35
  • fix: retain default value assigned to arguments on the class instance cf51363
  • fix: use the mutated value for validating minLength and maxLength 80081bb
  • fix: define serialize property for the attachment column b847dae
  • fix: do not persist pre-computed URL to be database ecc6397
  • feat: add config helper and type from infer disklist from config 92d1448
  • feat: add support for listing files inside a directory e98cb43
  • feat: add hashConfig method to define the hash config 87f8e4f
  • fix: add stayAlive to ListRoutes command (#3703) 36406f5
  • fix: check for the main command name inside command aliases as well 1f8da36
  • feat: add support to switch bucket at runtime 8d75ec4
  • feat: add support to switch bucket at runtime 208e5bb
  • feat: add mailConfig method to infer types from user-defined config 7cd6313
  • feat: add redisConfig method to infer types from config abc1ce3
  • fix: use response cookies to read the session id 38ccee9
  • refactor: regenerate session id as soon as regenerate method is called cb21abf
  • feat: add sessionConfig helper method 70cbca0
  • feat: add dumpSession method 7289496
  • refactor: use anonymous classes for migrations (#829) 3d12a45
  • chore: remove @vscode/sqlite3 in favor of sqlite3 2332fbf
  • refactor: pass factory builder instance to all factory operations 72f233c
  • refactor: remove inline callbacks in favor of tap method 13588af
  • feat: add support to define pivot attributes via factory builder 49a1d04
  • feat: allow passing model assignment options via relationship persistence methods 5b2f846
  • feat: allow model properties to report if they are dirty or not e40297c
  • feat: add --compact-output flag to run/rollback commands (#836) f8e0c8c
  • feat: make:factory command + --factory flag to make (#837) bd22c96
  • feat: add withMaterialized and withNotMaterialized to query builder 04c5c25
  • feat: add whereLike and whereILike methods 1b6001d
  • feat: expose knex query builder instance in schema classes dff3f84
  • feat: add --compact-output on DbSeed command 3880c8d
  • feat: add where"JSON" methods introduced by Knex 1.0 f875828
  • fix: prefix table name when making relationship join queries 9385a9b
  • feat: add support for recursively merging attributes from factories 8f708b3