kerosene
kerosene

Reputation: 973

Meteor: How to publish cursor that is depending on cursor of other collection

I'm having the following data structure in my Meteor project:
- Users with a set of list-ids that belong to the user (author)
- Lists that actually contain all the data of the list

Now I'm trying to publish all Lists of a user to the client. Here is a simple example:

if (Meteor.isClient) {
    Lists = new Meteor.Collection("lists");

    Deps.autorun(function() {
        Meteor.subscribe("lists");
    });

  Template.hello.greeting = function () {
    return "Test";
  };

  Template.hello.events({
    'click input' : function () {
      if (typeof console !== 'undefined')
        console.log(Lists.find());
    }
  });
}

if (Meteor.isServer) {
    Lists = new Meteor.Collection("lists");

    Meteor.startup(function () {
        if ( Meteor.users.find().count() === 0 ) {
               Accounts.createUser({        //create new user
                   username: 'test',
                   email: '[email protected]',
                   password: 'test'
               });

               //add list to Lists and id of the list to user
               var user = Meteor.users.findOne({'emails.address' : '[email protected]', username : 'test'});
               var listid = new Meteor.Collection.ObjectID().valueOf();
               Meteor.users.update(user._id, {$addToSet : {lists : listid}});
               Lists.insert({_id : listid, data : 'content'});
        }
     });


    Meteor.publish("lists", function(){
        var UserListIdsCursor = Meteor.users.find({_id: this.userId}, {limit: 1}).lists;
        if(UserListIdsCursor!=undefined){
            var UserListIds = UserListIdsCursor.fetch();

            return Lists.find({_id : { $in : UserListIds}});
        }
    });

    Meteor.publish("mylists", function(){
        return Meteor.users.find({_id: this.userId}, {limit: 1}).lists;
    });


//at the moment everything is allowed
Lists.allow({
    insert : function(userID)
    {
        return true;
    },
    update : function(userID)
    {
        return true;
    },
    remove : function(userID)
    {
        return true;
    }
});

}

But publishing the Lists doesn't work properly. Any ideas how to fix this? I'm also publishing "mylists" to guarantee that the user has access to the field "lists".

Upvotes: 1

Views: 3249

Answers (3)

David Weldon
David Weldon

Reputation: 64312

Solution

Lists = new Meteor.Collection('lists');

if (Meteor.isClient) {
  Tracker.autorun(function() {
    if (Meteor.userId()) {
      Meteor.subscribe('lists');
      Meteor.subscribe('myLists');
    }
  });
}

if (Meteor.isServer) {
  Meteor.startup(function() {
    if (Meteor.users.find().count() === 0) {
      var user = {
        username: 'test',
        email: '[email protected]',
        password: 'test'
      };

      var userId = Accounts.createUser(user);
      var listId = Lists.insert({data: 'content'});
      Meteor.users.update(userId, {
        $addToSet: {lists: listId}
      });
    }
  });

  Meteor.publish('lists', function() {
    check(this.userId, String);
    var lists = Meteor.users.findOne(this.userId).lists;
    return Lists.find({_id: {$in: lists}});
  });

  Meteor.publish('myLists', function() {
    check(this.userId, String);
    return Meteor.users.find(this.userId, {fields: {lists: 1}});
  });
}

Changes

  1. Declare the Lists collection outside of the client and server (no need to declare it twice).
  2. Ensure the user is logged in when subscribing. (performance enhancement).
  3. When inserting the test user, use the fact that all insert functions return an id (reduces code).
  4. Ensure the user is logged in when publishing.
  5. Simplified lists publish function.
  6. Fixed myLists publish function. A publish needs to return a cursor, an array of cursors, or a falsy value. You can't return an array of ids (which this code doesn't access anyway because you need to do a fetch or a findOne). Important note - this publishes another user document which has the lists field. On the client it will be merged with the existing user document, so only the logged in user will have lists. If you want all users to have the field on the client then I'd recommend just adding it to the user profiles.

Caution: As this is written, if additional list items are appended they will not be published because the lists publish function will only be rerun when the user logs in. To make this work properly, you will need a reactive join.

Upvotes: 3

nathan-m
nathan-m

Reputation: 8865

The real problem here is the schema.

Don't store "this user owns these lists" eg, against the users collection. Store "this list is owned by this user"

By changing your example to include an ownerId field on each List then publishing becomes easy - and reactive.

It also removes the need for the myLists publication, as you can just query client side for your lists.

Edit: If your schema also includes a userIds field on each List then publishing is also trivial for non-owners.

Solution

Lists = new Meteor.Collection('lists');

if (Meteor.isClient) {
  Deps.autorun(function() {
    if (Meteor.userId()) {
      Meteor.subscribe('lists.owner');
      Meteor.subscribe('lists.user');
    }
  });
}

if (Meteor.isServer) {
  Lists._ensureIndex('userIds');
  Lists._ensureIndex('ownerId');

  Meteor.startup(function() {
    if (Meteor.users.find().count() === 0) {
      var user = {
        username: 'test',
        email: '[email protected]',
        password: 'test'
      };

      var userId = Accounts.createUser(user);
      var listId = Lists.insert({data: 'content', ownerId: userId});
    }
  });

  //XX- Oplog tailing in 0.7 doesn't support $ operators - split into two publications -
  //      or change the schema to include the ownerId in the userIds list
  Meteor.publish('lists.user', function() {
    check(this.userId, String);
    return Lists.find({userIds: this.userId});
  });
  Meteor.publish('lists.owner', function() {
    check(this.userId, String);
    return Lists.find({ownerId: this.userId});
  });
}

Upvotes: 2

Tarang
Tarang

Reputation: 75945

Meteor.users.find() returns a cursor of many items but you're trying to access .lists with

Meteor.users.find({_id: this.userId}, {limit: 1}).lists;

You need to use findOne instead.

Meteor.users.findOne({_id: this.userId}).lists;

Additionally you're running .fetch() on an array which is stored in the user collection. If this is an array of ._id fields you don't need fetch.

You can't also do .lists in your second publish because its a cursor you have to check lists client side so just use Meteor.users.find(..) on its own since you can only publish cursors.

Upvotes: 0

Related Questions