ForEach wait for Observable Subscription - javascript

I have this array orderCodes that has the codes for specific orders, and then with the code I can get the details of that order (each order has multiple products), and I need to extract the code of each product inside the order details.
The getOrderDetails()is an Observable with results(an array of the products), and each resulthas a code which is what I need.
this.orderCodes.forEach((orderCode) => {
loadOrderDetails(orderCode);
getOrderDetails().subscribe((order: any) => {
if (order.results) {
order.results.map((result) => {
console.log(result.code);
});
}
});
});
I've tried with this forEach but since I'm subscribing to the Observable the forEach skips to the next iteration and I need it to wait
Any ideas?

rxjs way would be
from(this.orderCodes).pipe(
concatMap((orderCode) => // concatMap operator makes your items come "in order" one after another
defer(() => {
loadOrderDetails(orderCode);
return getOrderDetails();
}))
).subscribe((order: any) => {
if (order.results) {
order.results.map((result) => {
console.log(result.code);
});
}
});
or you could convert to promises and use async await (more elegant, but usually less prefered way in angular because of converting to promises and change detection issues if done wrong, but it depends...)
async myFunctionThatDoesAllThis(...) {
....
for(let orderCode of this.orderCodes) {
loadOrderDetails();
const order = await getOrderDetails().pipe(take(1)).toPromise(); // pipe(take(1)) could be skipped if getOrderDetails is just an http request.
if(order.results) {
order.results.forEach((result) => {
console.log(result.code);
});
}
}

Related

Knexjs how to handle multiple updates

I´m quite unsure on how to handle multiple updates / inserts in knex and return whatever it was successfull on the end or not.
I´m passing an array through req.body loop through it and trigger actions based on informations inside the array.
Example:
const data = [...req.body]
for(let i = 0; i < data.length; i++) {
data[i].totals.length
for(let y = 0; y < data[i].totals.length; y++) {
if(data[i].totals[y].info === "Holiday") {
calcHoliday(data[i].totals[y].total, data[i].id)
} else if(data[i].totals[y].info === "ZA") {
calcZA(data[i].totals[y].total, data[i].id)
}
}
calcOvertime(data[i].totalSum, data[i].id)
if(i === data.length -1) {
res.json("Success")
}
}
The Array I´m passing in looks like this:
[
{
"id": 1,
"totals": [
{
"info": "Holiday",
"total": 4
}
]
},
{
"id": 1,
"totals": [
{
"info": "Holiday",
"total": 4
}
]
}
]
Function Example which gets called in for loop:
const calcHoliday = (hours, userid) => {
knex.transaction(trx => {
trx.insert({
created_at: convertedTime,
info: "Booking Holiday - Hours: " + hours,
statuscode: 200
}).into("logs")
.then(() => {
return trx("hours")
.decrement("holiday_hours", hours)
}).then(trx.commit)
.catch(trx.rollback)
}).then(() => console.log("WORKED"))
.catch(err => console.log(err))
}
This is working perfectly fine but I can´t figure out how to gather the results from each table update in order to respond if everything worked or an error appeared. If I call e.g. after one calcHoliday call .then(resp => res.json(resp) I receive only the response from the first operation.
In short I need a way on how to res.json if everything succeeded or an error appeared somewhere.
Thanks in advance!
TLDR;
Turning your insert calls into an array of promises and then using await and a Promise.all() / Promise.allSettled() structure might solve this problem, but there are some UX decisions to make on what to rollback and how to return errors.
Error Handling Choices:
Any error --> all insertions in all loop iterations should be rolled back
Do you want partial success? The way the code is written now, rollback only applies to items in one function call. If one of the hour-decrement calls fails, it will roll back one log insert, but not any that succeeded for previous data in the loop. If you want the whole dataset to rollback, you'd need to pass the txn through each function call or do a bulk insert of all of your rows in one function call, which might be nice for performance reasons anyway depending on the use case.
Partial success --> commits successes, rolls back single loop iterations that fail, sends detailed list of errors and successes
You'd want to use Promise.allSettled(), which aggregates the successes and errors as an array from all promises in the loop.
Partial success --> commits the successes, rolls back single loop iterations that fail, sends just one error
Opinion: This can be a misleading UX unless the error is "some of the insertions were unsuccessful" and the endpoint is idempotent
This looks closest to what you're describing you want. If this is the case, you'd want to use Promise.all(), which throws an error as soon as one promise in the array errors.
Example Implementation:
Since the original code is incomplete, this is a loose, incomplete example of what option 2/3 might look like. This could easily be transformed into option 1.
First, it might help to modify all of your functions with asynchronous calls to be fulfillable as promises. Async/await helps avoid .then() trees that are hard to reason about.
const calcHoliday = async (hours, userid) => {
try {
const result = await knex.transaction(async(trx) => {
await trx.insert({
created_at: convertedTime,
info: "Booking Holiday - Hours: " + hours,
statuscode: 200
}).into("logs")
return trx("hours").decrement("holiday_hours", hours)
}
return result
} catch(err) {
console.log("It didn't work.")
throw new Error(`Error: Failure to insert for user ${userid}:`, err)
}
}
Here are some utilities to get the data transformed, and to get the appropriate unfulfilled promise to supply to the map in Promise.all/allSettled.
/*
Here's an example of how you might transform the original data with maps in order to avoid nested for-loops:
[
{ id: 1, info: 'Holiday', total: 4 },
{ id: 1, info: 'Holiday', total: 4 }
]
*/
const flatData = data.map(item => {
return item.totals.map(total => ({
id: item.id,
...total
}))
}).flat()
// Returns the appropriate promise based on data
const getTotalsPromises = (row) => {
const {info, id, total} = row
if(info === "Holiday") {
return calcHoliday(total, id)
} else if(info === "ZA") {
return calcZA(total, id)
}
}
const getcalcOvertimePromises = (rowInData) = {
// work left to reader
return calcOvertime(rowInData.correctData, rowInData.otherData)
}
If you want option 2:
// Replaces the loop
// Fulfills *all* the promises, creating an array of errors and successes
const responses = await Promise.allSettled([
...flatData.map(getTotalsPromises),
...data.map(getCalcOvertimePromises)
])
// insert loop here to do something with errors if you want
res.send(responses)
OR Option 3
Create an array of all of the promises you want to run, run them, and process up to one error.
// Replaces the loop
// Runs the promises and waits for them all to finish or the first error.
try {
const responses = await Promise.all([
...flatData.map(getTotalsPromises),
...data.map(getCalcOvertimePromises)
])
res.send(responses)
} catch(err){
// Reached if one of the rows errors
res.send(err)
}
Docs:
Promise.allSettled
Promise.all

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 };
})
);

Expected to return a value at the end of arrow function in react

I get the warning in the title when compiling. I understand that it is about not handling some cases of if, but how can I filter before mapping in the correct way?
componentDidMount() {
this.props.UserReducer.user.employeeInfoList.map(role => {
if (role.employeeType) this.rolesOfUser.push(role.employeeType);
if (role.xdockId) this.xdockIdsOfUser.push(role.xdockId);
});
}
It is because you are misusing map which is used for mapping/transforming one array to another. Having a call to map without a return value indicates a problem, as you shouldn't be using it to just iterate over an array performing some action.
It looks like what you really wanted was a forEach call.
To filter an array use Array#filter. Also you can use Array#forEach for your case
componentDidMount() {
this.props.UserReducer.user.employeeInfoList.forEach(role => {
if (role.employeeType) this.rolesOfUser.push(role.employeeType);
if (role.xdockId) this.xdockIdsOfUser.push(role.xdockId);
});
}
Or
componentDidMount() {
const rolesOfUser = this.props.UserReducer.user.employeeInfoList.filter(role => {
return role.employeeType;
})
const xdockIdsOfUser = this.props.UserReducer.user.employeeInfoList.filter(role => {
return role.xdockId;
})
// Do smth with both arrays
}

How to wait the operations until finishing they work using RXJS?

I need a way to wait the RXJS finish his work.
This is my function:
getLastOrderBeta() {
return this.db.list(`Ring/${localStorage.getItem('localstorage')}`, {
query: {
equalTo: false,
orderByChild: 'status',
limitToLast: 2,
}
})
.map((orders: any) => {
return orders.map((order: any) => {
order.userInfo = this.getProfile(order.userid);
return order;
});
}).do((s:any)=>{
console.log(s);
console.log(here);
});
}
When I have one item the log is normal:
When I have two items same log is normal:
But when I have three items the log is duplicated:
Maybe the limit to last causing this issue.
Any help to fix that?
Thanks.
Assuming your getLastOrderBeta method returns an Observable you could transform it to a Promise using toPromise:
const valueAsPromise = getLastOrderBeta().toPromise()
This Promise will reject if your Observable throws anywhere and resolves with the last emitted value.
valueAsPromise
.then((value) => {
// last value from your observable
})
.catch((error) => {
// something bad happened
})
If you need to wait for multiple emitted values then you can call toArray first:
const valuesAsPromise = getLastOrderBeta().toArray().toPromise()
Which will result in a Promise which resolves with all the emitted values.
If you need custom logic to aggregate the emitted values, use reduce.

Categories

Resources