Mongoose conditional populate with findById() - javascript

I'm attempting to populate a field only if the target model includes a certain property. Below, I want to populate the Book's product only if the Book's gift property is set to false, but it doesn't seem to work:
//Schema
const bookSchema = new mongoose.Schema({
gift: { type: Boolean, default: false },
date: { type: Date, default: Date.now },
author: { type: [authorSchema] },
product: { type: [productSchema] }
}
// Conditional Populate
result = await Book
.findById(bookID)
.populate("author", "name")
.populate("product", "price", { gift: false } )
[EDIT]:
As suggested by Vinícius Belló, populating an existing document works.
// Conditional Populate
const result = await Book
.findById(bookID)
.populate("author", "name");
if (!result.gift) {
await result
.populate("product", "price")
.execPopulate();
}

You can receive the result without populate product then populate the result variable if gift is false. I recommend you to read this part of mongoose docummentation https://mongoosejs.com/docs/populate.html#populate_an_existing_mongoose_document.

Related

Node.js: Mongoose filter data from array of ObjectIDs stored in collection [duplicate]

This question already has answers here:
mongodb/mongoose findMany - find all documents with IDs listed in array
(9 answers)
Closed 3 months ago.
I am trying to search using node.js, ejs and mongoose. All the filter parameters are working perfectly but only categoryIds is not (stored as a collection of ObjectIDs in the mongodb document, referring to the respective document in categories collection), always giving me the empty record set.
For example:
If I need to find the a movie called Cosmos (see the attached screenshot) then I can easily find it with all or any filter except categories. Once I select any category, the record-set will go blank even if the I have selected the one which it belongs to.
model.js
const Model = mongoose.model('Movie', new Schema({
...
categoryIds: [{
type: Schema.Types.ObjectId,
trim: true,
default: null,
ref: 'Category',
}],
copyrightId: {
type: Schema.Types.ObjectId,
trim: true,
default: null,
ref: 'Copyright',
},
...
}, {
timestamps: true
});
Controller.js
Router.get('/', (req, res) => {
const search = req.query;
const conditions = (() => {
let object = {};
['releaseYear', 'languageId', 'copyrightId'].forEach(filter => {
if (search[filter] != '') {
object[filter] = search[filter];
}
});
if (typeof search.categoryIds !== 'undefined') {
object.categoryIds = [];
search.categoryIds.forEach(item => object.categoryIds.push(item));
}
if (search.keywords != '') {
object.title = {
$regex: search.keywords,
$options: 'i'
};
}
return object;
})();
const count = await Model.count(conditions);
const items = await Model.find(conditions, {
__v: false,
imdb: false,
trailer: false,
createdAt: false,
updatedAt: false,
}).sort({
status: -1,
releaseYear: -1,
title: 1
})
.populate('languageId', ['title'])
.populate('copyrightId', ['title'])
.populate('categoryIds', ['title'])
.skip(serialNumber)
.limit(perPage);
...
});
All the fields in the search form
{
categoryIds: [
'6332a8a2a336e8dd78e3fe30',
'6332a899a336e8dd78e3fe2e',
'6332a87ba336e8dd78e3fe2c',
'634574ab339b1a6b09c1e144'
],
languageId: '',
copyrightId: '',
releaseYear: '',
rating: '',
seen: '',
status: '',
keywords: '',
submit: 'search' // button
}
filtered search parameters
{
categoryIds: [
'6332a8a2a336e8dd78e3fe30',
'6332a899a336e8dd78e3fe2e',
'6332a87ba336e8dd78e3fe2c',
'634574ab339b1a6b09c1e144'
]
}
Here is the screenshot of mongodb document.
...
if (typeof search.categoryIds !== 'undefined') {
object.categoryIds = {
$in: []
};
search.categoryIds.forEach(item => object.categoryIds.$in.push(
mongoose.Types.ObjectId(item))
);
}
console.log(object);
return object;
The is the final filter object
{
categoryIds: {
'$in': [
new ObjectId("6332a87ba336e8dd78e3fe2c"),
new ObjectId("634669f4a2725131e80d99f1")
]
}
}
Now, all the filters are working perfectly.
Thank you everyone.
The filter should contain all categoryIds and in the same order to match the document. It's not quite clear from the question if it is the intended functionality. If not, most popular usecases are documented at https://www.mongodb.com/docs/manual/tutorial/query-arrays/
I don't recall how mongoose handles types when you query with array function like $all, so you may need to convert string IDs to ObjectIDs manually, e.g.:
search.categoryIds.forEach(item => object.categoryIds.push(
mongoose.Types.ObjectId(item))
);

Mongoose is not populating the fields in MongoDB

When I run the following code, I get the object along with the populated fields logged on the console.
Screenshot
But, the fields have not been populated in the books collection. Can someone please help me figure this out?
const bookSchema = new Schema({
title: String,
genre: { type: Schema.Types.ObjectId, ref: "genre" },
author: { type: Schema.Types.ObjectId, ref: "author" },
numberInStock: { type: Number, default: 0 },
rating: Number,
yearPublished: Number,
dateAdded: { type: Date, default: Date.now },
liked: { type: Boolean, default: false },
});
const genreSchema = new Schema({ name: String });
const authorSchema = new Schema({ name: String });
const Book = model("book", bookSchema);
const Genre = model("genre", genreSchema);
const Author = model("author", authorSchema);
const books = [
{
title: "Sapiens",
genre: "632873144b0bbfc10ae1942d",
author: "632873e706fe265eaee77de3",
numberInStock: 6,
rating: 4.4,
yearPublished: 2011,
},
];
async function saveBook(b) {
let book = new Book(b);
book
.save()
.then((result) => {
populateBook(result._id);
})
.catch((err) => console.log("Error: ", err));
}
function populateBook(id) {
Book.findById(id)
.populate("genre")
.populate("author")
.exec((err, book) => {
if (err) {
console.log("Error: ", err);
return;
}
console.log(book);
});
}
books.forEach((b) => {
saveBook(b);
});
That's how population works, it only stores references to other documents in the database. At query time, and if you ask for it (using .populate()), Mongoose will retrieve the referenced documents and insert them into the "parent" document.
If you want the referenced documents to be stored in the database, you can't use population but have to use subdocuments.
However, this will limit the flexibility of your database, because if for example an author name needs to be changed, you need to change all the Book documents in your database to update the author's name. With population, you only need to change the Author document.

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?

Update or push object into nested array in found document mongoose

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.
});
}

How to modify virtual fields from sequelize findAll result?

I have looked everywhere and couldn't find any clear answers for this.
I have a complex findAll() with many inclusions and each with their own virtual fields.
What I want is to modify the virtual fields of the result, however as it is returning the model instance trying to access the virtual fields returns undefined as they are not in the result yet.
I have tried 'raw: true' but this removes all virtual fields and as my data has nested tables which also have their own virtual fields which I need, I cannot do that.
Example models
var Book = sequelize.define('Book', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
title: {
type: DataTypes.STRING
},
author: {
type: DataTypes.STRING
}
//....other columns,
myField: {
type: DataTypes.Virtual,
get() {
return this.getDataValue('title:') + this.getDataValue('author');
})
Getting the data
model.Book.findAll({
limit: 100
})
.then((result) => {
const newBook = result.map(row => {
return {...row, myField: 'setMyOwnValueHere'}
}
return newBook
}
Get model data first : get
model.Book.findAll({
limit: 100
}).then(result => {
const books = result.map(row => {
//this returns all values of the instance,
//also invoking virtual getters
const book = row.get();
book.myField = 'setMyOwnValueHere';
return book;
});
return books;
});

Categories