Non reactive helper and template #each crashing - mongodb

This is about a problem with a helper containing a dynamic query that involves reactive vars and the $where operator that does not rerun when the reactive vars values are changed. Then about how a try to solve it lead to a strange system behavior and crash.
I have a template in which we want to show a list of found documents inserted within an #each loop:
<template name="todo">
{{#each pendingMTs}}
<button class="showPendMT">
<h4> - <strong> {{name}}</strong> </h4>
</button>
{{/each}}
</template>
The pendingMTs helper searches for the documents in the Tasks collection with a dynamic and somehow elaborate -here simplified- query that involves using the $where operator:
pendingMTs() {
return Tasks.find(
{ $and: [ { owner: Meteor.userId() },
{ time: { $gt: 0 } },
{ $where: function()
{ return moment(this.createdAt).add( (((this.timeuts == 'Days') ? 1 : ((this.timeuts=='Meses') ? 30 : 365)) * this.time), 'days').diff(moment([currYear.get(), currMonth.get()])) < 0; }}
]
});
}
The two reactive vars involved in the search are defined at the creation of the template:
Template.todo.onCreated(function() {
var year = moment().format("YYYY");
var month = moment().format("M");
month = month - 1; //to values from 0 to 11
currYear = new ReactiveVar(year);
currMonth = new ReactiveVar(month);
});
Then in an event handler we modify the reactive vars upon a 'select' change, for instance for the currMonth:
'change #monthForecast' (event) {
event.preventDefault();
const target = event.target;
currMonth.set( target.value );
},
The first issue is that the helper is not rerun despite we modify through the event handler the value of the reactive vars: WHY??
Thinking that this might be due to the fact that the reactive vars are inside the $where function, I added a simple line at the beginning of the helper in order simply to create awareness for them in the helper:
var rerun = currYear.get(); rerun = currMonth.get();
Now certainly that made the helper to rerun any time any of the 2 reactive var was changed. But unfortunately this lead to a very strange behavior:
If the reactive vars were modified, but did not affect the documents retrieved by the helper, system was running fine, but:
When the modified reactive vars caused the helper to retrieve one more document than the number of documents retrieved the first time, the system crashed (and therefore the new document was not shown in the #each template):
Exception from Tracker recompute function: meteor.js:930:11
Error: Bad index in range.getMember: 3
While looking for the cause I found out that the bad index number given, 3 in this case, is always equal to the number of documents retrieved the first time the helper was executed. In this case, after modifying the value of one of the reactive vars, a 4th document had to be shown when system crashed.
I found some maybe related issues on https://github.com/GroundMeteor/db/issues/184
Could anyone point out how this dynamic query involving $where with reactive vars could be done, maintaining its natural reactivity?

Looks like you're not getting and setting your reactive-vars properly. You can't just call myReactiveVar.get() - they are (usually) attached to the template instance so your code should be:
In the pendingMTs helper, you access the instance with const instance = Template.instance(); and then in your $where function you would use instance.currentMonth.get() and instance.currentYear.get()
In the event, the second argument of the event is the template instance, so you would have:
'change #monthForecast' (event, templateInstance) {
event.preventDefault();
const target = event.target;
templateInstance.currMonth.set( target.value );
},
So you're not too far off!
https://docs.meteor.com/api/reactive-var.html
https://themeteorchef.com/tutorials/reactive-dict-reactive-vars-and-session-variables

Related

Rows binding in sap.ui.table.Table dynamically

I have made a title depending on variable how it's shown in: Title depending on other variable in SAPUI5
I would like to make the same with rows in sap.ui.table.Table so I tried:
rows="{= ${someData>/infos}.length > 0 ? ${someData>/infos} : ${someData>/result}}"
Whereas someData is an ODataModel (v2).
But got an error:
Uncaught TypeError: Cannot read property 'indexOf' of undefined
Problem
The problem is that you're trying to determine .length from an object. In ODataListBinding (someData>/infos), aggregations are resolved in an object rather than an array. Therefore the syntax can't work. Furthermore, the .length syntax implies that the whole collection is already available on the client-side, contradicting the purpose of sap.ui.table.Table.
Expression binding with .length makes only sense with a client-side JSONModel as mentioned here.
Alternative approach
There are multiple ways to define aggregation binding dynamically, but the most straight-forward solution would be just to access the table control reference and call bindRows dynamically. Something like this:
onInit: function() {
this.loadCountOf("SomeSet", this.bindTableRows.bind(this));
// ...
},
loadCountOf: function(entitySetName, handleCountSuccess) {
const odataModel = /*...*/;
odataModel.read(`/${entitySetName}/$count`, {
success: count => handleCountSuccess.call(this, +count),
});
},
bindTableRows: function(count) {
this.byId("myTable").bindRows({
path: count > 0 ? "/SomeSet" : "/TheOtherSet",
// ...
});
},
API reference: sap.ui.table.Table#bindRows
the errors seem to tell you that either infos or result is undefined. You should check the current value of those arrays.
Anyway, it's not a really good idea to bind table rows like that IMHO.
What's you scenario?

how to set a trigger to add to a date in meteor using quickform

I have MemberProfiles and MemberPayments collections.The MemberProfile has expiryDate field which is set to current date at insert.I need to extend expirDate of a unique MemberProfile whenever a MemberPayment is added to that MemberProfile.
MemberProfiles = new Mongo.Collection('memberProfiles');
MemberProfileSchema = new SimpleSchema({
expiryDate: {
type: Date,
autoValue: function () {
return moment().toDate();
},
autoform: {
type: "hidden"
}
}
// to insert into Memb erProfiles
{{> quickForm collection="MemberProfiles" id="insertMemberProfileForm" type="insert" class="new-recipe-form"}}
//the code for MemberPayments collection
MemberPayments = new Mongo.Collection('memberPayments');
MemberPayments.before.insert(function (userId, doc) {
let memberProfile= MemberProfiles.direct.findOne({profile: doc.memberId});
MemberProfiles.update(doc.memberId, {
$set: {
expiryDate: moment().add(31, 'days');
,
}
}
)
});
I have added all the necessary packages but still this doesnt work.I am getting error Cannot set property 'expiryDate' of undefined
It is challenging to try and resolve issues like this without having a more complete example of the app or reference to the complete project in github or somewhere else.
However, when I read through your code I noticed an issue in your MemberProfiles.update() function. I also noticed that it appears you are only processing your form from the client side (e.g. because your quickform is not using a Meteor Method) so you will have to manually call the SimpleSchema .clean() method to generate your autovalue. Keep in mind that your client side approach might work ok now, but once you remove the insecure package you will either have to implement a Meteor Method to perform the insert or configure your collection allow/deny rules to allow client side insert (this is dangerous).
Since you are using moment.js you need to be careful that you always pull the date from the moment object before storing in mongodb. In this case, you are trying to set expiryDate to the value returned from moment().add(31, 'days') which is just another moment object.
Also, I would assume you want to add 31 days to the current value of expiryDate, however you are never initializing moment with the expiryDate. Therefore, you will always be setting the expiryDate to 31 days from the time the function executes.
Lastly, you have a syntax error (; inside your $set object) and your findOne selector includes {profile: doc.memberId} however your MemberProfiles schema says there is only a _id and expiryDate field in your collection.
Try this new logic that addresses the above issues and see if that resolves your issue.
MemberPayments.before.insert(function (userId, doc) {
let memberProfile = MemberProfiles.direct.findOne({profile: doc.memberId});
if (memberProfile) {
if (!memberProfile.expiryDate) {
console.log("expiryDate was not previously set!");
} else {
MemberProfiles.update({profile: doc.memberId}, {
$set: {
expiryDate: moment(memberProfile.expiryDate).add(31, 'days').toDate()
}
});
}
} else {
console.log("memberProfile not found");
}
});
Now that this is fixed, you need to resolve the issue of your autovalue not being generated on the client side. You do this by calling the SimpleSchema .clean() method. Since you are not using Meteor Methods to process your quickForm (and therefore doing everything client side), you need to add the below AutoForm hook to ensure that the SimpleSchema .clean() method is called before the doc is saved (which will then execute your autovalue logic).
AutoForm.hooks({
insertMemberProfileForm: {
before: {
insert: function(doc) {
MemberProfileSchema.simpleSchema().clean(doc);
return doc;
}
}
}
});
You should put the above code in the onRendered() callback of the template that creates your quickform (e.g. the template that contains the below code in the HTML).
{{> quickForm collection="MemberProfiles" id="insertMemberProfileForm" type="insert" class="new-recipe-form"}}

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...);
},
});

Subscribing to Meteor.Users Collection

// in server.js
Meteor.publish("directory", function () {
return Meteor.users.find({}, {fields: {emails: 1, profile: 1}});
});
// in client.js
Meteor.subscribe("directory");
I want to now get the directory listings queried from the client like directory.findOne() from the browser's console. //Testing purposes
Doing directory=Meteor.subscribe('directory')/directory=Meteor.Collection('directory') and performing directory.findOne() doesn't work but when I do directory=new Meteor.Collection('directory') it works and returns undefined and I bet it CREATES a mongo collection on the server which I don't like because USER collection already exists and it points to a new Collection rather than the USER collection.
NOTE: I don't wanna mess with how Meteor.users collection handles its function... I just want to retrieve some specific data from it using a different handle that will only return the specified fields and not to override its default function...
Ex:
Meteor.users.findOne() // will return the currentLoggedIn users data
directory.findOne() // will return different fields taken from Meteor.users collection.
If you want this setup to work, you need to do the following:
Meteor.publish('thisNameDoesNotMatter', function () {
var self = this;
var handle = Meteor.users.find({}, {
fields: {emails: 1, profile: 1}
}).observeChanges({
added: function (id, fields) {
self.added('thisNameMatters', id, fields);
},
changed: function (id, fields) {
self.changed('thisNameMatters', id, fields);
},
removed: function (id) {
self.removed('thisNameMatters', id);
}
});
self.ready();
self.onStop(function () {
handle.stop();
});
});
No on the client side you need to define a client-side-only collection:
directories = new Meteor.Collection('thisNameMatters');
and subscribe to the corresponding data set:
Meteor.subscribe('thisNameDoesNotMatter');
This should work now. Let me know if you think this explanation is not clear enough.
EDIT
Here, the self.added/changed/removed methods act more or less as an event dispatcher. Briefly speaking they give instructions to every client who called
Meteor.subscribe('thisNameDoesNotMatter');
about the updates that should be applied on the client's collection named thisNameMatters assuming that this collection exists. The name - passed as the first parameter - can be chosen almost arbitrarily, but if there's no corresponding collection on the client side all the updates will be ignored. Note that this collection can be client-side-only, so it does not necessarily have to correspond to a "real" collection in your database.
Returning a cursor from your publish method it's only a shortcut for the above code, with the only difference that the name of an actual collection is used instead of our theNameMatters. This mechanism actually allows you to create as many "mirrors" of your datasets as you wish. In some situations this might be quite useful. The only problem is that these "collections" will be read-only (which totally make sense BTW) because if they're not defined on the server the corresponding `insert/update/remove' methods do not exist.
The collection is called Meteor.users and there is no need to declare a new one on neither the server nor the client.
Your publish/subscribe code is correct:
// in server.js
Meteor.publish("directory", function () {
return Meteor.users.find({}, {fields: {emails: 1, profile: 1}});
});
// in client.js
Meteor.subscribe("directory");
To access documents in the users collection that have been published by the server you need to do something like this:
var usersArray = Meteor.users.find().fetch();
or
var oneUser = Meteor.users.findOne();

How to update a doc value from an iron-router function

I know this is a nood question, but I'm trying to work out how to update a value in a document from a route in iron router. I've found the spot I need to put the function, but I'm struggling with the mongo code needed to make it work.
I'm trying to increment a views element each time a link is clicked, so have added the following code to the route.
data: function () {
var project = projectDocs.findOne(this.params._id);
// need to increment views value by one
console.log(project.views);
projectDocs.update({id: project.id},
{$inc: {views: 1}}
);
console.log(project.views);
return project;
}
});
The project.views value is returning the correct value, but the code to update the value throws an exception at the moment.
I tried the simple thing of project.views++ which increments the variable within the function but it never gets pushed to the database (no surprises there I guess).
Can someone point me in the direction I need to get this value to inc (and is this even the right place to do this?).
Thanks.
Peter.
OK, I found this link that has lead me part of the way http://books.google.com.au/books?id=uGUKiNkKRJ0C&pg=PA37&lpg=PA37&dq=Cannot+apply+$inc+modifier+to+non-number&source=bl&ots=h7qyOddRsf&sig=EWFw9kNLGHoFEUS-nTNsBStDRcQ&hl=en&sa=X&ei=cRGXUse0DNGciAfk6YHgCA&ved=0CFcQ6AEwBQ#v=onepage&q=Cannot%20apply%20%24inc%20modifier%20to%20non-number&f=false which explains that you can only inc numeric values (I had this as a string it seems.
Now the problem is that I seem to be in an endless loop.
The function now looks like
this.route('projectPage', {
path: '/projects/:_id',
waitOn: function() {
return Meteor.subscribe('singleProject', this.params._id);
},
data: function () {
var project = projectDocs.findOne(this.params._id);
// need to increment views value by one
console.log("Views", project.views);
console.log("Project", project);
projectDocs.update(project._id,
{$inc: {views: 1}}
);
console.log(project.views);
return project;
}
});
Why would this be looping?
Use _id instead of id. So
projectDocs.update({_id: project._id},
{$inc: {views: 1}}
);
If that's not it, perhaps you could update your answer with whatever exception you are getting.
Just read the fantastic new documentation on iron-router a bit further and moved the $inc function to the unload hook and all seems to be good.
this.route('projectPage', {
path: '/projects/:_id',
waitOn: function() {
return Meteor.subscribe('singleProject', this.params._id);
},
data: function () {
return projectDocs.findOne(this.params._id);
},
unload: function() {
var project = projectDocs.findOne(this.params._id);
// need to increment views value by one
projectDocs.update(project._id,
{$inc: {views: 1}}
);
}
// could possibly use layout: popup_layout? here
});
Would love some confirmation that this is actually where I should be doing this (and it does seem a bit inefficient to be doing so many "findOne"'s) but its working for the moment.