Tuesday, 2 May, 2017 UTC


Summary

This is the concluding part of Build A Support Ticket Application With AdonisJs. In part 1, we setup the application, built the authentication system for users to signup, login, and logout. We created the ticket and category model as well as their migrations and also defined their relationships. Lastly, we created our first ticket and sent the ticket information mail. In case you missed it, you should check Build A Support Ticket Application With AdonisJs - Part 1 out because we will be continuing from where we stopped.
In this part, we'll give users a way to see all the tickets they have created and also ability to view a particular ticket. We'll also cover commenting on a ticket and mark ticket as closed. Finally, we'll restrict access to the application to only authenticated users.
Displaying User Tickets
We start off by allowing our users to see a list of all the tickets they have created and from there they can go on to view a particular ticket. Add the code below to app/Http/Controllers/TicketsController.js:
// app/Http/Controllers/TicketsController.js

// remember to add these to the top of file
const Mail = use('Mail')
const Validator = use('Validator')
const Ticket = use('App/Model/Ticket')
const RandomString = use('randomstring')
const Category = use('App/Model/Category')

/**
* Display all tickets by a user.
*/
* userTickets(request, response) {
    // Get all the tickets created by the currently authenticated user
    const tickets = yield Ticket.query().where('user_id', request.currentUser.id).fetch()
    // Get all categories
    const categories = yield Category.all()

    yield response.sendView('tickets.user_tickets', { tickets: tickets.toJSON(), categories: categories.toJSON() })
}
We are getting the tickets created by the currently authenticated user by passing request.currentUser.id to the where(), where we also get all the categories. The tickets together with the categories are passed to a view file which we'll create shortly. Before creating the view file, let's create the route that will handle displaying all tickets created by a user. Add the line below to app/Http/routes.js:
// app/Http/routes.js

Route.get('my_tickets', 'TicketsController.userTickets')
With the route created, let's go on and create the view. Create a new view file named user_tickets.njk within the resources/views/tickets directory and paste the code below to it:

<!-- resources/views/tickets/user_tickets.njk -->

{% extends 'layouts.master' %}

{% block title %} My Tickets {% endblock %}

{% block content %}
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">
                    My Tickets</i>
                </div>

                <div class="panel-body">
                    {% if tickets.length == 0 %}
                        <p>You have not created any tickets.</p>
                    {% else %}
                    <table class="table">
                        <thead>
                            <tr>
                                <th>Category</th>
                                <th>Title</th>
                                <th>Status</th>
                                <th>Last Updated</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for ticket in tickets %}
                            <tr>
                                <td>
                                    {% for category in categories %}
                                        {% if category.id == ticket.category_id %}
                                            {{ category.name }}
                                        {% endif %}
                                    {% endfor %}
                                </td>
                                <td>
                                    <a href="tickets/{{ ticket.ticket_id }}">
                                        #{{ ticket.ticket_id }} - {{ ticket.title }}
                                    </a>
                                </td>
                                <td>
                                    {% if ticket.status == 'Open' %}
                                        <span class="label label-success">{{ ticket.status }}</span>
                                    {% else %}
                                        <span class="label label-danger">{{ ticket.status }}</span>
                                    {% endif %}
                                </td>
                                <td>{{ ticket.updated_at }}</td>
                            </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
{% endblock %}
First, we check if there are tickets and then display them within a table. For the ticket category, we check if the ticket category_id is equal to the category id then display the name of the category.
Now if you visit the route /my_tickets you should see a page listing the tickets that have been created my the currently authenticated user like the image below:
Showing A Ticket
Now that users can see a list of all the tickets they have created, wouldn't it be nice if they could view a particular ticket? Of course, it will.
Let's tackle that. We want the user to be able to access a particular ticket by this route tickets/ticket_id. Okay, let's create the route in app/Http/routes.js:
// app/Http/routes.js

Route.get('tickets/:ticket_id', 'TicketsController.show')
As you can see, we pass a parameter :ticket_id (which will be the ID of a specific ticket) along with the route. When the user hits that route, the show(), which we are yet to create, will be triggered.
Before we create the show(), let's define User to Ticket relationship.
User To Ticket Relationship
A Ticket can belong to a User, while a User can have many Tickets. This is a one to many relationship which we'll use Lucid to setup. Open the app/Model/User.js model and paste the code below into it:
// app/Model/User.js

/**
* A user can have many tickets
*/
tickets() {
    return this.hasMany('App/Model/Ticket')
}
Now let's edit the app/Model/Ticket.js model in the same manner to add the inverse relationship:
// app/Model/Ticket.js

/**
* A ticket belongs to a user
*/
user() {
    return this.belongsTo('App/Model/User')
}
Now add this code below to app/Http/Controllers/TicketsController.js:
// app/Http/Controllers/TicketsController.js

/**
* Display a specified ticket.
*/
* show(request, response) {
    // Get the ticket with the user that created it
    const ticket = yield Ticket.query()
                            .where('ticket_id', request.param('ticket_id'))
                            .with('user')
                            .firstOrFail()
    // Get the ticket category
    const category = yield ticket.category().fetch()

    yield response.sendView('tickets.show', {
        ticket: ticket.toJSON(),
        category: category.toJSON()
    })
}
We get the ticket (with the ticket_id passed from the route using request.param('ticket_id')) along with the user that created it (using the relationship defined above). Remember from Part 1 where we define our Ticket to Category relationship, so we can get the category which the ticket belongs to using ticket.category().fetch(). And finally, we pass the ticket and category to a view file.
Create a view file in the resources/views/tickets directory with the name show.njk and paste the code below into it:
<!-- resources/views/tickets/show.njk -->

{% extends 'layouts.master' %}

{% block title %}
{{ ticket.title }}
{% endblock %}

{% block content %}
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">
                    #{{ ticket.ticket_id }} - {{ ticket.title }}
                </div>

                <div class="panel-body">
                    {% include 'includes.status' %}
                    {% include 'includes.errors' %}

                    <div class="ticket-info">
                        <p>{{ ticket.message }}</p>
                        <p>Categry: {{ category.name }}</p>
                        <p>
                            {% if ticket.status == 'Open' %}
                                Status: <span class="label label-success">{{ ticket.status }}</span>
                            {% else %}
                                Status: <span class="label label-danger">{{ ticket.status }}</span>
                            {% endif %}
                        </p>
                        <p>Created on: {{ ticket.created_at }}</p>
                    </div>

                    <hr>

                    <div class="comment-form">
                        {{ form.open({ url: '/comment', method: 'POST', class: 'form' }) }}
                            {{ csrfField }}

                            {{ form.hidden('ticket_id', ticket.id) }}

                            <div class="form-group">
                                {{ form.textarea('comment', null, { class: 'form-control', rows: 10 }) }}
                            </div>

                            <div class="form-group">
                                {{ form.button('Submit', null, { class: 'btn btn-primary' }) }}
                            </div>
                        {{ form.close() }}
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}
You should get something like the image below when you click on the link to a particular ticket.
If you noticed, there is a text box which will allow the user to reply/comment on the ticket. Don't worry, we'll get to it shortly. We'll be updating the resources/views/tickets/show.njk too to display the comments on the ticket.
Commenting On A Ticket
Now let's allow the user to comment on a ticket. But first, we need a place to store the comments. Create a Comment model and a migration file with the command below:
./ace make:model Comment --migration
Open the migration file just created and update with the code below:
// database/migrations/...create_comment_table.js

// comments table migration showing only the up() schemas with our modifications

this.create('comments', (table) => {
    table.increments()
    table.integer('ticket_id').unsigned()
    table.integer('user_id').unsigned()
    table.text('comment')
    table.timestamps()
})
The schema is pretty straightforward. Run the migration:
./ace migration:run
Ticket To Comment Relationship
A Comment can belong to a Ticket, while a Ticket can have many Comments. This is a one to many relationship which we'll use Lucid to setup. Open the app/Model/Comment.js model and paste the code below into it:
// app/Model/Comment.js

/**
* A comment belongs to a particular ticket
*/
ticket() {
    return this.belongsTo('App/Model/Ticket')
}
Now let's edit the app/Model/Ticket.js model in the same manner to add the inverse relationship:
// app/Model/Ticket.js

/**
* A ticket can have many comments
*/
comments() {
    return this.hasMany('App/Model/Comment')
}
User To Comment Relationship
A Comment can belong to a User, while a User can have many Comments. This is a one to many relationship which we'll Lucid to setup. Open the app/Model/Comment.js model and paste the code below into it:
// app/Model/Comment.js

/**
* A comment belongs to a user
*/
user() {
    return this.belongsTo('App/Model/User')
}
Now let's edit the app/Model/User.js model in the same manner to add the inverse relationship:
// app/Model/User.js

/**
* A user can have many comments
*/
comments() {
    return this.hasMany('App/Model/Comment')
}
Comment Controller
We need to create a new HTTP request controller that will handle all comments specific logics.
./ace make:controller Comments
This will create app/Http/Controllers/CommentsController.js. The CommentsController will have only one method called postComment().
// app/Http/Controllers/CommentsController.js

// remember to add these to the top of file
const Mail = use('Mail')
const Validator = use('Validator')
const Comment = use('App/Model/Comment')

/**
* Persist comment and mail user
*/
* postComment(request, response) {
    // Get the currently authenticated user
    const user = request.currentUser

    // validate form input
    const validation = yield Validator.validateAll(request.all(), {
        comment: 'required'
    })

    // show error messages upon validation fail
    if (validation.fails()) {
        yield request
            .withAll()
            .andWith({ errors: validation.messages() })
            .flash()

        return response.redirect('back')
    }

    // persist comment to database
    const comment = yield Comment.create({
        ticket_id: request.input('ticket_id'),
        user_id: user.id,
        comment: request.input('comment'),
    })

    // Get the ticket the comment was made on
    const commentTicket = yield comment.ticket().fetch()
    // Get the user that created the ticket that was just commentted on
    const commentUser = yield commentTicket.user().fetch()

    // send mail if the user commenting is not the ticket owner
    if (commentUser.id != user.id) {
        yield Mail.send('emails.ticket_comments', { commentUser, user, commentTicket, comment }, (message) => {
            message.to(commentUser.email, commentUser.username)
            message.from('[email protected]')
            message.subject(`RE: ${commentTicket.title} (Ticket ID: ${commentTicket.ticket_id})`)
        })
    }

    yield request.with({ status: 'Your comment has been submitted.' }).flash()
    response.redirect('back')
}
What the postComment() does is simple; make sure the comment box is filled, store a comment in the comments table with the ticket_id, user_id and the actual comment. Then send an email, if the user commenting is not the ticket owner (that is, if the comment was made by an admin, an email will be sent to the ticket owner). And finally, display a status message.
You will notice that I used the relationships defined above extensively. Let me further explain the code snippet.
// Get the ticket the comment was made on
const commentTicket = yield comment.ticket().fetch()
// Get the user that created the ticket that was just commentted on
const commentUser = yield commentTicket.user().fetch()

// send mail if the user commenting is not the ticket owner
if (commentUser.id != user.id) {
    yield Mail.send('emails.ticket_comments', { commentUser, user, commentTicket, comment }, (message) => {
        message.to(commentUser.email, commentUser.username)
        message.from('[email protected]')
        message.subject(`RE: ${commentTicket.title} (Ticket ID: ${commentTicket.ticket_id})`)
    })
}
We get the ticket the comment was made on, and then we get the user that created the ticket that was just commented on. Next, we check to see if the user ID of the ticket that was commented on is not equal to the currently authenticated user ID. If it is (that is, ticket owner made the comment) there is no point sending an email to the user since he/she is the one that made the comment. If it is not (that is, an admin made the comment) an email is sent to the ticket owner.
Comment Form
Let's take a closer look at the comment box included in the resources/views/tickets/show.njk:
<!-- resources/views/tickets/show.njk -->

<div class="comment-form">
    {{ form.open({ url: '/comment', method: 'POST', class: 'form' }) }}
        {{ csrfField }}

        {{ form.hidden('ticket_id', ticket.id) }}

        <div class="form-group">
            {{ form.textarea('comment', null, { class: 'form-control', rows: 10 }) }}
        </div>

        <div class="form-group">
            {{ form.button('Submit', null, { class: 'btn btn-primary' }) }}
        </div>
    {{ form.close() }}
</div>
The form will be sent as a POST request to the comment route which we are yet to create. Go on and add this to app/Http/routes.js:
// app/Http/routes.js

Route.post('comment', 'CommentsController.postComment')
Now you should be able to comment on a ticket but even after commenting, you won't see the comment yet because we haven't updated the show ticket view to display comments. First, we need to update the TicketsController.js's show() to also fetch comments and pass it to the view.
// app/Http/Controllers/TicketsController.js

/**
* Display a specified ticket.
*/
* show(request, response) {
    // Get the ticket with the user that created it
    const ticket = yield Ticket.query()
                            .where('ticket_id', request.param('ticket_id'))
                            .with('user')
                            .firstOrFail()
    // Get the comments that have been made on the ticket with user that made the comments
    const comments = yield ticket.comments().with('user').fetch()
    const category = yield ticket.category().fetch()

    yield response.sendView('tickets.show', {
        ticket: ticket.toJSON(),
        comments: comments.toJSON(),
        category: category.toJSON()
    })
}
Also update resources/views/tickets/show.njk, add the code below just after <hr> to do just that.
<!-- resources/views/tickets/show.njk -->

<div class="comments">
    {% for comment in comments %}
        <div class="panel panel-{{ "default" if ticket.user.id == comment.user_id else "success" }}">
            <div class="panel panel-heading">
                {{ comment.user.username }}
                <span class="pull-right">{{ comment.created_at }}</span>
            </div>

            <div class="panel panel-body">
                {{ comment.comment }}
            </div>
        </div>
    {% endfor %}
</div>
When a comment is made on a ticket, we should get a screen similar to the image below:
Sending Comment Email
Let's take a closer look at the mail sending snippet from the postComment() in the CommentsController:
// app/Http/Controllers/CommentsController.js

yield Mail.send('emails.ticket_comments', { commentUser, user, commentTicket, comment }, (message) => {
    message.to(commentUser.email, commentUser.username)
    message.from('[email protected]')
    message.subject(`RE: ${commentTicket.title} (Ticket ID: ${commentTicket.ticket_id})`)
})
The Mail's send() accepts a path to a view file email/ticket_comments.njk, an object of data (commentUser, user, commentTicket, and comment) we want to pass to the view file and lastly a callback. The view file will be inside the emails directory and will be used to compose the mail we want to send. The message argument passed to the callback is an instance of message builder which is used to build the mail by specifying who we want to send the mail to, who is sending the mail and the subject of the mail.
Now let's create the email view file. Create a new ticket_comments.njk file within the resources/views/email directory and add:
<!-- resources/views/emails/ticket_comments.njk -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Suppor Ticket</title>
</head>
<body>
    <p>
    {{ comment.comment }}
    </p>

---
    <p>Replied by: {{ user.username }}</p>

    <p>Title: {{ commentTicket.title }}</p>
    <p>Ticket ID: {{ commentTicket.ticket_id }}</p>
    <p>Status: {{ commentTicket.status }}</p>

    <p>
        You can view the ticket at any time at http://localhost:3333/tickets/{{ commentTicket.ticket_id }}
    </p>

</body>
</html>
Marking Ticket As Closed
The last feature of our support ticket application is the ability to mark a ticket as closed. But a ticket can only be marked as closed by an admin. So we need to setup an admin. Remember from part 1 when we created our users table, we created a column called is_admin with a default value of 0 which indicate not an admin. A quick refresher:
table.integer('is_admin').unsigned().default(0);
AdminMiddleware
We are going to create an AdminMiddleware which will allow only an admin access to an admin only area. Run the command below to create AdminMiddleware:
./ace make:middleware AdminMiddleware
Update the handle() with:
// app/Http/Middleware/AdminMiddleware.js

* handle (request, response, next) {
    // Redirect to homepage if the currently authenticated user is not an admin
    if (request.currentUser.is_admin !== 1) {
        response.redirect('/');
    } 

    yield next
}
If the authenticated user is not an admin, he/she will be redirected to the homepage, else will proceed to the admin area. Next, we need to register our newly created middleware. Open app/Http/kernel.js and add the new middleware to the namedMiddleware array. You should have something like below after adding:
// app/Http/kernel.js

const namedMiddleware = {
    ...
    admin: 'App/Http/Middleware/AdminMiddleware'
}
Now that the admin middleware has been registered, let's go on and create our admin specific routes. Add the following to app/Http/routes.js
// app/Http/routes.js

Route.group('admin', function () {
    Route.get('tickets', 'TicketsController.index');
    Route.post('close_ticket/:ticket_id', 'TicketsController.close');
}).prefix('admin').middleware('admin')
The code above is pretty straightforward, define some routes only an admin can access. The first route /admin/tickets will trigger the index() (which we are yet to create) on the TicketsController which will get all the tickets that have been created and render a view file passed along the tickets.
// app/Http/Controllers/TicketsController.js

/**
* Display all tickets.
*/
* index(request, response) {
    // Get all tickets
    const tickets = yield Ticket.all()
    // Get all categories
    const categories = yield Category.all()

    yield response.sendView('tickets.index', {
        tickets: tickets.toJSON(),
        categories: categories.toJSON()
    })
}
For the view file, create a file index.njk within the resources/views/tickets directory and paste the code into it:
<!-- resources/views/tickets/index.njk -->

{% extends 'layouts.master' %}

{% block title %} All Tickets {% endblock %}

{% block content %}
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <i class="fa fa-ticket"> Tickets</i>
                </div>

                <div class="panel-body">
                    {% if tickets.length == 0 %}
                        <p>There are currently no tickets.</p>
                    {% else %}
                    <table class="table">
                        <thead>
                            <tr>
                                <th>Category</th>
                                <th>Title</th>
                                <th>Status</th>
                                <th>Last Updated</th>
                                <th style="text-align:center" colspan="2">Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for ticket in tickets %}
                                <tr>
                                    <td>
                                        {% for category in categories %}
                                            {% if category.id == ticket.category_id %}
                                                {{ category.name }}
                                            {% endif %}
                                        {% endfor %}
                                    </td>
                                    <td>
                                        <a href="/tickets/{{ ticket.ticket_id }}">
                                            #{{ ticket.ticket_id }} - {{ ticket.title }}
                                        </a>
                                    </td>
                                    <td>
                                        {% if ticket.status == 'Open' %}
                                            <span class="label label-success">{{ ticket.status }}</span>
                                        {% else %}
                                            <span class="label label-danger">{{ ticket.status }}</span>
                                        {% endif %}
                                    </td>
                                    <td>{{ ticket.updated_at }}</td>
                                    <td>
                                        <a href="/tickets/{{ ticket.ticket_id }}" class="btn btn-primary">Comment</a>
                                    </td>
                                    <td>
                                        {{ form.open({ url: '/admin/close_ticket/' + ticket.ticket_id, method: 'POST' }) }}
                                            {{ csrfField }}
                                            {{ form.button('Close', null, { class: 'btn btn-danger' }) }}
                                        {{ form.close() }}
                                    </td>
                                </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endblock %}
Below is what an admin page will look like:
The second route close_ticket/ticket_id will trigger the close() on the ticketsController. The method will handle closing a particular ticket.
// app/Http/Controllers/TicketsController.js

/**
* Close the specified ticket.
*/
* close(request, response) {
    // Get the ticket with the specified ticket_id
    const ticket = yield Ticket.query()
                            .where('ticket_id', request.param('ticket_id'))
                            .firstOrFail()
    // Change the ticket status to closed
    ticket.status = 'Closed'
    // Persist to database
    yield ticket.save()

    // Get the user that created the ticket
    const ticketOwner = yield ticket.user().fetch()

    // send email
    yield Mail.send('emails.ticket_status', { ticketOwner, ticket }, (message) => {
        message.to(ticketOwner.email, ticketOwner.username)
        message.from('[email protected]')
        message.subject(`RE: ${ticket.title} (Ticket ID: ${ticket.ticket_id})`)
    })

    yield request.with({ status: 'The ticket has been closed.' }).flash()
    response.redirect('back')
}
We get the ticket with the ticket_id passed from the route using request.param('ticket_id'), and set it status to Closed and save it. Then send an email to the owner of the ticket. And finally, display a status message.
We need one more view file; the ticket_status email view file. Go on and create ticket_status.njk and add:
<!-- resources/views/emails/ticket_status.njk -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Suppor Ticket Status</title>
</head>
<body>
    <p>
        Hello {{ ticketOwner.username }},
    </p>
    <p>
        Your support ticket with ID #{{ ticket.ticket_id }} has been marked has resolved and closed.
    </p>
</body>
</html>
Allowing Only Authenticated Users Access
Only authenticated users can use our support ticket application. AdonisJs has us covered by providing authenticate middleware which will allow only authenticated users access to the routes in which the auth middleware is added to. Open app/Http/routes.js and update the specified routes with:
// app/Http/routes.js

Route.get('new_ticket', 'TicketsController.create').middleware('auth')
Route.post('new_ticket', 'TicketsController.store').middleware('auth')
Route.get('tickets/:ticket_id', 'TicketsController.show').middleware('auth')
Route.get('my_tickets', 'TicketsController.userTickets').middleware('auth')

Route.group('admin', function () {
    Route.get('tickets', 'TicketsController.index');
    Route.post('close_ticket/:ticket_id', 'TicketsController.close');
}).prefix('admin').middleware(['auth', 'admin'])
We add the auth middleware to some of the routes we defined earlier. This will make these routes be accessible only to authenticated users.
Note: The admin routes have two middlewares: auth and admin (we created earlier) which means not only an authenticated user can access these routes but must also be an admin.
Handle Exception Thrown For Unauthenticated Users
Having set up our middlewares, when an unauthenticated user tries to access authenticated users only area, they will get the error below:
Oops! That's Awkward. Yeah, I know.
Let's give our users a nice user experience by kindly redirecting them to the login page if they are not authenticated. To do that, open app/Listeners/Http.js and add the snippet below to it just before the DEVELOPMENT REPORTER section:
// app/Listeners/Http.js

/**
* Handle exception thrown when unable to authenticate the request
*/
if (error.name === 'InvalidLoginException') {
    yield response.sendView('auth.login')
    return
}
The app/Listeners/Http.js will handle errors that occurred during an HTTP request so it a nice place to catch any exception errors not caught within a controller. Back to our error, noticed from the image above that a 401 InvalidLoginException exception error was thrown. All we have to do it listen for when the error name is InvalidLoginException and then handle it gracefully by redirecting the user to the login page.
Finishing Touches
To finish up with our application, let's add a kind of landing page that users will see upon visiting our application. Open app/Http/routes.js and add the line below to it:
// app/Http/routes.js

Route.on('/').render('home')
AdonisJs has a render() for displaying static pages. The route above will render a home view which we'll create shortly.
Create a new file named home.njk within the resources/views and add the code below to it:
// resources/views/home.njk

{% extends 'layouts.master' %}

{% block title %} Welcome {% endblock %}

{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-md-10 col-md-offset-1">
                <div class="panel panel-default">
                    <div class="panel-body">
                        <p>Welcome!</p>

                        {% if currentUser.is_admin %}
                            <p>
                                See all <a href="/admin/tickets">tickets</a>
                            </p>
                        {% else %}
                            <p>
                                See all your <a href="/my_tickets">tickets</a> or <a href="/new_ticket">open new ticket</a>
                            </p>
                        {% endif %}
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}
Now when we visit the homepage of our application, we will get a screen similar as below:
Below is an image of the final output of what we have built.
Conclusion
That's it. Our support ticket application is ready. In this tutorial, we have been able to see how easy and straightforward it is to build applications with AdonisJs. We have been able to utilize AdonisJs Query Builder to define our table structures without writing a single SQL command, layout our views with its built-in view engine without doing extra setup as with other Node.JS frameworks (Express, Hapi.js etc). We also saw Lucid ORM in action which makes it seamless to interact with our database irrelevant of the database type. Lastly, we saw Ace, a powerful interactive shell which we used to scaffold and perform some operations from within a terminal. Though we didn't touch all of the great features that AdonisJs provides out of the box. In subsequent tutorials, we'll be looking more at AdonisJs and how to do great stuff with it.
You can check out the project repository to view the complete source code. If you have any questions about the tutorial, let me know in the comments below.