Wed Apr 23 2014

Websites != CMS Platform - Storing Data - Part 2

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

The code can be found on GitHub

Previous Post

The first step is always (or at least should be) to take a step back and decide what to actually do…

In the last post the decision was made to store one document per page, and to have a unique index on the documents name property. This fits with a PUT request

Callers of a PUT method should anticipate the calls are idempotent and made to the URL of a given resource. That is we'll be sending data to /pages/pageName and not /pages and repeatedly sending the same document for storage means that the document should be updated not duplicated.

Tests

This feature requires a set of conditions are tested:

  • you can't PUT an empty page
  • if you PUT a new page you receive a 201
  • if you PUT an existing page you receive a 200
  • the inserted or updated resource URL is in the location header of the response
describe('PUTing pages', function() {
    it('should 400 when no body');

    describe('with new name', function(){
      it('respond with 201 status');
    });

    describe('with existing name', function(){
      it('respond with 200 status');
  });
});

After a little backwards and forwards the tests ended up as:

var request = require('supertest');
var should = require('should');

var server;
var db;

beforeEach(function() {
    //set environment to test and init things
    process.env.NODE_ENV = 'test'; 
    db = require('../server/db');
    server = require('../server').app;
});

describe('PUTing pages', function() {
    it('should 400 when no body', function(done) {
        request(server)
          .put('/pages/newPage')
          .set('Accept', 'text/json')
          .expect('Content-Type', /json/)
          .expect(400, done);
    });

    describe('with a new page name', function(){
      beforeEach(function() {
        db.pages.remove({}, false, function(err, doc) {});
      });

      it('should respond with 201 status', function(done){
        request(server)
          .put('/pages/newPage')
          .send({name:'newPage', url:'/somewhere'})
          .set('Accept', 'text/json')
          .expect('Content-Type', /json/)
          .expect('location', '/somewhere')
          .expect(201, done);
      });

    });

    describe('with an existing page name', function(){
      beforeEach(function() {
        db.pages.remove({}, false, function(err, doc) {});
        db.pages.insert({name:'existingPage'}, function(err, docs){});
      });

      it('should respond with 200 status', function(done){
        request(server)
          .put('/pages/existingPage')
          .send({name:'existingPage', url:'/somewhereElse'})
          .set('Accept', 'text/json')
          .expect('Content-Type', /json/)
          .expect('location', '/somewhereElse')
          .expect(200, done);
      });

    });
});

and an alteration to the server file to make those tests pass:

app.put('/pages/:page', function(req, res, next) {
    var pageName = req.params.page;
    if(!req.body || Object.getOwnPropertyNames(req.body).length === 0) {
        return res.json(400, {});
    }
    db.pages.findAndModify({
        query: { name: pageName },
        update: { $set: req.body },
        upsert: true,
        new: true
    }, function(err, doc, lastErrorObject) {
        if(err) {
            next(err);
        } else {
            res.location(doc.url || '/');
            if(lastErrorObject.updatedExisting) {
                res.json(200, {}); 
            } else {
                res.json(201,{}); 
            }
        }
    });
});

Again this code feels a bit ugly to me… there's a lot bunched up together - but it can be revisited as it's covered by tests. Importantly it works and allows storage of new pages and edits to existing pages

And, yes, I know that any unauthorised user can edit with this… authentication is still to come!