mapping a Promise function to an array does not persist results - javascript

I have an object with two arrays as properties:
I want to populate the arrays by running promises in series.
I fetch the result of the promises, and map a function to decorate all the items in my arrays.
While one array get populated and persist, the other get populated only while in the map function, but at the end the array is returned still empty.
Can you help to understand why?
I check the Promise is actually returned, and indeed in one case it works, not in the other.
this is my pseudo-code:
function formatMyObject( arrayOfIds ) {
// initialize the objet
var myObj = {
decorators = [],
nodes = []
...
}
// I map the Promise reconciliate() and push the results in the array:
return reconciliateNode(arrayOfIds)
.then( data => {
data.map( node => {
// I fetch results, and myObj.nodes
myObj.nodes.push( { ... })
})
})
return myObj
})
.then( myObj => {
// myObj.nodes is now a NON empty array
// I want to the same with myObj.decorators:
var data = myObj.nodes
// I think I am doing just as above:
data.map( node =>
decorateNode(node.source)
.then( decoration => {
decoration = decoration[node.source]
myObj['decorators'].push( {
...
} )
// I check: the array is NOT empty and getting populated:
console.log('myObj.decorators', myObj)
debugger
})
)
// instead now, just after the map() function, myObj.decorators is EMPTY!
console.log('myObj.decorators', myObj);
debugger
return myObj
)
... // other stuff
}

As in the second case the map callback returns a promise, that case is quite different from the first case.
In the second case, you would need to await all those promises, for which you can use Promise.all.
The code for the second part could look like this:
.then( myObj => {
return Promise.all(myObj.nodes.map(node => decorateNode(node.source)));
}).then(decorations => {
myObj.decorators = decorations.map(decoration => {
decoration = decoration[node.source];
return ({
...
});
})
console.log('myObj.decorators', myObj);
return myObj;
})

Related

Skipping top level of JSON data and retrieving data below it via JavaScript

Via a microservice, I retrieve several packages of JSON data and spit them out onto a Vue.js-driven page. The data looks something like this:
{"data":{"getcompanies":
[
{"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"},
{"id":7,"name":"McMillan","address":null,"zip":"15090"},
{"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}
]
}}
{"data":{"getusers":
[{"id":22,"name":"Fred","address":"Parmesean Street","zip":"15090"},
{"id":24,"name":"George","address":"Loopy Lane","zip":"15090"},
{"id":25,"name":"Lucy","address":"Farm Road","zip":"15090"}]}}
{"data":{"getdevices":
[{"id":2,"name":"device type 1"},
{"id":4,"name":"device type 2"},
{"id":5,"name":"device type 3"}]}}
...and I successfully grab them individually via code like this:
getCompanies() {
this.sendMicroServiceRequest({
method: 'GET',
url: `api/authenticated/function/getcompanies`
})
.then((response) => {
if(response.data) {
this.dataCompanies = response.data.getcompanies
} else {
console.error(response)
}
}).catch(console.error)
}
...with getUsers() and getDevices() looking respectively the same. getCompanies() returns:
[{"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"},
{"id":7,"name":"McMillan","address":null,"zip":"15090"},
{"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}]
...which I relay to the Vue template in a table, and this works just fine and dandy.
But this is obviously going to get unwieldy if I need to add more microservice calls down the road.
What I'm looking for is an elegant way to jump past the response.data.*whatever* and get to those id-records with a re-useable call, but I'm having trouble getting there. response.data[0] doesn't work, and mapping down to the stuff I need either comes back undefined, or in bits of array. And filtering for response.data[0].id to return just the rows with ids keeps coming back undefined.
My last attempt (see below) to access the data does work, but looks like it comes back as individual array elements. I'd rather not - if possible - rebuild an array into a JSON structure. I keep thinking I should be able to just step past the next level regardless of what it's called, and grab whatever is there in one chunk, as if I read response.data.getcompanies directly, but not caring what 'getcompanies' is, or needing to reference it by name:
// the call
this.dataCompanies = this.getFullData('companies')
getFullData(who) {
this.sendMicroServiceRequest({
method: 'GET',
url: 'api/authenticated/function/get' + who,
})
.then((response) => {
if(response) {
// attempt 1 to get chunk below 'getcompanies'
Object.keys(response.data).forEach(function(prop) {
console.log(response.data[prop])
})
// attempt 2
// for (const prop in response.data) {
// console.log(response.data[prop])
// }
let output = response.data[prop] // erroneously thinking this is in one object
return output
} else {
console.error(response)
}
}).catch(console.error)
}
...outputs:
(63) [{…}, {…}, {…}] <-- *there are 63 of these records, I'm just showing the first few...*
0: {"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"}
1: {"id":7,"name":"McMillan","address":null,"zip":"15090"},
2: {"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}...
Oh, and the return above comes back 'undefined' for some reason that eludes me at 3AM. >.<
It's one of those things where I think I am close, but not quite. Any tips, hints, or pokes in the right direction are greatly appreciated.
I feel it's better to be explicit about accessing the object. Seems like the object key is consistent with the name of the microservice function? If so:
getData(functionName) {
return this.sendMicroServiceRequest({
method: 'GET',
url: "api/authenticated/function/" + functionName
})
.then( response => response.data[functionName] )
}
getCompanies(){
this.getData("getcompanies").then(companies => {
this.dataCompanies = companies
})
}
let arrResponse = {data: ['x']};
let objResponse = {data: {getcompanies: 'x'}};
console.log(arrResponse.data[0]);
console.log(Object.values(objResponse.data)[0]);
response.data[0] would work if data was an array. To get the first-and-only element of an object, use Object.values(response.data)[0] instead. Object.values converts an object to an array of its values.
Its counterparts Object.keys and Object.entries likewise return arrays of keys and key-value tuples respectively.
Note, order isn't guaranteed in objects, so this is only predictable in your situation because data has exactly a single key & value. Otherwise, you'd have to iterate the entry tuples and search for the desired entry.
firstValue
Let's begin with a generic function, firstValue. It will get the first value of an object, if present, otherwise it will throw an error -
const x = { something: "foo" }
const y = {}
const firstValue = t =>
{ const v = Object.values(t)
if (v.length)
return v[0]
else
throw Error("empty data")
}
console.log(firstValue(x)) // "foo"
console.log(firstValue(y)) // Error: empty data
getData
Now write a generic getData. We chain our firstValue function on the end, and be careful not to add a console.log or .catch here; that is a choice for the caller to decide -
getData(url) {
return this
.sendMicroServiceRequest({ method: "GET", url })
.then(response => {
if (response.data)
return response.data
else
return Promise.reject(response)
})
.then(firstValue)
}
Now we write getCompanies, getUsers, etc -
getCompanies() {
return getData("api/authenticated/function/getcompanies")
}
getUsers() {
return getData("api/authenticated/function/getusers")
}
//...
async and await
Maybe you could spruce up getData with async and await -
async getData(url) {
const response =
await this.sendMicroServiceRequest({ method: "GET", url })
return response.data
? firstValue(response.data)
: Promise.reject(response)
}
power of generics demonstrated
We might even suggest that these get* functions are no longer needed -
async getAll() {
return {
companies:
await getData("api/authenticated/function/getcompanies"),
users:
await getData("api/authenticated/function/getusers"),
devices:
await getData("api/authenticated/function/getdevices"),
// ...
}
}
Above we used three await getData(...) requests which happen in serial order. Perhaps you want all of these requests to run in parallel. Below we will show how to do that -
async getAll() {
const requests = [
getData("api/authenticated/function/getcompanies"),
getData("api/authenticated/function/getusers"),
getData("api/authenticated/function/getdevices")
]
const [companies, users, devices] = Promise.all(requests)
return { companies, users, devices }
}
error handling
Finally, error handling is reserved for the caller and should not be attempted within our generic functions -
this.getAll()
.then(data => this.render(data)) // some Vue template
.catch(console.error)

How to map data correctly before subscription?

I have following function:
this.localStorage.getItem('user').subscribe(user => {
this.user = user;
this.authSrv.getOrders(this.user.einsender).pipe(map(orders => {
map(order => { order["etz"] = "23"; return order})
return orders;
})).subscribe(orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
})
})
The result without the map is:
[{id: 1, etz: "21"}]
With the map from above I try to enter the array, then the order and in the order I try to change the etz property but somehow nothing changes. Can someone look over?
I appreciate any help!
I see multiple issues here.
Try to avoid nested subscriptions. Instead you could use one of the RxJS higher order mapping operators like switchMap. You could find differences b/n different higher order mapping operators here and here.
To adjust each element of the array you need to use Array#map method in addition to the RxJS map operator.
You could use JS spread operator to adjust some of the properties of the object and retain other properties.
Try the following
this.localStorage.getItem('user').pipe(
switchMap(user => {
this.user = user;
return this.authSrv.getOrders(this.user.einsender).pipe(
map(orders => orders.map(order => ({...order, order['etz']: '23'})))
});
})
).subscribe(
orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
},
error => {
// good practice to handle HTTP errors
}
);
map is an operator that goes in a pipe like this:
someObs$.pipe(map(arg => { return 'something'}));
You've done this:
someObs$.pipe(map(arg => {
map(arg => { return 'something' }) // this line here does nothing
return arg;
}));
It doesn't make any sense to use map inside the function you've given to map

How to combine two observables to create new observable?

I have two services named 'PatientsService' and 'AppointmentService'. In third service 'AppointedPatientsService', I want to subscribe to AppointmentService to get all booked appointments with patientId and after that I want to repeatedly subscribe to PatientsService.getPatient(patientId) to get Patient's data with patientId. And then, I want to return new array named allAppointedPatients which holds all appointments with patient's data. I tried this...
getAppointments() {
let allAppointments: Appointment[] = [];
const allAppointedPatients: AppointedPatient[] = [];
return this.appointmentService.fetchAllAppointments().pipe(
take(1),
tap(appointments => {
allAppointments = appointments;
for (const appointment of allAppointments) {
this.patientsService.getPatient(appointment.patientId).pipe(
tap(patient => {
const newAppointment = new AppointedPatient(patient.firstName,
patient.lastName,
patient.address,
patient.casePaperNumber,
appointment.appointmentDateTime);
allAppointedPatients.push(newAppointment);
})
).subscribe();
}
return allAppointedPatients;
}),
pipe(tap((data) => {
return this.allAppointedPatients;
}))
);
}
This is not working and I know there must be better way to handle such scenario. Please help...
You are messing up the async code (observables) with sync code by trying to return the allAppointedPatients array synchronously.
Understand first how async code is working in Javascript and also why Observables (streams) are so useful.
Try the code below and make sure you understand. Of course, I was not able to test it so make your own changes if needed.
getAppointments(): Observable<AppointedPatient[]> {
return this.appointmentService.fetchAllAppointments()
.pipe(
switchMap(appointments => {
const pacientAppointments = [];
for (const appointment of allAppointments) {
// Extract the data aggregation outside or create custom operator
const pacientApp$ = this.patientsService.getPatient(appointment.patientId)
.pipe(
switchMap((pacient) => of(
new AppointedPatient(
patient.firstName,
patient.lastName,
patient.address,
patient.casePaperNumber,
appointment.appointmentDateTime
)
))
)
pacientAppoinments.push(pacientApp$);
}
return forkJoin(pacientAppointments);
});
}
You can use forkJoin:
forkJoin(
getSingleValueObservable(),
getDelayedValueObservable()
// getMultiValueObservable(), forkJoin on works for observables that complete
).pipe(
map(([first, second]) => {
// forkJoin returns an array of values, here we map those values to an object
return { first, second };
})
);

Angular RxJs - fill array in the second stream (nested stream)

I have in my Angular application a list of a specific model. Each item of this list has a propery xyzList. This xyzList property should be filled from a request which depends on the id of the first request. Here an example:
Model:
{
id: number;
name: string;
xyzList: any[]
}
Now I have two requests:
Request 1: Fills the model, but not the xyzList
this.myVar$ = this.myService.getElements<Model[]>();
Request 2: Should fill xyzList
this.myService.getXyzElements<XyzModel[]>(idOfTheModelOfTheFirstRequest);
At first I thought something like that:
this.myService.getElements<Model[]>()
.pipe(
mergeMap(items => items)
mergeMap(item => {
return this.myService.getXyzElements<XyzModel[]>(item.id)
.pipe(
map(xyzList => {
item.xyzList = xyzList
return item;
})
)
})
)
This does not work as I have flattened my list and I need an Observable<Array[]>, but I think is more or less clear what I want to achieve. I assume I can achieve this with forkJoin for instance, but I don't know how.Or is there a way to convert the flattened list back to list?
You need to use toArray(), because mergeMap/flatMap is used to flatten the array of data stream. It will return an Array instead of data steam.
this.myService.getElements<Model[]>()
.pipe(
mergeMap(items => items)
mergeMap(item => {
return this.myService.getXyzElements<XyzModel[]>(item.id)
.pipe(
map(xyzList => {
item.xyzList = xyzList
return item;
})
)
}),
toArray()
)
Well, I understand that you have a service function that return a list of "items". EachItem you need call another service function that return the data "xyzList".
So, each "item" must be a call, then, you create an array of "calls"
myService.getElements().pipe(
switchMap(result=>{
//We can not return the "list"
let calls:Observable[]=[]
//Before, the first calls was the own result
calls.push(of(result));
//Each result push the calls
result.forEach(e=>{
calls.push(myService.getXyzElements(e.id));
})
return forkJoin(calls).pipe(map(result=>{
//in result[0] we have the list of items
//In result[1] we have the Xyz of items[0]
//In result[2] we have the Xyz of items[1]
int i:number=0;
result[0].map(x=>{
i++;
return {
...x,
xyzList:result[i]
}
})
return result[0];
}))
})
).subscribe(res=>console.log(res));

Why am I getting back "undefined" from my Axios call, when it should return me objects?

Problem Intro:
In my React app I'm making some API calls to Github to fetch some user data. In another function I call these functions and wait for it with Axios's .all() method.
I want to fetch the data and do something with it using .then() but the returned value is just an array with 2 times undefined
What is the expected thing to happen:
It should return me 2 player objects with profile infos in the profile key of the object and a score value in the score key of the object.
What is my app doing in a nutshell?
It fetches the data from 2 usernames and they can "battle" each other. It just fetches the score and the followers and returns a sum. At the end it (should) returns an array with 2 player objects already sorted with the winner in the first place (exampleArray[0]).
General information's
It's an react app using components. It's really about one very tiny component and the helper functions in another file.
Here is where I call my custom function (the one returning undefined):
componentDidMount() {
const players = queryString.parse(this.props.location.search); //<== the usernames
const playersArray = [players.playerOneName, players.playerTwoName];
console.log(playersArray); // <== this logs the output as expected (an array with 2 usernames)
battle(playersArray).then((data) => { // <== the function it's all about
console.log(data); // <== data is => [undefined, undefined];
})
}
Next is the battle function from above that uses 2 other functions:
battle
export function battle(players) { // <== players aray with 2 usernames as string
return axios.all(players.map(getUserData)) // <== look 1 function below
.then(sortPlayers) // <== two functions below
.catch(handleError)
}
getUserData
let getUserData = (player) => {
axios.all([
getProfile(player),
getRepos(player)
]).then((data) => {
return {
profile: data[0],
score: calculateScore(data[0], data[1])
}
})
}
sortPlayers
let sortPlayers = (players) => {
return players.sort((a, b) => {
return b.score - a.score;
})
}
Ok so they also use other functions but they are really not too complicated. Let me know when you need examples from the other little helpers too.
I tried it with placing the debugger in different spots in the code and console logged different things, but I can't come through (first time I'm really working with promises). Sitting now 2 hours in front of this tiny problem and I can't figure it out.
I think the problem lies somewhere in battle function itself or getUserData
At the end a little screenshot, what the output of my console.log looks: http://prntscr.com/hz5abq
Thanks in advance
You don't have anything being returned in getUserData . Either add a return or remove the {} wrapping axios.all
let getUserData = (player) => {
return axios.all([
getProfile(player),
getRepos(player)
]).then((data) => {
return {
profile: data[0],
score: calculateScore(data[0], data[1])
}
})
}
getUserData needs to return the promise that it creates. At the moment it's not returning anything, so an implicit undefined is returned, and thus players.map(getUserData) results in an array of [undefined, undefined]
Ie, do this:
let getUserData = (player) => {
// VVV added return statement
return axios.all([
getProfile(player),
getRepos(player)
]).then((data) => {
return {
profile: data[0],
score: calculateScore(data[0], data[1])
}
})
}

Categories

Resources