Sails Tutorial — Chapter 6
This is the sixth article in the series. You can read the fifth article at this link.
In this article, we are going to add supports for the User entity. The code we are going to write for the User entity will be almost similar to the Contact entity. This gives us an opportunity to revise and practice what we learned in previous articles.
Let’s start with routes. Add these five routes for the User, same as we have for the Contact in config/routes.js
file.
module.exports.routes = {
'GET /': 'HomeController.index',
// Contacts
'GET /contacts': 'ContactsController.index',
'GET /contacts/:id': 'ContactsController.show',
'POST /contacts': 'ContactsController.create',
'PUT /contacts/:id': 'ContactsController.update',
'DELETE /contacts/:id': 'ContactsController.destroy',
// Users
'GET /users': 'UsersController.index',
'GET /users/:id': 'UsersController.show',
'POST /users': 'UsersController.create',
'PUT /users/:id': 'UsersController.update',
'DELETE /users/:id': 'UsersController.destroy',
};
We need to create this UsersController.js
file in the api/controllers
folder.
touch api/controllers/UsersController.js
Let’s add the following code within this UsersController.js
file.
module.exports = {
index: async (req, res) => {},
show: async (req, res) => {},
create: async (req, res) => {},
update: async (req, res) => {},
destroy: async (req, res) => {},
};
Before we go further and fill these actions with business logic, let’s create a User model in User.js
file in api/models
folder. For this, first we need to create a file with name User.js
at the same api/models
location.
touch api/models/User.js
Let’s write the following code in this User.js
file.
module.exports = {
tableName: 'users',
};
We are going to define two attributes, email
and password
. Both should string
and required
whereas email
should be unique
as follows.
module.exports = {
tableName: 'users',
attributes: {
email: {
type: 'string',
required: true,
unique: true,
},
password: {
type: 'string',
required: true,
},
},
};
Now, going back to the UserController.js
file, let’s write the code to fetch all the users in index
action as follows.
module.exports = {
index: async (req, res) => {
const users = await User.find();
res.json({ data: users });
},
show: async (req, res) => {},
create: async (req, res) => {},
update: async (req, res) => {},
destroy: async (req, res) => {},
};
We are fetching all the users from User
model and returning it as JSON response. Opening http://localhost:3000/users in Insomnia returns an empty array []
as we do not have any user in users
table.
Let’s write the code for show
action as follows.
module.exports = {
index: async (req, res) => {
const users = await User.find();
res.json({ data: users });
},
show: async (req, res) => {
const user = await User.findOne({ id: req.params.id });
res.json({ data: user });
},
create: async (req, res) => {},
update: async (req, res) => {},
destroy: async (req, res) => {},
};
Again nothing unusual. We are finding a user
based on passed id
and returning the details of it. Opening http://localhost:3000 return an empty object {}
as we do not have any user in database yet!
Let’s write the logic to create a user in the database. So, that we can finally have the user in system!
module.exports = {
index: async (req, res) => {
const users = await User.find();
res.json({ data: users });
},
show: async (req, res) => {
const user = await User.findOne({ id: req.params.id });
res.json({ data: user });
},
create: async (req, res) => {
const user = await User.create({
email: req.body.email,
password: req.body.password,
}).fetch();
res.status(201);
res.json({ data: user });
},
update: async (req, res) => {},
destroy: async (req, res) => {},
};
We are getting email
and password
from the request body and saving them into the database. Also, we are chaining the .fetch()
method to get the just created user from the database and returning it as a response.
Let’s test this and we should get the details of created user. For example, passing the following request body to POST http://localhost:3000/users should return the created user with unique id
.
{
"email": "bob@doe.com",
"password": "123456"
}
Response after creation of the user.
{
"data": {
"id": 1,
"email": "bob@doe.com",
"password": "123456"
}
}
Next, let’s add code for the update
action.
module.exports = {
index: async (req, res) => {
const users = await User.find();
res.json({ data: users });
},
show: async (req, res) => {
const user = await User.findOne({ id: req.params.id });
res.json({ data: user });
},
create: async (req, res) => {
const user = await User.create({
email: req.body.email,
password: req.body.password,
}).fetch();
res.status(201);
res.json({ data: user });
},
update: async (req, res) => {
const user = await User.updateOne({
id: req.params.id,
}).set({
email: req.body.email,
password: req.body.password,
});
res.json({ data: user });
},
destroy: async (req, res) => {},
};
To test this update
action, let’s change the password
for the created user to PUT http://localhost:3000/users/1 with the following request body.
{
"email": "bob@doe.com",
"password": "654321"
}
Response after the update of the user.
{
"data": {
"id": 1,
"email": "bob@doe.com",
"password": "654321"
}
}
Finally, let’s add a code for the destroy
action.
module.exports = {
index: async (req, res) => {
const users = await User.find();
res.json({ data: users });
},
show: async (req, res) => {
const user = await User.findOne({ id: req.params.id });
res.json({ data: user });
},
create: async (req, res) => {
const user = await User.create({
email: req.body.email,
password: req.body.password,
}).fetch();
res.status(201);
res.json({ data: user });
},
update: async (req, res) => {
const user = await User.updateOne({
id: req.params.id,
}).set({
email: req.body.email,
password: req.body.password,
});
res.json({ data: user });
},
destroy: async (req, res) => {
const user = await User.destroyOne({
id: req.params.id,
});
res.json({ data: user });
},
};
We can test this by calling DELETE http://localhost:3000/users/1 in Insomnia, and it will delete the user with the id
1
.
With these changes, we now have the CRUD operations around the User entity.
There are a couple of changes we should do in this User entity. For example, password should not be saved as plain text. Before we save the password in database, it should be encrypted. Now, we can write the code the same, or we can use a npm package called sails-hook-organics
. This package has many useful helper functions, and two are specifically related to the password encryption.
Let’s first install this package
npm i sails-hook-organics
In User.js
model, write the following code.
module.exports = {
tableName: 'users',
attributes: {
email: {
type: 'string',
required: true,
unique: true,
},
password: {
type: 'string',
required: true,
},
},
beforeCreate: async (user, next) => {},
};
We have added a function called beforeCreate
with user
and next
as the parameters of it. beforeCreate
is one of the lift-cycle method in Waterline ORM that we can utilize to encrypt the password before saving the user details into the database. To encrypt the password, we are going to use hashPassword
helper function from the installed package. All the helpers within this package are available under sails.helpers
namespace, and we are trying to use the hashPassword
method from the passwords
. So, it should be sails.helpers.password.hashPassword
and usage of it as follows.
module.exports = {
tableName: 'users',
attributes: {
email: {
type: 'string',
required: true,
unique: true,
},
password: {
type: 'string',
required: true,
},
},
beforeCreate: async (user, next) => {
try {
const hashPassword = await sails.helpers.passwords.hashPassword(user.password);
user.password = hashPassword;
return next();
} catch (error) {
next(error);
}
},
};
We wrote the code within try…catch
block as it might throw an error as it return promise or error. The hash of the password is saved within hashPassword
variable, and we are assigning this value back to the user.password
as we have the access to the current user when creating it in the database. Finally, we are calling next()
callback function either with error
or without error in case of success or failure.
This code will do the trick, as it’ll save the encrypted password in the database. Creating a new user to POST http://localhost:3000/users and should return the following result.
{
"data": {
"id": 2,
"email": "alice@doe.com",
"password": "$2a$10$v9cc/cDFULdV9Fc2mCdV1uguwWrCDgqXZDaJ6TXjvwgBpY6ObDPqC"
}
}
See! The password is encrypted now and we are getting the encrypted string. But, I don’t think we should return password
field at all in the response. Let’s remove the password
field from the response whenever we try to fetch the user details. In order to do that, we need to use customToJSON()
function of Waterline ORM. customToJSON()
function has access to this
, and we can return an object from this function. Whatever object we return should be always return when fetching the user details.
module.exports = {
tableName: 'users',
attributes: {
email: {
type: 'string',
required: true,
unique: true,
},
password: {
type: 'string',
required: true,
},
},
customToJSON: function () {
return {
id: this.id,
email: this.email,
};
},
beforeCreate: async (user, next) => {
try {
const hashPassword = await sails.helpers.passwords.hashPassword(user.password);
user.password = hashPassword;
return next();
} catch (error) {
next(error);
}
},
};
Pay attention here! I have defined customToJSON()
function with function
keyword and not with array syntax. If we try to use the array syntax, this
scope wouldn’t correctly match and return an empty object. Also, here I am explicitly returning the exact fields which I want to return instead of deleting fields from the object.
Now, let’s call http://localhost:3000/users, and it should return all the users details but without password
. The same is true for fetching single user details or user details after user is updated or deleted.
Now, the code looks better than previous one!
Take a break and read the seventh article in this series at this link.