Firebase Realtime Database - multiple sequencial fetch and write operations without nesting promises - javascript

I am using Firebase Realtime database for storage.
In order to update contact data for users when a "Note" is updated I need to perform some sequential operations. I wish to update/push contacts nodes to for each "affectedUser" that is included in the "Note".
Below is a crude representation of my database.
-notes
-note123456 <-- Note being updated
affectedUsers: {'L1234567890ABC': true, 'L0987654321XYZ': true} <-- affectedUsers
-users <-- Compose contact objects from here for all affectedUsers
-L1234567890ABC
name
alias
email
avatar
favouriteColour
-L0987654321XYZ
-contacts <-- Add new contacts here
-L1234567890ABC
-ABCDEFGHIJKLMNO0123 <-- Already added contact
alias
name
-L0987654321XYZ
-ABCDEFGHIJKLMNO0123 <-- Already added contact
My starting point is a list of "affectedUsers" that needs to be updated - a list of user Ids.
The desired, simplified, workflow looks like this
Iterate "affectedUsers" and compose "contact cards"
Then iterate all "affectedUsers" and add contact cards to each affectedUser
My current code
const dbRoot = snapshot.ref
affectedUsers = ['-L1234567890ABC', '-L0987654321XYZ']
let promises = [];
affectedUsers.forEach((affectedUser) => {
const ref = dbRoot.child(`users/${affectedUser}`)
promises.push(
ref.once('value', (userSnapshot)=>{
const userNodeData = userSnapshot.val()
const contactObject = {
alias: userNodeData.alias,
name: userNodeData.name
}
return contactObject
);
})
Promise.all(promises).then((contactObjects) => {
let updateContactsPromises = [] //Collect second promise chain
//Check contacts of affectedUsers
affectedUsers.forEach((userId) => {
const ref = dbRoot.child(`contacts/${userId}`)
updateContactsPromises.push(
ref.once('value', (updateUserContactsSnapshots) => {
updateUserContactsNodeData = updateUserContactsSnapshots.val()
//Remove userId from additions, prepare database update object, push data
//...
})
)
})
//Execute second, and last promise chain
Promise.all(updateContactsPromises) //Line 328
.then(()=>{
//...
})
.catch((err)=>{})
})
.then(()=>{
//...
})
.catch((err)=>{})
I realize nested promises is not a good thing - since I get warnings when performing a firebase deploy. ;)
328:9 warning Avoid nesting promises promise/no-nesting
328:9
warning Avoid nesting promises promise/no-nesting
✖ 2 problems (0 errors, 2 warnings)
How can I make sure my calls gets executed sequentially without nesting promises?

The problem is when you have a .then directly inside another .then. Usually, this can be fixed by returning the next Promise, instead of having a nested then. For example, change
prom.then(() => {
getAnotherProm().then(handleOther);
});
to
prom.then(() => {
return getAnotherProm()
})
.then(handleOther);
Here, you can return the second Promise.all to avoid nesting .thens:
Promise.all(promises)
.then((contactObjects) => {
let updateContactsPromises = [] //Collect second promise chain
//Check contacts of affectedUsers
affectedUsers.forEach((userId) => {
// ...
})
//Execute second, and last promise chain
return Promise.all(updateContactsPromises)
})
.then((updateContactsValues) => {
// handle resolve value of updateContactsPromises
})
.catch((err) => {
// handle errors
})
Remember to only .catch at a level where you can handle the error properly, and you can chain .thens together to avoid having to duplicate .catches.
You can also use .map instead of .forEach to construct the array of Promises all at once, eg:
const dbRoot = snapshot.ref
affectedUsers = ['-L1234567890ABC', '-L0987654321XYZ']
const affectedUserPromises = affectedUsers.map((affectedUser) => {
const ref = dbRoot.child(`users/${affectedUser}`)
return ref.once('value', (userSnapshot) => {
const userNodeData = userSnapshot.val()
return {
alias: userNodeData.alias,
name: userNodeData.name
};
});
});
Promise.all(affectedUserPromises).then((contactObjects) => {
// ...

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

Throwing an error TypeError: Cannot add property 0, object is not extensible

Please see the code below for reference.
export const listenToHotels = (hotelIds: string[]): Observable < Hotel[] > => {
return new Observable < Hotel[] > ((observer) => {
const hotels: any = [];
hotelIds.forEach((hotelId) => {
let roomingList: any;
return FirestoreCollectionReference.Hotels()
.doc(hotelId)
.onSnapshot(
(doc) => {
roomingList = {
hotelId: doc.id,
...doc.data()
}
as Hotel;
console.log(`roomingList`, roomingList);
hotels.push({
...roomingList
});
},
(error) => observer.error(error)
);
});
//Check for error handling
observer.next(hotels);
console.log('hotels', hotels);
});
};
As you can see I am trying to run a forEach on a hotelId Array and in that firestore listener is being executed. Now I want to save the response and push that into hotels array but it gives me an error object not extensible error.
The thing is observer and console.log('hotels',hotels) run first because of promise being executed at later stage.
Please let me know how can I resolve this issue.
I think you could use map instead of forEach, because you could use await with map
example of using map and await: https://flaviocopes.com/javascript-async-await-array-map/
Check out forkJoin. It takes an array of Observables and subscribes to them at the same time.
This example is written in Angular but the operators should be identical as long as you're using a current version of rxjs.
I'm starting with an array of User objects (users.mock.ts)
Each Object is then map to a single Observable (mapCalls)
I now have an array of Observables that will make HTTP calls. These calls will not be made until subscribed to
forkJoin is then added as a wrapper to the array
You subscribe to that forkJoin. All of the objects will make the call.
When the calls have completed, the logic inside of subscribe() will be run
So in your case:
Map the Hotel Ids to a variable calls. Each item is the Observable logic you posted
You would then run forkJoin(calls).subscribe() to make all the calls
export const listenToHotels = (hotelIds: string[]): Observable<Hotel[]> => {
const hotelsObservable = hotelIds.map((hotelId) => {
let roomingList: any;
return new Observable<Hotel>((observerInside) => {
FirestoreCollectionReference.Hotels()
.doc(hotelId)
.onSnapshot(
(doc) => {
roomingList = { hotelId: doc.id, ...doc.data() } as Hotel;
observerInside.next(roomingList);
},
(error) => observerInside.error(error)
);
});
});
const combinedObservable = combineLatest(hotelsObservable);
return combinedObservable;
//Check for error handling
};
The issue was how I was handling the chained observables(Execution of promise is delayed than a normal code because they will be put in micro-queue first). They need to be handled using zip or combineLatest but latter is much better in this use case as we need the latest values for the observables.

React with conditional promises chain and wait for them all before send dispatch

I'm new to react. I have a user list that fetched once when the component mounts.
and before the results dispatched to the reducer, it needs to loop inside the userlist and get user fullname/title from another endpoint, then add a new property to userlist object.
I cant figure out how to wait for all promises (getUserById() function) to finish before calling a dispatch. I tried the solution here, but failed:
How to return many Promises and wait for them all before doing other stuff
code below just to illustrate what I want:
const {
listUsers,
fetchUserData
} = useContext(GlobalContext);
const getUserById = async (userId) => {
return sp.web.siteUsers.getById(userId).get().then(user => user.Title);
}
useEffect(() => {
sp.web.lists.getById("8C271450-D3F9-489C-B4FC-9C7470594466").items.get()
.then(userLists => {
userLists = userLists.map(list => {
if (list.Person_x0020_ResponsibleId) {
getUserById(list.Person_x0020_ResponsibleId).then(username => {
list['Person_Responsible'] = username; // -> fetch user fullname and title
})
} else { // -> if id is null
list['Person_Responsible'] = '-';
}
return list
});
fetchListSuccess(userLists); // -> dispatch result to reducer
});
}, []);
You can accomplish this using Promise.all. First you need an array of promises from your second API calls. Then we'll give this array to Promise.all, and it will wait until they all resolve.
I've rewritten using async/await syntax. It works the same as using .then with the promises, but when you're working with a promise chain that's this complex it's easier to follow with async/await.
useEffect(async () => {
const userLists = await sp.web.lists.getById('8C271450-D3F9-489C-B4FC-9C7470594466').items.get();
const promises = userLists.map(async (list) => {
if (list.Person_x0020_ResponsibleId) {
const username = await getUserById(list.Person_x0020_ResponsibleId);
list.Person_Responsible = username; // -> fetch user fullname and title
} else { // -> if id is null
list.Person_Responsible = '-';
}
return list;
});
await Promise.all(promises);
fetchListSuccess(userLists); // -> dispatch result to reducer
}, []);
A few notes:
You don't really need to reassign userLists in the map, because you're just adding a property to existing objects. This will happen without the map.
Now the map is being used to return an array of promises for your second API calls. This is used by the Promise.all to wait until all those promises resolve.

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

Promises and race-conditions

I'm currently a little stuck with a race condition, and I'm pulling my hair out. Essentially, I'm querying an API, adding the results to the DB, then doing stuff with the returned/saved data.
I'm less asking about this specific problem, and more the design pattern of how to fix this sort of issue. The line p.push(formatter.createAccounts(account, id)); might run upto 1000 times, and might take 20 seconds or so.
Any ideas on what I'm doing wrong will be super helpful
Thanks,
Ollie
// get all users
Promise.all(updatedUsers)
.then(() => {
// create an array of promises
const p = [];
users.then((user) => {
...
ids.forEach((id) => {
const accounts = foo.getAccounts(id, true); // returns a promise
accounts.then((account) => {
// add the return data from here to the promise array at the bottom
p.push(formatter.createAccounts(account, id));
});
});
});
// this would ideally be a promise array of the data from above - but instead it just evaluates to [] (what it was set to).
Promise.all(p).then(() => {
// do my stuff that relies on the data here
})
});
The problem is that you are not including the foo.getAccounts promise into your promises array. Modified version:
Promise.all(updatedUsers)
.then(() => {
users.then((user) => {
return Promise.all(ids.map(id => {
//for each ID we return a promise
return foo.getAccounts(id, true).then(account => {
//keep returning a promise
return formatter.createAccounts(account, id)
})
}).then((results) => {
//results is an array of the resolved values from formatter.createAccounts
// do my stuff that relies on the data here
})
})
//rest of closing brackets

Categories

Resources