Chained fetches dependent on each other - javascript

Take the following scenario:
I need to show in a table a list with all countries and the population of each country. All data can be queried from here: api.population.io.
Thare are 2 api calls that can help me achieve what i want:
http://api.population.io:80/1.0/countries - returns a list of all existing countries
http://api.population.io:80/1.0/population/{$country}/today-and-tomorrow/ - returns the population of a particular country
As you can see i need to make 2 api calls since the second call is dependant of the name of the country made by the first call. I managed to make it work with the initial api call using fetch by using this code:
fetch('http://api.population.io:80/1.0/countries')
.then(results => {
return results.json();
}).then(data => {
//data.countries
})
This just returns me a list with all the countries.
Now i need to loop through data.countries and make a new api call for each country without breaking the whole process. I tried throwing another fetch call where data.countries is available while looping over data.countries but as you can imagine this breaks up the whole process, what i think happens is that the loop doesn't wait for the fetch call to complete thus messing up the query process.
I'm pretty new to this and i've tried googling it but i'm not sure what i can use to achieve what i need. Any help would be truly appreciated. I've been dealing with this problem the whole day

You could fire all the separate population requests at once and use the result when all of them have finished, with the help of Promise.all:
fetch("http://api.population.io:80/1.0/countries")
.then(results => {
return results.json();
})
.then(data => {
const populationPromises = data.countries.map(country => {
return fetch(
`http://api.population.io:80/1.0/population/${country}/today-and-tomorrow/`
).then(results => results.json());
});
return Promise.all(populationPromises);
})
.then(populations => {
console.log(populations);
})
.catch(error => {
console.error(error);
});

The approach with async/await makes the code more coherent and readable:
function getCountries() {
return fetch('http://api.population.io/1.0/countries/?format=json').then(s => s.json())
}
function getPopulation(country) {
return fetch(encodeURI(`http://api.population.io:80/1.0/population/${country}/today-and-tomorrow/?format=json`)).then(s => s.json())
}
(async () => {
try {
const { countries } = await getCountries();
const populations = await Promise.all(countries.map(getPopulation));
console.log(populations);
} catch(err) {
console.log(err);
}
})();

Related

Handling multiple API calls of a single API call

With my team we are trying to implement a command for a really common operation for the business logic but I'm having issues handling its implementation.
Basically:
We have to retrieve an array of objects (GET).
For each of that objects we have to retrieve (GET) another object inside its father.
For each of that sub-objects (childs) we have to check a condition and if it is the wanted condition we retrieve the child, otherwise we pass null.
Q: How do I handle multiple API calls that depends from a single API call without getting outside the CY chain?
This is my current implementation (doesn't works but kinda explains the wanted logic)
Cypress.Commands.add('myCommand', (sumCriteria: Function, anotherCriteria: Function) => {
// I only retrieve fathers with certain criteria
return cy.request('GET', fathersUrl).its('body').then(fatherObjects => {
return fatherObjects.filter(father => father.childs.length && father.childs.find(sumCriteria))
}).then(filteredFathers => {
filteredFathers.forEach(father => {
// For each father I retrieve a single child
const targetChildId = father.childs.find(sumCriteria).id;
// For each single child I retrieve its data and evaluate if it has the needed criteria
cy.request('GET', `${childsUrl}/${targetChildId}`)
.its('body')
.then(property => anotherCriteria(property))
})
});
})
Thanks in advance!
You almost have the correct pattern, but instead of returning results, put them on the queue.
Cypress does two things to make this work
in a custom command, it waits for any asynchronous commands to resolve
it returns whatever is on the queue at the last evaluation
Cypress.Commands.add('myCommand', (sumCriteria, anotherCriteria) => {
cy.request('GET', fathersUrl)
.its('body')
.then(fatherObjects => {
const filteredFathers = fatherObjects.filter(father => {
return father.childs.find(sumCriteria)
});
const results = []
filteredFathers.forEach(father => {
cy.request('GET', father) // waits for all these to resove
.its('body')
.then(property => anotherCriteria(property))
})
cy.then(() => results) // returns this last queued command
})
})
A reproducible example:
Cypress.Commands.add('myCommand', (sumCriteria, anotherCriteria) => {
const fathersUrl = 'https://jsonplaceholder.typicode.com/todos/1'
cy.request('GET', fathersUrl)
.then(() => {
// simulated url extraction
const filteredFathers = [
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3'
]
const results = []
filteredFathers.forEach(father => {
cy.request('GET', father)
.then(res => {
results.push(res.body.id)
})
});
cy.then(() => results)
});
})
cy.myCommand()
.should('deep.eq', [2,3]) // ✅ passes

Chaining different API calls in a Vue component including one with a for loop

I'm trying to understand how to chain two different API calls including one with a for loop in a 'notes' Vue component. I have a really basic experience of promises and I'm looking to improve.
I'm making a first API call to get all the notes and pushing them into an array using a Vuex mutation. During that first API call I'm also mapping the different users emails into an Object.
Using this mapped object, I'm making a second API call inside a for loop to get all the users avatars.
Here's what the first API call looks like :
getAllNotesAPI(entity) {
noteService.getNotes(entity)
.then((response) => {
if (response.data.length === '0') {
// Set hasData to false if the response is 0
this.hasData = false;
} else {
// Push data into the note array using a store mutation
this.setAllNotes(response.data);
}
// Mapping all users emails into 'userEmails'
this.userEmails = [...new Set(response.data.map(x => x.userEmail))];
// Calling my second API call here to get all the avatars associated with these emails
for (let i = 0; i < this.userEmails.length; i++) {
this.getAvatarAPI(this.userEmails[i])
}
})
.catch((error) => {
console.log(error);
})
.finally(() => {
this.endLoader('notes');
});
},
this.getAvatarAPI is the second API call which looks like this :
getAvatarAPI(login) {
userService.getAvatar(login)
.then((response) => {
let newAvatar = {
userEmail: login,
picture: response.data.picture
};
// Push the response into a userAvatar Object using a store mutation
this.setUserAvatar(newAvatar);
}).catch((error) => {
console.log(error)
})
},
I've tried using async / await but couldn't figure out how to bind this inside of an async function (this.getAvatarAPI(this.userEmails)) was undefined, I've tried chaining using multiples then but couldn't figure out how to : get all my notes then all my avatars then end the 'note' loader once both those API calls are done.
If any of you could give me some pointers or the beginning of an answer that would be truly appreciated !
First whilst not related to your problem, avoid for loop when non necessary:
Do you need the i index?
for (let i = 0; i < this.userEmails.length; i++) {
this.getAvatarAPI(this.userEmails[i])
}
no. You need the userMail. Then
this.userEmails.forEach(userMail => {
this.getAvatarAPI(userEmail)
})
Now, to synchronize promises, you need to return a promise (let's not talk about async yet)
make getAvatarAPI return a promise
getAvatarAPI(login) {
return userService.getAvatar(login).then(blabla) // notice the return here
retrieve the promises of getAvatar API
let promises = this.userEmails.map(userMail => {
return getAvatarAPI(userEmail)
})
return after all promises have fulfilled
let promises = this.userEmails.map(userMail => {
return getAvatarAPI(userEmail)
})
return Promise.all(promises)
On a side note with async/await
If you use it you are not forced anymore to write return, you need to write async/await though
The underlying idea stay the same. Specifying the async keywords says that your function will return a promise-like.
e.g
async function p () {
return 5
}
p.then(x => console.log(x)) // does print 5 even though we didn't explicitely write return Promise.resolve(5)
Now you have to ensure you await the async function when you call it:
getAvatarAPI: async login => {
return userService.getAvatar(login).then(blabla)
}
// DO NOT do it
this.userEmails.forEach(userMail => {
return await this.getAvatarAPI(userEmail)
})
In forEach loop above, you will do your getAvatarAPI call in sequence because await "stops" iterating as long as getAvatarAPI has not resolved.
The proper way would be
getAllNotesAPI: async entity => {
try { // notice the necesary try-catch block
const response = await noteService.getNotes(entity)
blabla
let promises = this.userEmails.map(userMail => {
return this.getA...
})
let result = await Promise.all(promises)
// eventually return result, or simply await Promise... without lefthand-side assignment
} catch (error) {
console.log(error);
}
console.log(this.end('loader'))
}

Async functions using value from a promise

So I know this question is asked a lot, but I'm trying to retrieve a variable that is created within a promise. The examples I've seen on here involve calling .then and using the data there, however what I'm trying to do involves an async function--which i cant use within the .then block.
Here's my code. I'm using the Asana API To call out a lists of tasks that are due. It successfuly logs it. But I want to save the list value from the last block as a variable that I can use elsewhere.
const asana = require('asana');
const client = asana.Client.create().useAccessToken("xxx");
client.users.me()
.then(user => {
const userId = user.id;
// The user's "default" workspace is the first one in the list, though
// any user can have multiple workspaces so you can't always assume this
// is the one you want to work with.
const workspaceId = user.workspaces[0].id;
return client.tasks.findAll({
assignee: userId,
workspace: workspaceId,
completed_since: 'now',
opt_fields: 'id,name,assignee_status,completed'
});
})
.then(response => {
// There may be more pages of data, we could stream or return a promise
// to request those here - for now, let's just return the first page
// of items.
return response.data;
})
.filter(task => {
return task.assignee_status === 'today' ||
task.assignee_status === 'new';
})
.then(list => {
console.log (util.inspect(list, {
colors: true,
depth: null
}));
})
.catch(e => {
console.log(e);
});
If you're open to rewriting your .then()'s as async/await something like this could work for you:
const fetch = require('node-fetch');
async function doit() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const json = await response.json();
console.log(json);
}
doit();

How to order the order of returned API calls with generators?

I'm practicing some more advanced Javascript techniques, and came across generators and iterators as something I wanted to look into. I know that I'm doing this incorrectly, but I'm not really sure how to go about it.
The idea of my little program is this: I want to make API calls to the OpenWeather API for four (or more, but I'm testing with four) cities. The cities are stored in an array and one by one, the city is appended to the URL and a fetch request is sent. Each response is appended to an array and the array is sent to the client.
This was my original code:
// node/express setup here
const cities = ["London%2Cuk", "New York%2Cus", "Johannesburg%2Cza", 'Kingston%2Cjm']
const url = process.env.URL_BASE;
const headers = {
"X-RapidAPI-Host": process.env.HOST,
"X-RapidAPI-Key": process.env.API_KEY
}
const requestInit = { method: 'GET',
headers: headers
};
const fetchWeather = (ep) => {
const appendedURL = url + ep;
return fetch(appendedURL, requestInit)
.then(r => r.json());
}
app.get('/', (req, res, err) => {
const data = []
Promise.all(
cities.map( async (city) => {
await fetchWeather(city)
.then(returns => {
data.push(returns)
})
})
)
.then(() => {
res.send(data)
return data;
})
.catch(err => console.log(err))
})
Right? Solid, works ok. But now I'm stuck on how to order it. The way I would think to do this is to switch await fetchWeather(city) to yield fetchWeather(city) and have a generator manager that would continue calling next(city) until the array had completed, but I'm having an issue figuring out the pattern. I refactored the api call to a generator and am testing out a generator management function.
The paradigm I have based on my understanding is this:
First .next() starts the iteration
Second .next(args) passes the designated city to the first yield
Third .next() sends the yielded fetch request and should (ideally) return the response object that can be .then()'d.
Here is my tester generator code:
function *fetchWeather() {
for (let i = 0; i < cities.length; i++){
const appendedURL = url + (yield);
yield fetch(appendedURL, requestInit)
.then(r => {
return r.json()
});
}
}
const generatorManager = (generator) =>{
if (!generator) {
generator = fetchWeather();
}
generator.next()
generator.next(cities[i])
generator.next().value.then( e =>
console.log(e));
}
I'm getting an error:TypeError: Cannot read property 'then' of undefined And I'm not sure where I'm going wrong here with my logic. How do I refactor this to allow me to wait for specific promises if I can't individually pass known values? I know there has to be a way, but I'm missing something.
Thanks in advance.
I don't understand what benefit you hope to get from using a generator here, but the reason you're getting that error is you're doing one to many .next()'s
The first generator.next() runs fetchWeather until the first yield, which is the yield at the end of const appendedURL = url + (yield);. The return value from calling generator.next() in this case is { value: undefined, done: false }
After that, generator.next(cities[i]) resumes fetchWeather, with cities[i] being the result of the previous yield. The generator continues running, calling fetch, then calling .then on that promise, and then yielding the resulting promise. So the return value that generatorManager sees from doing generator.next(cities[i]) is { value: /* a promise object */, done: false }.
So to fix that error, you need to reduce the number of calls you're making to generator.next
generator.next()
generator.next(cities[i]).value.then(e =>
console.log(e));
As mentioned in the comments, the usual way i'd do this is map the cities to promises, and then do promise.all. For example:
Promise.all(
cities.map((city) => fetchWeather(city)) // note, this is the original fetch weather, not the generator
).then((data) => {
res.send(data);
return data;
})
.catch(err => console.log(err))

Is there a way to set state for each iteration of a foreach

I'm working with an API within a React Application and I'm trying to make the API calls come back as one promise.
I'm using the Promise.all() method which is working great.
I'm stuck trying to set the results of two API calls to state with their own name. The promise code is working correctly and I am trying to forEach() or map() over the two sets of data and save them to state with their own name.
I'm sure there is a simple solution but I've been scratching my head for far too long over this!
I've tried searching all the docs for .map and .forEach with no luck!
fetchData(){
this.setState({loading: true})
const urls = ['https://api.spacexdata.com/v3/launches/past', 'https://api.spacexdata.com/v3/launches']
let requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => {
return responses
})
.then(responses => Promise.all(responses.map(r => r.json())))
.then(launches => launches.forEach(obj => {
// I need to set both values to state here
}))
.then(() => this.setState({loading: false}))
}
The API call returns two different arrays. I need to set both arrays to State individually with their own name. Is this possible?
If I understand your question correctly, a better approach might be to avoid iteration altogether (ie the use of forEach(), etc). Instead, consider an approach based on "destructuring syntax", seeing you have a known/fixed number of items in the array that is resolved from the prior promise.
You can make use of this syntax in the following way:
/*
The destructing syntax here assigns the first and second element of
the input array to local variables 'responseFromFirstRequest'
and 'responseFromSecondRequest'
*/
.then(([responseFromFirstRequest, responseFromSecondRequest]) => {
// Set different parts of state based on individual responses
// Not suggesting you do this via two calls to setState() but
// am doing so to explicitly illustrate the solution
this.setState({ stateForFirstRequest : responseFromFirstRequest });
this.setState({ stateForSecondRequest : responseFromSecondRequest });
return responses
})
So, integrated into your existing logic it would look like this:
fetchData() {
this.setState({
loading: true
})
const urls = ['https://api.spacexdata.com/v3/launches/past', 'https://api.spacexdata.com/v3/launches']
const requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([responseFromFirstRequest, responseFromSecondRequest]) => {
this.setState({ stateForFirstRequest : responseFromFirstRequest });
this.setState({ stateForSecondRequest : responseFromSecondRequest });
return responses
})
.then(() => this.setState({
loading: false
}))
}
If the two arrays won't interfere with each other in the state, is there a problem with just calling setState in each iteration?
.then(launches => launches.forEach(obj => {
this.setState({ [obj.name]: obj });
}))
If you want to minimise the number of updates then you can create an Object from the two arrays and spread that into the state in one call:
.then(launches => this.setState({
...launches.reduce((obj, launch) => {
obj[launch.name] = launch
return obj
}, {})
}))
forEach also provides the index as the second parameter. Wouldn't something like this work?
launches.forEach((obj, idx) => {
if (idx === 0) {
this.setState('first name', obj);
} else if (idx === 1) {
this.setState('second name', obj);
}
})
Also, this portion literally does nothing..
.then(responses => {
return responses
})
and the Promise.all() here also does nothing.
.then(responses => Promise.all(responses.map(r => r.json())))
should be
.then(responses => responses.map(r => r.json()))

Categories

Resources