Mongoose - Go to next element - node.js

I am trying to iterate over different IDs over a collection in nodejs. Something that would work like the following code:
//Callbacks removed for readability
var thisPost = mongoose.model('Post').findOne({tags: 'Adventure'});
console.log(thisPost.title); // 'Post #1 - Adventure Part 1'
var nextPost = thisPost.next({tags: 'Adventure');
console.log(nextPost.title); // 'Post 354 - Adventure Part 2'
Best idea so far would be to add a linkedlist to my schema so I could call find() over my next reference to a specific ID but I was hoping for something less 'tricky' that would permit me to use this Mongoose reference (thisPost) as a cursor where my find() could start from.
Thanks
EDIT: The iteration is meant to work over multiple page queries. Better example:
//Callbacks removed for readability
//User 'JohnDoe' visits the website for the first time
var thisQuote = mongoose.model('Quote').findOne().skip(Math.rand());
res.send(thisQuote); // On page output, JohnDoe will see the quote 42
//Saving the current quote cursor to user's metadatas
mongoose.model('User').update({user: 'JohnDoe'}, {$set: {lastQuote: thisQuote }});
//User 'JohnDoe' comes back to the website
var user = mongoose.model('User').findOne({user: 'JohnDoe});
var thisQuote = user.lastQuote.next();
res.send(thisQuote); // On page output, JohnDoe will see the quote 43
//Saving the current quote cursor to user's metadatas
mongoose.model('User').update({user: 'JohnDoe'}, {$set: {lastQuote: thisQuote }});
//And so on...

You might look into Mongoose's streaming capabilities:
var stream = mongoose.model('Post').find({tags: 'Adventure'}).stream();
// Each `data` event has a Post document attached
stream.on('data', function (post) {
console.log(post.title);
});
QueryStream, which is what stream() returns, inherits from Node.js' Stream, so you can do some interesting things using pause and resume if you need to.
[Edit]
Now that I understand your question a bit more, I would say that a QueryStream is probably not what you want to use. I worked on this a bit today and got a working solution at https://gist.github.com/3453567; just clone the Gist (git://gist.github.com/3453567.git), run npm install and then node index.js and you should be able to visit the site at http://localhost:3000. Refreshing the page should give you the "next" quote, and when you reach the end it should wrap around.
This works because of a couple things:
First, we save a reference to a user's "last viewed" quote in their data:
var UserSchema = new mongoose.Schema({
user: String,
lastQuote: { type: mongoose.Schema.Types.ObjectId, ref: 'Quote' }
});
Now when we do User.findOne().populate('lastQuote'), the lastQuote attribute on the User that gets returned will be an actual Quote object referred to by the value of the field stored in MongoDB (which is an ObjectId).
We can call next() on this Quote object because of the following code:
QuoteSchema.methods.next = function(cb) {
var model = this.model("Quote");
model.findOne().where('_id').gt(this._id).exec(function(err, quote) {
if (err) throw err;
if (quote) {
cb(null, quote);
} else {
// If quote is null, we've wrapped around.
model.findOne(cb);
}
});
};
This is the part that finds the next quote or else wraps around to the first quote.
Take a look at the code and let me know if you have any questions.

Related

Is it possible to refer to "this" document in Mongoose?

I'm using Mongoose in Node.js, and I am wondering if it is possible to refer to the currently selected document using "this" or a similar mechanism. Here is the use case I'm looking for :
Mongoose Schema :
const mySchema = mongoose.Schema({
position: Number,
date: Number,
lastEventDate: Number
});
Let's say that, at some point in time, an event occurs.
For a document selected through its position, I want to update "lastEventDate" to the document's date.
Here is my dream code :
myModel.findOneAndUpdate(
{position: myPosition},
{$set: {
'lastEventDate': THISDOCUMENT.date
}}
);
Note : I'm using $set here because the actual code updates subdocuments...
Is there a built-in "THISDOCUMENT" reference such as the one I'm dreaming of, to do it all in a single query ?
Or do I have to first query the value before updating the document (two queries).
Couldn't find anything on the web, and I'm quite the newbie when it comes to using "this".
Thanks for any kind of help !
[EDIT :] Precisions about the objective :
I am in a situation where I only have the position "myPosition" to identify the correct document, and I want to set "lastEventDate" to the same value as "date" for that document.
My question is about efficiency : is it possible to perform the update in a single upload query ? Or do I have to first download the "date" value before uploading it back to the "lastEventDate" key ?
Gathering all the information provided, I will venture on a possible answer!
You could try something like:
Your schema JS file
const mySchema = mongoose.Schema({
position: Number,
date: Number,
lastEventDate: Number
});
mySchema.methods.doYourThing(){
this.lastEventDate=this.date; //it will set the lastEventDate
}
mongoose.model("myModel", MySchema, "mycollection")
Now, whenever you call doYourThing(), the action wanted will take place, you call it after you have a instance of the mode.
This is from my own code
const token = user.generateJwt(expirationDate); //send a token, it will be stored locally in the browser
it is inside a function that return an instance of user, and in the model User I have done a function called generateJwt like I have showed, and we have something like this:
return jwt.sign(
{
_id: this._id, //this is created automatically by Mongo
email: this.email,
name: this.name,
exp: parseInt(expiry.getTime() / 1000, 10), //Includes exp as UNIX time in seconds
level: this.level,
lastLogin: this.lastLogin,
failedLogin: this.failedLogin
},
process.env.JWT_SECRET
); // DO NOT KEEP YOUR SECRET IN THE CODE!
It returns all the information of the user!
Please, do not hesitate to add comments and feebacks, I am not sure it is what you want, but that is why I have understood your request.
Anothe option is using Virtuals, they also have access to this.

Passing Route Parameter to Mongoose Query Express

Basically I'm trying to find a way in which to find a way to plug req.params.name into a find() query.
I have tried:
Trying to pass through my req.params variable in my find() object parameter Card.find({cardName: req.params.name}, callback) and any other possible variance of that.
I've tried a static method for findByName in which I just did Card.findByName(req.params.name, callback);
When I do console.lo(req.params.name) it returns the name of the card; however, when I got to show.ejs I do console.log(cardstatsok.cardName) it comes back undefined.
I've searched here on Stack Overflow, I've checked my Udemy message board, and I've tried finding any tutorial on passing route parameters to a find query, and alas, nothing.
Here's my code:
My schema and model:
var cardSchema = new mongoose.Schema({
cardName: String,
id: Number,
cardSet: String,
image: String,
ability: String
});
var Card = mongoose.model("Card", cardSchema);
My route for my single card page:
app.get("/cards/:name", function(req, res) {
Card.find({"cardName":req.params.name}, function(err, cardInfo){
if(err){
res.redirect("/cards");
console.log(err);
} else {
res.render("show", {cardstatsok: cardInfo});
}
});
});
When I do console.log(cardInfo) it returns many objects since I used "Forest" and the copy of the Magic: The Gathering card Forest has been printed many times. Here's one, though:
{ _id: 5a85bb554c8fff0c04f15b8e,
cardName: 'Forest',
id: 243461,
cardSet: 'Duel Decks: Knights vs. Dragons',
image: 'http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=243461&type=card',
__v: 0 }
find() returns an array which means cardstatsok is an array.
So console.log(cardstatsok.cardName) won't work. Use console.log(cardstatsok[0].cardName) instead for the first card or console.log(cardstatsok) for everything. If you want to print all the card names you have to loop over the array.
To find only one card you can use findOne() instead.

Is collection.findOne({query}) not supposed to return the document itself?

I see a lot of tutorials/guides using collection.findOne({query}).field to get a value of a field in the document that is returned, but for me that doesn't seem to work and I wonder why. I did figure out another way to do it though. See below:
var rank = function(id) {
// My way of doing it
collection.findOne({ _id: id }, function(err, doc) {
console.log(doc.score); // Prints the actual score
});
// How some tutorials do it
var score = collection.findOne({ _id: id }).score;
console.log(score); // Gives a TypeError (Cannot read property 'score' of undefined)
};
// How some tutorials do it
Those tutorials are likely using mongodb's shell, not node.js api. The shell's api looks similar (all the same words, findOne, etc.), but it doesn't use callbacks. Shell's findOne does return the document inline.

Making a mongoose model aware that it is nested

Consider the blog/comment schemas where nesting is appropriate (even if you disagree):
var CommentSchema = new Schema({ name: String, body: String });
var BlogPostSchema = new Schema({ title: String, comments: [CommentSchema] });
I understand how to add, update, delete comments for a blog post, but all of these methods require the save() method to be called on the parent blog post document:
blog_post.comments.push( new Comment({...}) );
blog_post.save();
I would like to be able to make the Comment schema aware that it is nested inside of another schema so that I can call save() on a comment document and it's smart enough to update the parent blog post. In my app logic, I already know the blog post id, so I would like to do something like this:
CommentSchema.virtual('blog_post_id');
CommentSchema.pre('save', function (next) {
var comment = this;
if( !comment.blog_post_id ) throw new Error('Need a blog post id');
BlogModel.findById( comment.blog_post_id, function(err, post) {
post.comments.push( comment );
post.save(next);
});
});
var comment = new Comment({ blog_post_id: 123, name: 'Joe', body: 'foo' });
comment.save();
The above works, but I still end up with a top-level Comments collection separate from the blog posts (this is just how mongoose works, I accept that).
Question: How do I prevent Mongoose from creating a separate "Comments" collection. In the pre-save method I would like to call next() without any write operations taking place afterwards. Any thoughts?
This has earned me the Tumbleweed badge... hooray!?!?
So I have written a lot of code which accomplished the above. I don't want to release it until I have done more testing. But if anybody is interested in this, please let me know by posting here. I will gladly hand over what I have (which is going into production soon).
Right now my code doesn't support deep nesting... meaning you can only work with "simple" nesting similar to the blog/comments example above. I have the architecture in place to handle more complex nesting in the future, but I don't have the time to test right now (darn deadlines). Here are some of the big points about my solution so far:
All operations require the parent document's id (this makes sense once you start using it)
find, findOne, save, and remove directly on a nested model
findById doesn't (can't) work - well it maybe could work but would require searching the entire collection, which is slow. Must use findOne + parent id instead (see examples).
Super fast - uses projection for finding, and saves using Model.update() on the parent model (which is really fast).
All middleware still executes (pre/post and validation)
None of the findAndUpdate/Remove methods work [yet?]
Setup
// setup the "nestedSchema" plugin
var nestedSchema = require("./plugins/nestedSchema");
CommentSchema.plugin(nestedSchema, {
path: 'comments',
ownerModelPath: './BlogPostModel',
ownerIdFieldName: 'blogpost_id'
});
Examples - take note that the parent's blogpost_id is ALWAYS used - this is a requirement which makes it stay fast (callbacks and error handling removed for brevity):
// create a new comment
var comment = new CommentModel({
blogpost_id: [id],
name: 'Joe Schmoe',
body: 'The content of the comment'
});
comment.save();
// use findOne in leu of findById
CommentModel.findOne({blogpost_id: [id], _id: [id]}, function( err, comment ) {
comment.set('body', 'This comment has been updated directly!');
comment.save();
});
// find all hateful comments and remove
CommentModel.find({blogpost_id: [id], body: /sucks|stupid|dumb/gi}).remove();
Using the mongoose-relationship plugin from https://www.npmjs.org/package/mongoose-relationship
it is possible to make your documents aware of their relations.
Corresponding references are updated by the plugin when adding/removing documents.
There is a good example on the github page: https://github.com/sabymike/mongoose-relationship

Mongoose key/val set on instance not show in JSON or Console.. why?

I have some information on my mongoose models which is transient. For performance reasons I dont wish to store it against the model.. But I do want to be able to provide this information to clients that connect to my server and ask for it.
Here's a simple example:
var mongoose = require('mongoose'),
db = require('./dbconn').dbconn;
var PersonSchema = new mongoose.Schema({
name : String,
age : Number,
});
var Person = db.model('Person', PersonSchema);
var fred = new Person({ name: 'fred', age: 100 });
The Person schema has two attributes that I want to store (name, and age).. This works.. and we see in the console:
console.log(fred);
{ name: 'fred', age: 100, _id: 509edc9d8aafee8672000001 }
I do however have one attribute ("status") that rapidly changes and I dont want to store this in the database.. but I do want to track it dynamically and provide it to clients so I add it onto the instance as a key/val pair.
fred.status = "alive";
If we look at fred in the console again after adding the "alive" key/val pair we again see fred, but his status isnt shown:
{ name: 'fred', age: 100, _id: 509edc9d8aafee8672000001 }
Yet the key/val pair is definitely there.. we see that:
console.log(fred.status);
renders:
alive
The same is true of the JSON representation of the object that I'm sending to clients.. the "status" isnt included..
I dont understand why.. can anyone help?
Or, alternatively, is there a better approach for adding attributes to mongoose schemas that aren't persisted to the database?
Adding the following to your schema should do what you want:
PersonSchema.virtual('status').get(function() {
return this._status;
});
PersonSchema.virtual('status').set(function(status) {
return this._status = status;
});
PersonSchema.set('toObject', {
getters: true
});
This adds the virtual attribute status - it will not be persisted because it's a virtual. The last part is needed to make your console log output correctly. From the docs:
To have all virtuals show up in your console.log output, set the
toObject option to { getters: true }
Also note that you need to use an internal property name other than status (here I used _status). If you use the same name, you will enter an infinite recursive loop when executing a get.
Simply call .toObject() on the data object.
For you code will be like:
fred.toObject()
This has been very helpful. I had to struggle with this myself.
In my case, I was getting a document from mongoose. When I added a new key, the key was not visible to the object if I console.log it. When I searched for the key (console.log(data.status), I could see it in the log but not visible if I logged the entire object.
After reading this response thread, it worked.
For example, I got an object like this one from my MongoDB call:
`Model.find({}).then(result=> {
//console.log(result);// [{ name:'John Doe', email:'john#john.com'}];
//To add another key to the result, I had to change that result like this:
var d = result[0];
var newData = d.toJSON();
newData["status"] = "alive";
console.log(newData);// { name:'John Doe', email:'john#john.com', status:'alive'};
}).catch(err=>console.log(err))`
Hope this helps someone else.
HappyCoding

Resources