Subscribe collection based on url parameters in meteor - meteor

I am trying to achieve a dynamic subscription of collection data based on the parameter in the url.
On Client side, I have the following subscription code in main.js file.
cSubscribe = Meteor.subscribe('cPublish', Session.get('param1'));
On Server, I have the following publish code in main.js file.
Test = new Meteor.Collection('test');
Meteor.publish('cPublish', function(param1) {
return Test.findOne({_id: param1});
});
In the router, I am setting the url parameter value in the Session, Session.set('param1', value); and when I try cSubscribe.ready(), it is returning false. Until the subscription is ready I am showing a loading template.
The route snippet,
'/test-url/:value': function(value) {
Session.set('param1', value);
if (cSubscribe.ready()) {
//some code
} else {
return 'loading';
}
}
What is wrong with the process ? Is there any better way of achieving dynamic subscription ?

First, I hope, you use iron-router (-:
In router, for example, in onBeforeAction() you have to set Session.set('param1', value);
Then on client try to use smth like this:
Meteor.startup(function() {
Deps.autorun(function() {
var param = Session.get("param1");
if(param) {
cSubscribe = Meteor.subscribe('cPublish', param);
});
});
In other words, you have to resubscribe after url changed.

Related

Meteor: Access Functions from Client, Run on Server

I'm attempting to create a program where I use the Steam API. I want to be able to call the method to retrieve a user's info from the client, while keeping the actual code of the method secret from the client, since it contains an API Key. I tried defining the methods as global in a server folder, like this:
key = 'xxxxxxxxxxxxxxxx';
Meteor.steamFunctions = {
getName: function(user){
var userSteamId = user.profile.id;
Meteor.http.get('http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' + key + '&steamids=' + userSteamId, function(error, resultJSON){
if (error){
return 'Error in Steam API';
} else {
var json = JSON.parse(resultJSON);
return json.personaname;
}
})
},
getPic: function(user){
var userSteamId = user.profile.id;
Meteor.http.get('http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' + key + '&steamids=' + userSteamId, function(error, resultJSON){
if (error){
return 'Error in Steam API';
} else {
var json = JSON.parse(resultJSON);
return json.avatarfull;
}
})
}
}
I then try to call it like this in a client-side script:
if (Meteor.isClient){
Template.profile.helpers({
'getName': function(){
return Meteor.steamFunctions.getName(Meteor.user());
}
});
}
That, however, throws
Exception in template helper: TypeError: Cannot read property 'getName' of undefined
at Object.Template.profile.helpers.getName
How can I go about keeping the key secret to the user while still accessing the data?
Well, it is not quite as simple as adding a property to the Meteor global. Also, the remote method/call API to do this will involve asynchronous code.
Put the call to the API, with the secret API key, on the server side in code only visible on the server, e.g. the ./server subdirectory. Define a Meteor.method on the server side that can be called with Meteor.call on the client side.
In the server side Meteor method there are method security checks you can make to check for a logged in user or userid, and use this to decide whether to make the calls or ignore the request. You can throw a new Meteor.Error from the server side if a request is improper or there is an error, but these take resources to communicate.
The thing to understand about Meteor is that it has nothing magical to change how Javascript behaves on the browser or the server. The server is ultimately running nodejs. Objects defined on the server do not magically migrate to the client, or vice versa. If an object is defined on both, it is actually two separate pieces of code.
Therefore, in the client code, the Meteor.call to call the server-side code from the browser... is actually using an existing websocket or ajax API that is asynchronous in nature. This means that you will need to structure client code to provide callback functions on the browser to handle the asynchronously returned results of looking up Name or Pic. A direct return and imperative coding style is not possible.
Typically you'll want to update something on a user's screen as a result of information returned from a lookup. The usual Meteor coding is to have the callback function update a session global variable with Session.set(). Templates can reference these session variables, and through an implied or explicit Tracker.autorun(), the screen can be updated when the API returns the data.
You need to:
Move your steamFunctions into methods which are defined only on the server.
Properly invoke the methods from the client.
Below is some example code based on your original question. Please note this has not been tested and may require some tweaking.
server/methods.js
const KEY = 'xxxxxxxxxxxxxxxx';
const URL = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002';
Meteor.methods({
getName() {
const userSteamId = Meteor.user().profile.id;
const params = {
key: KEY,
steamids: userSteamId,
};
try {
var result = HTTP.get(URL, { params });
// Double check this - I have no idea what this API returns. The value
// you want may be nested under result, like result.data or something.
return JSON.parse(result).personaname;
} catch (e) {
// Something bad happened - maybe throw an error.
return false;
}
},
});
Note this method is defined on the server, so we don't expose our KEY to the client. Also note we are using the synchronous version of the HTTP api, so the value can be returned to the client.
client/lib/user.js
Tracker.autorun(function () {
user = Meteor.user();
if (user && user.profile && user.profile.id) {
Meteor.call('getName', (err, name) => {
Session.set('steamName', name);
});
} else {
Session.set('steamName', '');
}
});
When the user logs is or is updated, get the steam name and set a global session variable.
client/templates/profile.js
Template.profile.helpers({
getName: function () {
return Session.get('steamName');
},
});
Read the steamName session variable for use in your template.

Can Meteor preserve object references between server and client?

Is it possible to preserve pointers (object references) when you send data from the server to the client with Meteor? My initial tests say No, but perhaps there is another way to do this.
Here are server-side and client-side scripts:
Server.js
Meteor.methods({
test: function test() {
var object = {
key: ['value']
}
var output = {
object: object
, reference: object.key
}
console.log(output.reference[0])
output.object.key[0] = "changed"
console.log(output.reference[0])
return output
}
})
Server output:
// value
// changed
Client.js
Meteor.call("test", testCallback)
function testCallback(error, data) {
if (!error) {
console.log(data.reference)
data.object.key[0]= "edited"
console.log(data.reference)
}
}
Client output in the console:
// changed
// changed
No, this is not possible, although subscriptions do something similar. If your data is not in a collection, you can still publish it (see this.added and friends in the Meteor docs), and the data will show up in a collection in the client.

Publishing data from external API in Meteor

For publishing a list of items coming from an external API, I followed the this tutorial: so I've a publication on the server called "items" and a collection with the same name on the client; iron-router is the trait d'union between them. So far so good.
Now I'm stuck in implementing the "item detail" route (e.g. /item/:id). I wrote a server method like this:
Meteor.methods({
'getItem': function(id) {
check(id, Match.Integer);
var self = this;
var asyncCall = Meteor.wrapAsync(requestToThirdParty);
// requestToThirdParty is local function which calls HTTP.get
var response = asyncCall('GET', id);
return response.data;
}
});
I don't know if it's the best way to do, but I'm wondering how to call this method from the following route:
Router.route('/items/:id', {
name: 'itemDetail',
data: function() {
var item = Meteor.call('getItem', this.params.id); // this should be sync
console.log('item: '+item);
return item;
}
});
I'm sure that the method on server works fine (I debugged), but the console log in the client always shows "undefined".
What am I missing?
Are there any other approaches?

AngularJS - refresh view after http request, $rootScope.apply returns $digest already in progress

I am simply trying to load data when my app starts. However, the view loads faster than the http request(of course). I want to refresh my view once my data has been properly loaded because that data defines my view.
I've tried $rootScope.apply from inside the factory where I do my http request, and I also tried directly doing the http request in my controller again with $scope.apply, and neither one worked as they both gave me "$digest already in progress"
Any idea how can I set up my code to make my views refresh on data load? I will be having several different http requests and I would like to know how to set them up properly! I would really appreciate any input!
Here is some of the code I am working with.
app.factory('HttpRequestFactory', function($http, $q) {
var HttpRequestFactory = {
async: function(url, params) {
var deferred = $q.defer();
$http({
url: url,
method: post,
params: params
})
.success(function(data, status, headers, config) {
deferred.resolve(data);
})
.error(function(data, status, headers, config) {
deferred.reject("An error occurred");
});
return deferred.promise;
}
};
return HttpRequestFactory;
});
Factory
function initializeAll(){
HttpRequestFactory.async('../api', {action: 'getall'}).then(function(data) {
//$rootScope.$apply(function () {
allData = data;
//});
angular.forEach(allData, function(value, index){
console.log('Voala!');
});
});
}
Controller calling the factory's function initializeAll()
app.controller("MainController", ["$scope", "$rootScope","MyFactory",
function($scope, $rootScope, MyFactory){
MyFactory.initializeAll();
}
]);
Oh my !
You got the f** matter with AngularJS !
In fact you have to do a "safeApply" like that for example :
$rootScope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
In AngularJS you can only have one $apply or $digest loop at the same time.
For details on these loops look at the docs :
http://docs.angularjs.org/guide/concepts
It will explain what is the $apply loop and you'll understand a lot of things about the two-way-data-binding in AngularJS
Hope it helps.
Don't use $apply: use $watch.
Calling $apply is (almost) always the wrong thing to do. The only time you should ever be calling it is if you've triggered a change outside of an 'angular' method; here, since the trigger occurs in an angular $http request, you can't call $apply because it's already being done at that moment by the $http block. Instead, what you want to do is $watch.
Official Doc for $scope.$watch() here
This will let you watch an object and update whenever it changes. I assume that your view is based on allData and you want it to update immediately; if you're using an ng method, then the watch is automatically setup for you and no more work should be needed. If you're using allData yourself inside a controller, you can write the watch in that controller like this:
$scope.$watch(function thingYouWantToWatch(){
return <accessor call to allData here>;
},
function whatToDoOnChange(newValue, oldValue){
$scope.myCoolThing = newValue; //this is the newValue of allData, or whatever you're watching.
}
);

In a node.js application, how can I asynchronously achieve the effect of a rails controller before_filter?

I have a node.js Express application with a basic user authentication system where the session is stored in redis. I'd like to write and re-use a current_user() function that populates a JSON representation of the logged in user (if there is one) in a current_user variable. Here is the code to retrieve the user:
if( req.cookie('fingerprint') ) {
redis_client.get("users:fingerprint:"+req.cookie("fingerprint"), function(err,id) {
redis_client.get("users:id:"+id, function(err,user) {
current_user = JSON.parse(user);
})
})
}
if I only wanted to use this once, I could put a res.render call after that current_user line, but I'd like to populate the current_user variable for all controllers and actions.
I'm new to node, so there's probably a fundamental concept I'm missing here...but basically I need a way to return the current_user variable from above in a synchronous context or achieve the same effect asynchronously. For example,
app.get('/', function(req,res) {
current_user = current_user();
res.render('home', {layout: "layout.ejs", locals: {current_user: current_user}})
})
Thanks.
Express routes accept a third argument: next, this allows you to pass control to the next matching route. You could use this to build up before filters based on routes, like:
app.get('/admin', function(req, res, next){
if(authenticated_user()) {
req.user = get_user();
next();
}
else // prompt for login
});
app.get('/admin/:id', function(req, res){
// foo
}
Hope this helps, for more information see Passing Route Control in the official Express guide
update
A DRYer way to achieve the same using the oh-so-cool Route Middleware
function checkAuthentication(req, res, next){
if(authenticated_user()) {
req.user = get_user();
next();
}
else // prompt for login
}
app.get('/admin/:id', checkAuthentication, function(req, res){
// foo
});
Why don't you just use dynamicHelpers?
firstly, store your current user in a session. Since you are using redis, check out something like the connect-redis for express. Then store it in session like so:
if( req.cookie('fingerprint') ) {
redis_client.get("users:fingerprint:"+req.cookie("fingerprint"), function(err,id) {
redis_client.get("users:id:"+id, function(err,user) {
req.session.user = JSON.parse(user);
})
})
}
Make it available as a dynamicHelper:
app.dynamicHelpers({
current_user: function(req) {
return req.session.user;
}
});
now current_user is available to all your route handlers as well as the views!
app.get('/admin/:id', function(req, res){
if(current_user) {
//do what you want here...
}
});
on login, just regenerate the session like so:
req.session.regenerate(function() {
// same as above!
req.session.user = JSON.parse(user);
});
on logout, destroy the session:
req.session.destroy(function() {
res.redirect('/');
});

Resources