In my Nodejs and Express app, I have a mongoose User schema, a Post schema and a Comment schema as follows:
const UserSchema = new Schema({
username: {
type: String,
required: true,
unique: true
},
password: String,
posts : [
{
type : mongoose.Schema.Types.ObjectId,
ref : 'Post'
}
]
});
const PostSchema = new Schema({
author : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
createdAt: { type: Date, default: Date.now },
text: String,
comments : [
{
type : mongoose.Schema.Types.ObjectId,
ref : 'Comment'
}
],
});
const CommentSchema = new Schema({
author : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
createdAt: { type: Date, default: Date.now },
text: String
});
I have coded the general CRUD operations for my User. When deleting my user, I can easily delete all posts associated with that user using deleteMany:
Post.deleteMany ({ _id: {$in : user.posts}});
To delete all the comments for all the deleted posts, I can probably loop through posts and delete all the comments, but I looked at mongoose documentation here and it seems that deleteMany function triggers the deleteMany middleware. So In my Post schema, I went ahead and added the following after defining schema and before exporting the model.
PostSchema.post('deleteMany', async (doc) => {
if (doc) {
await Comment.deleteMany({
_id: {
$in: doc.comments
}
})
}
})
When deleting user, this middleware is triggered, but the comments don't get deleted. I got the value of doc using console.log(doc) and I don't think it includes what I need for what I intend to do. Can someone tell me how to use the deleteMany middleware properly or if this is not the correct path, what is the most efficient way for me to delete all the associated comments when I delete the user and their posts?
deleteMany will not give you access to the affected document because it's a query middleware rather than a document middleware (see https://mongoosejs.com/docs/middleware.html#types-of-middleware). Instead it returns the "Receipt-like" object where it tells it successfully deleted n objects and such.
In order for your hook to work as expected, you'll need to use something other than deleteMany, such as getting all of the documents (or their IDs), and loop through each one, using deleteOne.
Related
This is the comment key pair I have in my post model:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const postSchema = new Schema({
user:{
type:Schema.Types.ObjectId,
// required:true,
refPath:'onModel'
},
onModel:{
type:String,
enum:['Doctor','Patient']
},
text:{
type:String,
required:true
},
comments:[{
user:{
type:Schema.Types.ObjectId,
refPath:'onModel'
},
reply:{
type:String
},
date:{
type:Date,
default:Date.now
}
}],
likes:[{
user: {
type: Schema.Types.ObjectId,
ref: 'Patient'
}
}]
})
module.exports= post = mongoose.model('post', postSchema);
When I try pushing object to the likes array by running the following code, it fails. The filter part works fine, just some problem with the update part which ends up executing catch block.
Post.updateOne({ _id: req.params.postid, "likes": { $ne : { user:
authorizedData.jwt_payload.patient._id }}},
{ "$set" : { "likes.$.user": "authorizedData.jwt_payload.patient._id"
}})
.then(re => res.json(re))
.catch(err => res.json("already liked"))
Will really appreciate any help.
Please make changes as below :
const mongoose = require('mongoose');
const patientObjectID = mongoose.Types.ObjectId(authorizedData.jwt_payload.patient._id);
Post.updateOne({
_id: req.params.postid,
'likes.user': {
$ne:
patientObjectID
}
},
{ $push: { likes: { user: patientObjectID } }
}).then(re => res.json(re)).catch(err => res.json("already liked"))
Couple of changes need to be done, So When you've a schema like this ::
likes: [{
user: {
type: Schema.Types.ObjectId,
ref: 'Patient'
}
}]
You need to pass an ObjectId() to user field but not a string, So
first we're converting string to ObjectId() & passing it in query.
Also $set is used to
update existing or insert new fields in a document, but when you wanted to push
new values to an array field in a document then you need to use
$push(this seems to be a normal update operation on a field, but here we're not replacing the likes array, rather we're just adding few more elements to it - different kind of update though, So that's why we need to use $push).
As you already have below filter, we're just doing $push assuming what we're pushing is not a duplicate but in the other way you can blindly use $addToSet to do the same without having to use below filter criteria :
"likes": {
$ne: {
user:
patientObjectID
}
}
About your question on $(update) why it isn't working ? This should be used to update the elements in an array, it helps to update first matching element in an array based on filter criteria, but what you wanted to do here is to add few more elements but not updating existing elements in likes array.
Here you should not send "already liked" in catch block, it should be a custom error for an actual error, in .then(re => res.json(re)) you need to check write result of update operation if anything updated you need to send added user, if not you need to send "already liked".
Hope this solves all your questions :-)
Try using $push aggregation which is used for pushing objects to inner arrays in mongoDB. Your update query should be something like the following:
Post.updateOne({ _id: req.params.postid, "likes": { $ne : { user:
authorizedData.jwt_payload.patient._id }}},
{ "$push" : { "likes": authorizedData.jwt_payload.patient._id
}})
.then(re => res.json(re))
.catch(err => res.json("already liked"))
I have the following method in a little node/express app :
async getAll(req, res) {
const movies = await movieModel
.find()
.populate({path: 'genres', select: 'name'})
.skip(0)
.limit(15);
return res.send(movies);
};
With the following schema :
const MovieSchema = new mongoose.Schema({
externalId: { required: true, type: Number },
title: { required: true, type: String },
genres: [{ ref: "Genre", type: mongoose.Schema.Types.ObjectId }],
releaseDate: {type: Date},
originalLanguage: {type : String},
originalTitle: {type : String},
posterPath: {type : String},
backdropPath: {type : String},
overview: {type: String},
comments: [{ ref: "Comment", type: mongoose.Schema.Types.ObjectId }],
votes: [VoteSchema]
}, {timestamps: true}
});
MovieSchema.virtual("averageNote").get(function () {
let avg = 0;
if (this.votes.length == 0) {
return '-';
}
this.votes.forEach(vote => {
avg += vote.note;
});
avg = avg / this.votes.length;
return avg.toFixed(2);
});
MovieSchema.set("toJSON", {
transform: (doc, ret) => {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
},
virtuals: true,
getters: true
});
However the query always return all document entries.
I also tried to add exec() at the end of the query or with .populate({path: 'genres', select: 'name', options: {skip: 0, limit: 15} }) but without result.
I tried on an other schema which is simpler and skip/limit worked just fine, so issue probably comes from my schema but I can't figure out where the problem is.
I also tried with the virtual field commented but still, limit and sort where not used.
My guess is that it's comes from votes: [VoteSchema] since it's the first time I use this, but it was recommanded by my teacher as using ref
isn't recommended in a non relational database. Furthermore, in order to calculate the averageNote as a virtual field, I have no other choice.
EDIT : just tried it back with votes: [{ ref: "Vote", type: mongoose.Schema.Types.ObjectId }] And I still can't limit nor skip
Node version : 10.15.1
MongoDB version : 4.0.6
Mongoose version : 5.3.1
Let me know if I should add any other informations
This is actually more about how .populate() actually works and why the order of "chained methods" here is important. But in brief:
const movies = await movieModel
.find()
.skip(0)
.limit(15)
.populate({path: 'genres', select: 'name'}) // alternately .populate('genres','name')
.exec()
The problem is that .populate() really just runs another query to the database to "emulate" a join. This is not really anything to do with the original .find() since all populate() does is takes the results from the query and uses certain values to "look up" documents in another collection, using that other query. Importantly the results come last.
The .skip() and .limit() on the other had are cursor modifiers and directly part of the underlying MongoDB driver. These belong to the .find() and as such these need to be in sequence
The MongoDB driver part of the builder is is forgiving in that:
.find().limit(15).skip(0)
is also acceptable due to the way the options pass in "all at once", however it's good practice to think of it as skip then limit in that order.
Overall, the populate() method must be the last thing on the chain after any cursor modifiers such as limit() or skip().
Basically I have two mongodb collections, users and projects.
I want to keep track on which projects a user is working on,
and also which users that are working on a given problem.
const User = mongoose.model('User', new Schema({
name: {
type: String,
},
_projects: [{
type: Schema.ObjectId,
ref: 'Project'
}]
})
and
const Project = mongoose.model('Project', new Schema({
title: {
type: String,
},
_usersWorkingOn: [{
type: Schema.ObjectId,
ref: 'User',
}]
})
Now, let's say i want to add multiple existing user-id to an existing project.
What would best practice be?
Right now I'm using findOneAndUpdate to get the project document to push the user-id to the array. And then a mongoose middleware were I take the last added entry and get the user-id from there:
Schema.post('findOneAndUpdate', addProjectToUser)
function addProjectToUser(updatedProject, next) {
User.findByIdAndUpdate(
updatedProject._usersWorkingOn[updatedProject._usersWorkingOn.length - 1],
{ $push: { '_projects': updatedProject._id}},
{ 'new': true }
).then(() => {
next()
})
}
This works when I add one user to a project. But if a want to add multiple users in one save, addProjectToUser have no way of knowing how many entries were added to the array.
Am I missing something here, or thinking about it all wrong?
Help! I'm losing my mind. I need to simply return a Mongo document, using Mongoose, IF a sub document does not exist.
My schemas:
var userSchema = new mongoose.Schema({
email: {type: String, unique: true, lowercase: true},
password: {type: String, select: false},
displayName: String,
picture: String,
facebook: String,
deactivation: deactiveSchema
});
var deactiveSchema = new mongoose.Schema({
when : { type: Date, default: Date.now, required: true },
who : { type: Schema.Types.ObjectId, required: true, ref: 'User' }
});
My goal is to lookup a user by their facebook ID if they have not been deactivated.
If they have been deactivated, then a deactivation subdocument will exist. Of course, to save space, if they are active then a deactivation will not exist.
On a side note, I'm also worried about how to properly construct the index on this logic.
I'd post snippets but every attempt has been wrong. =(
You can use $exists operator:
userSchema.find({deactivation:{$exists:false}}).exec(function(err,document){
});
or $ne:
userSchema.find({deactivation:{$ne:null}}).exec(function(err,document){
});
Since you are retiring data and not deleting, I'd go with one of two approaches:
Flag for retired (Recommended)
add to your schema:
retired: {
type: Boolean,
required: true
}
and add an index for this query:
userSchema.index({facebook: 1, retired: 1})
and query:
User.find({facebook: facebookId, retired: false}, callback)
Query for existence
User.find().exists("deactivation", false).exec(callback)
The latter will be slower, but if you really don't want to change anything, it will work. I'd recommend taking some time to read through the indexing section of the mongo docs.
Mongoose has many options for defining queries with conditions and a couple of styles for writing queries:
Condition object
var id = "somefacebookid";
var condition = {
facebook : id,
deactivation: { $exists : true}
};
user.findOne(condition, function (e, doc) {
// if not e, do something with doc
})
http://mongoosejs.com/docs/queries.html
Query builder
Alternatively, you may want to use the query builder syntax if you are looking for something closer to SQL. e.g.:
var id = "somefacebookid";
users
.find({ facebook : id }).
.where('deactivation').exists(false)
.limit(1)
.exec(callback);
I'm trying to make the following schema to work:
var FormSchema = new mongoose.Schema({
form_code: { type: String, unique: true },
...
});
var UserSchema = new mongoose.Schema({
...
submissions: [{
form_code: { type: String, unique: true },
last_update: Date,
questions: [{
question_code: String,
answers: [Number]
}]
}],
});
The rationale here is that a user can have many unique forms submitted, but only the last submission of each unique form should be saved. So, ideally, by pushing a submission subdocument when updating a user, the schema would either add the submission object to the set, or update the subdocument containing that form_code.
The following code doesn't work as desired (it pushes the new subdocument even if the form_code is already present):
User.findOneAndUpdate(
{ _id: user.id },
{ $addToSet: { submissions: submission_object } },
function (err, user) {
// will eventually have duplicates of form_code at user.submissions
}
);
The above schema clearly doesn't work, what must be changed to achieve that "upsertToSet"?