Problem with deleting account and documents with Firestore - swift

I want to delete the user account and all of its documents in Firestore, but with asynchronous queries, Firebase deletes the account before all documents.
Because of that I get an auth error because when firestorm delete the lasts documents, the user no longer exist.
db.collection("users").document(self.user.uid).collection("sachets").getDocuments() { (QuerySnapshot, err) in
if let err = err{
print("Erreur de lecture : \(err)")
} else {
for document in QuerySnapshot!.documents {
db.collection("users").document(self.user.uid).collection("sachets").document(document.documentID).delete(){ err in
if let err = err {
print(" 🔴 Probleme de suppression des docuemnts \(err)")
} else {
print(" 🔵 Documents supprimés")
}
}
}
}
}
self.user?.delete { error in
if let error = error {
print(" 🔴 Probleme de suppression du compte Utilisateur \(error)")
} else {
print(" 🔵 Utilisateur supprimé")
}
}
Someone can tell me how to do ?
thanks

Alright, I set up a test-project and solve this. You also need to make sure to remove all the data from the user fields. For instance, if you're having name, age etc... at the user node.
First I created a function to fetch the current users uid:
func currentUser() -> String {
return Auth.auth().currentUser!.uid
}
Then I created a function to remove all the data for the logged in user. This will first remove all the subcollection data and then it will remove the document for the userID.
To handle asynchronous methods you can use DispatchGroup to notify when all the asynchronous calls are done.
One side note: Make sure to NEVER force-unwrap a value if you can't 100% guarantee there are documents. Otherwise, your app will likely crash. To solve this either use guard or if let to solve this problem.
// Where "user" is the result from currentUser()
func removeData(from user: String) {
db.collection("users").document(user).collection("sachets").getDocuments { (snapshot, error) in
if let error = error {
// Handle error
} else if let documents = snapshot?.documents {
// Using if let to see if there are documents
// Time to delete all the subCollection for the user
self.deleteSubCollectionData(for: user, documents, completion: {
// Once that done, make sure to delete all the fields on the highest level.
self.db.collection("users").document(user).delete(completion: { (error) in
if let error = error {
// Handle error
} else {
// Delete the account
self.deleteAccount()
}
})
})
}
}
}
// This function will remove the subCollectionData
fileprivate func deleteSubCollectionData(for user: String, _ documents: [QueryDocumentSnapshot], completion: #escaping () -> ()) {
let group = DispatchGroup()
documents.forEach({
group.enter()
self.db.collection("users").document(user).collection("sachets").document($0.documentID).delete(completion: { (error) in
if let error = error {
// Handle error
}
group.leave()
})
})
// Once the dispatchGroup is done...
group.notify(queue: .main) {
completion()
}
}
And in the end...
func deleteAccount() {
Auth.auth().currentUser?.delete { (error) in
if let error = error {
print(error)
} else {
print("Deleted account")
}
}
}
If you don't remove all the levels of data, then there still will be data in Firestore.

Your getDocuments() method is asynchronous, so you should delete the account only after having deleted the documents.
Just put the user?.delete method inside the getDocuments() callback
db.collection("users").document(self.user.uid).collection("sachets").getDocuments() { (QuerySnapshot, err) in
if let err = err{
print("Erreur de lecture : \(err)")
} else {
for document in QuerySnapshot!.documents {
db.collection("users").document(self.user.uid).collection("sachets").document(document.documentID).delete(){ err in
if let err = err {
print(" 🔴 Problème de suppression des documents \(err)")
} else {
print(" 🔵 Documents supprimés")
}
}
}
}
self.user?.delete { error in
if let error = error {
print(" 🔴 Problème de suppression du compte Utilisateur \(error)")
} else {
print(" 🔵 Utilisateur supprimé")
}
}
}

Related

Why does Firestore think it retrieved a document when one doesn't exist?

I'm using swift(UI) with firebase and Google SignIn. So far sign in has been great but when I come to using a new user the code below fails - no fatal errors just doesn't add a new user document to Firestore because it seems to think it has retrieved a document which it couldn't because one with the specified ID don't exist.
My guess is the mistake is in the section:
if let error = error as NSError? {
print("Error getting document: \(error.localizedDescription)")
self.setFirestoreUser()
}
the full function:
func fetchUser(documentId: String) {
let docRef = Firestore.firestore().collection("users").document(documentId)
print("User id: \(documentId) ( via fetchUser )")
docRef.getDocument { document, error in
if let error = error as NSError? {
print("Error getting document: \(error.localizedDescription)")
self.setFirestoreUser()
}
else {
if let document = document {
do {
print("Working on coding to User.self")
self.appUser = try document.data(as: User.self)
self.fetchSites()
}
catch {
print("func - fetchUser() error: \(error)")
}
}
}
}
}
The argument 'documentId' is passed on from the google sign process
followup func to create the new Firestore document for this new user:
func setFirestoreUser() {
let googleUser = GIDSignIn.sharedInstance.currentUser
let db = Firestore.firestore()
self.appUser.emailAddress = googleUser?.profile?.email ?? "Unknown"
self.appUser.userGivenName = googleUser?.profile?.givenName ?? ""
self.appUser.userFirstName = googleUser?.profile?.name ?? ""
self.appUser.userProfileURL = googleUser?.profile!.imageURL(withDimension: 100)!.absoluteString ?? ""
do {
try db.collection("users").document(googleUser?.userID ?? "UnknownID").setData(from: self.appUser)
self.fetchUser(documentId: googleUser?.userID ?? "")
} catch {
print(error)
}
}
Calling getDocument on a reference to a non-existing document is not an error (as far as I know), and will return a DocumentSnapshot. To detect whether the document exists, check the exists property on the snapshot instead of (only) checking for errors.
if let document = document {
if !document.exists {
...

How to remove a certain field from map cloud firestore?

I continue to develop a directory application and now I need to delete a specific line in the map from cloud firestore.There is my database:
screenshot
I need to remove field with key "2324141" or change its value.
I know about this method:
"2324141": FieldValue.delete()
But how do I get into the map and delete field or change the value?
thanks!
Here is my model of Object:
class ObjectInFB: Codable {
var objectFromPartsCatalogueListCode: String?
var countOfCurrentObjectInShoppingCart: Int = 1}
Here is func, where i save object in users shoppingBag:
func addToUserShoppingCartFB(user: User?, object: ObjectInFB, count: Int){
guard let user = user else { return }
let objectReadyToBeWriten = [
"UserEmail":"\(user.email!)",
"shoppingCart" : ["\(object.objectFromPartsCatalogueListCode!)" : FieldValue.increment(Int64(count))]] as [String : Any]
db.collection("users")
.document("\(user.uid)")
.setData(objectReadyToBeWriten, merge: true)
{ err in
if let err = err {
print("Error adding document: \(err)")
} else {}
}
}
And after the user in their cart in the application clicks "remove from cart" I want to delete an object from their cart in Firebase. I can only remove any field from the document, but how can I remove a field from the nested ShoppingCart?
func removeShoppingCartFB(object: ObjectInFB, user: User?){
db.collection("users").document(user!.uid).updateData([
"shoppingCart": ["\(object.objectFromPartsCatalogueListCode!)" : FieldValue.delete()],
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
If the goal is to delete a field within a map, it can be accessed through dot notation. For example suppose your structure looks like this
users
uid_0
UserEmail: "test#test.com"
shoppingCart (a map)
2324141: 10
122323424: 13
and we want to remove the 2324141 field. Here's the Swift code to do that
let myCollection = self.db.collection("users")
let myDoc = myCollection.document("uid_0")
myDoc.updateData( ["shoppingCart.2324141": FieldValue.delete(),
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}

Firestore Querying a Snapshot within a Snapshot?

I am trying to listen for any notifications whenever someone has replied to a post that a user has commented on. Below is how my database structure looks.
Posts: (Collection)
Post 1: (Document)
replies: [user2, user3]
Replies: (Collection)
Reply 1: (Document)
ownerId: [user2]
Reply 2: (Document)
ownerId: [user3]
Currently my code has 2 snapshot listeners. The first one listens to the Posts collections, where a user is inside the 'replies' array. Then the second one listens to the Replies collection, where it returns all documents added that != the current user. When a new reply has been detected, it will set the Tab Bar item's badge.
This currently works right now, but I am curious if there is a better method of doing so.
func getNotifications() {
database.collection("Posts")
.whereField("replies", arrayContains: userData["userId"]!)
.order(by: "timestamp", descending: true)
.limit(to: 70)
.addSnapshotListener() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { documentd in
if (documentd.type == .added) {
let dataTemp = documentd.document.data()
let ifUser = dataTemp["ownerId"] as! String
if(ifUser == self.userData["userId"]!) {
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("timestamp", isGreaterThan: dataTemp["timestamp"] as! Int)
.addSnapshotListener() { (querySnapshot3, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot3 else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let temp = diff.document.data()
if((temp["ownerId"] as! String) != self.userData["userId"]!) {
print("new reply")
newArr.append(diff.document.data())
let data = diff.document.data()
let firebaseTime = data["timestamp"] as! Int
let date = lround(Date().timeIntervalSince1970)
if(firebaseTime+10 > date) {
self.tabBar.items![2].badgeValue = "●"
self.tabBar.items![2].badgeColor = .clear
self.tabBar.items![2].setBadgeTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for:.normal)
}
}
}
}
}
}
}
else {
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("ownerId", isEqualTo: self.userData["userId"]!)
.order(by: "timestamp", descending: false)
.limit(to: 1)
.getDocuments() { (querySnapshot2, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
var timestamp = Int()
for documentde in querySnapshot2!.documents {
let temp = documentde.data()
timestamp = temp["timestamp"] as! Int
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("timestamp", isGreaterThan: timestamp)
.addSnapshotListener() { (querySnapshot3, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot3 else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let temp = diff.document.data()
if((temp["ownerId"] as! String) != self.userData["userId"]!) {
print("new reply")
newArr.append(diff.document.data())
let data = diff.document.data()
let firebaseTime = data["timestamp"] as! Int
let date = lround(Date().timeIntervalSince1970)
if(firebaseTime+10 > date) {
self.tabBar.items![2].badgeValue = "●"
self.tabBar.items![2].badgeColor = .clear
self.tabBar.items![2].setBadgeTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for:.normal)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Your code does not have only two listeners, but one listener per post for which the user you are interested in has ever replied for. This will lead to terrible performances very soon and will potentially crash you app as Firestore has a limitation of 100 listeners per client.
I would advise to redesign your data model:
Only one listener on posts that the user has ever replied to (your first listener)
On each reply increment a reply counter in the post doc, this will trigger the snapshot above.
Optimisation 1: on each action on a post you could set a field lastactiontype, which would have a specific value reply for replies only. This way the snapshot is only triggered on replies.
Optimisation 2: set a field timestamp on each action to the current time and only pull the last n (for instance 10) posts in your snapshots, this will limit the number of reads at load time. You will have to implement some special logic to handle the case when your app goes offline and back online and all n posts of the snapshot have changed. This is a must if your app is meant to scale (you dont want a snapshot with no limit on a collection with 100k docs...)
Example:
firestore.collection("Posts")
.where( "lastaction", "==" , "reply")
.where( "replies", "array-contains", uid)
.orderBy("timestamp", "desc").limit(10)

GCD + Firebase Cloudstore

There is a structure that contains structures in the form of an array. In order to fill the core, I must first fill in the internal ones and assign them to the main one. For this, I used the Dispatch Group () and the notices to add and write to the main structure with which I will work. Below is the code I'm using.
But as a result of this method I got a problem. Notification is performed earlier than necessary. What did I do wrong here?
Here is the output from the console:
DONE
[]
FBRecipe(name: "Eel kebab", count: "2", complexity: "3.75", time: "2", category: "Завтрак", type: "САЛАТЫ", about: "Lsvdvskld v\t", ingredient: [], cook: [], photo: [], idOwner: "XT2pgRnAZ8Q5pHH3dHsz5jYUZ613", shared: "0", planing: "0", timestamp: "1536761784.24662")
ingredinet
ingredinet
ingredinet
...
let loadRecipesGroup = DispatchGroup()
let loadItemsQueue = DispatchQueue(label: "ru.bryzgalov.cookbook.loadrecipes", qos: .userInteractive, attributes: [], autoreleaseFrequency: .workItem)
...
func loadRecipeList() {
var recipe = [FBRecipe]()
db.collection("RECIPES").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for documentRecipe in querySnapshot!.documents {
self.loadItemsQueue.async {
var ingredinet = [FBIngredient]()
var stage = [FBStage]()
var photo = [FBDishPhoto]()
db.collection("RECIPES/\(documentRecipe.documentID)/INGREDIENT").getDocuments(completion: { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for documentIngredient in querySnapshot!.documents {
self.loadItemsQueue.async(group: self.loadRecipesGroup) {
let newIngredinet = FBIngredient(dict: documentIngredient.data() as Dictionary<String,AnyObject>)
ingredinet.append(newIngredinet)
print("ingredinet")
}
}
}
})
db.collection("RECIPES/\(documentRecipe.documentID)/STAGE").getDocuments(completion: { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for documentStage in querySnapshot!.documents {
self.loadItemsQueue.async(group: self.loadRecipesGroup) {
let newStage = FBStage(dict: documentStage.data() as Dictionary<String,AnyObject>)
stage.append(newStage)
}
}
}
})
db.collection("RECIPES/\(documentRecipe.documentID)/PHOTO").getDocuments(completion: { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for documentDishPhoto in querySnapshot!.documents {
self.loadItemsQueue.async(group: self.loadRecipesGroup) {
let newDishPhoto = FBDishPhoto(dict: documentDishPhoto.data() as Dictionary<String,AnyObject>)
photo.append(newDishPhoto)
}
}
}
})
self.loadRecipesGroup.notify(queue: .main) {
var newRecipe = FBRecipe(dict: documentRecipe.data() as Dictionary<String,AnyObject>)
newRecipe.ingredient = ingredinet
newRecipe.cook = stage
newRecipe.photo = photo
// recipe.append(contentsOf: newRecipe)
print(ingredinet)
print(newRecipe)
}
}
print("DONE")
}
}
}
}
Your done is called immediately because you the calls marked as async executed asynchronously. That means that the program continues execution while those calls are being executed. Once they have results they complete and print what you expect from them. So what happens is your code runs through every statement till the end. And async calls may finish some time after that.

Swift closures and error handling

hello, i need some help with function and or possibly closures in that function. I want my function to check the firestore users collection for duplicate documents (usernames). If a duplicate is found i want to display a message, if a duplicate is not found, create a new user. i have folowing code:
func checkIfUserExists(username: String, completion: #escaping (Bool) -> Void) {
let docRef = db.collection("users").document(username)
docRef.getDocument { (document, error) in
if error != nil {
print(error)
} else {
if let document = document {
if document.exists {
completion(true)
} else {
completion(false)
}
}
}
}
}
and i call the function with:
if let username = textField.text, username.count > 8 { // Username needs to be more then 8 Char
checkIfUserExists(username: username) { (doesExist) in
if doesExist {
print("user exists")
} else {
print("new User can be created")
}
}
} else {
print("Username needs to be more then 8 char")
}
}
It works, but i have the feeling it is not good practice and i'm making detours. Is this the right way to do it ?
I think the way you're doing it now should work well, but another option to prevent you from having to do a read of the database before writing is to use security rules. For example, if this is the structure of your users collection...
users: [
username1: { // doc ID is the username
userid: abcFirebaseUserId, // a field for the uid of the owner of the username
//...etc
}
]
...then you can use the following rules:
match /users/{username} {
allow create: if request.auth.uid != null;
allow update, delete: if resource.data.userId = request.auth.uid;
}
This allows any authenticated user to create a new username, but only the owner of that username can update it or delete it. If you aren't allowing users to change their username, you wouldn't even have to worry about the second rule. Then, in the client, you go right to creating a username, like so:
func createUsername(username: String, completion: #escaping (String?) -> Void) {
guard let userId = Auth.auth().currentUser.uid else {
completion("no current user")
return
}
let docRef = db.collection("users").document(username)
docRef.setData(data:[userId: userId]) { error in
if let error = error {
completion(error.debugDescription)
} else {
completion(nil)
}
}
}
This would write the new username to the database and pass an error to the closure if there is one. If the username already exists, an insufficient permissions error would be present. When checking if the user exists, you could display the error or alert the user however you wanted.
createUsername(username: username) { err in
if let err = err {
print("user exists")
} else {
print("new User has been created")
}
}
Just a suggestion though. I think they way you're doing it now is fine, too!