Pushing into nested subdocument arrays in mongoose - mongodb

I have a rather complex db:
var bookSchema = mongoose.Schema({
content: {
page: [pageSchema]
},
//otherThings
});
//my book has pages
var pageSchema = mongoose.Schema(
{
comments: [commentSchema]
},
{_id: false}
);
//comments have replies.
var commentSchema = mongoose.Schema({
comment: String,
replies: [this]
});
I am trying to add a reply to a comment.
I have the ID of the comment to which I want to add the reply.
But how?
I tried:
Book.findOneAndUpdate(
{
'content.page.comments._id': ID
},
{
$push: {'content.page.$.comments.$.replies': myReply}
},
function (err) {
//...
}
)
But that is impossible because I cant use a positional($) more than once.
I'm trying to figure this one out for hours now.
Any advice?

Use simple way - commentsSchema
Comment.update({_id: ID}, {$push: {replies: myReply}}, function (err) {});
This will update comment model, and the book model will get updated too

Related

Mongoose - which update function will also generate id for new property?

I have a subdocument inside a dosument and when I update my document, I want to generate ObjectId's. Both update and findByIdAndUpdate dont do that. I actually have to manually do that in my controller. Is there any way i can make it work?
const productSchema = new mongoose.Schema({
...
reports: [ReportSchema]
})
const ReportSchema = new mongoose.Schema({
...
info: String,
date: date
})
my controller:
updateProduct: async (req, res) => {
const product = await Product.update({id: id}, {
$push: {
report: {
info: info,
date: new Date(),
//_id: mongoose.Types.ObjectId() // only this works
},
}, $set: {isReport: true}}
)
res.status(200).send()
}
}

mongoose .findById & .find return object & array, causes compatibility issue

When the user visits a certain page of my App, the Component dispatches an action to fetch information. Namely, the action performs the following operations:
Base.find({req.params.id})
BaseUser.find({ baseId: req.params.id }) **returns multiple docs**
Message.find({ baseId: req.params.id }) **returns multiple docs**
The operation happens in this order. I could query the first one via .findById, but for uniformity of the problem I chose .find(). The problem now is that the results of
Promise.all([
Base.find({ _id: req.params.id }),
BaseUser.find({ baseId: req.params.id }),
Message.find({ baseId: req.params.id })
])
come in an array, like so:
[
[ { created: 2018-08-29T23:59:35.380Z,
_id: 5b8741151985662f10d04fdb,
creatorId: 5b86f7970cd98b2004969bf0,
title: 'testBase1',
} ],
[ { created: 2018-08-30T00:57:57.764Z,
acceptedMembership: true,
isCreator: true,
_id: 5b8741151985662f10d04fdc,
userId: 'tester1',
baseId: 5b8741151985662f10d04fdb }
],
[ { created: 2018-08-30T00:58:09.182Z,
_id: 5b8741211985662f10d04fdd,
baseId: 5b8741151985662f10d04fdb,
content: 'testMessage1' }
]
]
This quite obviously causes problems when further trying to map/filter/res.json() the data. Is there any known way to return this in a single array, or even better, pass it to the front-end (redux action) as an object? Does anyone know of a better solution which handles this problem slightly differently, and prevents me from fetching each of those methods on subcomponents ?
update:
I have now constructed this, which is fairly ugly to look at:
let completeObject = {
base: {},
users: [],
messages: []
};
Base.findById(req.params.id)
.then(data => {
completeObject.base = data;
return data;
})
.then(data => {
BaseUser.find({ baseId: req.params.id })
.then(data => {
completeObject.users = data;
return data;
})
.then(data => {
Message.find({ baseId: req.params.id }).then(data => {
completeObject.messages = data;
return res.json(completeObject);
});
});
})
Why don't you setup ref in the Base model to the BaseUser and Message and then use populate to fill those arrays and get one object as result filled with the arrays of BaseUser and Message?
From what I see you key on the req.params.id which means you have a cross-reference between those collections anyway.
Here is an example:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var BaseSchema = Schema({
_id: Schema.Types.ObjectId,
creatorId: Schema.Types.ObjectId,
title: String,
users: [{ type: Schema.Types.ObjectId, ref: 'User' }],
messages: [{ type: Schema.Types.ObjectId, ref: 'Message' }],
});
var UserSchema = Schema({
_id: Schema.Types.ObjectId,
acceptedMembership: Boolean,
isCreator: Boolean,
userId: String,
baseId: Schema.Types.ObjectId
});
var MessageSchema = Schema({
_id: Schema.Types.ObjectId,
baseId: Schema.Types.ObjectId,
content: String
});
var Base = mongoose.model('Base', BaseSchema);
var User = mongoose.model('User', UserSchema);
var Message = mongoose.model('Message', MessageSchema);
Now that the schemas are defined (and you added some records) you could find a Base by _id and populate users and messages:
Base.
findOne({ _id: req.params.id }).
populate('users').
populate('messages').
exec(function (err, base) {
if (err) return handleError(err);
console.log(base);
});
You should check the mongoose documentation on how to save / populate references etc.

mongodb relations find one with all their related

I know how to get data with their related
var UserSchema = new Schema({
username: String
});
var PostSchema = new Schema({
title: String,
author: {type: Schema.Types.ObjectId, ref: 'User'}
});
...
Post.findOne({_id: id})
.populate('author')
.exec(function(error, result) {
// do stuff
})
but how to do the opposite?
I mean when I want a user, with all their posts with single query?
Try adding a filter step after the query returns that manually filters out documents which don't have any posts that matched the populate criteria:
var username = "foo";
Post.find()
.populate('author', null, {username: username})
.sort({'_id': 1})
.exec(function (err, posts) {
posts = posts.filter(function(post){
return post.author.length;
});
res.send(posts);
});

remove nested object references mongoose

I have several mongoose models in my application - assignment, submisssion, and file.
var assignmentSchema = new Schema({
submissions:[{
type: mongoose.Schema.Types.ObjectId,
ref: 'Submission'
}]
});
var submissionSchema = new Schema({
assignment: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Assignment'
},
files: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'File'
}],
})
var fileSchema = new Schema({
submission: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Submission'
}
})
I defined a remove hook for assignments, so when an assignment is removed from my mongodb, all submissions are deleted too.
assignmentSchema.pre('remove', function (next) {
var assignment = this;
assignment.model('Class').update(
{_id: {$in: assignment.classes}},
{$pull: {assignments: assignment._id}},
{multi: true},
next
);
//will delete all submissions that were in the assignment
assignment.model('Submission').remove(
{_id: {$in: assignment.submissions}}).exec();
next
});
It works ok, but I also want to remove all files that are related to deleted submissions. How can I achieve this?
You have to setup a pre remove hook for your submission documents.
Then in your assignmentSchema loop through the assignment.submissions array, find each assignment document and execute a remove on that document.
Here is an example. Not tested.
assignmentSchema.pre('remove', function(nextAction) {
var assignment = this;
assignment.model('Class').update({
_id: {
$in: assignment.classes
}
}, {
$pull: {
assignments: assignment._id
}
}, {
multi: true
},
next
);
Async.each(assignment.submissions, function(subId, next) {
submissionSchema.findById(subId).exec(function(err, submission) {
submission.remove();
next();
})
}, function() {
nextAction();
})
});
submissionSchema.pre('remove', function (next) {
///remove files here.
})

Populating nested array with ObjectIDs

Trying to populate an array of ObjectID's within my schema. I've looked around at similar answers but it seems everyone is doing it slightly differently and I haven't been able to find a solution myself.
My schema looks like this:
var GameSchema = new Schema({
title: String,
description: String,
location: String,
created_on: { type: Date, default: Date.now },
active: { type: Boolean, default: true },
accepting_players: { type: Boolean, default: true },
players: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
admins: [{
type: Schema.Types.ObjectId,
ref: 'User'
}]
});
So far I've been trying to populate it like this, which obviously isn't working
exports.getAdmins = function(req, res) {
Game.findById(req.params.id)
.populate('admins')
.exec(function(err, game) {
return res.json(200, game.admins);
});
};
I hate to add to the list of population questions, but I've looked at many and haven't found a solution. Any help is greatly appreciated!
Edit:
Here's how I am adding admins to the document
// Add admin to game
exports.addAdmin = function(req, res) {
Game.findByIdAndUpdate(
req.params.id,
{ $push: { 'admins': req.params.user_id }},
function(err, game) {
if(err) { return handleError(res, err); }
if(!game) { return res.send(404); }
return res.json(200, game.admins);
});
};
Well I went back to mongoose documentation, and decided to change how I looked up a game by an ID and then populated the response.
Now my working function looks like this:
// Returns admins in a game
exports.getAdmins = function(req, res) {
Game.findById(req.params.id, function(err, game) {
if(err) { return handleError(res, err); }
if(!game) { return res.send(404); }
Game.populate(game, { path: 'admins' }, function(err, game) {
return res.json(200, game);
});
});
};
The issue I was having was that I was trying to call the .populate function directly with the .findById method, but that doesn't work because I found on mongoose's documentation the the populate method need the callback function to work, so I just added that and voila, it returned my User object.
to populate an array, you just have to put model name field after path field like this :
Game.findById(req.params.id)
.populate({path: 'admins', model: 'AdminsModel'})
.exec(function(err, game){...});
it works perfectly on my projects...