Async/Await chaining - javascript

Guess it's my turn to create a user at stackoverflow :)
I've tried and solve this assignment for 2 days now, i can get it to show the first two informations, but i have no idea how i access the last two...
Plus i have a slight idea, that i am doing it completely wrong...
This is what i have to do:
Use information from this link to find the first movie in which Luke Skywalker appeared
Use information from this link to find the first species, which appeared in this movie
Use information from this link to find the planet (homeworld) for this species
Now, Implement a method getPlanetforFirstSpeciesInFirstMovieForPerson(id){} which for id = 1 (Luke Skywalker) should log this info:
Name: Luke Skywalker
First film: The Empire Strikes Back
First species: Yoda's species
Homeworld for Specie: unknown
Hints:
This requires you to make a number of REST-requests (using fetch), read a value from the request, and use this value to perform a new request.
The lists in the responses are not sorted. For this exercise it’s ok to just use the first URL in the list: Like films[0] will actually give you the second movie, see below:
Data:
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
This is what I have so far:
const fetch = require('node-fetch')
const URL = "https://swapi.co/api/people/";
async function getPlanetForFirstSpeciesInFirstMovieForPersonID(id) {
const result = await fetch(URL);
const data = await result.json().then((nameData) => nameData.results[id]);
const name = data.name
const film = await fetch(data.films[id])
const filmdata = await film.json().then((movietitle) => movietitle.title)
console.log(name)
console.log(filmdata)
}
getPlanetForFirstSpeciesInFirstMovieForPersonID(0).catch((e) => {
console.log('There was an error :', e)
});
But I guess there has to be a cleaner and smarter way to do this.
I can do it without problems using promises the old way, but I have to use async/await.

Related

Firestore DocumentSnapshot.data() returns undefined, but in the console it definetly has and it works for other documents

In this Firebase Function I'm getting two DocumentSnapshots, the first works fine, I can get the data (emailNonce) from the db, but the second DocumentSnapshot somehow has no data, the object is there, I can see it in the logs, but calling .data() on it returns undefined:
const addRentalFct = async (data, context) => {
// this works:
const secretsRef = db.collection('user-secrets').doc('Yv3gZU8TeJTixl0njm7kUXXpvhc2');
const secretsSnap = await secretsRef.get();
const dbNonce = secretsSnap.data().emailNonce;
functions.logger.log('got the dbNonce: ', dbNonce);
// this doesn't work, but ir's the same logic as above:
const boxesSecretsRef = db.collection('box-secrets').doc('CB8lNQ8ZUnv4FDT6ZXGW');
const boxSecretsSnap = await boxesSecretsRef.get();
functions.logger.log('got the boxSecretsSnap: ', boxSecretsSnap);
functions.logger.log('got the boxSecretsSnap.data(): ', boxSecretsSnap.data());
const boxPassword = boxSecretsSnap.data().password;
functions.logger.log('the box secret is: ', boxPassword);
...
}
The DB:
box-secrets collection
user-secrets:
(the secrets are from my dev environment)
The problem was that I copied the id for the new document from an already existing document in the console like this:
Automatically there was a space added in front. When I created the new doc, the space was not visible, but I could create another doc with the same id, without the space in front. Here you see that it
s not that obvious, it looks like there are two docs with the exact same id:
When having it like this, the firebase function didn't find any of the two docs. I had delete both and readd it without space, then it worked.

API can't handle my request because of template literals to make the API dynamic

For a school project, I have to make a quiz app. It is possible to chose a difficulty, a category and an amount of desired questions. The api is a url which can be modified easily by changing some values. For example: https://quizapi.io/api/v1/questions?apiKey=MYAPIKEY&limit=15&difficulty=hard&category=cms. If you would just change the php to code in the url, you would get a max amount of 15 questions on a hard difficulty about HTML and CSS. I think you see where this is going.
However. I have setup my code that the difficulty, category and amount are stored in localstorage and they are fetched when the quiz is started. At the moment, I get the amout of questions I desire but I can't change my difficulty or category because probably Template Literals aren't working in a fetch api.. Maybe someone can give me an idea or maybe I'm making a mistake in my current code
let storageDif = localStorage.getItem("mD");
console.log(storageDif.toString());
let storageCat = localStorage.getItem("mC");
console.log(storageCat);
let geslideVragen = localStorage.getItem("slider");
let MAX_VRAGEN = geslideVragen;
console.log(MAX_VRAGEN);
let vragen = [];
fetch(`https://quizapi.io/api/v1/questions?apiKey=kAFKilHLeEcfLkGE2H0Ia9uTIp1rYHDTIYIHs9qf&limit=15&difficulty=hard&category=${storageCat}`)
.then((res) => {
return res.json();
})
.then((loadedQuestions) => {
for (let i = 0; i < MAX_VRAGEN; i++) {
vragen = loadedQuestions;
console.log(vragen[i].question);
};
startGame();
})
.catch( err => {
console.error(err);
});
I'm sure you found out by now that you're only interpolating the category. To get it to be correctly, you'd need to do this:
`https://quizapi.io/api/v1/questions?apiKey=kAFKilHLeEcfLkGE2H0Ia9uTIp1rYHDTIYIHs9qf&limit=${MAX_VRAGEN}&difficulty=${storageDif}&category=${storageCat}`
That being said, you should never expose your API keys this way, because especially for cloud services, it can easily cost you over 5 digits in a single day if someone decided to use it for their own means. There are plenty of scrapers that scour GitHub for exposed API keys for illegitimate uses.
Also, should apply a check to make sure all values are present using an if() statement so that it doesn't fetch anything if a value is undefined.

Cannot Delete Fields with Period in Key Name

I've tried every solution in this other StackOverflow thread, and none of them have worked for me. At this point I'm absolutely stumped, and have no idea on what to try next.
The data that I'm trying to access is this:
The key I'm trying to use is a URL, which is inside the accounts map. This is the code I'm running to try and delete the key:
var userRef = db.collection('userAccounts').doc(userEmail)
let dynamicKey = `accounts.${accountURL}`
console.log(dynamicKey)
userRef.set({
[dynamicKey]: firebase.firestore.FieldValue.delete()
}, { merge: true})
.then((result) => {
console.log(result)
})
.catch((error) => {
console.log(error)
})
Looking in console, accounts.www.stackoverflow.com is printed:
So, it looks like the path should match. A note to make is that a URL with no periods works just fine, so it seems like the path IS correct, and that periods are in fact the issue.
The accepted answer by J Livengood simply doesn't work for keys with periods in the name:
[`hello.${world}`]: firebase.firestore.FieldValue.delete()
The code posted by Sam Stern simply doesn't run, and I get an error mentioning that update only takes one parameter. Contrary to the last poster (ishandutta2007), adding a 'new' before the FieldPath doesn't fix the error:
doc.update(
firebase.firestore.FieldPath("hello.world"),
firebase.firestore.FieldValue.delete());
This code (in the comments) posted by OP, Sandeep Dinesh, just doesn't work at all, even when trying to delete with a key with no period. My code is the following, and the returned Promise is undefined in the "then" portion of the code:
var userRef = db.collection('accounts').doc(userEmail)
let dynamicKey = `accounts.${accountURL}`
userRef.set({
[dynamicKey]: firebase.firestore.FieldValue.delete()
}, { merge: true})
.then((result) => {
console.log(result)
})
.catch((error) => {
console.log(error)
})
The problem you have here is a little bit different than you described. Specifically, you're trying to delete a nested map key with periods in it. It's important to realize that this nesting requires special treatment that you didn't see in other questions, which were dealing only with top-level fields.
You will need to use FieldPath here, and specify the path of the field as array elements in the constructor. From the linked API documentation (emphasis mine):
Creates a FieldPath from the provided field names. If more than one field name is provided, the path will point to a nested field in a document.
So, you should set or update with a field of new firebase.firestore.FieldPath(["accounts", "www.stackoverflow.com"]) and a value of firebase.firestore.FieldValue.delete().
documentReference.update(
new firebase.firestore.FieldPath(["accounts", "www.stackoverflow.com"]),
firebase.firestore.FieldValue.delete()
);

Can't access innerText property using Puppeteer - .$$eval and .$$ is not yielding results - JavaScript

I am working on a web scraper that searches Google for certain things and then pulls text from the result page, and I am having an issue getting Puppeteer to return the text I need. What I want to return is an array of strings.
Let's say I have a couple nested divs within a div, and each has text like so:
<div class='mainDiv'>
<div>Mary Doe </div>
<div> James Dean </div>
</div>
In the DOM, I can do the following to get the result I need:
document.querySelectorAll('.mainDiv')[0].innerText.split('\n')
This yields: ["Mary Doe", "James Dean"].
I understand that Puppeteer doesn't return NodeLists, and instead it uses JSHandles, but I still can't figure out how to get any information using the prescribed methods. See below for what I have tried in Puppeteer and the corresponding console output:
In every scenario, I do await page.waitFor('selector') to start.
Scenario 1 (using .$$eval()):
const genreElements = await page.$$eval('div.mainDiv', el => el);
console.log(genreElements) // []
Scenario 2 (using evaluate):
function extractItems() {
const extractedElements = document.querySelectorAll('div.mainDiv')[0].innerText.split('\n')
return extractedElements
}
let items = await page.evaluate(extractItems)
console.log(items) // UnhandledPromiseRejectionWarning: Error: Evaluation failed: TypeError: Cannot read property 'innerText' of undefined
Scenario 3 (using evaluateHandle):
const selectorHandle = await page.evaluateHandle(() => document.querySelectorAll('div.mainDiv'))
const resultHandle = await page.evaluate(x => x[0], selectorHandle)
console.log(resultHandle) // undefined
Any help or guidance on how I am implementing or how to achieve what I am looking to do is much appreciated. Thank you!
Use page.$$eval() or page.evaluate():
You can use page.$$eval() or page.evaluate() to run Array.from(document.querySelectorAll()) within the page context and map() the innerText of each element to the result array:
const names_1 = await page.$$eval('.mainDiv > div', divs => divs.map(div => div.innerText));
const names_2 = await page.evaluate(() => Array.from(document.querySelectorAll('.mainDiv > div'), div => div.innerText));
Note: Keep in mind that if you use Puppeteer to automate searches on Google, you may be temporarily blocked and end up with an "Unusual traffic from your computer network" notice, requiring you to solve a reCAPTCHA. This may break your web scraper, so proceed with caution.
Try it like this:
let names = page.evaluate(() => [...document.querySelectorAll('.mainDiv div')].map(div => div.innerText))
That way you can test the whole thing in the chrome console.
Using page.$eval:
const names = await page.$eval('.mainDiv', (element) => {
return element.innerText
});
Here the element is retrieved by selector and directly passed to the function to be evaluated.
Using page.evaluate:
const namesElem = await page.$('.mainDiv');
const names = await page.evaluate(namesElem => namesElem.innerText, namesElem);
This is basically the first method split up into two steps. The interesting part is that ElementHandles can be passed as arguments in page.evaluate() and can be evaluated like JSHandles.
Note that for simplicity and clarification I used the methods for retrieving single elements. But page.$$() and page.$$eval() work the same way while selecting multiple elements and returning an array instead.

How to program a decision tree with Alexa?

I am currently trying to program an alexa skill. I am very stuck... trying to see if I can get Alexa to ask the user 'How they are feeling' and then use this to ask further questions. Not sure if I should make a variable or attribute... any help please.
For example, once the user says "SAD" for the emotion - I want to be able to ask further questions, like "Is it from your past or present?"
(AWS code)
const GREETING = [
'What emotion are you feeling today?',
'Hello, what emotion are you feeling right now?'
];
const SKILL_NAME = 'March Test';
const GET_FEEL = "That is unfortunate to hear?";
const HELP_MESSAGE = 'I can give you more information if you tell me
how you are feeling';
const HELP_REPROMPT = 'How are you feeling currently?';
const STOP_MESSAGE = 'Goodbye!';
const FeelingsList = [
{
Emotion: "SAD",
suggestion: "My suggestion for sad is dry your tears and find a
distraction"
},
{
Emotion: "HAPPY",
suggestion: "My suggestion for happy is keep smiling and keep shining"
},
{
Emotion: "ANGRY",
suggestion: "My suggestion for angry is count to ten and cool down"
}
];
const handlers = {
'LaunchRequest': function () {
const greetingArr = GREETING;
const greetingIndex = Math.floor(Math.random() *
greetingArr.length);
this.emit(':ask',greetingArr[greetingIndex]); //first action that
will be fired
},
'EmotionalState': function () {
var stateSlot = this.event.request.intent.slots.Emotion.value;
this.emit(':ask', EmotionalResponse(FeelingsList, 'Emotion',
stateSlot.toUpperCase()).suggestion);
},
'AMAZON.HelpIntent': function () {
const speechOutput = HELP_MESSAGE;
const reprompt = HELP_REPROMPT;
this.response.speak(speechOutput).listen(reprompt);
this.emit(':responseReady');
},
'AMAZON.CancelIntent': function () {
this.response.speak(STOP_MESSAGE);
this.emit(':responseReady');
},
'AMAZON.StopIntent': function () {
this.response.speak(STOP_MESSAGE);
this.emit(':responseReady');
},
Okay, thanks for answering my questions in comments, the EmotionalResponse function looks good. The way you have it set up currently should work well for a single response. If that is all you want Alexa to do here then just change ':ask' to ':tell' (which will respond and not expect the user to reply) so the line would become:
this.emit(':tell', EmotionalResponse(FeelingsList, 'Emotion', stateSlot.toUpperCase()).suggestion );
However, you want to be able to continue the conversation with multiple questions and response handlings. You might already have this but it's not in your code above, and you need it to use the Alexa SDK, so make sure you have this line at the very beginning:
const Alexa = require('alexa-sdk');
You also need this, preferably at the end:
exports.handler = function(event, context, callback) {
const alexa = Alexa.handler(event, context, callback);
alexa.registerHandlers(handlers);
alexa.execute();
};
Next, some things to know about continuing a conversation using Alexa SDK:
':ask'
will respond with the speechOutput sentence and expect a reply from the user, but if the user does not reply, then Alexa will "reprompt" the user with the repromptSpeech sentence.
':delegate'
will tell Alexa to determine which required slot to ask the user for, and use the prompt (set up in the Alexa developer console) to elicit the slot information from the user.
':elicitSlot'
will give Alexa the specific instructions of which slot to elicit and the exact prompt to use.
There are many ways to continue an Alexa conversation and build your logic of handling the user's input and build an appropriate response. But to use your example of requesting more information from the user such as "Is it from your past or present?", here is one way I would suggest:
First, create another slot for this intent (anytime you want to hold user input information you'll need another slot). Let's call it Timeframe.
The simplest way is to "delegate" to Alexa to elicit this slot, so in the console make the slot required and add a prompt message such as, "Are you feeling this way because of something in your past or present?" You can even get fancy and use the emotion slot in that message like this: "Are you feeling {Emotion} because of something in your past or present?" Alexa will auto fill that and it will sound more intelligent and conversational.
Next you'll want to improve the logic of this intent inside const handlers:
const handlers = {
...
'EmotionalState': function () {
var emotion = this.event.request.intent.slots.Emotion.value;
var timeframe = this.event.request.intent.slots.Timeframe.value;
if (!timeframe){
this.emit(':delegate');
} else {
var response = responseBuilder(emotion, timeframe);
this.emit(':tell', response);
}
},
...
} //<<--------------your code seems to be missing this to close handlers
function responseBuilder(emotion, timeframe) {
// compare emotion with timeframe to build the appropriate response
// just for example
if (emotion=="sad" && timeframe=="past") {
return "My suggestion for feeling sadness about the past is, dry your tears and find a distraction.";
}
}
That's just a rough idea, but should certainly get you progressing again. Good luck!
Create a global object emotions and have all the emotions as properties and the corresponding suggestions as the value for them. Something like this,
const emotions = {"Happy" : "Keep smiling and shining", "Angry" : "count to 10 and cool down"}
Then, access the global object with the varibale to which you got the slot value from the user utterance and add it along with your response.
For instance,
var stateSlot = this.event.request.intent.slots.Emotion.value;
var suggestion = emotions[stateSlot];
Use the square bracket to get the property in the emotions object that matches for the value in the variable stateSlot.

Categories