Mongoose query to filter the collection based on nested field - node.js

We want to see if mongoose could do heavy lifting to get the user's role for a given Organization Name and UserId.
This can be done easily by first finding out the organization data and use javascript to filter out based on User's ID. But i think it would give better performance if the query can do it instead of doing outside the mongo collection.
What we want to try something like below, but it is not giving the role of the user correctly.
Query (not working)
public async getUserOrgRole(orgName: string, userId) {
const result = await this.organizationModel
.findOne({ name: orgName, 'orgMembers.userId': userId })
.select('orgMembers.role')
.exec();
if (result) {
const orgMember = _.get(result, 'orgMembers');
return orgMember[0].role;
}
return null;
}
Query (working but we want the above query to work instead of pulling entire document)
public async getUserOrgRole(orgName: string, userId) {
const org = await this.organizationModel.findOne({ name: orgName })
if (!org)
return null;
const userInOrg = org.orgMembers.find(om => om.userId === userId)
console.log('--getUserOrgRole', userInOrg)
if (userInOrg)
return userInOrg.role
return null;
}
Schema
const UserOrgSchema = new Schema({
role: { type: Schema.Types.String, enum: ['MEMBER', 'OWNER', 'ADMIN'], default: 'MEMBER' },
inactive: { type: Schema.Types.Boolean, default: false },
userId: { type: Schema.Types.String, required: true },
});
const OrganizationSchema = new Schema({
name: { type: Schema.Types.String, unique: true },
picture: { type: Schema.Types.String },
orgMembers: { type: [UserOrgSchema] },
createdAt: { type: Schema.Types.Date, default: Date.now },
updatedAt: { type: Schema.Types.Date, default: Date.now },
});

You almost got it right. The reason why your attempt does not quite work is explained by Sunil. No matter what filters you apply to .find(), it will always return the whole document. If you want to select specific subdocuments, you need to do that using an additional select operator. This ought to work:
const result = await this.organizationModel
.findOne({ name: orgName, "orgMembers.userId": userId })
.select({
orgMembers: {
$elemMatch: {
userId,
},
},
})
.select("orgMembers.role")
.exec();
Note the use of $elemMatch! It does exactly what you wanted - filters the subdocuments by selecting only the ones that match the provided filter.

Related

E11000 duplicate key error collection error mongoose

ok so basically im getting this error and i dont know whats causing it
MongoServerError: Plan executor error during findAndModify :: caused by :: E11000 duplicate key error collection: test.profilemodels index: userID_1 dup key: { userID: "776359374783512608" }
i've literally tried dropping indexes and it still didnt work. all that works is when i drop my database/collection and i dont wanna have to do that everytime
my schema:
const mongoose = require("mongoose");
const profileSchema = new mongoose.Schema({
userID: { type: String, require: true, unique: true },
serverID: { type: String, require: true },
coins: { type: Number, default: 200 },
inventory: { type: Object },
bank: { type: Number },
bankSpace: { type: Number, default: 10000 },
bankNotes: { type: Number },
title: { type: String, default: 'Newbie'},
badges: { type: String, default: 'None'},
cPoints: { type: Number}
});
const model = mongoose.model("ProfileModels", profileSchema);
module.exports = model;
the code the error is coming from:
const cpoints = profileData.cPoints
if(cpoints < 10000) return message.reply('You don\'t have enough wins for this title!')
await profileModel.findOneAndUpdate(
{
userID: message.author.id,
title: 'Coinflipper'
}
);
message.reply('Successfully equipped the **Coinflipper** Title!')
im stumped by this error hopefully someone can help
As you've pointed in your schema, the userID field has to be unique.
With syntax below, you are telling to mongoose to update FIRST found document with userID that was already used by some other document. It's beacuse in that case first parameter is 'update' part.
await profileModel.findOneAndUpdate(
// update part
{
userID: message.author.id,
title: 'Coinflipper'
}
);
So to fix it, you have to
set some filtration in the first parameter, and in the second one 'fields to update'. So it will be like
await profileModel.findOneAndUpdate(
// filter part
{
userID: message.author.id
},
// update part
{
title: 'Coinflipper'
}
);
I think, you are not using properly the function findOneAndUpdate(..), https://mongoosejs.com/docs/tutorials/findoneandupdate.html
Try something like this:
await profileModel.findOneAndUpdate(
{ userID: message.author.id}, // filter
{ title: 'Coinflipper' } // fields to modify
);

MongoDB not adding default date for nested Schemas?

I am attempting to add a form result to an existing client in a collection and all form data variables being passed are added successfully, however, a default date variable is not being created and saved despite being in the schema.
Here is the schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create Schema
const FormSchema = new Schema({
formID: {
type: String
},
formName: {
type: String
},
date_completed: {
type: Date,
default: Date.now
},
formData: {
type: JSON
}
});
const ClientSchema = new Schema({
clientID: {
type: String,
required: true,
unique: true
},
dob: {
type: Date,
required: true
},
formResults: {
tags: [
{
type: FormSchema
}
]
}
});
module.exports = Client = mongoose.model('client', ClientSchema);
And here is the method posting the form results:
router.post('/:id', auth, (req, res) => {
Client.update(
{ clientID: req.params.id },
{
$push: {
formResults: {
$each: [
{
formID: req.body.formID,
formName: req.body.formName,
formData: req.body.formData
}
]
}
}
}
)
.then(() => res.json({ success: true }))
.catch(
err => res.status(404).json({ success: false }) && console.log(err)
);
});
I have tried forcing the date by passing date_completed: Date.now with the other form variables but this makes no difference. The results are still saved with no date variable listed. I have also tried dropping the collection and recreating it, this gave no changes. And I have checked the indexes for the collection, for which there is only _id and clientID.
Here is the data in saved in the database when executed and showing there is no date_completed: value.
Stored Data
At first glance your code is correct and should have no problem as it complies with the documentation and tutorials of mongoose, you can test this code:
// Create Schema
const FormSchema = new Schema({
formID: {
type: String
},
formName: {
type: String
},
date_completed: {
type: Date,
default: function() {
if (!this.date_completed) {
return Date.now();
}
return null;
}
},
formData: {
type: JSON
}
});
or:
var minuteFromNow = function(){
var timeObject = new Date();
return timeObject;
};
// Create Schema
const FormSchema = new Schema({
formID: {
type: String
},
formName: {
type: String
},
date_completed: {
type: Date,
default: minuteFromNow
},
formData: {
type: JSON
}
});
Let us also say this null is a valid value for a Date property, unless you specify required. Defaults only get set if the value is undefined, not if its falsy.
Honestly, I wasn't able to find any reason why it was not working. I waited a few days and the issue fixed itself. I don't believe there was anything wrong with the code, I think the issue was to do with MongoDB Atlas not updating results and displaying the dates that were being created but again I have no idea why this would be the case.

MongoDB Creating document and on success create another document

I am working on application using Node.js and MongoDB. I have a particular use case wherein I create a new user and on success add the user's ObjectId into another collection called 'cities' by fetching the user's city if not existing or create a new one and append User's ObjectId to Subscriber's List field of the city document.
The Schemas look like below:
CitiesSchema:
var CitiesSchema = new Schema({
City:{
type: String
},
SubscribersList: [{type: Schema.ObjectId}]
});
User Schema:
var UsersSchema = new Schema({
emailId: {
type: String,
required: 'Mandatory field'
},
city: {
type: String
},
subscribedOn: {
type: Date,
default: Date.now
},
lastEmailSent: {
type: Date,
default: null
},
isActive: {
type: Boolean,
default: true
}
});
Please let me know how I can tackle this in the cleanest way possible or is there any design pattern I need to follow ?
You can use the then notation to continue processing after you have created your User. Like this
UserSchema.create({
emailId: 'email#exmaple.com',
city: 'Rome'
})
.then(user => {
// For example if you want to push to the city of the user
CityScema.update({
City: user.city
}, {
$push: {
SubscribersList: user._id
}
}).then(() => {
res.sendStatus(200);
}).catch(err => res.status(500).send(err));
}).catch(err => {
// Handle your errors here
console.error(err);
});
Make sure you check the Mongoose docs on Models for more information

Mongoose query not returning updated information with { new: true } param

Why is my query not returning updated information?
UserSchema.findByIdAndUpdate(
{ _id: userId },
{ $set: { couponList: couponList } }, { new: true }).populate('couponList').exec().then(user => {
// user returning with the old information
}).catch(err => console.log(err));
I have 3 params:
first one is the id of the user i want to update (objectId)
second one is the information I want to update (objectId Array)
third is the flag that says I want to receive the updated information (Boolean)
My coupon schema goes like this:
import mongoose from 'mongoose';
const CouponSchema = new mongoose.Schema({
name: {
type: String,
default: 'Unknown'
},
description: {
type: String,
default: undefined
},
validity: {
type: Date,
default: null
},
code: {
type: String,
default: undefined
},
blackList: {
type: Array,
ref: 'user',
default: []
},
blackListFlag: {
type: Boolean,
default: false,
},
whiteList: {
type: Array,
ref: 'user',
default: []
},
limit: {
type: Number,
default: 0,
},
counter: {
type: Number,
default: 0,
},
amount: {
type: Number,
default: 0,
},
discountType: {
type: String,
default: undefined,
}
}, { collection: 'coupon' });
export default mongoose.model('coupon', CouponSchema);
And in my user schema I have a ref to the coupon schema:
couponList : {
type: Array,
ref: 'coupon',
default: []
},
I think you need to define the field couponList in your schema.
Edit: Just noticed the UserSchema, theoretically, you should be fine, if you are pushing correct objectIds.
findByIdAndUpdate with {new: true} must work as intended.
But I'm not aware of Your code totally and what kind of data You're sending as couponList.
So try to separate update and select operations and see what happens. In fact mongoose does the same when You call findByIdAndUpdate.
For example using express framework:
const User = mongoose.model('user');
router.put('/user/:userId/coupons', async (req, res) => {
try {
const {userId} = req.params;
const {couponList} = req.body;
await User.updateOne(
{_id: userId},
{$set: {couponList: couponList}},
{upsert: false}
);
const user = await User
.findById(userId)
.populate('couponList').lean();
res.status(200).send(user);
}
catch (error) {
console.log(error);
res.status(500).send({})
}
});
P.S. Only reason for that unexpected behavior may be that somehow (but it's not possible) it uses native driver for which {new: true} must be written as: {returnNewDocument: true}
Check this link
I found out that the problem was not with returning updated information but it was on populating the collection.
The correct reference to the coupon collection in user schema:
couponList: [ { type: mongoose.Schema.ObjectId, ref: 'coupon' } ],

Save array of ObjectId in Schema

I have a model called Shop whos schema looks like this:
'use strict';
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var ShopSchema = new Schema({
name: { type: String, required: true },
address: { type: String, required: true },
description: String,
stock: { type: Number, default: 100 },
latitude: { type: Number, required: true },
longitude: { type: Number, required: true },
image: String,
link: String,
tags: [{ type: Schema.ObjectId, ref: 'Tag' }],
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Shop', ShopSchema);
I want to use the array tags to reference to another model via ObjectId obviously. This set up works fine when I add ids into the property via db.shops.update({...}, {$set: {tags: ...}}) and the ids get set properly. But when I try to do it via the Express.js controller assigned to the model, nothing gets updated and there even is no error message. Here is update function in the controller:
// Updates an existing shop in the DB.
exports.update = function(req, res) {
if(req.body._id) { delete req.body._id; }
Shop.findById(req.params.id, function (err, shop) {
if (err) { return handleError(res, err); }
if(!shop) { return res.send(404); }
var updated = _.merge(shop, req.body);
shop.updatedAt = new Date();
updated.save(function (err) {
if (err) { return handleError(res, err); }
return res.json(200, shop);
});
});
};
This works perfect for any other properties of the Shop model but just not for the tags. I also tried to set the type of the tags to string, but that didn't help.
I guess I am missing something about saving arrays in Mongoose?
It looks like the issue is _.merge() cannot handle merging arrays properly, which is the tags array in your case. A workaround would be adding explicit assignment of tags array after the merge, if it is ok to overwrite the existing tags.
var updated = _.merge(shop, req.body);
if (req.body.tags) {
updated.tags = req.body.tags;
}
Hope this helps.. If the workaround is not sufficient you may visit lodash forums.

Resources