Delete all documents in Firestore collection - android

I made a button that is supposed to let users delete all their data based on their userid, the button has the same purpose like a "delete your account" button, but it is not working. I tried to delete all documents in the collection using this code:
final String userid = FirebaseAuth.getInstance.getCurrentUser().getUid();
db.collection("main").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
for (QueryDocumentSnapshot queryDocumentSnapshot : task.getResult()){
db.collection("main").document(userid).delete();
}
}
})
There is a runtimeExecution : no permission error
com.google.android.gms.tasks.RuntimeExecutionException: com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
I set the security rules as below so that no other users could access other users' data.
service cloud.firestore {
match /databases/{database}/documents {
match /main/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Not sure what to do, I thought the security rules could prevent security issues but I think it is causing this permission error?
Thank you in advance.

Update:
After doing some research and read so many other answers on this site about this matter, I used this method and I think it is so simple for a dummy like me but never mentioned on other answers I am not sure why:
To remove a user completely from authentication, I used the Firestore extension called "Delete User Data" which is a great help, it is magical and allows me to remove a user just by using this code:
FirebaseAuth.getInstance().getCurrentUser().delete().addOnSuccessListener(new OnSuccessListener<Void>() {...}
It doesn't only delete user authentication, with just that code, that particular user's data in the firestore and the images in the storage are also gone. It is fantastic!
The important point is that you have to connect the user UID from the authentication and connects the UID to the firestore and the storage.
To know more about how to use the "Delete User Data" Firestore extension(it is still in beta mode), try looking at this great blog tutorial from JORGE VERGARA : Installing the Delete User Data extension
How I set the configuration of the extension to delete a user:
How to use the extension: It is so simple I am so happy that it helps me to delete a user so easily without fuss
This is the storage structure:
The structure of my Firestore:
main(collection)--->userID---->journal(collection)----->journalIDdsvsfbsf
----->journalIDdfvdbgnd
--->userID2--->journal(collection)----->journalIDdsvsfbsf
----->journalIDdfvdbgnd
The Authentication structure:
See how the firestore and storage have to be connected with the same user UID so that the Firestore extension could delete a user easily by deleting the authentication.
If you still want to use the old method:
I noticed that you cannot delete a firestore document that contains one or multiple sub-collections with just delete method. For example, if you want to delete a document that contains 3 sub-collections, in order to delete that document(a user in my case), you have to get all the documents in one of the sub-collection, delete them all, then proceed to do the same to all the other sub-collections in that document. Once all the sub-collections in the document has been deleted, then the document that contains them will be gone itself. That's how Firestore works it seems.
To put it in code:
to delete a document that contains a sub-collection "journal"
Without using the extension, I have to do it like this:
FirebaseFirestore.getInstance().collection("main").document(userid).collection("journal").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (final QueryDocumentSnapshot document : task.getResult()) {
if (document.exists()){
db.collection("main").document(userid).collection("journal").document(document.getId()).delete().addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SettingsActivity.this, "Journal: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
} else {
Toast.makeText(SettingsActivity.this, "No data in JOURNAL", Toast.LENGTH_SHORT).show();
}
}
} else {
Toast.makeText(SettingsActivity.this, "Journal task: " + task.getException(), Toast.LENGTH_LONG).show();
}
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SettingsActivity.this, "Error getting all the documents: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
If you have multiple sub-collections in that particular document, to delete it you have to repeat that code multiple times for different sub-collections which will make the codes very long.
I hope this is helpful for someone out there who is trying to delete a user from Firestore. :) Use the extension already.

This query is trying to get all documents in the collection called "main":
db.collection("main").get()
Your security rules don't allow that. The rules only allow a user to read and write their own document in main.
It's not clear what exactly you're trying to delete, but if it's just the user document under main, you don't need a query at all. Just do this:
db.collection("main").document(userid).delete();

If you want to access all the documents under main collection you should change your Firebase rules to
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Although if you want to delete only single user data which you are doing in your code here :-
db.collection("main").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
for (QueryDocumentSnapshot queryDocumentSnapshot : task.getResult()){
db.collection("main").document(userid).delete();
}
}
})
You shouldn't right the whole query you can simply delete the user document using
db.collection("main").document(userid).delete();

There is no API to delete an entire collection (or its contents) in one go.
From the Firestore documentation:
https://firebase.google.com/docs/firestore/manage-data/delete-data
"To delete an entire collection or subcollection in Cloud Firestore, retrieve all the documents within the collection or subcollection and delete them. If you have larger collections, you may want to delete the documents in smaller batches to avoid out-of-memory errors. Repeat the process until you've deleted the entire collection or subcollection."

Related

Firebase FireStore # Android - best practices for offline first

I am using Firebase Firestore (Not the realtime database) and programming an android app with it.
I want my app to work as offline first, which means that when the app loads FireStore will load the data from the Chace and show it to the user, and after that query changes from the server.
To accomplish that, I am doing a normal GET request (Not listener) with source of Chace.
After the get call has completed, I am attaching a listener to listen for all the update. (Locally or from the server), so I Can present the user the updated status.
The problem with this method is that basiclly it means I will download the same object twice First time on the initial get request, and than on the initial listener. Which means I will send duplicates to the UI.
Is there any practice available in the firestore API to avoid this? So the listener will not return records that the get request has already synced?
See below example code. Thx!
GET request: (runs first)
db.collection(sCollection)
.get(SOURCE.CHACE)
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Log.d(TAG, document.getId() + " => " + document.getData());
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Listener: (runs second)
m_registration = db.collection(sName).
orderBy("stamp", Query.Direction.ASCENDING)
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
for (DocumentChange dc : value.getDocumentChanges()) {
}
catch(JSONException ex) {
}
}
The Firebase Firestore handles offline so good that you can use it as oofline first database without doing anything on your own. You can read, listen and edit data as if you are doing it synchornously. You should deffinitely check this out.
That means you don't need to use get to make the offline better. It will work seamlesly with the listener to.

Android Cloud Firestore efficient way to get few documents from collection

What is the most efficient way to get 3 documents (marked red) from firebase collection? One more obstacle for me is, that user document has auto-generated Id. The code below works but I think is not efficient way to do it. If 'times' collection grows, retrieving whole collection while want only one or two documents doesn't make sense.
If there is no way to improve it, any suggestions of modifying my firestore structure to achieve the goal?
FirebaseFirestore db = FirebaseFirestore.getInstance();
db.collection("users")
.whereEqualTo("userName", "tolek")
.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
// task - retrieves entire user document with all fields and collections, what I don't want - want only document Id
if (!task.getResult().getDocuments().isEmpty()) {
db.collection("users")
.document(task.getResult().getDocuments().get(0).getId())
.collection("times")
.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (!task.getResult().getDocuments().isEmpty()) {
for (DocumentSnapshot snapshot : task.getResult().getDocuments()) {
Log.d("TAG", "Documents : " + snapshot.getId());
//here also I receive whole "times" collection but want only 3 documents
}
}
}
});
}
}
});
To simplify the loading of the 3 times documents, you can use an in query:
CollectionReference timesRef = db.collection("users")
.document(task.getResult().getDocuments().get(0).getId())
.collection("times");
timesRef.whereIn(FieldPath.documentId(), Arrays.asList("20210225", "20210226", "20210227"));
This works for up to 10 values. If you have more than 10 IDs, you'll need to fire more than one query.

is there a possible way to catch no connection or timeout Exception in firestore?

I am making my app that when it firestore tries to communicate with the server then when it fails it will display to the user that he has no connection
first of all this code checks if the id that came from google Auth is already registered to the firestore
public FirebaseHelper CheckAccount(String id, final OnCheckAccountListener checkAccountListener){
DocumentReference docIdRef = db.collection(Values.users).document(id);
docIdRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()){
DocumentSnapshot document = task.getResult();
checkAccountListener.AccountExist(document.exists());
}else{
if (task.getException().equals(FirebaseFirestoreException.Code.UNAVAILABLE)){
//Shows user that he/she is offline
checkAccountListener.onNoConnection();
}else {
//other catastrophic error
checkAccountListener.onError(task.getException().getMessage());
}
}
}
});
return this;
}
public interface OnCheckAccountListener{
void AccountExist(boolean itExists);
void onNoConnection();
void onError(String Error);
}
now when i disconnect my internet then auth my account it will check the account then task.isSuccessful() is false and instead of displaying no connection like this
no connection dialog
it shows this
error dialog
and as you can see the reason is client is offline
there is a possible solution but its not nice
the solution is just
if(task.getException().getMessage().contains("offline")
sorry if bad explanation if you need more details just reply thx!
and if your wondering why there is an interface in the code
because it runs in different classes just to neat things up!
and the pictures are customdialog
and i also made a loading dialog like this!
enter image description here
all image in the dialog are GIF's and they are awesome!
Firestore does not have connection errors that you can catch. The SDK will automatically initiate and retry broken connections in the background for as long as your app process is alive. You don't have any direct control over how this works.
For a list of error codes that you can get by handling an FirebaseFirestoreException object, read the API documenatation.

How to add new fields in Firestore Document from two different sources simultaneously?

When a new user registers in my app using firebase-authentication custom sign in using email and password, I need to update that data into my firestore.
But Firebase only has FirebaseAuth.createUserWithEmailAndPassword(email, password) to create a new account and hence I cannot update my user's username at the same time.
To update the E-Mail in Firestore, I use Firebase cloud functions. Here's the code:
export const onNewUserJoined = functions.auth.user().onCreate((user) => {
//const newUserDisplayName = user.displayName //CAN'T USE THIS. REASON is BELOW
const newUserUID = user.uid
const newUserEmail = user.email
const timeCreated = Date.now()
console.log(`${newUserUID} has joined.`)
return admin.firestore().collection('Agent').doc(`${newUserUID}`).set({"E-Mail": newUserEmail, "Time": timeCreated})
})
OK, great now I have updated the E-Mail and time created in Firestore successfully.
But next challenge is I need to update the user's username in the same Firestore document. I do it instantly after the createUserWithEmailAndPassword() like this:
DocumentReference dDocRef = FirebaseFirestore.getInstance().document(documentPath);
Map<String, Object> updateUsernameAndPhone = new HashMap<>();
updateUsernameAndPhone.put("username", username);
updateUsernameAndPhone.put("phoneData", phoneModel);
dDocRef.update(updateUsernameAndPhone).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(), "Data successfully stored in Firestore", Toast.LENGTH_SHORT).show();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(getApplicationContext(), e.toString(), Toast.LENGTH_SHORT).show();
}
});
Now, it depends on who acts first, the cloud function or the user's device.
If the cloud functions act first, then there is no issue. The username and phoneModel both get updated into the document successfully. No issues.
But incase, the phone acts first then I get the following error:
As this error has occurred, username isn't in the document and only email and timeCreated are in the document updated by the cloud function which got late to create document so that user's device can update the username with ease.
I CAN'T use .SET instead of .update() in my app because if I use .set() and the cloud functions create the email and timeCreated fields first. Then the device will DELETE them and put username and phoneModel.
So how can I do this?
I can forcefully delay updating the username by putting it in the next activity so that cloud functions get enough time to do their job, but my signUpActivity asks for username along with email and password edit texts. I don't want to create a separate activity for that.
I used to use .update() when my data was stored in realtime database and it used to create the child even if the path didn't exist. But it looks firestore won't update if the field doesn't exist.
Any solution for this?
I tried as per #DougStevenson said and here's my code:
final String newUserUID = Objects.requireNonNull(signUpAuth.getCurrentUser()).getUid();
final String documentPath = "Agent/" + newUserUID;
FirebaseFirestore fFirestoreRef = FirebaseFirestore.getInstance();
final DocumentReference dDocRef = fFirestoreRef.document(documentPath);
fFirestoreRef.runTransaction(new Transaction.Function<Void>() {
#Nullable
#Override
public Void apply(#NonNull Transaction transaction) throws
FirebaseFirestoreException {
DocumentSnapshot documentSnapshot = transaction.get(dDocRef);
transaction.update(dDocRef, "username", username);
transaction.update(dDocRef, "phoneData", phoneModel);
return null;
}
}).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(), "Data updated in Firestore . . .", Toast.LENGTH_SHORT).show();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(getApplicationContext(), e.toString(), Toast.LENGTH_SHORT).show();
}
});
But no luck. It still gives me error: Cannot update a document which does not exist.
Use a transaction to resolve conflicts from multiple clients that are all trying to modify the same document. The transaction handler will be retried on the client if it detects the document was modified before the change could take place.

Performance of Chaining Firebase Firestore Query and Document Snapshot Tasks

I'm creating an Android app that uses Firebase Firestore to store some data. Since collection group queries are not yet supported in Firestore, in order to build some of the result sets that I need, I have to do a Query followed by multiple DocumentReference.get() calls to achieve the desired end result.
My implementation is as follows:
PlaylistFacade.java
public static Task<QuerySnapshot> GetPlaylistSubscriptionsByOwner(FirebaseFirestore db, #NonNull String ownerUserId)
{
Query playlistRef = db.collection("playlistSubscriptions")
.whereEqualTo("ownerId", ownerUserId);
return playlistRef.get();
}
public static Task<DocumentSnapshot> GetPlaylist(FirebaseFirestore db, #NonNull String playlistId)
{
return db.collection("playlists").document(playlistId).get();
}
PlaylistActivity.java
PlaylistFacade.GetPlaylistSubscriptionsByOwner(mFirestore, this.GetCurrentUser().getUid())
.continueWithTask(new Continuation<QuerySnapshot, Task<List<Task<?>>>>() {
#Override
public Task<List<Task<?>>> then(#NonNull Task<QuerySnapshot> task) throws Exception {
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for(DocumentSnapshot doc:task.getResult().getDocuments())
{
PlaylistSubscription ps = doc.toObject(PlaylistSubscription.class);
ps.setId(doc.getId());
tasks.add(PlaylistFacade.GetPlaylist(mFirestore, ps.getPlaylistId()));
}
return Tasks.whenAllComplete(tasks);
}
})
.addOnCompleteListener(this, new OnCompleteListener<List<Task<?>>>() {
#Override
public void onComplete(#NonNull Task<List<Task<?>>> task) {
if(task.isSuccessful()){
List<Task<?>> tasks = task.getResult();
List<Playlist> playLists = new ArrayList<>();
int errorCount = 0;
for(Task<?> docTask : tasks)
{
if(docTask.isSuccessful())
{
DocumentSnapshot ds = (DocumentSnapshot)docTask.getResult();
Playlist pl = ds.toObject(Playlist.class);
pl.setId(ds.getId());
playLists.add(pl);
}
else
{
Crashlytics.logException(docTask.getException());
errorCount++;
}
}
if(errorCount > 0)
Toast.makeText(PlaylistActivity.this, "Encountered " + errorCount + " errors.", Toast.LENGTH_LONG).show();
else
Toast.makeText(PlaylistActivity.this, "Success", Toast.LENGTH_LONG).show();
}
else
{
Crashlytics.logException(task.getException());
Toast.makeText(PlaylistActivity.this, task.getException().getMessage(), Toast.LENGTH_LONG).show();
}
}
});
The above works just fine, but I'm curious if there may be a better way to do it.
My questions are as follows:
Is chaining the Task objects using List<> the ideal approach? Am I giving up any potential performance by using this approach?
Will using the Task chain still allow me to take advantage of Firebase's ability to pipeline requests?
Will my use of Tasks.whenAllComplete() allow me to conditionally accept failure of some or all results, or does Firebase's pipelining cause errors to propagate across requests such that I should really just use Tasks.whenAllSuccess() and avoid the need check success of each individual request?
Response times of my implementation seem fine on small result sets. Am I likely to get better performance as my result set grows if I build my result set in a Cloud Function before returning it to my app instead?
At what complexity of Firestore actions should I really be using an Executor as demonstrated in the DocSnippets.java sample on the deleteCollection(...) function?
Would using a Transaction to bundle requests ever net me any performance gains? Performance implications of doing reads inside a transaction aren't discussed in the documentation.
Any news of when collection group queries will be available in Firestore? In time for Google IO, perhaps?
Thank you for your time.
Other helpful resources for those who end up here with similar questions:
Doug Stevenson's Firebase Blog Post on Task Wiring

Categories

Resources