What is the correct way to selectively publish in Meteor based on information in the database? - meteor

For instance, when your permissions are group-based, and your user document has a list of groups that the user belongs to. I'm publishing docs in an Items collection, and you should only be able to view items with a groupOwner field matching a group you belong to.
It would be nice if you could autorun inside a publish, but I doubt you can:
Meteor.publish 'screened-items', ->
Deps.autorun ->
user = Users.findOne #userId
return Items.find {groupOwner: {$in: user.groups}}
If you can't, this is the best I can come up with, but it's going to be slow and memory-intensive. Is this the only way to do it?
Meteor.publish 'screened-items', ->
user = Users.findOne #userId
# (hope that the db doesn't change between this line and the observeChanges)
saved_items = Items.find({groupOwner: {$in: user.groups}}).fetch()
# call #added on each item
handle = Users.findOne(#userId).observeChanges {
changed: (_, fields) =>
if fields.groups
new_items = Items.find({groupOwner: {$in: fields.groups}}).fetch()
# compare new_items to saved_items, and call #added() or #removed() for each difference
}
#ready()
#.onStop ->
handle.stop()

You can achieve this two ways:
Use the publish-with-relations package, for example:
Meteor.publish 'screend-items', ->
# select the current user
Meteor.publishWithRelations
handle: this
collection: Meteor.users
filter:
_id: #userId
options:
fields:
groups: 1
mappings: [
key: 'groupOwner' # and map to the `groupOwner` field on Items
collection: Items
]
Denormalize the relationship, providing a succinct list of users to use for publishing
Items._ensureIndex(userIds: 1) # best to index this field
# basic publications
Meteor.publish 'screend-items', ->
# don't expose `userIds` to the client
return Items.find({userIds: #userId}, {fields: userIds: false})

If you want the published docs to change when the userId changes, that is the default behaviour.
However, if the logged-in user changes, the publish function is rerun with the new value. - from docs.meteor.com.
Deps.autorun() only works on the client while Meteor.publish() only works on the server. So you can not autorun inside of publish.
If you are okay to let the client see the 'groups' they're in, the code is a bit simpler because you can start and stop the subscription when the groups change. Like this:
//on client
Deps.autorun( function() {
Meteor.subscribe( 'items', Meteor.user().groups );
});
//on server
Meteor.publish( 'items', function( groups ){
var self = this;
var user = Meteor.users.findOne( {_id: self.userId});
if ( ! (user && user.groups === groups) )
return;
return Items.find({groupOwner: {$in: groups}});
});
Otherwise you would need use two observers inside the publish function - one to watch user for changes to groups and another to manage publishing items that are in the group. See this example of doing a join of collections that way.

Related

Meteor Iron Router resubscribe on Rights change with meteor-roles

I am writing a meteor application with a user-role-system (alanning:roles) My Roles are group based. When a user knows my group url, it is allowed to access the group an get the role "defaultUser" in this group.
localUser is allowed to subscribe to all the local stuff of a group.
based on the group's ID, I also publish some contents.
The Problem is: the Subscription does not resubscribe.
Workflow:
user accesses app,
calls meteor-method to get the default role
gets the default role
subscribes to a publication
the publication (complete-group) publishes the contents if the user has the right role
My Publication looks like:
Meteor.publish "thisGroupPublic", (id) ->
return db.groups.find({_id: id}, {fields: {onlypublicones...}}
Meteor.publishComposite "thisGroupReactive", (id) ->
return {
find: () ->
if !Roles.userIsInRole(#userId, "defaultUser", id)
#ready()
console.log("[thisGroupReactive] => No Rights")
return;
return db.groups.find({_id: id});
children: [
{
find: (group) ->
return db.contents.find({groups: {$in: [group._id]}}, {fields: {apikey: 0}})
}
]
}
The user subscribes the subscription "thisGroupPublic" when the user is on the login page and gets the role "defaultUser" on its first visit to the group as a logged in user. But how do I need to configure iron:router to
resubscribe this subscription that the contents are shown and not only the public stuff?
Say that the user is on a route /something
You have some data that changes and you create a session variable:
Session.set("someDataThatChanges", myChangedData)
Your publish function takes some sort of input, which it uses to return different data from the collection:
Meteor.publish("myCollection", function(input){
return myCollection.find(
// do something here based on 'input'
);
});
Iron Router has a .subscribe method that is the same as Meteor.subscribe and also a subscriptions key that takes a function. You can wrap a Tracker.autorun around your .subscribe and put in your session variable to automatically re-subscribe to something based on the changing value of that session variable.
Router.route("/something", {
name: "templateName",
// a place to put your subscriptions
subscriptions: function() {
console.log("this in router ", this);
Tracker.autorun(function(){
this.subscribe('myCollection', Session.get("someDataThatChanges");
});
},
});

Modeling and publishing a follower-based feed with Meteor

I'm working on a simple app where a User can follow other users. Users can star posts. And a user's feed is composed of posts that have been starred by users they follow. Pretty simple actually. However, this all gets complicated in Mongo and Meteor...
There are basically two way of modeling this that I can think of:
A user has a property, following, which is an array of userIds that the user follows. Also, a post has a property, starrers, which is an array of userIds that have starred this post. The good thing about this method is that publications are relatively simple:
Meteor.publish 'feed', (limit) ->
Posts.find({starrers: {$in: Meteor.users.findOne(#userId).following}}, {sort: {date: -1}, limit:limit})
We aren't reactively listening to who the user is following, but thats not too bad for now. The main problem with this approach is that (1) the individual documents will become large and inefficient if 1000000 people star a post. Another problem is that (2) it would be pain to keep track of information like when a user started following another user or when a user starred a post.
The other way of doing this is having two more collections, Stars and Follows. If a user stars a post, then we create a document with properties userId and postId. If a user follows another user, then we create a document with properties userId and followId. This gives us the advantage of smaller document sizes for Users and Posts, but complicated things when it comes to querying, especially because Mongo doesn't handle joins!
Now, I did some research and people seem to agree that the second choice is the right way to go. Now the problem I'm having is efficiently querying and publishing. Based on the Discover Meteor chapter about Advanced Publications, I created a publication that publishes the posts that are starred by user's followers -- sorted, and limited.
# a helper to handle stopping observeChanges
observer = (sub, func) ->
handle = null
sub.onStop ->
handle?.stop?()
() ->
handle?.stop?()
handle = func()
Meteor.publish 'feed', (limit) ->
sub = this
userId = #userId
followIds = null
eventIds = null
publishFollows = observer sub, () ->
followIds = {}
Follows.find({userId:userId}).observeChanges
added: (id, doc) ->
followIds[id] = doc.followId
sub.added('follows', id, doc)
publishStars()
removed: (id) ->
delete followIds[id]
sub.removed('follows', id)
publishStars()
publishStars = observer sub, () ->
eventIds = {}
Stars.find({userId: {$in: _.keys(followIds)}).observeChanges
added: (id, doc) ->
eventIds[id] = null
sub.added('stars', id, doc)
publishEvents()
removed: (id) ->
delete eventIds[id]
sub.removed('stars', id)
publishEvents()
publishEvents = observer sub, () ->
Events.find({_id: {$in: _.keys(eventIds)}}, {sort: {name:1, date:-1}, limit:limit}).observeChanges
added: (id, doc) ->
sub.added('events', id, doc)
changed: (id, fields) ->
sub.changed('events', id, fields)
removed: (id) ->
sub.removed('events', id)
While this works, it seems very limited at scale. Particularly, we have to compile a list of every starred post by every follower. The size of this list will grow very quickly. Then we do a huge $in query against all posts.
Another annoyance is querying for the feed on the client after we subscribe:
Meteor.subscribe("feed", 20)
posts = null
Tracker.autorun ->
followers = _.pluck(Follows.find({userId: Meteor.userId()}).fetch(), "followId")
starredPostIds = _.pluck(Stars.find({userId: {$in: followers}}).fetch(), "postId")
posts = Posts.find({_id: {$in: starredPostIds}}, {sort: {date: -1}, limit: 20}).fetch()
Its like we're doing all this work twice. First we do all the work on the server to publish the feed. Then we need to go through the exact same logic again on the client to get those posts...
My question here is a matter of design over everything. How can I efficiently design this feed based on followers staring posts? What collection / collection schemas should I use? How should I create the appropriate publication? How can I query for the feed on the client?
So it turns out that Mongo and "non-relational" databases simply aren't designed for relational data. Thus, there is no solution here with Mongo. I've ended up using Neo4j, but SQL would work fine as well.
meteor add reywood:publish-composite
Meteor.publishComposite('tweets', function(username) {
return {
find: function() {
// Find the current user's following users
return Relationships.find({ follower: username });
},
children: [{
find: function(relationship) {
// Find tweets from followed users
return Tweets.find({user: relationship.following});
}
}]
}
});
Meteor.publish('ownTweets', function(username) {
return Tweets.find({user: username});
});

Publication of items where User is in group (Alanning Roles and Publications)

I am using Alanning Roles to maintain a set of groups/roles for the users of my application. When a user creates an "Application", I generate a new role for them as the app_name + UUID, then add that as a group with the roles of Admin to the user that created it. I can then use the combination of the generated group name plus either the Admin or Viewer roles to determine which Applications the user has rights to see and/or edit.
The issue that I am having is that I can't figure out a good way to get the publication to only publish the things the user should see. I know that, by default at least, publications are not "reactive" in the way the client is, and they they are only reactive for the cursors they return. But, in my code I create the group/role first, add it to the user, then save the "Application", which I thought would rerun my publication, but it did not:
Meteor.publish('myApplications', function(groups) {
if (this.userId) {
console.log('Running myApplications publication...');
console.log('Found roles for user ' + this.userId + ': ', Roles.getGroupsForUser(this.userId));
return Applications.find({group: {$in: Roles.getGroupsForUser(this.userId)}});
} else {
//console.log("Skipping null user");
return null;
}
});
But, contrary to what I thought would happen (the whole publication method would re-run), I am guessing what really happens is that only the Cursor is updates. So for my next attempt, I added the mrt:reactive-publications package and simply got a cursor to the Meteor.users collection for the user, thinking that would "trigger" the publication to re-run when the user gets updated with the new group/role, but that didn't work.
I have this finally working by simply passing in the groups for the user:
Meteor.publish('myApplications', function(groups) {
if (this.userId) {
if (!groups || groups.length === 0) {
groups = Roles.getGroupsForUser(this.userId);
}
console.log('Running myApplications publication...');
console.log('Found roles for user ' + this.userId + ': ', Roles.getGroupsForUser(this.userId));
return Applications.find({group: {$in: groups}});
} else {
//console.log("Skipping null user");
return null;
}
});
And then I just call the publication like Meteor.subscribe('myApplications', Roles.getGroupsForUser(Meteor.userId())) in my route's waitOn, but this would mean that any client could call the same publication and pass in any groups they like, and potentially see documents they were not intended to see. That seems like a pretty large security flaw.
Is there a better way to implement this such that the client would not be able to coax their way to seeing stuff not theirs? I think the only real way would be to gather the groups on the publication side, but then it breaks the reactivity.
After sifting through a bunch of docs and a few very helpful stack posts, this is the alternative I came up with. Works like a charm!
My objective was to publish 'guest' users' info to the group admins for approval/denial of enhanced permissions.
Meteor.publish('groupAdmin', function(groupId) {
// only publish guest users info to group admins
if(Roles.userIsInRole(this.userId, ['group-admin'], groupId)) {
// I can't explain it but it works!
var obj = {key: {$in: ['guest']}};
var query = {};
var key = ('roles.' + groupId);
query[key] = {$in: ['guest']};
return Meteor.users.find(query, {
fields: {
createdAt: 1,
profile: 1
}
});
} else {
this.stop();
return;
}
});
Reference: How to set mongo field from variable
& How do I use a variable as a field name in a Mongo query in Meteor?

How to push data from server to all clients not using Collections?

I need to inform clients about changes on server side. In my case I am using different Collections on server and on client (more about it in this question: how would you build pinterest like page with meteor.js).
On the server I am getting new Products from external API. I would like to publish the number of new items to all clients that they could update their local variables needed for layout to work well.
How to do it?
It would be nice if I could publish/subscribe other kinds of data than Meteor.Collection. I found Meteor.deps, but what I understand it works only on client side.
To accomplish what you want you do need another collection - on the client. On the server, in a publish function, build a document from scratch assigning the current count of Products to an attribute. Using observe() and set, modify count when documents are added or removed from Products. Subscribe to the count "record set" on the client.
// Server
Meteor.publish('count', function () {
// Build a document from scratch
var self = this;
var uuid = Meteor.uuid();
var count = Products.find().count();
// Assign initial Products count to document attribute
self.set('count', uuid, {count: count});
// Observe Products for additions and removals
var handle = Products.find().observe({
added: function (doc, idx) {
count++;
self.set('counts', uuid, {count: count});
self.flush();
},
removed: function (doc, idx) {
count--;
self.set('counts', uuid, {count: count});
self.flush();
}
});
self.complete();
self.flush();
self.onStop(function () {
handle.stop();
});
});
// Client
Counts = new Meteor.Collection('count');
Meteor.subscribe('count');
console.log('Count: ' + Counts.findOne().count);
I must say the above solution showed me one way, but still, what if I need to publish to client data that are not connected with observe()? Or with any collection?
In my case I have i.e. 1000 products. To engage visitors I am "refreshig" the collection by updating the timestamp of random number of products, and displaying collection sorted by timestamp. Thank to this visitors have impression that something is happening.
My refresh method returns number of products (it is random). I need to pass that number to all clients. I did it, but using (I think) ugly workaround.
My refresh method sets Session.set('lastRandomNo', random). BTW: I didn't know that Session works on server side. refresh updates Products collection.
Then accoriding to above answer:
Meteor.publish 'refreshedProducts', ->
self = this
uuid = Meteor.uuid()
# create a new collection to pass ProductsMeta data
self.set('products_meta', uuid, { refreshedNo: 0 })
handle = Products.find().observe
changed: (newDocument, atIndex, oldDocument) ->
self.set('products_meta', uuid, { refreshedNo: Session.get('lastRandomNo') })
self.flush()
self.complete()
self.flush()
self.onStop ->
handle.stop()
and on client side:
ProductsMeta = new Meteor.Collection('products_meta')
# subscribe to server 'products_meta' collection that is generated by server
Meteor.subscribe('refreshedProducts')
ProductsMeta.find({}).observe
changed: (newDocument, atIndex, oldDocument) ->
# I have access to refreshedNo by
console.log ProductsMeta.findOne().refreshedNo
What do you think?

Reactive updates when query is filtered by another query

I just started using meteor today and can't seem to figure out what I'm doing wrong. I have a query that is being run inside of a publish function, but this query is filtered by the result of another query.
In short, when I add a document to the database that is being published (CollectionTwo) it works as I would expect, but when I make my change in the database that is being used to filter (CollectionOne), meteor doesn't behave reactively.
CollectionOne = new Meteor.Collection("one")
CollectionTwo = new Meteor.Collection("two")
Meteor.publish("items", ->
not_hidden = CollectionOne.find().fetch()
return CollectionTwo.find( _id: {'$in':( t.my_id for t in not_hidden )} )
)
Meanwhile, on the client...
CollectionOne = new Meteor.Collection("one")
CollectionTwo = new Meteor.Collection("two")
Meteor.subscribe("items")
_.extend( Template.items,
items: ->
not_hidden = CollectionOne.find().fetch()
return CollectionTwo.find( _id: {'$in':( t.my_id for t in not_hidden )} )
)
Any ideas what the appropriate solution might be?
Reactivity doesn't work that way inside Meteor.publish on the server. Meteor won't recalculate the CollectionTwo.find query when the contents of CollectionOne changes.
To implement what you want, manage the publish by hand, instead of just returning a Cursor. You'll need to use observeinside your publish function to watch for changes on CollectionOne, and then manually call this.set and this.unset to push changes down to the client. There's an example of this technique in the publish documentation. The example only looks at one collection, but you can extend the idea to a nested set of observes.
We're going to work on sugar to make this sort of pattern easier to implement.
Until there's a better pattern for this in core meteor these two atmosphere packages solve the problem:
https://atmosphere.meteor.com/package/server-deps
https://atmosphere.meteor.com/package/reactive-publish
Install the second package with meteorite, use "Meteor.reactivePublish" instead of "Meteor.publish" and it will automatically update when the results of any queries with the option {"reactive": true} change.
This example from the readme will publish precisely those items which the user's team can see, and will update when either the user changes team or the team's visible items change.
Meteor.reactivePublish(null, function() {
if (this.userId) {
var user = Meteor.users.findOne({_id: this.userId}, {reactive: true});
if (user.team) {
var team = Collections.teams.findOne({_id: user.team}, {reactive: true});
var visibleItems = _.compact(team.visibleItems);
return Collections.items.find({_id: {$in: visibleItems}});
}
}
});
You can use the reactive-publish package (I am one of authors):
Meteor.publish "items", ->
#autorun (computation) =>
not_hidden = CollectionOne.find({}, {fields: my_id: 1}).fetch()
CollectionTwo.find _id: $in: _.pluck not_hidden, 'my_id'
The important thing is to limit queried fields from CollectionOne to only my_id, otherwise autorun will be rerun on any field change in CollectionOne documents, not just my_id.

Resources