Firestore cloud function to recursively update subcollection/collectionGroup - javascript

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

Related

Firebase Firestore nested collection in Cloud Functions

I am trying to implement the nested query with Firestore in Cloud Functions but stumbled upon issues with reading values in a for loop. Are there ways to adjust the following code so I could do some operations after reading all records from a collection?
const firestore = admin.firestore();
const today = new Date();
const snap = await firestore
.collection('places')
.where('endDate', '<', today)
.get()
const userIds = [...new Set(snap.docs.map((doc: any) => doc.data().owner))];
const updatePromises = snap.docs.map((d: any) => {
return d.ref.update({
isPaid: false,
isActive: false
})
})
await Promise.all(updatePromises);
const userCol = firestore.collection('users');
const userDocs = await Promise.all(userIds.map(uid => userCol.doc(uid).get()));
const userData = userDocs.reduce((acc, doc) => ({
...acc,
[doc.id]: doc.data()
}), {})
snap.docs.forEach((l: any) => {
const ownerData = userData[l.owner];
const { email, displayName } = ownerData;
console.log(email, displayName);
const message = {
// Some values
}
return sendGrid.send(message);
})
return null;
{ owner: '<firebaseUid'>, address: 'Royal Cr. Road 234' }
{ email: 'asdfa#afsdf.com' }
<firebase_uid>: {
displayName: '',
email: '',
phoneNumber: ''
}
The userIds.push(owner); will keep adding duplicate values in that array and if a single user is owner of multiple locations, you'll end up querying same data multiple times. If you are trying to read owner's data along with a location, then try refactoring the code as shown below:
const firestore = admin.firestore();
const today = new Date();
const snap = await firestore
.collection('locations')
.where('isActive', '==', true)
.get()
const userIds = [...new Set(snap.docs.map(doc => doc.data().owner))];
const updatePromises = snap.docs.map((d) => {
return d.ref.update({
isPaid: false,
isActive: false
})
})
// update documents
await Promise.all(updatePromises);
const userCol = firestore.collection("users")
const userDocs = await Promise.all(userIds.map(uid => userCol.doc(uid).get()))
const userData = userDocs.reduce((acc, doc) => ({
...acc,
[doc.id]: doc.data()
}), {})
// To get data of a location's owner
// console.log(userData[ownerId])
snap.docs.forEach((l) => {
const ownerData = userData[l.owner]
// run more logic for each user
})
return null;

async/await with firebase storage

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

useEffect with firestore

I'm trying to do the following, i cannot get any errors but what's weird is, while setRivalGuess in the first condition setRivalGuess(doc.data().guess2) doesn't work, the second one setRivalGuess(doc.data().guess1) works really well. I checked database and everything stored well, that is, each data that I want to fetch is available on the database. I don't know whether it is about my way of using useEffect.
const { rivalGuess, setRivalGuess } = useGame();
const game = query(roomColRef, where("roomId", "==", roomId))
useEffect(() => {
const getUsers = async () => {
const data = await getDocs(game);
data.forEach((doc)=> {
if (doc.data().numberOfPlayers == 2 ){
if(userValue == doc.data().players[0].username)
if (doc.data().guess2 =! 0){
setRivalGuess(doc.data().guess2)}
if (userValue == doc.data().players[1].username)
if (doc.data().guess1 =! 0){
setRivalGuess(doc.data().guess1)} }})};
getUsers();
}, [ rivalGuess, setRivalGuess ])
rivalGuess was before global state , but know it's in the hook.
const UseRivals = (collectionStr) =>{
const [ rivalGuess, setRivalGuess ] =useState([])
const { roomId, userValue } = useGame()
useEffect(() => {
const collectionRef = collection(db, collectionStr);
const q = query(collectionRef, where("roomId", "==", roomId ))
const unSub = onSnapshot(q , (snapshot) => {
snapshot.docs.forEach(doc => {
if (doc.data().numberOfPlayers==2) {
if (userValue == doc.data().players[0].username) if (doc.data().guess2 =! 0)
{ setRivalGuess(doc.data().guess2) }
if (userValue == doc.data().players[1].username) if (doc.data().guess1 =! 0)
{ setRivalGuess(doc.data().guess1)}}})
}, (err) => {
console.log(err.message);
});
return () => unSub();
}, [collectionStr]);
return { rivalGuess };
}
export default UseRivals;

Firestore pagination with react-redux

I try to add pagination using firebase and react redux-toolkit. I get the logic but having trouble using it with redux.
At first I wanted to set lastDoc in redux state but I got error since it is an object.
Then I changed the way and started to keep id of last document in the state. But then I can't get the firebase doc itself
const lastDocRef = firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`)
is not same with
const lastDoc = snap.docs[snap.docs.length-1];
I appreciate any help how to solve this.
import { createSlice, createAsyncThunk, createEntityAdapter } from '#reduxjs/toolkit';
import firebaseService from 'app/services/firebaseService';
const firestoreDB = firebaseService.firestoreDB;
export const getPublishedBooks = createAsyncThunk('adminApp/publishedBooks/getPublishedBooks',
async (params, { dispatch, getState }) => {
const promise = firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.limit(10)
.get()
.then(snap => {
const lastDoc = snap.docs[snap.docs.length-1];
dispatch(setLastDocId(lastDoc.id));
let books = [];
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
})
.catch(error => {
return {}
});
const result = await promise;
return result;
}
);
export const getPublishedBooksNext = createAsyncThunk('adminApp/publishedBooks/getPublishedBooksNext',
async (params, { dispatch, getState }) => {
const { publishedBooks } = getState().adminApp;
const lastDocRef = firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`)
const promise = firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.startAfter(lastDocRef)
.limit(10)
.get()
.then(snap => {
const lastDoc = snap.docs[snap.docs.length-1];
dispatch(setLastDocId(lastDoc.id));
let books = [];
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
})
.catch(error => {
return {}
});
const result = await promise;
return result;
}
);
const publishedBooksAdapter = createEntityAdapter({});
const initialState = publishedBooksAdapter.getInitialState({
lastDocId: null
});
export const {
selectAll: selectPublishedBooks,
selectById: selectPublishedBookById,
selectTotal: selectPublishedBooksTotal
} = publishedBooksAdapter.getSelectors(state => state.adminApp.publishedBooks);
const publishedBooksSlice = createSlice({
name: 'adminApp/publishedBooks',
initialState,
reducers: {
resetPublishedBooks: (state, action) => initialState,
setLastDocId: {
prepare: doc => {
const payload = doc
return { payload };
},
reducer: (state, action) => {
state.lastDocId = action.payload;
}
},
resetLastDocId: {
prepare: () => {
const payload = null
return { payload };
},
reducer: (state, action) => {
state.lastDocId = action.payload;
}
},
},
extraReducers: {
[getPublishedBooks.fulfilled]: publishedBooksAdapter.setAll,
[getPublishedBooksNext.fulfilled]: publishedBooksAdapter.upsertMany
}
});
export const { resetPublishedBooks, setLastDocId, resetLastDocId } = publishedBooksSlice.actions;
export default publishedBooksSlice.reducer;
lastDocRef only returns the doc reference. You need to get the actual doc itself.
const lastDocRef = await firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`).get();
And you should use await instead of then-catch for more readble code.
export const getPublishedBooksNext = createAsyncThunk('adminApp/publishedBooks/getPublishedBooksNext',
async (params, { dispatch, getState }) => {
const { publishedBooks } = getState().adminApp;
try {
const lastDocRef = await firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`).get();
const snap = await firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.startAfter(lastDocRef)
.limit(10)
.get()
const lastDoc = snap.docs[snap.docs.length-1];
let books = [];
dispatch(setLastDocId(lastDoc.id));
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
} catch (error) {
return {}
}
}
);
Edit: You can also save the lastDoc to redux then reference it later to avoid additional workload fetching for the lastDocRef.
export const getPublishedBooksNext = createAsyncThunk('adminApp/publishedBooks/getPublishedBooksNext',
async (params, { dispatch, getState }) => {
const { lastDocRef } = getState().adminApp; // get saved lastDoc
try {
const snap = await firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.startAfter(lastDocRef) // use it here.
.limit(10)
.get()
const lastDoc = snap.docs[snap.docs.length-1];
let books = [];
// dispatch(setLastDocId(lastDoc.id)); // instead of saving the doc id
dispatch(setLastDoc(lastDoc)); // save the last document instead
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
} catch (error) {
return {}
}
}
);

Async function is returning undefined

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
}

Categories