Publishing different versions of the same collections

Hello everyone,
I’m trying to understand better meteor pub/sub.
I was wondering what’s happens exactly if

  • I subscribe to two different publications with 2 different names
  • each acting on the same collection,
  • but not return the same values ?

I understand it should not happen, but there is no security against it, so I guess … it could happen…
Does it happen to publish different versions of the same collections ?
What do you think ?

Thanks!

This actually happens all the time, and this is a good thing. Think about a list of contacts, for example. The left hand side of the screen is a list of all of your contacts, the right hand side of the screen shows the details for the selected contact.

You’d want two publications – the list publication would probably just ask for basic fields – name, email, profile photo. The detail publication would ask for all of the fields – name, email, profile photo, phone number, address, etc.

On the client side, those data are merged together, so oftentimes, you will have client-side queries that mimic what you are asking the server for in a publication. That way, overlapping publications and data sets can still be query-controlled.

Another good example is a shopping cart. In your shopping cart publication, you want to have all of the items that the user has added to his/her cart. Let’s say they added a pair of jeans. Now they are looking at the t-shirts section of your site. You have a pub/sub that gets all of the t-shirt items. But if you did something like Items.find(), you would see that your jeans could potentially show up in your t-shirts section. So when you’re looking at t-shirts on the client, you would want something like Items.find({ type: 'tshirt' }), which is probably similar to what your publication query looks like.

You will need to anticipate these overlaps, as they can and will occur much more frequently than one might expect!

2 Likes

Thanks for your quick answer.
I understand the need of having several publications of the same collection. That’s why I was testing it.
As subscribes are not directly linked to the data you use, how to, in the same code, subscribe to both publications and use both results ?
I mean, if I do:
this.susbcribe(“tasks”);
this.susbcribe(“tasks_all_details”);
this.susbcribe(“tasks_images_only”);

What’s happening ? I guess it should not happen.
What’ts the proper meteor way to handle this ?

Let’s continue with the contact list idea. You can see in the helpers that you have to mimic parts of the server-side query on the client in order to limit the data to exactly what you want.

The data that you subscribe to in the same collection will be merged together. For example, if you put {{contact.phoneNumber}} in the contact list, then you would only see that phoneNumber for the selectedContact, not for all of the contacts in the list.

<template name="main">
  <ul class="contact-list">
    {{#each contact in contacts}}
      <li class="js-contact-list-item" data-id="{{contact._id}}">
        {{contact.firstName}} {{contact.lastName}}
      </li>
    {{/each}}
  </ul>
  <div class="contact-detail">
    {{#if selectedContact}}
      <span>{{selectedContact.firstName}} {{selectedContact.lastName}}</span>
      <span>{{selectedContact.phoneNumber}}</span>
      <span>{{selectedContact.email}}</span>
    {{else}}
      <span>No contact selected.</span>
    {{/if}}
  </div>
</template>

Our client js:

Template.main.onCreated(function() {
  this.selectedContactId = new ReactiveVar(false);

  this.autorun(() => {
    this.subscribe('contactList');
    const selectedContactId = this.selectedContactId.get();
    if (selectedContactId) {
      this.subscribe('contactDetail', selectedContactId);
    }
  });
});

Template.main.helpers({
  contacts() {
    return Contacts.find();
  },
  selectedContact() {
    const selectedContactId = Template.instance().selectedContactId.get();
    if (!selectedContactId) {
      return false;
    }
    return Contacts.findOne({ _id: selectedContactId });
  },
});

Template.main.events({
  'click .js-contact-list-item'(e, i) {
    i.selectedContactId.set(e.currentTarget.dataset.id);
  },
});

And server:

Meteor.publish('contactList', () => {
  return Contacts.find({}, { 
    fields: {
      firstName: 1,
      lastName: 1,
    },
 });
});

Meteor.publish('contactDetail', (contactId) => {
  return Contacts.find({
    _id: contactId,
  }, {
    fields: {
      firstName: 1,
      lastName: 1,
      email: 1,
      phoneNumber: 1,
    },
  });
});

Like @vigorwebsolutions mentioned, if there are several publications from the same collection, the data will get merged. For example, if your ‘tasks’ subscription gives you the following one document from Tasks collection:

{
_id: 1,
desc: “buy food”,
}

And at the same time your ‘tasks_all_details’ subscription gives you the following (from the same document in the same collection):

{
_id: 1,
detail1: “important task 1”,
detail2: “important task 2”,
detail3: “important task 3”
}

Then Meteor will see this client side (notice, the fields are merged):

{
_id: 1,
desc: “buy food”,
detail1: “important task 1”,
detail2: “important task 2”,
detail3: “important task 3”
}

However there is one specific issue here that you will sooner or later stumble onto that might be worth mentioning here - Meteor (intentionally, by the way) reliably only merges top level fields in collections. This is something to keep in mind, especially considering that common nosql use (i.e. outside of Meteor) strongly encourages the use of subfields.

So, in this case, if your ‘tasks’ subscription gives you the following:

{
_id: 1,
info: { desc: “buy food” }
}

And at the same time your ‘tasks_all_details’ subscription gives you the following:

{
_id: 1,
detail1: “important task 1”,
detail2: “important task 2”,
detail3: “important task 3”,
info: { priority: “asap” }
}

Then Meteor might see this client side:

{
_id: 1,
detail1: “important task 1”,
detail2: “important task 2”,
detail3: “important task 3”,
info: { desc: “buy food” }
}

…or it might see this (notice the subfields in the ‘info’ field are nor merged, Meteor just picks one or the other - either ‘priority’ of ‘desc’ under the ‘info’ top level field):

{
_id: 1,
detail1: “important task”,
detail2: “important task”,
detail3: “important task”,
info: { priority: “asap” }
}

By the way, hello to everyone. Been reading these forums daily since 2015. Decided to finally make an account.

2 Likes

HI everyone!

I’m following this thread because matches with my current issue…

In my case I have one Mongo Collection for tv series and two server publication methods:

1.- One publication method is for searching records and outputs filtered by a searchterm query.

2.- Second publication method is for output the most new series available on tv.

My current problem is related to merging. In my client subscriptions I have one for search results and other for new series available but only in my searc results subscription is getting the data from the newseries publication, why this happenning if each subscription is paried with each publication. Shall I use another way to avoid this merging…

First:
Inside… SimpleSearchResults.js I have subsSeries subscribing to moviedb.series.search but I’m getting merged results from NewSeries publication… explained down below…

import SeriesCol from '../../../api/Series/series';


const SimpleSearchResults = ({ loading, seriesList, peliculasList, searchTerm }) => (

	<div className="search-list">

[... react component user search output ...]

	</div>
);

SimpleSearchResults.propTypes = {
  loading: PropTypes.bool.isRequired,
  seriesList: PropTypes.arrayOf(PropTypes.object).isRequired,
	peliculasList: PropTypes.arrayOf(PropTypes.object).isRequired,
	searchTerm: PropTypes.string.isRequired
};

export default createContainer(( { searchQuery } ) => {

	const subsSeries = Meteor.subscribe('moviedb.series.search',searchQuery.get());
	const subsPeliculas = Meteor.subscribe('moviedb.peliculas.search',searchQuery.get());
	let subsReady = (subsSeries.ready() && subsSeries.ready()) ? true : false;

	return {
		loading: !subsReady,
		seriesList: SeriesCol.find().fetch(),
		peliculasList: PeliculasCol.find().fetch(),
		searchTerm: searchQuery.get()
	};

}, SimpleSearchResults);

Second:

Inside imports/api/Series/server/publications.js I am publishing two types of outputs from the same mongo db collection series

import series from '../series';

Meteor.publish('moviedb.series.search',(searchTerm) => {

	check(searchTerm, Match.OneOf(String, null, undefined));
	let query = {};
	const projection = {limit: 1, sort: { name:1 } };

	if(searchTerm){
		const regex = new RegExp(searchTerm, 'i');
		query = {
			$or: [
					{name: regex},
					{original_name: regex}
			],
		};
		projection.limit = 10;
	}
	return series.find(query, projection);

});


Meteor.publish('moviedb.series.NewSeries', () => {

  let gte_date = `${currentYear}-${currentMonth}-1`;
  let lt_date = `${currentYear}-${currentMonth}-${daysInMonth}`;

  let query = {
    first_air_date: {
      $gte: firstDateOfMonth,
      $lt: lastDateOfMonth
    },
    vote_average: {
      $gte: 6.0,
      $lt: 10.0,
    }
  };
	const projection = {limit: 40, sort: {  popularity: -1 } };

  return series.find(query, projection);
});

Third

… in NewSeries.js component I’m subscribing to ‘moviedb.series.NewSeries’ and it works properly.

import { Meteor } from 'meteor/meteor';
import React from 'react';
import PropTypes from 'prop-types';

import { createContainer } from 'meteor/react-meteor-data';

import Loading from '../../components/Loading/Loading';
import SeriesCol from '../../../api/Series/series';

const NewSeries = ({ loading, sectionTitle, seriesList}) => (
  <section>
    <h1>{sectionTitle} - Resultados: {seriesList.length} </h1>
    <ul>
      {seriesList.map( ({ id, name, first_air_date, popularity, vote_average}) => (
        <li key={id}>
          {name} - {first_air_date} - popularity: {popularity} - vote_average: {vote_average}
        </li>
      ))}
    </ul>
  </section>
);

NewSeries.propTypes = {
  sectionTitle: PropTypes.string.isRequired,
};


export default createContainer(() => {

	const subs = Meteor.subscribe('moviedb.series.NewSeries');

	return {
		loading: !subs.ready(),
		seriesList: SeriesCol.find().fetch()
	};

}, NewSeries);

How could I avoid getting merged results from both publications ‘moviedb.series.NewSeries’ & ‘moviedb.series.search’ ??
… I would like to do not get newseries query inside my search output… sorry for the length of this post I’m pretty sure this is a really newbie issue for pub/sub design patterns.

Thanks in advance for any advise!!!
Roger

Dear meteor mates,

I’ve solved the issue finally just using .observeChanges in server side like this:

SERVER

Meteor.publish('moviedb.series.search', function searchSeries(searchTerm) {

  check(searchTerm, Match.OneOf(String, null, undefined));

	if(searchTerm){
      let query = {};
  		const regex = new RegExp(searchTerm, 'i');
      const projection = {limit: 1, sort: { name:1 } };
  		query = {
  			$or: [
  					{name: regex},
  					{original_name: regex}
  			],
  		};
  		projection.limit = 5;

      let cursor = series.find(query,projection);
      let handle = cursor.observeChanges({
         added: (id, fields) => {
           this.added('seriesSearch', id, fields);
         },
         changed: (id, fields) => {
           this.changed('seriesSearch', id, fields);
         },
         removed: (id) => {
           this.removed('seriesSearch', id);
         }
       });

     this.ready();
     this.onStop(function() {
       handle.stop();
     });
  }
});

CLIENT

Subscribing to moviedb.series.search but finding and fetching inside the secondary Mongo.collection named seriesSearch instead of main collection SeriesCol…

import SeriesCol from '../../../api/Series/series';
const seriesSearch = new Mongo.Collection('seriesSearch');

const SimpleSearchResults = ({ loading, seriesList, peliculasList, searchTerm }) => (

	<div className="search-list">
[...]

</div>
);


SimpleSearchResults.propTypes = {
  loading: PropTypes.bool.isRequired,
  seriesList: PropTypes.arrayOf(PropTypes.object).isRequired,
	peliculasList: PropTypes.arrayOf(PropTypes.object).isRequired,
	searchTerm: PropTypes.string.isRequired
};

export default createContainer(( { searchQuery } ) => {

	const subsSeries = Meteor.subscribe('moviedb.series.search',searchQuery.get());
	const subsPeliculas = Meteor.subscribe('moviedb.peliculas.search',searchQuery.get());
	let subsReady = (subsSeries.ready() && subsSeries.ready()) ? true : false;

	return {
		loading: !subsReady,
		seriesList: seriesSearch.find().fetch(),
		peliculasList: PeliculasCol.find().fetch(),
		searchTerm: searchQuery.get()
	};

}, SimpleSearchResults);

1 Like

@codiwans I tried your solution but it did not work. Is this functional in Meteor 1.8?