Partial update of a subdocument with nodejs/mongoose - node.js

Is it possible to set multiple properties on a (sub)document in one go with Mongoose? An example of what I'm trying to do:
Let's say I have this schema:
var subSchema = new Schema({
someField: String,
someOtherField: String
});
var parentSchema = new Schema({
fieldOne: String,
subDocs: [subSchema]
})
Then I would like to do:
exports.updateMyDocument = function(req, res) {
var parentDoc = req.parentDoc; // The parent document. Set by parameter resolver.
var document = req.myDoc; // Sub document of parent. Set by parameter resolver.
var partialUpdate = req.body; // updated fields sent as json and parsed by body parser
// I know that the statement below doesn't work, it's just an example of what I would like to do.
// Updating only the fields supplied in "partialUpdate" on the document
document.update(partialUpdate);
parentDoc.save(function(err) {
if(err) {
res.send(500);
return;
}
res.send(204);
});
};
Normally, I could achieve this using the $set operator, but my problem is that document in this example is a subdocument (embedded schema) of parentDoc. So when I tried to do
Parent.update({_id: parentDoc._id, "subDocs._id": document._id},
{$set: {"subDocs.$" : partialUpdate}},
function(err, numAffected) {});
it replaced the subdocument instance identified by subDocs._id. Currently I have "solved" it by setting only fields manually, but I was hoping for a better way to do this.

Build up a $set object programmatically based on the fields of partialUpdate to update just those fields using dot notation:
var set = {};
for (var field in partialUpdate) {
set['subDocs.$.' + field] = partialUpdate[field];
}
Parent.update({_id: parentDoc._id, "subDocs._id": document._id},
{$set: set},
function(err, numAffected) {});

I've done different, in a REST application.
First, I have this route:
router.put('/:id/:resource/:resourceId', function(req, res, next) {
// this method is only for Array of resources.
updateSet(req.params.id, req.params.resource, req, res, next);
});
and the updateSet() method
function updateSet(id, resource, req, res, next) {
var data = req.body;
var resourceId = req.params.resourceId;
Collection.findById(id, function(err, collection) {
if (err) {
rest.response(req, res, err);
} else {
var subdoc = collection[resource].id(resourceId);
// set the data for each key
_.each(data, function(d, k) {
subdoc[k] = d;
});
collection.save(function (err, docs) {
rest.response(req, res, err, docs);
});
}
});
}
The brilliant part is mongoose will validate the data if you define the Schema for this subdocument. This code will be valid for any resource of the document that is an Array. I'm not showing all my data for simplicity, but is a good practice to check for this situations and handle the response error properly.

You can assign or extend embedded document.
Doc.findOne({ _id: docId })
.then(function (doc) {
if (null === doc) {
throw new Error('Document not found');
}
return doc.embeded.id(ObjectId(embeddedId));
})
.then(function(embeddedDoc) {
if (null === embeddedDoc) {
throw new Error('Embedded document not found');
}
Object.assign(embeddedDoc, updateData));
return embeddedDoc.parent().save();
})
.catch(function (err) {
//Do something
});
And in this case you should be shure that _id is not assigning.

I handled this in a slightly different manner without using the $set object. My approach is similar to Guilherme's but one difference is that I wrapped my method into the statics functionality so that it is easier to re-use throughout my application. Example below.
In CollectionSchema.js server model.
collectionSchema.statics.decrementsubdocScoreById = function decreasesubdoc (collectionId, subdocId, callback) {
this.findById(collectionId, function(err, collection) {
if (err) console.log("error finding collection");
else {
var subdoc = collection.subdocs.filter(function (subdoc) {
return subdoc._id.equals(subdocId);
})[0];
subdoc.score -= 1;
collection.save(callback);
}
});
};
In Server Controller
Collection.decrementsubdocScoreById(collectionId, subdocId, function (err, data) {
handleError(err);
doStuffWith(data);
});

Related

How to get data from one collection and insert into another collection in Nodejs?

Am using Nodejs and MongoDB and I am new to nodejs. I need to know how to get data from one collection and append some additional data and insert into another collection.
db.collection('collection1').find({ "Id" : 12345 }).toArray(function(err, result){
db.collection('collection2', function(err, collection){
collection.insert({
//some data
})
})
})
When I try this code its not working its giving me error insert is not defined.
thanks,
John.
db.collection('collection1').find({ "Id" : 12345 }).toArray(function(err, result){
//do the modification here
db.collection('collection2').insert(modifiedResult, function(err, result){
if(err) {
//log error
}else{
//log result
}
})
})
One more thing, If the result array length is more that one and you want to insert then separately then use promise
db.collection('collection1').find({ "Id" : 12345 }).toArray(function(err, result){
//do the modification here
Promise.all(modifiedResult.map((eachModifiedResult)=>{
return db.collection('collection2').insert(eachModifiedResult);
}).then((result)=>{
//result of the insert
}).catch((err){
//err if any happen
});
})
But if you have a very large doc then do it as Neil Said. Read the collection one by one using cursor and modify them and insert them to other db.
You can use callback library like async or Promises Q
Promise
var collectionData = null;
var modifiedResult = null;
// here i am using async library to avoid callbackHell
async.series([
// for get data from collection 1.
function(cb) {
var criteria = {
"Id": 12345
}
db.collection('collection1').find(criteria).toArray(function(dbErr, dbResult) {
if (err) {
cb(dbErr)
} else {
collectionData = dbResult;
cb()
}
})
},
// Append Data in collectionData
function(cb) {
// do you work here to append data in collectionData
modifiedResult = extendedData; // this is just an example you need to work on it
cb();
},
// Update collection 2 here
function(cb) {
db.collection('collection2').insert(modifiedResult, function(err, result) {
if (err) {
cb(dbErr)
} else {
collectionData = dbResult;
cb()
}
});
}
]);

Cant send data back to client with Node.js post request with for loop

I have a post request which I need to loop through an array, find the users in the database and sent the results back to the view but I can only seem to send back the first user. How are for-loops supposed to be implemented? I cant use re.send in a loop and res.JSON gives me the same result.
My code below sends back the first user:
app.post('/rankcandidates', function(req, res){
var array = JSON.parse(req.body.array);
for (var i = 0;i<array[0].length;i++){
User.find({"_id" : { "$in" : [ array[0][i]._id] }
}).exec(function (err, result) {
res.setHeader('Content-Header', 'application/json');
res.send(JSON.stringify(result));
// also tried res.JSON but doesn't work
});
}
});
My Ajax call:
$.post("/rankcandidates",
{ array:JSON.stringify(array) },
function(data,status){
console.log(data); // comes out as a string of the first user
}
});
New Problem - Inserting objects into database with for loop:
app.post('/insertPositionIndex', function(req, res){
var array = JSON.parse(req.body.array);
console.log(array[0]); // data shown below
var ids;
var indexes;
for (var i=0;i<array.length;i++){
ids = array[i][0].map(function(element) { return element.position_id });
indexes = array[i][0].map(function(element) { return element.index_position });
console.log(ids);
console.log(indexes);
}
User.update(
{ "_id": req.user._id},
{
"$push":
{
"positionsApplied":{
position_id: ids,
index_position: indexes
}
}
}
).exec(function (err, result) {
res.json({ results: result });
});
});
getting following error:
One request can return only one response.
In your case, you should first create array of user ids, and then query the user collection where you are searching all users whose id matches one element of that array. Here is a solution:
app.post('/rankcandidates', function(req, res){
var array = JSON.parse(req.body.array);
var ids = array[0].map(function(element) { return element._id })
User.find({"_id" : { "$in" : ids }})
.exec(function (err, results) {
res.json(results);
});
});

add extra parameters using mongoose transform on aggregate

I am new to node.js. I am using mongoose, node.
var schema = new Schema({
name: String,
});
schema.options.toObject.transform = function (doc, ret, options) {
tmp = ret;
tmp['abcd'] = 'abcd';
return tmp;
}
module.exports = mongoose.model('modelName', schema);
//my route.js
modelName.find({}, function (err, docs) {
docs.forEach(function(doc) {
console.log(doc);
console.log("doc['abcd'] is available");
});
});
modelName.aggregate({"$match":{"_id":{"$ne":0}}} function (err, docs) {
docs.forEach(function(doc) {
console.log(doc);
console.log("doc['abcd'] is not available");
});
});
I am using above code to add extra parameters while rendering. But when i use aggregate, it is not working. Is there any way to add extra parameters while rendering even on aggregate also? Thanks in advance.

mocha test function with mongoose call

Having some problems unit testing the below code, I'm unsure whether it is possible or not due to the way it is coded.
storeModel.js
var storeSchema = new Schema({
storeId : { type: String, index: true},
storeName : String
});
var model = mongoose.model('store', storeSchema);
var findStoresById = function(ids, callback) {
model.find({ storeId: { $in: ids }}, function (err, result) {
if (err) callback(err);
callback(err, result);
});
};
return {
findStoresById: findStoresById,
schema: storeSchema,
model: model
};}();
Which i test like so..
it('will call "findStoresById" and return matched values [storeId: 1111] ', function (done) {
storeModel.findStoresById(['1111'], function(err, store) {
assert.equal(store[0].storeId, '1111');
assert.equal(store[0].storeName, 'StoreName');
assert.equal(err, null);
done();
});
});
However the problem when i implement the following code within a separate function:
get: function (req, res) {
if (req.query.storeIds) {
var ids = req.query.storeIds.split(',');
storeModel.findStoresById(ids, function(err, stores) {
if (err) {
return res.send(err);
}
if (_.isEmpty(stores)) {
var error = {
message: "No Results",
errorKey: "XXXX"
}
return res.status(404).json(error);
}
return res.json(stores);
}); ...
How can i unit test this, i dont want to mock it as there is functionality in the "findStoreById" that needs testing, or is a refactor needed? suggestions?
I'd contest that you actually should be stubbing findStoreById because in not doing so get cannot strictly be unit tested, in that it's not isolated and could fail through no fault of its own. Seeing as the functionality you'd want to test lies within the callback of findStoreById and not the method itself, we can happily stub it and use the yields method of sinon to invoke its callback accordingly.
Note that, if you're testing routes, it's preferable to use supertest else you'll have a lot of mocking of request and response methods on your hands. Therefore, for example:
var request = require('supertest');
var express = require('express');
// stub database method
sinon.stub(storeModel, 'findStoresById');
// create a test app/route to which we direct test requests
var app = express();
app.get('/', myRouteFunction);
it('sends a 404 error when no stores are found', function(done) {
// use the second argument of `yields` to pass a result to the callback
storeModel.findStoresById.yields(null, []);
request(app).get('/').expect(404, done);
});
it('responds with any stores found', function(done) {
// pass an array of found stores to the callback
storeModel.findStoresById.yields(null, [{_id: 1}]);
request(app).get('/').end(function(err, res) {
if(err) return done(err);
assert.deepEqual(res.body, [{_id: 1}]);
done();
});
});
If what you want is test static's and method's of certain Mongoose model, I would recommend you to use sinon and sinon-mongoose.
But first, some tips for your code
var storeSchema = new Schema({
storeId : { type: String, index: true},
storeName : String
});
// 1) If you will callback with the same 'err' and 'result', pass the callback directly
function findStoresById(ids, callback) {
// Instead of this...
this.find({ storeId: { $in: ids } }, function (err, result) {
if (err) callback(err);
callback(err, result);
});
// Use this... it's the same, but shorter
this.find({ storeId: { $in: ids } }, callback);
}
// 2) Declare a static method on your model, instead of export manually (see Monggose documentation for more info)
storeSchema.static('findStoresById', function (ids, callback) {
});
// 3) Create your model after the statics were declared, and use CamelCase
var model = mongoose.model('Store', storeSchema);
// 4) Export just your model
// If you want the model -> var Store = mongoose.model('Store')
// If you want the schema -> var schema = Store.schema
// If you want to call your method -> Store.findStoresById(...)
module.exports = model;
Then, to test the method findStoresById
var sinon = require('sinon');
require('sinon-mongoose');
var Store = mongoose.model('Store');
sinon.mock(Store)
.expects('find').withArgs({ storeId: { $in: ['id1', 'id2'] } })
.yields(null, 'SUCCESS!');
Store.findStoresById(['id1', 'id2'], function (err, res) {
assert(res, 'SUCCESS!');
});
You can find working (and simple) examples on the sinon-mongoose repo.

how to populate schemaref in mongoose using callbacks?

I have a list of items retrieved from mongoose each with a list object referenced, but I need to somehow populate the item.list.user object associated with each list so I can use them in my template as item.list.user.username.
Item.find().populate('list').exec(function(err, items){
items.forEach(function(item){
User.findById(item.list.user), function(err, user){
item.list.user = user;
});
});
//how to get back here from User.findById() so I can render?
res.render('index', { items: items });
});
There are a few ways to go about this. The main issue is that you are assuming that the data will be populated when you render the template. That is not always the case, and you can and should always assume that any time you are doing asynchronous functions, that it won't be done unless you wait until each function call is completed.
Here is a naive way to make sure the data is available for render.
Item.find().populate('list').exec(function (err, items) {
var len = items.length
, populatedItems = [];
items.forEach(function(item, i){
User.findById(item.list.user, function (err, user) {
item.list = item.list.toObject();
item.list.user = user;
populatedItems.push(item);
if (i + 1 === len) {
res.render('index', { items: items });
}
});
});
});
Though that is not very efficient and makes unnecessary database calls. It is also harder to reason about in my opinion.
Item.find().populate('list').exec(function (err, items) {
var itemMap = {}
items.forEach(function (item, i) {
// Map the position in the array to the user id
if (!itemMap[item.list.user]) {
itemMap[item.list.user] = [];
}
itemMap[item.list.user].push(i)
item.list = item.list.toObject()
});
// Can pull an array of user ids from the itemMap object
User.find({_id: {$in: Object.keys(itemMap)}}, function (err, users) {
users.forEach(function (user) {
itemMap[user._id].forEach(function(id) {
// Assign the user object to the appropriate item
items[id].list.user = user;
})
});
res.render('index', { items: items });
});
});
After further discussion with you on IRC and troubleshooting the following is a working example for your particular case.
Item.find().populate('list').exec(function (err, items) {
var itemIds = [];
items.forEach(function (item) {
itemIds.push(item.list.user)
});
// Can pull an array of user ids from the itemMap object
User.find({_id: {$in: itemIds}}, function (err, users) {
var userMap = {}
users.forEach(function (user) {
userMap[user._id] = user
});
res.render('index', { items: items, userMap: userMap });
});
});

Resources