Meteor Collection advanced selector

Meteor Collection advanced selector,meteor,Meteor,I have a Project collection and a Task collection. Each project has a user_id field, this holds the owner of the project. Each task has a project_id field. So the structure is something like this: User 1 Project 1 Task 1 Task 2 Project 2 Task 3 User 2 Project 3 Task 4 Task 5 For security purposes I only want to publish the projects belonging to a certain logged in user. For the project itself that's quite easy: Meteor.publish('projects', function(){

I have a Project collection and a Task collection.

Each project has a user_id field, this holds the owner of the project.

Each task has a project_id field. So the structure is something like this:

  • User 1
    • Project 1
    • Task 1
    • Task 2
    • Project 2
    • Task 3
  • User 2
    • Project 3
    • Task 4
    • Task 5

For security purposes I only want to publish the projects belonging to a certain logged in user. For the project itself that's quite easy:

Meteor.publish('projects', function(){
    return Projects.find({user_id: this.userId});
});

But how do I do this in a clean way for the Task collection? And why does the Collection.Allow doesn't have a 'view' option?

Something like:

Tasks.allow({
  view: function (userId, doc) {
    return Projects.findOne(doc.project_id).user_id == userId;
  }
});

would be nice, is there a reason it's not there?


#1

First, some recommended reading:

Joins in meteor are currently tricky. It's easy to just join the collections in a publish function, but it isn't always straightforward to make them reactive (run again when things change).

Non-Reactive Options

You could publish both collections at the same time with:

Meteor.publish('projectsAndTasks', function() {
  var projectsCursor = Projects.find({user_id: this.userId});
  var projectIds = projectsCursor.map(function(p) { return p._id });

  return [
    projectsCursor,
    Tasks.find({project_id: {$in: projectIds}});
  ];
});

The potential problem is that if tasks were added to a new project, they would not be published (see "The Naive Approach" from the first article above). Depending on how your application starts and stops its subscriptions, this may not matter. If you find that it does, keep reading.

Reactive Options

A simple option is just to denormalize the data. If you also added user_id to your tasks, then no joins are necessary, and the publish function looks like:

Meteor.publish('projectsAndTasks', function() {
  var projectsCursor = Projects.find({user_id: this.userId});
  var tasksCursor = Tasks.find({user_id: this.userId});
  return [projectsCursor, tasksCursor];
});

If that doesn't appeal to you and you are using iron-router, you can do a client-side join in your routes (see "Joining On The Client" from the first article above). It's a bit slower because you need a second round trip but it's clean in that no data needs to be modified and no external packages need to be added.

Finally, you can do a reactive join on the server, either manually using observeChanges (not recommended), or by using a package. I have used publish-with-relations in the past, but it has some issues as pointed out in the articles). For a more complete list of package options, you can see this thread.


Not being a core developer on meteor, I don't have a precise answer for why allow/deny doesn't have a "read" option, but I'll take an educated guess. Depending on how the allow/deny function was written, the publisher would potentially have to run an expensive callback for every single document or partial update. The allow/deny callbacks are easy to tolerate when a single document is being modified, but if you suddenly need to publish several hundred documents and each one needs to be separately evaluated before being transmitted, I don't think that would be practical. I'm pretty sure that's why publishers can act alone as the arbiter of document read authorization.


#2

You can do this for the tasks:

Meteor.publish('tasks', function(){
     var projects = Projects.find({user_id: this.userId}, {fields: {_id: 1}});
     var projectIdList = projects.map(function(project) { return project._id;});
     return Tasks.find({project_id: {$in: projectIdList}});
});

First we get all the projects belonging to the user. We will only need the _id field so we filter the other fields

Then we map the _id's of the projects to a new array.

Then we publish a tasks.find that includes all the project ids in the mapped array.

The allow construction you mentionend is by my knowledge only ment to be used with updates and inserts


#3

First, some recommended reading:

Joins in meteor are currently tricky. It's easy to just join the collections in a publish function, but it isn't always straightforward to make them reactive (run again when things change).

Non-Reactive Options

You could publish both collections at the same time with:

Meteor.publish('projectsAndTasks', function() {
  var projectsCursor = Projects.find({user_id: this.userId});
  var projectIds = projectsCursor.map(function(p) { return p._id });

  return [
    projectsCursor,
    Tasks.find({project_id: {$in: projectIds}});
  ];
});

The potential problem is that if tasks were added to a new project, they would not be published (see "The Naive Approach" from the first article above). Depending on how your application starts and stops its subscriptions, this may not matter. If you find that it does, keep reading.

Reactive Options

A simple option is just to denormalize the data. If you also added user_id to your tasks, then no joins are necessary, and the publish function looks like:

Meteor.publish('projectsAndTasks', function() {
  var projectsCursor = Projects.find({user_id: this.userId});
  var tasksCursor = Tasks.find({user_id: this.userId});
  return [projectsCursor, tasksCursor];
});

If that doesn't appeal to you and you are using iron-router, you can do a client-side join in your routes (see "Joining On The Client" from the first article above). It's a bit slower because you need a second round trip but it's clean in that no data needs to be modified and no external packages need to be added.

Finally, you can do a reactive join on the server, either manually using observeChanges (not recommended), or by using a package. I have used publish-with-relations in the past, but it has some issues as pointed out in the articles). For a more complete list of package options, you can see this thread.


Not being a core developer on meteor, I don't have a precise answer for why allow/deny doesn't have a "read" option, but I'll take an educated guess. Depending on how the allow/deny function was written, the publisher would potentially have to run an expensive callback for every single document or partial update. The allow/deny callbacks are easy to tolerate when a single document is being modified, but if you suddenly need to publish several hundred documents and each one needs to be separately evaluated before being transmitted, I don't think that would be practical. I'm pretty sure that's why publishers can act alone as the arbiter of document read authorization.


#4

You can do this for the tasks:

Meteor.publish('tasks', function(){
     var projects = Projects.find({user_id: this.userId}, {fields: {_id: 1}});
     var projectIdList = projects.map(function(project) { return project._id;});
     return Tasks.find({project_id: {$in: projectIdList}});
});

First we get all the projects belonging to the user. We will only need the _id field so we filter the other fields

Then we map the _id's of the projects to a new array.

Then we publish a tasks.find that includes all the project ids in the mapped array.

The allow construction you mentionend is by my knowledge only ment to be used with updates and inserts


#5

Could I use https://github.com/matb33/meteor-collection-hooks to call the method in your first example on project.after.insert server side? Does this make sense? Or is a new request from the client required?

#6

If you are referring to the denormalization option, yes, collection-hooks would work. Alternatively you can add the user_id with this hack.

#7

Note that this won't work if the user's projects change. If the user creates a new project, they won't get the tasks in that project unless they unsubscribe and resubscribe. Or if the project changes owner, they'll still get the tasks for that project (until they resubscribe). There's no Deps reactivity on the server, the publish function won't re-run when Projects.find({user_id: this.userId}, {fields: {_id: 1}}) changes.

#8

Could I use https://github.com/matb33/meteor-collection-hooks to call the method in your first example on project.after.insert server side? Does this make sense? Or is a new request from the client required?

#9

If you are referring to the denormalization option, yes, collection-hooks would work. Alternatively you can add the user_id with this hack.

#10

Note that this won't work if the user's projects change. If the user creates a new project, they won't get the tasks in that project unless they unsubscribe and resubscribe. Or if the project changes owner, they'll still get the tasks for that project (until they resubscribe). There's no Deps reactivity on the server, the publish function won't re-run when Projects.find({user_id: this.userId}, {fields: {_id: 1}}) changes.