In my application I rate applications relatively to a question. So I have 3 collections : apps, questions & rates.
When I insert a rate in my db, I also check if all questions have been answered for an application. If so, I update an attribute ratingComplete of the current application :
Meteor.methods({
rateApplication: function(rateAttributes){
var user = Meteor.user();
var rateAlready = Rates.findOne({appId: rateAttributes.appId, qId: rateAttributes.qId});
// ... some checks & code ...
if(rateAlready !== undefined)
Rates.remove({_id: rateAlready._id});
rateId = Rates.insert(note);
if(Rates.find({appId:rateAttributes.appId}).count() === Questions.find().count()){
Applications.update({_id:rateAttributes.appId},{$set:{ratingComplete:true}});
}
return rateId;
}
});
It works well except when this part of the code occurs :
if(Rates.find({appId:rateAttributes.appId}).count() === Questions.find().count()){
Applications.update({_id:rateAttributes.appId},{$set:{ratingComplete:true}});
}
If this happens, the work is done but the client page refresh. Even if I put a e.preventDefault() in my Template event.
Anyone knows why and how I can avoid that?
Thanks a lot.
Related
I have a page that lets you edit user data. I'm using FlowRouter for the routing and it can be found on the route /employees/:id.
I need to update the detail form when data changes on the server and leave the route if it was deleted by other client.
I decided to use Tracker.autorun which informs me whenever the data changes. The previous user info is stored on the template so it's easy to tell if the record was deleted.
Template.UpdateEmployee.onCreated(function () {
const self = this;
self.subscribe('user', FlowRouter.getParam('id'));
self.autorun(function () {
const _id = FlowRouter.getParam('id');
const user = Meteor.users.findOne({_id});
if(!user && self.user)
FlowRouter.go('/employees');
self.user = user;
if(!user)
return;
user.email = user.emails[0].address;
$('.ui.form').form('set values',user);
});
});
And lastly in the onRendered callback I'm checking if the data was set on template as I believe not doing so could lead to data being available before the template is rendered and hence values wouldn't get set properly. Is this correct?
Template.UpdateEmployee.onRendered(function () {
if(this.user){
user.email = user.emails[0].address;
$('.ui.form').form('set values',user);
}
});
Are there any pitfalls to this solution?
I can see a couple drawbacks inherently. The first one is doing a find query on the client. Typically you would want to return data from the server using Meteor publish and subscribe.
The second is you are passing the key to find the data over the URL. This can be spoofed by other users for them to find that users data.
Lastly if you are doing a find on the user object, I assume you might be storing data there. This is generally bad practice. If you need to store user data with their profile, it's best to create a new collection and publish/subscribe what you need.
Let's say that two users do changes to the same document while offline, but in different sections of the document. If user 2 goes back online after user 1, will the changes made by user 1 be lost?
In my database, each row contains a JS object, and one property of this object is an array. This array is bound to a series of check-boxes on the interface. What I would like is that if two users do changes to those check-boxes, the latest change is kept for each check-box individually, based on the time the when the change was made, not the time when the syncing occurred. Is GroundDB the appropriate tool to achieve this? Is there any mean to add an event handler in which I can add some logic that would be triggered when syncing occurs, and that would take care of the merging ?
The short answer is "yes" none of the ground db versions have conflict resolution since the logic is custom depending on the behaviour of conflict resolution eg. if you want to automate or involve the user.
The old Ground DB simply relied on Meteor's conflict resolution (latest data to the server wins) I'm guessing you can see some issues with that depending on the order of when which client comes online.
Ground db II doesn't have method resume it's more or less just a way to cache data offline. It's observing on an observable source.
I guess you could create a middleware observer for GDB II - one that checks the local data before doing the update and update the client or/and call the server to update the server data. This way you would have a way to handle conflicts.
I think to remember writing some code that supported "deletedAt"/"updatedAt" for some types of conflict handling, but again a conflict handler should be custom for the most part. (opening the door for reusable conflict handlers might be useful)
Especially knowing when data is removed can be tricky if you don't "soft" delete via something like using a "deletedAt" entity.
The "rc" branch is currently grounddb-caching-2016 version "2.0.0-rc.4",
I was thinking about something like:
(mind it's not tested, written directly in SO)
// Create the grounded collection
foo = new Ground.Collection('test');
// Make it observe a source (it's aware of createdAt/updatedAt and
// removedAt entities)
foo.observeSource(bar.find());
bar.find() returns a cursor with a function observe our middleware should do the same. Let's create a createMiddleWare helper for it:
function createMiddleWare(source, middleware) {
const cursor = (typeof (source||{}).observe === 'function') ? source : source.find();
return {
observe: function(observerHandle) {
const sourceObserverHandle = cursor.observe({
added: doc => {
middleware.added.call(observerHandle, doc);
},
updated: (doc, oldDoc) => {
middleware.updated.call(observerHandle, doc, oldDoc);
},
removed: doc => {
middleware.removed.call(observerHandle, doc);
},
});
// Return stop handle
return sourceObserverHandle;
}
};
}
Usage:
foo = new Ground.Collection('test');
foo.observeSource(createMiddleware(bar.find(), {
added: function(doc) {
// just pass it through
this.added(doc);
},
updated: function(doc, oldDoc) {
const fooDoc = foo.findOne(doc._id);
// Example of a simple conflict handler:
if (fooDoc && doc.updatedAt < fooDoc.updatedAt) {
// Seems like the foo doc is newer? lets update the server...
// (we'll just use the regular bar, since thats the meteor
// collection and foo is the grounded data
bar.update(doc._id, fooDoc);
} else {
// pass through
this.updated(doc, oldDoc);
}
},
removed: function(doc) {
// again just pass through for now
this.removed(doc);
}
}));
Here is the problem :
I am currently programming a chatapp based on what i found on github (https://github.com/sasikanth513/chatDemo)
I am refactoring it with iron-router.
When I go to the page (clicking on the link) I get an existing chatroom (that's what I want)
When I refresh the page (F5) I get a new created chatroom ! (what i want is getting the existing chatroom ...)
Here is the code in ironrouter :
Router.route('/chatroom', {
name: 'chatroom',
data: function() {
var currentId = Session.get('currentId'); //id of the other person
var res=ChatRooms.findOne({chatIds:{$all:[currentId,Meteor.userId()]}});
console.log(res);
if(res){
Session.set("roomid",res._id);
}
else{
var newRoom= ChatRooms.insert({chatIds:[currentId, Meteor.userId()],messages:[]});
Session.set('roomid',newRoom);
}
}
});
You can find my github repo with the whole project : https://github.com/balibou/textr
Thanx a lot !
Your route data depends on Session variables which will be erased after a refresh. You have a few options but the easiest would be to put the room id directly into the route: '/chatroom/:_id'. Then you can use this.params._id to fetch the appropriate ChatRooms document. Note that you could still keep '/chatroom' for cases where the room doesn't exist, however you'd need to redirect to '/chatroom/:_id' after the insert.
In meteor, the Session object is empty when the client starts, and loading/refreshing the page via HTTP "restarts" the client. To deal with this issue, you could persist the user's correspondent id in a Meteor.user attribute, so that you could easily do:
Router.route('/chatroom', {
name: 'chatroom',
data: function() {
var currentId = Meteor.user().profile.correspondentId;
var res=ChatRooms.findOne({chatIds:{$all:[currentId,Meteor.userId()]}});
console.log(res);
if(res){
Session.set("roomid",res._id);
}
else{
var newRoom= ChatRooms.insert({chatIds:[currentId, Meteor.userId()],messages:[]});
Session.set('roomid',newRoom);
}
}
});
This would work, with the proper permissions, but I would recommend not allowing the direct update of that value on the client (I don't know if you want users to be able to override their correspondentId). So if you want to secure this process, replace all that code with a server method call, where your updates are safer.
Another (and more common case) solution was given by David Weldon, if you don't mind having ids in your URL (and therefore not a single url)
I'm wondering if given example could meet racing condition on a real server, does work on localhost. Given situation- user deletes a few person from a form ( lets say studyForm and studyPersons ) Ive added personToDelete to an array an later on- Edit Action im deleting them in a loop
// A punch of persons chosen to be deleted
self.markedForDeleted.forEach(function(trashed){
trashed.destroyRecord();
});
this.get('person').save().then(personEditSuccess, personEditFailure);
Should i edit it like this ?
var onDeleteSuccess = function(wasAnyToDelete){
if (wasAnyToDelete){
deletedPersons ++;
}
if(deletedPersons === lengthOfAllPersons) {
this.get('person').save().then(personEditSuccess, personEditFailure);
}
};
// A punch of persons chosen to be deleted
self.markedForDeleted.forEach(function(trashed){
trashed.destroyRecord().then(onDeleteSuccess, onDeleteFailure);
});
Working and currently using
Ember.RSVP.all(personsToDelete.invoke('destroyRecord')).then(deletePersonSuccess, deletePersonFailure);
I am seeing a repeatable issue where a user authenticates ("logs in") with a Meteor server, and then a client subscription that depends on userId is updated (and dependent UI templates reactively update) before Meteor.userId() registers the successful login.
For example, in this code snippet, the assert will throw:
var coll = new Meteor.Collection("test");
if (Meteor.isServer) {
Meteor.publish('mineOrPublic', function () {
// Publish public records and those owned by subscribing user
return coll.find({owner: { $in: [ this.userId, null ]}});
});
}
if (Meteor.isClient) {
var sub = Meteor.subscribe('mineOrPublic');
var cursor = coll.find({});
cursor.observe({
added: function (doc) {
if (doc.owner) {
// This should always be true?!
assert(doc.owner === Meteor.userId());
}
}
});
}
Analogous to the added function above, if I write a template helper that checks Meteor.userId(), it will see a value of null, even when it is invoked with a data context of a document with an owner.
There is apparently a race condition between Meteor collection Pub/Sub and the Account userId update mechanisms. It seems to me that Meteor.userId() should always be updated before any subscriptions update based on a change in this.userId in a server publish function, but for some reason the opposite usually seems to be true (that is, the assert in the code above will usually throw).
The reason I care is because I have packages that depend on obtaining a valid Meteor Authentication token (using Accounts._storedLoginToken()) on the client for use in securing HTTP requests for files stored on the Meteor server. And the authentication token isn't correct until Meteor.userId() is. So the flow of events usually goes something like this:
User logs in
Publish function on server reruns based on the change in this.userId.
Client begins receiving new documents corresponding to the change in userId.
UI Template reactively updates to add DOM elements driven by new documents
Some of the DOM elements are <img> tags with src= values that depend on the data context.
HTTP requests are triggered and ultimately fail with 403 (forbidden) errors because the required authentication cookie hasn't been set yet.
Meteor.userId() finally updates on the client, and code reactively runs to set the authentication cookie
Helpers in the template that depend on a session variable set in the cookie update code are rerun, but the DOM doesn't change, because the URLs in the <img> tags don't change.
Because the DOM doesn't change, the tags don't retry their failed attempts to load the images.
Everything settles down, and the user has to manually reload the page to get their images to appear.
I've come up with two possible approaches to work around this issue:
In the template helper that generates the URL for the <img> tag, always append a dummy query string such as: "?time=" + new Date().getTime(). This causes the DOM to change every time the helper is called and fixes the problem, but it screws-up browser caching and if not coordinated will cause some assets to unnecessarily load multiple times, etc.
In every template helper that touches document data add a test of:
if (this.owner && this.owner !== Meteor.userId()) {
// Perhaps Meteor.loggingIn() could be used above?
// Invalid state, output placeholder
} else {
// Valid state, output proper value for template
}
I really hope someone knows of a less kludgy way to work around this. Alternatively, if consensus arises that this is a bug and Meteor's behavior is incorrect in this respect. I will happily file an issue on Github. I mostly really enjoy working with Meteor, but this is the kind of gritty annoyance that grinds in the gears of "it just works".
Thanks for any and all insights.
After trying lots of things, this variation on the example code in the OP seems to consistently solve the race condition, and I find this an acceptable resolution, unlike my initial attempted workarounds.
I still feel that this kind of logic should be unnecessary and welcome other approaches or opinions on whether Meteor's behavior in the OP sample code is correct or erroneous. If consensus emerges in the comments that Meteor's behavior is wrong, I will create an issue on Github for this.
Thanks for any additional feedback or alternative solutions.
var coll = new Meteor.Collection("test");
if (Meteor.isServer) {
Meteor.publish('mineOrPublic', function (clientUserId) {
if (this.userId === clientUserId) {
// Publish public records and those owned by subscribing user
return coll.find({owner: { $in: [ this.userId, null ]}});
} else {
// Don't return user owned docs unless client sub matches
return coll.find({owner: null});
}
});
}
if (Meteor.isClient) {
Deps.autorun(function () {
// Resubscribe anytime userId changes
var sub = Meteor.subscribe('mineOrPublic', Meteor.userId());
});
var cursor = coll.find({});
cursor.observe({
added: function (doc) {
if (doc.owner) {
// This should always be true?!
assert(doc.owner === Meteor.userId());
}
}
});
}
This code works by giving the server publish function the information it needs to recognize when it is running ahead of the client's own login state, thereby breaking the race condition.
I think this is something that Meteor should do automatically: clients should not see documents based on changes to this.userId in a publish function until after the client Meteor.userId() has been updated.
Do others agree?
I tried with this code that works on server too. In association with FileCollection package.
if (Meteor.isServer) {
CurrentUserId = null;
Meteor.publish(null, function() {
CurrentUserId = this.userId;
});
}
....
OrgFiles.allow({
read: function (userId, file) {
if (CurrentUserId !== file.metadata.owner) {
return false;
} else {
return true;
}
}
...