Create Column Definitions & Insert New Contacts | Full-Stack Google Contacts Clone with AdonisJS (Node.js) and Quasar Framework (Vue.js)
In this lesson, we are going to create column definitions in our migration and model files for our Contact
model. Thereafter, we will create a new route and add logic into the controller for create new contacts. We will use Postman for sending the request. In the last lesson, we have created three files for the Contact
model:
database\migrations\xxxxxxxxxxx_contacts.ts
app\Controllers\Http\ContactsController.ts
app\Models\Contact.ts
We will be working on these three files. In additional to the api/start/routes.ts
file.
Let's start by creating a new branch for our repo:
# Make sure you are within your project
git checkout -b 12-create-column-definitions-for-contact-model
Adding Column Definitions in the app\Models\Contact.ts
File
Open app\Models\Contact.ts
. Refer to this snapshot of the file. I will encourage you to type out the content of the snapshot into the app\Models\Contact.ts
file. Alternatively, copy and paste the content of the snapshot file into the app\Models\Contact.ts
file. Let's discuss what's going on in the file.
- At Line 3, we import the
cuid
function from the@ioc:Adonis/Core/Helpers
package. The function will be used for creating unique, collision-resistant IDs for our contacts. - At Line 6, we add a static property
selfAssignPrimaryKey
and assign a value oftrue
to it. TheselfAssignPrimaryKey
informs the model that we want to generate our own primary key per contact instead of the default auto-increment ID from the database engine. The rest of the file contains instance properties representing the columns for our contact. You will notice that each property as a notation like
@column()
above them. This is called a decorator in JavaScript. A decorator is a special function used to modify the behaviour of properties/classes/functions it is attached to. In this case, we are informing the model that the properties decorated with@column()
are columns for our model. AdonisJS will inject special attributes to the properties via the decorator.At Line 8, the
@column()
decorator takes an object{ isPrimary: true }
as argument. The object specifies that theid
column will be considered the primary key for the model.Towards the end of the file, at Lines 63 and 66, we attach the
@column.dateTime()
decorator which transforms the property into adateTime
column. The@column.dateTime()
decorator can take an object{ autoCreate: true, autoUpdate: true }
which specifies if the values of the columns should be auto-created when inserted for the first time and auto-updated whenever the record is updated. The@column.dateTime()
decorator, very importantly, ensures that values assigned to those columns are transformed into JavaScriptdate
types before persistence in the database. Also, when they are retrieved, they will be transformed intoLuxon
'sDateTime
instances. Read more about Luxon.At Line 69, we declare a static method
generateUUID
which generates a random id and assigns the value to theid
property before the contact is created. The@beforeCreate
decorator is used to run thegenerateUUID
static method only when the contact is being created.
Read more about decorators in AdonisJS.
Save the file.
Adding Column Definitions in the Contact Migration File
The column definitions in the migration files are the ones actually used to create columns within tables in our database.
Open the contact migraton file database\migrations\xxxxxxxxxxxxx_contacts.ts
. Refer to this snapshot of the file. I will encourage you to type out the content of the snapshot into your database\migrations\xxxxxxxxxxxxx_contacts.ts
file. Alternatively, copy and paste the content of the snapshot file into the database\migrations\xxxxxxxxxxxxx_contacts.ts
file. Let's discuss more about the migration file.
- At Line 4, we declare the property
tableName
and assign the valuecontacts
. This will be the name of the table for thecontact
model. - Within the handler function of the
this.schema.createTable
function, we add more column definitions to the file. You will notice that we've change the column type of theid
column fromincrement
tostring
. This is because we want to manually generate theid
instead of using auto-increment values. We assign extra modifiers to theid
column such asprimary()
,index()
,unique()
, andnotNullable()
. - You will notice that the column name are in snake case (for example:
job_title
andphone_number1
). However, the corresponding property for the column in the model file is in camel case (e.g.jobTitle
andphoneNumber1
). This is because, the recommended case for naming columns in SQL is the snake case. AdonisJS has a naming strategy which will convert the camel case of model properties into snake case of our table columns. You will also notice that there is no need to separate numbers at the end of column names with underscores (_
). That is,phone_number1
instead ofphone_number_1
. - The
index()
modifier is used to index the values of the columns it is attached to. Indexing speeds up finding of rows and running of searches on the columns they are assigned to. Basically, all columns with theindex()
modifiers will allow searching in the future. Read more about indexes here. - The
unique()
modifier forces the column to hold only unique values. Values cannot repeat within the column. This is obvious reason it is only attached to theid
column. - Columns not assigned the
notNullable()
modifier arenullable
by default. - You will notice that the
notes
column has the typetext
. This allows for very long texts to be entered for the note column. - The
timestamp
type is used to define thecreated_at
andupdated_at
columns. This creates accurate timestamps of when each row in the table was created or modified. Thetimestamp
method takes a second argument which is used to specify if timezone offset should be added to the timestamp. For example: instead of the timestamp to be store as"2021-10-01T22:08:30.000"
, it will be store as"2021-10-01T22:08:30.000+01:00"
The timezone offset make it easy for the timestamp to be converted to the timezone of the user when it is rendered on the frontend. It is important to usetimestamp
data type to create timestamps instead of thedatetime
type.timestamp
columns have more functionalities thandatetime
column especially automatic timezone conversion. Read more that datetime and timezone data types here.
Read more about data types in MySQL.
Save the file.
Let's migrate our database so that contacts
table is created with the defined columns.
Make sure that you cross-check the syntaxes and name of the columns before migration to avoid errors.
# Ensure that you are in the `api` directory
node ace migration:run
This will migrate the database and create the contacts
table with the columns which were specified. Inspect this new contacts
table within MySQL Workspace. If you encounter errors during migration. Check the console for the emitted error. Also check/refresh the adonis_schema
table to see if that file has been added to the table. If it wasn't added to the adonis_schema
table, make the corrections, save the file, and attempt the migration again. If the file was added to the adonis_schema
table, run:
node ace migration:rollback
The above command rolls back the migration by exactly one step. It runs the down
method in the migration file which instructs the migration file to drop the contacts
table completely. After the rollback, run node ace migration:run
again to migrate the table.
Adding the /contacts
route
Open the api/start/routes.ts
and add these line to the end of the file:
Route.post('/contacts', 'ContactsController.store')
Refer to this snapshot. Save the file.
Here, we add a route with a POST
method. We define /contacts
as the path for the route and assign the store
method in the ContactsController
file/class as the handler for the route. This means that ContactsController.store()
method will be called when the /contacts
route is requested.
Why did we use /contacts
and not /contacts/new
as the route path? This follows the convention for creating API endpoints. Read more about it here. Take time to study it. It is very important for building API servers.
Let's add this route to Postman. Open Postman. Create a new collection named: CRUD
. Within the collection, add a new request. For the new request:
- Enter
Create Contact
as the request name, - Enter
{{baseURL}}/contacts
as the request path, - Change the request method from
GET
toPOST
, Below the request path, switch to the
Body
tab. TheBody
section is used to define the payload which will be sent in the body of the request. Clickraw
and paste the following JSON data:{ "firstName": "Hammad", "surname": "Pulham", "email1": "hpulham0@si.edu", "phoneNumber1": "+420 (767) 548-7576", "phoneNumber2": "+86 (442) 396-1670", "birthday": "1970-03-13", "website": "http://boston.com" }
Make sure that the type of raw body is
JSON
. See the end of the line.The JSON data contains the details of the contact we want to create.
Click the
Save
button and then click theSend
button. The request goes through but does nothing because there is nothing defined within thestore
method of theContactsController
class. We'll return to this after working on the controller file.
Working on the store
method within the ContactsController
file.
Open app\Controllers\Http\ContactsController.ts
. Refer to this snapshot.
First, let's remove some methods we will never use. Remove create
and edit
methods. These methods are usually used for server-side rendered views. For our API mode, we will use store
instead of create
method, and update
instead of edit
method.
I encourage you to type out the content of the snapshot file into your app\Controllers\Http\ContactsController.ts
file.
Let's discuss more about the store
method within the controller file.
- At Line 2, we have to import the Contact class from
App/Models/Contact
. - The
store
method has access to the HTTP context of the request lifecycle. Read more about theHTTP context
here. Thecontext
is destructured to get therequest
andresponse
objects. We do not need other properties in thecontext
object for now. - At Line 8, we call the
request.body()
method to access the payload we uploaded with the request in Postman. - You will notice some comments within the
store
method from Line 10. Since the keys of the JSON data we sent within the request body matches the properties in theContact
model, we could do this:await Contact.create(payload)
to quickly persist the new contact to the database. But it is not recommended, and dangerous. This is because malicious actors (aka hackers) could attempt to directly assign values of columns such asid
,created_by
, andupdated_by
from the outside. However, these values should be generated by the server. They might not succeed as the API server will override those sensitive value but there is no need to deliberately create that vulnerability. That is the reason we need to destructure the
payload
constant and extract the properties we really want to insert into the database. Then at Line 42, we assign those properties to an object which is the only argument to theContact.create()
method which then creates a new row ofContact
in ourcontacts
table. We have toawait
thecreate()
method because we are handing off (async) operation to the database andwaiting
for the database to complete the task and return a result to us. That result is the values which we inserted into the database. We store the result in thecontact
constant on Line 42.Note that we are using ES6 shorthand for assigning object properties where the key and value of the properties of the object have the same names. That is:
// This:
{
firstName,
surname,
company,
jobTitle,
...
}
// Is the same as:
{
firstName: firstName,
surname: surname,
company: company,
jobTitle: jobTitle
...
}
At Line 67, we refresh the contact
which was the result from the create
operation. Why?
For this JSON data to be sent as the request body:
{
"firstName": "Hammad",
"surname": "Pulham",
"email1": "hpulham0@si.edu",
"phoneNumber1": "+420 (767) 548-7576",
"phoneNumber2": "+86 (442) 396-1670",
"birthday": "1970-03-13",
"website": "http://boston.com"
}
Without the line await contact.refresh()
, if we send a request to the POST /contacts
route with the JSON data above as the body, we will get only the same values we inserted as the result which looks like this:
{
"first_name": "Hammad",
"surname": "Pulham",
"email1": "hpulham0@si.edu",
"phone_number1": "+420 (767) 548-7576",
"phone_number2": "+86 (442) 396-1670",
"birthday": "1970-03-13",
"website": "http://boston.com",
"id": "cku9d6arh0000o4vobq3bh0uq",
"created_at": "2021-10-02T06:37:56.499+01:00",
"updated_at": "2021-10-02T06:37:56.499+01:00"
}
Notice that we have id
, created_at
, and updated_at
in the JSON result. Also notice that the id
was auto-generated. That's what we achieved in the generateUUID
static method in our Contact
model file. Other properties of the Contact
model which were not inserted are not returned. So, if we need the complete Contact
model to be return to the client, we need to refresh
the Contact
model (stored in the contact
constant) by calling await contact.refresh()
before returning the result to the client via return response.created(contact)
. Refreshing the Contact
model gives us the complete JSON data like this:
{
"id": "cku9ddpl30000q4voex97a04d",
"first_name": "Hammad",
"surname": "Pulham",
"company": null,
"job_title": null,
"email1": "hpulham0@si.edu",
"email2": null,
"phone_number1": "+420 (767) 548-7576",
"phone_number2": "+86 (442) 396-1670",
"country": null,
"street_address_line1": null,
"street_address_line2": null,
"city": null,
"post_code": null,
"state": null,
"birthday": "1970-03-13",
"website": "http://boston.com",
"notes": null,
"created_at": "2021-10-02T06:43:42.000+01:00",
"updated_at": "2021-10-02T06:43:42.000+01:00"
}
Save all files and send the POST /contacts
request via Postman. A new record will be created in the contacts
table of the database. Modify the values of the properties of the JSON payload in the body of the request. You can add more properties to the JSON body as long as they are defined in the Contact
model. Send more request to create more records inside the table. Make reference to the mock data.
Congratulations!!! You have successfully created new database records with AdonisJS. Do you see how easy it was?
In the next lesson, we will learn about request validations used for verifying and sanitising data sent from the client to the backend before storage.
Save all your files, commit and merge with the master branch.
git add .
git commit -m "feat(api): create column definitions and insert new contacts"
git push --set-upstream origin 12-create-column-definitions-for-contact-model
git checkout master
git merge master 12-create-column-definitions-for-contact-model
git push