Publishing/Subscribing same collection based on different Session value - javascript

I want to publish and subscribe subset of same collection based on different route. Here is what I have
In /server/publish.js
Meteor.publish("questions", function() {
return Questions.find({});
});
Meteor.publish("questionSummaryByUser", function(userId) {
var q = Questions.find({userId : userId});
return q;
});
In /client/main.js
Deps.autorun(function() {
Meteor.subscribe("questions");
});
Deps.autorun(function () {
Meteor.subscribe("questionSummaryByUser", Session.get("selectedUserId"));
});
I am using the router package (https://github.com/tmeasday/meteor-router). They way i want the app to work is when i go to "/questions" i want to list all the questions by all the users and when i visit "/users/:user_id/questions", I want to list questions only by specific user. For this I have setup the "/users/:user_id/questions" route to set the userid in "selectedUserId" session (which i am also using in "questionSummaryByUser" publish method.
However when i see the list of questions in "/users/:user_id/questions" I get all the questions irrespective of the user_id.
I read here that the collections are merged at client side, but still could not figure a solution for the above mentioned scenario.
Note that I just started with Meteor, so do not know in and outs of it.
Thanks in advance.

The good practice is to filter the collection data in the place where you use it, not rely of the subset you get by subscribe. That way you can be sure that the data you get is the same you want to display, even when you add further subscriptions to the same collection. Imagine if later you'd like to display, for example, a sidebar with top 10 questions from all users. Then you'd have to fetch those as well, and if you have a place when you display all subscribed data, you'll get a mess of every function.
So, in the template where you want to display user's questions, do
Template.mine.questions = function() {
return Questions.find({userId: Meteor.userId()});
};
Then you won't even need the separate questionSummaryByUser channel.
To filter data in the subscription, you have several options. Whichever you choose, keep in mind that subscription is not the place in which you choose the data to be displayed. This should always be filtered as above.
Option 1
Keep everything in a single parametrized channel.
Meteor.publish('questions', function(options) {
if(options.filterByUser) {
return Questions.find({userId: options.userId});
} else {
return Questions.find({});
}
});
Option 2
Make all channel return data only when it's needed.
Meteor.publish('allQuestions', function(necessary) {
if(!necessary) return [];
return Questions.find({});
});
Meteor.publish('questionSummaryByUser', function(userId) {
return Questions.find({userId : userId});
});
Option 3
Manually turn off subcriptions in the client. This is probably an overkill in this case, it requires some unnecessary work.
var allQuestionsHandle = Meteor.subscribe('allQuestions');
...
allQuestionsHandle.stop();

Related

Mongoose save() not saving changes

I have a fully functioning CRUD app that I'm building some additional functionality for. The new functionality allows users to make changes to a list of vendors. They can add new vendors, update them and delete them. The add and delete seem to be working just fine, but updating doesn't seem to be working even though it follows a similar method I use in the existing CRUD functionality elsewhere in the app. Here's my code:
// async function from AXIOS request
const { original, updatedVendor } = req.body;
let list = await Vendor.findOne({ id: 1 });
if (!list) return res.status(500).json({ msg: 'Vendors not found' });
let indexOfUpdate = list.vendors.findIndex(
(element) => element.id === original.id
);
list.vendors[indexOfUpdate].id = updatedVendor.id;
list.vendors[indexOfUpdate].name = updatedVendor.name;
const updated = await list.save();
res.json(updated);
The save() isn't updating the existing document on the DB side. I've console logged that the list.vendors array of objects is, indeed, being changed, but save() isn't doing the saving.
EDIT:
A note on the manner of using save, this format doesn't work either:
list.save().then(res.json(list));
EDIT 2:
To answer the questions about seeing the logs, I cannot post the full console.log(list.vendors) as it contains private information, however, I can confirm that the change made to the list is showing up when I run the following in the VendorSchema:
VendorSchema.post('save', function () {
console.log(util.inspect(this, { maxArrayLength: null }));
});
However, the save still isn't changing the DB side.
Since you are using nested objects, Mongoose will not be able to detect the changes made. You need to mark the modified as an object before the save
list.markModified('vendors');

Firebase Realtime Database listener inside a loop is not working properly

I am using Firebase Realtime Database. I have an object which has all the posts created by all our users. This object is huge.
In order to display the posts in a fast way, we have given each user an object with relevant post IDs.
The structure looks like this:
/allPosts/$postID/
: { $postID: {id: $postID, details: '', title: '', timestamp: ''} }
/user/$userID/postsRelevantToThisUser/
: { $postID: {id: $postID} }
'postsRelevantToThisUser' only contains the IDs of the posts. I need to iterate over each of these IDs and retrieve the entire post information from /allPosts/
As a result, the client won't have to download the entire allPosts object and the app will be much faster.
To do this, I've written the below code. It is successfully retrieving and rendering only the relevant posts. Whenever a new postID is added or removed from /postsRelevantToThisUser/ in Firebase Realtime Database, React Native correctly re-renders the list.
However, when anything in /allPosts/$postID changes, for exampe: if title parameter changes, it is not reflected in the view.
What's a good way to solve this problem?
let userPostRef = firebase.database().ref(`/users/${uid}/postsRelevantToThisUser`)
userPostRef.on('value', (snapshot) => {
let relPostIds = [];
let posts = [];
snapshot.forEach(function(childSnapshot) {
const {id} = childSnapshot.val();
relPostIds.push(id);
})
relPostIds.map(postId => {
firebase.database().ref(`allPosts/${postId}`).on('value', (postSnapshot) => {
let post = postSnapshot.val()
posts.push(post);
this.setState({ postsToRender:posts });
})
})
Since you've spread the data that you need to show the posts to the user over multiple places, you will need to keep listeners attached to multiple places if you want to get realtime updates about that data.
So to listen for title updates, you'll need to keep a listener to each /allPosts/$postID that the user can currently see. While it can be a bit finicky in code to keep track of all those listeners, they are actually quite efficient for Firebase itself, so performance should be fine up to a few dozen listeners at least (and it seems unlikely a user will be actively reading more post titles at once).
Alternatively, you can duplicate the information that you want to show in the list view, under each user's /user/$userID/postsRelevantToThisUser nodes. That way you're duplicating more data, but won't need the additional listeners.
Either approach is fine, but I have a personal preference for the latter, as it keeps the code that reads the data (which is the most critical for scalability) simpler.

Standard pattern in Meteor for searching and displaying documents

Say I have a Meteor site where I want a function to search for a document in a collection and display it to the user.
This collection is very large and is not feasible to keep all on the client.
I have a publish statement that specifies what documents need to be pushed to the user.
Meteor.publish('listing', function (search) {
return TestObjs.find({"name": search });
});
And I have a search input textbox which when it changes I'd like to subscribe to the publication to get the records that I need.
'change #searchText' : function(e,t){
Session.set("searchTerm", $("#searchText").val());
Meteor.subscribe("listing", $("#searchText").val());
}
So every time I search for text, client is getting sent the documents it needs to be displayed.
The problem is, the subscribe call doesn't delete any old documents from the client for previous search terms. So the first problem is the more the user uses the search function, the more documents are going to be stored on the client. Secondly, if I want to display the search results for the current search term, I cannot just go
Template.home.helpers({
listOfObjs: function () {
return TestObjs.find().fetch();
}
});
This will return all record on the client which would be all records that match the current and any previous search term.
I have to replicate the publish statement
Template.home.helpers({
listOfObjs: function () {
return TestObjs.find({"name": Session.get("searchTerm") }).fetch();
}
});
This works, but I'm essentially duplicating the logic in multiple places. This is just a simple example but what if my search algorithm was more complicated with paging etc.
So I was wondering, what is the recommended Meteor way to return search results for collections that sit on the server?
Answer without try. Hope you looking for this.
var list;
'change #searchText' : function(e,t){
Session.set("searchTerm", $("#searchText").val());
if (list)
list.stop();
list = Meteor.subscribe("listing", $("#searchText").val());
}
From the doc
stop() Cancel the subscription. This will typically result in the server directing the client to remove the subscription's data from the client's cache.

Ember.js adds elements to collection "magically"

Question related somewhat to: Ember.js: retrieve random element from a collection
I've two routes: randomThing route and things route.
The former displays a... random thing from an API (GET /things/random) (there is a button to "Get another random thing"), the latter: displays all things: (GET /things).
The problem is that EVERY TIME when I click on Get another random thing and new thing is displayed and I go to recipes route this newly displayed random thing is added to the collection...
Action to get random thing performs a find("random") as suggested in related question and sets this.content to this value.
What is wrong here?
EDIT:
I'm using ember-data and my route is like this:
App.ThingsRoute = Ember.Route.extend({
model: function() {
return App.Thing.find();
}
});
The problem is that EVERY TIME when I click on Get another random thing and new thing is displayed and I go to recipes route this newly displayed random thing is added to the collection...
This is expected behavior. App.Thing.find() does not simply query the api and return results. Instead find() returns an array containing of all Things ember knows about. It includes objects returned by past calls to find(), objects created client-side via App.Thing.createRecord(), and of course individual objects queried via App.Thing.find('random'). After returning this array, find() and kicks off another API call and if that returns additional records they are pushed onto the array.
What is wrong here?
It does not sound like anything is wrong per-se. If you want to prevent random things from showing up in the ThingsRoute, you'll need to change that route's model to be a filter instead of just returning every Thing. For example:
App.ThingsRoute = Ember.Route.extend({
model: function() {
//Kick off query to fetch records from the server (async)
App.Thing.find();
//Return only non-random posts by applying a client-side filter to the posts array
return App.Thing.filter(function(hash) {
if (!hash.get('name').match(/random/)) { return true; }
});
}
});
See this jsbin for a working example
To learn more about filters I recommend reading the ember-data store-model-filter integration test

backbone.js cache collections and refresh

I have a collection that can potentially contain thousands of models. I have a view that displays a table with 50 rows for each page.
Now I want to be able to cache my data so that when a user loads page 1 of the table and then clicks page 2, the data for page 1 (rows #01-50) will be cached so that when the user clicks page 1 again, backbone won't have to fetch it again.
Also, I want my collection to be able to refresh updated data from the server without performing a RESET, since RESET will delete all the models in a collection, including references of existing model that may exist in my app. Is it possible to fetch data from the server and only update or add new models if necessary by comparing the existing data and the new arriving data?
In my app, I addressed the reset question by adding a new method called fetchNew:
app.Collection = Backbone.Collection.extend({
// fetch list without overwriting existing objects (copied from fetch())
fetchNew: function(options) {
options = options || {};
var collection = this,
success = options.success;
options.success = function(resp, status, xhr) {
_(collection.parse(resp, xhr)).each(function(item) {
// added this conditional block
if (!collection.get(item.id)) {
collection.add(item, {silent:true});
}
});
if (!options.silent) {
collection.trigger('reset', collection, options);
}
if (success) success(collection, resp);
};
return (this.sync || Backbone.sync).call(this, 'read', this, options);
}
});
This is pretty much identical to the standard fetch() method, except for the conditional statement checking for item existence, and using add() by default, rather than reset. Unlike simply passing {add: true} in the options argument, it allows you to retrieve sets of models that may overlap with what you already have loaded - using {add: true} will throw an error if you try to add the same model twice.
This should solve your caching problem, assuming your collection is set up so that you can pass some kind of page parameter in options to tell the server what page of options to send back. You'll probably want to add some sort of data structure within your collection to track which pages you've loaded, to avoid doing unnecessary requests, e.g.:
app.BigCollection = app.Collection.extend({
initialize: function() {
this.loadedPages = {};
},
loadPage: function(pageNumber) {
if (!this.loadedPages[pageNumber]) {
this.fetchNew({
page: pageNumber,
success: function(collection) {
collection.loadedPages[pageNumber] = true;
}
})
}
}
});
Backbone.Collection.fetch has an option {add:true} which will add models into a collection instead of replacing the contents.
myCollection.fetch({add:true})
So, in your first scenario, the items from page2 will get added to the collection.
As far as your 2nd scenario, there's currently no built in way to do that.
According to Jeremy that's something you're supposed to do in your App, and not part of Backbone.
Backbone seems to have a number of issues when being used for collaborative apps where another user might be updating models which you have client side. I get the feeling that Jeremy seems to focus on single-user applications, and the above ticket discussion exemplifies that.
In your case, the simplest way to handle your second scenario is to iterate over your collection and call fetch() on each model. But, that's not very good for performance.
For a better way to do it, I think you're going to have to override collection._add, and go down the line dalyons did on this pull request.
I managed to get update in Backbone 0.9.9 core. Check it out as it's exactly what you need http://backbonejs.org/#Collection-update.

Categories