There is another collection, a user data collection, which contains 1 object:
{userid: "123qdasc23d", color: ["Red", "Orange"]}
I want to publish Fruit collection but only user specific color fruits. So Meteor should give only “Apple” and “Orange” fruits, not “Banana”. And whenever user logs out, they should disappear.
Meteor.publish('fruits', function () {
if (this.userId) {
// Publish fruits for this user if logged in
return Fruits.find({color: {$in: Meteor.user().color}}); // Meteor.user().color is an **array** in this case
}
// Otherwise don't publish data
return this.ready();
});
Safer version (only for public data):
// server
Meteor.publish('user-data', function () {
if (! this.userId) {
this.ready();
}
return UserData.find({userId: this.userId});
});
Meteor.publish('fruits', function (userId) {
var colors = UserData.findOne({userId: userId}).color;
return Fruits.find({color: {$in: colors}});
});
// client
Tracker.autorun(function () {
var userId = UserData.findOne({userId: Meteor.userId()}).color
Meteor.subscribe(userId);
});
The canonical way to reactively update the published items (including removal on logout) is to use publish/subscribe. The Meteor Guide covers this very thoroughly.
Note that publications are not reactive, so your example will not work as you expect (the if clause will only be evaluated once) unless you either reactively force a re-subscribe from the client, or remove the if and construct the query to include this.userId as part of the find.
True, but they are re-run whenever the logged-in user changes, and we’re not concerned with a possibly-changing userId field on the documents we want to find in this case.
I agree that the alternative is safer though, I’ve updated it thanks!
I think return null is for autopublish, for named pubs this.ready() tells the subscriber not to wait for data.
You are right about the lack of reactivity on the ‘safe version’ - for that you can either publish the UserData document to the client and pass that in the Meteor.subcribe param or use the low-level API
Multi colors:
Meteor.publish(function (userIds /* send an array of ids from client */) {
// If you also want the currently logged in user
userIds.push(this.userId);
var colors = [];
UserData.find({userId: {$in: userIds}}).forEach(function (data) {
colors.push(data.color);
});
// use underscore to remove duplicates
colors = _.unique(colors);
return Fruits.publish({color: {$in: colors}});
});
Meteor finds the data. The problem is, I have to refresh the page to get the code working. “Unsafe” version of the code was dependent on user login so Meteor was able to publish or not publish the data reactively. The safer version doesn’t have this capability.
There is nothing preventing “safe” publications from reactively changing the results based on userId - that’s their intended purpose. I think there have been a number of crossed wires here and I’m probably guilty of crossing most of them!
From @reoh’s earlier safe code, is the issue here:
var colors = UserData.find({userId: this.userId}).color;
find returns a cursor, so you can’t select a property (color) on it. @reoh solves this in the last example by using forEach to iterate over the documents, but a solution might be to use findOne rather than find, which returns a (single) document.
Here’s a low-level version suitable for private data:
// psuedo-code
UserData = new Mongo.Collection('user-data');
Fruits = new Mongo.Collection('fruits');
Meteor.publish('fruits', function () {
var self = this;
var colors = [];
var handle = UserData.find({userId: this.userId}).observeChanges({
// Runs whenever a document is added to user data collection
// and on first run when it initialises
added: function (id, fields) {
// store current colors for later use
colors = fields.color
Fruits.find({color: {$in: fields.color}}).forEach(function (fruit) {
// publishes fruits with colors matching the user data
self.added('fruits', fruit._id, fruit);
});
},
// Runs whenever a document in the user data collection is edited
changed: function (id, fields) {
// If 'color' field on user data doc was updated
if (fields.color) {
// publish new fruits with matching colors
Fruits.find({color: {$in: fields.color}}).forEach(function (fruit) {
self.added('fruits', id, fields)
});
// clean up old fruits we don't want anymore on client
Fruits.find({color: {$in: colors}}).forEach(function (fruit) {
self.removed('fruits', fruit._id);
});
// store updated colors
colors = fields.color;
}
},
// and removed
removed: function (id) {
Fruits.find({color: {$in: colors}}).forEach(function (fruit) {
// clean up
self.removed('fruits', fruid._id);
});
}
});
self.ready();
self.onStop(function () {
handle.stop();
});
});
Thanks for the code! But I don’t understand why I need something this complex? Why do I have to use observeChanges ? Shouldn’t Meteor take care of this automatically?
Usually, it does - but it depends on changes to the parameter you pass to the subscribe function to re-run the publication in full.
Changes to cursors don’t re-run the publication function though (because it uses the added, changed, removed for updates), so you have move that code into the actual callbacks that the publication is using behind the scenes - this will make sure that the fruits data stays up-to-date with UserData.