Mongoose: how to structure a schema with a SET of subdocuments (one unique field)? - node.js

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"?

Related

Using "deleteMany" post middleware in mongoose

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.

How to update a document using update query in mongoose?

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"))

Match specific value in mongoose populate

Following is the schema of a user collection:
const Mongoose = require('mongoose')
const Schema = Mongoose.Schema
const userSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String
},
supporterOf: [{
type: Schema.Types.ObjectId,
ref: 'individual',
required: false
}],
})
module.exports = Mongoose.model('user', userSchema);
I want to populate 'supporterOf' which is a collection of individual (ref: individual).
The 'supporterOf' contains an array of ObjectId.
I am having the problem of matching the specific objectId with that ObjectId array.
Can anyone suggest me how I can match specific ObjectId with an array of ObjectIds in populate function?
You have a reference of 'individual' in supporterOf and you want to populate only relevant object from the array of individuals?
If this is right case then do the following:
YourUserModel.findById(userId, function (err, user) {
console.log(user)
}).populate({
path: 'supporterOf',
match: {
yourObjectOfIndividualDocument: yourMatchingIdOfIndividual
}
})
.exec()
Replace yourObjectOfIndividualDocument: yourMatchingIdOfIndividual by name: 'abcd'.
Here name is the field of Individual document not of User document.
You add condition in populate
if you wanted to populate fans array based on their age, and return, at most, any 5 of them
.populate('fans', null, { age: { $gte: 21 }}, { limit: 5 })

Aggregate Data into Array MongoDB, Mongoose, Node

Ok, here is what I am trying to do. I have a user schema which has a team key and an array as its value. For example:
user = {
team : Array
}
The values of the array are strings (team names). I have a team schema that has the team information, such as roster, state, etc. Here is my question. If my user has more than one team name in the array, how then would I aggregate all the data to one variable and then pass it to my view. I thought I could just use a for loop, but the view seems to render before the aggregation is done. Here is what I tried:
// if there is there is more than one team name in the user's team array
if( data[0].teams.length > 1 ){
for(var i = 0; i < data[0].teams.length; i++){
// for each team name in the user array
// find the corresponding team and add that data to the array called team
Team.find( { team : data[0].teams[i] }, function(err, data){
if(err){
throw err;
}
team.push(data);
});
}
// render the view
res.render('coach', {
user: req.user,
fname: self.fname,
lname: self.lname,
email: self.email,
phone: self.phone,
address: self.address,
state: self.state,
teams : [team]
});
}
Edit (added schemas)
User schema ( look in teams array, find corresponding teams, return data from team schema based on the name in the user schema teams array )
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
fname : { type: String, required: true },
lname : { type: String, required: true },
password : { type: String, required: true },
type : { type: String, required: true },
phone : String,
address : String,
state: String,
conference: String,
email : { type: String, required: true, index: { unique : true } },
teams: Array,
});
Team Schema:
var teamsSchema = new Schema({
coach: String,
state: String,
conference: String,
team: String,
roster: [{fname: String, lname: String, number: String}]
});
Now that you have posted your schema It is clear what you are trying to do and also clear how to show your various approaches to do this much better.
The first and basic case is that you seem to have an array of "teams" containing the "String" value for the "team name" presumably stored on your user object. This appears to work for you and does have the advantage of having those names accessible as you retrieve the user.
The thing is that you are iterating over the results and issuing a .find() for every element in the array, which is not the most efficient way. You can basically use the $in operator with your query and existing data and get around what you probably tried to do in merging the full "team data" to the user object by calling .toObject() to transform the mongoose document to an plain JavaScript object:
User.findById( req.user, function(err,user) {
var data = user.toObject();
Team.find({ "team": "$in": data.teams }, function(err,teams) {
data.teams = teams;
res.render( 'coach', data );
});
});
That is the simple approach, but really there are features available to mongoose that will sort of do this for you if you change your schema a little to reference the data in the Team model. So you can then just fill in using .populate():
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
fname : { type: String, required: true },
lname : { type: String, required: true },
password : { type: String, required: true },
type : { type: String, required: true },
phone : String,
address : String,
state: String,
conference: String,
email : { type: String, required: true, index: { unique : true } },
teams: [{ type: ObjectId, ref: "Team" }]
});
Now that stores just the references _id value and where to get it from. So now your query becomes this:
User.findById( req.user).populate("teams").exec(function(err,user) {
res.render("coach", user);
});
So what this does is basically the equivalent of the first example but using the _id values and in a little cleaner way, and the user object now has pulled in all of the data from the Team schema matching the list of _id values stored in your user array.
Of course you don't have the same immediate access to just the team name as you did before, but you can work around this by selecting the fields to populate with something like this:
User.findById( req.user).populate("teams","team").exec(function(err,user) {
res.render("coach", user);
});
So that would only pull in the "team" field from each object and is much the same as what you originally had as a result, albeit with actually two queries (under the hood).
Finally if you can live with the concept of a little data replication, then the most efficient way is to simply embed the data. While there is duplicated data being stored,the reading is the most efficient as it is a single query. If you don't need to regularly update that data, this may be worth consideration:
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
fname : { type: String, required: true },
lname : { type: String, required: true },
password : { type: String, required: true },
type : { type: String, required: true },
phone : String,
address : String,
state: String,
conference: String,
email : { type: String, required: true, index: { unique : true } },
teams: [teamSchema]
});
So with that data embedded you only need to retrieve the user and the "teams" data would already be there:
User.findById( req.user, function(err,user) {
res.render("coach", user);
});
So those are a few approaches to take. All show that the looping of the array is not required, and that the number of queries you are actually issuing can be greatly reduced and even down to 1 if you can live with that.
Firstly, the find operations are asynchronous and although you are waiting for a response before pushing to the team array you are not waiting for all responses before responding with res.render(). Something like below should show what I mean:
// if there is there is more than one team name in the user's team array
if( data[0].teams ){
var teamArr = [];
var responses = 0;
for(var i=0; i<data[0].teams.length; i++){
// for each team name in the user array
// find the corresponding team and add that data to the array called team
Team.find( { team : data[0].teams[i] }, function(err, team){
if(err){
throw err;
}
responses ++;
teamArr.push(team);
// if all teams have been pushed then call render
if (responses == data[0].teams.length - 1) {
// render the view
res.render('coach', {
user: req.user,
fname: self.fname,
lname: self.lname,
email: self.email,
phone: self.phone,
address: self.address,
state: self.state,
teams : teamArr
});
}
});
}
}
Edit: I changed some naming to avoid potential naming clashes (two data variables for example)
Secondly, another option if you change your document structure, given you are using mongoose, if you stored references to the team document objects in your array instead of name strings you could use the mongoose populate method and populate the array before returning the whole document.
Update: Based on Schema provided you could use populate as follows:
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
....
email : { type: String, required: true, index: { unique : true } },
teams: [{
type: Schema.Types.ObjectId, ref: 'teams'
}],
});
And then populate the document before returning it avoiding the for loop:
User.findOne({_id: userId})
.populate('teams')
.exec(function (err, user) {
if (err) return handleError(err);
res.render('coach', {
user: filterUser(user);
});
});
where user.teams now contains a populated array of teams and filterUser() returns a user object with only the properties you require;

Mongoose Relationship Populate Doesn't Return results

var SecuritySchema = new Mongoose.Schema({
_bids: [{
type: Mongoose.Schema.Types.ObjectId,
ref: 'BuyOrder'
}],
_asks: [{
type: Mongoose.Schema.Types.ObjectId,
ref: 'SellOrder'
}]
});
var OrdersSchema = new Mongoose.Schema({
_security: {
type: Mongoose.Schema.Types.ObjectId,
ref: 'Security'
},
price: {
type: Number,
required: true
},
quantity: {
type: Number,
required: true
}
});
// declare seat covers here too
var models = {
Security: Mongoose.model('Security', SecuritySchema),
BuyOrder: Mongoose.model('BuyOrder', OrdersSchema),
SellOrder: Mongoose.model('SellOrder', OrdersSchema)
};
return models;
And than when I save a new BuyOrder for example:
// I put the 'id' of the security: order.__security = security._id on the client-side
var order = new models.BuyOrder(req.body.order);
order.save(function(err) {
if (err) return console.log(err);
});
And attempt to re-retrieve the associated security:
models.Security.findById(req.params.id).populate({
path: '_bids'
}).exec(function(err, security) {
// the '_bids' array is empty.
});
I think this is some sort of naming issue, but I'm not sure, I've seen examples here and on the moongoose website that use Number as the Id type: http://mongoosejs.com/docs/populate.html
The ref field should use the singular model name
Also, just do:
models.Security.findById(req.params.id).populate('_bids').exec(...
My main suspicion given your snippet at the moment is your req.body.order has _security as a string instead of an array containing a string.
Also, you don't need an id property. Mongodb itself will automatically do the _id as a real BSON ObjectId, and mongoose will add id as a string representation of the same value, so don't worry about that.
While I don't understand your schema (and the circular nature of it?), this code works:
var order = new models.BuyOrder({ price: 100, quantity: 5});
order.save(function(err, orderDoc) {
var security = new models.Security();
security._bids.push(orderDoc);
security.save(function(err, doc) {
models.Security.findById({ _id: doc._id })
.populate("_bids").exec(function(err, security) {
console.log(security);
});
});
});
It:
creates a BuyOrder
saves it
creates a Security
adds to the array of _bids the new orderDoc's _id
saves it
searches for the match and populates
Note that there's not an automatic method for adding the document to the array of _bids, so I've done that manually.
Results:
{ _id: 5224e73af7c90a2017000002,
__v: 0,
_asks: [],
_bids: [ { price: 100,
quantity: 5,
_id: 5224e72ef7c90a2017000001, __v: 0 } ] }

Resources