I have a function that receives data, I use an asynchronous promise to get a link to the document item.poster = await Promise.all(promises), then the data does not have time to be added to the array and I get an empty array. But if I remove the function where I get the link to the document, then everything works fine. In debug mode, I can see all the data fine, but why am I getting an empty array?
async FetchData({ state, commit }, to) {
try {
const q = query(collection(db, to));
await onSnapshot(q, (querySnapshot) => {
let data = [];
querySnapshot.forEach(async (doc) => {
let promises = [];
let item = {
id: doc.id,
name: doc.data().name,
slug: doc.data().slug,
country: doc.data().country,
duration: doc.data().duration,
year: doc.data().year,
video: doc.data().video,
genres: doc.data().genres,
actors: doc.data().actors,
};
if (to === "films") {
const starsRef = ref(storage, `images/${doc.id}/poster.png`);
promises.push(getDownloadURL(starsRef));
item.poster = await Promise.all(promises);
}
data.push(item);
});
commit("setData", { data, to });
});
} catch (err) {
console.error(err);
} }
forEach and async do not work nice together, forEach is not awaitable. But the solution is simple, .map and Promise.all():
// async not really needed
async FetchData({ state, commit }, to) {
const q = query(collection(db, to));
// TODO: Call the unsubscribeFn method in order to stop
// onSnapshot callback executions (when needed)
const unsubscribeFn = onSnapshot(q, (querySnapshot) => {
const allPromises = querySnapshot.docs.map(async (doc) => {
const docData = doc.data();
let item = {
id: doc.id,
name: docData.name,
slug: docData.slug,
country: docData.country,
duration: docData.duration,
year: docData.year,
video: docData.video,
genres: docData.genres,
actors: docData.actors
};
// Or you can use
// let item = { id: doc.id, ...docData };
if (to === "films") {
const starsRef = ref(storage, `images/${doc.id}/poster.png`);
item.poster = await getDownloadURL(starsRef);
}
return item;
});
Promise.all(allPromises)
.then((data) => commit("setData", { data, to }))
.catch(console.error);
});
}
For example I have dynamic filter for my list of books where I can set specific color, authors and categories.
This filter can set multiple colors at once and multiple categories.
Book > Red, Blue > Adventure, Detective.
How can I add "where" conditionally?
firebase
.firestore()
.collection("book")
.where("category", "==", )
.where("color", "==", )
.where("author", "==", )
.orderBy("date")
.get()
.then(querySnapshot => {...
As you can see in the API docs, the collection() method returns a CollectionReference. CollectionReference extends Query, and Query objects are immutable. Query.where() and Query.orderBy() return new Query objects that add operations on top of the original Query (which remains unmodified). You will have to write code to remember these new Query objects so you can continue to chain calls with them. So, you can rewrite your code like this:
var query = firebase.firestore().collection("book")
query = query.where(...)
query = query.where(...)
query = query.where(...)
query = query.orderBy(...)
query.get().then(...)
Now you can put in conditionals to figure out which filters you want to apply at each stage. Just reassign query with each newly added filter.
if (some_condition) {
query = query.where(...)
}
Firebase Version 9
The docs do not cover this but here is how to add conditional where clauses to a query
import { collection, query, where } from 'firebase/firestore'
const queryConstraints = []
if (group != null) queryConstraints.push(where('group', '==', group))
if (pro != null) queryConstraints.push(where('pro', '==', pro))
const q = query(collection(db, 'videos'), ...queryConstraints)
The source of this answer is a bit of intuitive guesswork and help from my best friend J-E^S^-U-S
With Firebase Version 9 (Jan, 2022 Update):
You can filter data with multiple where clauses:
import { query, collection, where, getDocs } from "firebase/firestore";
const q = query(
collection(db, "products"),
where("category", "==", "Computer"),
where("types", "array-contains", ['Laptop', 'Lenovo', 'Intel']),
where("price", "<=", 1000),
);
const docsSnap = await getDocs(q);
docsSnap.forEach((doc) => {
console.log(doc.data());
});
In addition to #Doug Stevenson answer. When you have more than one where it is necessary to make it more dynamic as in my case.
function readDocuments(collection, options = {}) {
let {where, orderBy, limit} = options;
let query = firebase.firestore().collection(collection);
if (where) {
if (where[0] instanceof Array) {
// It's an array of array
for (let w of where) {
query = query.where(...w);
}
} else {
query = query.where(...where);
}
}
if (orderBy) {
query = query.orderBy(...orderBy);
}
if (limit) {
query = query.limit(limit);
}
return query
.get()
.then()
.catch()
}
// Usage
// Multiple where
let options = {where: [["category", "==", "someCategory"], ["color", "==", "red"], ["author", "==", "Sam"]], orderBy: ["date", "desc"]};
//OR
// A single where
let options = {where: ["category", "==", "someCategory"]};
let documents = readDocuments("books", options);
Note that a multiple WHERE clause is inherently an AND operation.
If you're using angular fire, you can just use reduce like so:
const students = [studentID, studentID2,...];
this.afs.collection('classes',
(ref: any) => students.reduce(
(r: any, student: any) => r.where(`students.${student}`, '==', true)
, ref)
).valueChanges({ idField: 'id' });
This is an example of multiple tags...
You could easily change this for any non-angular framework.
For OR queries (which can't be done with multiple where clauses), see here.
For example, there's an array look like this
const conditionList = [
{
key: 'anyField',
operator: '==',
value: 'any value',
},
{
key: 'anyField',
operator: '>',
value: 'any value',
},
{
key: 'anyField',
operator: '<',
value: 'any value',
},
{
key: 'anyField',
operator: '==',
value: 'any value',
},
{
key: 'anyField',
operator: '==',
value: 'any value',
},
]
Then you can just put the collection which one you want to set query's conditions into this funcion.
function* multipleWhere(
collection,
conditions = [{ field: '[doc].[field name]', operator: '==', value: '[any value]' }],
) {
const pop = conditions.pop()
if (pop) {
yield* multipleWhere(
collection.where(pop.key, pop.operator, pop.value),
conditions,
)
}
yield collection
}
You will get the collection set query's conditions.
async yourFunction(){
const Ref0 = firebase.firestore().collection("your_collection").doc(doc.id)
const Ref1 = appointmentsRef.where('val1', '==',condition1).get();
const Ref2 = appointmentsRef.where("val2", "!=", condition2).get()
const [snapshot_val1, snapshot_val2] = await Promise.all([Ref1, Ref2]);
const val1_Array = snapshot_val1.docs;
const val2_Array = snapshot_val2.docs;
const globale_val_Array = val1_Array .concat(val2_Array );
return globale_val_Array ;
}
/*Call you function*/
this.checkCurrentAppointment().then(docSnapshot=> {
docSnapshot.forEach(doc=> {
console.log("Your data with multiple code query:", doc.data());
});
});
As CollectionRef does not have query method in firebase web version 9,
I modified #abk's answer.
async getQueryResult(path, options = {}) {
/* Example
options = {
where: [
["isPublic", "==", true],
["isDeleted", "==", false]
],
orderBy: [
["likes"],
["title", "desc"]
],
limit: 30
}
*/
try {
let { where, orderBy, limit } = options;
let collectionRef = collection(<firestore>, path);
let queryConstraints = [];
if (where) {
where = where.map((w) => firestore.where(...w));
queryConstraints = [...queryConstraints, ...where];
}
if (orderBy) {
orderBy = orderBy.map((o) => firestore.orderBy(...o));
queryConstraints = [...queryConstraints, ...orderBy];
}
if (limit) {
limit = firestore.limit(limit);
queryConstraints = [...queryConstraints, limit];
}
const query = firestore.query(collectionRef, ...queryConstraints);
const querySnapshot = await firestore.getDocs(query);
const docList = querySnapshot.docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
...data,
};
});
return docList;
} catch (error) {
console.log(error);
}
}
Simple function where you can specify the path and an array of filters that you can pass and get you documents, hope it helps.
async function filterDoc(path, filters) {
if (!path) return [];
//define the collection path
let q = db.collection(path);
//check if there are any filters and add them to the query
if (filters.length > 0) {
filters.forEach((filter) => {
q = q.where(filter.field, filter.operator, filter.value);
});
}
//get the documents
const snapshot = await q.get();
//loop through the documents
const data = snapshot.docs.map((doc) => doc.data());
//return the data
return data;
}
//call the function
const data = await filterDoc(
"categories_collection",
[
{
field: "status",
operator: "==",
value: "active",
},
{
field: "parent_id",
operator: "==",
value: "kSKpUc3xnKjtpyx8cMJC",
},
]
);
I have the following method for displaying 8 latest posts in the app. User will press Show more button and other 8 posts will be displayed. I am trying to add a filter to search by specific fields of the post, and expect the results to be paginated as well. The problem is that I am stuck how to use the query created to get those specific posts and if the user clicks back or cancels filter how to retrieve the latest posts again. I would appreciate if someone could explain how should I get this done.
const getPosts = async () => {
let docs;
let postsReference = firebase.firestore().collection("products").orderBy("createdAt").limit(8);
await postsReference
.get()
.then(documentSnapshot => {
docs = documentSnapshot;
lastVisible = documentSnapshot.docs[documentSnapshot.docs.length - 1];
console.log("last", lastVisible);
});
docs["docs"].forEach(doc => {
postsArray.push(doc.data());
});
postsArray.forEach(function (post) {
createPost(post);
})
}
const paginate = async () => {
let docs;
let postsReference =
firebase.firestore().collection("products").orderBy("createdAt").startAfter(lastVisible).limit(8);
console.log(postsReference);
await postsReference
.get()
.then(documentSnapshot => {
docs = documentSnapshot;
console.log(docs);
lastVisible = documentSnapshot.docs[documentSnapshot.docs.length - 1];
});
docs["docs"].forEach(doc => {
createPost(doc.data());
postsSize++;
});
}
if (showMoreButton != null) {
showMoreButton.addEventListener("click", function () {
paginate();
});
}
applyFilterButton.addEventListener("click", async function () {
....
let filterQuery = firebase
.firestore()
.collection("products")
.where("productType", "==", productTypeOption)
.where("productLocation", "==", productLocationOption);
if (productPriceOption == "high") {
filterQuery = filterQuery.orderBy("price", "desc");
} else {
filterQuery = filterQuery.orderBy("price");
}
await filterQuery
.get()
.then(function (querySnapshot) {
console.log(querySnapshot.docs);
querySnapshot.forEach(function (doc) {
// Does not print to console
console.log("Inside querySnapshot");
console.log(doc.id, " => ", doc.data());
});
});
});
I have this cloud function:
import pLimit from "p-limit";
const syncNotificationsAvatar = async (
userId: string,
change: Change<DocumentSnapshot>
) => {
if (!change.before.get("published") || !change.after.exists) {
return;
}
const before: Profile = change.before.data() as any;
const after: Profile = change.after.data() as any;
const keysToCompare: (keyof Profile)[] = ["avatar"];
if (
arraysEqual(
keysToCompare.map((k) => before[k]),
keysToCompare.map((k) => after[k])
)
) {
return;
}
const limit = pLimit(1000);
const input = [
limit(async () => {
const notifications = await admin
.firestore()
.collectionGroup("notifications")
.where("userId", "==", userId)
.limit(1000)
.get()
await Promise.all(
chunk(notifications.docs, 500).map(
async (docs: admin.firestore.QueryDocumentSnapshot[]) => {
const batch = admin.firestore().batch();
for (const doc of docs) {
batch.update(doc.ref, {
avatar: after.avatar
});
}
await batch.commit();
}
)
);
})
];
return await Promise.all(input);
};
How can I recursively update the notifications collection but first limit the query to 1.000 documents (until there are not more documents) and then batch.update them? I'm afraid this query will timeout since collection could grow big over time.
Posting a solution I worked out, not following the context of the question though but it can easily be combined. Hope it helps someone else.
import * as admin from "firebase-admin";
const onResults = async (
query: admin.firestore.Query,
action: (batch: number, docs: admin.firestore.QueryDocumentSnapshot[]) => Promise<void>
) => {
let batch = 0;
const recursion = async (start?: admin.firestore.DocumentSnapshot) => {
const { docs, empty } = await (start == null
? query.get()
: query.startAfter(start).get());
if (empty) {
return;
}
batch++;
await action(
batch,
docs.filter((d) => d.exists)
).catch((e) => console.error(e));
await recursion(docs[docs.length - 1]);
};
await recursion();
};
const getMessages = async () => {
const query = admin
.firestore()
.collection("messages")
.where("createdAt", ">", new Date("2020-05-04T00:00:00Z"))
.limit(200);
const messages: FirebaseFirestore.DocumentData[] = [];
await onResults(query, async (batch, docs) => {
console.log(`Getting Message: ${batch * 200}`);
docs.forEach((doc) => {
messages.push(doc.data());
});
});
return messages;
};
I really need to brush up on my async await and promises. I would love some advice.
I'm making an async function call to firebase firestore. The function should return a string depending on a single input param.
The feature is for a 1-1 user chat.
The function is to create the chat/find existing chat, and return its ID.
Right now, I am getting undefined as the return value of openChat and can't work out why. The function otherwise works, apart from the return.
I have two functions. One is a React class component lifecycle method, the other my firebase async function.
Here is the class component lifecycle method:
async getChatId(userId) {
let chatPromise = new Promise((resolve, reject) => {
resolve(openChat(userId))
})
let chatId = await chatPromise
console.log('chatId', chatId) //UNDEFINED
return chatId
}
async requestChat(userId) {
let getAChat = new Promise((resolve, reject) => {
resolve(this.getChatId(userId))
})
let result = await getAChat
console.log('result', result) //UNDEFINED
}
render() {
return (<button onClick = {() => this.requestChat(userId)}>get id</button>)
}
and here is the async function:
// both my console.log calls show correctly in console
// indicating that the return value is correct (?)
export async function openChat(otherPersonId) {
const user = firebase.auth().currentUser
const userId = user.uid
firestore
.collection('users')
.doc(userId)
.get()
.then(doc => {
let chatsArr = doc.data().chats
let existsArr =
chatsArr &&
chatsArr.filter(chat => {
return chat.otherPersonId === otherPersonId
})
if (existsArr && existsArr.length >= 1) {
const theId = existsArr[0].chatId
//update the date, then return id
return firestore
.collection('chats')
.doc(theId)
.update({
date: Date.now(),
})
.then(() => {
console.log('existing chat returned', theId)
//we're done, we just need the chat id
return theId
})
} else {
//no chat, create one
//add new chat to chats collection
return firestore
.collection('chats')
.add({
userIds: {
[userId]: true,
[otherPersonId]: true
},
date: Date.now(),
})
.then(docRef => {
//add new chat to my user document
const chatInfoMine = {
chatId: docRef.id,
otherPersonId: otherPersonId,
}
//add chat info to my user doc
firestore
.collection('users')
.doc(userId)
.update({
chats: firebase.firestore.FieldValue.arrayUnion(chatInfoMine),
})
//add new chat to other chat user document
const chatInfoOther = {
chatId: docRef.id,
otherPersonId: userId,
}
firestore
.collection('users')
.doc(otherPersonId)
.update({
chats: firebase.firestore.FieldValue.arrayUnion(chatInfoOther),
})
console.log('final return new chat id', docRef.id)
return docRef.id
})
}
})
}
If you have any useful tips whatsoever, I would be forever grateful to hear them!
Expected results are a returned string. The string is correctly displayed the console.log of the async function).
Actual results are that the return value of the async function is undefined.
You do not return anything from your openChat function, so that function resolves to undefined.
You have to write:
export async function openChat(otherPersonId) {
const user = firebase.auth().currentUser
const userId = user.uid
return firestore // here you need to return the returned promise of the promise chain
.collection('users')
.doc(userId)
.get()
/* .... */
}
And those new Promise in getChatId and requestChat do not make much sense. It is sufficient to await the result of openChat(userId) or this.getChatId(userId)
async getChatId(userId) {
let chatId = await openChat(userId)
console.log('chatId', chatId) //UNDEFINED
return chatId
}
async requestChat(userId) {
let result = await this.getChatId(userId)
console.log('result', result) //UNDEFINED
}
You should await the results from your firestore calls if you want to return their values, you are already using async functions :
export async function openChat(otherPersonId) {
const user = firebase.auth().currentUser
const userId = user.uid
const doc = await firestore
.collection('users')
.doc(userId)
.get()
let chatsArr = doc.data().chats
let existsArr =
chatsArr &&
chatsArr.filter(chat => chat.otherPersonId === otherPersonId)
if (existsArr && existsArr.length >= 1) {
const theId = existsArr[0].chatId
//update the date, then return id
await firestore
.collection('chats')
.doc(theId)
.update({
date: Date.now(),
})
return theId
} else {
const docRef = await firestore
.collection('chats')
.add({
userIds: { [userId]: true, [otherPersonId]: true },
date: Date.now(),
})
const chatInfoMine = {
chatId: docRef.id,
otherPersonId: otherPersonId,
}
//add chat info to my user doc
firestore
.collection('users')
.doc(userId)
.update({
chats: firebase.firestore.FieldValue.arrayUnion(chatInfoMine),
})
//add new chat to other chat user document
const chatInfoOther = {
chatId: docRef.id,
otherPersonId: userId,
}
firestore
.collection('users')
.doc(otherPersonId)
.update({
chats: firebase.firestore.FieldValue.arrayUnion(chatInfoOther),
})
console.log('final return new chat id', docRef.id)
return docRef.id
}
}
You should also directly await your calls to the function :
async getChatId(userId) {
let chatId = await openChat(userId)
console.log('chatId', chatId) //UNDEFINED
return chatId
}
async requestChat(userId) {
let result = await this.getChatId(userId)
console.log('result', result) //UNDEFINED
}