In my meteor app with Flow router, I am using the meteor subscription for fetching user data, but data in the helpers are not updating according to the user collection updations.
In my publications code,
Meteor.publish('fetchUserData', function(usrnm) {
return Meteor.users.find({'username':usrnm}, {
fields: {
profile: true,
services: true,
emails: true,
votedPatents: true,
userType: true,
followingUsers: true,
followedByUsers: true
}
});
});
In my router.js file,
Subs = new SubsManager();
FlowRouter.route('/:username', {
subscriptions: function(params) {
this.register('fetchUserData', Meteor.subscribe('fetchUserData', params.username));
},
action: function(params) {
BlazeLayout.render('main', {center: 'profile'});
})
}
});
In my client code
Template.profile.onCreated(function() {
this.autorun(function() {
var handle = Subs.subscribe('fetchUserData', username);
})
})
Template.profile.helpers({
connections: function(){
var u = Meteor.users.findOne({'username':username});
return u;
}
})
Also should note that the helper function returns undefined for users other than Meteor.user() and I had also tried How to access FlowRouter subscriptions in Meteor template helpers? , but the result is same
Your publication returns 1 user: the user with the username passed as a param, so on the client, when you subscribe to this publication, you get one user.
When you do
Meteor.users.find({....});
you're searching through a collection that only has the one user you published.
To fix this, you need:
to publish all users to the client (not ideal for security reasons, unless you take good care of filtering out users that the signed in user is allowed to see)
or
subscribe to the publication with the user to look up each time you need to do a lookup (run the subscribe, then findOne() -> inefficient)
or
use a Method that will return the user you need without exposing everyone.
Related
In Meteor, one can add additional fields to the root-level of the new user document like so:
// See: https://guide.meteor.com/accounts.html#adding-fields-on-registration
Accounts.onCreateUser((options, user) =>
// Add custom field to user document...
user.customField = "custom data";
return user;
});
On the client, one can retrieve some data about the current user like so:
// { _id: "...", emails: [...] }
Meteor.user()
By default, the customField does not exist on the returned user. How can one retrieve that additional field via the Meteor.user() call such that we get { _id: "...", emails: [...], customField: "..." }? At present, the documentation on publishing custom data appears to suggest publishing an additional collection. This is undesired for reasons of overhead in code and traffic. Can one override the default fields for Meteor.user() calls to provide additional fields?
You have a couple of solutions that you can use to solve this.
Null Publication
Meteor.publish(null, function () {
if (this.userId !== null) {
return Meteor.users.find({ _id: this.userId }, { fields: { customField: 1 } });
} else {
return this.ready();
}
}, { is_auto: true });
This will give you the desired result but will also result in an additional database lookup.. While this is don't by _id and is extremely efficient, I still find this to be an unnecessary overhead.
2.Updating the fields the Meteor publishes for the user by default.
Accounts._defaultPublishFields.projection = { customField: 1, ...Accounts._defaultPublishFields.projection };
This has to be ran outside of any Meteor.startup blocks. If ran within one, this will not work. This method will not result in extra calls to your database and is my preferred method of accomplishing this.
You are actually misunderstanding the documentation. It is not suggesting to populate and publish a separate collection, just a separate publication. That's different. You can have multiple publications/subscriptions that all feed the same collection. So all you need to do is:
Server:
Meteor.publish('my-custom-user-data', function() {
return Meteor.users.find(this.userId, {fields: {customField: 1}});
});
Client:
Meteor.subscribe('my-custom-user-data');
I have the following code that renders the currentUsers' documents in a collection. However I want an admin belonging to the same organization to also be able to view and edit the collection. this.user.profile.organization does not work unfortunately. So, how would I allow the object to be available to admins from belonging to the same organization. EVery document that gets created gets the organization of the currentuser.
Meteor.publish('skills', function skillsPublication() {
return Skills.find({
owner: this.userId,
});
});
When you're on the server, you can always query the user document from the MongoDB database.
Meteor.publish('skills', function skillsPublication() {
const user = Meteor.users.findOne({ _id: this.userId })
// now you can do user.profile.organization
return Skills.find({
$or: [
{ owner: this.userId },
{ organization: user.profile.organization }
] // returns a document owned by the user or owned by the user's organization
})
})
Just a note, Meteor advises against using .profile field on your users collection, because that field is always published to the client. Meteor suggests that you use top-level keys on your user document instead.
For more info, read: https://guide.meteor.com/accounts.html#dont-use-profile
I'm using accounts-ui and accounts-google in Meteor v1.4.1. I can't get the user.services object to appear scoped in the client code. In particular, I need google's profile picture.
I've configured the server-side code to authenticate with Google like so:
import { Meteor } from 'meteor/meteor';
import { ServiceConfiguration } from 'meteor/service-configuration';
const services = Meteor.settings.private.oauth;
for (let service of Object.keys(services)) {
ServiceConfiguration.configurations.upsert({
service
}, {
$set: {
clientId: services[service].app_id,
secret: services[service].secret,
loginStyle: "popup"
}
});
}
...and the client side code to configure permissions like so:
Accounts.ui.config({
requestPermissions: {
google: ['email', 'profile']
},
forceApprovalPrompt: {
google: true
},
passwordSignupFields: 'EMAIL_ONLY'
});
When users click the 'Sign-In with Google' button, a pop-up appears and they can authenticate. No prompt appears, however, despite forceApprovalPrompt being set to true for google.
The big issue is that when I execute this,
const user = Meteor.user();
console.log(user.services);
anywhere in client code, I do not see the expected user services information. I check my database and it is definitely there for the taking:
$ mongo localhost:27017
> db.users.find({})
> ... "services" : { "google" : { "accessToken" : ... } } ...
I'm curious what I'm missing? Should I explicitly define a publish function in order for user services data to exist in the client?
The services property is intentionally hidden on the client side for security reasons. There are a couple of approaches here :
Suggestions
My preferred one would be to expose a meteor method to bring you the
public keys and avatars you might need in the few places you'd need
them.
On a successful login, you could record the data you need somewhere in the user object, but outside of the services property.
As you said, you could make a new publication which explicitly specifies which fields to retrieve and which ones to hide. You have to be careful what you publish, though.
Code Examples
Meteor methods:
// server
Meteor.methods({
getProfilePicture() {
const services = Meteor.user().services;
// replace with actual profile picture property
return services.google && services.google.profilePicture;
}
});
// client
Meteor.call('getProfilePicture', (err, profilePicture) => {
console.log('profile picture url', profilePicture);
});
Update on successful user creation (you might want to have a login hook as well to reflect any avatar/picture changes in google):
// Configure what happens with profile data on user creation
Accounts.onCreateUser((options, user) => {
if (!('profile' in options)) { options.profile = {}; }
if (!('providers' in options.profile)) { options.profile.providers = {}; }
// Define additional specific profile options here
if (user.services.google) {
options.profile.providers.google = {
picture: user.services.google.picture
}
}
user.profile = options.profile;
return user;
});
Publish only select data...
// Server
Meteor.publish('userData', function () {
if (this.userId) {
return Meteor.users.find({ _id: this.userId }, {
fields: { other: 1, things: 1 }
});
} else {
this.ready();
}
});
// Client
Meteor.subscribe('userData');
I'm using Iron Router. I have a RouterController that looks something like this:
var loggedInUserController = RouteController.extend({
layoutTemplate: "GenericLayout",
waitOn: function () {
return Meteor.subscribe("TheDataINeed");
}
});
And I have a route defined which uses this controller to wait for the 'TheDataINeed':
Router.route("/myapp", {
name: "Landing",
controller: loggedInUserController,
data: function () {
if(this.ready()){
return {content: "page-landing"};
}
}
});
Now, the problem is the data I am subscribed to is conditional: meaning, depending on the user's role, I publish different data, like so:
if (!Roles.userIsInRole(this.userId, 'subscribed') ) {
return [
myData.getElements({}, { fields: { _id: 1, title: 1}, limit: 5 })
];
} else {
return [
myData.getElements({}, { fields: { _id: 1, title: 1} })
];
}
When the user's role is not 'subscribed', I limit the published data to 5 elements.
The problem is publishing is not reactive, so when the user changes his role for the first time to 'subscribed' and I navigate to my route ("/myapp"), the user still sees the limited number of elements instead of all of them.
Is there a way to manually re-trigger the subscription when I am loading this route? If possible, I'd like to do this without adding new packages to my app.
Not sure about that approach but can you try to set session value in route instead of subscription code. Then in a file on client side where your subscriptions are you can wrap Meteor.subscribe("TheDataINeed") in Tracker.autorun and have a session as a subscription parameter. Every time that session value is changed autorun will rerun subscription and it will return you data based on a new value.
I have an app where each User has a Counter object associated with it. The User is subscribed to the corresponding Counter.
Each time a user submits the form, I want to save the current value of the User's Counter to the form. However, the client and server disagree on what Counter object to use.
Counters = new Mongo.Collection("counters");
Router.route('/register', { name: 'insertRegistration',
waitOn: function() {
return [ Meteor.subscribe('counters') ];
},
});
RegistrationSchema = new SimpleSchema({
somevalue: {
type: String,
autoValue: function() {
console.log(Counters.findOne()); // Different results on client and server
return "whatever";
}
}
});
if (Meteor.isServer) {
Meteor.publish('counters', function() {
return Counters.find({ user_id: this.userId });
});
}
Counters.findOne() on the client picks the Counter object associated with the current user.
Counters.findOne() on the server picks a Counter object out of the collection of all Counter objects.
I think this is because the Client asks for Counters and gets what it's subscribed to, while Server asks for Counters and gets the Collection.
Is there a way to consider the publish/subscribe rules on the server?
On the server, do
Counter.findOne({ user_id: this.userId })
to get the correct counter for the user.