Sails Tutorial — Chapter 5

Kiran Chauhan
8 min readJun 26, 2024

This is the fifth article in the series. You can read the forth article at this link.

In this article, we are going to work with a database. For the purpose of learning we are going to use an in-memory database. In future articles, we will use PostgreSQL. Further, we will not write the raw queries. Instead, we are going to use ORM — Waterline. Waterline ORM is developed by the same team that developed the Sails framework and Waterline has nice/tight integration with Sails. So, we are going to use the Waterline ORM with the Sails application.

In order to work with Waterline ORM, we need to install the appropriate adapter for the given database. For example, to work with in-memory database, we need to install sails-disk adapter or npm package. But, installing this sails-disk adapter and configuring it from scratch in the Sails application is complicated and no documentation is available for the same. Instead I’m going to install sails-hook-orm package that includes the sails-disk npm package and provides needed (but not all) configurations for us.

Go ahead and install thesails-hook-orm package using following the command.

npm i sails-hook-orm

We need to create a Contact model that contains the attributes or database columns information. The models in the sails application are usually lived within the api/models folder. We do not have this models folder within the api. So, let’s create a models folder first.

mkdir api/models

Now, within this models folder, we can create a model file for the Contact. The name of the file should be Contact.js. I will follow practice to give a singular name to the model.

touch api/models/Contact.js

Open this api/models/Contact.js file and write the following code.

module.exports = {};

Let’s first define the table name using tableName settings.

module.exports = {
tableName: 'contacts',
};

Next, we need to define columns within this table. Columns or attributes is an object that contains the details about the columns including name, type, and other constraints. For example, to define a firstName column we can write the following.

module.exports = {
tableName: 'contacts',

attributes: {
firstName: {
type: 'string',
required: true,
},
},
};

The type of firstName is string and to make it not null (and with validation), I have written required to true.

Refer attribute documentation at this link to know what are the other properties that you can write for the given column.

Similarly, we can define the lastName as well.

module.exports = {
tableName: 'contacts',

attributes: {
firstName: {
type: 'string',
required: true,
},
lastName: {
type: 'string',
required: true,
},
},
};

Before we go further, I would like to mention one important point and that is primary key is not auto generated by Waterline ORM. We need to explicitly mention about it. We can mention here like we did for the firstName and lastName or we can define within config/models.js. The advantage of defining within the configuration file is, it’ll create a primary key for all the models (or tables). Even further, whatever columns we define within configuration file, it will be created for every models.

Let’s create this model configuration file as models.js within the config folder.

touch config/models.js

Open this config/models.js file and write the following code.

module.exports.models = {};

Let’s define the attributes object within this default object.

module.exports.models = {
attributes: {},
};

Here, we can define the id attribute or column as follows.

module.exports.models = {
attributes: {
id: {
type: 'number',
autoIncrement: true,
},
},
};

By convention, id will be marked as the primary key by the Waterline. So, don’t worry about mentioning the primary key constraint!

While we are here, how about adding two more columns that track timestamp on when it is created and when it last updates. In other words, let’s add two more attributes createdAt and updatedAt as type number with autoCreatedAt and autoUpdatedAt settings to be true.

module.exports.models = {
attributes: {
id: {
type: 'number',
autoIncrement: true,
},
},
createdAt: {
type: 'number',
autoCreatedAt: true,
},
updatedAt: {
type: 'number',
autoUpdatedAt: true,
},
};

Don’t worry about the type as a number. Waterline doesn’t have date or datetime or timestamp type. number type is fine for timestamp and internally it’ll help it very well. autoCreatedAt and autoUpdatedAt insert and update the timestamp when data is inserted or updated automatically These three attributes are included in all our models by default.

Now, if you run the application, you’ll get a long console message and a prompt.

This prompt is about the migration of the model into the database. Although we have defined the model, we haven’t created the database and table yet! Sails is asking us what to do in this case with three options — alter (remove everything and re-insert the data), drop (remove the data but have all the tables), or safe (do nothing). In development, they even suggest to choose alter option. We can write 1 in prompt or we can define 1 or alter as default for migrate for the development in config/models.js file. Let’s open the models.js file and add this setting.

module.exports.models = {
migrate: 'alter',

attributes: {
id: {
type: 'number',
autoIncrement: true,
},
},
createdAt: {
type: 'number',
autoCreatedAt: true,
},
updatedAt: {
type: 'number',
autoUpdatedAt: true,
},
};

Now, check the terminal again! Waterline has auto migrated the schema. You should also see a folder with the name .tmp/localDiskDb in the application folder. This is where our in-memory or disk database is saved.

One more configuration we need to do in order to use this model (or any other models) in our controllers. This is one of those odd and confusing things about Sails.

Go ahead and create a file with the name globals.js within the config folder.

touch config/globals.js

Open this globals.js file and write the following code.

module.exports.globals = {};

Next, let’s define models to true in this default globals object.

module.exports.globals = {
models: true,
};

And three more settings as follows.

module.exports.globals = {
models: true,
sails: true,
_: false,
async: false,
};

These are a little bit confusing and few of these settings are there for legacy purposes. But, if you don’t define these three settings, Sails will throw an error in the console when you try to call/use the models.

Let’s use this model within the ContactsController.js file to fetch all the contacts in index action. To fetch all the contacts, we can use the .find() method as Contact.find(). One more thing — as we have done the above configurations in globals.js file, we do not have to import the Contact model from Contact.js file even if your text-editor/IDE complains about it. Sails internally imported all the models. We just need to use it.

The .find() method returns a promise so we need to update the index function to make it async/await and then we can save the result in the contacts variable and print it.

index: async (req, res) => {
const contacts = await Contact.find();
console.log(contacts);

res.json({ message: 'All contacts' });
},

Now, let’s call/open http://localhost:3000/contacts, we should see an [] in the console as we don’t have any contacts in the database yet.

Note: Here, try to remove those three settings we added in globals.js file and you’ll get an error. These errors are sometimes confusing.

As fetching all contacts is working, let’s remove that sample JSON response and send the contacts as data in index action.

index: async (req, res) => {
const contacts = await Contact.find();

res.json({ data: contacts });
},

To fetch a single contact, .findOne() method is used. This .findOne() method accepts an object that should have the criteria. Again, this .findOne() method returns a promise so we need to update the show function to make it async/await and then we can save the result in the contact variable to return it.

show: async (req, res) => {
const contact = await Contact.findOne({ id: req.params.id });

res.json({ data: contact });
},

Open http://localhost:3000/contacts/1 and it returns an empty object {} as we do not have the contact with id 1.

Going forward, let’s use the async .create() method to create a new contact in create action. This .create() method accepts the object with the key mentioned in model attributes and the values that you’re interested to save.

await Contact.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
});

This code will create a contact but not return the created contact details. To fetch the created contact details, we need to chain the .fetch() method on it and it’ll return the details of created contact that we can save in the contact variable and return it. create action should have the following code after these changes.

create: async (req, res) => {
const contact = await Contact.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
}).fetch();

res.status(201);
res.json({ data: contact });
},

Go ahead and call POST http://localhost:3000/contact with the JSON body that must have firstName and lastName and .create() method will save it in the contacts table. You’ll get that contact as a return object as well. For example, this is what I get after calling an API.

{
"data": {
"id": 1,
"firstName": "Jane",
"lastName": "Doe"
}
}

Before we go further and do changes in update action, try to call http://localhost:3000/contacts and http://localhost:3000/contacts/1 we should now get some data as response.

Let’s use async .update() or .updateOne() method to update an existing contact. I’m going to use the .updateOne() method as it does some extra validation for us. We need to chain this method with the .set() method. In the .updateOne() method we need to pass the criteria whereas in the .set() method we need to pass the values that we’re interested to update.

await Contact.updateOne({
id: req.params.id,
}).set({
firstName: req.params.firstName,
lastName: req.params.lastName,
});

After successful update, it’ll return the updated contact. So, we don’t have to chain the .fetch() method on the .updateOne() method. update action should have the following code after these changes.

update: async (req, res) => {
const contact = await Contact.updateOne({
id: req.params.id,
}).set({
firstName: req.body.firstName,
lastName: req.body.lastName,
});

res.json({ data: contact });
},

Call PUT http://localhost:3000/contacts/1 with sample JSON that should have different values for the firstName and lastName. After a successful call, it should return the updated contact.

Finally, to delete the contact, we can use the .destroy() or .destroyOne() method. I’m going to use the .destroyOne() method as it does some extra validation for us. This async method accepts an object with criteria to delete the contact.

Here is the code we need to write in destroy action.

destroy: async (req, res) => {
const contact = await Contact.destroyOne({
id: req.params.id,
});

res.json({ data: contact });
},

Go ahead and call DELETE http://localhost:3000/contact/1 in Insomnia, and it’ll return the deleted contact for the first time. If you call the same API again, it’ll return an empty object, as the contact is already deleted. This is because we haven’t added any validation. Leave it for future articles. But, for now, we have the working API+CRUD application with database support in Sails.

Take a break and read the sixth 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