How define conditional "Field Selection" in mongodb 'find()' query? - search

Is it possible to conditionally specify the fields returned by the query. Here is my use case: I have an object with nested user conversations as follows:
{
"_id" : "someId",
user_id: 'user1',
conversations:
[
{
user_id: 'user2',
comments:
[
{
user_id: 'user2',
text: 'Hi user1'
},
{
user_id: 'user1',
text: 'Hi user2'
},
]
},
{
user_id: 'user3',
comments:
[
{
user_id: 'user3',
text: 'Hi user1'
}
]
},
]
}
I would like to allow all users to search for and view all objects but not conversations they don't own. Something as follows:
findObj = function(criteria, user, callback) {
Object.find({criteria}, {conversation:
{
if (user_id == user.id || conversations[].user_id = user.id) {1} else {0}
} }
);
}
Thanks in advance for your help,
-Eric

You can get all users that have conversations that involve user X like so :
db.users.find({$or:[{user_id: X}, {'conversations.user_id':x}]})
However this will not do what you want. You're running into a problem with your schema. You have to remove conversations from the user objects and store them in a dedicated collection that allows for queries on specific conversations seperately.

Related

Mongoose populate query and unwrap the objects to obtain an array of strings

I have the following query with mongoose:
const user = await this.userModule.findById(id).populate('roles', 'label -_id');
The output will look like this:
{
_id: new ObjectId("61e2bc757644758d31578c7d"),
email: 'hehehe#hehe.com',
roles: [ { label: 'basic' }, { label: 'admin' } ]
}
My question is if there is a way to achieve something like this:
{
_id: new ObjectId("61e2bc757644758d31578c7d"),
email: 'hehehe#hehe.com',
roles: [ 'basic', 'admin']
}
I want to get the value of each label property and add it to the roles array instead of adding an object like {label: basic}

Mongoose: Infinite scroll with filtering

I have these two models:
User.js
const UserSchema = new Schema({
profile: {
type: Schema.Types.ObjectId,
ref: "profiles",
},
following: [
{
type: Schema.Types.ObjectId,
ref: "users",
},
],
});
module.exports = User = mongoose.model("users", UserSchema);
Profile.js
const ProfileSchema = new Schema({
videoURL: {
type: String,
},
});
module.exports = Profile = mongoose.model("profiles", ProfileSchema);
Here's an example of a User document:
{
"following": [
{
"profile":{
"videoURL":"video_url_1"
}
},
{
"profile":{
"videoURL":"video_url_2"
}
},
{
"profile":{}
},
{
"profile":{
"videoURL":"video_url_3"
}
},
{
"profile":{
"videoURL":"video_url_4"
}
},
{
"profile":{
"videoURL":"video_url_5"
}
},
{
"profile":{}
},
{
"profile":{
"videoURL":"video_url_6"
}
}
]
}
I am trying to implement an infinite scroll of the videos of the users followed by the connected user.
This means, I will have to filter user.following.profile.videoURL
WHERE videoURL exists
Suppose, I will be loading two videos, by two videos:
Response 1: ["video_url_1","video_url_2"]
Response 2: ["video_url_3","video_url_4"]
Response 3: ["video_url_5","video_url_6"]
Usually, infinite scroll is easy because all I have to load the documents 2 by 2 by order of storage without filtering on any field.
Example: Displaying the followed users two by two in an infinite scroll
User.findById(user_id).populate({
path: "following",
options: {
skip: 2 * page,
limit: 2,
},
});
But, now I have to perform filtering on each followed_user.profile.video, and return two by two. And I don't see how I can perform BOTH the filtering and the infinite scroll at the same time.
NOTE: According to the documentation:
In general, there is no way to make populate() filter stories based on properties of the story's author. For example, the below query won't return any results, even though author is populated.
const story = await Story.
findOne({ 'author.name': 'Ian Fleming' }).
populate('author').
exec();
story; // null
So I suppose, there is no way for me to use populate to filter based user.followers, based on each user.follower.profile.videoURL
I am not sure it is possible with populate method, but you can try aggregation pipeline,
$match user_id condition
$lookup with aggregation pipeline in users collection for following
$match following id condition
$lookup with profile for following.profile
$match videoURL should exists
$project to show profile field and get first element using $arrayElemAt
$slice to do pagination in following
let page = 0;
let limit = 2;
let skip = limit * page;
User.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(user_id) } },
{
$lookup: {
from: "users",
let: { following: "$following" },
pipeline: [
{ $match: { $expr: { $in: ["$_id", "$$following"] } } },
{
$lookup: {
from: "profiles",
localField: "profile",
foreignField: "_id",
as: "profile"
}
},
{ $match: { "profile.videoURL": { $exists: true } } },
{
$project: {
profile: { $arrayElemAt: ["$profile", 0] }
}
}
],
as: "following"
}
},
{
$addFields: {
following: {
$slice: ["$following", skip, limit]
}
}
}
])
Playground
Suggestion:
You can improve your schema design,
removing profile schema and add profile object in users collection, so you can achieve easily your requirement using populate method,
put match condition in following populate for videoURL exists
const UserSchema = new Schema({
profile: {
type: {
videoURL: {
type: String
}
}
},
following: [
{
type: Schema.Types.ObjectId,
ref: "users"
}
]
});
module.exports = User = mongoose.model("users", UserSchema);
User.findById(user_id).populate({
path: "following",
match: {
"profile.videoURL": { $ne: null }
},
options: {
skip: 2 * page,
limit: 2,
}
});
So what you want is table with infinite scroll and:
You can opt given ways to approach your problem :
Load data (first page) into grid.
Set filter on a col.
Load data again, this time using the filter.

MongoDB findOne with multiple query returns wrong results

I am trying to query mongodb documents by passing two fields as arguments in a findOne api. Of the two fields passed to the the query statement only can be true at a time. the code is shown below.
//login user with phone or email
userSchema.statics.loginUser = async (userData) => {
const user = await User.findOne({
$or: [{ email: userData.email }, { phone: userData.phone }],
});
if (!user) {
throw new Error("Wrong username or password one");
}
const isMatch = await bcrypt.compare(userData.password, user.password);
if (!isMatch) {
throw new Error("Wrong username or password");
}
return user;
};
But i have noticed that even if i pass a non existent email of phone, the query always returns an existing document. What am i doing wrong in that query?
I think this is happening because userData.email or userData.phone is null and you have a document in your User collection that also has a null email or phone.
When I add the following three records to my database:
> db.User.insertMany([
... {email: "user1#example.com", phone: "555-1234"},
... {email: "user2#example.com", phone: null},
... {email: null, phone: "555-4321"}])
And then perform the following query using the email address of the first user:
> db.User.findOne({$or: [{ email: "user1#example.com" }, { phone: null }]})
I get back the expected record:
{
"_id" : ObjectId("600724ae7d009a501642a783"),
"email" : "user1#example.com",
"phone" : "555-1234"
}
But if I use the this query with an unknown email address:
> db.User.findOne({$or: [{ email: "user3#example.com" }, { phone: null }]})
Then I get the first document with a null phone because there's no match for the email
{
"_id" : ObjectId("600724ae7d009a501642a784"),
"email" : "user2#example.com",
"phone" : null
}
If I change my query to use an $and clause in each $or clause, I now get null when I use a value that doesn't exist and a null phone
> db.User.findOne({$or: [
... { $and: [{ email: { $ne: null } }, { email: "user3#example.com" }] },
... { $and: [{ phone: { $ne: null } }, { phone: null }] }
... ]})
null
And I can still find a record if it exists and is not null, for example this query:
> db.User.findOne({$or: [
... { $and: [{ email: { $ne: null } }, { email: null }] },
... { $and: [{ phone: { $ne: null } }, { phone: "555-1234" }] }
... ]})
Returns this one matching record:
{
"_id" : ObjectId("600724ae7d009a501642a783"),
"email" : "user1#example.com",
"phone" : "555-1234"
}
I also thought of this solution. I transformed the incoming data and removed the undefined data as shown below
let username = {};
username["email"] = userData.email;
username["phone"] = userData.phone;
Object.keys(username).forEach((key) =>
username[key] === undefined ? delete username[key] : {}
);
const user = await User.findOne(username);

MongoDB Query Returns Empty Nested Object

I've got a 'conversations' collection in MongoDB which I'm querying from NodeJS to use the returned data to render the conversation's page.
The data has been stored in the database correctly as far as I can see, when I query it everything comes back as I'd expect, apart from a couple of nested objects - the two users that the conversation belongs to.
Here's what I get when I console.log a conversation (note the 'participants' field:
[ { _id: 57f96549cc4b1211abadf28e,
__v: 1,
messages: [ 57f96549cc4b1211abadf28d ],
participants: { user2: [Object], user1: [Object] } } ]
In Mongo shell the participants has the correct info - the id and username for both participants.
Here's the Schema:
var ConversationSchema = new mongoose.Schema({
participants: {
user1:
{
id: String,
username: String
},
user2:
{
id: String,
username: String
},
},
started: Number,
messages: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Message"
}
]
});
Here's the creation of the conversation document:
var conv = {
participants : {
"user1" : {
"id" : req.body.senderId,
"username" : req.body.senderName
},
"user2" : {
"id" : req.body.recipientId,
"username" : req.body.recipientName
}
},
created : Date.now(),
messages : [] // The message _id is pushed in later.
}
Conversation.create(conv, function(err, newConvo){
if(err){
console.log(err);
} else {
newConvo.messages.push(newMessage);
newConvo.save();
}
})
And lastly, in case it's useful, here's the query to Mongo:
// view all conversations a user belongs to
app.get('/messages', function(req, res){
Conversation.find({
$or : [
{"participants.user1.id" : req.user._id},
{"participants.user2.id" : req.user._id}
]
}, function(err, convos){
if(err){
console.log('Error getting Convos ' + err)
} else {
res.render('messages', {convos: convos, currentUser: req.user});
}
});
});
Thanks a lot for any help that!
It seems that everything is alright, the console.log just doesn't print nested objects by default. Try using:
console.log(JSON.stringify(conversation))
When logging a conversation in order to see the participants objects.
Fixed it!
Andresk's answer above was a big shove in the right direction. As he said, everything was OK, but I wasn't accessing the returned object in the correct way. It's obvious now, but I wasn't providing the index number for the 'convos' object.
I simply needed to do this, even though I was only getting one 'conversation' document back from MongoDB:
console.log(convos[0].participants.user1.username);

Find documents that contain certain value for sub-object field Mongoose

Note: I asked this question here, however at that time I was working purely with MongoDB, now I am trying to implement this with Mongoose. I decided it was appropriate to ask a separate question as I believe the answer will be fundamentally different, however please let me know if I was incorrect in that decision.
I have a collection with the following format:
[
{
firstname: 'Joe',
lastname: 'Blow',
emails: [
{
email: 'test#example.com',
valid: false
},
{
email: 'test2#example.com',
valid: false
}
],
password: 'abc123',
_id: 57017e173915101e0ad5d94a
},
{
firstname: 'Johnny',
lastname: 'Doe',
emails: [
{
email: 'test3#example.com',
valid: false
}
],
password: 'abc123',
_id: 57017e173915101e0ad5d87b
},
]
I am trying to find a user based on the emails.email field. Here is what I have so far:
UserModel.find()
.where('emails')
.elemMatch(function (elem) {
elem.where('email').equals(userEmail);
})
.limit(1)
.exec(
(err, usersReturned) => {
console.log('test2#example.com');
});
What am I doing wrong? I am new to Mongoose and just can't seem to figure this out.
You could do something like this :
UserModel.find({"emails.email": userEmail}).limit(1).exec(function(err, user){
if(err) console.log("Error: " + JSON.stringify(err));
else if(user) console.log("User Returned is : " + JSON.stringify(user));
});
You can use Mongodb aggregate function .Use $unwind on "emails.email" field and it will separate the array make as independent documents.
UserModel.aggregate( [
{ $unwind : "$emails" },
{ $match: {$emails.email:"email you want to put"}}
],function(err,result){
//write code but you want to do
});

Resources