QuerySnapshot removing data after limit() reached - firebase

I currently have a query snapshot set up to let me recieve live data from my Firestore instance:
let query = db.collection('Tasks')
.where('active', '==', 1)
.where("starts", ">", +new Date())
.where('user', '==', userId)
.orderBy('starts', 'asc').limit(25);
let observer = query.onSnapshot(async snapshot => {
snapshot.docChanges().forEach(change => {
renderTask(change.doc, change.type);
});
}, err => {
console.trace(err);
});
However, when adding a new "Task" and going over the limit value in the query, ie when the query currently has 25 documents being listened to, it invokes the snapshot listener, saying that a document has been removed. Where change.type === 'removed', but in reality, no documents that were being listened to have changed.
I currently use this method to check if a user has actually deleted a "Task", where active == 0 but when I go over the limit for calling documents in the query it also deletes a random "Task".
I've tried adding an if clause to check if the document data contains active = 0, but this doesn't work and the document (even if it has been changed to active = 0, it still contains active = 1 and I assume Firestore doesn't bother with returning an updated document, as it assumes it's not going to be used? This isn't ideal as well as I would also assume that the listnener for the document would be decoupled as well if firestore believes the document has been removed.
How can I differentiate between documents which have actually been deleted, ie active === 0 and documents that are getting removed due to reaching the query limit threshold?
Many thanks in advance.

This is an interesting use case! There is no out-of-the-box solution. Even by using the oldIndex and newIndex properties, you cannot detect if the document was really deleted from the database ("hard delete") or just removed from the query.
One possible solution would be to query the document to see if it still exists, as shown below:
let observer = query.onSnapshot(
(snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'removed') {
const docRef = db.collection('Tasks').doc(change.doc.id);
docRef
.get()
.then(function (doc) {
if (doc.exists) {
console.log('STILL EXISTS');
//Do something...
} else {
console.log('HARD DELETED');
//Do something else...
}
})
} else {
//.....
}
});
},
(err) => {
console.trace(err);
}
);
Note that you should not use a async in query.onSnapshot(async snapshot => {...}) since the observer in not an async function.

Related

How to access all documents, my all documents only have sub collection in firestore

I have create document like this in react native, I am using rnfirebase library
firestore()
.collection('WaterCanData')
.doc(EntryDate)
.collection('Entries')
.doc(values.customerName)
.set({
CustomerName: values.customerName,
CansOut: values.cansOut,
JarsOut: values.jarsOut,
EmptyCansIn: values.emptyCansIn,
JarsIn: values.jarsIn,
Bottles: values.bottles,
Ice: values.ice
})
.then(() => {
console.log('Entry added!!!!!!!!!');
})
When I try to retrieve EntryDate from WaterCanData Coellection I am not able to fetch it(Document name appears in italic font), So how should I retrive this document which contains a subcollection, Below I have attached my ss of data structure
Data structure
Data structuree
The reason your document appears in italics is because it doesn't currently exist. In Cloud Firestore, subcollections can exist without requiring their parent document to also exist.
Non-existant documents will not appear in queries or snapshots in the client SDKs as stated in the Firebase Console.
This document does not exist, it will not appear in queries or snapshots
If you want to be able to get your entry dates, you need to create the document (which can be empty).
firebase.firestore()
.collection('WaterCanData')
.doc(EntryDate)
.set({}); // an empty document
To create the document at the same time as an entry on it's subcollection, you can use a batched write like so:
const db = firebase.firestore();
const batch = db.batch();
// get references to the relevant locations
const entryDateRef = db
.collection('WaterCanData')
.doc(EntryDate);
const customerRef = entryDateRef
.collection('Entries')
.doc(values.customerName);
// queue the data to write
batch.set(entryDateRef, {});
batch.set(customerRef, {
CustomerName: values.customerName,
CansOut: values.cansOut,
JarsOut: values.jarsOut,
EmptyCansIn: values.emptyCansIn,
JarsIn: values.jarsIn,
Bottles: values.bottles,
Ice: values.ice
})
// make changes to database
batch.commit()
.then(() => {
console.log('Entry added!!!!!!!!!');
});
This will then allow you to list all of the entry dates in your database using something like:
firebase.firestore().collection('WaterCanData')
.get()
.then((querySnapshot) => {
querySnapshot.forEach(doc => {
const entryDate = doc.id;
// const customerEntriesRef = doc.ref.collection('Entries');
console.log('Entry date found: ' + entryDate);
}
});
If (as an example) you wanted to also find how many entries were linked to a given date, you would need to also query each subcollection (here the code gets a little more confusing).
firebase.firestore().collection('WaterCanData')
.get()
.then((querySnapshot) => {
const fetchSizePromises = [];
// for each entry date, get the size of it's "Entries" subcollection
querySnapshot.forEach(doc => {
const entryDate = doc.id;
const customerEntriesRef = doc.ref.collection('Entries');
// if this get() fails, just store the error rather than throw it.
const thisEntrySizePromise = customerEntriesRef.get()
.then(
(entriesQuerySnapshot) => {
return { date: entryDate, size: entriesQuerySnapshot.size }
},
(error) => {
return { date: entryDate, size: -1, error }
}
);
// add this promise to the queue
fetchSizePromises.push(thisEntrySizePromise)
}
// wait for all fetch operations and return their results
return Promise.all(fetchSizePromises);
})
.then((entryInfoResults) => {
// for each entry, log the result
entryInfoResults.forEach((entryInfo) => {
if (entryInfo.error) {
// this entry failed
console.log(`${entryInfo.date} has an unknown number of customers in its Entries subcollection due to an error`, entryInfo.error);
} else {
// got size successfully
console.log(`${entryInfo.date} has ${entryInfo.size} customers in its Entries subcollection`);
}
}
});
Using below code you can console every document id inside waterCanData collection. In your database you have only one document, then it will console your document id. (10042021)
firestore()
.collection('WaterCanData')
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id)
});
})

Retrieve the document ids of a nested subcollection in Firestore

I have the following structure in my Firebase Firestore:
Is it possible to query just the document ids shown in the middle column of the second image? This is my current query, which seems to me the most logical way to do it. The console.log returns the success message, but the array is empty:
async function getListofPosts() {
try{
return await useFireStore
.collection('chats')
.doc(`${postId}`)
.collection('chat')
.get().then(res => {
let theMessageList = [];
res.forEach(doc => {
theMessageList.push(doc.data(), doc.id )
});
setMessageList(theMessageList);
});
}
catch {
console.log('error on getListofPosts')
}
finally {
console.log('getListofPosts worked')
}
}
Any help very much appreciated.
Cheers, Matt
Related issue - Firestore Cloud Function empty collection
It seems like your documents under collections chats and chat are empty (document IDs are shown in italics). So to solve this you shall use collectionGroup query.
return await useFireStore
.collectionGroup('chat')
.get().then(res => {
let theMessageList = [];
res.forEach(doc => {
theMessageList.push(doc.data(), doc.id )
});
setMessageList(theMessageList);
});
Before going with collectionGroup query, I recommend you to read this query and it's limitations from here - https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query

How to get documents with array containing a specific string in Firestore with React-Native

I am just getting started with Firebase and am trying to determine how to best structure my Firestore database.
What I want is to find all documents from an 'events' collection where 'participants' (which is an array field on each event document which contains objects with keys 'displayName' and 'uid') contains at least one matching uid. The list of uids I am comparing against will be the users' friends.
So in more semantic terms, I want to find all events where at least one of the participants of that event is a 'friend', using the uid of the event participants and of the users friends.
Hope I haven't lost you. Maybe this screenshot will help.
Here is how I've designed the 'events' collection right now
Would a deep query like this doable with Firestore? Or would I need to do the filtering on client side?
EDIT - added code
// TODO: filter events to only those containing friends
// first get current users friend list
firebase.firestore().doc(`users/${this.props.currentUser.uid}`)
.get()
.then(doc => {
return doc.data().friends
})
.then(friends => { // 'friends' is array of uid's here
// query events from firestore where participants contains first friend
// note: I plan on changing this design so that it checks participants array for ALL friends rather than just first index.
// but this is just a test to get it working...
firebase.firestore().collection("events").where("participants", "array-contains", friends[0])
.get()
.then(events => {
// this is giving me ALL events rather than
// filtering by friend uid which is what I'd expect
console.log(events)
// update state with updateEvents()
//this.props.dispatch(updateEvents(events))
})
})
I am using React-Native-Firebase
"react-native": "^0.55.0",
"react-native-firebase": "^4.3.8",
Was able to figure this out by doing what #Neelavar said and then changing my code so that it chains then() within the first level collection query.
// first get current users' friend list
firebase.firestore().doc(`users/${this.props.currentUser.uid}`)
.get()
.then(doc => {
return doc.data().friends
})
// then search the participants sub collection of the event
.then(friends => {
firebase.firestore().collection('events')
.get()
.then(eventsSnapshot => {
eventsSnapshot.forEach(doc => {
const { type, date, event_author, comment } = doc.data();
let event = {
doc,
id: doc.id,
type,
event_author,
participants: [],
date,
comment,
}
firebase.firestore().collection('events').doc(doc.id).collection('participants')
.get()
.then(participantsSnapshot => {
for(let i=0; i<participantsSnapshot.size;i++) {
if(participantsSnapshot.docs[i].exists) {
// if participant uid is in friends array, add event to events array
if(friends.includes(participantsSnapshot.docs[i].data().uid)) {
// add participant to event
let { displayName, uid } = participantsSnapshot.docs[i].data();
let participant = { displayName, uid }
event['participants'].push(participant)
events.push(event)
break;
}
}
}
})
.then(() => {
console.log(events)
this.props.dispatch(updateEvents(events))
})
.catch(e => {console.error(e)})
})
})
.catch(e => {console.error(e)})
})

Does deleting a document from Firebase's Cloud Firestore delete any sub collections in that document?

Here is an example of how I am currently deleting documents:
let transactionsRef = db.collection(Globals.GroupsPath).document(Group.instance.getAll().id).collection(Globals.GroupTransactions)
let query = transactionsRef.whereField(Globals.TransactionCreator, isEqualTo: Auth.auth().currentUser!.uid)
query.getDocuments { (snapshot, error) in
guard error == nil else {
print(error!.localizedDescription)
callback(error)
return
}
if let snapshot = snapshot {
let batch = self.db.batch()
for doc in snapshot.documents {
batch.deleteDocument(doc.reference)
}
batch.commit()
callback(nil)
}
// TODO: Create an error here and return it.
}
I noticed however that after doing this that in the Firestore database the document is greyed out but I can still click on it and view collections within that document and their data!
Do I need to manually delete each item from the sub collection before deleting the parent document or does it just take a while for the delete to complete? What's going on here?
Deleting a document does not delete subcollections. You do need to manually delete all subcollections, as outlined here in the documentation. You will see that the documentation doesn't recommend deleting subcollections from the client because there are so many ways that could go wrong. It's labor-intensive for the client, may involve read and write permissions issues, etc. You'll want to use a server or serverless solution. So, for example, this is how deleting subcollections would work server-side with Node.js:
function deleteCollection(db, collectionPath, batchSize) {
var collectionRef = db.collection(collectionPath);
var query = collectionRef.orderBy('__name__').limit(batchSize);
return new Promise((resolve, reject) => {
deleteQueryBatch(db, query, batchSize, resolve, reject);
});
}
function deleteQueryBatch(db, query, batchSize, resolve, reject) {
query.get()
.then((snapshot) => {
// When there are no documents left, we are done
if (snapshot.size == 0) {
return 0;
}
// Delete documents in a batch
var batch = db.batch();
snapshot.docs.forEach((doc) => {
batch.delete(doc.ref);
});
return batch.commit().then(() => {
return snapshot.size;
});
}).then((numDeleted) => {
if (numDeleted === 0) {
resolve();
return;
}
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(() => {
deleteQueryBatch(db, query, batchSize, resolve, reject);
});
})
.catch(reject);
}

Can Firestore update multiple documents matching a condition, using one query?

In other words, I'm trying to figure out what is the Firestore equivalent to this in SQL:
UPDATE table SET field = 'foo' WHERE <condition>`
Yes, I am asking how to update multiple documents, at once, but unlike the linked questions, I'm specifically asking how to do this in one shot, without reading anything into memory, because there's no need to do so when all you want is to set a flag on all documents matching a condition.
db.collection('table')
.where(...condition...)
.update({
field: 'foo',
});
is what I expected to work, CollectionReference doesn't have an .update method.
The
Transactions and Batched Writes documentation mentions transactions and batched writes. Transactions are out because "A transaction consists of any number of get() operations followed by any number of write operations" Batched writes are also not a solution because they work document-by-document.
With MongoDB, this would be
db.table.update(
{ /* where clause */ },
{ $set: { field: 'foo' } }
)
So, can Firestore update multiple documents with one query, the way SQL database or MongoDB work, i.e. without requiring a round-trip to the client for each document? If not, how can this be done efficiently?
Updating a document in Cloud Firestore requires knowings its ID. Cloud Firestore does not support the equivalent of SQL's update queries.
You will always have to do this in two steps:
Run a query with your conditions to determine the document IDs
Update the documents with individual updates, or with one or more batched writes.
Note that you only need the document ID from step 1. So you could run a query that only returns the IDs. This is not possible in the client-side SDKs, but can be done through the REST API and Admin SDKs as shown here: How to get a list of document IDs in a collection Cloud Firestore?
Frank's answer is actually a great one and does solve the issue.
But for those in a hurry maybe this snippet might help you:
const updateAllFromCollection = async (collectionName) => {
const firebase = require('firebase-admin')
const collection = firebase.firestore().collection(collectionName)
const newDocumentBody = {
message: 'hello world'
}
collection.where('message', '==', 'goodbye world').get().then(response => {
let batch = firebase.firestore().batch()
response.docs.forEach((doc) => {
const docRef = firebase.firestore().collection(collectionName).doc(doc.id)
batch.update(docRef, newDocumentBody)
})
batch.commit().then(() => {
console.log(`updated all documents inside ${collectionName}`)
})
})
}
Just change what's inside the where function that queries the data and the newDocumentBody which is what's getting changed on every document.
Also don't forget to call the function with the collection's name.
The simplest approach is this
const ORDER_ITEMS = firebase.firestore().collection('OrderItems')
ORDER_ITEMS.where('order', '==', 2)
.get()
.then(snapshots => {
if (snapshots.size > 0) {
snapshots.forEach(orderItem => {
ORDER_ITEMS.doc(orderItem.id).update({ status: 1 })
})
}
})
For Dart / Flutter user (editted from Renato Trombini Neto)
// CollectionReference collection = FirebaseFirestore.instance.collection('something');
// This collection can be a subcollection.
_updateAllFromCollection(CollectionReference collection) async {
var newDocumentBody = {"username": ''};
User firebaseUser = FirebaseAuth.instance.currentUser;
DocumentReference docRef;
var response = await collection.where('uid', isEqualTo: firebaseUser.uid).get();
var batch = FirebaseFirestore.instance.batch();
response.docs.forEach((doc) {
docRef = collection.doc(doc.id);
batch.update(docRef, newDocumentBody);
});
batch.commit().then((a) {
print('updated all documents inside Collection');
});
}
If anyone's looking for a Java solution:
public boolean bulkUpdate() {
try {
// see https://firebase.google.com/docs/firestore/quotas#writes_and_transactions
int writeBatchLimit = 500;
int totalUpdates = 0;
while (totalUpdates % writeBatchLimit == 0) {
WriteBatch writeBatch = this.firestoreDB.batch();
// the query goes here
List<QueryDocumentSnapshot> documentsInBatch =
this.firestoreDB.collection("student")
.whereEqualTo("graduated", false)
.limit(writeBatchLimit)
.get()
.get()
.getDocuments();
if (documentsInBatch.isEmpty()) {
break;
}
// what I want to change goes here
documentsInBatch.forEach(
document -> writeBatch.update(document.getReference(), "graduated", true));
writeBatch.commit().get();
totalUpdates += documentsInBatch.size();
}
System.out.println("Number of updates: " + totalUpdates);
} catch (Exception e) {
return false;
}
return true;
}
Combining the answers from Renato and David, plus async/await syntax for batch part. Also enclosing them a try/catch in case any promise fails:
const updateAllFromCollection = async (collectionName) => {
const firebase = require('firebase-admin');
const collection = firebase.firestore().collection(collectionName);
const newDocumentBody = { message: 'hello world' };
try {
const response = await collection.where('message', '==', 'goodbye world').get();
const batch = firebase.firestore().batch();
response.docs.forEach((doc) => {
batch.update(doc.ref, newDocumentBody);
});
await batch.commit(); //Done
console.log(`updated all documents inside ${collectionName}`);
} catch (err) {
console.error(err);
}
return;
}
I like some of the answers but I feel this is cleaner:
import * as admin from "firebase-admin";
const db = admin.firestore();
const updates = { status: "pending" }
await db
.collection("COLLECTION_NAME")
.where("status", "==", "open")
.get()
.then((snap) => {
let batch = db.batch();
snap.docs.forEach((doc) => {
const ref = doc.ref;
batch.update(ref, updates);
});
return batch.commit();
});
It uses batched updates and the "ref" from the doc.
If you have already gathered uids for updating collections, simply do these steps.
if(uids.length) {
for(let i = 0; i < uids.length; i++) {
await (db.collection("collectionName")
.doc(uids[i]))
.update({"fieldName": false});
};
};

Resources