7

I'm trying to figure out how to conditionally send data to the client in meteor. I have two user types, and depending on the type of user, their interfaces on the client (and thus the data they require is different).

Lets say users are of type counselor or student. Every user document has something like role: 'counselor' or role: 'student'.

Students have student specific information like sessionsRemaining and counselor, and counselors have things like pricePerSession, etc.

How would I make sure that Meteor.user() on the client side has the information I need, and none extra? If I'm logged in as a student, Meteor.user() should include sessionsRemaining and counselor, but not if I'm logged in as a counselor. I think what I may be searching for is conditional publications and subscriptions in meteor terms.

Diogenes
  • 2,697
  • 3
  • 26
  • 27
  • I've gotten a few answers now and I'm not sure I know how to pick the best because they all seem to work on the surface. I guess I'd like the simplest, most 'meteoric' version which works for even more complex situations (i.e. when roles are not mutually exclusive, etc.) – Diogenes Dec 28 '12 at 23:00
  • In that case you should probably pick @debergalis's answer since he's one of the creators of Meteor. Doesn't get much more meteoric than that :) – Rahul Dec 28 '12 at 23:08

3 Answers3

13

Use the fields option to only return the fields you want from a Mongo query.

Meteor.publish("extraUserData", function () {
  var user = Meteor.users.findOne(this.userId);
  var fields;

  if (user && user.role === 'counselor')
    fields = {pricePerSession: 1};
  else if (user && user.role === 'student')
    fields = {counselor: 1, sessionsRemaining: 1};

  // even though we want one object, use `find` to return a *cursor*
  return Meteor.users.find({_id: this.userId}, {fields: fields});
});

And then on the client just call

Meteor.subscribe('extraUserData');

Subscriptions can overlap in Meteor. So what's neat about this approach is that the publish function that ships extra fields to the client works alongside Meteor's behind-the-scenes publish function that sends basic fields, like the user's email address and profile. On the client, the document in the Meteor.users collection will be the union of the two sets of fields.

debergalis
  • 11,870
  • 2
  • 49
  • 43
  • 2
    One subtle note: if the user's role can change dynamically, this publisher won't notice that and change which fields it is publishing. If you need to do something like that, you'll currently have to implement it manually on top of `observe`; hopefully in the future Meteor will have some way of doing a fully reactive publish. – David Glasser Dec 29 '12 at 01:08
  • Also note that since `Meteor.user()` will change once `extraUserData` is marked as ready, all Autoruns will re-run twice: once when the logged in user is first loaded, and once when `extraUserData` is loaded. To avoid this, use `Meteor.userId()` instead: it will only change once. – Jonatan Littke Jul 10 '13 at 09:39
  • 1
    Also note that when merging overlapping record sets (cursors), only top level values compared. This means that if one subscription contains `{a: {x: 'x'} }` and another contains `{a: {y: 'y'} }` (for documents with the same `_id`) then the client will **not** get `{a: {x: 'x', y: 'y'} }` as you might expect, but an arbitrary one of the originals. See [this open issue](https://github.com/meteor/meteor/issues/3764), better described in [this closed issue](https://github.com/meteor/meteor/issues/903). – BudgieInWA May 10 '16 at 12:46
3

Meteor users by default are only published with their basic information, so you'll have to add these fields manually to the client by using Meteor.publish. Thankfully, the Meteor docs on publish have an example that shows you how to do this:

// server: publish the rooms collection, minus secret info.
Meteor.publish("rooms", function () {
  return Rooms.find({}, {fields: {secretInfo: 0}});
});

// ... and publish secret info for rooms where the logged-in user
// is an admin. If the client subscribes to both streams, the records
// are merged together into the same documents in the Rooms collection.
Meteor.publish("adminSecretInfo", function () {
  return Rooms.find({admin: this.userId}, {fields: {secretInfo: 1}});
});

Basically you want to publish a channel that returns certain information to the client when a condition is met, and other info when it isn't. Then you subscribe to that channel on the client.

In your case, you probably want something like this in the server:

Meteor.publish("studentInfo", function() {
  var user = Meteor.users.findOne(this.userId);

  if (user && user.type === "student")
    return Users.find({_id: this.userId}, {fields: {sessionsRemaining: 1, counselor: 1}});
  else if (user && user.type === "counselor")
    return Users.find({_id: this.userId}, {fields: {pricePerSession: 1}});
});

and then subscribe on the client:

Meteor.subscribe("studentInfo");
debergalis
  • 11,870
  • 2
  • 49
  • 43
Rahul
  • 12,181
  • 5
  • 43
  • 64
  • 1
    Whoops, we overlapped. But `Meteor.user` doesn't work in publish functions. See the variant in my answer. – debergalis Dec 28 '12 at 19:57
  • My excuse is that I wrote // pseudocode above it! But you're right ;-) – Rahul Dec 28 '12 at 19:58
  • How would this work if the user type/roles aren't mutually exclusive? (I actually simplified my case a bit). – Diogenes Dec 28 '12 at 22:57
  • What I mean to say is, lets say you are making a blog and users have roles (comment moderator, admin, editor) and you can be one or more of those things. Using these publish and subscribe tactics, how can I make sure to always send the information for the current user, plus that which they would need to see based on all their roles? – Diogenes Dec 28 '12 at 22:59
  • 1
    What do you mean? You can just expand the example code I gave for the various roles with more if/else statements. – Rahul Dec 28 '12 at 23:08
  • The if/else statements are exclusive so only one of the set can happen. Lets say an editor can see a list of articles published, and a moderator can see a list of comments moderated. How would something like this work if a user can be an editor *and* a moderator. – Diogenes Dec 28 '12 at 23:16
  • That's more complicated, but you'd just have to build up the `fields` argument to the `find` method differently. Like with any other role-based logic. – Rahul Dec 28 '12 at 23:23
0

Because Meteor.users is a collection like any other Meteor collection, you can actually refine its publicized content like any other Meteor collection:

Meteor.publish("users", function () {
    //this.userId is available to reference the logged in user 
    //inside publish functions
    var _role = Meteor.users.findOne({_id: this.userId}).role;
    switch(_role) {
        case "counselor":
            return Meteor.users.find({}, {fields: { sessionRemaining: 0, counselor: 0 }});
        default: //student
            return Meteor.users.find({}, {fields: { counselorSpecific: 0 }});
    }
});

Then, in your client:

Meteor.subscribe("users");

Consequently, Meteor.user() will automatically be truncated accordingly based on the role of the logged-in user.

Here is a complete solution:

if (Meteor.isServer) {
    Meteor.publish("users", function () {
        //this.userId is available to reference the logged in user 
        //inside publish functions
        var _role = Meteor.users.findOne({ _id: this.userId }).role;
        console.log("userid: " + this.userId);
        console.log("getting role: " + _role);
        switch (_role) {
            case "counselor":
                return Meteor.users.find({}, { fields: { sessionRemaining: 0, counselor: 0 } });
            default: //student
                return Meteor.users.find({}, { fields: { counselorSpecific: 0 } });
        }
    });

    Accounts.onCreateUser(function (options, user) {
        //assign the base role
        user.role = 'counselor' //change to 'student' for student data

        //student specific
        user.sessionRemaining = 100;
        user.counselor = 'Sam Brown';

        //counselor specific
        user.counselorSpecific = { studentsServed: 100 };

        return user;
    });
}

if (Meteor.isClient) {
    Meteor.subscribe("users");

    Template.userDetails.userDump = function () {
        if (Meteor.user()) {
            var _val = "USER ROLE IS " + Meteor.user().role + " | counselorSpecific: " + JSON.stringify(Meteor.user().counselorSpecific) + " | sessionRemaining: " + Meteor.user().sessionRemaining + " | counselor: " + Meteor.user().counselor;
            return _val;
        } else {
            return "NOT LOGGED IN";
        }
    };
}

And the HTML:

<body>
    <div style="padding:10px;">
        {{loginButtons}}
    </div>

    {{> home}}
</body>

<template name="home">
    <h1>User Details</h1>
    {{> userDetails}}
</template>

<template name="userDetails">
   DUMP:
   {{userDump}}
</template>
TimDog
  • 8,758
  • 5
  • 41
  • 50