Ability for a subscription to return multiple cursors from the same collection

Hi guys,

In the docs we have this:

If you return multiple cursors in an array, they currently must all be from different collections. We hope to lift this restriction in a future release.

Is this an easy thing to fix? One of my use cases is that I want to return user documents, and in practically all situations I want to be able to return a different set of fields (for security reasons) depending on the relationship that the caller has to those users. It would be far more elegant for me to encapsulate this logic on the server rather than having to do so at the client (for various reasons). I have a large application and this feature would make a big difference.

The above note has been in your docs for as long as I can remember (at least 3 years) - during which time I’m loving Meteor :slight_smile:

Thanks

Having looked in the Meteor server code for this, it’s probably rather tricky! So, in case someone else has a similar issue here’s what I’ve done:

Created a new method on the TemplateInstance prototype called subscribeUsers that calls a subscription on the user collection 3 times (as I have that many possible permutations of returning/suppressing certain fields). The server then decides which combination of subscriptions need return any documents:

// UserSubscription is the name of the original subscription which the 'subscribe.users'
// subscription now switches() on to implement the specific logic for each type of subscription
Blaze.TemplateInstance.prototype.subscribeUsers=function(userSubscription,params) {
      this.subscribe('subscribe.users',{params,userSubscription, userSubscriptionMode: 1});
      this.subscribe('subscribe.users',{params,userSubscription, userSubscriptionMode: 2});
      this.subscribe('subscribe.users',{params,userSubscription, userSubscriptionMode: 3});
};

Then, in the onCreated() template methods I change very little (In theory. In practice I have many subscriptions that involve multiple cursors which I’m having to split out):

this.subscribeUsers('my.fellowMembers',{clubId: Template.currentData().clubId});

Instead of:

this.subscribe('my.fellowMembers',{clubId: Template.currentData().clubId});

This encapsulates the circumstances where the calling user may or may not have rights to see certain details about other users. Not only that, all users can also set a switch to determine how much of their data some other users can see. My app has hundreds of pages and modules and so there are many times and variations of subscribing to this and other collections, so I need a general solution.

None of this logic can be put on the client as I can’t have the “forbidden” data fields getting passed to the client.

My first thought is that you could have two subscriptions, one with detail and one without, and any crossover between the two will merge the top level fields and leave you with more detail.

But since you also say this:

It sounds like your needs are more complicated.

In which case, you could use the low-level added/changed/removed DDP message instead of returning cursors.
This is an example, if you had a field on users called allowedFields which specified what can be published. This can be extended to support arbitrary filtering (check if the current user is in the same group, or if they are friends, etc)

Meteor.publish('my.fellowMembers', function({ clubId }) {
    check(clubId, String);

    // `observeChanges` only returns after the initial `added` callbacks have run.
    const handle = Meteor.users.find({ clubId }).observeChanges({
        added: (id, fields) => {
            let toPublish = fields;
            if (fields.allowedFields) {
                toPublish = Object.fromEntries(
                    Object.entries(fields)
                    .filter(([key]) => fields.allowedFields.includes(key))
                )
            }
            this.added('users', id, toPublish);
        },
        changed: (id, fields) => {
            let toPublish = fields;
            const fullDoc = Meteor.users.findOne(id);
            if (fullDoc.allowedFields) {
                toPublish = Object.fromEntries(
                    Object.entries(fields)
                    .filter(([key]) => fullDoc.allowedFields.includes(key))
                )
            }
            this.changed('users', id, toPublish);
        },
        removed: id => {
            this.removed('users', id);
        },

    });
    this.ready();
    // Stop observing the cursor when the client unsubscribes. Stopping a
    // subscription automatically takes care of sending the client any `removed`
    // messages.
    this.onStop(() => handle.stop());
});
1 Like

Many thanks for this. Great suggestion on the observerChanges() method. I had briefly considered that but was reticent on this one on the grounds of performance/efficiency.

I general avoid doing this as I have a (perhaps groundless) perception that it’s more work for the server rather than doing the inbuilt cursor subscription. Perhaps I’m wrong.