For loop with nested Firebase queries Javascript - javascript

I have a node with the users list and a second node with the rating information for each user.
I would like to loop within the users of the first node and for each one get the rating from the second node. The problem is that the loop doesn't wait for the second node to give the answer, therefore the array returned is always empty.
I know I need to use await/async method but it's not working.
Thanks for help
return admin.database().ref('/user').once('value').then(async(snap) => {
for(const userId of snap) {
admin.database().ref('/rating/' + userId.key).once('value').then(await (snap) =>{
var rating = snap.val();
array.push([userId.key, rating]);
}).catch(error => {
console.log("failed 1 " + error.code);
});
}
return array;
}).catch(error => {
console.log("failed 2 " + error.code);
});

You need to use Promise.all(), as follows (untested):
const array = [];
const userIds = [];
return admin
.database()
.ref('/user')
.once('value')
.then((snap1) => {
const promises = [];
snap1.forEach((childSnapshot1) => {
promises.push(
admin
.database()
.ref('/rating/' + childSnapshot1.key)
.once('value')
);
userIds.push(childSnapshot1.key);
});
return Promise.all(promises);
})
.then((snap2) => {
snap2.forEach((childSnapshot2, idx) => {
const rating = childSnapshot2.val();
array.push([userIds[idx], rating]);
});
return array;
})
.catch((error) => {
//...
});
As explained in the Promise.all() doc, "returned values will be in order of the Promises passed, regardless of completion order". So for the userIds and snap2 arrays, their elements orders are corresponding: you can therefore do array.push([userIds[idx], rating]);

Related

Firestore slow queries are causing the entire logic to crash

I am currently designing a todo app with react and firebase without any node js server code. Everything was fine when inserting data and authenticating, but when I tried to query all the tasks where uid equals a certain value(that means all the tasks that belongs to a particular user), somehow the query gets skipped and returns null. But then, after a second or so, the array got retrieved and printed out.
The code:
function retrieveTasksOfUser(uid) {
// get all tasks where the uid equals the given value
return db.collection("tasks").where("uid", "==", uid).get();
}
function retrieveTasks() {
let res = [];
retrieveTasksOfUser(currentUser["uid"])
.then((snapshot) => {
snapshot.forEach((doc) => {
// include the doc id in each returned object
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
console.log(res);
return res;
})
.catch((err) => {
console.error("Error retrieving tasks: ", err);
return res;
});
}
let tasks = retrieveTasks();
console.log({ tasks });
EDIT:
Inspired by Frank's answer, I modified my code and got:
async function retrieveTasks() {
let res = [];
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
snapshot.forEach((doc) => {
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
return res;
}
let tasks = [];
let promise = retrieveTasks();
promise.then((res) => {
console.log("Tasks are");
tasks = res;
console.log(res);
});
console.log(tasks);
But the result turns out to be an empty array
Thanks a bunch in advance.
You're not returning anything from the top-level code in retrieveTasks, so that means that tasks will always be undefined.
The simplest fix is to return the result of the query, which then means that your return res will be bubbled up:
return retrieveTasksOfUser(currentUser["uid"])
...
But this means you're returning a promise, as the data is loaded asynchronously. So in the calling code, you have to wait for that promise to resolve with then:
retrieveTasks().then((tasks) => {
console.log({ tasks });
})
You can make all of this a lot more readable (although it'll function exactly the same) by using the async and await keywords.
With those, the code would become:
async function retrieveTasks() {
let res = [];
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
snapshot.forEach((doc) => {
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
return res;
}
let tasks = await retrieveTasks();
console.log({ tasks });
Or with a bit more modern JavaScript magic, we can make it even shorter:
async function retrieveTasks() {
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
return snapshot.docs.map((doc) => { ...doc.data, id: doc.id });
}
let tasks = await retrieveTasks();
console.log({ tasks });

Instead of getting undefined, how can I get the data from the asynchronous call?

subject in user.subjects = subjects below is undefined instead of being an array of subjects.
Background:
I have three tables - users, subjects, and a relation between users and subjects called users_subjects.
My goal is that the final res should include an array of subject names.
To do this, I need three asynchronous calls.
Get the users from the users table
Get the subjects is for each user from the table representing the relation between users and subjects (users_subjects)
Get the name for each subject id from subject table
My challenge is getting inputing the subject id and receiving the name. the upper half of addSubjects is my attempt to solve it, albeit unsuccessfully:
//for every user take its id
//run its id with tutors-subjects to get all subject id with that user id
//for each subject id get is subject name through with subjects service
//add subjects key
//add each subject to an array
const addSubject = (knexInstance, users, res) => {
let resultsName = []
function testIt(subjectsArray) {
// let resultsName = []
Promise.all(
subjectsArray.map((id) =>
SubjectsServes.getById(knexInstance, id)
.then(subjectNameObject => {
console.log(subjectNameObject)
resultsName.push(subjectNameObject.subject_name)
})
.catch(err => {
//console.log(err)
})
)).then(() => {
return resultsName
})
}
let results = []
Promise.all(
users.map((user) =>
TutorsSubjectsService.getAllSubjectsForATutor(knexInstance, user.user_id)
.then(subjectsIdArray => {
return testIt(subjectsIdArray)
})
.then(sub => {
user.subjects = sub
results.push(user)
})
.catch(err => {
//console.log(err)
})
)).then(() => {
return res.json(results)
})
.catch(err => {
//console.log(err)
})
}
The following code is where it starts - it calls the above function:
.get((req, res, next) => {
const knexInstance = req.app.get('db');
UsersService.getAllUsers(knexInstance)
.then(Users => {
return addSubject(knexInstance, Users, res)
})
.catch(next)
The only problem I have is with the testIt function. Everything till that point works fine. They're just here for context.
My question is how can I get the correct subjectNames back from testIt?
(The return in testIt last then does not help; the whole function testIt just returns undefined as said in the title)
const testIt = (subjectsArray)=> {
return subjectsArray.map((id) => {
return SubjectsServes.getById(knexInstance, id);
}).reduce((acc,el)=>{
acc.push(el);
return acc;
},[])
//return array of promises
}
const addSubject = (knexInstance, users, res) => {
Promise.all(
users.reduce((acc,el)=>{
acc.push(TutorsSubjectsService.getAllSubjectsForATutor(knexInstance, el.user_id)); return acc;
},[])
).then((userResults) => {
//resolve all promises userResult is an array of result from promises above
Promise.all( userResults.reduce((acc,el)=>{
//for each user create array of promises given the subject list
acc = acc.concat(testIt(el))
} ,[]).then((results)=>{
return res.status(200).send(result);
}).catch( (err)=>{
console.log(err);
return res.status(500).end();
} )
})
.catch(err => {
console.log(err);
return res.status(500).end();
})
}
when you .then() it means you are resolving promises. So nothing return to promise.all. Promise.all().then( result=>{} )
result is an array of output from promises. You have to chain all promises inside each other. They are async, 2 promises all run concurrently. So your result name will be ambiguous, possibly inital value (empty array)

How to avoid nesting promises with firebase transaction?

In an onDelete trigger I'm running a transaction to update some object. I now need to do some cleanup and delete some other objects before running that transaction. After adding the cleanup code I'm getting a warning about nesting promises which I don't know how to get rid of. Here is a snippet:
exports.onDeleteAccount = functions.firestore
.document('accounts/{accountID}')
.onDelete((account, context) => {
// First do the cleanup and delete addresses of the account
const query = admin.firestore().collection('account_addresses').where('accountID', '==', account.id);
return query.get().then(addresses => {
var promises = [];
addresses.forEach(address=>{
promises.push(address.ref.delete());
})
return Promise.all(promises);
}).then(()=> {
// Then run the transaction to update the account_type object
return runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const acc_type = account.data().type;
const accountTypeRef = admin.firestore().doc("account_types/"+acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
// Do some stuff and update an object called users
transaction.update(accountTypeRef, {users: users});
return;
})
})
})
.catch(error => {
console.log("AccountType delete transaction failed. Error: "+error);
});
})
I don't think the problem comes from the Transaction but from the forEach loop where you call delete(). You should use Promise.all() in order to return a single Promise that fulfills when all of the promises (returned by delete()) passed to the promises array have been fulfilled, see below.
In addition, you do runTransaction(transaction => {...}) but runTransaction is a method of Firestore. You should do admin.firestore().runTransaction(...).
Therefore, the following should do the trick:
exports.onDeleteAccount = functions.firestore
.document('accounts/{accountID}')
.onDelete((account, context) => {
// First do the cleanup and delete addresses of the account
const query = admin.firestore().collection('account_addresses').where('accountID', '==', account.id);
return query.get()
.then(addresses => {
const promises = [];
addresses.forEach(address => {
promises.push(address.ref.delete());
})
return Promise.all(promises);
}).then(() => {
// Then run the transaction to update the account_type object
return admin.firestore().runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const acc_type = account.data().type;
const accountTypeRef = admin.firestore().doc("account_types/" + acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
// Do some stuff and update an object called users
transaction.update(accountTypeRef, { users: users });
})
})
})
.catch(error => {
console.log("AccountType delete transaction failed. Error: " + error);
});
})

Param didn't pass to local function in firebase cloud functions

I have a firebase cloud function as follows:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
})
.then(jobs => {
return jobs.forEach(job => {
var percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate); // add to team's feed
}
});
})
.catch(err => {
console.log("firebase got an error: ", err);
});
});
In function foo, I call a local non-cloud function getMatchedPercent which is defined as below:
const getMatchedPercent = (candidate, job) => {
console.log("In get percent: ", candidate, job);
// do something
};
The problem is when I checked job.val() in foo before calling getMatchedPercent, I can see valid data got printed from console for job.val(). When once get in getMatchedPercent, I tried to print job, it complains it's undefined.
Is there anything I missed? Why the information of job can be lost during calling a function? Thanks!
Your problem is caused by these lines:
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
job.val() returns an object (of the data) not a promise, so Promise.all() incorrectly interprets it as a resolved promise with no value. In your next block of code, the array jobs is an array of undefined values rather than the data you were expecting.
To fix this, you would instead return the array of values rather than using Promise.all().
const jobValues = [];
jobs.forEach(job => {
jobValues.push(job.val());
});
return jobValues;
But because no asyncronous work is taking place here you can flatten your Promise chain. By doing so, you will use less memory because you won't need an array containing of all of your job.val() objects at once.
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = []; // will contain any team feed update promises
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
promises.push(
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate) // add to team's feed
);
}
});
return Promise.all(promises);
})
.catch(err => {
console.log("Failed to update team feeds: ", err);
});
});
However, this still has another problem where some of the feed updates may succeed and others may fail which leaves your database in an unknown state. So instead you might want to consider writing to the database atomically (all data is written, or nothing at all).
This could be achieved using:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const pendingUpdates = {}; // "path: value" pairs to be applied to the database
const feedsRef = admin.database().ref("feeds");
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
const pushId = feedsRef.push().key; // push() without arguments doesn't write anything to the database, it just generates a new reference with a push ID we can use.
const path = job.feedId + "/upcomingWeek/" + pushId;
pendingUpdates[path] = candidate; // queue add to team's feed
}
});
// apply all updates in pendingUpdates object,
// relative to feedsRef as an all-or-nothing operation.
// e.g. pendingUpdates["feed001/upcomingWeek/9jksdfghsdjhn"] = "someUserId"
// will be written to "feeds/feed001/upcomingWeek/9jksdfghsdjhn"
return feedsRef.update(pendingUpdates); // commit changes
})
.catch(err => {
console.log("Failed to apply all feed updates: ", err);
});
});

Promise.all not working correctly with array of Mongo Queries

I have an array (recommendedRoutes) of routes that I need to iterate over and get the owner (User) of the route from MongoDB. I then attach an additional userObj to the route after I get it from the database.
let promiseArr = [];
for (let index = 0; index < obj.recommendedRoutes.length; index++) {
console.log("Pushed: "+index);
promiseArr.push(
User.find({
_id : obj.recommendedRoutes[index].userId,
},(err,data) => {
if(err) {
console.log("Database error: " + err);
}else{
obj.recommendedRoutes[index].userObj = data[0].toObject();
console.log("Completed: "+index);
}
})
);
}
I need to return this array after I have attached the userObj to all the route objects in the array. So I pushed all the queries into a promiseArr and used Promise.all to ensure I only return the array when all the promises have been resolved.
Promise.all(promiseArr)
.then(() => {
console.log("Return Object");
return res.status(200).send({
success: true,
message: "Successfully retrieved Recommended Routes/Carpools",
obj: obj,
});
})
.catch((e) => {
throw "There was an error: "+e;
});
The problem is that it occasionally returns the array before the promises resolves.
Expected output:
Pushed: 0
Pushed: 1
Completed: 1
Completed: 0
Return Object
What happens occasionally:
Pushed: 0
Pushed: 1
Completed: 1
Return Object
Completed: 0
In the name of better code, please await all responses before you start updating your recommendedRoutes.
With you current approach, you update the individual route-objects as soon as the respective response arrives. Between these responses your array is in a transitional state. It doesn't contain the (entirety of) old data anymore, but not yet the (entire) new data from the server; and god forbid that anyone of your requests may fail. How do you roll back OR move forward from this, as each update (every item in the array) is handled individually.
#m1ch4ls answer also has the same problem, it just fixes your use of callbacks instead of the promises.
imho. some better code
Promise.all(
//make all the individual requests
obj.recommendedRoutes.map(
route => User.find({ _id: route.userId })
)
).then(
//and only whe they ALL have returned successfully
data => {
//update all your state at once
data.forEach((item, index) => {
obj.recommendedRoutes[index].userObj = item.toObject()
});
},
err => {
//or return an error-message if anyone fails
console.log("Database error: " + err);
}
);
You are not using Promises but callbacks...
This is the correct way:
for (let index = 0; index < obj.recommendedRoutes.length; index++) {
console.log('Pushed: ' + index);
promiseArr.push(
User.find({
_id: obj.recommendedRoutes[index].userId,
}).then((data) => {
obj.recommendedRoutes[index].userObj = data[0].toObject();
console.log('Completed: ' + index)
}, err => console.log('Database error: ' + err))
);
}

Categories

Resources