Publications and subscriptions are one of Meteor's most powerful features. It will take care of privacy and make sure that you don't to access someone else's information unless you have permissions to do so. You can look at it as a replacement for a RESTful API. More information and a deeper look at Meteor's publication and subscription system can be found here.
So back to our app implementation, why is this even relevant? Because our app doesn't have any privacy. Each user can see all parties available on database, a behavior which we're not interested in. But let's set an exception, an exception, a party which is flagged as party
can be viewed by anyone.
By default, a newly created Meteor project will be initialized with an autopublish
package, a package which will publish all datasets. This is a good practice for development, not for production. So the first thing we should before implementing any publication would be typing the following command in the terminal:
meteor remove autopublish
Obviously, if you will refresh the app now you will see no parties, because the auto-publication has been removed and you have access to non of them. To set a new publication we will use a method called Meteor.publish(). The publication function is relevant only to the server, because it's its job to publish the data, and the user should subscribe to that data, but no need to jump the gun, we will get to it in a glance.
Let's create a new file located under imports/api/parties/publish.js
where all our parties publications are going to be defined. Here's how the initial publication should look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Meteor } from 'meteor/meteor';
import { Parties } from './collection';
if (Meteor.isServer) {
Meteor.publish('parties', function() {
const selector = {
$or: [{
// the public parties
$and: [{
public: true
}, {
public: {
$exists: true
}
}]
}, {
// when logged in user is the owner
$and: [{
owner: this.userId
}, {
owner: {
$exists: true
}
}]
}]
};
return Parties.find(selector);
});
}
The first parameter of a publication should be its name and the second parameter should be it's handler. A publication should always return a cursor. That cursor we determine which data will be available to our client once he subscribes to that publication. More information about Mongo.Collection.prototype.find()
can be found here.
The returned cursor is returned by a Mongo query so it's easy to understand if you're familiar with MongoDB's API, but in our case what the query does it basically fetches all the parties owned by the currently logged in user and it fetches all the public parties, since they are relevant to the logged in user as well regardless of who he is.
Now we will move the parties collection file from parties.js
to parties/collection.js
so our code can be organized properly and we won't have party files spread all over:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Mongo } from 'meteor/mongo';
export const Parties = new Mongo.Collection('parties');
Parties.allow({
insert(userId, party) {
return userId && party.owner === userId;
},
update(userId, party, fields, modifier) {
return userId && party.owner === userId;
},
remove(userId, party) {
return userId && party.owner === userId;
}
});
Now we want existing importations of the parties collection to stay the same, so we will make our parties
dir export the parties collection whenever we import it by creating the following file:
1
2
3
import './publish';
export * from './collection';
Our parties publication is finally set. Let's go ahead and add a subscription to the parties
dataset in the client using a function provided to us by angular-meteor
called this.subscribe():
13
14
15
16
17
18
19
20
$reactive(this).attach($scope);
this.subscribe('parties');
this.helpers({
parties() {
return Parties.find({});
The first argument of the subscription is gonna be the name of its belonging publication and the rest of the arguments are gonna be the parameters sent to the publication's handler. More information about Meteor.subscribe()
can be found here.
If you remember, our publication had an exception of parties which are public. Right no there is no functionality which reveals public parties in the view, in which case we will have to update it. We will start by adding a check-box to the PartyAdd
view representing if the party currently being added is public or not:
7
8
9
10
11
12
13
14
15
Description:
</label>
<input type="text" ng-model="partyAdd.party.description" />
<label>
Public Party?
</label>
<input type="checkbox" ng-model="partyAdd.party.public">
<button ng-click="partyAdd.submit()">Add Party!</button>
</form>
Notice how easy it is to bind the view to a model when using Angular's API. Let's apply the same additions to the PartyDetails
component.
2
3
4
5
6
7
8
<form>
Party name: <input type="text" ng-model="partyDetails.party.name" />
Description: <input type="text" ng-model="partyDetails.party.description" />
Public Party? <input type="checkbox" ng-model="partyDetails.party.public">
<button ng-click="partyDetails.save()">Save</button>
</form>
This requires us to update its controller as well.
30
31
32
33
34
35
36
37
}, {
$set: {
name: this.party.name,
description: this.party.description,
public: this.party.public
}
}, (error) => {
if (error) {
And of course, don't forget to subscribe to the parties
dataset:
15
16
17
18
19
20
21
22
this.partyId = $stateParams.partyId;
this.subscribe('parties');
this.helpers({
party() {
return Parties.findOne({
Now we can run our app and test it. To do so, log in with two different accounts, create new parties and mess around with it. To log in with two different accounts we recommend opening two instances of the browser, once of them is gonna be incognito, and them you can log in without interrupting the other account.
In the next step, we will want to invite users to private parties. For that, we will need to get all the users, but only their emails without other data which will hurt their privacy.
So let's create another publish method for getting only the needed data on the user.
Notice the we don't need to create a new Meteor collection like we did with parties. Meteor.users is a pre-defined collection which is defined by the meteor-accounts package.
So let's start with defining our publish function.
Create a new file under the api
folder named users.js
and place the following code in:
1
2
3
4
5
6
7
8
9
10
11
12
import { Meteor } from 'meteor/meteor';
if (Meteor.isServer) {
Meteor.publish('users', function() {
return Meteor.users.find({}, {
fields: {
emails: 1,
profile: 1
}
});
});
}
And make it available on the server side:
1
2
3
import '../imports/startup/fixtures';
import '../imports/api/parties';
import '../imports/api/users';
So here again we use the Mongo API to return all the users (find with an empty object) but we select to return only the emails and profile fields.
_id
field.The emails field holds all the user's email addresses, and the profile might hold more optional information like the user's name (in our case, if the user logged in with the Facebook login, the accounts-facebook package puts the user's name from Facebook automatically into that field).
Now let's subscribe to that publish Method. In the PartyDetails
component file add the following line inside the controller:
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
this.partyId = $stateParams.partyId;
this.subscribe('parties');
this.subscribe('users');
this.helpers({
party() {
return Parties.findOne({
_id: $stateParams.partyId
});
},
users() {
return Meteor.users.find({});
}
});
}
users
publicationusers
collectionNow let's add the list of users to the view to make sure it works.
Add this ng-repeat list to the end of the template:
7
8
9
10
11
12
13
14
15
16
</form>
<button ui-sref="parties">Back</button>
<ul>
Users:
<li ng-repeat="user in partyDetails.users">
<div>{{ user.emails[0].address }}</div>
</li>
</ul>
1
2
3
4
12
13
14
15
16
17
18
19
20
21
22
25
26
27
28
29
30
31
32
49
50
51
52
53
54
55
56
57
58
59
60
import { Meteor } from 'meteor/meteor';
import { name as PartyAdd } from '../partyAdd';
import { Parties } from '../../../../api/parties';
import 'angular-mocks';
...some lines skipped...
let controller;
const party = {
name: 'Foo',
description: 'Birthday of Foo',
public: true
};
const user = {
_id: 'userId'
};
beforeEach(() => {
...some lines skipped...
$scope: $rootScope.$new(true)
});
});
spyOn(Meteor, 'userId').and.returnValue(user._id);
});
describe('reset()', () => {
...some lines skipped...
});
it('should insert a new party', () => {
expect(Parties.insert).toHaveBeenCalledWith({
name: party.name,
description: party.description,
public: party.public,
owner: user._id
});
});
it('should call reset()', () => {
12
13
14
15
16
17
18
19
41
42
43
44
45
46
47
48
const party = {
_id: 'partyId',
name: 'Foo',
description: 'Birthday of Foo',
public: true
};
beforeEach(() => {
...some lines skipped...
expect(Parties.update.calls.mostRecent().args[1]).toEqual({
$set: {
name: party.name,
description: party.description,
public: party.public
}
});
});
Run the app and see the list of all the users' emails that created a login and password and did not use a service to login.
Note that the structure of the Users collection is different between regular email-password, Facebook, Google etc.
The Document structure looks like this (notice where the email is in each one):
Email-Password
:
{
"_id" : "8qJt6dRSNDHBuqpXu",
"createdAt" : ISODate("2015-05-26T00:29:05.109-07:00"),
"services" : {
"password" : {
"bcrypt" : "$2a$10$oSykELjSzcoFWXZTwI5.lOl4BsB1EfcR8RbEm/KsS3zA4x5vlwne6"
},
"resume" : {
"loginTokens" : [
{
"when" : ISODate("2015-05-26T00:29:05.112-07:00"),
"hashedToken" : "6edmW0Wby2xheFxyiUOqDYYFZmOtYHg7VmtXUxEceHg="
}
]
}
},
"emails" : [
{
"address" : "[email protected]",
"verified" : false
}
]
}
Facebook
:
{
"_id" : "etKoiD8MxkQTjTQRY",
"createdAt" : ISODate("2015-05-25T17:42:16.850-07:00"),
"services" : {
"facebook" : {
"accessToken" : "CAAM10fSvI...",
"expiresAt" : 1437770457288.0000000000000000,
"id" : "10153317814289291",
"email" : "[email protected]",
"name" : "FirstName LastName",
"first_name" : "FirstName",
"last_name" : "LastName",
"link" : "https://www.facebook.com/app_scoped_user_id/foo"
"gender" : "male",
"locale" : "en_US"
},
"resume" : {
"loginTokens" : []
}
},
"profile" : {
"name" : "First Name LastName"
}
}
Google
:
{
"_id" : "337r4wwSRWe5B6CCw",
"createdAt" : ISODate("2015-05-25T22:53:32.172-07:00"),
"services" : {
"google" : {
"accessToken" : "ya29.fwHSzHvC...",
"idToken" : "eyJhbGciOiJSUzI1NiIs...",
"expiresAt" : 1432624691685.0000000000000000,
"id" : "107497376789285885122",
"email" : "[email protected]",
"verified_email" : true,
"name" : "FirstName LastName",
"given_name" : "FirstName",
"family_name" : "LastName",
"picture" : "https://lh5.googleusercontent.com/-foo.jpeg"
"locale" : "en",
"gender" : "male"
},
"resume" : {
"loginTokens" : [
{
"when" : ISODate("2015-05-25T23:18:11.788-07:00"),
"hashedToken" : "NaKS2Zeermw+bPlMLhaihsNu6jPaW5+ucFDF2BXT4WQ="
}
]
}
},
"profile" : {
"name" : "First Name LastName"
}
}
Right now it means that the emails of the users that logged in with with email-password will be displayed.
In the chapter of Angular 1 filters we will change the display code to show all emails.
It is very important to understand Meteor's Publish-Subscribe mechanism so you don't get confused and use it to filter things in the view!
Meteor accumulates all the data from the different subscription of a collection in the client, so adding a different subscription in a different view won't delete the data that is already in the client.
More information about publications and subscription in this blog article and this meteorpedia article.
We've added the support of privacy to our parties app.
We also learned how to use the Meteor.publish
command to control permissions and the data sent to the client
and how to subscribe to it with the $meteor.collection subscribe function.
In the next step we will learn how to deploy. You will see that Meteor makes it easy to put the application online.