Question

MEAN stack newbie here. Probably asking a silly question.

As an exercise, I have been trying to implement a prototype SPA which shows a series of task cards on the screen (kinda like Trello).

For now, each card has 4 fields:

  • _id: ObjectId
  • content: String
  • workflow: String
  • state: String

I am using MongoDB for the database (entered some test data using Robomongo), I have node.js installed on my machine, as well as Express.js.

My server.js file looks like the following:

var express = require('express'), 
    cards = require('./routes/cards');

var app = express();

app.configure(function() {
    app.use(express.logger('dev'));
    app.use(express.bodyParser());
});

app.get('/cards', cards.findAll);
app.get('/cards/:id', cards.findById);
app.post('/cards', cards.addCard);
app.put('/cards/:id', cards.updateCard);

app.listen(3000);
console.log('Listening on port 3000...');

My routes/cards.js on the server side look like the following:

    var mongo = require('mongodb');
var Server = mongo.Server,
    Db = mongo.Db,
    BSON = mongo.BSONPure;

var server = new Server('localhost', 27017, {auto_reconnect: true});
var db = new Db('mindr', server);
db.open(function(err, db) {
    if(!err) {
        console.log("Connected to 'mindr' database");
        db.collection('cards', {strict:true}, function(err, collection) {
            if (err) {
                console.log("The 'cards' collection doesn't exist.");
            }
        });
    }
});

exports.findById = function(req, res) {
    var id = req.params.id;
    console.log('Retrieving card: ' + id);
    db.collection('cards', function(err, collection) {
        collection.findOne({'_id':new BSON.ObjectID(id)}, function(err, item) {
            res.send(item);
        });
    });
};

exports.findAll = function(req, res) {
    db.collection('cards', function(err, collection) {
        collection.find().toArray(function(err, items) {
            res.send(items);
        });
    });
};

exports.addCard = function(req, res) {
    var newCard = req.body;
    console.log('Adding card: ' + JSON.stringify(newCard));
    db.collection('cards', function(err, collection) {
        collection.insert(newCard, {safe:true}, function(err, result) {
            if (err) {
                res.send({'error':'An error has occurred'});
            } else {
                console.log('Success: ' + JSON.stringify(result[0]));
                res.send(result[0]);
            }
        });
    });
}

exports.updateCard = function(req, res) {
    var id = req.params.id;
    var card = req.body;
    console.log('Updating card: ' + id);
    console.log(JSON.stringify(card));
    db.collection('cards', function(err, collection) {
        collection.update({'_id':new BSON.ObjectID(id)}, card, {safe:true}, function(err, result) {
            if (err) {
                console.log('Error updating card: ' + err);
                res.send({'error':'An error has occurred'});
            } else {
                console.log('' + result + ' document(s) updated');
                res.send(card);
            }
        });
    });
}

exports.deleteCard = function(req, res) {
    var id = req.params.id;
    console.log('Deleting card: ' + id);
    db.collection('cards', function(err, collection) {
        collection.remove({'_id':new BSON.ObjectID(id)}, {safe:true}, function(err, result) {
            if (err) {
                res.send({'error':'An error has occurred - ' + err});
            } else {
                console.log('' + result + ' document(s) deleted');
                res.send(req.body);
            }
        });
    });
}

When I get the cards from the DB in my AngularJS controller, everything goes fine. All the cards are correctly displayed on the screen. This is the code that gets the cards:

var mindrApp = angular.module('mindrApp', ['ngResource'])

mindrApp.controller('WorkflowController', function ($scope, $resource) {
    var CardService = $resource("http://localhost:3000/cards/:cardId", {cardId:"@id"});
    $scope.cards = CardService.query();
});

On each card there are some buttons that can be used to change the state of the card to the next state available in the workflow (as defined by the current state available actions).

When the button is clicked, the card id and the next state are passed to a function in the controller:

<div class="btn-group btn-group-xs">
    <button type="button" class="btn btn-default" 
        ng-repeat="currentAction in currentState.actions | filter:{default:true}" 
        ng-click="processCard(currentCard._id, currentAction.next)">
        {{currentAction.name}}
    </button>
</div> 

And this is the processCard function in the controller:

$scope.processCard = function(id, nextState) {
    var currentCard = CardService.get({cardId: id}, function(){
        currentCard.state = nextState;
        currentCard.$save();
    });
};

What's happening is that when I click the button, instead of changing the state of the current card, a new card is created with an id field of type String. This is the output of the server:

Retrieving card: 52910f2a26f1db6a13915d9f
GET /cards/52910f2a26f1db6a13915d9f 200 1ms - 152b
Adding card: {"_id":"52910f2a26f1db6a13915d9f","content":"this is some content for this really cool card","workflow":"simple","state":"completed"}
Success: {"_id":"52910f2a26f1db6a13915d9f","content":"this is some content for this really cool card","workflow":"simple","state":"completed"}
POST /cards 200 1ms - 150b

Any idea why this is happening? Why is it calling the addCard function on the server instead of calling the updateCard function?

Was it helpful?

Solution 2

Ok, so I figured it out. The two problems I were having were:

1) instead of updating the existing item in the database, it was creating a new one with the same ID but in string format instead of using the ObjectId format.

2) any time I called $update, it would not append the ID to the path, but always PUT to /cards.

So here are the solutions to each of the problems.

1) This is really a hack that assumes that ALL id are in ObjectId format. I don't like this solution but for now it works and I am sticking to it. All I had to do was to add the line that converts the card._id back to ObjectId format to the updateCard function inside the cards.js file on the server side.

exports.updateCard = function(req, res) {
    var id = req.params.id;
    var card = req.body;
    console.log('Updating card: ' + id);
    console.log(JSON.stringify(card));
    card._id = new BSON.ObjectID.createFromHexString(card._id); // HACK!
    db.collection('cards', function(err, collection) {
        collection.update({'_id':new BSON.ObjectID(id)}, card, {safe:true}, function(err, result) {
            if (err) {
                console.log('Error updating card: ' + err);
                res.send({'error':'An error has occurred'});
            } else {
                console.log('' + result + ' document(s) updated');
                res.send(card);
            }
        });
    });
}

2) This was a two part fix. First, I had to modify the services.js file to explicitly say that I want to use update via PUT:

    var mindrServices = angular.module('mindrServices', ['ngResource']);
    mindrServices.factory("Card", ["$resource",
    function($resource) {
        return $resource("http://localhost:3000/cards/:cardId", {cardId:"@id"},
            {
                query: {method: "GET", isArray:true},
                update: {method: "PUT"}
            }
        );
    }]);

Next, I was under the assumption that simply calling currentCard.$update() would grab the ID from the calling instance, instead I have to explicitly pass in the ID as follows:

var mindrControllers = angular.module('mindrControllers', []);
mindrControllers.controller('CardsController', ["$scope", "Card", 
    function ($scope, Card) {
        $scope.cards = Card.query();
        console.log("cards populated correctly...");

        $scope.processCard = function(currentCard, currentAction) {
            console.log("processCard: card[" + currentCard._id + "] needs to be moved to [" + currentAction.next + "] state... ");
            currentCard.state = currentAction.next;
            currentCard.$update({cardId: currentCard._id}); // passing the ID explicitly
        }

This is the output I get on the server side:

    Updating card: 52910eb526f1db6a13915d9c
{"_id":"52910eb526f1db6a13915d9c","content":"this is some content for this really cool card","workflow":"simple","state":"backlog"}
    1 document(s) updated
    PUT /cards/52910eb526f1db6a13915d9c 200 4ms - 111b

OTHER TIPS

The $save() action of a $resource object use POST as default request type (Read more here). So in your case, a POST request to the route /cards/:id was called, so as a result, a new card was created.

Either create a new route entry to handle POST update request in server.js

app.post('/cards/:id', cards.updateCard);

Or add another action that use PUT to your CardService and call it when you want to update your card

var CardService = $resource("http://localhost:3000/cards/:cardId", {cardId:"@id"},
                    { update: { method: 'PUT' } }
                  );

// update the card
...
currentCard.$update();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top