Sails Tutorial — Chapter 6

Kiran Chauhan
7 min readJul 22, 2024

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.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Kiran Chauhan
Kiran Chauhan

Written by Kiran Chauhan

I design software with and for people.

No responses yet

Write a response