Meteor Subscription hook? - meteor

I have a really stupid question on the subscription hook function on the routecontroller.
I don't really know where the .ready variable or method come from, is it with the built in variable for the subscription?
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? nextPath : null
};
}
});

Related

Why isn't Meteor.subscribe receiving data from a publication

I have this publication
Meteor.publish('user', function(id){
if(id == this.userId) {
return Meteor.users.find({_id: id}, {fields: {dob: 1, name: 1, ability: 1, location: 1, clubs: 1, coach: 1, friends: 1}});
} else {
var user = Meteor.users.findOne({_id: id});
var fields = {};
if(user.dob.allowed)
fields.dob = 1;
if(user.ability.allowed)
fields.ability = 1;
if(user.coach.allowed)
fields.coach = 1;
if(user.location.allowed)
fields.location = 1;
if(user.clubs.allowed)
fields.clubs = 1;
if(user.friends.allowed)
fields.friends = 1;
fields.name = 1;
return Meteor.users.find({_id: id}, {fields: fields});
}
});
However when i subscribe to this on the client and try to findOne on the user I have just subscribed to, the return is undefined.
Router.route('/user/:_id/profile', {
template: "home",
yieldRegions: {
"profile": {to: "option"}
},
data: function() {
Meteor.subscribe('user', this.params._id);
return Meteor.Users.findOne({_id: this.params._id});
},
onBeforeAction: function() {
if(Meteor.userId() == null) {
Router.go('/');
} else {
this.next()
}
}
});
What am I doing wrong here?
If you follow the approach in the Iron Router guide, you may want to set up your route as follows:
Router.route('/user/:_id/profile', {
template: "home",
yieldRegions: {
"profile": {to: "option"}
},
// make the route wait on the subscription to be ready
waitOn: function() {
Meteor.subscribe('user', this.params._id);
},
data: function() {
return Meteor.Users.findOne({_id: this.params._id});
},
onBeforeAction: function() {
if(Meteor.userId() == null) {
Router.go('/');
} else {
this.next()
}
}
});
This way your route data method will not be called until the subscription is ready.

Unable to display data from database

I am building a forum with Meteor and would like to display comments to answers. And the answers are on the same page as an individual question. I am able to save the comments once submitted (I can check the mongo database and I see that I am successfully submitting comments), but I'm not able to display them using templates. I would think the problem has something to do with publications and subscriptions, but I can't locate the error. Below are the relevant snippets of code.
answer_item.js
Template.answerItem.helpers({
submittedText: function() {
return this.submitted.toString();
},
comments: function() {
return Comments.find({answerId: this._id});
}
});
answer_item.html
<template name="answerItem">
...
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
...
</template>
comment_item.html
<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submitted}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
comment_item.js
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
lib/collections/comment.js
Comments = new Mongo.Collection('comments');
Meteor.methods({
commentInsert: function(commentAttributes) {
check(this.userId, String);
check(commentAttributes, {
answerId: String,
body: String
});
var user = Meteor.user();
var answer = Answers.findOne(commentAttributes.answerId);
if (!answer)
throw new Meteor.Error('invalid-comment', 'You must comment on an answer');
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
Answers.update(comment.answerId, {$inc: {commentsCount: 1}});
comment._id = Comments.insert(comment);
return comment._id
}
});
router.js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
QuestionsListController = RouteController.extend({
template: 'questionsList',
increment: 5,
questionsLimit: function() {
return parseInt(this.params.questionsLimit) || this.increment;
},
findOptions: function() {
return {sort: this.sort, limit: this.questionsLimit()};
},
subscriptions: function() {
this.questionsSub = Meteor.subscribe('questions', this.findOptions());
},
questions: function() {
return Questions.find({}, this.findOptions());
},
data: function() {
var self = this;
return {
questions: self.questions(),
ready: self.questionsSub.ready,
nextPath: function() {
if (self.questions().count() === self.questionsLimit())
return self.nextPath();
}
};
}
});
NewQuestionsController = QuestionsListController.extend({
sort: {submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.newQuestions.path({questionsLimit: this.questionsLimit() + this.increment})
}
});
FollowedQuestionsController = QuestionsListController.extend({
sort: {follows: -1, submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.followedQuestions.path({questionsLimit: this.questionsLimit() + this.increment})
}
});
Router.route('/', {
name: 'home',
controller: NewQuestionsController
});
Router.route('/new/:questionsLimit?', {name: 'newQuestions'});
Router.route('/followed/:questionsLimit?', {name: 'followedQuestions'});
Router.route('/questions/:_id', {
name: 'questionPage',
waitOn: function() {
return [
Meteor.subscribe('singleQuestion', this.params._id),
Meteor.subscribe('answers', this.params._id),
Meteor.subscribe('comments', this.params._id)
];
},
data: function() { return Questions.findOne(this.params._id); }
});
Router.route('/questions/:_id/edit', {
name: 'questionEdit',
waitOn: function() {
return Meteor.subscribe('singleQuestion', this.params._id);
},
data: function() { return Questions.findOne(this.params._id); }
});
Router.route('/submit', {name: 'questionSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'questionPage'});
Router.onBeforeAction(requireLogin, {only: 'questionSubmit'});
server/publications.js
Meteor.publish('comments', function(answerId) {
check(answerId, String);
return Comments.find({answerId: answerId});
});
It looks like you need to run a separate query in your comment publication, to get a list of all of the answers for the given question, then use the results of that query to get a list of all the comments for all of the answers.
Meteor.publish('comments', function(questionId) {
check(questionId, String);
var answerIds = _.pluck(Answers.find({'questionId': questionId}, {fields: {_id: 1}}).fetch(), '_id');
return Comments.find({answerId: {$in: answerIds});
});
EDIT
I have a similar feature within an app that I'm working on now, with the same issue you were running into. I spent a few hours on it yesterday and came to the conclusion that the issue has to do with the fact that the _.pluck statement converts the results from the Answers cursor to an array, which prevents the publish function from being reactive.
After looking into several solutions the best one I found was the publish composite package. The syntax is a little verbose, but it gets the job done. To make it all work properly you need to merge all of the publish functions for the question, answers, and comments all into one publish function. Under the covers it creates observeChanges watchers on each of the answers under the question, so it can be reactive.
Meteor.publishComposite('question', function(questionId) {
return {
find: function() {
return Questions.find({_id: questionId});
},
children: [
{
find: function(question) {
return Answers.find({questionId: question._id});
},
children: [
{
find: function(answer, question) {
return Comments.find({answerId: answer._id});
}
}
]
}
]
}
});

Need help updating Router.js for upgrade to Meteor 1.0 and iron:router package

I'm in the midst of updating from Meteor 8.2 to Meteor 1.0. I've removed all my old meteorite packages and installed the relevant meteor package system packages. I had to install the new iron-router package and I'm getting the following error in my console on meteor run:
Route dispatch never rendered. Did you forget to call this.next() in an onBeforeAction?
The migration notes for the package say: "onBeforeAction hooks now require you to call this.next(), and no longer take a pause() argument."
I tried following the example by remove pause from the function and adding this.next(); after the else statement, but to no avail.
How to edit my router so it uses the new onBeforeAction hook? Also, anything else you can call out from the migration that might be problematic would be much appreciated. Thanks!
Here's my router file:
/*****************************************************************************/
/* Client and Server Routes */
/*****************************************************************************/
// TODO: use these as per the Event Mind CLI tool.
//Router.configure({
// templateNameConverter: 'upperCamelCase',
// routeControllerNameConverter: 'upperCamelCase'
//});
Router.configure({
layoutTemplate: 'devLayout',
notFoundTemplate: 'devMain',
loadingTemplate: 'loading'
});
Router.onRun(function () {Session.set("waiting-on", null); });
Router.onBeforeAction(function() { Alerts.clearSeen(); });
var filters = {
nProgressHook: function (pause) {
// we're done waiting on all subs
if (this.ready()) {
NProgress.done();
} else {
NProgress.start();
pause(); // stop downstream funcs from running
}
}
};
Router.onBeforeAction(filters.nProgressHook);
Meteor.startup(function () {
Router.map(function () {
this.route('loading');
// reset password urls use hash fragments instead of url paths/query
// strings so that the reset password token is not sent over the wire
// on the http request
this.route('reset-password', {
template: 'devMain',
layoutTemplate: 'devLayout',
onRun: function () {
var token = this.params.hash;
Meteor.logout(function () {
Session.set("viewing-settings", true);
Session.set("set-password-token", token);
Session.set("settings-set-password", true);
// Session.set("enrolling", true) // do something special?
});
}
});
this.route('verify-email', {
template: 'devMain',
layoutTemplate: 'devLayout',
action: function () {
var self = this;
var token = self.params.hash;
Accounts.verifyEmail(token, function (err) {
if (!err) {
Alerts.throw({
message: "Your email address is now verified!",
type: "success", where: "main",
autoremove: 3000
});
Router.go('home');
} else {
Alerts.throw({
message: "Hmm, something went wrong: \""+err.reason +
"\". Try again?",
type: "danger", where: "main"
});
Session.set("viewing-settings", true);
Router.go('home');
}
});
}
});
this.route('leave-game', {
template: 'devMain',
layoutTemplate: 'devLayout',
action: function () {
var self = this;
var token = self.params.hash;
Meteor.call("leaveGameViaToken", token, function (err, res) {
if (!err) {
// Idempotently verify user's email,
// since they got the token via email.
Accounts.verifyEmail(token);
if (res.error) {
// e.g. "Leave-game link is for unknown game"
Alerts.throw({
message: res.error.reason, type: "danger", where: "main"
});
Router.go("home");
} else {
Alerts.throw({
message: "OK, you are no longer in this game.",
type: "success", where: res.gameId
});
Router.go("devDetail", {_id: res.gameId});
}
} else {
Alerts.throw({
message: "Hmm, something went wrong: \""+err.reason + "\".",
type: "danger", where: "main"
});
Router.go("home");
}
});
}
});
this.route('game-on', {
template: 'devMain',
layoutTemplate: 'devLayout',
action: function () {
var self = this;
var token = self.params.hash;
Meteor.call("gameOnViaToken", token, function (err, res) {
if (err || (res && res.error)) {
errorMessage = err ? "Hmm, something went wrong: \"" + err.reason + "\"." : res.error.reason;
Alerts.throw({
message: errorMessage, type: "danger", where: "main"
});
Router.go("home");
} else {
Alerts.throw({
message: "Woohoo! Players will be notified.",
type: "success", where: res.gameId
});
Router.go("devDetail", {_id: res.gameId, token: token });
}
});
}
});
this.route('cancel-game', {
template: 'devMain',
layoutTemplate: 'devLayout',
action: function () {
var self = this;
var token = self.params.hash;
Meteor.call("cancelGameViaToken", token, function (err, res) {
if (!err) {
Accounts.verifyEmail(token);
if (res.error) {
Alerts.throw({
message: res.error.reason, type: "danger", where: "main"
});
Router.go("home");
} else {
Alerts.throw({
message: "OK, your game is now cancelled, and players "
+ "will be notified.",
type: "success", where: "main"
});
Router.go("home");
}
} else {
Alerts.throw({
message: "Hmm, something went wrong: \""+err.reason + "\".",
type: "danger", where: "main"
});
Router.go("home");
}
});
}
});
// quite similar to 'leave-game' route
this.route('unsubscribe-all', {
template: 'devMain',
layoutTemplate: 'devLayout',
action: function () {
var self = this;
var token = self.params.hash;
Meteor.call("unsubscribeAllViaToken", token, function (err, res) {
if (!err) {
// Idempotently verify user's email,
// since they got the token via email.
Accounts.verifyEmail(token);
if (res.error) {
// e.g. "Token provided in link is not an unsubscribe-all token"
Alerts.throw({
message: res.error.reason, type: "danger", where: "main"
});
Router.go("home");
} else {
Alerts.throw({
message: "OK, you will no longer receive emails "
+ "from Push Pickup.",
type: "success", where: "main"
});
Router.go("home");
}
} else {
Alerts.throw({
message: "Hmm, something went wrong: \""+err.reason + "\".",
type: "danger", where: "main"
});
Router.go("home");
}
});
}
});
this.route('enroll-account', {
template: 'devMain',
layoutTemplate: 'devLayout',
onRun: function () {
var token = this.params.hash;
Meteor.logout(function () {
Session.set("viewing-settings", true);
Session.set("set-password-token", token);
Session.set("settings-set-password", true);
// Session.set("enrolling", true) // do something special?
});
}
});
// the home page. listing and searching for games
this.route('home', {
path: '/',
template: 'devMain',
layoutTemplate: 'devLayout'
});
// typical user interaction with a single game
this.route('devDetail', {
path: '/g/:_id/:token?',
layoutTemplate: 'devLayout',
onRun: function () {
Session.set("joined-game", null);
},
waitOn: function () {
return Meteor.subscribe('game', this.params._id);
},
onBeforeAction: function (pause) {
Session.set("soloGame", this.params._id);
},
data: function () {
var game = Games.findOne(this.params._id);
if (game) {
Session.set("gameExists", true);
}
return game;
},
action: function () {
var token = this.params.token;
if (Session.get("gameExists")) {
this.render();
} else {
Router.go('home');
Alerts.throw({
message: "Game not found",
type: "warning", where: "top"
});
}
if (token) {
Meteor.call("sendReminderEmailsViaToken", token, function (err, res) {
var errorMessage;
Accounts.verifyEmail(token);
if (err || (res && res.error)) {
errorMessage = err ? "Hmm, something went wrong: \"" + err.reason + "\"." : res.error.reason;
Alerts.throw({
message: errorMessage, type: "danger", where: "main"
});
Router.go("home");
}
});
}
},
onStop: function () {
Session.set("soloGame", null);
Session.set("gameExists", null);
}
});
this.route('devAddGame', {
path: '/addGame',
template: 'devEditableGame',
layoutTemplate: 'devLayout',
onRun: function () {
Session.set("selectedLocationPoint", null);
Session.set("newGameDay", null);
Session.set("newGameTime", null);
InviteList.remove({});
},
waitOn: function() {
Meteor.subscribe('recently-played');
},
data: function () {
return {
action: 'add',
title: 'Add game',
submit: 'Add game'
};
}
});
this.route('invitePreviousPlayers', {
path: 'invitePlayers',
template: 'invitePreviousPlayers',
layoutTemplate: 'devLayout'
});
this.route('devEditGame', {
path: '/editGame/:_id',
template: 'devEditableGame',
layoutTemplate: 'devLayout',
onRun: function () {
Session.set("selectedLocationPoint", null);
},
waitOn: function () {
return Meteor.subscribe('game', this.params._id);
},
onBeforeAction: function (pause) {
Session.set("soloGame", this.params._id);
},
data: function () {
return _.extend({
action: 'edit',
title: 'Edit game',
submit: 'Update game'
}, Games.findOne(this.params._id));
},
action: function () {
var self = this;
var user = Meteor.user();
var game = self.data();
if (user && user._id === game.creator.userId ||
user && user.admin) {
self.render();
} else {
Router.go('home');
}
}
});
this.route('adminView', {
path: '/admin',
onBeforeAction: function () {
var user = Meteor.user();
if (!user || !user.admin) {
this.render('home');
}
}
});
});
});
New onBeforeAction hook must call this.next(); You must call it in every onBeforeAction. For example your admin route in new Iron Router would look like this:
Router.route('/admin', {
name: 'adminView',
onBeforeAction: function () {
var user = Meteor.user();
if (!user || !user.admin) {
this.render('home');
}
this.next();
}
});
Replace all this.route(...) in Router.map with Router.route('/path', options) and remove Router.map()
Your global onBeforeAction will look like this:
Router.onBeforeAction(function() {
Alerts.clearSeen();
this.next();
});
Also, you don't need to wrap your routes in Meteor.startup(...). You can remove it.
And there is no pause parameter anymore, instead of pause call this.next() outside condition:
var filters = {
nProgressHook: function () {
// we're done waiting on all subs
if (this.ready()) {
NProgress.done();
} else {
NProgress.start();
}
this.next();
}
};
Router.onBeforeAction(filters.nProgressHook);

IronRouter: template is rendered before call in waitOn is finished

I want to create charts based on data from my collection.
This data is called by an aggregate pull in my router-settings and set by Session.data in the template.rendered function.
The Meteor.call is placed in the waitOn-function.
If the template is rendered, the data is not present.
I tried onBeforeAction, action, setTimeout... but i can't set the render-function to wait until the call-data is present.
I tried to set the calls in the onBeforeAction and onRun hooks, in the action, waitOn and data functions both on my RouteController and Router.route.
I wrapped my rendered-code with setTimeout, but it didn't work.
Router.configure({
layoutTemplate: 'global',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
});
Router.onBeforeAction("loading");
is set in my global routing settings.
I've already tried following solutions:
question 23575826
question 26198531
https://github.com/EventedMind/iron-router/issues/554#issuecomment-39002306
and more in the last days.
Is there any suggestion for my router settings or another way to solve this problem and get the data rendered in time?
I consider to pick the npm-modules fiber/future, but i've no idea how to embed and use them.
My settings:
Meteor is v1.0.2.1
router.js with own controller
StatsController = RouteController.extend({
template: 'statsShow',
waitOn: function () {
return [
Meteor.call('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'SellerOne', 2014, function(error, result){
if(!error)
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
}),
Meteor.call('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'SellerTwo', 2014, function(error, result){
if(!error)
Session.set('info3Units', result['units']);
Session.set('info3Volumes', result['volumes']);
}),
Meteor.call('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'SellerThree', 2014, function(error, result){
if(!error)
Session.set('avaUnits', result['units']);
Session.set('avaVolumes', result['volumes']);
})
];
},
data: function () {
return Books.findOne({_id: this.params._id});
},
action: function () {
if (!this.ready()) {
this.render('Loading');
} else {
this.render();
}
}
});
Router.route('stats/show/', {
name: 'stats.show',
controller: 'TestController'
});
methods.js
Meteor.methods({
saleGetDataPerYear: function(bookId, seller, year) {
var sellerUnits = [];
var sellerVolumes = [];
var resultData = {};
var pipeline = [
{
$match : { bookId: bookId, salesSeller: seller, salesYear: year }
},
{
$group : {
_id : {
sale: { "salesMonth": "$salesMonth" }
},
units: { $sum: "$salesUnits" },
volumes: { $sum: "$salesVolumes" },
month: { $first: "$salesMonth" },
year: { $first: "$salesYear" },
seller: { $first: "$salesSeller" }
}
},
{
$sort : {
month: 1
}
}
];
result = Sales.aggregate(pipeline);
if(result){
sellerUnits.push(seller);
sellerVolumes.push(seller);
result.forEach(function(data){
sellerUnits.push(data.units);
sellerVolumes.push(data.volumes);
});
resultData['units'] = sellerUnits;
resultData['volumes'] = sellerVolumes;
}
if(resultData){
return resultData;
} else {
throw new Meteor.Error("no-data", "No Data collected");
}
}
template
//-- template rendered functions
Template.statsShow.rendered = function(){
var chartUnitsBrockhaus = Session.get('brockhausUnits');
var chartUnitsInfo3 = Session.get('info3Units');
var chartUnitsAva = Session.get('avaUnits');
var chartUnitsSumme = Session.get('sumUnits');
console.log(chartUnitsBrockhaus);
var chartUnits = c3.generate({
bindto: this.find('.chartUnits'),
data: {
columns: [
chartUnitsBrockhaus,
chartUnitsInfo3,
chartUnitsAva,
chartUnitsSumme
],
type: 'bar',
types: {
Summe: 'spline',
},
},
axis: {
x: {
type: 'category',
categories: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
}
},
bar: {
width: {
ratio: 0.5
}
}
});
};
packages
accounts-password 1.0.5
accounts-ui 1.1.4
alanning:roles 1.2.13
aldeed:autoform 4.2.2
aldeed:autoform-select2 1.0.3
aldeed:collection2 2.3.1
aldeed:simple-schema 1.3.0
anti:fake 0.4.1
chrismbeckett:fontawesome4 4.2.2
coffeescript 1.0.5
ctjp:meteor-bootstrap-switch 3.3.1_1
dburles:collection-helpers 1.0.2
francocatena:status 1.0.3
iron:router 1.0.7
lepozepo:accounting 1.0.0
less 1.0.12
matteodem:easy-search 1.4.6
meteor-platform 1.2.1
meteorhacks:aggregate 1.1.0
mrt:jquery-csv 0.7.1
natestrauser:select2 3.5.1
nemo64:bootstrap 3.3.1_1
ongoworks:security 1.0.1
peerlibrary:xml2js 0.4.4_3
peernohell:c3 1.1.2
sacha:spin 2.0.4
service-configuration 1.0.3
underscore 1.0.2
zimme:select2-bootstrap3-css 1.4.1
Edit
as #DavidWeldon mentioned i changed my waitOn function to:
waitOn: function () {
return [
// first call
Meteor.callWithReady('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'Brockhaus', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
};
}),
// second call
Meteor.callWithReady('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'Info3', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
};
}),
// third call
Meteor.callWithReady('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'AVA', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
};
}),
// fourth call
Meteor.callWithReady('saleGetSumDataPerYear', 'nYWpgxR3kEY8kwBkA', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('sumUnits', result['units']);
Session.set('sumVolumes', result['volumes']);
}
})
];
},
and added test.coffee under /lib:
_.defaults Meteor,
callWithReady: (method, options...) ->
dep = new Deps.Dependency
ready = false
lastOption = _.last options
if _.isFunction lastOption
Meteor.apply method, _.initial(options), (err, result) ->
lastOption err, result
ready = true
dep.changed()
else
Meteor.apply method, options, (err, result) ->
ready = true
dep.changed()
ready: ->
dep.depend()
ready
result is: my calls loop.
I tested the answer from #apendua.
function waitUntilDone (action) {
var isReady = new ReactiveVar(false);
action(function () {
isReady.set(true);
});
return {
ready: function () {
return isReady.get();
}
};
}
waitOn: function () {
return [
// first call
waitUntilDone(function(done) {
Meteor.callWithReady('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'Brockhaus', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
};
done();
})
}),
// second call
waitUntilDone(function(done) {
Meteor.call('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'Info3', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
done();
};
})
}),
// third call
waitUntilDone(function(done) {
Meteor.call('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'AVA', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
done();
};
})
}),
// fourth call
waitUntilDone(function(done) {
Meteor.call('saleGetSumDataPerYear', 'nYWpgxR3kEY8kwBkA', 2014, function(error, result){
if(!error) {
console.log(result);
Session.set('sumUnits', result['units']);
Session.set('sumVolumes', result['volumes']);
done();
}
})
})
];
},
or
waitOn: function () {
return [
// first call
waitUntilDone(function(done) {
Meteor.callWithReady('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'Brockhaus', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
};
done();
}),
Meteor.call('saleGetDataPerYear', 'nYWpgxR3kEY8kwBkA', 'Info3', 2014, function(error, result){
if(!error) {
console.log(result); //debug
Session.set('brockhausUnits', result['units']);
Session.set('brockhausVolumes', result['volumes']);
done();
};
})
[...]
})
];
},
both results are: my calls loop.
What you're missing here is that in your waitOn you need to return a list of objects, each of them having a ready method which acts as a reactive data source. Unfortunately, Meteor.call does not return this kind of object, but for example Meteor.subscribe does.
What you can do is to use the following wrapper but, make sure you have reactive-var package added to your project first.
function waitUntilDone (action) {
var isReady = new ReactiveVar(false);
action(function () {
isReady.set(true);
});
return {
ready: function () {
return isReady.get();
}
};
}
Now, instead of returning a list of results of Meteor.call like this
waitOn: function () {
return [
Meteor.call(..., function () { ... }),
Meteor.call(..., function () { ... }),
// ...
]
}
use the above wrapper in the following way
waitOn: function () {
return [
waitUntilDone(function(done) {
Meteor.call(..., function () {
// ...
done();
}),
}),
// ...
]
}
Update: After some days of testing i changed the functions to Meteor.publish instead of Meteor.method so the waitOn function is now working.
i didn't realise that this also work with publish. The examples for aggregate db-calls are all with Meteor.method.
publications.js
Meteor.publish('saleGetAllDataPerYear', function(bookId, year) {
self = this;
var pipeBH = [];
var resultBH = '';
var unitsBH = [];
var volumesBH = [];
var monthBH = [];
var pipeI3 = [];
var resultI3 = '';
var unitsI3 = [];
var volumesI3 = [];
var monthI3 = [];
var pipeAVA = [];
var resultAVA = '';
var unitsAVA = [];
var volumesAVA = [];
var monthAVA = [];
var pipeSum = [];
var resultSum = '';
var unitsSum = [];
var volumesSum = [];
var monthSum = [];
// Set Brockhaus data
pipeBH = [
{ $match : { bookId: bookId, salesSeller: 'Brockhaus', salesYear: year } },
{ $group : { _id : { sale: { "salesMonth": "$salesMonth" } },
units: { $sum: "$salesUnits" }, volumes: { $sum: "$salesVolumes" }, month: { $first: "$salesMonth" }, year: { $first: "$salesYear" }, seller: { $first: "$salesSeller" }
}
},
{ $sort : { month: 1 } }
];
resultBH = Sales.aggregate(pipeBH);
if(resultBH != ''){
unitsBH.push('Brockhaus');
volumesBH.push('Brockhaus');
resultBH.forEach(function(data){
unitsBH.push(data.units);
volumesBH.push(data.volumes);
monthBH.push(data.month);
});
self.added('stats', Random.id(), {seller: 'Brockhaus', units: unitsBH, volumes: volumesBH, month: monthBH, year: year});
self.ready();
} else {
self.ready();
}
// Set Info3 data
pipeI3 = [
{ $match : { bookId: bookId, salesSeller: 'Info3', salesYear: year } },
{ $group : { _id : { sale: { "salesMonth": "$salesMonth" } },
units: { $sum: "$salesUnits" }, volumes: { $sum: "$salesVolumes" }, month: { $first: "$salesMonth" }, year: { $first: "$salesYear" }, seller: { $first: "$salesSeller" }
}
},
{ $sort : { month: 1 } }
];
resultI3 = Sales.aggregate(pipeI3);
if(resultI3 != ''){
unitsI3.push('Info3');
volumesI3.push('Info3');
resultI3.forEach(function(data){
unitsI3.push(data.units);
volumesI3.push(data.volumes);
monthI3.push(data.month);
});
self.added('stats', Random.id(), {seller: 'Info3', units: unitsI3, volumes: volumesI3, month: monthI3, year: year});
self.ready();
} else {
self.ready();
}
// Set AVA data
pipeAVA = [
{ $match : { bookId: bookId, salesSeller: 'AVA', salesYear: year } },
{ $group : { _id : { sale: { "salesMonth": "$salesMonth" } },
units: { $sum: "$salesUnits" }, volumes: { $sum: "$salesVolumes" }, month: { $first: "$salesMonth" }, year: { $first: "$salesYear" }, seller: { $first: "$salesSeller" }
}
},
{ $sort : { month: 1 } }
];
resultAVA = Sales.aggregate(pipeAVA);
if(resultAVA != ''){
unitsAVA.push('AVA');
volumesAVA.push('AVA');
resultAVA.forEach(function(data){
unitsAVA.push(data.units);
volumesAVA.push(data.volumes);
monthAVA.push(data.month);
});
self.added('stats', Random.id(), {seller: 'AVA', units: unitsAVA, volumes: volumesAVA, month: monthAVA, year: year});
self.ready();
} else {
self.ready();
}
// Set Sum data
pipeSum = [
{ $match : { bookId: bookId, salesYear: year } },
{ $group : { _id : { sale: { "salesMonth": "$salesMonth" } },
units: { $sum: "$salesUnits" }, volumes: { $sum: "$salesVolumes" }, month: { $first: "$salesMonth" }, year: { $first: "$salesYear" }, seller: { $first: "$salesSeller" }
}
},
{ $sort : { month: 1 } }
];
resultSum = Sales.aggregate(pipeSum);
if(resultSum != ''){
unitsSum.push('Summe');
volumesSum.push('Summe');
resultSum.forEach(function(data){
unitsSum.push(data.units);
volumesSum.push(data.volumes);
monthSum.push(data.month);
});
self.added('stats', Random.id(), {seller: 'Summe', units: unitsSum, volumes: volumesSum, month: monthSum, year: year});
self.ready();
} else {
self.ready();
}
});
router.js
waitOn: function () {
year = Number(Session.get('year'));
return [
Meteor.subscribe('saleGetAllDataPerYear', this.params._id, year),
Meteor.subscribe('getStats')
];
},
Thanks to #JeremyS for the inspiration on another way. That sounds like the better solution because now the waitOn function works but the data is not rendered in my chart without manual refresh of my template.

Meteor.js collection aggregate returning undefined is not a function

I am trying to make collection aggregate in my Meteor.js app as shown below, yet each time I call my server logSummary method I get the following error. Can someone please tell me what I am doing wrong / how to resolve this error? Thanks.
Note: I am using Meteor-aggregate package
TypeError: undefined is not a function
at Object.Template.detailedreport.helpers.myCollection (http://localhost:3000/client/views/report.js?
Code:
Template.detailedreport.rendered = function() {
Session.set("dreport_customer", "");
Session.set("dreport_project", "");
Session.set("dreport_startDate", new Date());
Session.set("dreport_endDate", new Date());
$('.set-start-date').datetimepicker({
pickTime: false,
defaultDate: new Date()
});
$('.set-end-date').datetimepicker({
pickTime: false,
defaultDate: new Date()
});
$('.set-start-date').on("dp.change",function (e) {
Session.set("dreport_startDate", $('.set-start-date').data('DateTimePicker').getDate().toLocaleString());
});
$('.set-end-date').on("dp.change",function (e) {
Session.set("dreport_endDate", $('.set-end-date').data('DateTimePicker').getDate().toLocaleString());
});
};
Template.detailedreport.helpers({
customerslist: function() {
return Customers.find({}, {sort:{name: -1}});
},
projectslist: function() {
return Projects.find({customerid: Session.get("dreport_customer")}, {sort:{title: -1}});
},
myCollection: function () {
var now = Session.get("dreport_startDate");
var then = Session.get("dreport_endDate");
var custID = Session.get("dreport_customer");
var projID = Session.get("dreport_project");
Meteor.call('logSummary', now, then, projID, custID, function(error, data){
if(error)
return alert(error.reason);
return data;
});
},
settings: function () {
return {
rowsPerPage: 10,
showFilter: true,
showColumnToggles: false,
fields: [
{ key: '0._id.day', label: 'Day' },
{ key: '0.totalhours', label: 'Hours Spent'}
]
};
}
});
Template.detailedreport.events({
'submit form': function(e) {
e.preventDefault();
Session.set('dreport_endDate', $('.set-end-date').data('DateTimePicker').getDate().toLocaleString());
Session.set('dreport_startDate', $('.set-start-date').data('DateTimePicker').getDate().toLocaleString());
Session.set('dreport_project', $(e.target).find('[name=project]').val());
Session.set('dreport_customer', $(e.target).find('[name=customer]').val());
},
'change #customer': function(e){
Session.set("dreport_project", "");
Session.set("dreport_customer", e.currentTarget.value);
},
'change #project': function(e){
Session.set("dreport_project", e.currentTarget.value);
}
});
Template:
<div>
{{> reactiveTable class="table table-bordered table-hover" collection=myCollection settings=settings}}
</div>
Server:
Meteor.methods({
logSummary: function(startDate, endDate, projid, custid){
var pipeline = [
{ $match: { date: { $gte: new Date(startDate), $lte: new Date(endDate) },
projectid: projid,
customerid: custid
}
},
{ $group: {
_id: {
"projectid": "$projectid",
"day": { "$dayOfMonth": "$date" },
"month": { "$month": "$date" },
"year": { "$year": "$date" }
},
totalhours: {"$sum": "$hours"}
}}
];
return ProjectLog.aggregate(pipeline);;
}
});
Looking at the ReactiveTable documentation, it looks like you need to do something like:
Template.myTemplate.helpers({
myCollection: function () {
return myCollection;
}
});
Where myCollection is the name of a Mongo/Meteor collection (e.g. BlogPosts) which you defined with e.g. BlogPosts = new Mongo.Collection('blogPosts');
The reason you're getting undefined is not a function is that you are calling a Meteor method inside a template helper. The call is asynchronous so the return value is undefined. Now you are passing undefined to ReactiveTable. ReactiveTable will be trying to call something like myCollection.find() which is essentially undefined.find() and will therefore throw the error you're seeing.
Later on the Meteor call will finish and the data value will be lost because the function has already returned.
You can call Meteor.call inside the onCreated function like so:
Template.myTemplate.onCreated(function () {
Meteor.call('myFunction', 'my', 'params', function (err, result) {
if (err) {
// do something about the error
} else {
Session.set('myData',result);
}
});
});
Template.myTemplate.helpers({
myData: function () {
Session.get('myData')
}
});
This won't fix the issue with ReactiveTable, however.
If the collection you're trying to display is only used for this single page, you could put the aggregation inside the publish function so that minimongo contains only the documents that match the aggregation and therefore the correct documents will appear in your ReactiveTable.

Resources