Sails Tutorial — Chapter 7
This is the seventh article in the series. You can read the sixth article at this link.
In this article, we are going to add the relationship between the Contact
and User
model, as users can create one or many contacts within the CRM system that we are currently building. In ORM terms, we normally call it a one-to-many relationship, where one user can have many contacts.
In order to add this relation, we need to modify both User.js
and Contact.js
models. As per the documentation, “one” side must contain the collection
attribute, and the “many” side must contain a model
attribute.
Open Contact.js
file and write the following code.
module.exports = {
tableName: "contacts",
attributes: {
firstName: {
type: "string",
required: true,
},
lastName: {
type: "string",
required: true,
},
createdBy: {
model: "user",
},
},
};
We added a new column or an attribute with name createdBy
which makes sense as we save the user id
for the created contact. We also fulfill the requirement as stated above by having model
attributes on the “many” side. The value of the model is in lower-case and in singular form.
Open User.js
file and write the following code.
module.exports = {
tableName: "users",
attributes: {
email: {
type: "string",
required: true,
unique: true,
},
password: {
type: "string",
required: true,
},
contacts: {
collection: "contact",
via: "createdBy",
},
},
customToJSON: function () {
// ...
},
beforeCreate: async (user, next) => {
// ...
},
};
We added contacts
as a virtual attribute. Sails will not create this as a column in the database, instead it is a virtual attribute live within the model/memory only. The name of this attribute is the plural of the “many” side, which is readable as — user has many “contacts”. We also fulfill the requirement as stated above by having a collection
attribute on “one” side. Also, we added via
property to mention that the relationship between User
and Contact
is via createdBy
column of contacts
table. And with these changes, the relationship is established.
We now need to update the ContactsController
to accept createdBy
value when creating a new contact as follows in create
action.
module.exports = {
index: async (req, res) => {
// ...
},
show: async (req, res) => {
// ...
},
create: async (req, res) => {
const contact = await Contact.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
createdBy: req.body.createdBy,
}).fetch();
res.status(201);
res.json({ data: contact });
},
update: async (req, res) => {
// ...
},
destroy: async (req, res) => {
// ...
},
};
We are now accepting the createdBy
with firstName
and lastName
. Open the Insomnia and call POST http://localhost:3000/contacts with the following request body. Make sure to have the valid user id
for the createdBy
.
{
"firstName": "Jane",
"lastName": "Doe",
"createdBy": 1
}
We should get the response as follows after successful creation of the contact.
{
"data": {
"id": 2,
"firstName": "Jane",
"lastName": "Doe",
"createdBy": 1,
}
}
Now, fetch all the contacts by calling http://localhost:3000/contacts. You should get either null
for other existing contacts and 1
for recent created contact. Instead of displaying the user id
, we should display the details of the user. We can fetch the details created by the user by chaining the .populate()
method on .find()
and passing the attribute name that we are interested to populate, which is createdBy
in our example.
Open the ContactsController.js
file and write the following code in the index
action.
module.exports = {
index: async (req, res) => {
const contacts = await Contact.find().populate("createdBy");
res.json({ data: contacts });
},
show: async (req, res) => {
// ...
},
create: async (req, res) => {
// ...
},
update: async (req, res) => {
// ...
},
destroy: async (req, res) => {
// ...
},
};
Now, again call the http://localhost:3000/contacts in Insomnia and you should get the following output.
{
"data": [
{
"id": 1,
"firstName": "Kai",
"lastName": "Doe",
"createdBy": null,
},
{
"id": 2,
"firstName": "Jane",
"lastName": "Doe",
"createdBy": {
"id": 1,
"email": "jane@doe.com"
},
}
]
}
The first contact was created before the relationship between Contact
and User
models established. Hence, it returns null
for the createdBy
column. But, a second contact was recently created with createdBy
value and with .populate()
method we are now getting the id
and email
of the createdBy
user.
The relationship is on both sides. If we are on the contacts
side, we can use createdBy
. But, if we are on the users side, we can use contacts
. But, before this works, we need to do 2 modifications. First, we need to add the .populate()
method in the index
action for the contacts
as follows.
module.exports = {
index: async (req, res) => {
const users = await User.find().populate("contacts");
res.json({ data: users });
},
show: async (req, res) => {
// ...
},
create: async (req, res) => {
// ...
},
update: async (req, res) => {
// ...
},
destroy: async (req, res) => {
// ...
},
};
Next, we need to update the return object of customToJSON()
function as we are only returning the id
and email
of the user. Let’s update the return object to also return contacts
as well.
module.exports = {
tableName: "users",
attributes: {
// ....
},
customToJSON: function () {
return {
id: this.id,
email: this.email,
contacts: this.contacts,
};
},
beforeCreate: async (user, next) => {
// ....
},
};
Now, let’s call the http://localhost:3000/users and we should get the output as follows.
{
"data": [
{
"id": 1,
"email": "jane@doe.com",
"contacts": [
{
"id": 2,
"firstName": "Jane",
"lastName": "Doe",
"createdBy": 1,
}
]
},
{
"id": 2,
"email": "merry@doe.com",
"contacts": []
},
]
}
I have two users and the first user has created one contact so we are also getting the details of the created contact whereas for the second user it is an empty array. And with these changes, we now have the relationship on both sides with data fetching capabilities!
Before we go further and update show
action, let’s add one more column in Contact.js
model, and that is updatedBy
. Again this column saves the user id
but the one who updates the contact.
Open Contact.js
file and write the following code.
module.exports = {
tableName: "contacts",
attributes: {
firstName: {
type: "string",
required: true,
},
lastName: {
type: "string",
required: true,
},
createdBy: {
model: "user",
},
updatedBy: {
model: "user",
},
},
};
Now, we have the updatedBy
column which is a reference to the User
model. Back to the User
model, open User.js
file and write the following code.
module.exports = {
tableName: "users",
attributes: {
email: {
type: "string",
required: true,
unique: true,
},
password: {
type: "string",
required: true,
},
contacts: {
collection: "contact",
via: "createdBy",
},
contacts: {
collection: "contact",
via: "updatedBy",
},
},
customToJSON: function () {
// ...
},
beforeCreate: async (user, next) => {
// ...
},
};
I copied the same code that I wrote for the createdBy
column relation but changed it to updatedBy
as we should populate the value of updatedBy
column. We need to update the update
action to accept the updatedBy
field in the request body.
Open ContactsController.js
file and write the following code in update
action.
module.exports = {
index: async (req, res) => {
// ...
},
show: async (req, res) => {
// ...
},
create: async (req, res) => {
// ...
},
update: async (req, res) => {
const contact = await Contact.updateOne({
id: req.params.id,
}).set({
firstName: req.body.firstName,
lastName: req.body.lastName,
updatedBy: req.body.updatedBy,
});
res.json({ data: contact });
},
destroy: async (req, res) => {
// ...
},
};
The change is quite simple as we are now accepting the updatedBy
value in the request body. Let’s call PUT http://localhost:3000/2 with the following request body.
{
"firstName": "Jane",
"lastName": "Doe",
"updatedBy": 2
}
Here, I’m passing a different user id
for the updatedBy
than the one who has created this contact. We should get the following response after a successful update.
{
"data": {
"id": 2,
"firstName": "Jane",
"lastName": "Doe",
"createdBy": 1,
"updatedBy": 2
}
}
And the value for the updatedBy
user is saved in the database. In order to fetch the details created by and updated by user from the database, we again need to chain the .populate()
method but this time we need to pass updatedBy
attribute in the index
action of ContactsController.js
file.
module.exports = {
index: async (req, res) => {
const contacts = await Contact.find().populate("createdBy").populate("updatedBy");
res.json({ data: contacts });
},
show: async (req, res) => {
// ...
},
create: async (req, res) => {
// ...
},
update: async (req, res) => {
// ...
},
destroy: async (req, res) => {
// ...
},
};
Open the Insomnia and call http://localhost:3000/contacts. We should get the following response.
{
"data": [
{
"id": 1,
"firstName": "Kai",
"lastName": "Doe",
"createdBy": null,
},
{
"id": 2,
"firstName": "Jane",
"lastName": "Doe",
"createdBy": {
"id": 1,
"email": "jane@doe.com"
},
"updatedBy": {
"id": 2,
"email": "merry@doe.com"
}
}
]
}
We are now getting the correct user details of createdBy
and updatedBy
fields.
Let’s also update the show
action to populate both createdBy
and updatedBy
attributes so that when we fetch the single contact, it also returns the details of the users instead of ids.
module.exports = {
index: async (req, res) => {
// ...
},
show: async (req, res) => {
const contact = await Contact.findOne({ id: req.params.id }).populate("createdBy").populate("updatedBy");
res.json({ data: contact });
},
create: async (req, res) => {
// ...
},
update: async (req, res) => {
// ...
},
destroy: async (req, res) => {
// ...
},
};
Finally, let’s also update the show
action of UsersController.js
file to fetch the contact details when fetching the given user.
module.exports = {
index: async (req, res) => {
// ...
},
show: async (req, res) => {
const user = await User.findOne({ id: req.params.id }).populate("contacts");
res.json({ data: user });
},
create: async (req, res) => {
c// ...
},
update: async (req, res) => {
// ...
},
destroy: async (req, res) => {
// ...
},
};
Passing the createdBy
and updatedBy
in the request body is intermediate code as in future when we add the support for the authentication, we will fetch the user id
from the session token.