Updating nested object in mongoose - node.js

I have searched many questions on nested objects, but all I found where related to array[s].
I am looking for a updating simple nested object in mongoose.
From here http://mongoosejs.com/docs/guide.html
there is an example schema :
var blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
Once created a document,
How can I change the favs number later on?
There is no document for the same that I could find.
This is what I did:
blog.findById(entityId, function(err, mainDoc){
if(err || !mainDoc) return next(err || 'Document not found');
var subDoc = mainDoc['meta'];
if(subDoc){
subDoc = _.extend(subDoc, { favs : 56 }); //_ lib already available
console.log(mainDoc.get('meta')); //Prints the updated result with favs = 56 OK
mainDoc.save(function(err, doc){
console.log(doc.get('meta')); // prints the updated results with favs = 56 OK
});
} else next('Not found');
});
Everything works file and all console gives the desired result.
But when I switch to mongoose console and query the document, I do not get the updated result.
I know there can be other ways to achieve the same, but I am only looking for what I am doing wrong in this particular code.
Why the console, after saving document, gives unmatched data from database?
Upon enabling the mongoose debug option, I found the in query there is no such data to be updated. Query fires with blank $set. { $set : {} }

If you just want to change the value of favs, you can use a simpler query:
blog.findByIdAndUpdate(entityId, {$set: {'meta.favs': 56}}, function(err, doc) {
console.log(doc);
});

Hope I ain't late and will be able to help someone. This Works with deep nested objects as well. No limitations.
const updateNestedObjectParser = (nestedUpdateObject) => {
const final = {
}
Object.keys(nestedUpdateObject).forEach(k => {
if (typeof nestedUpdateObject[k] === 'object' && !Array.isArray(nestedUpdateObject[k])) {
const res = updateNestedObjectParser(nestedUpdateObject[k])
Object.keys(res).forEach(a => {
final[`${k}.${a}`] = res[a]
})
}
else
final[k] = nestedUpdateObject[k]
})
return final
}
console.log(updateNestedObjectParser({
a: {
b: {
c: 99
},
d: {
i: {
l: 22
}
}
},
o: {
a: 22,
l: {
i: "ad"
}
}
}))

The problem is that you can't do anything with data from mongoose once you've got it other than sending it to the client.
HOWEVER, there is the lean method that makes it so you can then update the info and do whatever you want with it.
That would look like this:
blog.findById(entityId).lean().exec(function (err, mainDoc) {
if (err || !mainDoc) {
return next(err || 'Document not found');
}
var subDoc = mainDoc.meta;
if(subDoc){
subDoc.favs = 56;
blog.update({_id: entityId}, mainDoc, function(err, doc){
console.log(doc.get('meta'));
});
} else {
next('Not found');
}
});

Related

node js , mongodb : check if an object already exist

I'am newbie to nodejs and mongodb, so how can I check if an object already exist in the collections , Note that my field type in the schema is object or JSON
const BillSchema = mongoose.Schema(
{
content: {
type: Object //or JSON
},
}
);
const Bill = module.exports = mongoose.model('Bill', BillSchema);
module.exports.addBill = function (newBill, callback) {
//Check for all bill titles and content, if newBill doesn't exist then add else do nothing
Bill.count({ content: newBill.content }, function (err, count) {
//count == 0 always ???
if (err) {
return callback(err, null);
} else {
if (count > 0) {
//The bill already exists in db
console.log('Bill already added');
return callback(null, null);
} else { //The bill doesnt appear in the db
newBill.save(callback);
console.log('Bill added');
}
}
});
}
One Of Nice Question You asked, I was suppose to achieve the same task before, I make the use of mongoose-unique-validator third party npm Package, & plugin to our schema
https://www.npmjs.com/package/mongoose-unique-validator
npm install mongoose-unique-validator
var uniqueValidator = require('mongoose-unique-validator');
const BillSchema = mongoose.Schema(
{
content: {type:Object , unique:true },
}
);
BillSchema.plugin(uniqueValidator, {message: 'is already taken.'});
Usage:
module.exports.addBill = function (newBill, callback) {
newBill.save(callback);
}
I Hope If this work for you too.

Query top level array of subdocs with MONGOOSE, to only return N most recent subdocuments in array

I'm looking for the conditions, fields, etc that i would need in a query, or an async flow to query a single document which has an array of subdocuments at the top-level.
Model:
let postSchema = new mongoose.Schema({
date : {type: Boolean, require: true},
message : {type: String, require: true}
});
let Post = new mongoose.Schema({
locid: {type: String, unique: {indexed: true}, require: true},
posts : {type: [postSchema], require: false}
});
Essentially, I would provide a Locid value as shown above into a function that i'm hoping would look like this:
Post.methods.getLastTwentyPostsForOne = function (locid) {
return new Promise(function (values, error) {
let p = new Post();
p.find({"locid" : locid}, {/*...the conditions and elements here...*/}).exec()
.then(function (found) {
//..would return the last 20 posts, or less if there are less...
}, function (err) {
//..handle errors..
})
})
};
The most basic way i can think of doing this, would be to just fetch the whole array, and since mongo stores entries one after the other chronologically, to just parse the array and appending the last 20 or less posts contained in it into a final array (with a second promise-based function).
this makes the previous method, look like this:
function returnLastTwenty(posts) {
return new Promise(function (results) {
if (posts.length === 0) {
return results([]);
}
if (posts.length <= 20) {
return results(posts);
}
let length = posts.length;
var next = [];
for (i = length - 21; i < (length -1); i++) {
next.append(posts[i]);
}
if (next.length === 20) {
return results(next);
} else {
console.log('Some Error: found more than 20 posts, but parsed less');
return results(next)
}
});
}
Post.methods.getLastTwentyPostsForOne = function (locid) {
return new Promise(function (values, error) {
let p = new Post();
p.find({"locid" : locid})
.then(function (found) {
if (found.length === 1) {
returnLastTwenty(found[0].posts)
.then(function (results) {
return values(results)
})
} else {
return error("Didn't find a unique document for locid: " + locid);
}
}, function (err) {
return error(err)
})
})
};
While this does work.. the for loop and second async method seems too much.. Any clues as to how to achieve this with mongoose with a single query?
Update - Answer
Thanks to Ratan's comment, the following seemed to do the trick.
function getPosts(amount, asOfIndex, locid) {
return new Promise(function (posts, none) {
Post.find({'locid' : locid},
{'posts' :
{"$slice" : [asOfIndex, amount]}
}
)
.exec()
.catch(function (err) {
return none(err);
})
.then(function (found) {
if (found.length === 0 || found.length > 1) return none(); //This can be changed for more specific error handling
return posts(found[0].posts);
});
});
}
With index being an arbitrary Number passed through. Which could be an index from an API call, etc. Where asOfIndex should be negative in order to slice from the bottom of the array, and positive from the top (https://docs.mongodb.com/manual/reference/operator/projection/slice/). Slicing a given interval then just becomes a question of playing with the values of asOfIndex and amount.
Also while doing some digging, found a way to query the date element of the subdocs inside the array based on a date interval thanks to the following posts:
https://stackoverflow.com/a/29026860/7183483
and
https://stackoverflow.com/a/22287745/7183483 . For those who might want something like it!
function getPostsBetween(recentDate, farthestDate, forLocid) {
return new Promise(function (posts, none) {
Post.aggregate(
{'$match': {'locid': forLocid}},
{'$unwind' : '$posts'},
{'$match' :
{'$and': [
{'posts.date': {'$gt': new Date(farthestDate.toISOString())}},
{'posts.date': {'$lt': new Date(recentDate.toISOString())}}
]
}
},
{"$group" : {
'_id' : '$_id',
"posts" : {'$push' : '$posts'}
}
})
.exec()
.catch(function (err) {
return none(err);
})
.then(function (found) {
if (found.length === 0 || found.length > 1) return none();
return found(found[0].posts);
})
})
}

one query inside another MongoDB hangs

I am trying to get only get Notes [from the notes collection] that come from meetings [from the meetings collection] that don't contain the word 'test' in them:
function getNotes(done) {
noteSchema.find({}).exec((err, notes) => {
var numNotes = 0;
async.each(notes, (n, next) => {
userSchema.findById(n.userId, (err, user) => {
if (err || !user) { next(); return; }
var emailsStr = utils.getEmailsString(user.emails);
if (!utils.toSkipEmail(emailsStr)) {
meetingSchema.findById(n.meetingId, (err, meeting) => {
if (err || !meeting) { next(); return; }
if (meeting.name.displayValue.indexOf('test', 'Test') == -1) {
numNotes++;
}
next();
});
}
})
}, (err, result) => {
console.log(util.format('Total Number of Notes: %d', numNotes));
done(null);
});
});
}
The code works fine without adding in the lines to find the meetings by ID. It hangs at that point.
For reference, here is the start of a function that comes later to filter out any 'test' or 'Test' containing meetings.
function getMeetings(done) {
meetingSchema.find({
'name.displayValue': { '$regex' : '(?!.*test)^.*$' , '$options' : 'i' }
}).exec((err, meetings) => {
Relevant lines of Notes Schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var noteSchema = mongoose.Schema({
meetingId: {type: String, default: ''},
});
exports.Note = mongoose.model('Note', noteSchema);
The meeting schema has no notes field.
So, if I were going to go after a solution like this, where I wanted to get all notes that weren't part of a meeting w/ the word 'test' in the name, I'd probably go the other way with it, unless there's a whole ton of notes you could just get all notes and populate them with their meetings, and then do the filtering. assuming your NoteSchema defines something like:
meeting : { type: ObjectId, ref: 'Meeting'}
then in your query you could do (given the notATest function that returns true or false appropriately:
Note.find({}).populate('meeting').exec((e, n) => {
_.omit(n, (note) => { return notATest(note.meeting.name); });
});
Alternatively, you could search for all meetings that are not a test first, and call .populate('notes') on them, if the 'ref' goes the other way.

FindOneAndUpdate not updating nested field with passed in parameters

I am trying to create a service that can be used to update nested fields in a Mongoose model. In the following example I am trying to set the field 'meta.status' to the value 2. This is the service:
angular.module('rooms').factory('UpdateSvc',['$http', function($http)
{
return function(model, id, args)
{
var url = '/roomieUpdate/' + id;
$http.put(url, args).then(function(response)
{
response = response.data;
console.log(response.data);
});
}
}]);
This is how it is called in the controller:
var newStatus = {'meta.$.status' : 2};
var update = UpdateSvc("roomie", sessionStorage.getItem('userID'), newStatus);
And this is the model:
var RoomieSchema = new Schema(
{
meta:
{
created:
{
type: Date,
default: Date.now
},
status:
{
type: Number,
default: '1',
}
}
}
And this is the route:
app.put('/roomieUpdate/:id', function(req,res)
{
var id = req.params.id;
Roomie.findOneAndUpdate(
{_id: mongoose.Types.ObjectId(id)},
req.body,
{ new : true },
function(err, doc)
{
if(err)
{
console.log(err);
}
res.json(doc);
console.log(doc);
});
});
The argument is received correctly, but I can't seem to get this to work. I am not even getting an error message. console.log(doc) simply prints out the object and the field meta.status remains '1'. I have done a direct Mongo search on the target object to make sure that I wasn't just reading the old document. I've tried a great many things like separating the key and value of req.body and use {$set:{key:value}}, but result is the same.
findOneAndUpdate() by default will return the old document, not the new (updated) document.
For that, you need to set the new option:
Roomie.findOneAndUpdate({
_id : mongoose.Types.ObjectId(id)
}, req.body, { new : true }, function(err, doc) {
...
});
As it turns out, var newStatus = {'meta.$.status' : 2}; should have been var newStatus = {'meta.status' : 2}; The document now updates correctly.
The reason the $ was there in the first place was probably based on this thread:
findOneAndUpdate - Update the first object in array that has specific attribute
or another of the many threads I read through about this issue. I had tried several solutions with and without it, but couldn't get anything to go right.

Accessing properties of object/cursor returned from .find and .forEach in mongodb with nodejs

changed schema and everything went crazy (see changes below). now accessing properties from .find() and cursor.forEach() is returning 'undefined' in backend:
EDIT: have found
.find().lean().exec(callback)
allows access to properties in callback but hard to do anything with them and that to access properties by doing
doc._doc.property
works in callbacks:
.find(function(err,doc){for (i in docs){doc=docs[i]; console.log(doc._doc.property)}}
and .forEach(function(doc){console.log(doc._doc.property)}:
My schema once looked like this
for collection of people
{
name: String,
v: Types.ObjectId, ref: V //shorthand
r: {
e: [{}],
u: [{}]
}
}
now it looks like this
var people = new mongoose.Schema (
{
name: String,
v: {type: mongoose.Schema.Types.ObjectId, ref: V}
r: {
e: [{type: mongoose.Schema.Types.ObjectId, ref: R}],
u: [{type: mongoose.Schema.Types.ObjectId, ref: R}]
}
}
)
mongoose.model('people',people);
for collection of r
var collR = new mongoose.Schema({}, {strict:false})
mongoose.model('R',collR)
nodejs controller 1:
module.exports.getProducts = function (req, res) {
people.find(req.query)
.populate('v r.e r.u')
.exec(function (err, data) {
if (err) {sendJsonResponse(res,400,err)}
else {
data.forEach(function(single){
single.r.e.forEach(function(sing){
console.log(sing) //defined, and i saw rating, and its defined
console.log(sing.rating); //undefined
// do something with sing.rating but it's undefined here
})
})
sendJsonResponse(res,200,data); //not undefined on frontend success callback
}
});
};
node controller 2:
module.exports.getProducts = function (req, res) {
people.find(req.query)
.populate('v r.e r.u')
.exec(function (err, data) {
if (err) {sendJsonResponse(res,400,err)}
else {
data.forEach(function(single){
R.find({person: single.name}, function (err, dat) {
dat.forEach(function(sing){
console.log(sing) //defined and rating defined
console.log(sing.rating); //undefined ugh.
//do something with rating but cant bc undefined here
})
})
})
//if i send data back here, in success callback, data[i].r.e[j].rating is defined for all i and j, whaaa!?!
}
});
};
one of the sing's logged from the cursor.forEach loop---
{_id: 1254357653, name: peep, rating: 6, type: some type}
EDIT:
ya so:
collection.find(query).exec(function(err,docs) {
docs.forEach(function(singleDoc) {
console.log(singleDoc._doc.property); //DEFINED, bad boyz 4 lyfe *_*
})
})
so i finally decided to console.log the darn keys of the document returned from a cursor.forEach
this also returns defined:
collection.find(query).lean().exec(function(err,docs) {
console.log(docs[i].property); //for all i, THEY'RE DEFINED!!!!! wooo
})
well now another issue pops up when i try to do an update inside a find
collection.find(query).exec(function(err,docs) {
if (err) {return errorHandler(err)};
var doc = docs[0];
var captainKeyes = Object.keys(req.body);
for (k = 0 ; k < captainKeyes.length ; k++) {
//update the doc key/value pairs with what is sent in req.body
doc._doc[captainKeyes[k]] = req.body[captainKeyes[k]];
//from above, learned to access properties captainKeyes[k], you have to first access
//the hidden property _doc to get to actual doc
}
doc.save()
//old doc is still in db, damn. and all this used to work before
//we added that R collection :(
})
I changed the schema for the collection R to have some keys, changing it from just an empty object with strict: false.
from {{},strict:false} to {{name: String, rating: Number, person: String},strict:false}
now i dont have to use _doc, wooohoooo, and all the queries works normally again.
moral of the story, i didn't really understand how to implement a schemaless collection properly, and then stuff got cray

Resources