Meteor, publish double join reactive - mongodb

as you see below, in my publish method I need to do a join twice, and the way I've done it throws an "Error: Publish function returned an array of non-Cursors" exception. and it is not even reactive!
is it possible to do it in a better way, I am very performance conscious and don't want to do 3 trips back and force between client and server, also some packages hit the database once for every item in an array!!
Meteor.publish('post', function(id) {
Posts.incView(id);
parentPost= Posts.findOne({_id: id});
console.log(parentPost);
eachPost= Posts.find({_id: {$in : parentPost.childs }});
users=[parentPost.createdBy];
eachPost.forEach( function(each) { users.push(each.users)});
return [
parentPost,
eachPost,
UInfo.find({_id:{$in:users}})
];
});

findOne() returns an object, not a cursor, which is why you're getting an error. Change findOne code to:
parentPost = Posts.find({_id: id});
This way you'll return three cursors.

Related

Meteor (Iron Router subscriptions): handling server errors with waitOn

I'm attempting to redirect my users to a 404 page (or even to home) if they stumble onto a page for which there is no database match.
My waitOn code (inside the Iron Router Route)
waitOn: function(){
return Meteor.subscribe('cars', this.params.slug);
},
My publish method:
Meteor.publish("cars", function (slug) {
var selectedCar = Cars.findOne({slug: slug})._id;
return [
Cars.find({ _id: selectedCar}),
Parts.find({carid: selectedCar}),
]
});
Everything is fine, except waitOn hangs whenever someone visits a page for which there is no matching Car (i.e the :slug doesn't match anything in the database)
Example Server error:
Exception from sub cars id CTusRZSAPqJaK9ws3 TypeError: Cannot read
property '_id' of undefined
I've tried all sorts of things as recommended on various blogs/posts, and still not sure how to deal with such server errors when waitOn is involved.
Has anyone been able to deal with such errors in their subscriptions?
In your current code you are not handling the case where findOne method might return undefined as a result.
Please modify your publication:
Meteor.publish("cars", function (slug) {
var selectedCar = Cars.findOne({slug: slug});
if (selectedCar) {
return [
Cars.find({ _id: selectedCar._id}),
Parts.find({carid: selectedCar._id}),
]
}
this.ready()
});
in above code if findOne returns an undefined result, we are calling the this.ready() method which will set the subscription ready.
on the client side if you don't receive any data in your subscription you can display the 404 message (item not found).
Also you should validate the slug, before querying. Just to avoid any nosql injection. for that you could use the check package.

Meteor: Increment DB value server side when client views page

I'm trying to do something seemingly simple, update a views counter in MongoDB every time the value is fetched.
For example I've tried it with this method.
Meteor.methods({
'messages.get'(messageId) {
check(messageId, String);
if (Meteor.isServer) {
var message = Messages.findOne(
{_id: messageId}
);
var views = message.views;
// Increment views value
Messages.update(
messageId,
{ $set: { views: views++ }}
);
}
return Messages.findOne(
{_id: messageId}
);
},
});
But I can't get it to work the way I intend. For example the if(Meteor.isServer) code is useless because it's not actually executed on the server.
Also the value doesn't seem to be available after findOne is called, so it's likely async but findOne has no callback feature.
I don't want clients to control this part, which is why I'm trying to do it server side, but it needs to execute everytime the client fetches the value. Which sounds hard since the client has subscribed to the data already.
Edit: This is the updated method after reading the answers here.
'messages.get'(messageId) {
check(messageId, String);
Messages.update(
messageId,
{ $inc: { views: 1 }}
);
return Messages.findOne(
{_id: messageId}
);
},
For example the if(Meteor.isServer) code is useless because it's not
actually executed on the server.
Meteor methods are always executed on the server. You can call them from the client (with callback) but the execution happens server side.
Also the value doesn't seem to be available after findOne is called,
so it's likely async but findOne has no callback feature.
You don't need to call it twice. See the code below:
Meteor.methods({
'messages.get'(messageId) {
check(messageId, String);
var message = Messages.findOne({_id:messageId});
if (message) {
// Increment views value on current doc
message.views++;
// Update by current doc
Messages.update(messageId,{ $set: { views: message.views }});
}
// return current doc or null if not found
return message;
},
});
You can call that by your client like:
Meteor.call('messages.get', 'myMessageId01234', function(err, res) {
if (err || !res) {
// handle err, if res is empty, there is no message found
}
console.log(res); // your message
});
Two additions here:
You may split messages and views into separate collections for sake of scalability and encapsulation of data. If your publication method does not restrict to public fields, then the client, who asks for messages also receives the view count. This may work for now but may violate on a larger scale some (future upcoming) access rules.
views++ means:
Use the current value of views, i.e. build the modifier with the current (unmodified) value.
Increment the value of views, which is no longer useful in your case because you do not use that variable for anything else.
Avoid these increment operator if you are not clear how they exactly work.
Why not just using a mongo $inc operator that could avoid having to retrieve the previous value?

How to call cascading find() in Meteor + MongoDB?

I use Meteor & Iron-router. I have the following data context defined in the router:
data: function() { return {
eventId: this.params._id,
registrants: Registrants.find({eventIds: {$elemMatch: { $in: [this.params._id]}}}, {sort: {name:1, phone:1, email:1}}),
}}
I want to enable Registrants to be filtered further by user input. In my case, I already have ReactiveVar called filterName which listen to input text from user. Whenever the input text changed, the filterName is updated. ( I followed this answer ng-repeat + filter like feature in Meteor Blaze/Spacebars)
Now, I want to add $and to the Registrants.find() method to derive new registrants data context. How should I do it so that the query is reactive to the filterName?
Another approach is by defining Template helper method filteredRegistrants. Initially, its value is the same as return this.registrants. Whenever filterName changed, I would do return this.registrants.find({name: filterName}), but somehow I can't invoke find from registrants cursor, can I? I got undefined is not function error when doing that.
this.registrants is already a cursor (result of Registrants.find()), and not a collection, thus it doesn't have the find() method you look for. However, there is nothing wrong with making another query in the helper if the functionality provided by your controller is not enough:
Template.registrantsTemplate.helpers({
filteredRegistrants: function() {
return Registrants.find(...query...);
},
});

Mongoose No matching document found using id() method. Error caused by asynchronous delete requests

Making asynchronous requests in a loop to delete documents from an embedded collection:
_.each deletedItem, (item) ->
item.$delete()
Erratically throws this error:
{ message: 'No matching document found.', name: 'VersionError' }
When executing:
var resume = account.resumes.id(id);
resume.remove();
account.save(function (err, acct) {
console.log(err);
if(err) return next(err);
res.send(resume);
});
After logging account.resumes and looking through the _id's of all of the resumes, the document I am attempting to find by id, exists in the collection.
530e57a7503d421eb8daca65
FIND:
{ title: 'gggff', _id: 530e57a7503d421eb8daca65 }
IN:
[{ title: 'asddas', _id: 530e57a7503d421eb8daca61 }
{ title: 'gggff', _id: 530e57a7503d421eb8daca65 }
{ title: 'ewrs', _id: 530e57a7503d421eb8daca64 }]
I assume this has to do with the fact that I am performing these requests asynchronously, or that there is a versioning issue, but I have no idea how to resolve it.
It doesn't make any sense to me how when I log the resumes, I can see the resume I attempt to find, yet if I log:
log(account.resumes.id(id));
I get undefined.
UPDATE
I've discovered that my issue is with versioning.
http://aaronheckmann.blogspot.com/2012/06/mongoose-v3-part-1-versioning.html
But I am still unsure how to resolve it without disabling versioning, which I don't want to do.
In mongodb version 3, documents now have an increment() method which manually forces incrementation of the document version. This is also used internally whenever an operation on an array potentially alters array element position. These operations are:
$pull $pullAll $pop $set of an entire array
changing the version key
The version key is customizable by passing the versionKey option to the Schema constructor:
new Schema({ .. }, { versionKey: 'myVersionKey' });
Or by setting the option directly:
schema.set('versionKey', 'myVersionKey');
disabling
If you don’t want to use versioning in your schema you can disable it by passing false for the versionKey option.
schema.set('versionKey', false);
MongooseJs API docs explicitly warn on disabling versioning, and recommend against it. Your issue is due to workflow. If you're updating your collection from the UI, sending the API request and not refreshing your object with the object from the backend -- then attempt to update it again, you'll encounter the error you are reporting. I suggest either consuming/updating the object scope from the API response, then __v is correctly incremented. Or don't send the __v field on the PUT API request, this way it won't conflict with version on the collection in the database.
Another option is -- when requesting the object from the backend, have the API response not send the __v field, this way you don't have to code logic to NOT send it from the frontend. On all your gets for that collection, do either one of the following (depends how you write your queries):
var model = require('model');
var qry = model.find();
qry.select('-__v');
qry.exec(function(err,results){
if(err) res.status(500).send(err);
if(results) res.status(200).json(results);
});
OR
var model = require('model');
model.find({}, '-__v', function(err,results){
if(err) res.status(500).send(err);
if(results) res.status(200).json(results);
});

Average Aggregation Queries in Meteor

Ok, still in my toy app, I want to find out the average mileage on a group of car owners' odometers. This is pretty easy on the client but doesn't scale. Right? But on the server, I don't exactly see how to accomplish it.
Questions:
How do you implement something on the server then use it on the client?
How do you use the $avg aggregation function of mongo to leverage its optimized aggregation function?
Or alternatively to (2) how do you do a map/reduce on the server and make it available to the client?
The suggestion by #HubertOG was to use Meteor.call, which makes sense and I did this:
# Client side
Template.mileage.average_miles = ->
answer = null
Meteor.call "average_mileage", (error, result) ->
console.log "got average mileage result #{result}"
answer = result
console.log "but wait, answer = #{answer}"
answer
# Server side
Meteor.methods average_mileage: ->
console.log "server mileage called"
total = count = 0
r = Mileage.find({}).forEach (mileage) ->
total += mileage.mileage
count += 1
console.log "server about to return #{total / count}"
total / count
That would seem to work fine, but it doesn't because as near as I can tell Meteor.call is an asynchronous call and answer will always be a null return. Handling stuff on the server seems like a common enough use case that I must have just overlooked something. What would that be?
Thanks!
As of Meteor 0.6.5, the collection API doesn't support aggregation queries yet because there's no (straightforward) way to do live updates on them. However, you can still write them yourself, and make them available in a Meteor.publish, although the result will be static. In my opinion, doing it this way is still preferable because you can merge multiple aggregations and use the client-side collection API.
Meteor.publish("someAggregation", function (args) {
var sub = this;
// This works for Meteor 0.6.5
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
// Your arguments to Mongo's aggregation. Make these however you want.
var pipeline = [
{ $match: doSomethingWith(args) },
{ $group: {
_id: whatWeAreGroupingWith(args),
count: { $sum: 1 }
}}
];
db.collection("server_collection_name").aggregate(
pipeline,
// Need to wrap the callback so it gets called in a Fiber.
Meteor.bindEnvironment(
function(err, result) {
// Add each of the results to the subscription.
_.each(result, function(e) {
// Generate a random disposable id for aggregated documents
sub.added("client_collection_name", Random.id(), {
key: e._id.somethingOfInterest,
count: e.count
});
});
sub.ready();
},
function(error) {
Meteor._debug( "Error doing aggregation: " + error);
}
)
);
});
The above is an example grouping/count aggregation. Some things of note:
When you do this, you'll naturally be doing an aggregation on server_collection_name and pushing the results to a different collection called client_collection_name.
This subscription isn't going to be live, and will probably be updated whenever the arguments change, so we use a really simple loop that just pushes all the results out.
The results of the aggregation don't have Mongo ObjectIDs, so we generate some arbitrary ones of our own.
The callback to the aggregation needs to be wrapped in a Fiber. I use Meteor.bindEnvironment here but one can also use a Future for more low-level control.
If you start combining the results of publications like these, you'll need to carefully consider how the randomly generated ids impact the merge box. However, a straightforward implementation of this is just a standard database query, except it is more convenient to use with Meteor APIs client-side.
TL;DR version: Almost anytime you are pushing data out from the server, a publish is preferable to a method.
For more information about different ways to do aggregation, check out this post.
I did this with the 'aggregate' method. (ver 0.7.x)
if(Meteor.isServer){
Future = Npm.require('fibers/future');
Meteor.methods({
'aggregate' : function(param){
var fut = new Future();
MongoInternals.defaultRemoteCollectionDriver().mongo._getCollection(param.collection).aggregate(param.pipe,function(err, result){
fut.return(result);
});
return fut.wait();
}
,'test':function(param){
var _param = {
pipe : [
{ $unwind:'$data' },
{ $match:{
'data.y':"2031",
'data.m':'01',
'data.d':'01'
}},
{ $project : {
'_id':0
,'project_id' : "$project_id"
,'idx' : "$data.idx"
,'y' : '$data.y'
,'m' : '$data.m'
,'d' : '$data.d'
}}
],
collection:"yourCollection"
}
Meteor.call('aggregate',_param);
}
});
}
If you want reactivity, use Meteor.publish instead of Meteor.call. There's an example in the docs where they publish the number of messages in a given room (just above the documentation for this.userId), you should be able to do something similar.
You can use Meteor.methods for that.
// server
Meteor.methods({
average: function() {
...
return something;
},
});
// client
var _avg = { /* Create an object to store value and dependency */
dep: new Deps.Dependency();
};
Template.mileage.rendered = function() {
_avg.init = true;
};
Template.mileage.averageMiles = function() {
_avg.dep.depend(); /* Make the function rerun when _avg.dep is touched */
if(_avg.init) { /* Fetch the value from the server if not yet done */
_avg.init = false;
Meteor.call('average', function(error, result) {
_avg.val = result;
_avg.dep.changed(); /* Rerun the helper */
});
}
return _avg.val;
});