Reputation: 485
I'm using node.js to get data from 2 different collections in Firestore.
This issue is similar to this, but there is no answer to this question: Nested Firebase Firestore forEach promise queries
In my case, I'm creating an instagram-like app, where I have a 'timeline' collection. Inside the timeline document, I have a user key. From that user key, I would like to perform another query from 'user' collection.
So logically here are the query steps:
The problem is, the JSON response is returned before getting the user data.
This is my code:
tlRoute.get((req,res)=>{
//reading from firestore
let afs = req.app.get('afs');
var timelineArr = [];
let timelineRef = afs.collection(`timeline/${req.params.key}/timeline`);
timelineRef.get().then((snapshot) => {
snapshot.forEach((doc) => {
if(!doc.exists){
console.log('No such document!');
} else {
//populating timelineData with doc.data()
console.log('populating timelineData');
let timelineData = doc.data();
let userRef = afs.doc(`user/${doc.data().userKey}`);
userRef.get().then((doc) => {
//adding user details to timelineData
console.log('adding user details to timelineData');
timelineData.profileImageUrl = doc.data().profileImageUrl;
timelineData.username = doc.data().username;
timelineArr.push(timelineData);
});
}
});
})
.catch(err => {
console.log(err);
});
console.log('this is the final timelineArr', timelineArr);
//returning response json data to client
return res.json(timelineArr);
});
In console log, this is the log that I get:
this is the final timelineArr []
populating timelineData
populating timelineData
adding user details to timelineData
adding user details to timelineData
Any help would be appreciated.
Upvotes: 1
Views: 1524
Reputation: 62686
I've tried to refactor your code to produce the same output, adding a few methods, each one with a simpler, isolated, testable purpose relating to a specific kind of object.
// return a promise that resolves to a user doc snapshot with the given key
function getUserWithKey(db, userKey) {
return db.doc(`user/${userKey}`).get();
}
// return a promise that resolves to a timeline object augmented to include
// its doc id and its associated user's username
function timelineObject(db, timelineDocSnapshot) {
let timelineObject = timelineDocSnapshot.data();
timelineObject.postKey = timelineDocSnapshot.id;
return getUserWithKey(db, timelineObject.userKey).then(userDocSnapshot => {
timelineObject.username = userDocSnapshot.data().username;
timelineObject.profileImageUrl = userDocSnapshot.data().profileImageUrl;
return timelineObject;
});
}
// return a promise that resolves to all of the timeline objects for a given key
function getTimeline(db, key) {
let timelineRef = db.collection(`timeline/${key}/timeline`);
return timelineRef.get().then(querySnapshot => {
let promises = querySnapshot.docs.map(doc => timelineObject(db, doc));
return Promise.all(promises);
});
}
// get route for a timeline
tlRoute.get((req,res)=>{
let db = req.app.get('db');
let key = req.params.key;
return getTimeline(db, key).then(timelineObjects => {
return res.json(timelineObjects);
});
})
This code could be further improved with the use of async / await syntax.
Upvotes: 3
Reputation: 485
I decided to use a for
to loop through the inner promise.
Here is my code:
tlRoute.get((req,res)=>{
let db = req.app.get('db');
let key = req.params.key;
var timelineArr = [];
getData(db, key, timelineArr).then(results => {
for (let index = 0; index<results.length; index++) {
timelineArr[index].profileImageUrl = results[index].data().profileImageUrl;
timelineArr[index].username = results[index].data().username;
}
console.log(results.length);
console.log(timelineArr);
console.log('this is the final timelineArr');
//returning response json data to client
return res.json(timelineArr);
});
});
function getData(db, key, timelineArr){
let timelineRef = db.collection(`timeline/${key}/timeline`);
return timelineRef.get().then((snapshot) => {
console.log('snapshot length: ', snapshot.length);
var promiseArr = [];
snapshot.forEach((doc) => {
if(!doc.exists){
console.log('No such document!');
} else {
//populating timelineData with doc.data()
console.log('populating timelineData');
let timelineData = doc.data();
timelineData.postKey = doc.id;
timelineArr.push(timelineData);
console.log('userKey: ', doc.data().userKey);
let userRef = db.doc(`user/${doc.data().userKey}`);
promiseArr.push(userRef.get());
}
});
return Promise.all(promiseArr);
})
.catch(err => {
console.log(err);
});
}
Upvotes: 0
Reputation: 126
Sooo Firebase uses callbacks or promises(the “.then((snapshot) => {})” thingy) which runs after the data has been retrieved from Firestore. What you are doing is returning the timelineArr before the callback-method has runned, and thus before it has been populated with the data from Firestore!
One solution to this is to move the return statement inside the callback-method and make the whole function asynchronous. It will be something like this:
var timelineArr = [];
async function RetrieveData() {
let timelineRef = afs.collection(`timeline/${req.params.key}/timeline`);
await timelineRef.get().then((snapshot) => {
snapshot.forEach((doc) => {
if(!doc.exists){
console.log('No such document!');
} else {
//populating timelineData with doc.data()
console.log('populating timelineData');
let timelineData = doc.data();
let userRef = afs.doc(`user/${doc.data().userKey}`);
userRef.get().then((doc) => {
//adding user details to timelineData
console.log('adding user details to timelineData');
timelineData.profileImageUrl = doc.data().profileImageUrl;
timelineData.username = doc.data().username;
timelineArr.push(timelineData);
});
}
});
//returning response json data to client
return res.json(timelineArr);
}).catch(err => {
console.log(err);
});
}
RetrieveData()
console.log('this is the final timelineArr', timelineArr);
Good luck!
Best regards, Eskils.
Upvotes: 0