I'm sorry, this definitely has been answered before. I especially know about this thread here:
How do I return the response from an asynchronous call?
But to be honest, I'm still lost. I simply do not understand how to call a function asynchronously and include the return of this function in the response to the client.
I have a rather simple route (in routes.js) which shall provide content to a web page. A specific element of the site is a list of rfid tags. This list is populated from a function, which, of course I'd like and must call asynchron.
I'm using pug (formerly known as jade) as the template render engine and my tags.pug looks like this:
extends index.pug
block content
h2!= subheadline
p!= messagetext
p!= bodyContent
See the last p!=bodyContent? This element shall be the list of tags.
Below is a code snippet from my routes.js with the app.get('/tags') in which this site shall be returned:
// get the listing of all stored rfid tags
app.get("/tags", function(req, res) {
res.render('tags', {
title: 'RFID Tag Startseite',
headline: 'RFID Tag Startseite',
subheadline: 'Verfügbare Tags',
messagetext: 'Bitte ein RFID Tag auswählen, um mehr Daten angezeigt zu bekommen',
bodyContent: tagController.getTagList(app, function(tagController))
});
})
Now, as you can see, I'm trying to call the function (getTagList) at the position bodyContent from a different module (tagController) and this shall provide the list of tags.
Following you can see the code of my getTagList() function:
var getTagList = function(app, result){
// get global app variables
var DEBUG = app.get('DEBUG');
var svrAddr = app.get('svrAddr');
var rfidTagDir = app.get('rfidTagDir');
var responseContent = '';
fs.readdir(rfidTagDir, function(err, items) {
responseContent = "{\'response\': \'info\', \'message\': \'list of all stored rfid tags\', \'tags\': ["
for (i in items) {
var tag = items[i].toString().substring(0,items[i].indexOf('.'));
responseContent += "{\'tag\': \'" + tag + "\', \'endpoint\': \'"+svrAddr+"/tags/tag/" + tag + "\', \'file\': \'"+items[i]+"\'}"
if (i<items.length-1) responseContent += ",";
}
responseContent += "]}"
if (DEBUG) console.log(responseContent);
result = responseContent;
return result;
)};
My problem is, that it doesn't work. The code above gives the error:
SyntaxError: Unexpected token )
For the last of the two ). If I try to populate a variable (say tagContent or whatever) prior to the res.send and include that variable to the response, I simply do not see the result of the function call. In the console.log, however, I can see the function being executed and the list of tags generated.
Can someone please, please, please tell me, how to call my function getTagList from module tagController from within my routes.js so that the content is displayed???
Kind regards,
Christian
So the problem is that getTagList will execute an async operation no matter what you are trying to return its not guaranteed that its correct ... so to be able to handle this you pass a callback function to getTagList which will be called inside the callback function of the async fs.readdir this is where you are 100% sure you have the correct result expected.
So below is the updated getTagList function
var getTagList = function(app, callback) {
// get global app variables
var DEBUG = app.get('DEBUG');
var svrAddr = app.get('svrAddr');
var rfidTagDir = app.get('rfidTagDir');
var responseContent = '';
fs.readdir(rfidTagDir, function(err, items) {
if (err) {
callback(err);
} else {
responseContent = "{\'response\': \'info\', \'message\': \'list of all stored rfid tags\', \'tags\': ["
for (i in items) {
var tag = items[i].toString().substring(0, items[i].indexOf('.'));
responseContent += "{\'tag\': \'" + tag + "\', \'endpoint\': \'" + svrAddr + "/tags/tag/" + tag + "\', \'file\': \'" + items[i] + "\'}"
if (i < items.length - 1) responseContent += ",";
}
responseContent += "]}"
if (DEBUG) {
console.log(responseContent);
}
//Here we are 100% sure we have the correct expected result so we call the callback function with correct data `responseContent`
callback(null, responseContent);
}
)
};
}
Also you update your route to send the request only when data is available which is inside the callback function of getTagList, check code below
app.get("/tags", function(req, res) {
tagController.getTagList(app, function(err, result) {
if (err) {
console.log(err);
//Handle error response
} else {
res.render('tags', {
title: 'RFID Tag Startseite',
headline: 'RFID Tag Startseite',
subheadline: 'Verfügbare Tags',
messagetext: 'Bitte ein RFID Tag auswählen, um mehr Daten angezeigt zu bekommen',
bodyContent: result
});
}
});
})
You can also notice how the callback functions are implemented in node ... we pass any error to the first argument, and the result to the second argument .. to have a better maintainable code.
Finally you can see the same concept is actually used in the fs.readdir that already exist in your code you will see that you are using a callback function to be able to access items correctly.
Related
I've been trying to modify the sample dashboard widget at this location
https://learn.microsoft.com/en-us/vsts/extend/develop/add-dashboard-widget?view=vsts#part-2-hello-world-with-vsts-rest-api
However, reluctantly have to admit I simply can't understand the structure required to extend it
Near the end, it uses "load: function" and returns the outputs of a REST API call, which I can consume however I want
However, I need to make more than one different REST call, and I simply cannot figure out how to get that info usable in my function
I modified the code so it starts like this:
VSS.require(["TFS/Dashboards/WidgetHelpers", "TFS/Work/RestClient","VSS/Service", "TFS/WorkItemTracking/RestClient" ],
I then created a handle for the other call I want to make like this:
var queryClient = VSS_Service.getCollectionClient(TFS_Wit_QueryAPI.WorkItemTrackingHttpClient);
var queryResults = queryClient.getQuery(projectId, "Shared Queries/My Bugs");
However, I cannot consume the contents of queryResults - I know it's working up to a point as if I put in an invalid URL it will error as it knows it can't access anything there. If the URL is correct, no matter what I've tried - even stringify just to see what comes back - I get 'undefined' or something similar (it's definitely a valid JavaScript object)
The key seems to be right at the end when you have "load: function" except that only allows one thing to be returned? The reason I know this is if I change the function that it returns to be the one I've written rather than the one from the sample, it works fine - but the problem remains the same in that I can only process the results of one API call.
You can call more than one APIs, the code in that article is just the simple sample.
For Widget extension, you just need to return the status (e.g. Success()) in load function, so you can return status at the end of the function. For example:
var getQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Feedback")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
var getAnOhterQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Bug")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
return {
load: function (widgetSettings) {
// Set your title
var $title = $('h2.title');
$title.text('Hello World');
var r1= getQueryInfo(widgetSettings);
var r2=getAnOhterQueryInfo(widgetSettings);
if(r1==true && r2==true){
return WidgetHelpers.WidgetStatusHelper.Success();
}else{
return WidgetHelpers.WidgetStatusHelper.Failure("failed, check error in console");
}
}
Im working on a NodeJs app that takes an event from FB and puts it into a local database. For every event in the first page of the api query this goes well, except for the last one.
I am getting the following error:
[December 1st 2016, 1:48:39 pm] TypeError: Cannot read property 'name'
of undefined
at IncomingMessage. (/home/node/virgo/app.js:217:32)
at IncomingMessage.emit (events.js:129:20)
at _stream_readable.js:907:16
at process._tickCallback (node.js:372:11)
Just after
console.log(resultBody);
Code:
function addFBEvent(facebookId){
console.log("getting event: " + facebookId);
var options = {
hostname: 'graph.facebook.com',
port: 443,
path: '/v2.8/'+facebookId+'?fields=name,description,start_time,end_time,place,photos{images}&access_token={INSERT API ACCESS CODE HERE}',
method: 'GET'
};
https.request(options, function(res2) {
var resultBody = "";
res2.setEncoding('utf8');
res2.on('data', function (chunk) {
resultBody = resultBody + chunk;
});
res2.on('end', function () {
dbConnection = sql.createConnection({
host : settings.dbHost,
user : settings.dbUser,
password : settings.dbPassword
});
dbConnection.connect(function(err){
if(!err) {
console.log("Database is connected ... nn");
} else {
console.log("Error connecting database ... nn");
}
});
var json = JSON.parse(resultBody);
console.log(resultBody);
if (json != undefined){
var eventName = json.name;
var eventStart = json.start_time;
var eventEnd = json.end_time;
var eventDescription = json.description;
var eventPlace = json.place.name;
var eventPoster = json.photos.data[json.photos.data.length-1].images[0].source;
var eventId = json.id;
console.log("name: " + eventName + ", start: " + eventStart + ", end: " + eventEnd + ", place: " + eventPlace + ", Poster: " + eventPoster);
//console.log("Description: " + eventDescription);
dbConnection.query('INSERT INTO SVVirgo.activities(title, description, image, start, end, price, location, facebook) VALUES ("'+eventName+'","'+eventDescription+'","'+eventPoster+'","'+eventStart+'","'+eventEnd+'",0,"'+eventPlace+'","'+eventId+'")', function (err, result){
});
}
dbConnection.end();
})
}).end();
}
See graph api explorer for the breaking event: Graph API
Event code: 1682486658666506
I've tried catching the undefined json object with no luck, i've also tried to validate the json using this solution: Stackoverflow validate JSON javascript
Instead of https.request try using request.
It will give you parsed JSON and you won't have to do it manually.
If you want to do it manually like you do, then remember to wrap var json = JSON.parse(resultBody); in a try/catch block (or use tryjson) because the JSON.parse cannot be used on unknown data outside of a try/catch - it can throw exceptions.
Another thing, don't open you database connection in your route handlers. You should open the connection once and just use it in your handlers. See this answer for more info about it.
Right now you are connecting to the database but you continue outside of the connection callback, so you run all the lines beginning from var json = JSON.parse(resultBody); before the DB connection is established.
Additionally, the error may be not because json is undefined but because json.place is undefined.
You can change this:
var eventPlace = json.place.name;
to this:
var eventPlace = json.place && json.place.name;
You must also check json.photos before you access json.photos.data and test if json.photos.data is an array before you treat it as such (you can do it with:
if (json.photos && Array.isArray(json.photos.data)) {
// ...
}
Basically, you need to make sure the values are what you want them to be before you access them. For example accessing this:
json.photos.data[json.photos.data.length-1].images[0].source
can fail when json is undefined, when json.photos is undefined, when json.photos.data is undefined, when json.photos.data is not an array, when json.photos.data.length is zero, when json.photos.data[json.photos.data.length-1] is undefined, when json.photos.data[json.photos.data.length-1].images is undefined, when json.photos.data[json.photos.data.length-1].images is not an array, when json.photos.data[json.photos.data.length-1].images is an array but it's empty, or when json.photos.data[json.photos.data.length-1].images is a non-empty array but json.photos.data[json.photos.data.length-1].images[0].source is undefined.
As you can see there are a lot of assumptions that you are doing here. When those assumptions are not met, the code will fail.
I'm trying to access a variable named city in the Meteor Accounts.onCreateUser user creation function and have not been successful in my attempts to do so. I have tried to return city at the end of the HTTP.get call and have also tried creating the city var outside of HTTP.get and simply setting city without using var but none of these things seems to have worked. When console.log(city) runs it does accurately output the desired information so this variable must not be the issue. If I am making a nube mistake forgive me.
Accounts.onCreateUser( function (options, user) {
if (options.profile) {
options.profile.picturelrg = "http://graph.facebook.com/" + user.services.facebook.id + "/picture/?type=large";
user.profile = options.profile;
options.profile.picturesm = "http://graph.facebook.com/" + user.services.facebook.id + "/picture/?type=small";
options.profile.messenger = "https://www.messenger.com/t/" + user.services.facebook.id;
HTTP.get("http://ipinfo.io", function (error, result) {
var place = JSON.parse(result.content);
var city = place.city;
console.log(city);
});
options.profile.city = city;
}
return user;
});
The technically correct repose to this is that you need to use the synchronous version of HTTP.get. See the docs and this question for examples.
However there's a more fundamental issue: you're trying to grab the user's location data but onCreateUser only runs on the server. So even if you do solve this problem, you'll end up with the same data in each user's profile. You'll need to run the get on the client and update via a method. Try something like this:
Accounts.createUser(options, function(err) {
if (!err) {
HTTP.get(..., function(err, result) {
var city = ...;
var place = ...;
Meteor.call('updateProfileWithCityandPlace', city, place);
});
}
});
Meteor.methods({
updateProfileWithCityandPlace: function(city, place) {
check(city, String);
check(place, String);
Meteor.users.update(this.userId, {
$set: {'profile.city': city, 'profile.place': place}
});
}
});
I'm having some trouble understanding asynchronous functions. I've read the chapter in Mixu's Node Book but I still can't wrap my head around it.
Basically I want to request a ressource (using the node package cheerio), parse it for valid URLs and add every match to my redis set setname.
The problem is that in the end it's only adding the first match to the redis set.
function parse(url, setname)
{
request(url, function (error, response, body)
{
if (!error && response.statusCode == 200)
{
$ = cheerio.load(body)
// For every 'a' tag in the body
$('a').each(function()
{
// Add blog URL to redis if not already there.
var blog = $(this).attr('href')
console.log("test [all]: " + blog);
// filter valid URLs
var regex = /http:\/\/[^www]*.example.com\//
var result = blog.match(regex);
if(result != null)
{
console.log("test [filtered]: " + result[0]);
redis.sismember(setname, result[0], function(err, reply)
{
if(!reply)
{
redis.sadd(setname, result[0])
console.log("Added " + result[0])
}
redis.quit()
})
}
})
}
})
}
I'd be very grateful for pointers on how I'd have to restructure this so the redis.sadd method is working with the correct result.
The output of the current implementation looks like:
test [all]: http://test1.example.com/
test [filtered]: http://test1.example.com/
...
Added http://test2.example.com/
So it's adding the test1.example.com but not printing the "added" line, and it's not adding the test2.example.com but it's printing the "added" line for it.
Thank you!
The first issue is caused by redis.sismember() being asynchronous: when its callback is called, you have already overwritten the result variable so it will point to the last value it had, and not the value at the moment at which you called redis.sismember().
One way to solve that is to create a new scoped variable by wrapping the asynchronous function in a closure:
(function(result) {
redis.sismember(setname, result[0], function(err, reply) {
...
});
})(result);
Another option is to create a partial function that's used as callback:
redis.sismember(setname, result[0], function(result, err, reply) {
...
}.bind(this, result));
The second issue is, I think, caused by redis.quit() being called, which closes the Redis connection after the first sadd(). You're not checking err, but if you do it might tell you more.
Update: Solved, it was indeed a scope issue. I got around it by moving the user list code inside the database class and returning the prebuilt list.
Using node.js, I make an asynchronous call to function findUser and I build a list of users from the callback into the variable content. This works fine during the loop (where it says content variable is available) but when the loop exits, the variable is empty. How can I rewrite the code so that the value of variable content is available outside the loop?
exports.listUsers=function(req,res) {
var content=''
isLoggedIn=user.findUser({},function(myuser) {
content = content +'<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
})
//here value of content var is empty
console.log(content)
showPage(req,res,{'content':content})
}
If findUser() is asynchronous (which you indicate), then the issue is that findUser() has not yet completed when showPage() is called.
For asynchronous functions, you can only use their results from the success handler function. You can't call an asynchronous function and expect to use it synchronously like your current code.
I don't know exactly what you're trying to accomplish, but this is the general design pattern you would need to use:
exports.listUsers=function(req,res) {
isLoggedIn=user.findUser({},function(myuser) {
var content = '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
showPage(req,res,{'content':content})
});
}
Or, if the callback is being called many times once for each user, you can accumulate the content and call showPage() on the last callback:
exports.listUsers=function(req,res) {
var content = "";
isLoggedIn=user.findUser({},function(myuser) {
content += '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
// devise some logic to know when the last callback is being called
// perhaps based on a user count
if (this is the last user callback) {
showPage(req,res,{'content':content})
}
});
}