How to iterate on mongoose subdocument array of objects - mongodb

Trying to implement conditional statement relying on subdocument array of objects, so i need to iterate over collection of users in database and check inside each user subdocument array of objects with findIndex as for javascript
Users collection
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true,
lowercase: true
}
friends: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
],
family: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
],
acquaintances: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
],
following: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
],
pendingFriendConfirmationData:[
{
storedUserId : {type: String},
choosenCategories: [{label: {type: String}, value: {type: String}}]
}
]
});
const Users = mongoose.model("Users", userSchema);
module.exports = Users;
now i can access Users collection with
db.Users.find()
my example result for
let filter = {"_id": userId}
let projection = {username: 1, friends: 1, family: 1, acquaintances: 1, following: 1, pendingFriendConfirmationData: 1}
db.Users.findOne(filter, projection, (err, user)=>{
console.log(user)
})
{
friends: [],
family: [],
acquaintances: [],
following: [],
_id: 5ca1a43ac5298f8139b1528c,
username: 'ahmedyounes',
pendingFriendConfirmationData: [
{
choosenCategories: [Array],
_id: 5ccb0fcf81a7944faf819883,
storedUserId: '5cc95d674384e302c9b446e8'
}
]
}
focusing on pendingFriendConfirmationData
the following screenshot from MongoDB Compass
I want to iterate over like this
let filter = {"_id": userId}
let projection = {username: 1, friends: 1, family: 1, acquaintances: 1, following: 1, pendingFriendConfirmationData: 1}
db.Users.findOne(filter, projection, (err, user)=>{
let data = user.pendingFriendConfirmationData
for(let i in data){
if(data[i].choosenCategories.findIndex(v => v.label === "friend") !== -1){
console.log("he is a friend")
}
}
})
How to iterate over pendingFriendConfirmationData and choosenCategories
like above
for now if i console.log(data) as following
db.Users.findOne(filter, projection, (err, user)=>{
let data = user.pendingFriendConfirmationData
console.log(data)
})
I get

I figured it out Faster Mongoose Queries With Lean
The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain JavaScript objects (POJOs), not Mongoose documents. In this tutorial, you'll learn more about the tradeoffs of using lean().
In my previous example the solution would be adding {lean: true}
db.Users.findOne(filter, projection, {lean: true}, (err, user)=>{
let data = user.pendingFriendConfirmationData
console.log(data)
})
also here
db.Users.findOne(filter, projection, {lean: true}, (err, user)=>{
let data = user.pendingFriendConfirmationData
for(let i in data){
if(data[i].choosenCategories.findIndex(v => v.value === "friends") !== -1){
console.log("he is a friend")
}
}
})
// he is a friend
Conclusion
to iterate over deeply nested subdocument array of objects you need to make sure
that you are working with plain JavaScript objects (POJOs) using lean()
db.Users.find().lean()

Related

MongoDB Query from multiple models / schemas and return in one field

I am using Nodejs and MongoDB, mongoose and expressjs, creating a Blog API having users, articles, likes & comments schema. Below are schemas that I use.
const UsersSchema = new mongoose.Schema({
username: { type: String },
email: { type: String },
date_created: { type: Date },
last_modified: { type: Date }
});
const ArticleSchema = new mongoose.Schema({
id: { type: String, required: true },
text: { type: String, required: true },
posted_by: { type: Schema.Types.ObjectId, ref: 'User', required: true },
images: [{ type: String }],
date_created: { type: Date },
last_modified: { type: Date }
});
const CommentSchema = new mongoose.Schema({
id: { type: String, required: true },
commented_by: { type: Schema.Types.ObjectId, ref: 'User', required: true },
article: { type: Schema.Types.ObjectId, ref: 'Article' },
text: { type: String, required: true },
date_created: { type: Date },
last_modified: { type: Date }
});
What I actually need is when I * get collection of articles * I also want to get the number of comments together for each articles. How do I query mongo?
Since you need to query more than one collection, you can use MongoDB's aggregation.
Here: https://docs.mongodb.com/manual/aggregation/
Example:
Article
.aggregate(
{
$lookup: {
from: '<your comments collection name',
localField: '_id',
foreignField: 'article',
as: 'comments'
}
},
{
$project: {
comments: '$comments.commented_by',
text: 1,
posted_by: 1,
images: 1,
date_created: 1,
last_modified: 1
}
},
{
$project: {
hasCommented: {
$cond: {
if: { $in: [ '$comments', '<user object id>' ] },
then: true,
else: false
}
},
commentsCount: { $size: '$comments' },
text: 1,
posted_by: 1,
images: 1,
date_created: 1,
last_modified: 1
}
}
)
The aggregation got a little big but let me try to explain:
First we need to filter the comments after the $lookup. So we $unwind them, making each article contain just one comment object, so we can filter using $match(that's the filter stage, it works just as the <Model>.find(). After filtering the desired's user comments, we $group everything again, $sum: 1 for each comment, using as the grouper _id, the article's _id. And we get the $first result for $text, $images and etc. Later, we $project everything, but now we add hasCommented with a $cond, simply doing: if the $comments is greater than 0(the user has commented, so this will be true, else, false.
MongoDB's Aggregation framework it's awesome and you can do almost whatever you want with your data using it. But be aware that somethings may cost more than others, always read the reference.

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.

Complex query mongoose - embedded documents array

I want to execute a query for getting all "group" documents that have the userId in there array of users.
I've tried several different ways of query, but I always get an empty array.
What am I doing wrong?
group.js
let mongoose = require('mongoose');
const Group = mongoose.Schema({
name: {
type: String,
required: true
},
users: [{
userId: {
type: mongoose.SchemaTypes.ObjectId,
ref: 'users',
required: true
},
userType: {
type: String,
required: true
},
userStatus: {
type: String,
required: true
}
}]
})
module.exports = mongoose.model('group', Group);
groupController.js
exports.getUserGroups = function (req, res) {
Group.find({
"users.userid": "req.user._id"
}, function (err, groups) {
if (err)
res.send(err)
res.json(groups);
});
}
Field names are case-sensitive, so "users.userid" should be "users.userId" instead:
Group.find({
"users.userId": "req.user._id"
}, ...
You can try using $in operator
db.collection.find({
"users.userId": {
$in: [
req.user._id
]
}
})

Mongoose pull ObjectId from array

i'm trying to do a pretty simple operation, pull an item from an array with Mongoose on a Mongo database like so:
User.update({ _id: fromUserId }, { $pull: { linkedUsers: [idToDelete] } });
fromUserId & idToDelete are both Objects Ids.
The schema for Users goes like this:
var UserSchema = new Schema({
groups: [],
linkedUsers: [],
name: { type: String, required: true, index: { unique: true } }
});
linkedUsers is an array that only receives Ids of other users.
I've tried this as well:
User.findOne({ _id: fromUserId }, function(err, user) {
user.linkedUsers.pull(idToDelete);
user.save();
});
But with no luck.
The second option seem to almost work when i console the lenghts of the array at different positions but after calling save and checking, the length is still at 36:
User.findOne({ _id: fromUserId }, function(err, user) {
console.log(user.linkedUsers.length); // returns 36
user.linkedUsers.pull(idToDelete);
console.log(user.linkedUsers.length); // returns 35
user.save();
});
So it looks like i'm close but still, no luck. Both Ids are sent via the frontend side of the app.
I'm running those versions:
"mongodb": "^2.2.29",
"mongoose": "^5.0.7",
Thanks in advance.
You need to explicitly define the types in your schema definition i.e.
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
linkedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }]
and then use either
User.findOneAndUpdate(
{ _id: fromUserId },
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
or
User.findByIdAndUpdate(fromUserId,
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
I had a similar issue. I wanted to delete an object from an array, using the default _id from mongo, but my query was wrong:
const update = { $pull: { cities: cityId }};
It should be:
const update = { $pull: { cities: {_id: cityId} }};

Populate does not retrieve the whole referenced object just the ids

I've been reading a few answers regarding this and yet I still can't get it to work.
My model objects aren't deeply nested and are quite simple. It's events that have a list of users attending them and users that have a list of events they've attended. like so:
let DinnerSchema = new mongoose.Schema({
date: {
type: Date,
unique: true,
timestamps: true,
required: true
},
title:{type: String, require: true},
attending: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
})
and the users:
let UserSchema = new mongoose.Schema({
email: {
type: String,
lowercase: true,
unique: true,
required: true
},
name:{ type: String, require: true },
password: {type: String ,required: true},
dinners: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Dinner'
}]
})
And for clarity here's the entire route that's using populate:
userpage.get('/', authCheck, (req, res) => {
const options = { _id: '57ebbf48bd6914036f99acc7' }
return Dinner
.findOne(options)
.populate('User', 'name') //I'VE TRIED ADDING 'name' BASED ON SOME ANSWERS I SAW
.exec((err, newDinner) => {
if (err) {
console.log(err)
res.status(400).end()
}
console.log(newDinner) // SHOW'S USERS ID'S BUT NO OTHER FIELDS
return res.json({
sucsess: true,
data: newDinner
})
})
})
If I understand correctly in the database itself there should only be a reference to the other model and not actually all of it's fields and the join happens with the populate. My db structure show's just the reference so that's ok.
I've tried specifying the name of the fields i'm after (the name field in this case) but that didn't work.
My population result always looks like the following and doesn't show any other fields except for the _id one:
{
_id: 57ebbf48bd6914036f99acc7,
date: 2016-09-27T22:00:00.000Z,
title: '1',
__v: 0,
attending: [ 57ebbcf02c39997f9cf26891, 57ebbdee098d3c0163db9905 ]
}
What am I screwing up here?
In mongoose populate receives 4 parameters.
path
selection(fields to be return) ,
condition
options (like {limit:10})
In your case you are not passing right path to populate. It should be
userpage.get('/', authCheck, (req, res) => {
const options = { _id: '57ebbf48bd6914036f99acc7' }
return Dinner
.findOne(options)
.populate('attending', 'name')
.exec((err, newDinner) => {
if (err) {
console.log(err)
res.status(400).end()
}
console.log(newDinner) // SHOW'S USERS ID'S BUT NO OTHER FIELDS
return res.json({
sucsess: true,
data: newDinner
})
})
})
Now it will return all the names of attending users.
you need to populate attending - that's your user reference in the dinner schema