Cheerio.load messes with responses for Google Assistant - node.js

I have this intent that has a cheerio.load() being called and it messes with with the responses. The Google Assistant keeps telling me that no response has been set even though later down in the code I have responses. The console is also telling me that an async call wasn't being returned to the handler, which I believe is the cheerio.load(). Is there anyway I can fix this so that it continues looking for the correct conv.ask at the bottom of the code? It still continues to run down to there too, for the console.log(map) shows up. Thanks for any help!
app.intent("late drop", (conv,{term,year}) => {
var date = new Date();
var month;
if(term == null){
month = date.getMonth();
if(month >= 9 && month <=12){
term = "fall";
//console.log("fall")
}
else if (month >= 1 && month <= 5) {
term = "spring";
//console.log("spring")
}
else {
term = "summer";
//console.log("summer")
}
}
if(year == null){
yearDig = date.getFullYear();
year = yearDig;
//console.log(year)
}
var strYear = year.toString();
var semester = term+strYear.substr(2);
const options = {
uri: `https://www.registrar.psu.edu/academic_calendar/${semester}.cfm`,
transform: function (body) {
return cheerio.load(body);
}
};
rp(options)
.then(($) => {
let map = {};
let columnOne = [];
let columnThree = [];
$('table').find('tr td:nth-child(1)').each(function (index, element) {
columnOne.push($(element).text());
});
$('table').find('tr td:nth-child(3)').each(function (index, element) {
columnThree.push($(element).text());
});
columnOne.forEach((item, i) => {
map[item] = columnThree[i];
});
console.log(map);
date = map["2Late Drop Begins"];
conv.ask("The late drop period begins on " + map["2Late Drop Begins"])
})
.catch((error) => {
console.log(error);
conv.ask("An error occured, please try again.");
})
});

The issue is not with cheerio.
It looks like you are using request-promise or request-promise-native to make your HTTP call. This does an asynchronous operation that will return a Promise (as is evidenced by your use of .then() and .catch().
Since Intent Handlers that do asynchronous operations must return a Promise, you can simply return the one that is returned by the rp/then/catch chain. Something like changing this line should work:
return rp(options)

I have modified your code to return a Promise. Check if this works for you.
app.intent("late drop", (conv, {
term,
year
}) => {
return new Promise(function (resolve, reject) {
var date = new Date();
var month;
if (term == null) {
month = date.getMonth();
if (month >= 9 && month <= 12) {
term = "fall";
//console.log("fall")
} else if (month >= 1 && month <= 5) {
term = "spring";
//console.log("spring")
} else {
term = "summer";
//console.log("summer")
}
}
if (year == null) {
yearDig = date.getFullYear();
year = yearDig;
//console.log(year)
}
var strYear = year.toString();
var semester = term + strYear.substr(2);
const options = {
uri: `https://www.registrar.psu.edu/academic_calendar/${semester}.cfm`,
transform: function (body) {
return cheerio.load(body);
}
};
rp(options)
.then(($) => {
let map = {};
let columnOne = [];
let columnThree = [];
$('table').find('tr td:nth-child(1)').each(function (index, element) {
columnOne.push($(element).text());
});
$('table').find('tr td:nth-child(3)').each(function (index, element) {
columnThree.push($(element).text());
});
columnOne.forEach((item, i) => {
map[item] = columnThree[i];
});
console.log(map);
date = map["2Late Drop Begins"];
conv.ask("The late drop period begins on " + map["2Late Drop Begins"])
resolve()
})
.catch((error) => {
console.log(error);
conv.ask("An error occured, please try again.");
reject()
})
});
});
Hope that helps!

Related

Using node.js for-loop index in a coinbase-api callback function

I am new to node.js and i am trying to make a simple script that will connect to the coinbase-api and get the current price of whatever markets are defined in the MARKET array.
The problem i am having is that the for-loop that iterates through the array is asynchronous and the callback function is not getting the correct index value for the array.
The two main solutions i have found are to use promises or force the loop to wait. I think i need to be using promises rather than forcing the for loop to wait but honestly i have failed to implement a solution either way. I have found may example of promises but i just cant seem to figure out how to implement them into my script. I would appreciate any help.
const coinbaseModule = require('coinbase-pro');
const COINBASE_URI = 'https://api-public.sandbox.pro.coinbase.com';
// const MARKET = ['BTC-USD'];
const MARKET = ['BTC-USD', 'ETH-BTC'];
let askPrice = [null, null];
let averagePrice = [null, null];
let tickerCount = null;
const getCallback = (error, response, data) =>
{
if (error)
return console.log(error);
if ((data!=null) && (data.ask!=null) && (data.time!=null))
{
askPrice[tickerCount] = parseFloat(data.ask);
if (averagePrice[tickerCount]===null)
{
averagePrice[tickerCount] = askPrice[tickerCount];
console.log(MARKET[tickerCount] + " ask price: " + askPrice[tickerCount].toFixed(6));
}
else
{
averagePrice[tickerCount] = (averagePrice[tickerCount] * 1000 + askPrice[tickerCount]) / 1001;
console.log(MARKET[tickerCount] + " ask price: " + askPrice[tickerCount].toFixed(6) + " average price: "+ averagePrice[tickerCount].toFixed(6));
}
}
}
setInterval(() =>
{
console.log('\n');
publicClient = new coinbaseModule.PublicClient(COINBASE_URI);
for (tickerCount = 0; tickerCount < MARKET.length; tickerCount++)
{
publicClient.getProductTicker(MARKET[tickerCount], getCallback);
}
}, 10000);
I was able to figure out how to use promises with trial and error from the helpful examples on the Mozilla Developer Network. I am sure i am making some mistakes but at least it is working now. Another little bonus is that i was able to remove a global.
const coinbaseModule = require('coinbase-pro');
const COINBASE_URI = 'https://api-public.sandbox.pro.coinbase.com';
// const MARKET = ['BTC-USD'];
const MARKET = ['BTC-USD', 'ETH-BTC'];
let askPrice = [null, null];
let averagePrice = [null, null];
function getProductTicker(tickerCount) {
return new Promise(resolve => {
publicClient.getProductTicker(MARKET[tickerCount],function callback(error, response, data){
if (error)
return console.log(error);
if ((data!=null) && (data.ask!=null) && (data.time!=null))
{
askPrice[tickerCount] = parseFloat(data.ask);
if (averagePrice[tickerCount]===null)
{
averagePrice[tickerCount] = askPrice[tickerCount];
console.log(MARKET[tickerCount] + " ask price: " + askPrice[tickerCount].toFixed(6));
}
else
{
averagePrice[tickerCount] = (averagePrice[tickerCount] * 1000 + askPrice[tickerCount]) / 1001;
console.log(MARKET[tickerCount] + " ask price: " + askPrice[tickerCount].toFixed(6) + " average price: "+ averagePrice[tickerCount].toFixed(6));
}
resolve();
}
});
});
}
setInterval( async () =>
{
console.log('\n');
publicClient = new coinbaseModule.PublicClient(COINBASE_URI);
for (var tickerCount = 0; tickerCount < MARKET.length; tickerCount++)
{
await getProductTicker(tickerCount);
}
}, 10000);

my sync function doesn't wait for all the subfunctions to finish

i'm trying to make an api that can see if i made a post on imgur and i'm really close to finish but my function returns before the end
exports.handler = (event, context, callback) => {
ddb.scan(params1, function(err, data) {
if (err) callback(null, err);
else {
data.Items.forEach(function(item) {
for (var o = 0; item.links && item.links.L && o < item.links.L.length; o++) {
var taction = "";
var treaction = "";
var action = item.links.L[o].S.substring(0, item.links.L[o].S.indexOf(' '));
var saction = action.substring(0, action.indexOf(':'));
var reaction = item.links.L[o].S.substring(item.links.L[o].S.indexOf('=')+2);
var sreaction = reaction.substring(0, reaction.indexOf(':'));
for (var z = 0; item.accsplus && item.accsplus.L && z < item.accsplus.L.length; z++) {
if (item.accsplus.L[z].S.substring(0, item.accsplus.L[z].S.indexOf(':')) == saction)
taction = item.accsplus.L[z].S.substring(item.accsplus.L[z].S.indexOf('token:')+6);
if (item.accsplus.L[z].S.substring(0, item.accsplus.L[z].S.indexOf(':')) == sreaction)
treaction = item.accsplus.L[z].S.substring(item.accsplus.L[z].S.indexOf('token:')+6);
}
if (taction == "" || treaction == "") log += "no token for this service #" +action+reaction + " ";
else{
console.log("testing " +action+reaction)
if ((saction == "imgur" || saction == "reddit") && (sreaction == "imgur" || sreaction == "reddit")) {
if (saction == "imgur")
imgur(action.substring(action.indexOf(':')+1), item, taction, reaction.substring(reaction.indexOf(':')+1));
}
}
}
})
callback(null, "ok");
}
});
async function imgur(action, whom, token, reaction) {
if (action =="") return;
var requestdone = false;
var toret;
var old = "";
var needed = action;
if (action == "onpost" || action == "onrem") needed = "postnbr";
if (action == "onlike" || action == "ondis") needed = "likenbr";
console.log("imgur " + needed);
var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer " + token);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
async function request(success) {
const response = await fetch("https://api.imgur.com/3/account/me/images", requestOptions)
const json = await response.json();
return await success(json);;
}
function success(json) {
var worked;
if (needed == "postnbr") actual = (json.data.length);
if (needed == "likenbr"){
if (json.data[0].vote == null || json.data[0].vote == "null") actual = (0);
actual = (json.data[0].vote);
}
if (needed == "postnbr") console.log("postnbr = " + actual);
if (needed == "likenbr") console.log("likenbr = " + actual);
if (whom.old && whom.old.L && whom.old.L.length > 0){
for (var p = 0; old == "" && p < whom.old.L.length; p++){
if (whom.old.L[p].S.substring(0,whom.old.L[p].S.indexOf(':')) == "imgur"+needed)
{
old = whom.old.L[p].S.substring(whom.old.L[p].S.indexOf(':')+1);
if (action == "onpost" && old < actual) worked = true;
if (action == "onrem" && old > actual) worked = true;
if (action == "onlike" && old < actual) worked = true;
if (action == "ondis" && old > actual) worked = true;
}
}
}
if (worked)
{
return imgur(reaction, whom, token, "");
}
upold("imgur", needed, whom, actual)
}
await request(success)
}
var getactual = function(service, token, needed) {
return new Promise(function(resolve, reject){
if (service == "imgur"){
}
if (service == "reddit"){
console.log("do reddit mofo");
}
})
};
function upold(service, needed, whom, actual) {
var toret = [];
if (whom.old && whom.old.L.length > 0){
for (var m = 0; m < whom.old.L.length ; m++) {
if (whom.old.L[m].S.substring(0,whom.old.L[m].S.indexOf(':')) != service+needed)toret.push(whom.old.L[m].S);
}
}
toret.push(service+needed + ":" +actual);
param = {
TableName:"biipboop",
Key:{
"email": whom.email.S
},
UpdateExpression: "set old=:r",
ExpressionAttributeValues:{
":r":toret
},
ReturnValues:"UPDATED_NEW"
};
docClient.update(param, function() {});
}
};
so far it seems like the function works and the request subfunction is called but the main doesn't wait for an answer and returns Promise {} then the log does happen once the success completes itself but i don't have my variable at the end of the imgur function, it just logs itself when finished
EDITED :
since you asked for the actual code i had, i copy pasted it brutally
i have my scan working well, gets all my user and then it doesn't wait for function "imgur" to give a proper answer and just leaves triggers it and iterate through the foreach.
i'm trying to use lambda and ddb through aws
the logs do arrive in this order :
testing imgur:onpostimgur:onlike
imgur postnbr
testing reddit:postreddit:onpost
postnbr = 3
The way you are returning data from imgur looks really off to me.. This would be a lot easier, in my opinion, if you turned imgur into an async function so that you can await for the data that request returns.
async function imgur(action, whom, token) {
var toret;
var old = "";
var needed = "postnbr";
var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer " + token);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
async function request(success) {
const response = await fetch("https://api.imgur.com/3/account/me/images", requestOptions)
const json = await response.json();
return /* await */ success(json); // <-- shouldn't have to await this
}
function success(json) {
old = "2";
if (needed == "postnbr") actual = (json.data.length);
if (needed == "postnbr") console.log("postnbr = " + actual);
if (action == "onpost" && old < actual)
return("you made a post recently");
updateold("imgur", needed, whom, actual)
}
return await request(success); // .then(function(msg) {return msg;});
}
Then you can use it like:
async function useImgur() {
const imgurData = await imgur('some-action', 'some-whom', 'some-token');
console.log(imgurData);
}
useImgur();
...or like:
imgur('some-action', 'some-whom', 'some-token')
.then(imgurData => console.log(imgurData))
.catch(err => console.error(err));
Well imgur() returns a promise. So,
console.log(imgur("onpost", accounts[i], accounts[i].token))
will always log an unresolved promise, every time. Even if that promise is eventually going to get a resolved value from
return("you made a post recently");
it won't have it yet since promises are never resolved synchronously. The soonest the promise that imgur() returns will be resolved is on the next tick of the event loop which is after your console.log().
You can change the code to this to log the value:
async function main(accounts) {
for (var i = accounts.length - 1; i >= 0; i--) {
let val = await imgur("onpost", accounts[i], accounts[i].token);
console.log(val);
}
}
Keep in mind that some code paths through imgur() do not return a value which is probably not a desired design, thus sometimes the resolved value from the imgur() promise can be undefined and sometimes not.

Firebase Function throws returned undefined, expected Promise or value

My below firebase function throws the error Function returned undefined, expected Promise or value. I am doing a return everywhere and not sure why it still throws this error
I have looked into similar code samples and not sure what point does it throw the error.
exports.on_order_updated_update_new_vs_repeat_order = functions.database.ref("/orders/{id}")
.onUpdate((change, context) => {
const newValue = change.after.val();
const oldValue = change.before.val()
//if order is marked delivered then update the data
if(oldValue.order._orderStatus !== 'Delivered' && newValue.order._orderStatus === 'Delivered'){
//find the uid of the customer
const uid = newValue.customer._uid
var isOldOrder = false //to track weather the customer is new or repeat
var db = admin.database();
var ref = db.ref('users')
return ref.child(uid).child('orders').once("value").then(
(resp) => {
const orderKeys = Object.keys(resp.val())
if(orderKeys.length > 1)
isOldOrder = true //existing orders there so just set it to true
var date = new Date()
var begDate = findDayBegninning(date)
var endDate = findDayEnd(date)
var anaRef = db.ref('analytics')
return anaRef.child('newVsRepeatOrders').orderByChild("date").startAt(begDate).endAt(endDate).once("value").then(
(rp) => {
if(rp !== undefined && rp.val() !== null){
const newOldObj = rp.val()
var oldOrderVal = 0
var newOrderVal = 0
if(isOldOrder === true){
oldOrderVal = newOldObj[begDate].oldOrdersCount + 1
newOrderVal = newOldObj[begDate].newOrdersCount
}
return anaRef.child('newVsRepeatOrders/' + begDate).update({"oldOrdersCount": oldOrderVal, "newOrdersCount": newOrderVal}).then(
(resp1) => console.log("updated order count")
).catch(
(err) => console.error("error in updating old vs new order count:" + err)
)
}else{
console.log("no data found for today so adding first record")
var oldOrderCount = 0
var newOrderCount = 0
if(isOldOrder === true)
oldOrderCount++
else
newOrderCount++
var payload = {
"date" : begDate,
"oldOrdersCount": oldOrderCount,
"newOrdersCount" : newOrderCount
}
return anaRef.child('newVsRepeatOrders/' + begDate).set(payload).then(
(rpp) => console.log("updated newVsRepeatOrders")
).catch(
(err) => console.error("Error updating newVsRepeatOrders::" + err)
)
}
}
).catch(
(err) => console.error("Could not execute path newVsRepeatOrders for the customer uid:" + uid + " error is:" + err)
)
}
).catch(
(err) => console.error("Could not find orders for the customer uid:" + uid + " error is:" + err)
)
}
}
)
Please ignore the text from here. stackoverflow does not let me post saying i only have code and add text.
You're not returning anything from the function in the event that the top level condition is false. Let me compress your function down so you can see more clearly:
exports.on_order_updated_update_new_vs_repeat_order = functions.database.ref("/orders/{id}")
.onUpdate((change, context) => {
const newValue = change.after.val();
const oldValue = change.before.val()
//if order is marked delivered then update the data
if(oldValue.order._orderStatus !== 'Delivered' && newValue.order._orderStatus === 'Delivered'){
// return some promise...
}
// nothing is returned if the above condition was false
}
)
You need to return something in every case. If your function has no async work to do in case the main condition is false, just return null.
As a matter of better style and clarity, you may also wish to chain your promises rather than nesting them. Nested promises are difficult to read, and linters may complain about that, as they are also prone to error.

How do I return a value inside a Promise? In "then()" and "catch()"

I just want to return 1 and return 0 at the specified places. I've looked at numerous sources but was unable to solve this issue.
Below is the code :
exports.getLatLng = function(row){
var attractionId = row['attractionid'];
var attractionName = row['attractionname'] + ' ' +
row['Destination_name'];
var googleMapsResults;
return googleMapsClient.geocode({address: attractionName}).asPromise()
.then((response) => {
googleMapsResults = response.json.results[0];
// console.log(googleMapsResults);
model.dumpIntoMongo(attractionId, googleMapsResults);
// var y=tmp[0];
var latitude = googleMapsResults.geometry.location.lat;
var longitude = googleMapsResults.geometry.location.lng;
row["id"] = parseInt(attractionId);
// delete row['attractionid'];
delete row['destination_id'];
delete row['destination_name'];
delete row['attraction_id'];
delete row['success'];
// row["lat"] = latitude;
// row["lon"] = longitude;
row["latlon"] = latitude.toString() + "," + longitude.toString();
exports.indexIntoSolr(row);
return 1; //return 1
})
.catch((err) => {
console.log(err);
return 0; // return 0
});
}
In case If you want another more clear implementation, you can refer this one.
Here, First write a function which returns some data as promise after certain calculations:
function getSomething(credentials) {
const {
name
} = credentials;
return new Promise((resolve, reject) => {
if(everything_as_expected){
return resolve(some_data)
}
else if(some_error){
return reject(error);
}
});
}
To handle results/Data returned by the function( promisely ), call that previous function:
getSomething(credentials)
.then((message) => {
console.log(`${message} got after success.`);
})
.catch((error_message) => {
console.log(error_message);
});
The method returns a promise that is pending until it resolves. To get a value you need to attach a continuation to it:
getLatLng(row).then( result => {
console.log( result );
});
This will definitely display 0 or 1 as your internal implementation of the getLatLng looks like it correctly handles both execution paths.

Proper way to make callbacks async by wrapping them using `co`?

It is 2016, Node has had nearly full ES6 support since v4, and Promises have been around since 0.12. It's time to leave callbacks in the dust IMO.
I'm working on a commander.js-based CLI util which leverages a lot of async operations - http requests and user input. I want to wrap the Commander actions in async functions so that they can be treated as promises, and also to support generators (useful for the co-prompt library I'm using for user input).
I've tried wrapping the CB with co in two ways:
1)
program.command('myCmd')
.action(program => co(function* (program) {...})
.catch(err => console.log(err.stack)) );
and
2) program.command('myCmd').action(co.wrap(function* (program) { .. }));
The problem with 1) is that the program parameter isn't passed
The problem with 2) is that errors are swallowed...
I'd really like to get this working as it yields much nicer code in my use case - involving a lot of http requests and also waiting for user input using the co-prompt library..
Is it a better option altogether perhaps to wrap program.Command.prototype.action somehow?
thanks!
I've used a bespoke version of something like co to get a db.exec function which uses yield to do database request. You can pass parameters into a generator function (I pass in a connection object - see the comment where I do it).
Here is by db.exec function that is very similar to what co does
exec(generator) {
var self = this;
var it;
debug('In db.exec iterator');
return new Promise((accept,reject) => {
debug('In db.exec Promise');
var myConnection;
var onResult = lastPromiseResult => {
debug('In db.exec onResult');
var obj = it.next(lastPromiseResult);
if (!obj.done) {
debug('db.exec Iterator NOT done yet');
obj.value.then(onResult,reject);
} else {
if (myConnection) {
myConnection.release();
debug('db.exec released connection');
}
accept(obj.value);
debug('db.exec Promise Resolved with value %d',obj.value);
}
};
self._connection().then(connection => {
debug('db.exec got a connection');
myConnection = connection;
it = generator(connection); //This passes it into the generator
onResult(); //starts the generator
}).catch(error => {
logger('database', 'Exec Function Error: ' + error.message);
reject(error);
});
});
}
the connection object also wraps by database connection object and provides a generator function ability to process the rows of the results from the database, but I won't post that here (although the example below is using it to process the rows).
Here is an example of using the exec function to run a sequence of sql
db.exec(function*(connection) {
if (params.name === ADMIN_USER) {
debug('Admin Logon');
user.name = ADMIN_DISPLAY;
user.keys = 'A';
user.uid = 0;
let sql = 'SELECT passwordsalt FROM Admin WHERE AdminID = 0';
connection.request(sql);
yield connection.execSql(function*() {
let row = yield;
if (row) {
user.nopass = (row[0].value === null);
} else {
user.nopass = false;
}
debug('Admin Password bypass ' + user.nopass.toString());
});
} else {
debug('Normal User Logon');
let sql = `SELECT u.UserID,PasswordSalt,DisplayName,AccessKey,l.LogID FROM Users u
LEFT JOIN UserLog l ON u.userID = l.userID AND DATEDIFF(D,l.LogDate,GETDATE()) = 0
WHERE u.UserName = #username`;
let request = connection.request(sql);
request.addParameter('username',db.TYPES.NVarChar,params.name);
let count = yield connection.execSql(function*() {
let row = yield;
if (row) {
user.uid = row[0].value;
user.name = row[2].value;
user.keys = (row[3].value === null) ? '' : row[3].value;
user.nopass = (row[1].value === null) ;
user.lid = (row[4].value === null) ? 0 : row[4].value;
debug('Found User with uid = %d and lid = %d, keys = %s',
user.uid, user.lid, user.keys);
}
});
if (count === 0) {
debug('Not Found User');
// couldn't find name in database
reply(false,false);
return;
}
}
if (!user.nopass) {
debug('Need a Password');
//user has a password so we must check it
passGood = false; //assume false as we go into this
let request = connection.request('CheckPassword');
request.addParameter('UserID',db.TYPES.Int,user.uid);
request.addParameter('password',db.TYPES.VarChar,params.password);
yield connection.callProcedure(function*() {
let row = yield;
if (row) {
//got a valid row means we have a valid password
passGood = true;
}
});
} else {
passGood = true;
}
if (!passGood) {
debug('Not a Good Pasword');
reply(false,true);
} else {
if (user.uid !== 0 && user.lid === 0) {
let sql = `INSERT INTO UserLog(UserID,LogDate,TimeOn,UserName) OUTPUT INSERTED.logID
VALUES(#uid,GETDATE(),GETDATE(),#username)`;
let request = connection.request(sql);
request.addParameter('uid',db.TYPES.Int,user.uid);
request.addParameter('username',db.TYPES.NVarChar,user.name);
yield connection.execSql(function*() {
let row = yield;
if (row) {
user.lid = row[0].value;
debug('Users Log Entry = %d',user.lid);
}
});
}
reply(true,user);
}
})
.catch((err) => {
logger('database','Error on logon: ' + err.message);
reply(false,false);
});
});
There is a quite simple way to do async function in Commander.js
async function run() {
/* code goes here */
}
program
.command('gettime')
.action(run);
program.parse(process.argv);

Resources