Update or push object into nested array in found document mongoose - javascript

I have this mongoose Schema:
const UserSchema = new mongoose.Schema({
name: {
type: String,
trim: true
},
userId: {
type: String,
required: [true, "user ID required."],
unique: [true, "user ID must be unique"]
},
votes: [
{
pollId: type: mongoose.Schema.Types.ObjectId,
candidates: [
{
candidateId: mongoose.Schema.Types.ObjectId
}
]
}
],
role: {
type: String,
enum: ["user", "admin"],
default: "user"
}
});
I find user first, because I want to authorize the user. and now I want to update some parts. just want to be clear, I want to update some parts of a found user document.
I have multiple polls that user can vote for multiple candidates in each one. so if user has not voted at all, votes array will be empty, and we have to push first pollId and also first candidateId that he/she votes. and if the pollId exists,we have to find that subdocument first by pollId then we should just add candidateId inside candidates array.
how can I do this? preferred is just one operation not multiple. and if I can get updated user its better.
if it's not clear let me know. I'll try to explain more.
thanks.

I would do something like this
function updateUsersVotes(userId, newVote) {
User.findById(userId)
.exec()
.then((dbUser) => {
if (!dbUser.votes.length) { // no votes yet
dbUser.votes.push(newVote);
} else { // check current votes
const voteIndex = dbUser.votes.findIndex((vote) => vote.pollId.toString() === newVote.pollId.toString());
if (voteIndex > 0) { // if the incoming vote pollId matches an existing pollId
dbUser.votes[voteIndex].candidates.push(...newVote.candidates);
} else {
dbUser.votes.push(newVote); // if the incoming pollId can't be found, then add it.
}
}
dbUser.save({ validateBeforeSave: true }); // update the user.
})
.catch((error) => {
handleError(error); // you should always handle your errors.
});
}

Related

How to return an error when $addToSet does not add an item because it already exists in Mongoose?

I have the following schema which contains a property with an array:
const projectSchema = new mongoose.Schema(
{
title: {
type: String,
required: [true, "Please add a title"],
},
users: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
},
);
In my project controller, I'm trying to add users to this array without getting a duplicate user. So I use $addToSet. It works fine but it doesn't return an error when there is a duplicate user.
const project = await Project.findOneAndUpdate(
{ _id: id },
{ $addToSet: { users: userID } },
{ new: true }
);
How can I detect that it didn't add a user (because the user already exists in the array) and return an error?

Apply unique only inside array or sub-document mongoose

Hope you are doing good.
I have a schema like this:
const Person = new mongoose.Schema({
username: {
type: String,
required: [true, "Name is a required field"],
unique: [true, "Another Person with the same name already exists"],
trim: true
},
friends: [
{
name: {
type: String,
required: [true, "Every friend must have a name"],
unique: [true, "Another friend with the same name already exists"],
trim: true
},
favoriteFood: String
}
],
createdAt: {
type: Date,
default: Date.now()
}
});
here I want name just be unique inside friends array, obviously different persons can have the same friend. but MongoDB does not let me define two person with the same name inside different persons object with this implementation. how can I do that?how to force friends name only be unique for one person?
another thing I found is if I try to add two friends with the same name under one person it will be accepted, but if you try to add same friend name across two different persons, it will throw the error of duplication. it is the exact opposite of what it should be, or at least opposite of what I want.
thanks.
It seems there is no default way, or some sort of indexing that can do that, so we have to check for duplication manually. I done it this way:
//check for duplications
Person.pre("save", function (next) {
const friends = this.friends;
const lookup = friends.reduce((a, e) => {
a[e.name] = ++a[e.name] || 0;
return a;
});
const duplicates = friends.filter(friend => lookup[friend.name]);
if (duplicates.length) next(new Error("There are two friends with the same name"));
else next();
});

Using findById to find the id of a schema in an array

Hey I was wondering how do I use findById for a schema inside an array? For example, I have the following Schema:
const GameSchema = new mongoose.Schema({
users: [
{
user: { type: mongoose.Schema.ObjectId, ref: 'User' },
role: {
type: String,
required: true,
enum: ['user', 'moderator', 'creator'],
default: 'user',
},
},
]
}]
I want to find the user with a mongoose function like findById, such as the following:
const user = await game.users.findById({ user: req.user.id })
It doesn't seem to work since users is not a mongodb model. I know I can find the user by using find() like the following:
const user = await game.users.find(
(gameUser) => gameUser.user == req.user.id
)
The only problem is that the type of gameUser and req.user.id is not the same and I can't use '==='. Is there some way to go through the array and use the mongoose function findById?
As docs explains, findById method:
Finds a single document by its _id field
So you have to use findOne() instead of findById().
Also, to return only one field from the entire array you can use projection into find.
Check this example. This query find an object by its id (i.e. user field) and return only the object, not the whole array.
db.collection.find({
"users": { "$elemMatch": { "user": 1 } }
},
{
"users.$": 1
})
Using mongoose you can do:
yourModel.findOne(({
"users": { "$elemMatch": { "user": 1 } }
},
{
"users.$": 1
})).then(result => {
console.log(result)
}).catch(e => {
// error
})

how do I query from parent model a array of other model in Mongoose?

I have two Schema for user & todo. Every todo has an owner as a user, every user has an array of todos.
// user.js
const TodoSchema = require('./todo').TodoSchema;
var UserSchema = mongoose.Schema({
name: {
type: String,
required: true
},
todos: {
type: [TodoSchema]
}
});
module.exports.UserSchema = UserSchema;
module.exports.UserModel = mongoose.model('UserModel', UserSchema);
// todo.js
var TodoSchema = mongoose.Schema({
body: {
type: String, required: true
},
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'UserModel',
required: true
}
});
module.exports.TodoSchema = TodoSchema;
module.exports.TodoModel = mongoose.model('TodoModel', TodoSchema);
I entered data like this.
var nUser = new UserModel({
name: "Alex
)};
nUser.save().then(user => {
var t = new TodoModel({
body: "my new todo",
owner: user._id
});
t.save().then();
});
But the problem is I want to get all the todos from a specific user, something like this...What is the correct way?
UserModel.findOne({name: "Alex"})
.then(user => {
// user.todos
});
P.S.
I can do this like TodoModel.find({owner: specific_user._id}), but I want it from UserModel.
Since you're asking for the proper way of doing it, I am gonna start with your User Schema. If you want to find all the todos of a user, then putting the todo documents inside an array in the User document is not required. So you should probably remove that from your schema.
After that you can use a simple aggregation to get your desired outcome.
UserModel.aggregate([
{
$match:{
name:"Alex"
}
},
{
$lookup:{
from:"todomodels",
localField:"$_id",
foreignField:"$owner",
as:"todos"
}
}
])
this will return all the todos for that user in an array of the same name.

Mongoose custom validation of several fields on update

First of all, this doesn't help.
Let's say, we have a User model:
const schema = new mongoose.Schema({
active: { type: Boolean },
avatar: { type: String }
});
const User = mongoose.model('User', schema);
When we update it (set an avatar):
// This should pass validation
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
We want to validate it based on current (or changed) active attribute value.
Case #1
active is false
we should not be able to set avatar - it should not pass the validation
Case #2
active is true
we should be able to set avatar - it should pass the validation
Ideas
Use a custom validator
const schema = new mongoose.Schema({
active: { type: Boolean },
avatar: { type: String, validate: [validateAvatar, 'User is not active'] }
});
function validateAvatar (value) {
console.log(value); // user.avatar
console.log(this.active); // undefined
}
So this will not work as we don't have an access to active field.
Use pre "validate" hook
schema.pre('validate', function (next) {
// this will never be called
});
This hook doesn't work with update method.
Use pre "update" hook
schema.pre('update', function (next) {
console.log(this.active); // undefined
});
This will not work for us as it doesn't have an access to model fields.
Use post "update" hook
schema.post('update', function (next) {
console.log(this.active); // false
});
This one works, but in terms of validation is not quite good choice, as the function is being called only when model was already saved.
Question
So is there a way to validate the model based on several fields (both saved in DB and new ones) before saving it, while using model.update() method?
As a summary:
Initial user object
{ active: false, avatar: null }
Update
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
Validation should have an access to
{ active: false, avatar: 'user1.png' }
If validation fails, changes should not be passed to DB
Due to limitation of working with update() I've decided to solve the problem this way:
Use custom validators (idea #1 mentioned in the question)
Don't use update()
So instead of
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
I use
User.findOne({ _id: id })
.then((user) => {
user.avatar = 'user1.png';
user.save();
});
In this case custom validators work as expected.
P.S. I choose this answer as a correct one for me, but I will give bounty to the most relevant answer.
You can do this with the context option specified in the mongoose documentation.
The context option
The context option lets you set the value of this in update validators
to the underlying query.
So in your code you can define your validator on the path like this:
function validateAvatar (value) {
// When running update validators with the `context` option set to
// 'query', `this` refers to the query object.
return this.getUpdate().$set.active;
}
schema.path('avatar').validate(validateAvatar, 'User is not active');
And while updating you need to enter two options runValidators and context. So your update query becomes:
var opts = { runValidators: true, context: 'query' };
user.update({ _id: id }, { $set: { avatar: 'user1.png' }, opts });
Did you try giving active a default value so it would not be undefined in mongodb.
const schema = new mongoose.Schema({
active: { type: Boolean, 'default': false },
avatar: { type: String,
trim: true,
'default': '',
validate: [validateAvatar, 'User is not active']
}});
function validateAvatar (value) {
console.log(value); // user.avatar
console.log(this.active); // undefined
}
When creating do you set the user in this way
var User = mongoose.model('User');
var user_1 = new User({ active: false, avatar: ''});
user_1.save(function (err) {
if (err) {
return res.status(400).send({message: 'err'});
}
res.json(user_1);
});
You can try with pre "save" hook. I used it before and can get the value in "this".
schema.pre('save', function (next) {
console.log(this.active);
});
Hope this work with you too !
You have to use an asynchronous custom validator for that:
const schema = new mongoose.Schema({
active: { type: Boolean },
avatar: {
type : String,
validate : {
validator : validateAvatar,
message : 'User is not active'
}
}
});
function validateAvatar(v, cb) {
this.model.findOne({ _id : this.getQuery()._id }).then(user => {
if (user && ! user.active) {
return cb(false);
} else {
cb();
}
});
}
(and pass the runValidators and context options to update(), as suggested in the answer from Naeem).
However, this will require an extra query for each update, which isn't ideal.
As an alternative, you could also consider using something like this (if the constraint of not being able to update inactive users is more important than actually validating for it):
user.update({ _id : id, active : true }, { ... }, ...);

Categories