Multi level population in Mongoose is not working - node.js

I have three models, programs (Program), tiers(Tier), offers (Offer). A program can have multiple tiers and a tiers can have multiple offers. So my program has an array of tiers and correspondingly a tier can have array of offers. Now I am trying to populate programs as follows:
var Program = mongoose.model('Program');
var Offer = mongoose.model('Offer');
var Tier = mongoose.model('Tier');
Program.findOne({
_id: p_id
}).populate('tiers').exec(function(err, docs){
var opts = {
path: 'tiers.offers'
}
Program.populate(docs, opts, function(err, docs){
console.log('populated');
// var s = require('util').inspect(docs, {depth : null})
console.log(docs);
console.log(docs.tiers[0]); //Printing complete tier information
console.log(docs.tiers[0].offers[0]) //Just printing Object ID, not taking data from offers table
})
})
Problem is it is just populating tiers and not offers. How do I go deeper? Corresponding example is here:
https://github.com/paulcsmith/mongoose-currency/blob/master/node_modules/mongoose/examples/population-across-three-collections.js

I had a similar problem, I have a song that haves mixes (Next songs) and also Artists. In here first I find the song by name, then I populate the songs that go next and also their Artists, on the song that goes next I populate the song and also the Artist information.
Hope it helps.
Song.find({songName: req.params.id})
.lean()
.populate({ path: 'songMixs songArtist' })
.exec(function(err, docs) {
var options = {
path: 'songMixs.nextSong',
model: 'Song'
};
if (err) return res.json(500);
Song.populate(docs, options, function (err, projects) {
var options2 = {
path: 'songMixs.nextSong.songArtist',
model: 'Artist'
};
Artist.populate(docs, options2, function (err, projects) {
res.json(projects);
});
//res.json(projects);
});
});

Related

Mongoose text search pagination in MongoDB/Node.js

I have this problem , i don't know how to make a pagination in Mongoose text search module, i am totally beginner, so please help, this is my code:
searchModul.create({ title: title, description: description }, function (err) {
if (err) return handleError(err);
searchModul.textSearch(title, function (err, output) {
if (err) return handleError(err);
res.render('search', {
title : 'title',
results : output.results
});
});
});
and also i want to know how to display that pagination in the search.ejs view. and thanks in advance.
To implement pagination, use $limit to define a limit for each query, and $skip to navigate pages.
From the docs:
$limit takes a positive integer that specifies the maximum number of documents to pass along.
$skip takes a positive integer that specifies the maximum number of documents to skip.
There are previous questions like this one, check out the answers here and a more detailed tutorial here.
Hope that helps!
I think you want something like this:
searchModul.find({ $text: { $search: title }}).skip(50).limit(50).exec(function (err, output) {
if (err) return handleError(err);
res.render('search', {
title : 'title',
results : output.results
});
});
This will return the second 50 items that match. .skip(50) the number of items skip and .limit(50) is the number of items to return.
const field = req.query.field;
const search = {};
search[field] = req.query.searchValue;
search["role"] = req.params._role;
search["status"] = parseInt(req.query.status);
user = await Users.paginate(search, {
limit: parseInt(req.query.limit),
page: parseInt(req.query.page),
});
OR
var ObjectId = mongoose.Types.ObjectId;
let _id = new ObjectId(req.query.q);;
user = await Users.paginate(
{
role: req.params._role,
_id: _id,
},
{
limit: parseInt(req.query.limit),
page: parseInt(req.query.page),
});

node.js/express/mongoose noob issue

I just start to use node.js with express and mongoose, and I have a stupid question...
Somewhere in my routes.js file, I have the following section :
// DASHBOARD SECTION, GET MY GROUPS
app.get('/dashboard', isLoggedIn, function(req, res) {
var Group = require('../app/models/group'); //adding mongoose Schema
Group.find({"groupDetails.userId" : req.user._id})
.exec(function(err, myGroups) {
if (err)
res.send(err);
var myGroups = myGroups;
//console.log("myGroups: " + myGroups); // check with "heroku logs"
res.render('dashboard.ejs', {
user : req.user,
myGroups : myGroups
});
});
});
This code works. When someone browse the dashboard page, I receive "myGroups" which is an array with all the groups for the current logged in user.
Now, here is my question :
Actually when someone browse the dashboard page, I would like to make a second query (based on the exact same pattern) to get all groups and all files for the current logged in user.
Then I will send "user", "myGroups" and "myFiles" to the dashboard page.
How can I do that ?
I tried several things with no result so far... I think I'm a little bit lost in node.js callback functions :D
Thanks a lot for your help.
You have two options here:
1) deal with callback hell (callback inside callback inside...) to retrieve 3 sets of data. This way is least elegant and efficient
2) Use a library that will do the job asynchronously and have one callback when all the data is retrieved, you can use async library which is just awesome. In this case you will have just one callback in which you can access all the data you have fetched.
Here's what you can do with async in your case:
var async = require('async');
..........
app.get('/dashboard', isLoggedIn, function(req, res) {
var Group = require('../app/models/group'); //adding mongoose Schema
var User = require('../app/models/user'); //adding mongoose Schema
var Files = require('../app/models/files'); //adding mongoose Schema
async.parallel({
groups: function(callback){
Group.find(...).exec(callback);
},
users: function(callback){
Users.find(...).exec(callback);
},
files: function(callback){
Files.find(...).exec(callback);
}
}, function(err, results) {
if (err)
res.send(err);
var groups = results.groups;
var users = results.users;
var files = results.files;
res.render('dashboard.ejs', {
user : req.user,
myGroups : groups,
users: users,
files: files
});
});
});

Sails.js populate nested associations

I've got myself a question regarding associations in Sails.js version 0.10-rc5. I've been building an app in which multiple models are associated to one another, and I've arrived at a point where I need to get to nest associations somehow.
There's three parts:
First there's something like a blog post, that's being written by a user. In the blog post I want to show the associated user's information like their username. Now, everything works fine here. Until the next step: I'm trying to show comments which are associated with the post.
The comments are a separate Model, called Comment. Each of which also has an author (user) associated with it. I can easily show a list of the Comments, although when I want to display the User's information associated with the comment, I can't figure out how to populate the Comment with the user's information.
In my controller i'm trying to do something like this:
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments') // I want to populate this comment with .populate('user') or something
.exec(function(err, post) {
// Handle errors & render view etc.
});
In my Post's 'show' action i'm trying to retrieve the information like this (simplified):
<ul>
<%- _.each(post.comments, function(comment) { %>
<li>
<%= comment.user.name %>
<%= comment.description %>
</li>
<% }); %>
</ul>
The comment.user.name will be undefined though. If I try to just access the 'user' property, like comment.user, it'll show it's ID. Which tells me it's not automatically populating the user's information to the comment when I associate the comment with another model.
Anyone any ideals to solve this properly :)?
Thanks in advance!
P.S.
For clarification, this is how i've basically set up the associations in different models:
// User.js
posts: {
collection: 'post'
},
hours: {
collection: 'hour'
},
comments: {
collection: 'comment'
}
// Post.js
user: {
model: 'user'
},
comments: {
collection: 'comment',
via: 'post'
}
// Comment.js
user: {
model: 'user'
},
post: {
model: 'post'
}
Or you can use the built-in Blue Bird Promise feature to make it. (Working on Sails#v0.10.5)
See the codes below:
var _ = require('lodash');
...
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.then(function(post) {
var commentUsers = User.find({
id: _.pluck(post.comments, 'user')
//_.pluck: Retrieves the value of a 'user' property from all elements in the post.comments collection.
})
.then(function(commentUsers) {
return commentUsers;
});
return [post, commentUsers];
})
.spread(function(post, commentUsers) {
commentUsers = _.indexBy(commentUsers, 'id');
//_.indexBy: Creates an object composed of keys generated from the results of running each element of the collection through the given callback. The corresponding value of each key is the last element responsible for generating the key
post.comments = _.map(post.comments, function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
res.json(post);
})
.catch(function(err) {
return res.serverError(err);
});
Some explanation:
I'm using the Lo-Dash to deal with the arrays. For more details, please refer to the Official Doc
Notice the return values inside the first "then" function, those objects "[post, commentUsers]" inside the array are also "promise" objects. Which means that they didn't contain the value data when they first been executed, until they got the value. So that "spread" function will wait the acture value come and continue doing the rest stuffs.
At the moment, there's no built in way to populate nested associations. Your best bet is to use async to do a mapping:
async.auto({
// First get the post
post: function(cb) {
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.exec(cb);
},
// Then all of the comment users, using an "in" query by
// setting "id" criteria to an array of user IDs
commentUsers: ['post', function(cb, results) {
User.find({id: _.pluck(results.post.comments, 'user')}).exec(cb);
}],
// Map the comment users to their comments
map: ['commentUsers', function(cb, results) {
// Index comment users by ID
var commentUsers = _.indexBy(results.commentUsers, 'id');
// Get a plain object version of post & comments
var post = results.post.toObject();
// Map users onto comments
post.comments = post.comments.map(function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
return cb(null, post);
}]
},
// After all the async magic is finished, return the mapped result
// (or an error if any occurred during the async block)
function finish(err, results) {
if (err) {return res.serverError(err);}
return res.json(results.map);
}
);
It's not as pretty as nested population (which is in the works, but probably not for v0.10), but on the bright side it's actually fairly efficient.
I created an NPM module for this called nested-pop. You can find it at the link below.
https://www.npmjs.com/package/nested-pop
Use it in the following way.
var nestedPop = require('nested-pop');
User.find()
.populate('dogs')
.then(function(users) {
return nestedPop(users, {
dogs: [
'breed'
]
}).then(function(users) {
return users
}).catch(function(err) {
throw err;
});
}).catch(function(err) {
throw err;
);
Worth saying there's a pull request to add nested population: https://github.com/balderdashy/waterline/pull/1052
Pull request isn't merged at the moment but you can use it installing one directly with
npm i Atlantis-Software/waterline#deepPopulate
With it you can do something like .populate('user.comments ...)'.
sails v0.11 doesn't support _.pluck and _.indexBy use sails.util.pluck and sails.util.indexBy instead.
async.auto({
// First get the post
post: function(cb) {
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.exec(cb);
},
// Then all of the comment users, using an "in" query by
// setting "id" criteria to an array of user IDs
commentUsers: ['post', function(cb, results) {
User.find({id:sails.util.pluck(results.post.comments, 'user')}).exec(cb);
}],
// Map the comment users to their comments
map: ['commentUsers', function(cb, results) {
// Index comment users by ID
var commentUsers = sails.util.indexBy(results.commentUsers, 'id');
// Get a plain object version of post & comments
var post = results.post.toObject();
// Map users onto comments
post.comments = post.comments.map(function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
return cb(null, post);
}]
},
// After all the async magic is finished, return the mapped result
// (or an error if any occurred during the async block)
function finish(err, results) {
if (err) {return res.serverError(err);}
return res.json(results.map);
}
);
You could use async library which is very clean and simple to understand. For each comment related to a post you can populate many fields as you want with dedicated tasks, execute them in parallel and retrieve the results when all tasks are done. Finally, you only have to return the final result.
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments') // I want to populate this comment with .populate('user') or something
.exec(function (err, post) {
// populate each post in parallel
async.each(post.comments, function (comment, callback) {
// you can populate many elements or only one...
var populateTasks = {
user: function (cb) {
User.findOne({ id: comment.user })
.exec(function (err, result) {
cb(err, result);
});
}
}
async.parallel(populateTasks, function (err, resultSet) {
if (err) { return next(err); }
post.comments = resultSet.user;
// finish
callback();
});
}, function (err) {// final callback
if (err) { return next(err); }
return res.json(post);
});
});
As of sailsjs 1.0 the "deep populate" pull request is still open, but the following async function solution looks elegant enough IMO:
const post = await Post
.findOne({ id: req.param('id') })
.populate('user')
.populate('comments');
if (post && post.comments.length > 0) {
const ids = post.comments.map(comment => comment.id);
post.comments = await Comment
.find({ id: commentId })
.populate('user');
}
Granted this is an old question, but a much simpler solution would be to loop over the comments,replacing each comment's 'user' property (which is an id) with the user's full detail using async await.
async function getPost(postId){
let post = await Post.findOne(postId).populate('user').populate('comments');
for(let comment of post.comments){
comment.user = await User.findOne({id:comment.user});
}
return post;
}
Hope this helps!
In case anyone is looking to do the same but for multiple posts, here's one
way of doing it:
find all user IDs in posts
query all users in 1 go from DB
update posts with those users
Given that same user can write multiple comments, we're making sure we're reusing those objects. Also we're only making 1 additional query (whereas if we'd do it for each post separately, that would be multiple queries).
await Post.find()
.populate('comments')
.then(async (posts) => {
// Collect all comment user IDs
const userIDs = posts.reduce((acc, curr) => {
for (const comment of post.comments) {
acc.add(comment.user);
}
return acc;
}, new Set());
// Get users
const users = await User.find({ id: Array.from(userIDs) });
const usersMap = users.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
// Assign users to comments
for (const post of posts) {
for (const comment of post.comments) {
if (comment.user) {
const userID = comment.user;
comment.user = usersMap[userID];
}
}
}
return posts;
});

Mongoose searching FindOne with multiple arguments

My first attempt at building something with Angular + express + mongodb, so I'm probably going about this completely the wrong way. Express is being used to serve up json. Angular then takes care of all the views etc.
I'm using Mongoose to interact with Mongo.
I have the following database schema:
var categorySchema = new mongoose.Schema({
title: String, // this is the Category title
retailers : [
{
title: String, // this is the retailer title
data: { // this is the retailers Data
strapLine: String,
img: String , // this is the retailer's image
intro: String,
website: String,
address: String,
tel: String,
email: String
}
}
]
});
var Category = mongoose.model('Category', categorySchema);
and in Express I have a couple of routes to get the data:
app.get('/data/categories', function(req, res) {
// Find all Categories.
Category.find(function(err, data) {
if (err) return console.error(err);
res.json(data)
});
});
// return a list of retailers belonging to the category
app.get('/data/retailer_list/:category', function(req, res) {
//pass in the category param (the unique ID), and use that to do our retailer lookup
Category.findOne({ _id: req.params.category }, function(err, data) {
if (err) return console.error(err);
res.json(data)
});
});
The above works - I'm just having big problems trying to get at a single retailer. I'm passing the category, and retailer id through... I've tried all sorts of things - from doing a find on the category, then a findOne on the contents within... but I just cant get it to work. I'm probably going about this all wrong...
I found this thread here: findOne Subdocument in Mongoose and implemented the solution - however, it returns all my retailers - and not just the one I want.
// Returns a single retailer
app.get('/data/retailer_detail/:category/:id', function(req, res) {
//pass in the category param (the unique ID), and use that to do our retailer lookup
Category.findOne({_id: req.params.category , 'retailers.$': 1}, function(err, data) {
console.log(data);
if (err) return console.error(err);
res.json(data)
});
});
Thanks,
Rob
Now that I see your full filter/query, you should be able to use the array positional operator in this case as part of the projection rather than doing client side filtering:
app.get('/data/retailer_detail/:category/:id', function(req, res) {
//pass in the category param (the unique ID), and use that to do our retailer lookup
Category.findOne({
/* query */
_id: req.params.category ,
'retailers._id' : req.params.id
},
{ /* projection */
"retailers.$" : 1
},
function(err, data) {
var retailer = _.where(data.retailers , { id : req.params.id });
if (err) return console.error(err);
res.json(retailer)
});
});
For the { "retailers.$" : 1 } to work properly, the query must include a field from an element in the array. The $ operator returns the first match only.
The guys next door use Mongo + Express and gave me some pointers: they explained to me how mongo worked, and advised I should use underscore.js to assist with my filter.
They said I needed to pull the entire category out - and then run the filter. I don't strictly need , 'retailers._id' : req.params.id} but they said to leave it in as it guaranteed that the category would only be returned if an item within it contained that information. I still don't really know why or how... So can't really mark this as solved.. it it solved, but I don't really get why as yet - so will do more reading :)
app.get('/data/retailer_detail/:category/:id', function(req, res) {
//pass in the category param (the unique ID), and use that to do our retailer lookup
Category.findOne({_id: req.params.category , 'retailers._id' : req.params.id}, function(err, data) {
var retailer = _.where(data.retailers , { id : req.params.id });
if (err) return console.error(err);
res.json(retailer)
});
});

Return mongojs query result from function

I made the function below for getting usernames from ids. It is not working well.
I can write console.log(result.first_name); within the query function, and the usernames shows up in my terminal, but not the browser. I tried adding “return 'something';” at the end of the function, to see if that showed up in the browser – It did. How can I write the function so that the query result is returned?
function (global function in app.js)
function usernameFromId(id, callback){
db.users.findOne({ _id: ObjectId(id.toString()) }, function(err, result) {
var first_name = result.first_name;
console.log(first_name); // names show up in the console…
callback(first_name);
});
};
page handler (in app.js)
app.get('/books', function(req, res){
function timeSince(dato){
moment.lang('nb');
return moment(dato).fromNow();
};
db.books.find().sort({ added:-1 }, function(err, docs) {
var books = docs;
db.activity.find().limit(9).sort({ time:-1 }, function(err, docs) {
var activity = docs;
res.render('books', {
books: books,
activity: activity,
timeSince: timeSince,
usernameFromId: usernameFromId
})
});
});
});
template (books.jade)
- each a in activity
p=usernameFromId(a.user_id, function(name){return name;})
No because of the asynchronous nature of JavaScript. I have added some comments to your code to indicate the actual order of execution. This is why you are getting the error.
function usernameFromId(id){
var id = id.toString(); // 1
db.users.findOne({ _id: ObjectId(id) }, function(err, result) {
var first_name = result.first_name; // 3
});
return first_name; // 2
};
Edit: you probably want something like the following
function usernameFromId(id, callback){
var id = id.toString();
db.users.findOne({ _id: ObjectId(id) }, function(err, result) {
var first_name = result.first_name;
callback(first_name);
});
};
Okay, I found a solution. Not sure whether it’s any good, but it works. No need for a function.
page handler (in app.js):
app.get('/books', function(req, res){
db.activity.find().limit(9).sort({ time:-1 }, function(err, docs) {
var activity = docs;
db.users.find(function(err, docs) {
var users = docs;
res.render('books', {
page_title:'books',
activity: activity,
users: users
})
});
});
});
template (books.jade):
- each a in activity
- for u in users
- if (a.user_id == u._id.toString())
| #{u.first_name}

Resources