Mongo query to get only the keys of an object, but no values

I want to publish the social login services users have linked their accounts to, but without publishing the configuration details of these services. So, what I need is a query that gets all keys of the services subtree of the user object, but not the values of these keys.

Is there a simple way to do that with a simple Mongo query, or do I have to get the whole service configuration and create a custom publication based on the results?

Check out this section of the Meteor documentation (publish-subscribe):

Alternatively, a publish function can directly control its published record set by calling the functions added (to add a new document to the published record set), changed (to change or clear some fields on a document already in the published record set), and removed (to remove documents from the published record set). These methods are provided by this in your publish function.

There is an example before that paragraph that shows pretty much what you want, I think.

Plus there is also another interesting example in an answer on SO:

Meteor.publish("publicationsWithHTML", function (data) {
    var self = this;
    Publications
        .find()
        .forEach(function(entry) {
            addSomeHTML(entry);  // this function changes the content of entry
            self.added("publications", entry._id, entry);
        });
    self.ready();
});

This is from 2014, and maybe things stopped working like that since – but if they didn’t, this gives you complete control to alter the returned document(s).

Thanks for the feedback. Yes, I solved this using observeChanges and an added hook in the meantime. However, with my current solution, I actually need two cursors in my ownUser publication: one for the user object without the service configuration (in order to not publish sensitive data to the client) and one that just observes changes on the services key, clears all data inside these keys and sends just an empty object for every service key to the client:

  // publish social services, but without sensitive information
  const servicesCursor = Meteor.users.find({ _id }, { fields: { services: 1 } });
  const clear = (services = {}) => {
    const cleared = {};
    const keys = _.intersection(Object.keys(services), allServices);
    keys.forEach(key => (cleared[key] = {}));
    return cleared;
  };
  servicesCursor.observeChanges({
    added: (id, fields) => this.added('users', id, { services: clear(fields.services) })
  });
  // publish other fields that are safe to publish
  const cursor = Meteor.users.find({ _id }, { fields: allowedFields });
  return cursor;

This works, but does not look very elegant. So I am still wondering if there is an easier way to do it, using just a Mongo query.

You can only publish one service field which is not important. Then you can have list of keys at the client side.

How do you mean that?

I mean the list of services keys is limited. For example you have password, email, github, facebook, twiter. You can select what key is available by project:

'services.password.fieldNameNeverExists': 1,
'services.email.fieldNameNeverExists': 1,
'services.github.fieldNameNeverExists': 1,
'services.facebook.fieldNameNeverExists': 1,
'services.twiter.fieldNameNeverExists': 1,

Then if user has password, email and facebook in services you will have a list:

services {
   password {},
   email {},
   facebook {},
},
3 Likes

Ha, that’s indeed an interesting approach.

2 Likes

That’s very inventive indeed! :+1:

Do you need it to be a publication? Could you just use a method?

I need it as a publication, because it will be the signal to the user that the connection has been established. Although I might as well rely on the callback methods, I wasn’t sure if they would work reliably enough. And a publication won’t do any harm in my use-case, as I already have a publication for the user’s own profile data.

You can do this with (reactive) aggregate as long as your MongoDB is at least v3.6. For example, something like this with tunguska-reactive-aggregate:

Meteor.publish('userServices', function () {
  ReactiveAggregate(this, Meteor.users, [
    {
      $match: {
        _id: this.userId
      }
    },
    {
      $project: {
        keys: {
          $objectToArray: "$services"
        }
      }
    },
    {
      $unwind: "$keys"
    },
    {
      $group: {
        _id: "$_id",
        services: {
          $addToSet: "$keys.k"
        }
      }
    },
  ], {
    clientCollection: 'availableServices',
    noAutomaticObserver: true,
    observers: [
      Meteor.users.find({ _id: this.userId })
    ],
  });
});
5 Likes

Wow, that was what I was looking for. I was quite sure it was possible with aggregations somehow, but this turned out to be pretty complex…

2 Likes