add a item inside a nested schema mongoose with addToSet - node.js

I know populating schemas is not a new question but I am having a little trouble following the logic on this in regards to multiple schemas. I am working with
"mongoose": "^4.8.5",
"express": "^4.15.0",
I have a schema with a collection of caffeine drinks. When a user selects a drink i would like for that drink to be assigned to the user.
** If at any point I am missing something simple in the architecture please let me know. This project has been my intro to mongodb.
I am reading through populating on the mongoose documentation http://mongoosejs.com/docs/populate.html.
Essentially, if I am to assign the drinks to the list it looks like I want to add them as a reference in an array. This was my approach with caffeine_list
const SelectedDrinks = require('./userDrinks');
const UserSchema = mongoose.Schema({
name: {
type: String
},
email: {
type: String,
required: true
},
username: {
type: String,
required: true
},
password: {
type: String,
required: true
},
caffeine_list: caffeine_list: [ // attempting to reference selected drinks
{
type: mongoose.Schema.Types.ObjectId,
ref: 'SelectedDrinks'
}
]
})
SelectedDrinks comes from the schema below. I added a reference to the user as the creator below
const User = require('./user');
let userDrinkSchema = new mongoose.Schema({
creator : {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
caffeine: Number,
mgFloz: Number,
name: String,
size: Number,
updated_at: {
type: Date,
default: Date.now()
}
});
This is where I start to get confused. I initially tried populate but could not get it going. If that was correct please let me know.
In regards to my task of adding a selected drink to the user I used addToSet. I was hoping that this would give me the drink info. I did my set up like so....
const User = require('../../models/user');
const UserDrinks = require('../../models/userDrinks');
router.post('/addDrink', (req, res, next) => {
let newDrink = new UserDrinks({
creator: req.body.creator,
caffeine: req.body.caffeine,
mgFloz: req.body.mgFloz,
name: req.body.name,
size: req.body.size,
updated_at: req.body.updated_at
});
newDrink.save( (err) => {
if(err) {
res.send(err);
} else {
User.findOne({ _id: newDrink.creator}, (err, user) => {
user.caffeine_list.addToSet(newDrink)
user.save( function (err) {
if(err) {
console.log(err);
}else {
res.status(201).json(newDrink);
}
})
})
}
})
});
However, after i do a post in postman I check caffeine_list and the result is
"caffeine_list" : [
ObjectId("58d82a5ff2f85e3f21822ab5"),
ObjectId("58d82c15bfdaf03f853f3864")
],
Ideally I would like to have an array of objects being passed with the caffeine info like so
"caffeine_list" : [
{
"creator": "58d6245cc02b0a0e6db8d257",
"caffeine": 412,
"mgFloz": 218.7,
"name": "1.95 Perfect Drink!",
"size": 42.93,
"updated_at": "2017-03-24T18:04:06.357Z"
}
]

Change your else part with below code instead of findOne and save use update
User.update(
{ _id: newDrink.creator},
{ $addToSet:{
caffeine_list: newDrink
}}).exec(function (err, updatedrink){
if(err) {
console.log(err);
}else {
res.status(201).json(updatedrink);
}
})

Although I am not sure this is the best approach I did find this to be give me the result that I was desiring. I had to make two small changes and I was able to get the caffeine_list to give me the desired response
I had to access the schema for selected drinks
const SelectedDrinks = require('./userDrinks').schema; //** need schema
Afterwards I was able to change
caffeine_list: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'UserDrinks' // name of the file
}
]
to
caffeine_list: [SelectedDrinks]
Now that I have the schema I am able to add the drinks directly into the caffeine_list on the UserSchema.

Related

Delete all nested items in database when deleting a top item using MongoDB and Express

I have three collections in my mongodb: clients, programs, and data. Data is nested in Programs and Programs are nested in Clients. That is a single client can have multiple programs and every such program can have a number of Data sets. Here is how clients' schema looks like:
const ClientSchema = new Schema({
fullName: { type: String, required: true },
dateEnrolled: { type: Date, required: true },
isCurrent: { type: String, required: true },
Programs: [{ type: Schema.Types.ObjectId, ref: 'Program' }]
});
// and my Programs schema looks like:
const ProgramSchema = new Schema({
programName: { type: String, required: true },
dateStarted: { type: Date, required: true },
dateFinished: { type: Date },
Data: [{ type: Schema.Types.ObjectId, ref: 'Answer' }]
});
Now, when I delete a client from my database I want to delete all the programs that belong to the clients and all the data sets that were created for those programs. Please, help me.
Try something like this:
router.delete('/:clientId', (req, res, next) => {
const id = req.params.id;
Client.findById(id).then((client) => {
client.Programs.forEach((programId) => {
Program.findById(programId).then((program) => {
Answer.deleteMany({ _id: { $in: program.Data } });
});
Program.deleteOne({ _id: programId })
});
Client.deleteOne({ _id: client._id })
})
});
You can register a middleware function for remove in ClientSchema.
ClientSchema.pre('remove', { query: true, document: true }, async function() {
// call deleteMany on ProgramModel
await ProgramModel.deleteMany({ _id: { $in: this.Programs } });
});
You can do the same thing for ProgramSchema if you want to cascade delete Answer.
Nenad, thank you very much, again! You gave me an idea to loop through first program ids, then through programs. My solution comes in two parts:
Part 1:
`router.delete('/:id', (async (req, res, next) => {
const { id } = req.params;
const client = await Client.findById(id);
await client.Programs.forEach(async (element) => {
const programs = [];
const program = await Program.findById(element);
programs.push(program);
programs.forEach(async (program) => {
await Answer.deleteMany({ _id: { $in: program.Data } });
})
});
await Client.findByIdAndDelete(id);
res.redirect('/clients');
}))`
part 2 that goes to Client schema file and makes sure that all the programs are deleted as well:
`ClientSchema.post('findOneAndDelete', async function (doc) {
if (doc) { await Program.deleteMany({ _id: { $in: doc.Programs } }) }
})`
Now, when I delete a client all instances of related programs are deleted and all instances of data-sets related to each program are deleted, too. Thank you, guys, are all awesome!

It is possible to pull elements from a referred objects' array using mongoose?

I have 2 mongo schemas related one with the other using ObjectId:
var User = new Schema({
username: {
type:String,
unique: true
},
password: {
type:String
},
verified: {
type: Boolean,
default: false
},
lastActivity:{
type:Date,
default:Date.now
}
});
And a watitingRoom schema with lists all the users:
var WaitingRoom = new Schema({
lastActivity:{
type:Date,
default:Date.now
},
clients: [{
type : mongoose.Schema.ObjectId,
ref: 'User'
}],
videocalls: [{
type: mongoose.Schema.ObjectId,
ref:'VideoCall'
}]
});
So, I want to 'refresh' my clients array pulling all the clients which a lastActivity less than the current time. I tried it by using the $pull tool present in mongoose. After googling and mixing different examples I tried things like:
WaitingRoom.findOneAndUpdate({}, { lastActivity: new Date(),
$pull : {clients : {"clients.lastActivity": { $lt: new Date() }}}
}, options)
.populate("clients")
.exec( function(error, waitingRoom) {
if (err) { return res.status(500).send({ msg: err.message }); }
})
Which finds the unique waiting room, updates the lastActivity field and tries to pull all the clients that has a clients.lastActivity less than the current date.
(Obviously this snipped code doesn't work)
The problem is that I didn't find any documentation or example that explains if it is possible to pull elements from a referred ObjectId schema using a nested condition clients.lastActivity
You need to first find the ids from User database and then need to $pull them from the WaitingRoom database
User.find({ lastActivity: new Date() }).then((users) => {
const ids = []
users.map((user) => {
ids.push(user._id)
})
WaitingRoom.update({}, { $pull: { clients: ids }}, { multi: true }).then(() => {
console.log('removed')
})
})

Why $pull does not work when using mongodb on node js [duplicate]

i'm trying to do a pretty simple operation, pull an item from an array with Mongoose on a Mongo database like so:
User.update({ _id: fromUserId }, { $pull: { linkedUsers: [idToDelete] } });
fromUserId & idToDelete are both Objects Ids.
The schema for Users goes like this:
var UserSchema = new Schema({
groups: [],
linkedUsers: [],
name: { type: String, required: true, index: { unique: true } }
});
linkedUsers is an array that only receives Ids of other users.
I've tried this as well:
User.findOne({ _id: fromUserId }, function(err, user) {
user.linkedUsers.pull(idToDelete);
user.save();
});
But with no luck.
The second option seem to almost work when i console the lenghts of the array at different positions but after calling save and checking, the length is still at 36:
User.findOne({ _id: fromUserId }, function(err, user) {
console.log(user.linkedUsers.length); // returns 36
user.linkedUsers.pull(idToDelete);
console.log(user.linkedUsers.length); // returns 35
user.save();
});
So it looks like i'm close but still, no luck. Both Ids are sent via the frontend side of the app.
I'm running those versions:
"mongodb": "^2.2.29",
"mongoose": "^5.0.7",
Thanks in advance.
You need to explicitly define the types in your schema definition i.e.
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
linkedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }]
and then use either
User.findOneAndUpdate(
{ _id: fromUserId },
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
or
User.findByIdAndUpdate(fromUserId,
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
I had a similar issue. I wanted to delete an object from an array, using the default _id from mongo, but my query was wrong:
const update = { $pull: { cities: cityId }};
It should be:
const update = { $pull: { cities: {_id: cityId} }};

Updating 2 mongoose schemas in an api call

Currently I'm trying to update Two different User Schema's in an api call.
The first schema is logged in user schema, we give it a name = Tom
The second schema is other users who signup for the app, we give it a name = John
The schema code
schema.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');
var UserSchema = new Schema({
name: String,
username: { type: String, required: true, index: { unique: true }},
password: { type: String, required: true, select: false },
followers: [{ type: Schema.Types.ObjectId, ref: 'User'}],
following: [{ type: Schema.Types.ObjectId, ref: 'User'}],
followersCount: Number,
followingCount: Number
});
module.exports = mongoose.model('User', UserSchema);
The api name is '/follow/:user_id', what I want to achieve is . Whenever user Tom follows other user's like John, Tom's following field will be updated as well as John's follower field.
My current attempt (req.decoded.id is the logged in user)
api.js
// The first way
apiRouter.post('/follow/:user_id', function(req, res) {
User.findOneAndUpdate(
{
_id: req.decoded.id,
following: { $ne: req.params.user_id }
},
{
$push: { following: req.params.user_id},
$inc: { followingCount: 1}
},
function(err, currentUser) {
if (err) {
res.send(err);
return;
}
console.log(currentUser);
});
User.findOneAndUpdate(
{
_id: req.params.user_id,
followers: { $ne: req.decoded.id }
},
{
$push: { followers: req.decoded.id },
$inc: { followersCount: 1}
}, function(err, user) {
if(err) {
res.send(err);
return;
}
res.json({
message: "Successfully followed"
});
}
)
});
//Second way
apiRouter.post('/follow/:user_id', function(req, res) {
// find a current user that has logged in
User.update(
{
_id: req.decoded.id,
following: { $ne: req.params.user_id }
},
{
$push: { following: req.params.user_id},
$inc: { followingCount: 1}
},
function(err) {
if (err) {
res.send(err);
return;
}
User.update(
{
_id: req.params.user_id,
followers: { $ne: req.decoded.id }
},
{
$push: { followers: req.decoded.id },
$inc: { followersCount: 1}
}
), function(err) {
if(err) return res.send(err);
res.json({ message: "Successfully Followed!" });
}
});
});
Both have problems,
The first way: The problem is, 'Can't set headers that already sent', because of the two separate mongoose query in one api call, it response twice that's why I get that error.
The second way: The problem is, the following field of logged in user(Tom) gets updated while the other user's followers field (John) return null. I console log both value and as well test it with POSTMAN chrome app.
Lend me your thoughts fellas!
The first route you took seems to be fine.
However, as #cdbajorin mentioned, the error "can't send headers that already sent" has nothing to do with mongoose but the fact that you're trying to set the header after sending a response to the client already. (see this lovely answer)
My suggestion would be to make sure that both database calls are successful before you send a response.
You may also want to look into a two phase commit in this situation, as MongoDB does not support traditional DB transactions and you're updating two documents, one at a time. If for some reason either database call fails, a procedure to recover to a stable state should be taken.
The first way can be improved in two ways. One is updating followers field inside the callback of updating following field. The other way is using async-waterfall. I suggest to go with async-waterfall(npm async-waterfall).
The second way it is correct (could be improved running both of them in parallel) I guess the problem is in another place. I don't know which framework you are using but i guess the field _id is from mongoDB and is an ObjectId and looks like that the decoded.id can be an objectId while the one that comes from the request is of course just a string. So I guess it is empty because it does not find any user with that string.
Try do make it an objectId out of that string ( reffering to req.params.user_id in the second query)

Insert into embedded document

I have the following schema:
var UserSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true },
userType: { type: String, default: 'user'},
quizzHistory: [{
quizzName: String,
quizzScore: Number
}]
});
my goal is to change document into embedded quizzHistory or insert new one if not exists document in embedded quizzeHistory
I try to set document into embedded quizzHistory :
User.findOneAndUpdate({ _id: req.session.user['_id'], 'quizzHistory.quizzName': testName},
{
'$set': {
'quizzHistory.$.quizzName': testName,
'quizzHistory.$.quizzScore': finalScore
}
}, {upsert: true},
function(err, upd) {
console.log("added");
})
code above works if there is document in quizzHistory with required _id and quizzHistory.quizzName,but don't pushed new one if there isn't any document.
Is there any way in Mongodb to change document into embedded collection or insert new one if not exists ?
the reason is because you are using "find and update" you are not handling the condition when the row hasn't been found and create a new document, being said that you need manage the in a different way like
User.update({ _id: req.session.user['_id'], 'quizzHistory.quizzName': testName},
{
'$push': {
'quizzHistory.quizzName': testName,
'quizzHistory.quizzScore': finalScore
}
}, {upsert: true},
function(err, upd) {
console.log("added");
})
this worked for me
User.update({ _id: req.session.user['_id'],'quizzHistory.quizzName':testName},
{
$set: {
"quizzHistory.$.quizzName":testName,
"quizzHistory.$.quizzScore":finalScore
}
},
function(err, upd) {
if(!upd){
User.update({ _id: req.session.user['_id']},
{ "$addToSet": { quizzHistory: newScoreData }},function(err,data){
});
}
});
If you want to benefit for all possible plugins and methods added to model and don't want to fight with actual Mongo queries you should first retrieve user object, then push new element to quizzHistory array and do save. Something like below (you need to align that code to your needs).
var entry = {
quizzName : 'abc',
quizzScore : 123
};
User.findOne({_id:id, function(err, object) {
if(err) {
return someErrorCallback();
}
var
object.quizzHistory.push(entry);
object.save(function(err) {
if(err) {
return someErrorCallback();
}
someSuccessCallback();
});
});
Updating directly may be efficient, but questions usage of mongoose.
Hope it makes sense.

Resources