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
Top comments (7)
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 })
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
});
}
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?
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:
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:@arapl3y said that he was only able to fix the 500 status by changing to:
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:
Do that:
And in the place where you create the event, instad of passing the
userID
variable, simply do that:But anyway, great tutorial, @nilomiranda ! And I hope you don''t get angry with my feedback.
Cya!
do you have any tutorial about socket programing? thanks
No
Hey, what is the format I need to pass to table.datetime('date')?
It always should be yyyy-mm-dd?
Very good, but how can we consume this api with react front end using material-ui?