Objectives

Include an object relationship from donor to user, and render a donors full name on the report view. Incorporate validation into registration view.

Lab 8 Exercise Solutions

app/controllers/accounts.js

exports.viewSettings = {

  handler: function (request, reply) {
    var userEmail = request.auth.credentials.loggedInUser;
    User.findOne({ email: userEmail }).then(foundUser => {
      reply.view('settings', { title: 'Edit Account Settings', user: foundUser });
    }).catch(err => {
      reply.redirect('/');
    });
  },

};

exports.updateSettings = {

  handler: function (request, reply) {
    const editedUser = request.payload;
    const loggedInUserEmail = request.auth.credentials.loggedInUser;

    User.findOne({ email: loggedInUserEmail }).then(user => {
      user.firstName = editedUser.firstName;
      user.lastName = editedUser.lastName;
      user.email = editedUser.email;
      user.password = editedUser.password;
      return user.save();
    }).then(user => {
      reply.view('settings', { title: 'Edit Account Settings', user: user });
    }).catch(err => {
      reply.redirect('/');
    });
  },

};

Incorporate Donor Object Reference into Donation

Currently our donation Schema looks like this:

const donationSchema = mongoose.Schema({
  amount: Number,
  method: String,
  donor: String,
});

The donor is represented by a simple string - we have been using the users email. To retrieve further information on the donor we would need to do an additional query.

A more sophisticated approach would be to use an object reference directly to the User object:

app/models/donation.js

const donationSchema = mongoose.Schema({
  amount: Number,
  method: String,
  donor: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
  },
});

app/controler/donation.js

Now, when we create a donation, we will need to gain access to the object reference of the donor and use this reference to initialise the donation object.

First, import the User model:

const User = require('../models/user');

Then reimplement the donation handler to establish the link to the donation:

exports.donate = {

  handler: function (request, reply) {
    var userEmail = request.auth.credentials.loggedInUser;
    User.findOne({ email: userEmail }).then(user => {
      let data = request.payload;
      const donation = new Donation(data);
      donation.donor = user._id;
      return donation.save();
    }).then(newDonation => {
      reply.redirect('/report');
    }).catch(err => {
      reply.redirect('/');
    });
  },

};

Restart the app now and make a donation. Examine the donation object in Robomongo:

Note the object reference donor. Verify that the id corresponds with the corresponding user object:

The report view, however, will now only display these ids:

We will fix this in the next step.

Populate Donor Object in Donation Query

app/views/partials/donation.hbs

Having established an object reference, we might like to display the full name of the donor in the report view:

          {{#each donations}}
            <tr>
              <td> {{amount}} </td>
              <td> {{method}} </td>
              <td> {{donor.firstName}} {{donor.lastName}} </td>
            </tr>
          {{/each}}

Run the app again - and inspect the report view. The donor column will be blank however. This is because object references are not automatically retrieved so our form draws a blank on the donor object.

app/controllers/donation.js

So here is minor update to the report handler - with a populate('donor') call inserted into the query:

exports.report = {

  handler: function (request, reply) {
    Donation.find({}).populate('donor').then(allDonations => {
      reply.view('report', {
        title: 'Donations to Date',
        donations: allDonations,
      });
    }).catch(err => {
      reply.redirect('/');
    });
  },

};

This will ensure that the donor object will be retrieve on the single query - and thus our donor object should successfully resolve in the form.

Try this now and verify that the donor's full name is displayed in the report view.

You may notice that it does not work on the first attempt - as your Mongo database may have older version of the donation object in the donations collection. These older reference do not have the donor id as a field, and hence the above query will throw an exception. Delete all your donations objects completed, and the app should run correctly.

Install Joi plugin

Currently we do very little validation in the app. If the user enters invalid or inappropriate values in our forms we are not really alerting the user to correct their entries, or what constitutes a valid entry for a given field.

HAPI has a plugin to help solve this problem:

Like all pligins, we install it in the usual way:

npm install joi -save
{
  "name": "donation-web",
  "version": "1.0.0",
  "description": "an application to host donations for candidates",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "handlebars": "^4.0.5",
    "hapi": "^14.1.0",
    "hapi-auth-cookie": "^6.1.1",
    "inert": "^4.0.1",
    "joi": "^9.0.4",
    "mongoose": "^4.5.8",
    "vision": "^4.1.0"
  }
}

Form Error Partial

We need a new partial to display potential errors in our forms:

app/views/partials/formerror.hbs

{{#if errors}}
  <div class="ui negative message transition">
    <i class="close icon"></i>
    <div class="header">
      There was some errors with your submission
    </div>
    <ul class="list">
      {{#each errors}}
        <li>{{message}}</li>
      {{/each}}
    </ul>
  </div>
{{/if}}

This partial will only populate the view if errors are passed, otherwise it will be silent.

We need placeholders in our register view to include this partial:

app/views/signup.hbs

        </form>
        {{> formerror }}
      </div>

These are introduced just outside the end of each of the forms.

Engage Validation in Register From

We can now make use of the error reporting partial in the register handler.

app/controllers/accounts.js

First - import the joi module:

const Joi = require('joi');

In the register handler, we need a new validate property:

  validate: {

    payload: {
      firstName: Joi.string().required(),
      lastName: Joi.string().required(),
      email: Joi.string().email().required(),
      password: Joi.string().required(),
    },

    failAction: function (request, reply, source, error) {
      reply.view('signup', {
        title: 'Sign up error',
        errors: error.data.details,
      }).code(400);
    },

  },

This contains two properties:

  • payload: This defines a schema which defines rules that our fields must adhere to.
  • failAction: This is the handler to invoke of one or more of the fields fails the validation.

This is the complete register handler:

exports.register = {
  auth: false,

  validate: {

    payload: {
      firstName: Joi.string().required(),
      lastName: Joi.string().required(),
      email: Joi.string().email().required(),
      password: Joi.string().required(),
    },

    failAction: function (request, reply, source, error) {
      reply.view('signup', {
        title: 'Sign up error',
        errors: error.data.details,
      }).code(400);
    },

  },

  handler: function (request, reply) {
    const user = new User(request.payload);

    user.save().then(newUser => {
      reply.redirect('/login');
    }).catch(err => {
      reply.redirect('/');
    });
  },

};

Try this now, and enter invalid data in some of the register form fields. Confirm that the error partial is rendered.

Try more than one invalid field - you will notice only a single error is reported. This is because the default behaviour is to stop at the first error.

We can have the component report all error by including this additional property:

    options: {
      abortEarly: false,
    },

Verify that multiple errors are reported.

Exercises

Archive of the project so far:

Using the registration form as a guide, introduce validation into the login and settings forms.