Privacy & Subscriptions

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

You may have noticed that all available parties were always shown on the page at the time, regardless of whether they had been added by a logged-in user or anonymously.

In future versions of our app, we'll probably want to have a RSVP feature for parties, which should only display:

  • public parties
  • parties the current user has been invited to

In this step we are going to learn how we can restrict data flow from the server side to the client side for only desired data in Meteor, based on the user currently logged-in and some other parameters.

Autopublish

First we need to remove the autopublish Meteor package.

autopublish is added to any new Meteor project. Like it was mentioned before, it pushes a full copy of the database to each client. It helped us rapidly develop our app, but it's not so good for privacy...

Write this command in the console:

meteor remove autopublish

Now run the app. Oops, nothing on the page!

Meteor.publish

Data in Meteor is published from the server and subscribed to by the client.

We need to tell Meteor what parties we want transferred to the client side.

To do that we will use Meteor's publish function.

Publication functions are placed inside the "server" folder so clients won't have access to them.

Let's create a new file inside the "server/imports/publications" folder called parties.ts. Here we can specify which parties to pass to the client.

10.2 Publish Parties collection server/imports/publications/parties.ts
1
2
3
4
import { Meteor } from 'meteor/meteor';
import { Parties } from '../../../both/collections/parties.collection';
 
Meteor.publish('parties', () => Parties.find());

As you can see, parameters of the Meteor.publish are self-explanatory: first one is a publication name, then there goes a function that returns a Mongo cursor, which represents a subset of the parties collection that server will transfer to the client.

The publish function can take parameters as well, but we'll get to that in a minute.

We've just created a module, as you already know, one necessary thing is left — to import it in the main.ts in order to execute code inside:

10.3 Add publication to server-side entry point server/main.ts
2
3
4
5
6
7
8
9
 
import { loadParties } from './imports/fixtures/parties';
 
import './imports/publications/parties'; 
 
Meteor.startup(() => {
  loadParties();
});

Meteor.subscribe

Meteor.subscribe is the receiving end of Meteor.publish on the client.

Let's add the following line to subscribe to the parties publications:

MeteorObservable.subscribe('parties').subscribe();

Note that the first subscribe belongs to Meteor API, and the return value in this case is an Observable because we used MeteorObservable. The second subscribe is a method of Observable.

When using MeteorObservable.subscribe, the next callback called only once - when the subscription is ready to use.

But beyond that simplicity there are two small issues we'll need to solve:

1) Ending a subscription.

Each subscription means that Meteor will continue synchronizing (or in Meteor terms, updating reactively) the particular set of data, we've just subscribed to, between server and client. If the PartiesList component gets destroyed, we need to inform Meteor to stop that synchronization, otherwise we'll produce a memory leak.

In this case, we will use OnDestroy and implement ngOnDestroy, and we will use the Subscription object returned from our Observable - so when the Component is destroyed - the subscription will end.

2) Updating Angular 2's UI

In order to connect Angular 2 and Meteor, we will use a special Observable operator called zone() - we use it on the Collection query object, so when the collection changes - the view will update.

So, we are going to extend the PartiesListComponent component and make use of MeteorObservable.subscribe:

10.4 Subscribe to Parties publication client/imports/app/parties/parties-list.component.ts
1
2
3
4
5
6
7
 
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component, OnInit, OnDestroy } from [email protected]/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { MeteorObservable } from 'meteor-rxjs';
 
import { Parties } from '../../../../both/collections/parties.collection';
import { Party } from '../../../../both/models/party.model';
...some lines skipped...
  selector: 'parties-list',
  template
})
export class PartiesListComponent implements OnInit, OnDestroy {
  parties: Observable<Party[]>;
  partiesSub: Subscription;
 
  ngOnInit() {
    this.parties = Parties.find({}).zone();
    this.partiesSub = MeteorObservable.subscribe('parties').subscribe();
  }
 
  removeParty(party: Party): void {
    Parties.remove(party._id);
  }
 
  ngOnDestroy() {
    this.partiesSub.unsubscribe();
  }
}

Now run the app. Whoa, we've just made all the parties come back using pub/sub!

As it's mentioned earlier, it'd be nice for the app to implement some basic security and show parties based on who owns them. Let's do it.

Firstly, we'll add a new public field to the party data through several steps:

Add UI with the new "Public" checkbox to the right of the "Location" input;

10.5 Add checkbox for public property client/imports/app/parties/parties-form.component.html
7
8
9
10
11
12
13
14
15
 
  <label>Location</label>
  <input type="text" formControlName="location">
 
  <label>Public</label>
  <input type="checkbox" formControlName="public">
  
  <button type="submit">Add</button>
</form>

Update Party in both/models/party.model.ts to include public: boolean;

10.6 Update Party interface both/models/party.model.ts
4
5
6
7
8
9
  name: string;
  description: string;
  location: string;
  owner?: string;
  public: boolean;
}

Change the initial data on the server in parties.ts to contain the public field as well:

10.7 Update parties initials server/imports/fixtures/parties.ts
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    const parties: Party[] = [{
      name: 'Dubstep-Free Zone',
      description: 'Can we please just for an evening not listen to dubstep.',
      location: 'Palo Alto',
      public: true
    }, {
      name: 'All dubstep all the time',
      description: 'Get it on!',
      location: 'Palo Alto',
      public: true
    }, {
      name: 'Savage lounging',
      description: 'Leisure suit required. And only fiercest manners.',
      location: 'San Francisco',
      public: false
    }];
 
    parties.forEach((party: Party) => Parties.insert(party));

Now, let's add the public field to our Form declaration:

10.8 Add public property to form builder client/imports/app/parties/parties-form.component.ts
21
22
23
24
25
26
27
28
    this.addForm = this.formBuilder.group({
      name: ['', Validators.required],
      description: [],
      location: ['', Validators.required],
      public: [false]
    });
  }
 

Next, we are limiting data sent to the client. A simple check is to verify that either the party is public or the "owner" field exists and is equal to the currently logged-in user:

10.9 Limit data sent to the client server/imports/publications/parties.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Meteor } from 'meteor/meteor';
import { Parties } from '../../../both/collections/parties.collection';
 
Meteor.publish('parties', function() {
  const selector = {
    $or: [{
      // party is public
      public: true
    },
    // or
    { 
      // current user is the owner
      $and: [{
        owner: this.userId 
      }, {
        owner: {
          $exists: true
        }
      }]
    }]
  };
 
  return Parties.find(selector);
});

$or, $and and $exists names are part of the MongoDB query syntax. If you are not familiar with it, please, read about them here: $or, $and and $exists.

We also need to reset the database since schema of the parties inside is already invalid:

meteor reset

Let's check to make sure everything is working.

Run the app again, and you will see only two items. That's because we set the third party to be private.

Log in with 2 different users in 2 different browsers. Try to create a couple of public parties and a couple of private ones. Now log out and check out what parties are shown. Only public parties should be visible.

Now log in as a party owner user and verify that a couple of private parties got to the page as well.

Subscribe with Params

There is one page in our app so far where we'll need parameterized publishing — the PartyDetails component's page. Besides that, let's add another cool feature to Socially, party search by location.

As you already know, the second parameter of Meteor.publish is a callback function that can take a variable number of parameters, and these parameters are passed by the user to Meteor.subscribe on the client.

Let's elaborate our "party" publication on the server. We want to publish both a list of parties and a single party.

10.10 Add party publication server/imports/publications/parties.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { Parties } from '../../../both/collections/parties.collection';
 
Meteor.publish('parties', function() {
  return Parties.find(buildQuery.call(this));
});
 
Meteor.publish('party', function(partyId: string) {
  return Parties.find(buildQuery.call(this, partyId));
});
 
 
function buildQuery(partyId?: string): Object {
  const isAvailable = {
    $or: [{
      // party is public
      public: true
...some lines skipped...
    }]
  };
 
  if (partyId) {
    return {
      // only single party
      $and: [{
          _id: partyId
        },
        isAvailable
      ]
    };
  }
 
  return isAvailable;
}

Looks like a lot of code, but it does something powerful. The privacy query, we introduced above, was moved to a separate method called buildQuery. We'll need this function to avoid repetition with each different parties query.

Notice that we used buildQuery.call(this) calling syntax in order to make context of this method the same as in Meteor.publish and be able to use this.userId inside that method.

Let's subscribe to the new publication in the PartyDetails to load one specific party:

10.11 Subscribe to the party publication client/imports/app/parties/party-details.component.ts
1
2
3
4
5
6
7
8
 
19
20
21
22
23
24
25
 
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 
58
59
60
61
62
63
import { Component, OnInit, OnDestroy } from [email protected]/core';
import { ActivatedRoute } from [email protected]/router';
import { Subscription } from 'rxjs/Subscription';
import { Meteor } from 'meteor/meteor';
import { MeteorObservable } from 'meteor-rxjs';
 
import 'rxjs/add/operator/map';
 
...some lines skipped...
  partyId: string;
  paramsSub: Subscription;
  party: Party;
  partySub: Subscription;
 
  constructor(
    private route: ActivatedRoute
...some lines skipped...
    this.paramsSub = this.route.params
      .map(params => params['partyId'])
      .subscribe(partyId => {
        this.partyId = partyId;
        
        if (this.partySub) {
          this.partySub.unsubscribe();
        }
 
        this.partySub = MeteorObservable.subscribe('party', this.partyId).subscribe(() => {
          this.party = Parties.findOne(this.partyId);
        });
      });
  }
 
...some lines skipped...
 
  ngOnDestroy() {
    this.paramsSub.unsubscribe();
    this.partySub.unsubscribe();
  }
}

We used MeteorObservable.subscribe with the parameter we got from the Router params, and do the same logic of OnDestroy.

Note that in this case, we use the Subscription of the MeteorObservable.subscribe, in order to know when the subscription is ready to use, and then we used findOne to get the actual object from the Collection.

Run the app and click on one of the party links. You'll see that the party details page loads with full data as before.

Search

Now it's time for the parties search. Let's add a search input and button to the right of the "Add" button. We are going to extend the PartiesList component since this features is related to the parties list itself:

1
2
3
4
5
6
7
8
<div>
  <parties-form style="float: left"></parties-form>
  <input type="text" #searchtext placeholder="Search by Location">
  <button type="button" (click)="search(searchtext.value)">Search</button>
  
  <login-buttons></login-buttons>
 
  <ul>

As you may have guessed, the next thing is to process the button click event:

25
26
27
28
29
30
31
32
33
34
    Parties.remove(party._id);
  }
 
  search(value: string): void {
    this.parties = Parties.find(value ? { location: value } : {}).zone();
  }
 
  ngOnDestroy() {
    this.partiesSub.unsubscribe();
  }

Notice that we don't re-subscribe in the search method because we've already loaded all parties available to the current user from the published parties, so we just query the loaded collection.

Understanding Publish-Subscribe

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 subscriptions of the same collection in the client, so adding a different subscription in a different view won't delete the data that is already in the client.

Please, read more about Pub/Sub in Meteor here.

Summary

In this step we've clearly seen how powerful Meteor and Angular 2 are and how they become even more powerful when used together. With rather few lines of code, we were able to add full privacy to Socially as well as search capabilities.

Meanwhile, we've learned about:

  • the importance of removing autopublish;
  • the Publish-Subscribe mechanism in Meteor;
  • how to query particular data from the database via the server side.

In the next step, we'll look at how quick and easy it is to deploy your Meteor app.