Meteor Sub/pub with reactive vars break the DDP push? - meteor

I try to use reactive vars to filter the published data, on a data list page, users can pickup 2 dates to show the data created during the dates; all works great until i got few computers to access this page at same time; the data changes would not push to every computers automatically- only the computer which makes the change has the new data listed. other computers have to refresh the page manually to see the new data or updated data.
if i remove the reactive vars in the sub/pub, all are good - if one computer changes the data, all computers get the new data immediately and automatically.
i even put the date filter to the helper - still same - no DDP push, same as in the sub/pub.
any ideas? any input are very appreciated.v
sub
Template.TestRV.onCreated(function () {
this.startDate = new ReactiveVar({});
this.endDate =new ReactiveVar({});
var sDate = new Date(new Date().setDate(new Date().getDate() - 30));
var eDate = new Date();
//show last 30 days data by default
this.startDate.set(sDate);
this.endDate.set(eDate);
this.autorun(() => {
this.subscribe('shipAllRV',this.startDate.get(),this.endDate.get());
});
//this.autorun(() => {
//this.subscribe('shipAll');
//});
});
Template.TestRV.helpers({
testListRV: function () {
let start = Template.instance().startDate.get();
let end = Template.instance().endDate.get();
return SHIP.find(
{createdAt: { $lte: end, $gte: start }},
{ sort: { createdAt: -1 } }
);
},
testList: function(){
return SHIP.find({},{ sort: { createdAt:-1} });
}
});
pub -SHIP is my collection
Meteor.publish('shipAll', function() {
return SHIP.find({});
});
Meteor.publish('shipAllRV', function(startDate,endDate) {
return SHIP.find({createdAt:{$lte:endDate,$gte:startDate}},{ sort: { createdAt: -1 } });
});
BTW,
1. i tried session variable is same;
2. if i don’t update the createdAt field in my SHIP.update method, it seems all good even the reactive vars in sub/pub
how can i do it correctly? i need the date filter and DDP push. thanks
Robin

Related

Meteor subscription is not stopping

I've got what should be a relatively simple issue. I set a session, then a subscribe to a collection using the string stored in the session. But when that session changes, I need to clear the subscription data and start again.
My code is as follows:
let subscriptionReady;
let filteredResults = [];
let rawResults = [];
let county = Session.get('county');
let type = Session.get('type');
This is mostly just prep work to create some empty objects to populate later. This all gets set on a click event. After we set these placeholder objects we go and subscribe by those sessions:
if (county && !type) {
return function() {
if (subscriptionReady) {
subscriptionReady.stop();
}
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
}
});
}
At the third line I run a check to see if subscriptionReady exists, then it will have the stop method available. So then I run it. But, it doesn't actually stop anything.
What am I missing?
After trial and error, I've got it solved. The issue was the placement of the stop call. I no longer have to check if subscriptionReady exists, instead I stop the subscription inside of the onReady method:
return function() {
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
subscriptionReady.stop();
}
});
It's .stop() not .stop docs
Also you can probably avoid your filtering loop by including score in your query. Are you looking for documents where the score key exists {score: {$exists: true}} or just where it is non zero {$score: {$ne: 0}}?
Also you shouldn't need to clear the subscription and start again. If you make your subscription parameter resourcesearch a reactive data source then the subscription will automatically update to give you the documents you need. Starting/stopping a subscription in response to a search would be an anti-pattern.

Non-reactive and reactive data in Meteor (same helper)

I've got one view displaying some pictures published by users with some data (let's image Instagram).
I already have these pictures as non-reactive data (otherwise you could see many updates) but these images have one button to like the picture. If I have this as non-reactive data I can't see when I click on "Like" the filled heart (I need to refresh).
This is my subscribe function:
this.subscribe('food', () => [{
limit: parseInt(this.getReactively('perPage')),
//skip: parseInt((this.getReactively('page') - 1) * this.perPage),
sort: this.getReactively('sort')
}, this.getReactively('filters'), this.getReactively('searchText'), this.getReactively('user.following')
]);
And this is my helper:
food() {
const food = Food.find({}, {reactive: true}, {
sort: this.sort
}).fetch().map(food => {
const owner = Meteor.users.findOne(food.owner, {fields: {username: 1, avatarS: 1, following: 1}});
food.avatarS = owner && owner.avatarS;
food.username = owner && owner.username;
if (food.likes.indexOf(Meteor.userId()) == -1) {
// user did not like this plate
food.liked = false;
} else {
// user liked this plate
food.liked = true;
}
return food;
});
}
Is possible to have a non-reactive model but with some reactive properties on it?
I'm using Angular 1.X with TS btw
Thanks in advance!
PS: is it normal that this works as non-reactive when I change reactive to true?
Modification to your code:
//console.log(food.likes);
this.subscribe('reactiveFoodData', {ownerId: food.owner, userId: Meteor.userId()}).subscribe(()=>{
console.log(this.user);
});
// THIS IS THE PUBLISH METHOD LOCATED IN THE SERVER SIDE:
Meteor.publish('reactiveFoodData', function(params: {ownerId:string, userId:string) {
const owner = Meteor.users.findOne(params.ownerId);
if (!owner) {
throw new Meteor.Error('404', 'Owner does not exist');
}
let result = {};
result.avatarS = owner.avatarS;
result.username = owner.username;
const food = Food.find({});
result.liked = !(food.likes.indexOf(params.userId) == -1);
return result;
});
You have few problems:
1. The reactive flag is true by default, you do not need to set it.
2. The function find is accepting only two arguments, not 3.
Should be:
const food = Food.find({}, {reactive: true, sort: this.sort})
If you need some, subset of data to be reactive only (from some collection). You could create a specific Method (which udpates only "likes").
https://guide.meteor.com/methods.html
UPDATE:
Here is how you write a method with return parameter (check two examples, with Future and without):
How to invoke a function in Meteor.methods and return the value
UPDATE2:
You have lost reactivity when you used fetch(). Because you moved from reactive cursor to just simple array over which you map values. Do not expect reactivity after fetch(). If you want fetch or do not want to use Cursors, you could wrap the find inside Tracker.autorun(()=>{}) or utilize publish/subscribe.
Note: But be careful, if you somehow manage to get "empty" cursor in find(), your Tracker.autorun will stop react reactively. Autorun works only if it has something to watch over.
The main point with method, is that if you want to have one time non-reactive action for something. You define the method on server:
Meteor.methods({
myMethod: ()=> {
return "hello";
}
});
And you can call it from client with:
Meteor.call('myMethod', (error, result) => {
console.log(result); // "hello"
});
Instead of working with pure collections. You could start using publish/subscribe. On server you publish 'likes' and on client you just listens to this new reactive view. E.g.,
Meteor.publish('likes', (options: {owner: string, likes: Array<any>}) => {
let result: any = {}
const owner = Meteor.users.findOne(options.owner, username: 1, avatarS: 1, following: 1}});
result.avatarS = options.owner && options.owner.avatarS;
result.username = options.owner && options.owner.username;
result.liked = !(options.likes.indexOf(Meteor.userId()) == -1)
return result;
});
On client side: Meteor.subscibe('likes', {food.owner, food.likes}).subscribe(()=>{});
This is just off the top of my head.
Have you tried looking at Tracker ? https://docs.meteor.com/api/tracker.html
But more specifically the method Tracker.nonreactive
https://docs.meteor.com/api/tracker.html#Tracker-nonreactive

How to unit test collection methods with velocity/jasmine

I'm completely new to javascript testing and I am trying to get a grasp on how to approach testing methods that touch the database
For example, I have this method that returns true if there are any documents in the db matching the query
Payments = new Mongo.Collection('payments');
_.extend(Payments, {
hasAnyPayments: function(userId) {
var payments = Payments.find({ userId: userId });
return payments.count() > 0;
}
});
So far I have only written the structure that I think is correct, but I am pretty lost
describe('Payments', function() {
describe('#hasAnyPayments', function() {
it('should return true when user has any payments', function() {
});
});
});
Are such tests even supposed to touch the database? Any advice is much appreciated
Unless you are manually inputting data into Mongo manually (or outside of Meteor) then you don't need to test the database.
What you should be testing, are the execution paths in your code.
So for the case above, hasAnyPayments is a unit that finds all user payments and returns true if there are more than 0. So your test would look something like this:
describe('Payments', function() {
describe('#hasAnyPayments', function() {
it('should return true when user has any payments', function() {
// SETUP
Payments.find = function() { return 1; } // stub to return a positive value
// EXECUTE
var actualValue = Payments.hasAnyPayments(); // you don't really care about the suer
// VERIFY
expect(actualValue).toBe(true);
});
});
});

Iron Router waitOn not working when setting data on a route in Meteor 1.0.2

I'm defining a route that will show an appointment for a patient. I would like the template to show both the patient information and the appointment information.
I have this published:
Meteor.publish('userAppointment', function(appointmentId){
check(appointmentId, String);
var userId = Appointments.findOne(appointmentId).patientId;
return [
Appointments.find({_id: appointmentId}),
Meteor.users.find({_id: userId}, {fields: {profile: true, emails: true}})
];
});
Unfortunately Iron Router doesn't seem to be successfully waiting on the data subscription to complete before it tries to set the data context.
Note where I put debugger:
Router.route('/admin/appointment/:id', {
name: 'AppointmentShow',
waitOn: function(){
return [
Meteor.subscribe("userAppointment", this.params.id)
]
},
data: function(){
var appointmentId = this.params.id;
debugger
var patientId = Appointments.findOne(appointmentId).patientId;
return {
appointment: Appointments.findOne(appointmentId),
patient: Meteor.users.findOne(patientId)
}
}
});
At the time when debugger stops the code, when I do Meteor.users.find().fetch() and Appointments.find().fetch() in the console only the currently logged-in user (me) is available and there are no appointments available.
I expect to see two users (me and the patient) and one appointment available because that's the data that should be available after the waitOn has finished subscribing.
Am I missing something here?
EDIT----- Still doesn't make sense to me ------
When I change my route to this:
Router.route('/admin/appointment/:id', {
name: 'AppointmentShow',
waitOn: function(){
return [
Meteor.subscribe("userAppointment", this.params.id)
]
},
data: function(){
var appointmentId = this.params.id;
return {
appointment: Appointments.findOne(appointmentId),
// patient: Meteor.users.findOne(Appointments.findOne(appointmentId).patientId)
}
}
});
Appointments.findOne(appointmentId) returns an object:
{
_id: "23efref34qr2",
reason: "coughing",
patientId: "785g45g4f"
}
When my data function only returns
appointment: Appointments.findOne(appointmentId)
it works. But if I have it also return
patient: Meteor.users.findOne(Appointments.findOne(appointmentId).patientId)
I get an error message (can't read property 'patientId' of undefined.) Huh? It was just defined on the line above!
To clarify, I think you should be allowing your data function to run (and rerun when collections are populated), but be careful to make sure your function doesn't throw an error when it runs before data is available. This is a general Meteor pattern.
data: function(){
var appointmentId = this.params.id,
appointment = Appointments.findOne(appointmentId);
return { appointment: appointment,
patient: Meteor.users.findOne(appointment ? appointment.patientId : null) }
}
Sorry about the formatting, I'm doing this from an awful phone...
The problem seems to be that it runs the data function() before running the waitOn, and is therefore timing dependent. I have seen the same problem, and also had to check if the data was actually there.

Adding new users to a collection and having them render on a list automatically?

So im doing the leaderboard example on the meteor site but instead of the predefined data I start off with, I want to create a new name and score that automatically appears on the screen when someone creates an account, so at this point I get the name and the score on the screen only after I create an account and hit the refresh button on the browser, what do I want to do so that I don't have to hit the refresh button and the user login name and score automatically appears on the screen?
do I want to use deps.flush() or meteor.render somehow?
server.js
// newUser Method
Meteor.methods({
newUser: function() {
var user = Meteor.user();
userVar = {
name: user.username,
score: 0
};
Players.insert(userVar);
}
});
client.js
Deps.autorun(function() {
Meteor.call('newUser');
});
Template.leaderboard.players = function () {
return Players.find({}, {sort: {score: -1, name: 1}});
};
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
Template.player.selected = function () {
return Session.equals("selected_player", this._id) ? "selected" : '';
};
Template.leaderboard.events({
'click input.inc': function () {
Players.update(Session.get("selected_player"), {$inc: {score: 5}});
}
});
Template.player.events({
'click': function () {
Session.set("selected_player", this._id);
}
});
If your starting point is a working version of the example then you should be seeing reactive changes to the web page each time the Players collection changes. Deps.flush or Meteor.render are unnecessary.
The Deps.autorun() function you have is only called once when the client starts. At that point you may not have a user and your method will fail when you try to get a username from the null variable, 'user'.
To trigger the autorun each login and when you have a user you need it to refer to a reactive data source. If you rewrite it like this you should see a new player showing up each time a user logs in:
//on client
Deps.autorun( function(){
if ( Meteor.userId() ){
Meteor.call('newUser');
}
});
I also wonder if your method on the server will have a problem as this.userId is how I usually get the user information inside a method. Here is an alternative to avoid the method and just insert the player on the client:
//on client
Deps.autorun( function(){
var user = Meteor.user();
if ( user ) { //insert will run on login or any change in the user
var userVar = {
name: user.username,
score: 0
};
Players.insert(userVar);
}
});
So I assume then that "player" records belong to a user in some way? So when you create a new user, you create their new default player record?
Maybe you just need your helpers to check that a player record for the user exists, and if not, create it.
Template.leaderboard.players = function () {
var players = Players.find({/* Get players for this user */ }, {sort: {score: -1, name: 1}});
if(!players) {
players = /* Insert default player record */
}
return players;
};

Resources