Meteor filtering data when asserting fields from queries - meteor

Why does running this code
var userId = Meteor.userId();
var user = Users.findOne(userId, { fields: { earnings: 1 } });
Return
{ _id: 'Co5bMySeaqySgDP6h', earnings: { period: 0.6, total: 52.5 } }
Instead of returning all the fields on the user, including the earnings (custom field)
Also, is there a way to make user queries automatically return custom specified fields, so I dont have to manually specify it each time I need it?
Much appreciated

The reason that you only get the specified field (plus the id) is given in the docs:
To include only specific fields in the result documents, use 1 as the value. The _id field is still included in the result.
If instead you just call Meteor.users.findOne(userId) it will return all of the available fields. If this is called on the server, that will be the entire document, but if you use it on the client, it will only return the fields that have been published from the server, which by default is just the username and the emails and profile fields. Again, per the docs:
On the client, this will be the subset of the fields in the document that are published from the server (other fields won't be available on the client). By default the server publishes username, emails, and profile (writable by user). See Meteor.users for more on the fields used in user documents.
This means that if you have added a new field to you user docs, you need to explicitly publish it for it to be available on the client (assuming autopublish has been removed). Note that it's fine to do this using the previously discussed fields specifier as the other required details (username, profile) will not be overwritten by another publish function unless you try to publish the same top-level field again.
Meteor.publish('earnings', function() {
return Meteor.users.find(this.userId, { fields: { earnings: 1 } });
};
(Publish functions expect you to return a cursor rather than an array, so you need to use find rather than findOne even if there will only be one result).
Finally, it's easy to add your own methods to a collection to make finding stuff you want more concise.
Meteor.users.findSimple = function(selector, options) {
options = options || {};
options.fields = options.fields || {};
options.fields.earnings = 1;
\\ same thing for any other fields you want to limit this find to;
return this.find(selector, options);
};

Related

userId gets into the document magically

This Meteor code is working fine, but I would like to ask if it is the way Meteor does things or it is a un predictable side effect that may change under some condition later.
The things is that when I do
DisplayCol.insert({action: 'task1', element: 'p', value: value_variable});
Meteor also inserts the correct userId (using 2 different browsers logged in as 2 different users) which I did not explicitly included in the document.
The above line of code is inside a server side function which is called from Meteor method.
here is the relevant information;
//lib/collection.js
DisplayCol = new Mongo.Collection('displayCol');
//server.js
Meteor.publish('displayCol', function () {
return DisplayCol.find({userId: this.userId});
});
DisplayCol.before.insert(function (userId, doc) {
doc.userId = userId;
});
In the docs of Collection hooks > Additional notes > second bulleted paragraph says:
userId is available to find and findOne queries that were invoked within a publish function.
But this is a collection.insert. So should I explicitly include the userId in the document or let the collection hook do its hidden magic? Thanks
No, there is no hidden magic in that code, your before hook is inserting the userId field in the document.
When you do an insert like this,
DisplayCol.insert({action: 'task1', element: 'p', value: value_variable});
the doc that your are inserting is { action: 'task1', element: 'p', value: value_variable }
Because, you have this hook,
DisplayCol.before.insert(function (userId, doc) {
doc.userId = userId;
});
it changes the doc before inserting into collection. So the above hook will change your doc to {action: 'task1', element: 'p', value: value_variable, userId: 'actual-user-id' }
This is the expected behaviour.
Regarding your other point in the question,
userId is available to find and findOne queries that were invoked
within a publish function.
Previously userId parameter in the find and findOne returns null, so user needs to pass userId as a parameter as mentioned in this comment. Additional notes mentions that the hack is not required any more. It has nothing to do with inserting userId field into the collection document.
To have a quick test, remove the DisplayCol.before.insert hook above, you will not see userId field in the newly inserted documents.
UPDATE
Just to clarify your doubt further, from the 4th point in the docs that you provided
It is quite normal for userId to sometimes be unavailable to hook
callbacks in some circumstances. For example, if an update is fired
from the server with no user context, the server certainly won't be
able to provide any particular userId.
which means that if the document is inserted or updated on the server, there will be no user associated with the server, in that case, userId will return null.
Also you can check the source code yourself here. Check the CollectionHooks.getUserId method, it uses Meteor.userId() to get the userId.
CollectionHooks.getUserId = function getUserId() {
var userId;
if (Meteor.isClient) {
Tracker.nonreactive(function () {
userId = Meteor.userId && Meteor.userId(); // <------- It uses Meteor.userId() to get the current user's id
});
}
if (Meteor.isServer) {
try {
// Will throw an error unless within method call.
// Attempt to recover gracefully by catching:
userId = Meteor.userId && Meteor.userId(); // <------- It uses Meteor.userId() to get the current user's id
} catch (e) {}
if (!userId) {
// Get the userId if we are in a publish function.
userId = publishUserId.get();
}
}
return userId;
};

Meteor observeChanges vs manual update

I have a simple todo schema: (just a sample to draw my question)
{
title: {
type: string
},
value: {
type: string
},
author: {
type: object
},
"author._id": {
type: string
},
"author.firstName": {
type: string
},
"author.lastName": {
type: string
},
}
The author entries are from meteor.user. If the meteor user changes the firstName or lastName i have to update the todo. I have two possibilities:
observerChanges (server side) to users collection and update all todos from this user with the new firstname/lastname
if i call the user update method i can call a method to update all todos
when it's better to use cursor.observeChanges and when it's better to call a update method manual? And why?
As the comment says, you should not store the author name / email in the document if it is mutable:
Store the ID of the user only in the document, the UserID is immutable.
When building your ToDo template, look up the User information by ID: you would need to publish a Publication for user by Id, and subscribe to it on the client with the userId as parameter.
Meteor.publish('userById', function(userId) {
return Meteor.users.find({_id: userId}, {limit:1});
});
in your route / template.onCreated depending on your Router, assuming the document is called doc
this.subscribe('userById', this.doc.author._id);
in the template helper
Template.todoTemplate.helpers({
'Author': function() {
return Meteor.users.findOne({_id: this.doc.author._id});
}
});
and call the Author info in the template
<Template name="todoTemplate">
First Name: {{Author.first_name}}
Last Name: {{Author.last_name}}
</Template>
I think you shouldn't rely on the second method, because sometimes you (or your teammate) might forget to update it. Moreover, if you're denormalizing user data in other collections, users knowing Meteor might just call your Meteor.method or manipulate db from the browser console...
You can use this package:
meteor add matb33:collection-hooks
It adds some hooks to your mongo insert/update/remove call
For example:
Meteor.users.after.update(function (userId, doc, fieldNames, modifier, options) {
if (this.previous.firstName === doc.firstName && this.previous.lastName === doc.lastName) {
return;
}
Todos.update({'author._id': doc._id}, {
$set: {
'author.firstName': doc.firstName,
'author.lastName': doc.lastName,
}
})
}, {fetchPrevious: true})
(To update the Todos collection efficiently, make sure to add index to author field)
This is just a handier way than writing your own observeChanges, and better than manually updating Todos collection every time you update the users collection, because you might forgot to call it in some case, or some hacker user just calls Meteor.users.update(Meteor.userId(), {...}) perhaps...
But still, I think you should always add some auto-correct mechanism to avoid wrong data being displayed, because no matter which method you choose, some error will occur (maybe the server watching the db just crashes right after users update). You can check on the client side when displaying content, if author.firstName doesn't match Meteor.users.findOne(author._id) (but you have to publish the user though...), than call a method to tell the server to update it.

Meteor: what is the right way to add custom settings object to users collection?

There are multiple examples on publish/subscribe but not clear on what is the best practice for storing custom data in the in-built "users" collection in Meteor (especially in the new possibility of template specific collections).
For example, I need to store user browse history - something that is accessible through Meteor.user().settings.history.lastvisited[]
The challenge is:
Is any special publish / subscribe required for the above? (the
reason being, I am assuming the users collection is already
published and available on client side - so do we need another?)
How to take care of edge cases where user is new and hence settings.history object may not be defined? Can we have a special publish that automatically takes care of creating an empty object if the settings is undefined? How to do it?
I did this :
// server side
Meteor.publish('userSettings', function (maxRows) {
if (this.userId) {
return Meteor.users.find({ _id: this.userId }, { fields: {'settings':1}});
}
this.ready();
});
//client side
Meteor.subscribe('userSettings');
But I do not see anyway how I can access the published "userSettings" object on the client side - what is missing ??
You can create a field and set it to false/'', on each user you create using the accountsOnCreateUser method.
Accounts.onCreateUser(function(options, user) {
//this function gets called each time a user has been created on the Meteor.user collection
if (options.profile)
user.settings = ''; //this is just and example.
return user;
})
Now the publish looks ok, but in order to get it work im always use a Tracker.autorun function.
Tracker.autorun(function(){
Meteor.subscribe('userSettings');
})
Why the autorun? well if you don't call the auto run here, the subscription get only called 1 time when the apps loads, and not when the user documents.
Take care of yours deny/allow permissions, check this meteor:common mistakes post on the Profile editing section
Also the subscribe function have a callback function. Meteor.subscribe(name, [arg1, arg2...], [callbacks]), so you can do something like this.
var myUserSubscription = Meteor.subscribe('userSettings',function(){
console.log("ok im here on the client side")
console.log("this user subscription is ready " + myUserSubscription.ready())
})
console.log("outside the subscription why not? " + myUserSubscription.ready();
About ready();
True if the server has marked the subscription as ready. A reactive
data source.

How to honour user privacy settings in Meteor

I have a set of users defined like this:
Accounts.createUser({
username:'Simon',
email:'simon#email.com',
profile:{
firstname:'Simon',
lastname:'Surname',
location:'Home Address',
privacy: {
location:0,
emails:0 } //Location and emails are private and should not be disclosed
}
});
My question is how can I publish this user's record for other users to view, taking into account the profile privacy settings. In this example, I have set the privacy for location and emails to zero with the intention that this information is not published for this user.
I would like to publish it using the standard method:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find();
});
But I cannot see a way to specify the selector or fields in such a way that only public information will be published.
I have tried adding additional publications of the form:
Meteor.publish("allUsers", function () {
return Meteor.users.find( {}, {fields:{username:1}} );
});
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, emails:1}} );
});
but the selector does not seem to be returning the emails as I expected. I am looking for optimal way to do this from a performance point of view.
Mongodb is not a relational database so whenever I want to join or query based on metadata I remember I have to do things differently. In your case I would make a separate Collection for user privacy if I wanted to query on user privacy. In addition, if I cared about performance I probably would never want "all of x", I would just want enough to show the user, thus paginate. With these two ideas in mind you can easily get what you want: query based on privacy settings and performance.
Privacy = new Mongo.Collection("privacy");
Whenever we want to add privacy to an account:
Privacy.insert({
emails: 1,
userId: account._id,
});
Then later, one page at a time, showing ten results each page, tracking with currentPage:
Meteor.publish("usersWithPublicEmails function (currentPage) {
var results = []
var privacyResults = Privacy.find({"emails":1}, {skip: currentPage,
limit: 10});
var result;
while (privacyResults.hasNext() ) {
result = privacyResult.next();
results.append(Meteor.users.find({_id: result.userId});
}
return result;
});
I didn't test this code, it may have errors, but it should give you the general idea. The drawback here is that you have to keep privacy and users in sync, but these are the kinds of problems you run into when you're not using a relational database.
Mongodb has a way to do this kind of reference lookup with less code, but it still happens on demand and I prefer the flexibility of doing it myself. If you're interested take a look at Database references
That's because you have a typo in your publish function's fields object, instead of email you've typed emails
So the correct function would be:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, email:1}} );
});
Furthermore, you're already publishing all usernames in your allUsers publication, therefore, in order to add the missing data for relevant public users, you'll just need this:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{email:1}} );
});
and Meteor will automatically merge those records for you.
A simple solution in the end. I had missed the additional subscription in my router:
Router.route('/users', {
name: 'userList',
waitOn: function(){
return Meteor.subscribe('allUsers') &&
Meteor.subscribe('usersWithPublicEmails');
},
data: function(){
return Meteor.users.find();
}
});
A basic mistake:-(

How to open a collection item only on one client?

I have a meteor collection like this:
Cases = new Meteor.Collection('cases');
As well i have registered users (max 10). I now want to be able to "give" a single case to a registered user and be sure, that no other user is getting that specific case.
The User is working with the case (updating fields, deleting fields) and then sends it in some kind of archive after submitting the user should get a new case that is in the collection.
My thought was to have field called "locked" which initially is set to false and in the moment it is displayed at the user "locked" gets true and is not returned anymore:
return Cases.find({locked: false, done: false}, {limit: 1});
Any ideas how to do that in meteor?
Thanks
You just need to attach an owner field (or similar) to the case. That would allow you to do things like:
Only publish the case to the user who is also the owner using something like:
Meteor.publish('cases/unassigned', function() {
return Cases.find({owner: {$exists: false}});
});
Meteor.publish('cases/mine', function() {
return Cases.find({owner: this.userId});
});
Not allow a user to update or delete a case if it's not assigned to them:
Cases.allow({
update: function(userId, fieldNames, doc, modifier) {
return userId === doc.owner;
},
delete: function(userId, doc) {
return userId === doc.owner;
}
});
Obviously, these would need amending for stuff like super-users and you probably need some methods defined to allow users to take cases, but that's the general idea.
There are concurrency issues to deal with, to reliably allocate a case to only one person.
We need to solve two things:
1. Reliably assign the case to a user
2. Fetch the cases assigned to a user
Number 2. is easy, but depends on 1.
To solve 1., this should work:
var updated = Cases.update(
{_id: <case-to-assign>, version: "ab92c91"},
{assignedTo: Meteor.userId(), version: Meteor.Collection.ObjectID()._str});
if (updated) {
// Successfully assigned
} else {
// Failed to assign, probably because the record was changed first
}
Using this you can query for all of a users cases:
var cases = Cases.find({assignedTo: Meteor.userId()});
If 10 people try get a case at the same time, it should have a pre-set version field, and the MongoDB will only let the .update work once. As soon as the version field changes (due to an .update succeeding) the remaining updates will fail as the version field could no longer match.
Now that the allocation has taken place reliably, fetching is very simple.
As suggested by #Kyll, the filtering of cases should be done inside a Meteor publication.
It would also make sense to perform the case-assignment inside a Meteor method.
UPDATE:
#richsilv's solution is simpler than this one, and works fine.
This solution is useful if you need to know who won immediately, without making further requests to the server.

Resources