one query inside another MongoDB hangs - node.js

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.

Related

How do I keep mongo's array index from changing during an update?

I am trying to update an array within my object. However, every time I send the post call, the index in the array changes.
I have tried using $set and manually updating the array... but the index on the array keeps changing.
Here is the model:
const MappedImageSchema = new Schema({
imageUrl: {type: String, required: true},
name: {type: String, required: true},
areas:[
{
name: {type: String},
shape: {type: String},
coords:[{type: Number}],
}
]
});
module.exports = MappedImage = mongoose.model('mappedImages', MappedImageSchema)
Here is the code that performs the update:
// #route POST api/maps/:id/areas
// #desc add an area to a map (by map id)
// #access Private
router.post('/:id/areas/:id_area', passport.authenticate('jwt', { session: false }),
(req, res) => {
MappedImage.findById(req.params.id)
.then(map => {
// get all of the areas from the map...
var allAreas = map.areas;
// now get the index of the area we are going to update
const areaIndex = map.areas.map(item => item._id.toString()).indexOf(req.params.id_area);
// update the information
var coords = req.body.coords.split(',');
const updatedArea = {
name: req.body.name,
shape: req.body.shape,
coords: coords,
};
// set the updated information in the correct map area
allAreas[areaIndex] = updatedArea;
var query = {_id: req.params.id}; // this is the MAP id
var update = {$set: {areas:allAreas}}; // update the areas
var options = {new: true};
MappedImage.findOneAndUpdate(query, update, options)
.then(map => res.json(map))
.catch(err => res.status(404).json({ mapnotfound: err }));
})
.catch(err => res.status(404).json({ mapnotfound: 'Map not found while updating area' }));
}
);
Here is the data BEFORE the call
{
"_id": "5c5c69dda40e872258b4531d",
"imageUrl": "url test",
"name": "name test",
"areas": [
{
"coords": [1,2,3,4,5,6],
"_id": "5c5c8db2f904932dd8d4c560", <---- _id changes every time !
"name": "area name",
"shape": "poly"
}
],
"__v": 3
}
Here is the Postman call I make:
The result of the call is the name gets changed... but so does the index... making the next call fail with "no area found with that index".
What is perplexing about this problem is the _id for the map does not get updated when I run this code:
router.post('/:id', passport.authenticate('jwt', { session: false }),
(req, res) => {
var query = {_id: req.params.id};
var update = {imageUrl: req.body.imageUrl, name: req.body.name};
var options = {new: true};
MappedImage.findOneAndUpdate(query, update, options)
.then(map => res.json(map))
.catch(err => res.status(404).json({ mapnotfound: err }));
});
Update 1
I tried using the areas index and updating just that area... but the _id changes with this code as well:
... same code all the way down to here
allAreas[areaIndex] = updatedArea;
// but instead of calling 'findOneAndUpdate'... call map save
map.save().then(map => res.json(map));
Update 2
I can't get this code to work as areas._id and areas.$ are undefined ?
var query = {_id: req.params.id, areas._id: id_area}; // this is the MAP id
var update = {$set: {areas.$: updatedArea}}; // update the area
Update 3
So, putting the _id in the updatedArea fixes this issue... but it "feels" wrong to do so: ( per eol answer )
const updatedArea = {
_id: req.params.id_area,
name: req.body.name,
shape: req.body.shape,
coords: coords,
};
Update 4
eol - thanks for the verification on the mongoDB side... If that solves the DB id problem... I just need to know why my query is failing. I tried this and all I see in the terminal output is "creating query"... I never see the "query" and it's definition... so something is wrong and I don't know how to figure out what. Here is what I have now:
console.log('creating query');
var query = {"_id": req.params.id, "areas._id": id_area};
console.log('query');
console.log(query);
Update 5
Figured it out why the query not being output, id_area is not defined... but req.params.id_area is !
console.log('creating query');
var query = {"_id": req.params.id, "areas._id": req.params.id_area};
console.log('query');
Update 6
Code is in... but it is still not working. A picture is worth a 1000 words... so here are two:
This one shows the areas ID is still changing:
Here is the code I have now:
console.log('Update area');
console.log('changing area ' + req.params.id_area);
//console.log(req.body);
const { errors, isValid } = mapValidators.validateAreaInput(req.body);
// Check Validation
if(!isValid){
return res.status(400).json(errors);
}
MappedImage.findById(req.params.id)
.then(map => {
// Check to see if area exists
if (
map.areas.filter(
area => area._id.toString() === req.params.id_area
).length === 0
) {
return res.status(404).json({ areanotfound: 'Area does not exist' });
}
console.log('area exists');
// get all of the areas from the map...
var allAreas = map.areas;
console.log('all areas');
console.log(allAreas);
// now get the index of the area we are going to update
const areaIndex = map.areas.map(item => item._id.toString()).indexOf(req.params.id_area);
console.log('area index');
console.log(areaIndex);
// update the information
var coords = req.body.coords.split(',');
const updatedArea = {
name: req.body.name,
shape: req.body.shape,
preFillColor: req.body.preFillColor,
fillColor: req.body.fillColor,
coords: coords,
};
console.log('updated area');
console.log(updatedArea);
// set the updated information in the maps areas
allAreas[areaIndex] = updatedArea;
console.log('creating query');
var query = {"_id": req.params.id, "areas._id": req.params.id_area};
console.log('query');
console.log(query);
var update = {$set: {"areas.$": updatedArea}};
console.log('update');
console.log(update);
var options = {new: true};
MappedImage.findOneAndUpdate(query, update, options)
.then(map => res.json(map))
.catch(err => res.status(404).json({ mapnotfound: err }));
})
.catch(err => res.status(404).json({ mapnotfound: 'Map not found while updating area' }));
Here is the terminal output:
You could try setting the _id property in the updatedArea object with the value of the area that you'd like to update. This would prevent creating a new id while using the $set operator. Something like this:
// now get the index of the area we are going to update
const areaIndex = map.areas.map(item => item._id.toString()).indexOf(req.params.id_area);
// update the information
var coords = req.body.coords.split(',');
const updatedArea = {
_id: id_area,
name: req.body.name,
shape: req.body.shape,
coords: coords,
};
...
Note that with the above solution you're always setting a new array, which is why new id's are generated.
You could also try updating the specific element in the array using the $ operator:
var query = {"_id": req.params.id, "areas._id": id_area}; // this is the MAP id
var update = {$set: {"areas.$": updatedArea}}; // update the area
See the screenshots below for an example (executing the commands in the mongodb-shell) where I'm trying to only update the second array element (i.e. with _id 5c5c8db2f904932dd8d4c561)

Can't find a easy way out of multiple async for each node js (sails)

So here's the deal :
I have an array of objects with a child array of objects
askedAdvices
askedAdvice.replayAdvices
I'm looping trough the parent and foreach looping trough the childs and need to populate() two obejcts (I'm using sails)
The child looks like :
askedAdvices = {
replayAdvices : [{
bookEnd : "<ID>",
user : "<ID>"
}]
}
So my goal is to cycle and populate bookEnd and user with two findOne query, but I'm going mad with the callback hell.
Here's the Models code :
AskedAdvices Model
module.exports = {
schema : false,
attributes: {
bookStart : {
model : 'book'
},
replayAdvices : {
collection: 'replybookend'
},
user : {
model : 'user',
required : true
},
text : {
type : "text"
}
}
};
ReplyBookEnd Model
module.exports = {
schema : false,
attributes: {
bookEnd : {
model : 'book'
},
user : {
model : 'user',
required : true
},
text : {
type : "text"
}
}
};
Here's the Method code :
getAskedAdvices : function(req, res) {
var queryAskedAdvices = AskedAdvices.find()
.populate("replayAdvices")
.populate("user")
.populate("bookStart")
queryAskedAdvices.exec(function callBack(err,askedAdvices){
if (!err) {
askedAdvices.forEach(function(askedAdvice, i){
askedAdvice.replayAdvices.forEach(function(reply, i){
async.parallel([
function(callback) {
var queryBook = Book.findOne(reply.bookEnd);
queryBook.exec(function callBack(err,bookEndFound) {
if (!err) {
reply.bookEnd = bookEndFound;
callback();
}
})
},
function(callback) {
var queryUser = User.findOne(reply.user)
queryUser.exec(function callBack(err,userFound){
if (!err) {
reply.user = userFound;
callback();
}
})
}
], function(err){
if (err) return next(err);
return res.json(200, reply);
})
})
})
} else {
return res.json(401, {err:err})
}
})
}
I can use the async library but need suggestions
Thanks folks!
As pointed out in the comments, Waterline doesn't have deep population yet, but you can use async.auto to get out of callback hell. The trick is to gather up the IDs of all the children you need to find, find them with single queries, and then map them back onto the parents. The code would look something like below.
async.auto({
// Get the askedAdvices
getAskedAdvices: function(cb) {
queryAskedAdvices.exec(cb);
},
// Get the IDs of all child records we need to query.
// Note the dependence on the `getAskedAdvices` task
getChildIds: ['getAskedAdvices', function(cb, results) {
// Set up an object to hold all the child IDs
var childIds = {bookEndIds: [], userIds: []};
// Loop through the retrieved askedAdvice objects
_.each(results.getAskedAdvices, function(askedAdvice) {
// Loop through the associated replayAdvice objects
_.each(askedAdvice.replayAdvices, function(replayAdvice) {
childIds.bookEndIds.push(replayAdvice.bookEnd);
childIds.userIds.push(replayAdvice.user);
});
});
// Get rid of duplicate IDs
childIds.bookEndIds = _.uniq(childIds.bookEndIds);
childIds.userIds = _.uniq(childIds.userIds);
// Return the list of IDs
return cb(null, childIds);
}],
// Get the associated book records. Note that this task
// relies on `getChildIds`, but will run in parallel with
// the `getUsers` task
getBookEnds: ['getChildIds', function(cb, results) {
Book.find({id: results.getChildIds.bookEndIds}).exec(cb);
}],
getUsers: ['getChildIds', function(cb, results) {
User.find({id: results.getChildIds.userIds}).exec(cb);
}]
}, function allTasksDone(err, results) {
if (err) {return res.serverError(err);
// Index the books and users by ID for easier lookups
var books = _.indexBy(results.getBookEnds, 'id');
var users = _.indexBy(results.getUsers, 'id');
// Add the book and user objects back into the `replayAdvices` objects
_.each(results.getAskedAdvices, function(askedAdvice) {
_.each(askedAdvice.replayAdvices, function(replayAdvice) {
replayAdvice.bookEnd = books[replayAdvice.bookEnd];
replayAdvice.user = users[replayAdvice.bookEnd];
});
});
});
Note that this is assuming Sails' built-in Lodash and Async instances; if you're using newer versions of those packages the usage of async.auto has changed slightly (the task function arguments are switched so that results comes before cb), and _.indexBy has been renamed to _.keyBy.

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.

sails js save many to many is only one way

i have two models:
user.js
module.exports = {
attributes: {
...
profile: {
model: 'Profile'
},
groups: {
collection: 'group',
via: 'users',
dominate: true
},
roles: {
collection: 'role',
via: 'users',
dominate: true
}
}};
and, group.js
module.exports = {
attributes: {
...
users: {
collection: 'user',
via: 'groups'
}
}};
when i try to add users to a group (when i select a group and add users to it), it works as it is supposed to,
var defer = q.defer();
baseDbContext.single(req, 'users')
.then(function(op){
if(!op.status || !op.obj) {
defer.resolve(notFound);
return;
}
op.obj.users = [];
_.each(req.users, function(item){
op.obj.users.add(item);
});
op.obj.save(function(err, obj){
if(err) defer.reject(operationResult().throwException(err));
else defer.resolve(operationResult().succeed());
});
});
return defer.promise;
but when i try to add groups to the user (when i select the user and add groups to it) it fails silently!!!
var defer = q.defer();
baseDbContext.single(req, 'groups')
.then(function(op){
if(!op.status || !op.obj) {
defer.resolve(notFound);
return;
}
op.obj.groups = [];
_.each(req.groups, function(item){
op.obj.groups.add(item);
});
op.obj.save(function(err, obj){
if(err) defer.reject(operationResult().throwException(err));
else defer.resolve(operationResult().succeed());
});
});
return defer.promise;
when i check it in sails console it shows :
throw new Error('Unknown rule: ' + ruleName);
Error: Unknown rule: dominate
this is a simple many to many insertion why would it fail?
(a note about code, the function baseDbContext.single finds a object based on its id and the second parameter is for populate)
Seems like you have a misprint, documentation says that the rule you need is writes as "dominant: true", not "dominate: true".

Updating nested object in mongoose

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');
}
});

Resources