Mongoose push item to array if exists - node.js

I have a model schema like this :
favMovieSchema = mongoose.Schema({
movieId: {type: mongoose.SchemaTypes.ObjectId, ref: "Movie"},
rating: [{
type: Number,
// trim:true,
//MinMax validator
}],
});
movieSchema={
some fields,
favMovies: [favMovieSchema],
}
I want to push a rating into rating column if movieId already exists, to maintain a history of ratings done by a particular user.
If it does not exists I am simply updating a new object with no issue.
I need some complex mongoose query to help me make a check for this.

It sounds like the complex query you need is:
collection.update({movieId: movieId}, {$push: {rating: rating}}, {upsert: true})
The first part is the matching: you are looking for a document with this movieId.
The 2nd part is the update: pushing one more rating to the rating array.
The 3rd part is the options, upsert meaning update if the matching exist and create a new document if it does not.

Related

Proper way of updating average rating for a review system using Mongoose

I'm currently learning some backend stuff using an Udemy course and I have an example website that lets you add campgrounds (campground name, picture, description, etc.) and review them. I'm using the Express framework for Node.js, and Mongoose to access the database.
My campground schema looks like:
const campgroundSchema = new mongoose.Schema({
name: String,
image: String,
description: String,
price: String,
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment"
}
],
rating: {type: Number, default: 0}
});
And my comment/review schema looks like:
const commentSchema = new mongoose.Schema({
text: String,
rating: {
type: Number,
min: 1,
max: 5,
validate: {validator: Number.isInteger}
},
campground: {type: mongoose.Schema.Types.ObjectId, ref: "Campground"}
});
Campgrounds and Comments also have references to a User but I've left that out for simplicity.
I'm looking to know the best practice for updating and displaying the campground average rating.
The method used by the tutorial I'm following is to recalculate the average rating each time a comment is added, changed, or deleted. Here's how it would work for a new comment:
Campground.findById(campgroundId).populate("comments").exec(function(err, campground) {
Comment.create(newComment, function(err, comment) {
campground.comments.push(comment);
campground.rating = calculateRating(campground.comments);
campground.save();
});
});
"calculateRating" iterates through the comment array, gets the total sum, and returns the sum divided by the number of comments.
My gut instinct tells me that there should be a way to make the "rating" field of Campground perform the functionality of the "calculateRating" function, so that I don't have to update the rating every time a comment is added, changed, or removed. I've been poking around documentation for a while now, but since I'm pretty new to Mongoose and databases in general, I'm a bit lost on how to proceed.
In summary: I want to add functionality to my Campground model so that when I access its rating, it automatically accesses each comment referenced in the comments array, sums up their ratings, and returns the average.
My apologies if any of my terminology is incorrect. Any tips on how I would go about achieving this would be very much appreciated!
Love,
Cal
I think what you are trying to do is get a virtual property of the document that gets the average rating but it does not get persisted to the mongo database.
according to mongoosejs :- Virtuals are document properties that you can get and set but that do not get persisted to MongoDB. They are set on the schema.
You can do this:
CampgroundSchema.virtual('averageRating').get(function() {
let ratings = [];
this.comments.forEach((comment) => ratings.push(comment.rating));
return (ratings.reduce((a,b)=>a+b)/ratings.length).toFixed(2);
});
After that on your view engine after finding campgrounds or a campground, all you need to call is ; campground.averageRating;
Read more here : https://mongoosejs.com/docs/guide.html#virtuals
also note that you can not make any type of query on virtual properties.

Non-existing field in Mongodb document appears in mongoose findById() result

I'm somewhat new in what is related to Mongoose and I came to this behaviour I consider as strange. The document returned by Mongoose has fields that are not present in the actual MongoDb document, and seem to be added by Mongoose based on the schema.
I use a schema similar to this (this one is simplified) :
const ProfessionalSchema = new mongoose.Schema({
product: {
details: [{
_id: false,
id: String, // UUID
name: String,
prestations: [{
_id: false,
id: String, // UUID
name: String,
price: Number,
}],
}],
},
[...]
My document as shown in Mongodb with mongo CLI utility doesn't have a product field.
What I don't understand is why the result of Professional.findById().exec() returns a document with a product:{details[]} field. I expect not to have that field in the Mongoose returned result, since it is not present in the original MongoDb document.
The Mongoose documentation found https://mongoosejs.com/docs/guide.html (Schema and Model paragraph) didn't help.
My business logic would require that field not to be present, instead of being forced by the schema. Is this achievable ?
Try taking a look at the default option. You could e.g. default your product to null and then, in your business logic, handle the "product is null" case rather than the "product field does not exist" case.
As for why this is happening, it's because you're dealing with a schema. If the field doesn't exist on the document, it's going to be auto-populated. The whole point of a schema is to ensure consistency of your document structure.

Mongoose : How to find documents in which fields match an ObjectId or a string?

This is mongoose DataModel in NodeJs
product: {type: mongoose.Schema.Types.ObjectId, ref: 'products', required: true}
But in DB, this field is having multiple type of values in documents, have String and ObjectId
I'm querying this in mongoose
{
$or: [
{
"product": "55c21eced3f8bf3f54a760cf"
}
,
{
"product": mongoose.Types.ObjectId("55c21eced3f8bf3f54a760cf")
}
]
}
But this is only fetching the documents which have that field stored as ObjectId.
Is there any way that it can fetch all the documents having both type of values either String OR ObjectId?
Help is much appreciated. Thanks
There is a schema in Mongoose, so when you query a document, it will search it by this schema type. If you change the model's product type to "string", it will fetch only documents with string IDs.
Even if there is a way to fetch either a string OR ObjectId, it's smelly to me to have such inconsistency.
I've encountered the same problem, so the solution was to standardize all documents by running a script to update them.
db.products.find().forEach(function(product) {
db.products.update({ type: product.type},{
$set:{ type: ObjectId(data.type)}
});
});
The only problem I see there is if this type field is actually an _id field. _id fields in MongoDB are immutable and they can't be updated. If that is your case, you can simply create a new document with the same (but parsed) id and remove the old one.
This is what I did: (in Robomongo)
db.getCollection('products').find().forEach(
function(doc){
var newDoc = doc;
newDoc._id = ObjectId(doc._id);
db.getCollection('products').insert(newDoc);
}
)
Then delete all documents, which id is a string:
db.getCollection('products').find().forEach(
function(doc){
db.getCollection('products').remove({_id: {$regex: ""}})
}
)
There is another way to do this. If we Update the type to Mixed then it will fetch all the documents with each type, either String or ObjectId
Define this in your Schema
mongoose.Schema.Types.Mixed
Like
product: {type: mongoose.Schema.Types.Mixed, required: true}

Mongoose – linking objects to each other without duplicating

I have a model "Category". Collection categories contains several objects.
I also a have model "Post". Collection posts may contain a lot of objects with users' posts. "Post" object may relate to 1+ categories. How to link "Post" object to 1+ "Category"-objects without placing "Post"-object inside "Category"-object as subdocument? Certainly, I need to have an option to find all posts related to certain category.
One of the ways I can imagine is to store in "Post"-object obj_id of all categories which it's related to. Smth like this:
var postSchema = mongoose.Schema({
title: String,
description: String,
category: [ObjectId],
created_time: Number,
})
and add category later...
post.category.push(obj_id);
but is it really a mongoose-way? Which way is correct? Thanks.
P.S. I've also read about population methods in mongoose docs, may it be useful in my case? Still not completely clear for me what is this.
Populate is a better tool for this since you are creating a many to many relationship between posts and categories. Subdocuments are appropriate when they belong exclusively to the parent object. You will need to change your postSchema to use a reference:
var postSchema = mongoose.Schema({
title: String,
description: String,
category: [{ type: Schema.Types.ObjectId, ref: 'Category' }],
created_time: Number,
});
You can add categories by pushing documents onto the array:
post.category.push(category1);
post.save(callback);
Then rehydrate them during query using populate:
Post.findOne({ title: 'Test' })
.populate('category')
.exec(function (err, post) {
if (err) return handleError(err);
console.log(post.category);
});

is there a way to auto generate ObjectId when a mongoose Model is new'ed?

is there a way to declare a Model schema in mongoose so that when the model is new'ed the _id field would auto-generate?
for example:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectIdSchema = Schema.ObjectId;
var ObjectId = mongoose.Types.ObjectId;
var PersonSchema = new Schema({
_id: ObjectIdSchema,
firstName: {type: String, default: 'N/A'},
lastName: {type: String, default: 'N/A'},
age: {type: Number, min: 1}
});
var Person = mongoose.model('Person', PersonSchema);
at first, i thought great!, i'll just do
_id: {type:ObjectIdSchema, default: new ObjectId()}
but of course that doesn't work, because new ObjectId() is only called on initialize of schema. so calling new Persion() twice creates two objects with the same _id value.
so is there a way to do it so that every time i do "new Person()" that a new ObjectId() is generated?
the reason why i'm trying to do this is because i need to know the value of the new person's _id value for further processing.
i also tried:
var person = new Person({firstName: "Joe", lastName: "Baz"});
person.save(function(err, doc, num){
console.log(doc._id);
});
even then, doc doesn't contain the ObjectId. but if i look in the database, it does contain it.
p.s. i'm using mongoose 2.7.1
p.p.s. i know i can manually create the ObjectId when creating the person as such:
var person = new Person({_id: new ObjectId(), firstName: "Joe", lastName: "Baz"});
but i rather not have to import ObjectId and have to new it every time i want to new a Person. guess i'm used to using the java driver for mongodb, where i can just create the value for the _id field in the Model constructor.
Add the auto flag:
_id: {
type: mongoose.Schema.Types.ObjectId,
index: true,
required: true,
auto: true,
}
source
the moment you call var person = new Person();
person._id should give you the id (even if it hasn't been saved yet). Just instantiating it is enough to give it an id. You can still save it after, and that will store the id as well as the rest of the person object
Instead of:
_id: {type:ObjectIdSchema, default: new ObjectId()}
You should do:
_id: {type:ObjectIdSchema, default: function () { return new ObjectId()} }
Taken from the official MongoDB Manual and Docs:
_id
A field required in every MongoDB document. The _id field must have a
unique value. You can think of the _id field as the document’s primary
key. If you create a new document without an _id field, MongoDB
automatically creates the field and assigns a unique BSON ObjectId.
Source
ObjectID
ObjectIds are small, likely unique, fast to generate, and ordered.
ObjectId values consist of 12 bytes, where the first four bytes are a
timestamp that reflect the ObjectId’s creation. Specifically:
a 4-byte value representing the seconds since the Unix epoch, a 5-byte
random value, and a 3-byte counter, starting with a random value. In
MongoDB, each document stored in a collection requires a unique _id
field that acts as a primary key. If an inserted document omits the
_id field, the MongoDB driver automatically generates an ObjectId
for the _id field.
This also applies to documents inserted through update operations with
upsert: true.
MongoDB clients should add an _id field with a unique ObjectId.
Using ObjectIds for the _id field provides the following additional
benefits: in the mongo shell, you can access the creation time of the
ObjectId, using the ObjectId.getTimestamp() method. sorting on an _id
field that stores ObjectId values is roughly equivalent to sorting by
creation time. IMPORTANT While ObjectId values should increase over
time, they are not necessarily monotonic. This is because they:
Only contain one second of temporal resolution, so ObjectId values
created within the same second do not have a guaranteed ordering, and
Are generated by clients, which may have differing system clocks.
Source
Explicitly declaring _id:
When explicitly declaring the _id field, specify the auto option:
new Schema({ _id: { type: Schema.ObjectId, auto: true }})
ObjectId only - Adds an auto-generated ObjectId default if turnOn is true.
Source
TL;DR
If you create a new document without an _id field, MongoDB automatically creates the field and assigns a unique BSON ObjectId.
This is good way to do this
in model:
const schema = new Schema({ userCurrencyId:{type: mongoose.Schema.Types.ObjectId,
index: true,
required: true,
auto: true});

Resources