Easily strub new action classes inside your AdonisJS 6 project
node ace add @adocasts.com/actions- Installs
@adocasts.com/actions. - Automatically configures the
make:actioncommand via youradonisrc.tsfile.
First, install
npm i @adocasts.com/actions@latestThen, configure
node ace configure @adocasts.com/actionsOnce @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 RegisterUserWhich creates an action class at: app/actions/register_user.ts
typeParams={}exportdefaultclassRegisterUser{staticasynchandle({}: 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=authThis will then create our action class at:
app/actions/auth/register_from_form.tsAlso, 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=authWhich then creates: app/actions/auth/register_from_form.ts
import{inject}from'@adonisjs/core'import{HttpContext}from'@adonisjs/core/http'typeParams={}
@inject()exportdefaultclassRegisterUser{constructor(protectedctx: HttpContext){}asynchandle({}: Params){// do stuff}}Unfamiliar with this approach? You can learn more via the AdonisJS HTTP Context documentation.
As of v1.0.5 you can now also create CRUD actions for a resource all in one go using the --resource flag!
node ace make:action user --resourceThis one command will then generate the following actions
- GetUser (
app/actions/users/get_user.ts) - GetUsers (
app/actions/users/get_users.ts) - StoreUser (
app/actions/users/store_user.ts) - UpdateUser (
app/actions/users/update_user.ts) - DestroyUser (
app/actions/users/destroy_user.ts)
What does this look like in practice? Let's take a look! Let's say we have a simple Difficulty model
// app/models/difficulty.tsexportdefaultclassDifficultyextendsBaseModel{
@column({isPrimary: true})declareid: number
@column()declareorganizationId: number
@column()declarename: string
@column()declarecolor: string
@column()declareorder: number
@column.dateTime({autoCreate: true})declarecreatedAt: DateTime
@column.dateTime({autoCreate: true,autoUpdate: true})declareupdatedAt: DateTime
@belongsTo(()=>Organization)declareorganization: BelongsTo<typeofOrganization>}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 updateFor our example, we'll stub it with a store and update method, and the generated file will look like this:
// app/controllers/difficulties_controller.tsimporttype{HttpContext}from'@adonisjs/core/http'exportdefaultclassDifficultiesController{asyncstore({}: HttpContext){}asyncupdate({}: HttpContext){}}Cool, now let's get it taking in the request and returning a response for both handlers.
// app/controllers/difficulties_controller.tsimport{difficultyValidator}from'#validators/difficulty'importtype{HttpContext}from'@adonisjs/core/http'exportdefaultclassDifficultiesController{asyncstore({ request, response }: HttpContext){constdata=awaitrequest.validateUsing(difficultyValidator)// TODO: create the difficultyreturnresponse.redirect().back()}asyncupdate({ request, response, params }: HttpContext){constdata=awaitrequest.validateUsing(difficultyValidator)// TODO: update the difficultyreturnresponse.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=difficultiesAnd, another to update a difficulty:
node ace make:action difficulties/update_difficultyNote, 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.tsimportOrganizationfrom'#models/organization'import{difficultyValidator}from'#validators/difficulty'import{Infer}from'@vinejs/vine/types'typeParams={organization: Organizationdata: Infer<typeofdifficultyValidator>}exportdefaultclassCreateDifficulty{staticasynchandle({ organization, data }: Params){// finds the next `order` for the organizationconstorder=awaitorganization.findNextSort('difficulties')// creates the difficulty scoped to the organizationreturnorganization.related('difficulties').create({
...data,
order,})}}Assupmtion: the organization has a method on it called findNextSort
And, our UpdateDifficulty action:
// app/actions/difficulties/update_difficulty.tsimportOrganizationfrom'#models/organization'import{difficultyValidator}from'#validators/difficulty'import{Infer}from'@vinejs/vine/types'typeParams={organization: Organizationid: numberdata: Infer<typeofdifficultyValidator>}exportdefaultclassUpdateDifficulty{staticasynchandle({ organization, id, data }: Params){// find the existing difficulty via id within the organizationconstdifficulty=awaitorganization.related('difficulties').query().where({ id }).firstOrFail()// merge in new data and updateawaitdifficulty.merge(data).save()// return the updated difficultyreturndifficulty}}Lastly, we just need to use our actions inside our controller.
// app/controllers/difficulties_controller.tsimportCreateDifficultyfrom'#actions/difficulties/create_difficulty'importUpdateDifficultyfrom'#actions/difficulties/update_difficulty'import{difficultyValidator}from'#validators/difficulty'importtype{HttpContext}from'@adonisjs/core/http'exportdefaultclassDifficultiesController{asyncstore({ request, response, organization }: HttpContext){constdata=awaitrequest.validateUsing(difficultyValidator)awaitCreateDifficulty.handle({ organization, data })returnresponse.redirect().back()}asyncupdate({ params, request, response, organization }: HttpContext){constdata=awaitrequest.validateUsing(difficultyValidator)awaitUpdateDifficulty.handle({id: params.id,
organization,
data,})returnresponse.redirect().back()}}Assumption: the organization is being added onto the HttpContext within a middleware prior to our controller being called.