loading...

Building an API with AdonisJS (part 3)

nilomiranda profile image Danilo Miranda ・8 min read

Hello folks! The third part of the series is finally here! πŸ‘πŸ‘πŸ‘πŸ‘

If you are a newcomer, this is a series that will cover all the steps we need to build an API using AdonisJS. This is the third part of the series, and here are the links for the previous posts:

In this part, and I promise this will be shorter, we will cover how to implement the feature for a user to create a new event, setting a specific date, location and time.

So we'll learn how to create a new model, as the previous one was already created at the moment we scaffolded our application, how to create a new migration to properly set the columns we'll need in our table and how to establish a relationship between to models.

So let's get our hands dirty...

Creating the events table

This API will allow the user to schedule events, setting a location, the time, date and the event's title (name)

So we will need to set 4 columns:

  • Title (string)
  • Location (string)
  • Date (date)
  • Time (timestamp)

As this table will have a relationship with the user, as one may have any number of events we'll also need a column of the user's ID. This column will make a reference to the main column, User.

In Adonis, to create a new model we will do the following:

adonis make:model Event -c -m

What I'm doing here is telling adonis to make a new model, called Event and I'm passing two flags: -c and -m. These two flags will tell adonis to also create the controller (-c) and the migration (-m).

Now let's begin to structure our table. Head to the migration file database/migrations/1551814240312_event_schema.js

Inside your class EventSchema, for the up() method, do the following:

class EventSchema extends Schema {
  up () {
    this.create('events', (table) => {
      table.increments()
      table
        .integer('user_id')
        .unsigned()
        .references('id')
        .inTable('users')
        .onUpdate('CASCADE')
        .onDelete('SET NULL')
      table.string('title').notNullable()
      table.string('location').notNullable()
      table.datetime('date').notNullable()
      table.time('time').notNullable()
      table.timestamps()
    })
  }

Let's see what we are doing:

table
    .integer('user_id')
    .unsigned()
    .references('id')
    .inTable('users')
    .onUpdate('CASCADE')
    .onDelete('SET NULL')

This piece of code above is the one responsible to create the user's ID column and reference to the User table.

First, we set the data type to integer with table.integer('user_id') setting the column name as user_id inside the parameter.

With .unsigned() we set the column to only accept positive values (-1, -2, -3 are invalid numbers).

We then tell the column to reference to the user's id column with .references('id) in the User table with `.inTable('users').

If we happen to change the event's owner ID, then all the changes will reflect to the Event table, so the user's ID in the column user_id will also change (.onUpdate('CASCADE')).

In case the user's account ends up deleted, the event's user_id column of the events the deleted user owned will all be set to null .onDelete('SET NULL').

javascript
table.string('title').notNullable()
table.string('location').notNullable()
table.datetime('date').notNullable()
table.time('time').notNullable()

Now we set the other columns:

  • The title column, as a STRING with table.string('title')
  • The location column, also as a STRING with table.string('location')
  • The date column, as a DATETIME with table.datetime('date')
  • And the time column, as a TIME with `table.time('time')

Notice that for all these columns in each one of them I also set .notNullable() because the user will have to set each of these values every time he creates a new event.

After all this work we can run our migration:

adonis migration:run

To finish setting up the relationship between the Event and User tables we have two options:

  • We set the relationship in the User's model
  • We set the relationship in the Event's model

In this example, we will set the relationship in the User's model. We don't need to set the relationship in both models as Adonis' documentation itself states that:

There is no need to define a relationship on both the models. Setting it one-way on a single model is all that’s required.

So let's go to App/Models/User.js and add the method events().

events () {
    return this.hasMany('App/Models/Event')
}

That's all we need to do! Now we'll be able to start creating our controller to create and list new events.

Creating and saving a new event

First, let's create our store() method to enable a user to create and save a new event.

In App/Controllers/Http/EventController.js we'll do:

async store ({ request, response, auth }) {
    try {
      const { title, location, date, time } = request.all() // info for the event
      const userID = auth.user.id // retrieving user id current logged

      const newEvent = await Event.create({ user_id: userID, title, location, date, time })

      return newEvent
    } catch (err) {
      return response
        .status(err.status)
        .send({ message: {
          error: 'Something went wrong while creating new event'
        } })
    }
  }

It's really simple. We retrieve the data coming from the request with request.all()

We also need to retrieve the logged user's ID, but we get this data saved in the auth object, provided by the context.

const userID = auth.user.id

To use this controller we just create a new route, inside Route.group():

Route.post('events/new', 'EventController.store')

To test the event creation we send a request to this route, sending a JSON data following the structure below:

{
    "title": "First event",
    "location": "Sao Paulo",
    "date": "2019-03-16",
    "time": "14:39:00"
}

If everything runs smoothly the request will return you the created event:

{
  "user_id": 10,
  "title": "First event",
  "location": "Sao Paulo",
  "date": "2019-03-16",
  "time": "14:39:00",
  "created_at": "2019-03-16 14:40:43",
  "updated_at": "2019-03-16 14:40:43",
  "id": 6
}

Listing events

We will have two ways to list events in this API, list all events or by date.

Let's begin by listing all events. We'll create a method index():

async index ({ response, auth }) {
    try {
      const userID = auth.user.id // logged user ID

      const events = await Event.query()
        .where({
          user_id: userID
        }).fetch()

      return events
    } catch (err) {
      return response.status(err.status)
    }
  }

We'll list all events searching by the logged user's ID, so as we did before we use auth.user.id to get this information.

The way we query the data here will be a bit different than we previously did as we won't use any static method in this case.

const events = await Event.query()
        .where({
          user_id: userID
        }).fetch()

We open the query with .query() and then we set the where statement, passing an object as a parameter to pass the filters to search for the data:

.where({
    user_id: userID
})

Unlike the special static methods, we need to chain the method .fetch() to correctly retrieve the data.

This is easier to test, we just need to set a route for a GET request in start/routes.js:

Route.get('events/list', 'EventController.index')

This request won't need any parameter. If successfully completed you'll have as return the list of all events inside an array:

[
  {
    "id": 6,
    "user_id": 10,
    "title": "First event",
    "location": "Sao Paulo",
    "date": "2019-03-16T03:00:00.000Z",
    "time": "14:39:00",
    "created_at": "2019-03-16 14:40:43",
    "updated_at": "2019-03-16 14:40:43"
  }
]

Now we'll list the events by date, and for that, we'll create a method called show().

async show ({ request, response, auth }) {
    try {
      const { date } = request.only(['date']) // desired date
      const userID = auth.user.id // logged user's ID

      const event = await Event.query()
        .where({
          user_id: userID,
          date
        }).fetch()

      if (event.rows.length === 0) {
        return response
          .status(404)
          .send({ message: {
            error: 'No event found'
          } })
      }

      return event
    } catch (err) {
      if (err.name === 'ModelNotFoundException') {
        return response
          .status(err.status)
          .send({ message: {
            error: 'No event found'
          } })
      }
      return response.status(err.status)
    }

What we are doing is, retrieving the data sent in the request and the logged user's ID. Then again we manually query for events using the user's ID and the date he provided within his request.

Now we need to check if we have events in the given date or not.

If there's no event the following piece of code runs and returns a message:

if (event.rows.length === 0) {
    return response
        .status(404)
        .send({ message: {
            error: 'No event found'
        } })
}

If there's the event we simply return it.

Don't forget to create a route to call this controller when accessed:

Route.get('events/list/date', 'EventController.show')

In this example, we created an event to happen on March 16th, 2019. If we sent the following JSON in the request:

{
    "date": "2019-03-16"
}

We receive as a return:

[
  {
    "id": 6,
    "user_id": 10,
    "title": "First event",
    "location": "Sao Paulo",
    "date": "2019-03-16T03:00:00.000Z",
    "time": "14:39:00",
    "created_at": "2019-03-16 14:40:43",
    "updated_at": "2019-03-16 14:40:43"
  }
]

If we, for example, look for an event in March 26th:

{
    "date": "2019-03-26"
}

We'll receive the following:

{
  "message": {
    "error": "No event found"
  }
}

Deleting events

The only feature missing is the ability to delete an event. It'll be quite simple. We'll get, as usual, the logged user's ID and the event ID. Then we look for the event in the database. To make sure that the user only deletes his owned events we'll check if the logged user's ID is the same as the event being deleted and then proceed to delete the event.

Let's add some code to our destroy() method:

async destroy ({ params, response, auth }) {
    try {
      const eventID = params.id // event's id to be deleted
      const userID = auth.user.id // logged user's ID

      // looking for the event
      const event = await Event.query()
        .where({
          id: eventID,
          user_id: userID
        }).fetch()

      /**
       * As the fetched data comes within a serializer
       * we need to convert it to JSON so we are able 
       * to work with the data retrieved
       * 
       * Also, the data will be inside an array, as we
       * may have multiple results, we need to retrieve
       * the first value of the array
       */
      const jsonEvent = event.toJSON()[0]

      // checking if event belongs to user
      if (jsonEvent['user_id'] !== userID) {
        return response
          .status(401)
          .send({ message: {
            error: 'You are not allowed to delete this event'
          } })
      }

      // deleting event
      await Event.query()
        .where({
          id: eventID,
          user_id: userID
        }).delete()

Just a side note: As we are working with the query builder we need to delete it 'manually', also using the query builder, but if you are, in another example, fetching data using the static methods provides by the models, you just need to using the static method .delete().

Let's test our destroy() method. In your start/routes.js file add the following delete request:

Route.delete('events/:id/delete', 'EventController.destroy')

As we are only sending all the data we need through the Url we won't need to send any data in the request's body.

This is it for this one guys!

Today we learned how to create a new model, together with a controller and a migration file and also how to set a relationship between different tables

Discussion

pic
Editor guide
Collapse
arapl3y profile image
Alex Rapley

In 4.0 it seems as though some of the Lucid Model methods have changed. I was getting an unresponsive server until I changed to the newer Lucid Model method syntax such as:
const newEvent = await user.events().create({ user_id: user.id, title, location, date, time })

Collapse
yonglee79 profile image
Yong Lee

Hi Danilo Miranda,
I was following your instructions but there seems a typo from EventController.store.
this code worked

async store({ request, response, auth }) {
try {
const { title, location, date, time } = request.all(); // info for the event
const user = await auth.getUser(); // retrieving user id currently logged in
console.log(user);
const newEvent = await Event.create({
user_id: user.id,
title,
location,
date,
time
});

  return newEvent;
} catch (err) {
  return response.status(err.status).send({
    message: { error: 'Something went wrong while creating a new event.' }
  });
}

}

I kept getting no response from a server when I used
const userID = auth.user.id

and isn't it better to pass jwt to the server?

and in route.js
instead of just Route.post('events/create', 'EventController.store'),
Route.post('events/create', 'EventController.store').middleware(['auth]);
should be used.

How do you know that the user is logged in when it just api?
Did you want to just pass the user id which is saved in frontend to the server to create an event after
logging in from the login route?

Collapse
azeveco profile image
Gabriel Azevedo

Great tutorial, but I think you should provide more tips for beginners. Ok, you want them to learn, but yould show a little more ou maybe something like "Go and try to do that. If you can't, no worries, down below there's a hint for you".

Anyway, for everyone who's having problems with the method store of the EventController, it's because @nilomiranda tells you to use the following:

const newEvent = await Event.create({...

But he forgot to mention that you need to import the model to the controller. So, below the 'use strict', just import it like this:

const Event = use('App/Models/Event')

@arapl3y said that he was only able to fix the 500 status by changing to:

const newEvent = await user.events().create({...

Yeah, it works, but are at the EventController, so it would be good if the only thing I use is the Event itself to keep things clean. Of course we need to get the authenticated user and pass his id to the creation of the event, but that should be it.

And I strongly recommend you all to use the implementation that @yonglee79 said in his comment. Instead of this:

const userID = auth.user.id

Do that:

const user = await auth.getUser()

And in the place where you create the event, instad of passing the userID variable, simply do that:

const newEvent = await Event.create({
  user_id: user.id,
  title,
  location,
  date,
  time
})

But anyway, great tutorial, @nilomiranda ! And I hope you don''t get angry with my feedback.

Cya!

Collapse
mahdipishguy profile image
Mahdi Pishguy

do you have any tutorial about socket programing? thanks

Collapse
nilomiranda profile image
Collapse
rodolphonetto profile image
Rodolpho Netto

Hey, what is the format I need to pass to table.datetime('date')?

It always should be yyyy-mm-dd?

Collapse
yvpaulo profile image
Yvson Paulo

Very good, but how can we consume this api with react front end using material-ui?