In Meteor, how can I query only the records of a given subscription? - meteor

I understand that a a subscription is a way to flow records into a client-side collection, from this post, and others...
However, per this post, You can have multiple subscriptions that flow into the same collection.
// server
Meteor.publish('posts-current-user', function publishFunction() {
return BlogPosts.find({author: this.userId}, {sort: {date: -1}, limit: 10});
// this.userId is provided by Meteor - http://docs.meteor.com/#publish_userId
}
Meteor.publish('posts-by-user', function publishFunction(who) {
return BlogPosts.find({authorId: who._id}, {sort: {date: -1}, limit: 10});
}
// client
Meteor.subscribe('posts-current-user');
Meteor.subscribe('posts-by-user', someUser);
Now - I obtained my records via two different subscriptions, can I use the subscription to get to the records that it pulled back? Or must I requery my collection? What is the best practice for sharing that query between client and server?
I hope I'm not missing something obvious here, but executing the Meteor.subscribe function only for its side-effects seems to be losing a very useful piece of information - namely which subscription a record came from. Presumably the names of publications and subscriptions are chosen to be meaningful - it would be nice if I could get to records associated with that name.

What you seem to want to do is maintain two separate collections of records, where each collection is populated by a different publication. If you read the DDP specification, you'll see that the server tells the client which collection (not publication) each record belongs to, and multiple publications can actually provide different fields to the same record.
However, Meteor actually lets you send records to any arbitrary collection name, and the client will see if it has that collection. For example:
if (Meteor.isServer) {
Posts = new Mongo.Collection('posts');
}
if (Meteor.isClient) {
MyPosts = new MongoCollection('my-posts');
OtherPosts = new MongoCollection('other-posts');
}
if (Meteor.isServer) {
Meteor.publish('my-posts', function() {
if (!this.userId) throw new Meteor.Error();
Mongo.Collection._publishCursor(Posts.find({
userId: this.UserId
}), this, 'my-posts');
this.ready();
});
Meteor.publish('other-posts', function() {
Mongo.Collection._publishCursor(Posts.find({
userId: {
$ne: this.userId
}
}), this, 'other-posts');
this.ready();
});
}
if (Meteor.isClient) {
Meteor.subscribe('my-posts', function() {
console.log(MyPosts.find().count());
});
Meteor.subscribe('other-posts', function() {
console.log(OtherPosts.find().count());
});
}

This is what's happening:
Say that your server-side BlogPosts Mongo collection contains 500 posts from 10 different users. You then subscribe to two different subscriptions on the client:
Meteor.subscribe('posts-current-user'); // say that this has 50 documents
Meteor.subscribe('posts-by-user', someUser); // say that this has 100 documents
Meteor will see Meteor.subscribe('posts-current-user'); and proceed to download the posts of the current user to the client-side Mini-Mongo's BlogPosts collection.
Meteor will then see Meteor.subscribe('posts-by-user', someUser); and proceed to download the posts of someuser to the client-side Mini-Mongo's BlogPosts collection.
So now the client-side Mini-Mongo BlogPosts collection has 150 documents, which is a subset of the 500 total documents in the server-side BlogPosts collection.
So if you did BlogPosts.find().fetch().count in your client (Chrome Console) the result would be 150.

Of course! It just depends on where you write your subscriptions. In a lot of cases you might be using Iron Router, in which case you would have a given route subscribe to just the data that you need. Then from within that route template's helper you can only query documents within that subscription.
But the general idea is that you hook up a particular subscription to a particular template.
Template.onePost.helpers({
post: function() {
Meteor.subscribe('just-one-post', <id of post>);
return Posts.findOne();
}
});
Template.allPosts.helpers({
posts: function() {
Meteor.subscribe('all-posts');
return Posts.find();
}
));

Related

Meteor Publication and Subscription fetch all records after use skip and limit

We have use publication and subscription with skip and limit in meteor and it fetch all records at client side.
Meteor.publish('jobOrders', function(skip, limit) {
return (jobOrders.find({}, {
skip: skip,
limit: limit
}));
});
Template.templateName.onCreated(function() {
this.subscribe("jobOrders", 0, 2);
});
Template.templateName.helpers({
methodName: function() {
var totalNoOfRecords = jobOrders.find({}).count();
console.og("records are", totalNoOfRecords);
return totalNoOfRecords;
}
});
The reason could be because you have other pub-subs in other templates which are fetching the data from jobOrders collection.
Please comment your subscription in your current template and see whether you are still able to fetch the records from this collection. If you are seeing all the records even after commenting the subscription, it should be because of some other subscription.
To overcome this problem, you have to either do one of the following:
Change the other subscription which is fetching all the records such that the data is available only with in that template by using
Template.instance().subscribe()
instead of using Meteor.subscribe()which will make the data available throughout client side across all templates.
Filter you records on the client side as well.
Template.templateName.helpers({
methodName: function() {
var totalNoOfRecords = jobOrders.find({skip:0, limit: 2}).count(); //Filter on the client side
console.og("records are", totalNoOfRecords);
return totalNoOfRecords;
}
});
Note: Another possibility could be that you did not remove the insecure package which will automatically publish all the records in all the collection.

Meteor observeChanges vs manual update

I have a simple todo schema: (just a sample to draw my question)
{
title: {
type: string
},
value: {
type: string
},
author: {
type: object
},
"author._id": {
type: string
},
"author.firstName": {
type: string
},
"author.lastName": {
type: string
},
}
The author entries are from meteor.user. If the meteor user changes the firstName or lastName i have to update the todo. I have two possibilities:
observerChanges (server side) to users collection and update all todos from this user with the new firstname/lastname
if i call the user update method i can call a method to update all todos
when it's better to use cursor.observeChanges and when it's better to call a update method manual? And why?
As the comment says, you should not store the author name / email in the document if it is mutable:
Store the ID of the user only in the document, the UserID is immutable.
When building your ToDo template, look up the User information by ID: you would need to publish a Publication for user by Id, and subscribe to it on the client with the userId as parameter.
Meteor.publish('userById', function(userId) {
return Meteor.users.find({_id: userId}, {limit:1});
});
in your route / template.onCreated depending on your Router, assuming the document is called doc
this.subscribe('userById', this.doc.author._id);
in the template helper
Template.todoTemplate.helpers({
'Author': function() {
return Meteor.users.findOne({_id: this.doc.author._id});
}
});
and call the Author info in the template
<Template name="todoTemplate">
First Name: {{Author.first_name}}
Last Name: {{Author.last_name}}
</Template>
I think you shouldn't rely on the second method, because sometimes you (or your teammate) might forget to update it. Moreover, if you're denormalizing user data in other collections, users knowing Meteor might just call your Meteor.method or manipulate db from the browser console...
You can use this package:
meteor add matb33:collection-hooks
It adds some hooks to your mongo insert/update/remove call
For example:
Meteor.users.after.update(function (userId, doc, fieldNames, modifier, options) {
if (this.previous.firstName === doc.firstName && this.previous.lastName === doc.lastName) {
return;
}
Todos.update({'author._id': doc._id}, {
$set: {
'author.firstName': doc.firstName,
'author.lastName': doc.lastName,
}
})
}, {fetchPrevious: true})
(To update the Todos collection efficiently, make sure to add index to author field)
This is just a handier way than writing your own observeChanges, and better than manually updating Todos collection every time you update the users collection, because you might forgot to call it in some case, or some hacker user just calls Meteor.users.update(Meteor.userId(), {...}) perhaps...
But still, I think you should always add some auto-correct mechanism to avoid wrong data being displayed, because no matter which method you choose, some error will occur (maybe the server watching the db just crashes right after users update). You can check on the client side when displaying content, if author.firstName doesn't match Meteor.users.findOne(author._id) (but you have to publish the user though...), than call a method to tell the server to update it.

How to honour user privacy settings in Meteor

I have a set of users defined like this:
Accounts.createUser({
username:'Simon',
email:'simon#email.com',
profile:{
firstname:'Simon',
lastname:'Surname',
location:'Home Address',
privacy: {
location:0,
emails:0 } //Location and emails are private and should not be disclosed
}
});
My question is how can I publish this user's record for other users to view, taking into account the profile privacy settings. In this example, I have set the privacy for location and emails to zero with the intention that this information is not published for this user.
I would like to publish it using the standard method:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find();
});
But I cannot see a way to specify the selector or fields in such a way that only public information will be published.
I have tried adding additional publications of the form:
Meteor.publish("allUsers", function () {
return Meteor.users.find( {}, {fields:{username:1}} );
});
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, emails:1}} );
});
but the selector does not seem to be returning the emails as I expected. I am looking for optimal way to do this from a performance point of view.
Mongodb is not a relational database so whenever I want to join or query based on metadata I remember I have to do things differently. In your case I would make a separate Collection for user privacy if I wanted to query on user privacy. In addition, if I cared about performance I probably would never want "all of x", I would just want enough to show the user, thus paginate. With these two ideas in mind you can easily get what you want: query based on privacy settings and performance.
Privacy = new Mongo.Collection("privacy");
Whenever we want to add privacy to an account:
Privacy.insert({
emails: 1,
userId: account._id,
});
Then later, one page at a time, showing ten results each page, tracking with currentPage:
Meteor.publish("usersWithPublicEmails function (currentPage) {
var results = []
var privacyResults = Privacy.find({"emails":1}, {skip: currentPage,
limit: 10});
var result;
while (privacyResults.hasNext() ) {
result = privacyResult.next();
results.append(Meteor.users.find({_id: result.userId});
}
return result;
});
I didn't test this code, it may have errors, but it should give you the general idea. The drawback here is that you have to keep privacy and users in sync, but these are the kinds of problems you run into when you're not using a relational database.
Mongodb has a way to do this kind of reference lookup with less code, but it still happens on demand and I prefer the flexibility of doing it myself. If you're interested take a look at Database references
That's because you have a typo in your publish function's fields object, instead of email you've typed emails
So the correct function would be:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, email:1}} );
});
Furthermore, you're already publishing all usernames in your allUsers publication, therefore, in order to add the missing data for relevant public users, you'll just need this:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{email:1}} );
});
and Meteor will automatically merge those records for you.
A simple solution in the end. I had missed the additional subscription in my router:
Router.route('/users', {
name: 'userList',
waitOn: function(){
return Meteor.subscribe('allUsers') &&
Meteor.subscribe('usersWithPublicEmails');
},
data: function(){
return Meteor.users.find();
}
});
A basic mistake:-(

How do I use the results of a subscription for additional subscriptions at the route level?

I have a template that displays documents from three different collections Cars, CarPaints, and CarPaintTypes. I know I need all these upfront at the Router level. The template will show a Car document, all the CarPaints that reference that Car, and all the CarPaintTypes that reference the returned CarPaints respectively (think nested list). The route to the template takes an id from the URL that represents Car._id.
Both the Cars collection and CarPaints collection make use of the Car._id as a field (it's the native _id of the Cars collection and a field in the CarPaints collection) so that's easy. However, the CarPaintTypes uses the CarPaint._id as a reference to what CarPaint it belongs to.
So I have three publications:
Meteor.publish('car', function(carId) {
return Cars.find({_id: carId});
});
Meteor.publish('carPaints', function(carId) {
return CarPaints.find({carId: carId});
});
Meteor.publish('carPaintTypes', function(carPaintId) {
return CarPaintTypes.find({carPaintId: carPaintId});
});
My route looks like:
this.route('car', {
path: '/car/:_id',
waitOn: function() {
return [Meteor.subscribe('car', this.params._id),
Meteor.subscribe('carPaints', this.params._id)];
// Can't figure out how to subscribe to or publish
// the carPaintTypes using all the results of what gets
// returned by 'carPaints'
}
});
My question is CarPaintTypes doesn't have the Car._id as a field, just the CarPaint._id to reference to a CarPaint document. Where and how I do take the results of the subscription to carPaints and pass each carPaint document that's returned to a subscription to carPaintTypes? Or is there a way to combine them all in the publication? Is it better to do it later on in my helpers? I figure since I know what I need at the route level, all the subscription calls should be in the route code.
You can grab all 3 cursors inside Meteor.publish method and simply return them:
Meteor.publish('carThings', function(carId){
var carPaint = CarPaints.findOne({carId:carId});
return [
Cars.find({_id: carId}),
CarPaints.find({carId: carId}),
CarPaintTypes.find({carPaintId: carPaint._id});
]
})
On client:
this.route('car', {
path: '/car/:_id',
waitOn: function() {
return [Meteor.subscribe('carThings', this.params._id)]
}
}
With Kuba Wyrobek's help, I figured it out. For what I was trying to achieve, the publish looks like this:
Meteor.publish('carThings', function(carId){
var carPaints = CarPaints.find({carId: carId}).fetch();
return [
Cars.find({_id: carId}),
CarPaints.find({carId: carId}),
CarPaintTypes.find({carPaintId: {$in: _.pluck(carPaints, "_id")}})
];
});
I didn't get that you could do manipulations inside your publication blocks. This is super cool and flexible. Thanks for your help.

Publish documents in a collection to a meteor client depending on the existence of a specific document in another collection (publish-with-relations)

I have two collections
Offers (relevant fields: _id)
ShareRelations (relevant fields: receiverId and offerId)
and I'd like to publish only Offers to the logged in user which have been shared to him.
Actually, I'm doing this by using a helper array (visibleOffers) which I fill by looping for each ShareRelations and use this array later on the Offers.find as $in selector.
I wonder if this might be the meteor way to do this, or if I could do with less and/or prettier code?
My actual code to publish the Offers is the following:
Meteor.publish('offersShared', function () {
// check if the user is logged in
if (this.userId) {
// initialize helper array
var visibleOffers = [];
// initialize all shareRelations which the actual user is the receiver
var shareRelations = ShareRelations.find({receiverId: this.userId});
// check if such relations exist
if (shareRelations.count()) {
// loop trough all shareRelations and push the offerId to the array if the value isn't in the array actually
shareRelations.forEach(function (shareRelation) {
if (visibleOffers.indexOf(shareRelation.offerId) === -1) {
visibleOffers.push(shareRelation.offerId);
}
});
}
// return offers which contain the _id in the array visibleOffers
return Offers.find({_id: { $in: visibleOffers } });
} else {
// return no offers if the user is not logged in
return Offers.find(null);
}
});
Furthermore, the actual solution has the downside that if a new share relations is being created, the Offers collection on the client doesn't get updated with the newly visible offer instantly (read: page reload required. But I'm not sure if this is the case because of this publish method or because of some other code an this question is not primary because of this issue).
What you are looking for is a reactive join. You can accomplish this by directly using an observe in the publish function, or by using a library to do it for you. Meteor core is expected to have a join library at some point, but until then I'd recommend using publish-with-relations. Have a look at the docs, but I think the publish function you want looks something like this:
Meteor.publish('offersShared', function() {
return Meteor.publishWithRelations({
handle: this,
collection: ShareRelations,
filter: {receiverId: this.userId},
mappings: [{collection: Offers, key: 'offerId'}]
});
});
This should reactively publish all of the ShareRelations for the user, and all associated Offers. Hopefully publishing both won't be a problem.
PWR is a pretty legit package - several of us use it in production, and Tom Coleman contributes to it. The only thing I'll caution you about is that as of this writing, the current version in atmosphere (v0.1.5) has a bug which will result in a fairly serious memory leak. Until it gets bumped, see my blog post about how to run an updated local copy.
update 2/5/14:
The discover meteor blog has an excellent post on reactive joins which I highly recommend reading.
The way to do this is along the lines of this Question using observeChanges(). Still trying to figure out how to get it all working for my example, see Meteor, One to Many Relationship & add field only to client side collection in Publish?
You can use the reactive-publish package (I am one of authors):
Meteor.publish('offersShared', function () {
// check if the user is logged in
if (this.userId) {
this.autorun(function (computation) {
// initialize helper array
var visibleOffers = [];
// initialize all shareRelations which the actual user is the receiver
var shareRelations = ShareRelations.find({receiverId: this.userId}, {fields: {offerId: 1}});
// loop trough all shareRelations and push the offerId to the array if the value isn't in the array actually
shareRelations.forEach(function (shareRelation) {
if (visibleOffers.indexOf(shareRelation.offerId) === -1) {
visibleOffers.push(shareRelation.offerId);
}
});
// return offers which contain the _id in the array visibleOffers
return Offers.find({_id: { $in: visibleOffers } });
});
} else {
// return no offers if the user is not logged in
return Offers.find(null);
}
});
You can simply wrap your existing non-reactive code into an autorun and it will start to work. Just be careful to be precise which fields you query on because if you query on all fields then autorun will be rerun on any field change of ShareRelations, not just offerId.

Resources