Let’s build a Blog: Models, Associations, Actions, Policies & Routes.

Conway
15 min readSep 18, 2018

--

Greetings developers. Conway here again! Last time, we covered how Sails.js handles Models, Controllers and ultimately how this will structure our application. In this article, I will be explaining an in-depth analysis of Models & Actions as well as creating Associations and authorising our Actions with Policies. Ensure you are in the backend directory of our project. If you skipped the last article and want to start from here, make a pull form the GitHub Repository.

Table of Contents

The Models & Associations

Firstly, I will be creating my applications models; these will include Users, Posts, Categories and Comments. I will need to generate these with Sails CLI’ run the following commands:

sails generate model user
sails generate model post
sails generate model category
sails generate model comment

This will generate all of our Models within the api/models folder. To begin with, I am going to set the properties in with the User Model. Open up User.js and underneath primitives, add the following:

emailAddress: {
type: 'string',
required: true,
unique: true,
isEmail: true,
maxLength: 200,
example: 'conway@fullstackedkush.com',
description: 'Unique valid email address'
},

fullName: {
type: 'string',
required: true,
maxLength: 50,
example : 'Conway Kush',
description: 'Users full-name'
},

password: {
type: 'string',
required: true,
protect: true,
example: '_[`~(xY([H9}vD`"',
description: 'Encrypted password'
},

isAdmin : {
type: 'boolean',
example: true,
description: 'Declares if the user is an admin'
},

passwordResetToken : {
type: 'string',
example: 'D3QKeC320SxUi9i2',
description: 'Password reset token'
},

passwordResetTokenExpiry : {
type: 'number',
example: 1537027020609,
description: 'Date when password reset token expires'
},

emailVerifyToken : {
type: 'string',
example: 'HX2I9xFftfQJfke12dwAQ',
description: 'Email verification token'
},

emailVerifyTokenExpiry : {
type: 'number',
example: 1537027020609,
description: 'Date when email verification token expires'
},

emailStatus: {
type: 'string',
isIn : ['confirmed', 'unconfirmed', 'newEmail'],
defaultsTo : 'confirmed',
description: 'Status of users email'
},

emailRequested: {
type: 'string',
isEmail: true,
maxLength: 200,
example: 'conway@fullstackedkush.com',
description: 'Email that has been requested by the user '
},

This includes all the properties that we will be using for our users. I have added extra values for email verification & password reset. These properties will come in handy later. The main properties that we will be using now are: emailAddress, fullName, password & isAdmin. The first three you will recognise from last time though the isAdmin property will be used to determine if the user is an admin. This will allow them to make changes to the Post & Category Models.

Next, I will add the properties for the Post Model. Open up Post.js and underneath Primitives add the following:

title: {
type: 'string',
required: true,
unique: true
},

content: {
type: 'text'
},

slug: {
type: 'slug',
from: 'title',
},

The post model will be pretty basic apart from one of the properties, this is theslug. We need to be able to generate a unique slug from the title so that we are able to reference it from the front end of our application. This is not part of the Sails core. Instead, we are required to write or include a Hook that will do this for you on demand. To do this, I will be using sails-hook-slugs. All you have to do is run the following command: npm i sails-hook-slugs –s. This will now allow you to use the datatype slug with the property form to generate a slug from a specified property.

I will now add the properties for the Category Model. Open Category.js and under Primitives add the following:

title: {
type: 'string',
required: true,
unique: true
},

description: {
type: 'text'
},

slug: {
type: 'slug',
from: 'title',
},

This is similar to the Post Model. It will allow us to add a title, description and generate a slug from the title.

Finally, we will add properties to the Comments Model. To do this, open Comment.js and under Primitives, add the following:

content: {
type: 'text',
required: true,
},

username: {
type: 'string'
},

email: {
type: 'string',
isEmail: true
},

I will now have all the properties within my models to get started to build my application. With pour models, as well as having literal types like string and number within our models properties, we are able to create links to other records within our database. These properties in Sails are called Associations. I am going to need to associate Posts with Users & Comments in a One-to-many relationship, I will need to state that a User can be associated with many Posts and that a Post can be associated with many Comments. To do this, I will add the following underneath Associations within User.js:

posts: {
collection: 'post',
via: 'user'
}

This will state that I will be Associating posts within the collection post by the user property on the Post. Next, within Post.js add:

user: {
model: 'user',
required: true
},

comments: {
collection: 'comment',
via: 'post'
},

The Post Model will now have another property that will be linked to the User Model as well as stating an association to the Comments Model.

The last One-to-many relationship will be adding is the for the Comments Model, in Comment.js under Associations add the following:

user: {
model: 'user'
},

post: {
model: 'post',
required: true
}

We also have to relate the Posts with Categories. We will need to create a Many-to-many relationship. To do this, we need to add the following to Post.js:

categories: {
collection: 'category',
via: 'posts'
}

And then in Category.js :

posts: {
collection: 'post',
via: 'categories'
}

This will allow us to have to associate our Posts to many Categories and our Categories to many Posts.

Actions, Routes & Policies

Now that I have the models set up for my application it is time to create some actions to communicate the API with the database, as well as authenticate the requests with polices.

The main actions I will be creating for most Models will be to Create, Update & Delete. I will be adding the User’s Actions first. In addition to the main actions, I will be adding three extra Actions these are: Login, Logout and one to check the User within the current Session. e are able to use Sails’ CLI to generate the actions. Also, we need to install sails-hook-organics sso we can create password hashes and utilise other hooks. We do this by running the following commands:

sails generate action user.create
sails generate action user.update
sails generate action user.delete
sails generate action user.login
sails generate action user.logout
sails generate action user.check
npm i sails-hook-organics -s

This will create the actions within api/controllers/user. The first action we will be working on will be Create. We need to declare the inputs that are required to be passed into Create requested. To do this, open up create.js and add the following within the inputs property:

emailAddress: {
type: 'string',
required: true,
isEmail: true,
maxLength: 200,
example: 'conway@fullstackedkush.com'
},

fullName: {
type: 'string',
required: true,
maxLength: 50,
example: 'Conway Kush'
},

password: {
type: 'string',
required: true,
example: '_[`~(xY([H9}vD`"'
}

These inputs are all required for the user to input. If any of the values are not passed in or if the Email field isn’t valid, it will throw a E_MISSING_OR_INVALID_PARAMS response as well as if the Email is already being used it will throw a E_UNIQUE response.

Next, we need to create a function to add the User. Within the fn function we need to add the following:

let newUser = await User.create({
emailAddress: inputs.emailAddress.toLowerCase(),
fullName: inputs.fullName,
password:
await sails.helpers.passwords.hashPassword(inputs.password),
emailVerifyToken:
await sails.helpers.strings.random('url-friendly'),
emailVerifyTokenExpiry: Date.now() + 24 * 60 * 60 * 1000,
isAdmin: true
}).fetch();

this.req.session.userId = newUser.id;
return exits.success(newUser);

This will allow us to create a user with a hashed password as well as generate an email verify token with its expiry which will come in useful later on in the series. I have also added the isAdmin: true. This is only for our testing purposes. You will have to remove it in production. The user will also be logged in and added to the current session.

Next, we have to create an Update Action. We will be using the same inputs as the Create with an additional field to confirm the Users current password.

confirmPassword: {
type: 'string',
example: '_[`~(xY([H9}vD`"'
}

The Update Action is a bit more complicated than the Create one. We will need to create some custom Exits to see if certain conditions are not met. You will need to add the following underneath the exits property:

emailNotUnique: {
statusCode: 409, // Set status as conflict
description: 'The email address provided is already in use'
},

badCombo: {
description: 'Password provided does not match the current user',
statusCode: 401,
},

nothingToUpdate: {
statusCode: 400,
description: 'Nothing to update'
},

Now that we have declared our inputs and responses, we now have to build the create function. Within the fn property add the following:

let user = await User.findOne({
id: this.req.session.userId,
});

let updatedValues = {};

let newEmailAddress =
inputs.emailAddress !== undefined ?
inputs.emailAddress.toLowerCase() : undefined;

if (newEmailAddress !== undefined
&& newEmailAddress !== user.emailAddress) {
if (await User.findOne({
or: [{emailAddress: newEmailAddress},
{emailRequested: newEmailAddress}]
}))
throw 'emailNotUnique';

updatedValues = Object.assign(updatedValues, {
emailRequested: inputs.emailAddress.toLowerCase(),
emailStatus: 'newEmail',
emailVerifyToken:
await sails.helpers.strings.random('url-friendly'),

emailVerifyTokenExpiry: Date.now() + 24 * 60 * 60 * 1000,
});
}

if (inputs.fullName !== undefined
&& inputs.fullName !== user.fullName)
updatedValues = Object.assign(updatedValues, {
fullName: inputs.fullName
});

if (inputs.password) {
await sails.helpers.passwords.checkPassword
(inputs.confirmPassword, user.password)
.intercept('incorrect', 'badCombo');
updatedValues = Object.assign(updatedValues, {
password:
await sails.helpers.passwords.hashPassword(inputs.password)
});
}

if (!Object.keys(updatedValues).length)
throw 'nothingToUpdate';

let updatedUser = await User.update({id: user.id})
.set(updatedValues).fetch();

return updatedUser ? exits.success(updatedUser) : exits.notFound;

This will update the user. If they are requesting a new email address, this will regenerate the email verify token and expiry as well as change the email status to newEmail.

Next, we are going to add values to the Delete Action. By now you should comprehend that we are adding content to the inputs, exits & fn properties. From here, we will be replacing the whole file. Open delete.js and add the following:

module.exports = {

friendlyName: 'Delete',

description: 'Delete user.',

inputs: {

password: {
type: 'string'
},

},

exits: {

badCombo: {
description:
'Password provided does not match the current user',
statusCode: 401,
},

},
fn: async function (inputs, exits) {

let user = await User.findOne({
id: this.req.session.userId,
});

await sails.helpers.passwords.checkPassword(
inputs.password, user.password)
.intercept('incorrect', 'badCombo');

await User.destroy({id: user.id});

delete this.req.session.userId;

return exits.success();

}

};

The Delete action will require the users’ password to be verified before destroying the record.

These are the main Actions for the User Model. Next, we are going to add the Login, Logout & Check Actions. Add the following to login.js:

module.exports = {

friendlyName: 'Login',

description: 'Login user.',

inputs: {

emailAddress: {
type: 'string',
required: true
},

password: {
type: 'string',
required: true
},

rememberMe: {
description:
'Whether the user wishes to extend the session length.',
type: 'boolean'
}

},

exits: {

badCombo: {
description:
'Password provided does not match the current user',
statusCode: 401,
},

},

fn: async function (inputs, exits) {

let user = await User.findOne({
emailAddress: inputs.emailAddress.toLowerCase(),
});

if(!user)
throw 'badCombo';

await sails.helpers.passwords.checkPassword(
inputs.password, user.password)
.intercept('incorrect', 'badCombo');

if (inputs.rememberMe)
this.req.session.cookie.maxAge = 30*24*60*60*1000;

this.req.session.userId = user.id;

return exits.success(user);

}
};

This will login the User and add them to the current session. If the rememberMe field is set to true then the max cookie age will be changed to 30 days.

The next Action will be Logout, add the following to logout.js:

module.exports = {

friendlyName: 'Logout',

description: 'Logout user.',

fn: async function (inputs, exits) {
delete this.req.session.userId;
return exits.success();
}
};

This will delete the current user from the session.

Finally, we will add the Action that will check the cookie for the current user. This will be used of the user refreshes the application. Add the following in check.js:

module.exports = {

friendlyName: 'Check',

description: 'Check user.',

fn: async function (inputs, exits) {
let user = null;

// Find the user from the current session
if(this.req.session.userId)
user = await User.findOne({
id: this.req.session.userId,
});

return exits.success(user);
}
};

One Model down, three to go! The next three are more direct. Again, I will be adding in the whole Action for these. The next Model is Post; the only Actions that are required here are Create, Update & Delete. Run the following command to add them:

sails generate action post.create
sails generate action post.update
sails generate action post.delete

This will create the actions within api/controllers/post. First, we will add the Create action. In create.js add the following:

module.exports = {

friendlyName: 'Create',

description: 'Create post.',

inputs: {

title: {
type: 'string',
required: true,
unique: true
},

content: {
type: 'string'
},

categories: {
type: 'json'
}

},

fn: async function (inputs, exits) {
let newPost = await Post.create(
Object.assign(inputs,
{user: this.req.session.userId})).fetch();

if(inputs.categories)
await Post.addToCollection(
newPost.id, 'categories', inputs.categories);

return exits.success(newPost);

}

};

This will create a Post. It will also assign the categories that are defined. Note that the categories need to be passed in as an Array, hence the data type json.

Next, we need to add the following to update.js:

module.exports = {

friendlyName: 'Update',

description: 'Update post.',

inputs: {

title: {
type: 'string',
required: true,
unique: true
},

content: {
type: 'string'
},

id: {
type: 'number',
required: true
},
categories: {
type: 'json'
}
},

fn: async function (inputs, exits) {

let updatedPost = await Post.update({id: inputs.id}).set({
title: inputs.title,
content: inputs.content
}).fetch();

if (inputs.categories)
await Post.replaceCollection(
updatedPost.id, 'categories', inputs.categories);

return updatedPost ?
exits.success(updatedPost) : exits.notFound();

}

};

This will perform an update on the Post. Notice that when the categories are synced, it is using the function replaceCollection. This will ensure that the categories that you chose to remove are no longer in the collection.

Finally, we will add the Delete Action, add the following to delete.js:

module.exports = {

friendlyName: 'Delete',

description: 'Delete post.',

inputs: {

id: {
type: 'number',
required: true
}

},

fn: async function (inputs, exits) {
await Post.destroy({id: inputs.id});

return exits.success();

}

};

Now that we have all the Actions for Users & Posts, I will be quickly adding Categories & Comments.

Categories will need the Create, Update & Delete. For Comments, I do not want my users to be able to edit them. Comments will just be using Create & Update. Generate these with the following commands:

sails generate action category.create
sails generate action category.update
sails generate action category.delete
sails generate action comment.create
sails generate action comment.delete

This will create the Category Actions within api/controllers/category and the Comment Actions within api/controllers/comment. We need to add the following to category/create.js:

module.exports = {

friendlyName: 'Create',
description: 'Create category.',

inputs: {

title: {
type: 'string',
required: true,
unique: true
},

description: {
type: 'string'
},

},

fn: async function (inputs, exits) {
let newCategory = await Category.create(inputs)
.fetch();

return exits.success(newCategory);

}

};

Then in category/update.js we add:

module.exports = {

friendlyName: 'Update',

description: 'Update category.',

inputs: {

title: {
type: 'string',
required: true,
unique: true
},

description: {
type: 'string'
},

id: {
type: 'number',
required: true
}

},

fn: async function (inputs, exits) {
let updatedCategory = await Category.update({id: inputs.id})
.set(inputs)
.fetch();

return updatedCategory ?
exits.success(updatedCategory) : exits.notFound();

}

};

And finally within category/delete.js we add:

module.exports = {

friendlyName: 'Delete',

description: 'Delete category.',

inputs: {

id: {
type: 'number',
required: true
}

},

fn: async function (inputs, exits) {
await Category.destroy({id: inputs.id});

return exits.success();

}

};

Comments are slightly different; it requires a Post to relate to. If a logged-in User writes a Comment we want to relate it to them and if an unknown user posts an email is required. To do this, in comment/create.js we write the following:

module.exports = {

friendlyName: 'Create',

description: 'Create comment.',

inputs: {

content: {
type: 'string',
required: true,
},

username: {
type: 'string',
},

email: {
type: 'string',
isEmail: true
},

post: {
type: 'number',
required: true
}

},

exits: {

noUser: {
statusCode: 401,
description: 'There is no user attached to the comment'
}

},

fn: async function (inputs, exits) {

if(this.req.session.userId)
inputs = Object.assign(inputs,
{user : this.req.session.userId})
else if(inputs.email && inputs.username)
inputs = Object.assign(inputs,
{email : inputs.email },
{username : inputs.username });
else throw 'noUser';

let newComment = await Comment.create(Object.assign(inputs, {
post: inputs.post})).fetch()

return exits.success(newComment);

}

};

And then in comment/delete.js:

module.exports = {

friendlyName: 'Delete',

description: 'Delete comment.',

inputs: {

id: {
type: 'number',
required: true
}

},

fn: async function (inputs, exits) {

await Comment.destroy({id: inputs.id});

return exits.success();

}

};

Now that we finally have our Actions in order, we need to create Routes to point URL’s towards our Actions. We will also make policies to only allow Users to make changes to their account and also allow only Admins to make changes to the content.

First, we are going to create our Routes. Open config/routes.js and add the following:

// User API endpoints
'POST /api/v1/user/create': { action: 'user/create' },
'PUT /api/v1/user/update': { action: 'user/update' },
'DELETE /api/v1/user/delete': { action: 'user/delete' },

'PUT /api/v1/user/login': { action: 'user/login' },
'GET /api/v1/user/logout': { action: 'user/logout' },
'GET /api/v1/user/check': { action: 'user/check' },

// Post API endpoints
'POST /api/v1/post/create': { action: 'post/create' },
'PUT /api/v1/post/update': { action: 'post/update' },
'DELETE /api/v1/post/delete': { action: 'post/delete' },

// Comment API endpoints
'POST /api/v1/comment/create': { action: 'comment/create' },
'DELETE /api/v1/comment/delete': { action: 'comment/delete' },


// Category API endpoints
'POST /api/v1/category/create': { action: 'category/create' },
'PUT /api/v1/category/update': { action: 'category/update' },
'DELETE /api/v1/category/delete': { action: 'category/delete' },

Now, we are able to make requests to our Actions so that we can CRUD all of our models. You can test them all within Postman. Note that the words in capitals before the URL are the request types.

I have created a Postman Collection that you can add to yours here, the collection contains all the appropriate requests.

Finally, we need to add our Policies to authorise our requests. We will be creating two Policies isLoggedIn and isAdmin Sails’ does not ship with any generators for Policies so you have to add them yourself. Create the file api/polices/isLoggedIn.js and add the following:

module.exports = async function (req, res, proceed) {
if (req.session.userId) {
return proceed();
}

return res.forbidden();

};

This will check the session to see if the User is logged in. If it is not, throw a 403 Forbidden response. The next Policy we need to create will be for isAdmin to do this create the file api/polices/isAdmin.js and add the following:

module.exports = async function (req, res, proceed) {
if (!req.session.userId)
return res.forbidden();

let user = await User.findOne({
id: req.session.userId,
});

if(user.isAdmin)
return proceed();

return res.forbidden();
};

This will check both if the user is logged in and if that user has the isAdmin parameter set to true. Now, if you were to test these within Postman you will only be able to update the current user if you are logged in or create any content other than Comments if you are an Admin.

Conclusion

Apologies for the long article; though I’m sure it will be the most extensive! We now have built the server side architecture for our Application. We now have Models, Associations, Actions, Policies & Routes. This is the main part of our API. In my next article, I will show you how to create Emails and custom Responses. After, we will get into the front-end of our application. We will learn about Vue.js and Nuxt.js and the importance of Server Side Rendering.

I would recommend pulling the series GitHub Repository as I have commented throughout the code. You can also follow my progress on Instagram.

--

--