I am wanting to load a topic, 25 of its comments and up to 5 sub comments for each comment, repeated recursively over each comment/sub-comment until all related comments are found.
I'm currently using an angular directive to recursively subscribe and add to the local collection whenever the comment has children. It works quite well, but there is some lag (to be expected) between loading the initial 25 comments, and loading their children, then their children and so on.
This issue isn't a problem when just loading a page at a time. It becomes an issue when using infinite scrolling and increasing that initial 25 comment limit. It will cause the page to jump up and down a bit as the sub comments disappear and reappear once loaded again.
I was wondering how I could recursively look up all comments prior to sending to the local client so I don't need to make more than one round trip for each topic.
I have a demo loaded up at ck-gaming.com
If you scroll to the bottom it will load more and you'll see it jump all over as the sub comments are reloaded into the page.
The two options I can think of would be to use a resolve to wait for all collections prior to loading the page or using recursive publish to get them all first.
Thoughts? Ideas?
Ok, my first attempt that I would like some thoughts on if possible.
For the publishing I decided to go with publish-composite to make publishing from the same collection easier.
for the publication I wrote:
Meteor.publishComposite('oneDiscussion', function (slug, options) {
var query = {};
query.find = function () {
return Discussions.find({ slug: slug }, { limit: 1 });
};
var mainChildQuery = Comments.find({ slug: slug }, { limit: 1 });
query.children = [];
query.children[0] = {};
query.children[0].find = function (discussion) {
return mainChildQuery;
};
query.children[0].children = [];
query.children[0].children[0] = {};
query.children[0].children[0].find = function (comment) {
return Meteor.users.find({ _id: comment.author.id }, { limit: 1, fields: { profile: 1, roles: 1, createdAt: 1, username: 1 } });
};
query.children[0].children[1] = {};
query.children[0].children[1].find = function (parent) {
Counts.publish(this, 'numberOfComments', Comments.find(
{ parent_id: parent._id }
), { noReady: true });
console.log(options)
return Comments.find({ parent_id: parent._id }, options);
};
// var parentQuery = Comments.find({ slug: slug });
var parent = mainChildQuery.fetch();
var children = Comments.find({ parent_id: parent[0]._id }, { limit: 25 }).fetch();
var childrenIds = _.pluck(children, '_id');
var getChildren = function (children_ids, thisParent) {
var i = 0;
thisParent.children = [];
var recursive = function getEm(children, parent) {
_.each(children, function (id) {
// parent.children[i] = new Children(id);
var query = Comments.find({ parent_id: id }, { limit: 5, sort: { date: -1 } });
parent.children[i] = {
find: function () {
return Comments.find({ parent_id: id }, { limit: 5, sort: { date: -1 } });
}
};
var children1 = query.fetch();
var newChildrenIds = _.pluck(children1, '_id');
i++;
if (newChildrenIds.length > 0) {
getEm(newChildrenIds, parent);
}
});
}
recursive(children_ids, thisParent);
};
getChildren(childrenIds, query.children[0].children[1]);
return query;
});
Seems to be working ok so far, though running it on my desktop it's not as performant as I would think it should be. I'll deploy it and see if there's a difference online. I'll update when I get home and can update the live site. If anyone can find something wrong with what I've written it would be much appreciated.
I've came up with what I think is the best solution. I improved on the function above and so far I'm really enjoying the results.
Here is the publish function:
Meteor.publishComposite('comments', function (item_id, options) {
/**
* TODO: Add query to find a user for each comment.
*/
/**
* Start building our query.
* Add the latest 25 (depending on options) child comments of the viewed item
* to the query.
*/
var query = {
find: function () {
return Comments.find({ parent_id: item_id }, options);
}
};
// Query the database for the first 25? comments, we'll need their _id's
var mainChildren = Comments.find({ parent_id: item_id }, options).fetch();
// pluck the id's from the initial comments
var mainChildrenIds = _.pluck(mainChildren, '_id');
/**
* Builds the remaining query based on the id's plucked from the children
* above.
* #param childrens_id The id's we just plucked from the above query
* #param thisParent This is the parent query
*/
var getChildren = function (children_ids, parentQuery) {
// initiate i to 0
var i = 0;
// add a child array to the current parent query.
parentQuery.children = [];
var recursive = function getem(children, parent) {
_.each(children, function (id) {
var query = Comments.find({ parent_id: id }, { limit: 5, sort: { date: 1 } });
parent.children[i] = {
find: function () {
return query;
}
};
var children1 = query.fetch();
var newChildrenIds = _.pluck(children1, '_id');
i++;
if (newChildrenIds.length > 0) {
getem(newChildrenIds, parent);
}
});
};
// recursively build the query if there are children found.
recursive(children_ids, parentQuery);
};
// initiate the query build function
getChildren(mainChildrenIds, query);
return query;
});
I created an example app you can get on GitHub here
And you can view it running on meteorpad here
what it does
All the function does is build the publishComposite query, recursively looping over the children id's, as long as there are children id's. When there are no more children it stops.
to use it
You want a collection of comments (or whatever you're nesting) that have a parent_id field. This field will be filled with either the parent post Id, parent Item id (if say making a store with reviews/comments). The parent post id would of course be the comment or post you are commenting on. See the example for more information.
Related
I have defined a PostSchema as follows. A post is written by an author, and can be read by many people: lastOpens is an array of { time: ... , userId: ... }.
var PostSchema = new mongoose.Schema({
title: { type: String }
author: { type: mongoose.Schema.Types.ObjectId, ref: 'user' },
lastOpens: { type: Array, default: [] }
})
Now, I want to write a static method that returns all the posts read by one user:
PostSchema.statics.postsOpenedByUser = function (userId, cb) {
// need to go through all the posts, and check their `lastOpens`.
// If `userId` is in `userId` of a `lastOpen`, then count the post in
}
What I know is the methods like find({ ... }) of MongoDB. But I don't know how to specify a more complicated search like mine.
Could anyone help?
Edit 1: I tried to use $where operator as follows, it did not work:
PostSchema.statics.postsOpenedByUser = function (userId, cb) {
return this.find({ $where: function () {
var index = -1;
for (var i = 0; i < this.lastOpens.length; i++)
if (this.lastOpens[i].userId === userId) { index = i; break }
return !(index === -1)
}}, cb)
Is there anything we could not do inside $where?
You can use Mongo's query an array of embedded documents.
In your case it will look something like :
PostSchema.statics.postsOpenedByUser = function (userId, cb) {
return this.find( { "lastOpens.userId" : userId }, cb );
}
This will return all posts that have userId in the lastOpens
I have a 1 cursor to calculate total number of voters. I have two methods where first counts the total number of voters in my favour, second method counts the total number of voters done voting.
LIB/collection.js
infavourcount = new Mongo.Collection('infavourcount');
votedone = new Mongo.Collection('votedone');
SERVER/publish.js [count voters in my favour]
function upsertInFavourCount() {
var yes = voters.find({favour: {$regex:"Y", $options: 'i'}}).count();
var maybe = voters.find({favour: {$regex:"M", $options: 'i'}}).count();
var no = total - (yes + maybe);
infavourcount.upsert('infavour',
{
yes: yes ,
maybe: maybe,
no:no
}
);
}
// null name means send to all clients
Meteor.publish(null,function() {
upsertInFavourCount();
return infavourcount.find();
});
SERVER/publish.js [count successful votings]
function upsertVoteDone() {
var done = voters.find({voted: {$regex:"Y", $options: 'i'}}).count();
votedone.upsert('votedone',
{
done: done
}
);
}
Meteor.publish(null,function() {
upsertVoteDone();
return votedone.find();
});
var cursor = voters.find();
cursor.observe({
changed: upsertVoteDone
});
CLIENT/template/home.js
Template.home.onCreated(function(){
Meteor.subscribe('voters');
Meteor.subscribe('infavourcount');
Meteor.subscribe('votedone');
});
Template.home.helpers({
yesvote : function() {
return infavourcount.findOne().yes;
},
maybevote : function() {
return infavourcount.findOne().maybe;
},
novote : function() {
return infavourcount.findOne().no;
},
votedone : function() {
return votedone.findOne().done;
}
});
My problem is how to call multiple upsert methods in one observe method of Meteor published collection.
I got my solution by the way. The answer to this is add an function on "changed" or "added" or "removed" event and call n number of upsert methods therein.
cursor.observe({
changed: function(id, fields){
upsertInFavourCount();
upsertVoteDone();
}
});
I have a mongoDB collection called Assignments, which have multiple bids (embedded arrays). When one of those bids are set as accepted:true, they are considered an accepted_bid.
I want a function which returns all the docs (or a count of docs) that have one bid out of many (embedded arrays), which are owned by the logged in user.
The following does not work. I'm looking to have {{stats.count}} in the HTML file.
Template.dashboard.stats = function() {
return Assignments.find({completed:true}, {
transform: function(doc) {
if(doc.bids) {
var accepted_bid = _(doc.bids).findWhere({owner:Meteor.userId(),accepted:true});
doc.bid = accepted_bid;
}
return doc;
}
});
};
Im not sure if this would work but it returns a count:
Template.dashboard.helpers({
stats: function() {
var assignments = Assignments.find({
completed: true
}, {
transform: function(doc) {
if (doc.bids) {
var accepted_bid = _(doc.bids).findWhere({
owner: Meteor.userId(),
accepted: true
});
if(accepted_bid) doc.bid = accepted_bid;
}
return doc;
}
}).fetch();
return _(assignments).chain().pluck("bid").compact().value().length;
}
});
It can be used with {{stats}}
I'm using Mongoose 3.6 and my (simplified) schemas look like this
var commentSchema = new Schema({
created: Date
});
var userSchema = new Schema({
comments : [commentSchema],
});
I'd like to paginate the comments, so that if I have a user with 25 comments the pages would divide into:
page 1: comments 15-24
page 2: comments 5-14
page 3: comments 0-4
page 4: empty
page 5: empty
...
This is what I've tried
this.findById(user_id, {
comments: {
$slice: [-comment_page_index * comments_per_page, comments_per_page]
}
}, function(err, user) {
//...
});
While that seemed to be working at first, I was disappointed to find out that the $slice operator cuts off the index given when it hits the collection bounds.
So what I get instead is
page 1: comments 15-24
page 2: comments 5-14
page 3: comments 0-9
page 4: comments 0-9
page 5: comments 0-9
...
A better options here would probably to combine the sort, skip and limit functions, but while I know how to use them on a regular collection, I have to clue on how to apply them to a subdocument. Any thoughs?
If it's find all or related to displaying, pagination would be something like the code below. The code is just an example:
findAll: async function (req, res) {
var responseObj = {};
try {
var responseObj = {};
var queryArray = [];
var finalAnswer = [];
var loggedinUser = mongoose.Types.ObjectId(req.user.id)
var auser = await user.findOne({ _id: loggedinUser })
var places = auser.placesUnderIt;
for (i of places) {
var singlePlaceId = mongoose.Types.ObjectId(i);
var singlePlaceData = await place.findOne({ _id: singlePlaceId })
var list = singlePlaceData.controlledStore;
for (k of list) {
finalAnswer.push(k);
}
}
queryArray.push({ _id: { $in: finalAnswer} });
var query = {
$and: queryArray
}
responseObj.count = await store.countDocuments(query);
responseObj.data = await store.find(query)
.populate('ownerId', ["firstName", "lastName", "phoneNumber", "email", "personalDetails"])
.populate('businesses')
.limit(parseInt(req.query.limit))
.skip(parseInt(req.query.skip));
var data = responseObj.data;
return res.send(responseObj);
} catch (err) {
return res.send('Error');
}
}
I would like to increment the views count by 1 each time my document is accessed. So far, my code is:
Document
.find({})
.sort('date', -1)
.limit(limit)
.exec();
Where does $inc fit in here?
Never used mongoose but quickly looking over the docs here it seems like this will work for you:
# create query conditions and update variables
var conditions = { },
update = { $inc: { views: 1 }};
# update documents matching condition
Model.update(conditions, update).limit(limit).sort('date', -1).exec();
Cheers and good luck!
I ran into another problem, which is kind of related to $inc.. So I'll post it here as it might help somebody else. I have the following code:
var Schema = require('models/schema.js');
var exports = module.exports = {};
exports.increase = function(id, key, amount, callback){
Schema.findByIdAndUpdate(id, { $inc: { key: amount }}, function(err, data){
//error handling
}
}
from a different module I would call something like
var saver = require('./saver.js');
saver.increase('555f49f1f9e81ecaf14f4748', 'counter', 1, function(err,data){
//error handling
}
However, this would not increase the desired counter. Apparently it is not allowed to directly pass the key into the update object. This has something to do with the syntax for string literals in object field names. The solution was to define the update object like this:
exports.increase = function(id, key, amount, callback){
var update = {};
update['$inc'] = {};
update['$inc'][key] = amount;
Schema.findByIdAndUpdate(id, update, function(err, data){
//error handling
}
}
Works for me (mongoose 5.7)
blogRouter.put("/:id", async (request, response) => {
try {
const updatedBlog = await Blog.findByIdAndUpdate(
request.params.id,
{
$inc: { likes: 1 }
},
{ new: true } //to return the new document
);
response.json(updatedBlog);
} catch (error) {
response.status(400).end();
}
});