Mongoose - Upserting Documents with Nested Models - javascript

I have a basic document with a 'checked_in' flag in my express app:
module.exports = Book= mongoose.model('Book', new Schema({
name : String,
checked_in : Boolean
},{ collection : 'Book' }));
I wanted to keep a log of when books are checked in and out so I came up with another schema:
var action = new Schema({
checked_in: Boolean,
});
module.exports = Activity = mongoose.model('Activity', new Schema({
book_id: String,
actions: [action]
},{ collection : 'Activity' }));
The 'book_id' should be the document id of a book and when I update a book I need to either create or update the activity log for that book with a new item inside of actions:
exports.update = function(req, res){
return Book.findById(req.params.id, function(err, book) {
var activity = new Activity({book_id: book.id});
activity.actions.push({
checked_in: req.body.checked_in,
});
Activity.update({ book_id: book.id}, activity.toObject(), { upsert: true }));
book.checked_in = req.body.checked_in;
return device.save(function(err) {
return res.send(book);
});
});
};
The problem I am having is that nothing gets inserted into the Activity collection. If I use .save() then i just get lots of duplicates in the collection.
UPDATE
I've started re-working things with the advice given below but am still not having any luck with this. Here's what I have now:
module.exports = Activity = mongoose.model('Activity', new Schema({
book_id: Schema.ObjectId,
actions: [new Schema({
checked_in: Boolean,
last_user: String
})]
},{ collection : 'Activity' }));
Here's the update code now:
exports.update = function(req, res){
// TODO: Check for undefined.
return book.findById(req.params.id, function(err, book) {
if(!err) {
// Update the book.
book.checked_in = req.body.checked_in;
book.last_user = req.body.last_user;
book.save();
// If there's no associated activity for the book, create one.
// Otherwise update and push new activity to the actions array.
Activity.findById(book._id, function (err, activity) {
activity.actions.push({
checked_in: req.body.checked_in,
last_user: req.body.last_user
})
activity.save();
});
}
});
};
What I want to end up with is a document for each book with an array of check outs/ins that gets updated each time someone checks a book in or out. i.e:
{
book_id: "5058c5ddeeb0a3aa253cf9d4",
actions: [
{ checked_in: true, last_user: 'ralph' },
{ checked_in: true, last_user: 'gonzo' },
{ checked_in: true, last_user: 'animal' }
]
}
Eventually I will have a time stamp within each entry.

There are a couple problems:
You're trying to find the book's activity doc using findById using the book's id instead of the activity's id.
You're not handling the case where the book's activity doc doesn't exist yet.
Try this instead:
Activity.findOne({book_id: book._id}, function (err, activity) {
if (!activity) {
// No Activity doc for the book yet, create one.
activity = new Activity({book_id: book._id});
}
activity.actions.push({
checked_in: req.body.checked_in,
last_user: req.body.last_user
});
activity.save();
});

I see a few things that can be improved...
The book_id field in the Activity model should be Schema.ObjectId instead of a String. You will then be able to use populate if you wish.
You aren't doing any error checking in exports.update. If the user passes in an invalid id, you will want to check if book is undefined or not, as well as the common if (err) return next(err) (this requires your function params to be res, res, next).
When you create the activity in exports.update, you want to use book._id instead of book.id
All the return statements are not needed
The device variable is not declared anywhere, I'm not sure what you are trying to save... I think you meant book there.
You can then just .save() the activity instead of doing the Activity.update.

Related

Friend Request System - Express, MongoDB, EJS

I want to create a social network thus allowing users to send and interact with frind requests. As of now I have created the register, log-in and "search for other users function".
When I find and select another user, I display their user-info and have created a "Add friend" button.
Can anyone help me in a direction of the creation of the "Add friend" option? I have looked around for some time now, and not been able to find the correct solution. Below I have attached my UserSchema and route for finding users:
//User Schema
const UserSchema = new mongoose.Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
password: {
type: String,
required: true
},
},{ collection: 'Users' });
//Get single user based on ID
router.get('/user/get:id', ensureAuthenticated, function (req, res) {
MongoClient.connect(DBUri,{useUnifiedTopology: true }, function (err, db) {
let dbo = db.db(DBName);
const query = {_id: objectId(req.params.id)}
dbo.collection("Users").find(query).toArray(function(err, resultTasks) {
if (err) throw err;
res.render('../View/findFriend', {
resultTasks: resultTasks
});
db.close();
});
});
});
You can add something like this in your user schema:
friends: [{ type : ObjectId, ref: 'User' }],
OR
friends: userSchema
Take the one which suits you.
What that will do is add an array to the user, Then you can store IDs of friends.(Who are other users, hence the ref: 'User')
Then, When you have to fetch users you can do:
User.find(<ID or whatever you have to find Users>).populate('friends')
Also, To push a new friend simply use: user.friends.push(newFriend._id)

Mongoose auto fill data by searching in reference

const UserSchema = new Schema(
{
referrals: {
ref: 'User',
type: [mongoose.Schema.Types.ObjectId],
},
referredBy: {
ref: 'User',
type: mongoose.Schema.Types.ObjectId,
},
}
);
I want Mongoose to find users who have current user _id in referredBy reference.
In other words, eg: find all users who have '_IDOfSpecificUser' in their referredBy field and put all the found users in the array of referrals where user's _id is '_IDOfSpecificUser'.
How can I handle that in mongoose?
Simplest is using find
User.
find({ "referredBy" : "xxxxxxxxxxxx" }).
exec(function (err, users) {
if (err) return handleError(err);
console.log('The users are an array: ', users);
});
Refer to https://mongoosejs.com/docs/populate.html
If you want to convert bellow function to static method inside UserSchema, please refer to this https://mongoosejs.com/docs/api.html#schema_Schema-static and https://mongoosejs.com/docs/2.7.x/docs/methods-statics.html

Removing one-one and one-many references - Mongoose

I have an Assignment schema which has references to Groups and Projects.
Assignment == Group [One-One Relationship]
Assignment == Projects [One-Many Relationship]
Below is my Asssignment Schema
var AssignmentSchema = new Schema({
name: String,
group: {
type: Schema.Types.ObjectId,
ref: 'Group'
},
projects: [{type: mongoose.Schema.Types.ObjectId, ref: 'Project'}],
});
If a Group/Project is removed, how can i update my Assignment Schema.
var ProjectSchema = new Schema({
name: String
});
var GroupSchema = new Schema({
name: String
});
From couple of answers in stackoverflow, i came to know about the remove middleware, but i am not sure how to implement it for one-one and one-many relationship. Can anyone show me an example of doing it.
ProjectSchema.pre('remove', function(next){
this.model('Assignment').update(
);
});
Relationships:
A one-to-one is a relationship such that a state has only one
capital city and a capital city is the capital of only one state
A one-to-many is a relationship such that a mother has many
children, and the children have only one mother
A many-to-many is a relationship such that a book can be written by
several authors or co-authors, while an author can write several
books.
one-one relationship - If a Project/Group is removed, how can i update my Assignment Schema.
Typically you will have one project mapped to one assignment and similarly one assignment mapped to one project. what you can do here is removing a project and then find the associated project in assignment model and remove their references.
delete: function(req, res) {
return Project.findById(req.params.id, function(err, project){
return project.remove(function(err){
if(!err) {
Assignment.update({_id: project.assignment}},
{$pull: {projects: project._id}},
function (err, numberAffected) {
console.log(numberAffected);
} else {
console.log(err);
}
});
});
});
}
one-many relationship - If a Project/Group is removed, how can i update my Assignment Schema.
In this scenario we are removing a project and then finding all the assignments which belongs to this project and removing its reference from them. Here the situation is, there can be many assignments for a single project.
delete: function(req, res) {
return Project.findById(req.params.id, function(err, project){
return project.remove(function(err){
if(!err) {
Assignment.update({_id: {$in: project.assingments}},
{$pull: {project: project._id}},
function (err, numberAffected) {
console.log(numberAffected);
} else {
console.log(err);
}
});
});
});
}
Remove middleware
You could achieve the same thing via middleware as pointed out by Johnny, just a correction on that..
ProjectSchema.pre('remove', function (next) {
var project = this;
project.model('Assignment').update(
{ projects: {$in: project.assignments}},
{ $pull: { project: project._id } },
{ multi: true },
next
);
});
Typically there can be many projects belonging to an assignment and many assignments belonging to the same project. You will have an assignment column in your Project Schema where one project will relate to multiple assignments.
Note: remove middleware won't work on models and it would only work on your documents. If you are going with remove middleware ensure in your delete function, you find project by id first and then on the returned document apply the remove method, so for the above to work... your delete function would look like this.
delete: function(req, res) {
return Project.findById(req.params.id, function(err, project){
return project.remove(function(err){
if(!err) {
console.log(numberAffected);
}
});
});
}
In the remove middleware, you're defining the actions to take when a document of the model for that schema is removed via Model#remove. So:
When a group is removed, you want to remove the group reference to that group's _id from all assignment docs.
When a project is removed, you want to remove the projects array element references to that project's _id from all assignment docs.
Which you can implement as:
GroupSchema.pre('remove', function(next) {
var group = this;
group.model('Assignment').update(
{ group: group._id },
{ $unset: { group: 1 } },
{ multi: true },
next);
});
ProjectSchema.pre('remove', function (next) {
var project = this;
project.model('Assignment').update(
{ projects: project._id },
{ $pull: { projects: project._id } },
{ multi: true },
next);
});

an error about tailable cursor for a query in mongoose

User model contian SubscriptionSchema and AccessToken Schema, I had defined this two plugin schemas with {capped : 234556} too.
var User = new Schema({
email : String
, firstName : String
, password : String
, isAdmin : Boolean
, lastSeen : Date
, subscriptions : [ SubscriptionSchema ]
, accessTokens : [ AccessToken ]
}, {
toObject : { virtuals : true }
, toJSON : { virtuals : true }
, capped : 234556
});
var streamTest = User.find().limit(1).tailable().stream();
When I try to run the above code, I still get the error:
MongoError: tailable cursor requested on non capped collection
That doesn't look like a correct usage of a capped collection or tailable stream. But perhaps a little code first to demonstrate a working example:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var userSchema = new Schema({
email: String,
},{
capped: 2048
});
var User = mongoose.model( "User", userSchema );
mongoose.connect('mongodb://localhost/atest');
var stream;
async.waterfall(
[
function(callback) {
var user = new User({ email: "existing" });
user.save(function(err,doc) {
if (err) throw err;
callback();
});
},
function(callback) {
User.find({}).sort({ "$natural": -1 }).limit(1).exec(function(err,docs) {
if (err) throw err;
console.log( docs );
callback(err,docs[0]);
});
},
function(doc,callback) {
stream = User.find({ "_id": { "$gt": doc._id } }).tailable().stream();
stream.on("data",function(doc) {
console.log( "Streamed:\n%s", doc );
});
callback();
},
function(callback) {
async.eachSeries(
['one','two','three'],
function(item,callback) {
var user = new User({ email: item });
user.save(function(err,doc) {
if (err) throw err;
console.log( "Saved:\n%s", doc );
callback();
});
},
function(err) {
if (err) throw err;
callback();
}
);
}
]
);
First things first, there really needs to be something in the capped collection for anything to work. This presumes that the collection does not exist and it is going to be initialized as a capped collection. Then the first step is making sure something is there.
Generally when you want to "tail", you just want the new documents that are inserted to appear. So before setting up the tailable cursor you want to find the "last" document in the collection.
When you know the last document in the collection, the "tailable stream" is set up to look for anything that is "greater than" that document, which are the new documents. If you did not do this, your first "data" event on the stream would empty all of the current collection items. So the options to .sort() and .limit() here do not apply. Tailing cursors initialize and "follow".
Now that the streaming interface is set up and an listener established, you can add items to the stream. These will then log accordingly, yet as this is "evented", there is no particular sequence to the logging here as either the "save" or the "stream" data event may actually fire first.
Now onto your implementation. These two lines stand out:
, subscriptions : [ SubscriptionSchema ]
, accessTokens : [ AccessToken ]
Those contain embedded arrays, they are not "external" documents in another collection, even though it would not matter if it even did.The general problem here is that you are (at least in some way) introducing an array, which seems to imply some concept of "growth".
Unless your intention is to never "grow" these arrays and only ever insert the content with the new document and never update it, then this will cause problems with capped collections.
Documents in capped collections cannot "grow" beyond their initial allocated size. Trying to update where this happens will result in errors. If you think you are going to be smart about it and "pad" your documents, then this is likely to fail when replicated secondary hosts "re-sync". All documented with capped collections.

Mongoose Followers/Friends Relationship Query

I am trying to make a query that returns posts from both Friends and Non-Friends, but ensures Friends Posts are at top of list. What I have now only gets posts from Friends:
Schemas
var Post = mongoose.Schema({
name: String,
user: { type:ObjectId, ref:'User' },
createdAt: { type:Date, default:Date.now }
});
var User = mongoose.Schema({
username: String
});
var Relationship = mongoose.Schema({
from: { type:ObjectId, ref:'User' },
to: { type:ObjectId, ref:'User' }
});
Query looks like
Relationship.find({ from : thisUser },function(err,docs){
if (err) {console.log(err);}
var query = Post.find();
var plucked = _.pluck(docs,'to');
query.where('user').in( plucked );
query.sort('-createdAt');
query.limit(20);
query.skip( 20 * page);
query.exec(function(err,posts){
if (err) {console.log(err);}
res.send(posts);
});
});
The client will grab say 20 posts on each page. So any suggestions on how I can return all posts, but ensure posts by Friends appear first? For instance, if there are 100 posts that meet the query criteria and 30 of those are from friends, the first page and half of the second will all be friends posts (sorted by createdAt).
If I need to redo the schemas and relationships thats fine as well.
What I would have done is to skip the Relationship model all together (if it is not absolutely essential) and have a friends field in User like this instead:
var User = mongoose.Schema({
username: String,
friends: [User],
});
Then you're able to query the friends posts like this:
Post.find({user: {$in: thisUser.friends}}, function(err, posts) {
...
})
And append it with non-friends using $nin instead of $in.
Honestly, the best thing to do is to store the user names with the friendship object. This information is extremely unlikely to change. How many times have you changed your name on Facebook?

Categories