Getting all documents from one collection in Firestore - javascript

Hi I'm starting with javascript and react-native and I'm trying to figure out this problem for hours now. Can someone explain me how to get all the documents from firestore collection ?
I have been trying this:
async getMarkers() {
const events = await firebase.firestore().collection('events').get()
.then(querySnapshot => {
querySnapshot.docs.map(doc => {
console.log('LOG 1', doc.data());
return doc.data();
});
});
console.log('LOG 2', events);
return events;
}
Log 1 prints all the objects(one by one) but log 2 is undefined, why ?

The example in the other answer is unnecessarily complex. This would be more straightforward, if all you want to do is return the raw data objects for each document in a query or collection:
async getMarker() {
const snapshot = await firebase.firestore().collection('events').get()
return snapshot.docs.map(doc => doc.data());
}

if you want include Id
async getMarkers() {
const events = await firebase.firestore().collection('events')
events.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
console.log(tempDoc)
})
}
Same way with array
async getMarkers() {
const events = await firebase.firestore().collection('events')
events.get().then((querySnapshot) => {
const tempDoc = []
querySnapshot.forEach((doc) => {
tempDoc.push({ id: doc.id, ...doc.data() })
})
console.log(tempDoc)
})
}

I made it work this way:
async getMarkers() {
const markers = [];
await firebase.firestore().collection('events').get()
.then(querySnapshot => {
querySnapshot.docs.forEach(doc => {
markers.push(doc.data());
});
});
return markers;
}

if you need to include the key of the document in the response, another alternative is:
async getMarker() {
const snapshot = await firebase.firestore().collection('events').get()
const documents = [];
snapshot.forEach(doc => {
const document = { [doc.id]: doc.data() };
documents.push(document);
}
return documents;
}

The docs state:
import { collection, getDocs } from "firebase/firestore";
const querySnapshot = await getDocs(collection(db, "cities"));
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
However I am using the following (excuse the TypeScript):
import { collection, Firestore, getDocs, Query, QueryDocumentSnapshot, QuerySnapshot } from 'firebase/firestore'
const q: Query<any> = collection(db, 'videos')
const querySnapshot: QuerySnapshot<IVideoProcessed> = await getDocs(q)
const docs: QueryDocumentSnapshot<IVideoProcessed>[] = querySnapshot.docs
const videos: IVideoProcessed[] = docs.map((doc: QueryDocumentSnapshot<IVideoProcessed>) => doc.data())
where db has the type Firestore

You could get the whole collection as an object, rather than array like this:
async function getMarker() {
const snapshot = await firebase.firestore().collection('events').get()
const collection = {};
snapshot.forEach(doc => {
collection[doc.id] = doc.data();
});
return collection;
}
That would give you a better representation of what's in firestore. Nothing wrong with an array, just another option.

Two years late but I just began reading the Firestore documentation recently cover to cover for fun and found withConverter which I saw wasn't posted in any of the above answers. Thus:
If you want to include ids and also use withConverter (Firestore's version of ORMs, like ActiveRecord for Ruby on Rails, Entity Framework for .NET, etc), then this might be useful for you:
Somewhere in your project, you probably have your Event model properly defined. For example, something like:
Your model (in TypeScript):
./models/Event.js
export class Event {
constructor (
public id: string,
public title: string,
public datetime: Date
)
}
export const eventConverter = {
toFirestore: function (event: Event) {
return {
// id: event.id, // Note! Not in ".data()" of the model!
title: event.title,
datetime: event.datetime
}
},
fromFirestore: function (snapshot: any, options: any) {
const data = snapshot.data(options)
const id = snapshot.id
return new Event(id, data.title, data.datetime)
}
}
And then your client-side TypeScript code:
import { eventConverter } from './models/Event.js'
...
async function loadEvents () {
const qs = await firebase.firestore().collection('events')
.orderBy('datetime').limit(3) // Remember to limit return sizes!
.withConverter(eventConverter).get()
const events = qs.docs.map((doc: any) => doc.data())
...
}
Two interesting quirks of Firestore to notice (or at least, I thought were interesting):
Your event.id is actually stored "one-level-up" in snapshot.id and not snapshot.data().
If you're using TypeScript, the TS linter (or whatever it's called) sadly isn't smart enough to understand:
const events = qs.docs.map((doc: Event) => doc.data())
even though right above it you explicitly stated:
.withConverter(eventConverter)
Which is why it needs to be doc: any.
(But! You will actually get Array<Event> back! (Not Array<Map> back.) That's the entire point of withConverter... That way if you have any object methods (not shown here in this example), you can immediately use them.)
It makes sense to me but I guess I've gotten so greedy/spoiled that I just kinda expect my VS Code, ESLint, and the TS Watcher to literally do everything for me. 😇 Oh well.
Formal docs (about withConverter and more) here: https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects

I prefer to hide all code complexity in my services... so, I generally use something like this:
In my events.service.ts
async getEvents() {
const snapchot = await this.db.collection('events').ref.get();
return new Promise <Event[]> (resolve => {
const v = snapchot.docs.map(x => {
const obj = x.data();
obj.id = x.id;
return obj as Event;
});
resolve(v);
});
}
In my sth.page.ts
myList: Event[];
construct(private service: EventsService){}
async ngOnInit() {
this.myList = await this.service.getEvents();
}
Enjoy :)

Here's a simple version of the top answer, but going into an object with the document ids:
async getMarker() {
const snapshot = await firebase.firestore().collection('events').get()
return snapshot.docs.reduce(function (acc, doc, i) {
acc[doc.id] = doc.data();
return acc;
}, {});
}

General example to get products from Cloud Firestore:
Future<void> getAllProducts() async {
CollectionReference productsRef =
FirebaseFirestore.instance.collection('products');
final snapshot = await productsRef.get();
List<Map<String, dynamic>> map =
snapshot.docs.map((doc) => doc.data() as Map<String, dynamic>).toList();
}

In version 9 sdk of firebase you can get all the documents from a collection using following query:
const querySnapshot = await getDocs(collection(db, "cities"));
querySnapshot.docs.forEach((doc) => {
console.log(doc.id, " => ", doc.data());
});
See Get multiple documents from a collection

I understand your query, This is because how Javascript handles promises and variables. So basically events variable is hoisted with the value undefined and printed on the LOG 2 console log, while the Event Loop responsible for the promise call resulted in an array of objects as the value of the events variable and then the console log (LOG 1) was printed with the resolved promise response

All answers are true, but when you have heavy data you will face memory and bandwidth problems, so you have to write a [cursor] function to read data part by part.
also, you may face to Bandwidth Exhausted error, please have a look at this solution I have implemented on a gist
https://gist.github.com/navidshad/973e9c594a63838d1ebb8f2c2495cf87
Otherwise, you can use this cursor I written to read a collection doc by doc:
async function runCursor({
collection,
orderBy,
limit = 1000,
onDoc,
onDone,
}) {
let lastDoc;
let allowGoAhead = true;
const getDocs = () => {
let query = admin.firestore().collection(collection).orderBy(orderBy).limit(limit)
// Start from last part
if (lastDoc) query = query.startAfter(lastDoc)
return query.get().then(sp => {
if (sp.docs.length > 0) {
for (let i = 0; i < sp.docs.length; i++) {
const doc = sp.docs[i];
if (onDoc) onDoc(doc);
}
// define end of this part
lastDoc = sp.docs[sp.docs.length - 1]
// continue the cursor
allowGoAhead = true
} else {
// stop cursor if there is not more docs
allowGoAhead = false;
}
}).catch(error => {
console.log(error);
})
}
// Read part by part
while (allowGoAhead) {
await getDocs();
}
onDone();
}

Related

Can't iterate through array from an async Javascript function?

I'm stuck on an issue where I'm parsing the results from an API in getSubscriptions(). This calls getUserSubs() which returns the following object:
When I call on subscriptions I get the expected array console (See "Works") snippet.
But when I try to iterate on the array subscriptions (See "Doesn't work"), then the contents of the function are not even called.
userSubs() is called for API data
async function getUserSubs(userId) {
const ref = collection(db, "users", userId, "subscriptions")
let subList = []
try {
const subIds = await getDocs(ref)
subIds.forEach(subRef => {
const docRef = subRef.data()
getDocData(docRef.product).then(product => {
subList.push(product)
})
})
return subList
} catch (e) {
console.error("Error in getting subscriptions: ", e)
return []
}
}
Works
function getSubscriptions(userId) {
getUserSubs(userId).then(subscriptions => {
console.log(subscriptions) // Works as intended
}
}
Doesn't work
function getSubscriptions(userId) {
getUserSubs(userId).then(subscriptions => {
subscriptions.forEach(x => {
console.log(x) // ISSUE: This isn't called
})
}
Also doesn't work
let a = []
getUserSubs(userId).then(subscriptions => {
subscriptions.forEach(x => a.push(x))
})
console.log(a)
I know there are similar questions asked but after reading them I'm still not able to resolve my issue.
Similar issues:
How to access the value of a promise?
Using async/await with a forEach loop
getUserSubs(userId).then(subscriptions => {
console.log(subscriptions) // Works as intended
}
No it doesn't. It only appears so because you are inspecting the live array that was mutated after it has been logged to the console.
Also doesn't work:
let a = []
getUserSubs(userId).then(subscriptions => {
subscriptions.forEach(x => a.push(x))
})
console.log(a)
Yes, for rather obvious reasons: the array is logged before you fill it. It would need to be either
getUserSubs(userId).then(subscriptions => {
let a = []
subscriptions.forEach(x => a.push(x))
console.log(a)
})
or
let a = []
const subscriptions = await getUserSubs(userId)
subscriptions.forEach(x => a.push(x))
console.log(a)
But none of these will solve your core problem: getUserSubs returns an empty array before it gets filled, in the lines
subIds.forEach(subRef => {
const docRef = subRef.data()
getDocData(docRef.product).then(product => {
subList.push(product)
})
})
return subList
you never wait for the getDocData promise. If you change that to
let subList = []
for (const subRef of subIds) {
const docRef = subRef.data()
const product = await getDocData(docRef.product)
subList.push(product)
}
return subList
or just
return Promise.all(subIds.map(subRef => {
const docRef = subRef.data()
return getDocData(docRef.product)
}))
it would work, as described in the question you already found.
(This might still not work. subIds looks suspiciously like a firebase snapshot, which is not an array and can neither be iterated nor does it have a .map method. In that case, you'll need to use forEach+push manually).
Honestly I wouldn't use the forEach() method. I think all you need to do to fix this is iterate over the results in a normal for loop.
for(let subscription of subscriptions) {
console.log(subscription);
}
OR
for(let index in subscriptions) {
console.log(subscriptions[index]);
}
If this doesn't do the trick, I'll open up a sandbox and look more in depth.

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

Calling async / await function and receiving a value back

I have got a function in an API library that calls firestore and gets data back. This part works fine:
export const getUserByReferrerId = async id => {
let doc = await firestore
.collection(FIRESTORE_COLLECTIONS.USERS)
.where('grsfId', '==', id)
.get()
.then(querySnapshot => {
if (!querySnapshot.empty) {
console.log ("we found a doc");
// use only the first document, but there could be more
const snapshot = querySnapshot.docs[0];
console.log ("snapshot", snapshot.id);
return snapshot.id // uid of the user
}
});
}
I am calling this library from a component. I have another function that runs on a button click. I cannot figure out how to get the value from the async api call.
I have tried this - a promise appears when I console.log the return:
testCreditFunction = (id) => {
let uid = getUserByReferrerId(id).then(doc => {
console.log("uid1", doc);
});
}
I have also tried this - log shows null for uid.
testCreditFunction = (id) => {
let uid = '';
(async () => {
uid = await getUserByReferrerId(id);
console.log ("uid in the function", uid);
})();
}
I have seen this question asked a few times and I have tried several of the answers and none are working for me. The odd thing is that I have done this same thing in other areas and I cannot figure out what the difference is.
Change your funtion to this.
export const getUserByReferrerId = async id => {
return await firestore
.collection(FIRESTORE_COLLECTIONS.USERS)
.where('grsfId', '==', id)
.get();
}
Try getting data this way.
testCreditFunction = (id) => {
let querySnapshot = '';
(async () => {
querySnapshot = await getUserByReferrerId(id);
const snapshot = querySnapshot.docs[0];
console.log ("snapshot", snapshot.id);
})();
One thing I notice right off the bat is that you're mixing async/await & .then/.error. While it's not strictly forbidden it definitely makes things more difficult to follow.
As others have mentioned in the comments, you need to make a return for the promise to resolve (complete). Here's how you might write getUserByReferrerId (using async/await).
const getUserByReferrerId = async id => {
const usersRef = firestore.collection('users');
const query = usersRef.where('grsfId', '==', id);
const snapshot = await query.get();
if (snapshot.empty) {
console.log('no doc found');
return;
}
console.log('doc found');
const doc = snapshot.docs[0]; // use first result only (there might be more though)
return doc.id
}
You'll notice how I split up the query building steps from the snapshot request. This was intentional as the only promised function in this is the get request.
Using this getUserByReferrerID can be done with a simple await. Just be sure to check that the result isn't undefined.
const userID = await getUserByReferrerId('someid')
if (!userID) {
console.log('user not found');
}
console.log(`User ID is ${userID}`);
p.s - I would recommend renaming the function to getUserIdByReferrerId to more accurately reflect the fact that you're returning an ID and not a user doc. Alternatively, you can return the user object and leave the name as is.

Iterate through list of document uids and create listeners in Firestore

In my Vuex store, I have an action that takes a list of uids of followed users from the current user's Firestore UserDataCollection document, iterates through them, and produces the data for each document to be shown on the UI. It works fine using .get(), but I'm trying to convert it to .onSnapshot() so that I can get real-time updates.
I have been totally unsuccessful trying to use .onSnapshot(), as I can't find any references online or in the Firebase docs on how to implement this after mapping through the array of uids.
I tried removing the promises, since onSnapshot doesn't seem to work with promises, and replaced the .get() with .onSnapshot(), but that didn't work.
Does anyone know the correct way to implement the Firestore .onSnapshot() listener given the code below?
getCircle({state, commit}) {
const circle = state.userProfile.circle
let promises = circle.map(u => userDataCollection.doc(u).get())
return Promise.all(promises)
.then(querySnapShot => {
let circleData = []
if (querySnapShot.empty) {
console.log("empty")
} else {
querySnapShot.forEach(doc => {
let item = doc.data()
circleData.push(item)
}
)
}
commit('setUserCircle', circleData)
})
},
Edit based on response
I added .onSnapshot within the forEach as shown in the code below. In vue devtools it's showing the correct number of data entries in my Vuex store, however they are all undefined.
getCircle({state, commit}) {
const circle = state.userProfile.circle
let promises = circle.map(u => userDataCollection.doc(u).get())
return Promise.all(promises)
.then(querySnapShot => {
let circleData = []
if (querySnapShot.empty) {
console.log("empty")
} else {
querySnapShot.forEach(x => {
let itemId = x.data().uid
userDataCollection.doc(itemId)
.onSnapshot((doc) => {
let item = doc.data()
console.log(doc.data())
circleData.push(item)
})
}
)
}
commit('setUserCircle', circleData)
})
},
In presented code commit is run only once before onSnapshot callback. I tried to create example showing the mechanism (I am assuming that circle array is assigned properly and I have 3 items hardcode'ed in mine):
let promises = circle.map(u => userDataCollection.doc(u).get())
Promise.all(promises)
.then( querySnapShot => {
var circleData = []
querySnapShot.forEach( x => {
let itemId = x.data().uid
userDataCollection.doc(itemId).onSnapshot(doc => {
let item = doc.data()
circleData.push(item)
console.log("inside snapshot: ",circleData.length)
})
})
console.log("outside foreach: ",circleData.length)
})
If you run this code you should see in console something like this (I have run this in node):
outside foreach: 0
inside snapshot: 1
inside snapshot: 2
inside snapshot: 3
And if you do any change in Firestore you will get another console log inside snapshot: 4.
I do not have full understanding of your application logic, however I think that commit statement should be inside onSnapshot listener. But of course, with information I have, this is only guess.
Thank you everyone for the help. The solution that worked was to remove the .map and Promise and used an 'in' query. Code shown below:
getCircle({state, commit}) {
const circle = state.userProfile.circle
userDataCollection.where('__name__', 'in', circle).onSnapshot(querySnapShot => {
var circleData = []
querySnapShot.forEach(doc => {
let item = doc.data()
circleData.push(item)
commit('setUserCircle', circleData)
})
})
},

Execution doesn't go past the second fetch call (Gatsby related stuff)

I am generating data for querying with gatsby's sourceNodes api.
The problem is that inside the getHooksInfo call something weird happens.
I do the first await fetch(rawUrl)(i am using node-fetch, but it doesn't matter that much) request, everything's fine. But when i do the second request const res = await fetch(url) inside for (const guess of guesses) it... does nothing. I see the first console.log('before fetch');, but not the console.log('after fetch');. No error either. It just goes straight to the return statement with result being always []. I tried debugging, but debugger jumped to the return statement also on this line. Even stepping into the actual fetch call didn't help that much. So if you smell anything (besides the code, obviously;D ) here that leads to that behavior, i'd really appreciate your help.
exports.sourceNodes = async ({ actions }) => {
const { createNode } = actions;
...
await Promise.allSettled(someArray
.map(async ({ package: { links, author, description }, score, searchScore }, i) => {
const { npm, homepage, repository } = links;
const { final, detail: { popularity } } = score;
const { ok, result } = await getHooksInfo(repository);
...
createNode(...);
}));
async function getHooksInfo(repository) {
...
const res = await fetch(rawUrl);
if (!res.ok) {
return {
ok: false,
result: null
};
}
const readme = await res.text();
const hooks = [...];
const result = [];
await Promise.allSettled(
hooks.map(async (hook) => {
for (const guess of guesses) {
...
console.log('before fetch');
const res = await fetch(someUrl);
console.log('after fetch');
...
}
})
);
return {
ok: true,
result
};
}
I'm guessing that Promise.allSettled is swallowing some syntax error in your fetch(url) and that that is why you aren't getting errors.

Categories

Resources