Creating custom validation errors for SailsJS using AngularJS

Creating custom validation errors for SailsJS using AngularJS

Over the last few months have been working with the NodeJS framework SailsJS. Let me be the first to say that it is extremely light-weight but can be easily scaled very quickly to be able to handle large complex applications. It's feature rich, and much like other NodeJS frameworks, you can extend it using existing Express modules.

With that said, one thing that I really had a hard time understanding was the error messages that SailsJS's ORM validation would create. While they were very easy to understand, they are not exactly polished enough to be able to serve these directly to the user. What's more, there was nothing that I could do to change these errors. Being a Laravel developer where I can easily override any validation error that I want when I want, I became frustrated at the lack of flexibility.

After doing some digging, I uncovered the sails-hook-validation package which allows a little more customization than an out-of-the-box Sails installation. After some tinkering, loud cursing and a little intuition, I was able to come up with a solution that is very modular and extendable. Curious to know what I did? Keep reading!

How to install and implement Sails validation hook

Installation

To install the Sails validation hook, enter the following command into your Terminal:

$ npm install sails-hook-validation --save

Congratulations, you have completed the easy step.

Implementation

The errors and the validation that the errors are tied to can be configured at the model level. For example, here is a model that I came up with for a user:

module.exports = {
  attributes: {
    firstName: {
      type: 'string',
      required: true,
      minLength: 2,
      maxLength: 30
    },
    lastName: {
      type: 'string',
      required: true,
      minLength: 2,
      maxLength: 30
    },
    email: {
      type: 'email',
      required: true,
      unique: true,
      maxLength: 40
    },
    password: {
      type: 'string',
      required: true
    }
  },
  validationMessages: {
    firstName: {
      required: "'First Name' should be a name and is required.",
      minLength: 'The name that was provided was not long enough.',
      maxLength: 'The name that was provided was too long.'
    },
    lastName: {
      required: "'Last Name' should be a name and is required.",
      minLength: 'The name that was provided was not long enough.',
      maxLength: 'The name that was provided was too long.'
    },
    email: {
      required: "'Email' is required.",
      type: 'This is not a valid email address.',
      maxLength: 'The email address that was provided is too long.'
    }
  }
};

While this looks simple, pay attention to the validationMessages object towards the bottom. Each nested object refers to the entry and each corresponding key/value pair refers to the type of validation and the message that should appear if the validation is tripped. Keep in mind that this will need to be done for EVERY model that you have to be able to tap into these error messages. Also if you are going to be relying on localizations (translations), refer to the repository for further documentation as your workflow will change a little.

Now that we have custom error messages being generated, we need to be able to parse these messages. While we CAN build something into our response that pulls certain messages every single time, this isn't very clean and utilitarian. So I took the liberty of adding some Angular and created a custom SailsJS service to handle our code.

Creating our SailsJS service

Before we do anything to our controller and anything on the front-end, we need to create a service that will handle all of the parsing. So within your Sails project, create a file with the api/services directory, title it ErrorService.js and then add the following code:

module.exports = {
  ParseUserErrors: function(errors) {
    var validationErrors = [];
    for (var key in errors) {
      if (errors.hasOwnProperty(key)) {
        for (var item in errors[key]) {
          if (errors[key][item].rule !== "string") {
            validationErrors.push(errors[key][item].message);
          }
        }
      }
    }
    return validationErrors;
  }
};

Notes: Feel free to name the file and method anything you want, just be sure to reflect the changes in your controller.

Admittedly there is some funky array and object parsing going on here, but essentially for each error, we are getting the model key (ie firstName) and then looping through each validation error that is attached to the model key and then adding it to an array.

Adding the new service to our controller

So now that we have created our service, we need to add it to our controller that can put some data in our model that was listed above. Here's what I came up with.

module.exports = {
  create: function (req, res) {
    User.create({
      firstName: req.param('firstName'),
      lastName: req.param('lastName'),
      email: req.param('email'),
      password: req.param('password')
    }, function userCreated(err, newUser) {
      if (err) {
        // err.Errors is the custom array that sails-hook-validation adds
        if (err && err.Errors) {
          var parsedErrors = ErrorService.ParseUserErrors(err.Errors);
          return res.badRequest({err : parsedErrors});
        }
        return res.negotiate(err);
      }
      return res.ok(newUser);
    });
  }
}

Pay extra attention to this if statement:

if (err && err.Errors) {
  var parsedErrors = ErrorService.ParseUserErrors(err.Errors);
  return res.badRequest({err : parsedErrors});
}

The reason why I am testing for err.Errors is because this value is a custom array that the sails-hook-validation module adds to the traditional response that SailsJS produces in the event of an error. So am passing this array into the service that we just created, assigning it to a variable, and then returning it as a 400 error.

Handling the response in the front-end

Now that we have scaffolded everything we need for the backend, lets transition to the front-end. For this exercise I am using AngularJS. Just like for the backend, we need to generate a factory to handle the error parsing to prevent us from repeating ourselves. So lets create a factory like so:

app = angular.module('sample');

app.factory('ErrorAPI', [function() {
  return {
    ParseErrors: function(errors) {
      if (Array.isArray(errors.data.err)) {
        for (var item in errors.data.err) {
          console.log(errors.data.err[item], 'Error');
        }
      } else if (errors.data.err) {
        console.log(errors.data.err, 'Error');
      } else {
        console.log(errors.data, 'Error');
      }
    }
  }
}]);

Note the console.log entries that I have added to this code. Feel free to use a toast service like toastr to handle these errors. Also note the additional if statements. This allows this error factory to handle single errors like 403 or 500 errors that you may need to return.

Finally we need to tie this to a basic $http call . Again note that this is simply for an example and you will need to name and organize your dependencies accordingly and build out your form as you normally would.

$http({
  method: 'POST',
  url: '/api/user',
  withCredentials: true,
  data: {
    firstName: $scope.formData.firstName,
    lastName: $scope.formData.lastName,
    email: $scope.formData.email,
    password: $scope.formData.password
  }
}).then(function successCallback(response) {
  console.log('success',response)
}, function errorCallback(errorResponse) {
  console.log('error',errorResponse);
  ErrorAPI.ParseErrors(errorResponse);
});

If you look at the bottom of the $http request, you will see the ErrorAPI factory being referenced above and that we are passing the errors created by SailsJS into it so it can be parsed and rendered.

Wrapping up

I've found that the code that I touched on in this tutorial is pretty global and can be used in a variety of situations without any issues.

I've created a GitHub gist that can be found here for your reference. If you find a better, more efficient way to execute this, let me know in the comments! I'm currently using this code for my own use so it will only make my work better as well. Enjoy!

Show Comments