Considering user history

Our users will probably want to always be paired to meet new people, so we have to avoid repetitive meetings. How should we handle this?

First, we need to allow for a method to set up new meetings. Think of it as a button in an app that would trigger a request to the route POST/meeting/new.

This endpoint will reply with the status 200 when the request is allowed and a pair is found, or if there is no pair but they are now attached to a meeting object and can now be matched with another user; 412 if the user is already scheduled in another meeting and 400 in case the expected e-mail of the user isn't sent; in this case, it can't be fulfilled because the user wasn't specified.

Tip

The usage of status codes is somewhat subjective, (see a more comprehensive list on Wikipedia at http://en.wikipedia.org/wiki/List_of_HTTP_status_codes). However, having distinct responses is important so that the client can display meaningful messages to the user.

Let's implement an Express.js middleware, that requires an e-mail for all requests that are made on behalf of the user. It should also load their document and attach it to res.locals, which can be used in subsequent routes.

Our src/routes/index.js will look like this:

'''javascript
//...
  app.post("/register", register.create);
  app.post("/meeting", [filter.requireUser], meeting.create);
//...
'''

The filter in 'src/routes/filter.js' is:

'''javascript
module.exports = function(Model) {
  var methods = {};

  methods.loadUser = function(req,res,next) {
    var email = req.query.email || req.body.email
    if(!email) return res.status(400).send({error: "email missing, it should be either in the body or querystring"});
    Model.User.loadByEmail(email, function(err,user) {
      if(err) return next(err);
      if(!user) return res.status(400).send({error: "email not associated with an user"});
      res.locals.user = user;
      next();
    })
  }

  return methods;
};

The goal of this middleware is to stop and return an error message for every request that doesn't have a user email. It's a validation that would usually require a username and password or a secret token.

Let's set up a small but important test suite for this middleware:

  • Clear DB
  • Try to get me without email and fail
  • Create a valid user; it succeeds
  • Try to get me with another email; it fails
  • Access me with the email we registered and it works!

Now that we have a way to load the user who's making the request, let's go back to the goal of matching people without repetition. As a pre-condition, their past meeting time has to be in the past already, otherwise it returns a 412 code.

If we want to schedule a meeting for our users but any scheduled meeting will be set for tomorrow, how can we test it? Meet timekeeper (https://github.com/vesln/timekeeper), library with a simple interface to alter the system dates in Node.js; this is especially useful for tests. Look closely for the snippet of this test:

'''javascript
describe('Meeting Setup', function() {
  before(dbCleanup);

  after(function() {
    timekeeper.reset();
  });

  // ...

  it('should try matching an already matched user', function(done) {
    request(app)
      .post('/meeting')
      .send({email:userRes1.email})
      .expect(412, done);
  });

  it('should be able match the user again, 2 days later', function() {
    var nextNextDay = moment().add(2,'d'),
    timekeeper.travel(nextNextDay.toDate());
    request(app)
      .post('/meeting')
      .send({email:userRes1.email})
      .expect(200, function(err,res){
        done(err);
      });
  });

It's of vital importance to set an after hook to reset timekeeper so that the dates go back to normal after the scenario is finished in either success or failure; otherwise, there is a chance it will alter the results of other tests. It's also worth checking how date manipulation is made easy with moment() method and once you use timekeeper.travel() function, the time is warped to that date. For all Node.js knows, the new warped time is the actual time (although it does not affect any other applications). We can also switch it back and forth as required.

The Meeting method to perform this check on our user (defined at models/meeting.js) is as follows:

  methods.isUserScheduled = function(user, cb) {
    Meeting.count({
      $or:[
        {'user1.email': user.email},
        {'user2.email': user.email}
      ],
      at: {$gt: new Date()}
    }, function(err,count) {
      cb(err, count > 0);
    });
  };

The $or operator is necessary because we don't know whether the user we are looking for is going to be user1 or user2, so we take advantage of the query capabilities of MongoDB that can look inside objects in a document and match the email as a String, and the at field as mentioned earlier.

Our newly created src/routes/meeting.js, is given as follows:

'''javascript
module.exports = function(Model) {
  var methods = {};

  methods.create = function(req,res,next) {
    var user = res.locals.user;
    Model.Meeting.isUserScheduled(user, function(err,isScheduled) {
      if(err) return next(err);
      if(isScheduled) return res.status(412).send({error: "user is already scheduled"});
      Model.Meeting.pair(user, function(err,result) {
        // we don't really expect this function to fail, if that's the case it should be an internal error
        if(err) return next(err);
        res.send({});
      })
    })
  }

  return methods;
};

Moving on, we'll define a very important helper function that finds previous meetings involving the user who's making the request and returns the emails of everyone they have been matched with, so we can avoid matching those two users again.

Helper functions like this are super useful to keep the code understandable when dealing with complicated pieces of logic. As a rule of thumb, always separate into smaller functions when a chunk of code can be abstracted into a concept.

  /**
   * the callback returns an array with emails that have previously been
   * matched with this user
   */
  methods.userMatchHistory = function(user,cb) {
    var email = user.email;
    Meeting.find({
      $or:[
        {'user1.email': email},
        {'user2.email': email}
      ],
      user1: {$exists: true},
      user2: {$exists: true}
    }, function(err, meetings) {
      if(err) return cb(err);
      var pastMatches = meetings.map(function(m) {
        if( m.user1.email != email) return m.user1.email;
        else return m.user2.email;
      });
      // avoid matching themselves!
      pastMatches.push(user.email);
      cb(null, pastMatches);
    })
  }

The key to userMatchHistory object; is through the MongoDB $nin operator, which performs a match when the element doesn't match what's in the array. The matching logic follows the very same logic we had in naive pairing.

In our Meeting model, we removed our previous pairNaive method with the pair method, which does similar, but first build a list of the previous matches to ensure we don't match those again.

  methods.pair = function(user,done) {
    // find the people we shouldn't be matched with again
    methods.userMatchHistory(user, function(err, emailList) {
      if(err) return done(err);

      Meeting.findAndModify({
        new: true,
        query: {
          user2: { $exists: false },
          'user1.email': {$nin: emailList}
        },
        update: {
          $set: {
            user2: user,
            at: arrangeTime()
          }
        }
      }, function(err, newPair) {
        if (err) { return done(err); }

        if (newPair){
          return done(null, newPair);
        }

        Meeting.insert({user1: user}, function(err,meeting) {
          done();
        })
        return;
      });
    })
  }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.86.183