Cloud Firestore Swift: How to delete a query of documents - ios

I would like to delete all documents in my collection Usernames that has the field UID as the current user's ID. This is my code so far:
let uid = Auth.auth().currentUser!.uid
db.collection("Usernames").whereField("UID", isEqualTo: uid).delete
but here comes the error:
Value of type 'Query' has no member 'delete'.
Any special technique to this? Thanks!

The guide shows you how to delete data. It also notes that deleting entire collections shouldn't be done from the client. There's no way to delete from a query -- you will have to get all the documents and delete them individually. If there is the potential for lots of documents, then you should do this server-side. If you know for sure there will only be a few, you can query them, get the doc references, and then delete them, like this:
let uid = Auth.auth().currentUser!.uid
db.collection("Usernames").whereField("UID", isEqualTo: uid).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
document.reference.delete()
}
}
}
If this is something that will need to be done frequently, then this is a good opportunity to examine your data structure. Maybe make a collection where user's Usernames are listed under their uid. Then you could just query that single doc and use it to reference the other ones to delete.
Usernames: {
byUID: {
uid1: {
funky_monkey: true,
goodTimes: true
}
}
byUsername: {
funky_monkey: {
UID: uid1
}
}
}
This is just one suggestion. There are definitely other options that may be better for your app.

Related

Getting Information from Two Firebase Root Collections

I am using Firebase Firestore with Swift and SwiftUI. I have two collections Users and Tweets. Each Tweet store the userId of the user who tweeted. I want to fetch all the tweets along with the user information associated with tweet. Since both are stored in different collections at the root level, this poses a problem. Here is one of the solutions.
private func populateUserInfoIntoTweets(tweets: [Tweet]) {
var results = [Tweet]()
tweets.forEach { tweet in
var tweetCopy = tweet
self.db.collection("users").document(tweetCopy.userId).getDocument(as: UserInfo.self) { result in
switch result {
case .success(let userInfo):
tweetCopy.userInfo = userInfo
results.append(tweetCopy)
if results.count == tweets.count {
DispatchQueue.main.async {
self.tweets = results.sorted(by: { t1, t2 in
t1.dateUpdated > t2.dateUpdated
})
}
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
Kinda messy!
Another way is to store the documentRef of the User doc instead of the userId.
If I store the User document reference then I am using the following code:
init() {
db.collection("tweets").order(by: "dateUpdated", descending: true)
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
snapshot?.documents.compactMap { document in
try? document.data(as: Tweet.self)
}.forEach { tweet in
var tweetCopy = tweet
tweetCopy.userDocRef?.getDocument(as: UserInfo.self, completion: { result in
switch result {
case .success(let userInfo):
tweetCopy.userInfo = userInfo
self.tweets.append(tweetCopy)
case .failure(let error):
print(error.localizedDescription)
}
})
}
}
}
The above code does not work as expected. I am wondering there must be a better way. I don't want to store all the user information with the Tweet because it will be storing a lot of extra data.
Recommendations?
The common alternative is to duplicate the necessary information of the user in their tweets, a process known as denormalization. If you come from a background in relational databases this may sounds like blasphemy, but it's actually quite common in NoSQL data modeling (and one of the reasons these databases scale to such massive numbers of readers).
If you want to learn more about such data modeling considerations, I recommend checking out NoSQL data modeling and watching Todd's excellent Get to know Cloud Firestore series.
If you're wondering how to keep the duplicated data up to date, this is probably also a good read: How to write denormalized data in Firebase

How to get user specific data from firebase in iOS app?

Firstly I've reviewed Firebase documentation, they only have two separate explanations on login user and CRUD operations but not really how both they can be connected with security rules. Despite that I've worked on it here is the security rules. In iOS code I fetch the list and also add the item with user id
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /contacts/{creatorsID} {
allow read, write: if request.auth != null && request.auth.uid == creatorsID;
}
}
}
ERROR: Write at contacts/wv6fMMhBpw3ROEOyUMBe failed: Missing or insufficient permissions
{ creatorsID: "", name: "" }
func fetchData() {
db.collection("contacts").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.contacts = documents.map { (queryDocumentSnapshot) -> Contact in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let userID = data["creatorsID"] as? String ?? ""
return Contact(name: name)
}
}
}
func addData(name: String) {
do {
_ = try db.collection("contacts").addDocument(data: ["name": name, "creatorsID": Auth.auth().currentUser?.uid])
}
catch {
print(error.localizedDescription)
}
}
The problem here is that I still receive all the items from all users instead of specific user's list(FYI, CreatorsId is as same as UID). I can't figure out if the issue is in rules or in iOS code.
You're getting all users because you read the data with:
db.collection("contacts").addSnapshotListener...
So this gives you all documents from the contacts collection. If you want to only read a single document, you'll have to identify that document either by its ID or by using a query.
Since you use addDocument to add the document, Firestore generates a unique ID for you. That means that with your current data structure, the way to get the user document is by querying on the createdID field:
db.collection("contacts")
.whereField("createdID", isEqualTo: Auth.auth().currentUser?.uid)
.addSnapshotListener { (querySnapshot, error) in
...
A more idiomatic way to store user profile documents is to use the UID of each user as the ID of the document too. That both guarantees that each user can only have one document (as document IDs are by definition unique within a collection), and it means you can look up the profile document for a user without a query:
db.collection("contacts")
.document(Auth.auth().currentUser?.uid)
.addSnapshotListener { (documentSnapshot, error) in
....
Doing this will also solve your security errors, as this code matches exactly with what you're checking here:
match /contacts/{createdID} {
if request.auth != null && request.auth.uid == createdID
In your current approach the value of createdID is the random ID that Firestore generates when you call addDocument, and not the user's UID.

Swift: Firestore sub collections, custom objects and listeners

I have a model pattern as depicted in the below picture.
In the UI I am trying to get all the Countries and related data. I have created respective structures and my idea was to use the custom object approach shown in the link Custom_Objects. Now the problem I have is that subcollections won't come in the querysnapshot ( I am getting only the fields) and hence I cannot do the direct object mapping (as I have to query and retrieve the subcollections to make the object complete). for example: Country structure has States as one of the properties and without states Country object cannot be created and States further has provinces. Right now I am doing recursive looping to construct the whole structure which I am not feeling quite good.
So my question would be, what is the best way to handle this kind of data (provided there is no room for normalization and we can't avoid subcollections)?
Also if I want to get notified on any changes to State, Province or Town, do I need to separately add listeners to each collection or adding to the root is enough?
Here is the current code snap
db.collection("countries").getDocuments { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
var docCount = QuerySnapshot!.documents.count
for document in QuerySnapshot!.documents {
self.fetchStatesForDoc(document: document, completion: { (nodes) in
var data = document.data()
data["states"] = nodes
let country = Country(dictionary: data)
self.countryList.append(country!)
print(self.sectionList)
docCount = docCount - 1
if docCount == 0{
DispatchQueue.main.async {
self.countryCollection.reloadData()
}
}
})
}
}
}
}
func fetchStatesForDoc(document: DocumentSnapshot, completion:#escaping ([State])-> Void){
var states = [State]()
document.reference.collection("states").getDocuments(completion: { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
var docCount = QuerySnapshot!.documents.count
for document in QuerySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
var data = document.data()
self.fetchProvincesForDoc(document: document, completion: { (provinces) in
data["Provinces"] = provinces
let state = State(dictionary: data)
states.append(state!)
docCount = docCount - 1
if docCount == 0{
completion(state)
}
})
}
}
})
}
func fetchProvincesForDoc(document: DocumentSnapshot, completion:#escaping ([Province])-> Void){
var provinces = [Province]()
document.reference.collection("provinces").getDocuments(completion: { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
var docCount = QuerySnapshot!.documents.count
for document in QuerySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
var data = document.data()
self.fetchTownsForDoc(document: document, completion: { (towns) in
data["towns"] = provinces
let province = Province(dictionary: data)
provinces.append(province!)
docCount = docCount - 1
if docCount == 0{
completion(province)
}
})
}
}
})
}
func fetchTownssForDoc(document: DocumentSnapshot, completion:#escaping ([Towns])-> Void) {
var towns = [Towns]()
document.reference.collection("towns").getDocuments(completion: { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
for document in QuerySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
}
towns = QuerySnapshot!.documents.compactMap({Towns(dictionary: $0.data())})
completion(towns)
}
})
}
Now the problem I have is that subcollections wont come in the querysnapshot ( I am getting only the fields)
That's right, this is how Cloud Firestore queries works. The queries are named shallow, which means they only get items from the collection that the query is run against. There is no way to get documents from a top-level collection and a subcollections in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use properties of documents in a single collection. That's why ypu cannot see the subcollection in the querysnapshot object so you can do the direct object mapping.
what is the best way to handle these kind of data (provided there is no room for normalization and we cant avoid subcollections)?
In this case you should query the database twice, once to get the objects within the collection and second to get all the objects within the subcollection.
There is also another practice which is called denormalization and is a common practice when it comes to Firebase. This technique implies also querying the database twice. If you are new to NoQSL databases, I recommend you see this video, Denormalization is normal with the Firebase Database for a better understanding. It is for Firebase realtime database but same rules apply to Cloud Firestore.
Also, when you are duplicating data, there is one thing that need to keep in mind. In the same way you are adding data, you need to maintain it. With other words, if you want to update/detele an item, you need to do it in every place that it exists.
So in this case, you can denormalize your data by creating a top-level collection in which you should add all the objects that exist in your subcollections. It's up to you to decide which practice is better for you.

why I still get deleted documents when getting data using listener from Firestore in Swift?

I trying to make an event app. and I add a new field in my events documents. I try to add "venue" field for my event documents
so before I run the app, I delete all the available data on my Firestore database. But when I retrieve my data back to the app, it is said that the "venue" is nil, it seems that the "venue" field is not exist, even though in fact, the "venue" field exist on my firestore database.
I suspect my app still retrieve my deleted documents. here is why
here is the code I use :
enum FirestoreCollectionReference {
case users
case events
case cities
case APIKey
private var path : String {
switch self {
case .users : return "users"
case .events : return "events"
case .cities : return "cities"
case .APIKey : return "secretAPIKeyKM"
}
}
func reference () -> CollectionReference {
return Firestore.firestore().collection(path)
}
}
FirestoreCollectionReference.events.reference()
.whereField("city", isEqualTo: selectedCity)
.whereField("eventType", isEqualTo: selectedEventType)
.whereField("coordinate", isGreaterThan: lesserGeopoint)
.whereField("coordinate", isLessThan: greaterGeopoint)
.order(by: "coordinate")
.order(by: "dateTimeStart", descending: true)
.limit(to: 20)
.addSnapshotListener { (snapshot, error) in
if let error = error {
completion(nil,eventListener)
print("Error when observing events document: \(error.localizedDescription)")
} else {
print("Successfully get events data from Firestore by Listener")
guard let documentsSnapshot = snapshot else {
completion(nil, eventListener)
return
}
let eventDocuments = documentsSnapshot.documents
print("the number of documents: \(eventDocuments.count)")
var eventsArray = [EventKM]()
for document in eventDocuments {
let eventDictionary = document.data()
let theEvent = EventKM(dictionary: eventDictionary)
eventsArray.append(theEvent)
}
completion(eventsArray,eventListener)
}
}
}
I try to print the number of documents, and it shows that I have 8 documents from this query, in fact, it should be only one document available in my database.
I try to delete the composite indexes from firebase console, but usually, after I delete the composite indexes, It will give an error + a link to generate the composite indexes in my debugging area on my Xcode, but after I delete the composite indexes, I don't get the error + link to generate the indexes, and give 8 documents (it should be one document only)
it seems the data is cached on my iOS app. isn't it? or is this a bug since Firestore is still in Beta version? I need to understand why and how to solve this issue so I can understand firebase better. Thanks in advance.
FireBase has cached in device so if user stay in outside of internet,
But still can use Firebase.
So You just remove your app in your simulator.
In my case, It fixed.

How to access a specific field from Cloud FireStore Firebase in Swift

Here is my data structure:
I have an ios app that is attempting to access data from Cloud Firestore. I have been successful in retrieving full documents and querying for documents. However I need to access specific fields from specific documents. How would I make a call that retrieves me just the value of one field from Firestore in swift? Any Help would be appreciated.
There is no API that fetches just a single field from a document with any of the web or mobile client SDKs. Entire documents are always fetched when you use getDocument(). This implies that there is also no way to use security rules to protect a single field in a document differently than the others.
If you are trying to minimize the amount of data that comes across the wire, you can put that lone field in its own document in a subcollection of the main doc, and you can request that one document individually.
See also this thread of discussion.
It is possible with server SDKs using methods like select(), but you would obviously need to be writing code on a backend and calling that from your client app.
This is actually quite simple and very much achievable using the built in firebase api.
let docRef = db.collection("users").document(name)
docRef.getDocument(source: .cache) { (document, error) in
if let document = document {
let property = document.get(field)
} else {
print("Document does not exist in cache")
}
}
There is actually a way, use this sample code provided by Firebase itself
let docRef = db.collection("cities").document("SF")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let property = document.get('fieldname')
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
I guess I'm late but after some extensive research, there is a way in which we can fetch specific fields from firestore. We can use the select keyword, your query would be somthing like (I'm using a collection for a generalized approach):
const result = await firebase.database().collection('Users').select('name').get();
Where we can access the result.docs to further retrieved the returned filtered result. Thanks!
//this is code for javascript
var docRef = db.collection("users").doc("ID");
docRef.get().then(function(doc) {
if (doc.exists) {
//gives full object of user
console.log("Document data:", doc.data());
//gives specific field
var name=doc.get('name');
console.log(name);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});

Resources