Easily strub new action classes inside your AdonisJS 6 project
node ace add @adocasts.com/actions
- Installs
@adocasts.com/actions
. - Automatically configures the
make:action
command via youradonisrc.ts
file.
First, install
npm i @adocasts.com/actions@latest
Then, configure
node ace configure @adocasts.com/actions
Once @adocasts.com/actions
is installed & configured in your application,
you'll have access to the node ace make:action [name]
command.
For example, to create a RegisterFromForm
action, you can do:
node ace make:action RegisterUser
Which creates an action class at: app/actions/register_user.ts
type Params = {}
export default class RegisterUser {
static async handle({}: Params) {
// do stuff
}
}
Apps have lots of actions they perform, so it's a great idea to group them into feature/resource folders.
This can be easily done via the --feature
flag.
node ace make:action register_user --feature=auth
This will then create our action class at:
app/actions/auth/register_from_form.ts
Also, note in both the above examples, the file name was normalized.
Though actions are typically meant to be self contained, if your action is only going to handle an HTTP Request, you can optionally include an injection of the HttpContext
directly within your action class via the --http
flag.
This, obviously, is up to you/your team with whether you'd like to use it.
node ace make:action register_user --http --feature=auth
Which then creates: app/actions/auth/register_from_form.ts
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
type Params = {}
@inject()
export default class RegisterUser {
constructor(protected ctx: HttpContext) {}
async handle({}: Params) {
// do stuff
}
}
Unfamiliar with this approach? You can learn more via the AdonisJS HTTP Context documentation.
What does this look like in practice? Let's take a look! Lets say we have a simple Difficulty
model
// app/models/difficulty.ts
export default class Difficulty extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare organizationId: number
@column()
declare name: string
@column()
declare color: string
@column()
declare order: number
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
@belongsTo(() => Organization)
declare organization: BelongsTo<typeof Organization>
}
First, we'll want to create a controller, this will be in charge of taking in the request and returning a response.
node ace make:controller difficulty store update
For our example, we'll stub it with a store
and update
method, and the generated file will look like this:
// app/controllers/difficulties_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
export default class DifficultiesController {
async store({}: HttpContext) {}
async update({}: HttpContext) {}
}
Cool, now let's get it taking in the request and returning a response for both handlers.
// app/controllers/difficulties_controller.ts
import { difficultyValidator } from '#validators/difficulty'
import type { HttpContext } from '@adonisjs/core/http'
export default class DifficultiesController {
async store({ request, response }: HttpContext) {
const data = await request.validateUsing(difficultyValidator)
// TODO: create the difficulty
return response.redirect().back()
}
async update({ request, response, params }: HttpContext) {
const data = await request.validateUsing(difficultyValidator)
// TODO: update the difficulty
return response.redirect().back()
}
}
Think of actions like single-purpose service classes. We'll have a single file meant to perform one action. As you may have guessed, this means we'll have a good number of actions within our application, so we'll also want to nest them within folders to help scope them. The depth of this will be determined by the complexity of your application.
Our application is simple, so let's nest ours within a single "resource" feature folder called difficulties
.
So, we'll have one action to create a difficulty:
node ace make:action create_difficulty --feature=difficulties
And, another to update a difficulty:
node ace make:action difficulties/create_difficulty
Note, you can easily nest within folders by either using the --feature
flag or including the folder path in the name parameter.
When we create an action, we're provided an empty Params
type.
We'll want to fill that in with our handler's expected parameters.
Then, handle the needed operations to complete an action
Here's our CreateDifficulty action:
// app/actions/difficulties/create_difficulty.ts
import Organization from '#models/organization'
import { difficultyValidator } from '#validators/difficulty'
import { Infer } from '@vinejs/vine/types'
type Params = {
organization: Organization
data: Infer<typeof difficultyValidator>
}
export default class CreateDifficulty {
static async handle({ organization, data }: Params) {
// finds the next `order` for the organization
const order = await organization.findNextSort('difficulties')
// creates the difficulty scoped to the organization
return organization.related('difficulties').create({
...data,
order,
})
}
}
Assupmtion: the organization has a method on it called findNextSort
And, our UpdateDifficulty action:
// app/actions/difficulties/update_difficulty.ts
import Organization from '#models/organization'
import { difficultyValidator } from '#validators/difficulty'
import { Infer } from '@vinejs/vine/types'
type Params = {
organization: Organization
id: number
data: Infer<typeof difficultyValidator>
}
export default class UpdateDifficulty {
static async handle({ organization, id, data }: Params) {
// find the existing difficulty via id within the organization
const difficulty = await organization
.related('difficulties')
.query()
.where({ id })
.firstOrFail()
// merge in new data and update
await difficulty.merge(data).save()
// return the updated difficulty
return difficulty
}
}
Lastly, we just need to use our actions inside our controller.
// app/controllers/difficulties_controller.ts
import CreateDifficulty from '#actions/difficulties/create_difficulty'
import UpdateDifficulty from '#actions/difficulties/update_difficulty'
import { difficultyValidator } from '#validators/difficulty'
import type { HttpContext } from '@adonisjs/core/http'
export default class DifficultiesController {
async store({ request, response, organization }: HttpContext) {
const data = await request.validateUsing(difficultyValidator)
await CreateDifficulty.handle({ organization, data })
return response.redirect().back()
}
async update({ params, request, response, organization }: HttpContext) {
const data = await request.validateUsing(difficultyValidator)
await UpdateDifficulty.handle({
id: params.id,
organization,
data,
})
return response.redirect().back()
}
}
Assumption: the organization is being added onto the HttpContext within a middleware prior to our controller being called.