How to update partial of profile using Firestore - swift

I'm creating Edit profile page using Firestore.
When user navigate to the Edit profile, the user have their profile. Because they already registered.
User can change all items in their profile in edit page.
But of course they can change partial of their profile.
How do I detect data they did change?
First I thought I would use update method below
func updateProfile(user: User){
let user = db.collection("user").document("user-id")
user.updateData([
"name":user.name,
"profile":user.profile ?? ""
"phone":user.phone ?? "",
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
It is possible user did change only their name.
But this way can update not changed data.
I mean I will update all datas user didn't change.
How do I detect the only data user did change.

Related

Updating Firebase Data for Any User in Swift

I am trying to allow documents (reports) that are created by a user to be viewed by other users and then updated with changes. The documents show up in a tableView that when selected show the appropriate data for that specific report loaded up in a ViewController. However when I go to select the update button within that ViewController of the individual report, the error comes back that no document can be found.
Below is the current variation of code I am working with for the update function.
private let database = Firestore.firestore()
private init() {}
// UPDATE REPORT
public func updateTheData(
reportPost: ReportPost,
thisReport: String,
completion: #escaping (Bool) -> Void
){
let data = [
"id": reportPost.identifier,
"title": reportPost.title,
"timestamp1": reportPost.timestamp1,
"address": reportPost.address,
"customerPO": reportPost.customerPO,
"authNum": reportPost.authNum,
"contactName": reportPost.contactName,
"contactPhone": reportPost.contactPhone,
"modelNum": reportPost.modelNum,
"serialNum": reportPost.serialNum,
"addInfo": reportPost.addInfo,
"equipProblem": reportPost.equipProblem,
"action": reportPost.action,
"followUp": reportPost.followUp,
"techName1": reportPost.techName1,
"techName2": reportPost.techName2,
"techName3": reportPost.techName3,
"techName4": reportPost.techName4,
"timestamp2": reportPost.timestamp2,
"timestamp3": reportPost.timestamp3,
"timestamp4": reportPost.timestamp4,
"milesTraveled1": reportPost.milesTraveled1,
"milesTraveled2": reportPost.milesTraveled2,
"milesTraveled3": reportPost.milesTraveled3,
"milesTraveled4": reportPost.milesTraveled4,
"travelTime1": reportPost.travelTime1,
"travelTime2": reportPost.travelTime2,
"travelTime3": reportPost.travelTime3,
"travelTime4": reportPost.travelTime4,
"siteTime1": reportPost.siteTime1,
"siteTime2": reportPost.siteTime2,
"siteTime3": reportPost.siteTime3,
"siteTime4": reportPost.siteTime4,
"serviceType1": reportPost.serviceType1,
"serviceType2": reportPost.serviceType2,
"serviceType3": reportPost.serviceType3,
"serviceType4": reportPost.serviceType4,
"timeTotal1": reportPost.timeTotal1,
"timeTotal2": reportPost.timeTotal2,
"timeTotal3": reportPost.timeTotal3,
"timeTotal4": reportPost.timeTotal4,
"partNum1": reportPost.partNum1,
"partNum2": reportPost.partNum2,
"partNum3": reportPost.partNum3,
"partNum4": reportPost.partNum4,
"quantity1": reportPost.quantity1,
"quantity2": reportPost.quantity2,
"quantity3": reportPost.quantity3,
"quantity4": reportPost.quantity4,
"price1": reportPost.price1,
"price2": reportPost.price2,
"price3": reportPost.price3,
"price4": reportPost.price4,
"priceTotal1": reportPost.priceTotal1,
"priceTotal2": reportPost.priceTotal2,
"priceTotal3": reportPost.priceTotal3,
"priceTotal4": reportPost.priceTotal4,
]
let docRef = database.collection("reports").document(thisReport)
docRef.updateData(data) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
}
For Reference, this is how a report is saved and uploaded to the database in Firebase.
// FSR REPORT POSTING
public func insert(
reportPost: ReportPost,
email: String,
completion: #escaping (Bool) -> Void
) {
let userEmail = email
.replacingOccurrences(of: ".", with: "_")
.replacingOccurrences(of: "#", with: "_")
let data = [
"id": reportPost.identifier,
"title": reportPost.title,
"timestamp1": reportPost.timestamp1,
"address": reportPost.address,
"customerPO": reportPost.customerPO,
"authNum": reportPost.authNum,
"contactName": reportPost.contactName,
"contactPhone": reportPost.contactPhone,
"modelNum": reportPost.modelNum,
"serialNum": reportPost.serialNum,
"addInfo": reportPost.addInfo,
"equipProblem": reportPost.equipProblem,
"action": reportPost.action,
"followUp": reportPost.followUp,
"techName1": reportPost.techName1,
"techName2": reportPost.techName2,
"techName3": reportPost.techName3,
"techName4": reportPost.techName4,
"timestamp2": reportPost.timestamp2,
"timestamp3": reportPost.timestamp3,
"timestamp4": reportPost.timestamp4,
"milesTraveled1": reportPost.milesTraveled1,
"milesTraveled2": reportPost.milesTraveled2,
"milesTraveled3": reportPost.milesTraveled3,
"milesTraveled4": reportPost.milesTraveled4,
"travelTime1": reportPost.travelTime1,
"travelTime2": reportPost.travelTime2,
"travelTime3": reportPost.travelTime3,
"travelTime4": reportPost.travelTime4,
"siteTime1": reportPost.siteTime1,
"siteTime2": reportPost.siteTime2,
"siteTime3": reportPost.siteTime3,
"siteTime4": reportPost.siteTime4,
"serviceType1": reportPost.serviceType1,
"serviceType2": reportPost.serviceType2,
"serviceType3": reportPost.serviceType3,
"serviceType4": reportPost.serviceType4,
"timeTotal1": reportPost.timeTotal1,
"timeTotal2": reportPost.timeTotal2,
"timeTotal3": reportPost.timeTotal3,
"timeTotal4": reportPost.timeTotal4,
"partNum1": reportPost.partNum1,
"partNum2": reportPost.partNum2,
"partNum3": reportPost.partNum3,
"partNum4": reportPost.partNum4,
"quantity1": reportPost.quantity1,
"quantity2": reportPost.quantity2,
"quantity3": reportPost.quantity3,
"quantity4": reportPost.quantity4,
"price1": reportPost.price1,
"price2": reportPost.price2,
"price3": reportPost.price3,
"price4": reportPost.price4,
"priceTotal1": reportPost.priceTotal1,
"priceTotal2": reportPost.priceTotal2,
"priceTotal3": reportPost.priceTotal3,
"priceTotal4": reportPost.priceTotal4,
]
database
.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.setData(data) { error in
completion(error == nil)
}
}
When the report is first inserted, it's stored at this path
database.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.setData(data)
which is
users/the_email/reports/some id
but then when you're attempting to update that report it's at this path
database.collection("reports")
.document(thisReport)
.updateData(data)
which is
database/reports/some id
and that's two different locations.
To update data, it needs to point to the same document reference. so the data needs to be updated at the same location it was initially written to
database.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.updateData(data) //<- same path as it was initially
As a side note, the documentId is both being stored as the documentId as well as within the document in the "id" field and there's no reason to do that. A documentId is static and if it's known, you can get to that data directly without a query.
I would also suggest not using users emails as documentIds'; user emails can change and if that happens you won't be able to find the report. Additionally, if you want to update that, you'll have to find every occurance of that old email in the entire database, read it and it's child data in, delete it and re-write it. What a pain.
Also, substituting _ for # and . in the email address may work to a point but if want to get back that email address, there could be multiple _ in the address; how would you know which _ was the # and which one was actually part of the email address.
some_person#thing.com -> some_person_thing_com = some#person_thing.com?
EDIT
Per the question, suppose there are a series of reports that are 1) shared amongst users 2) the creating user can edit their own reports
Here's one option for storing the reports
reports (collection)
document_0 //a document with an auto-generated documentId
created_by_uid: "uid_0"
report_info: "some info about this report, fields etc"
document_1
created_by_uid: "uid_1"
report_info: "some info about this report"
The reports collection can be read by any user, which meets criteria 1. It's easy to get the reports created by user with uid_0 as a simple query can be run against the reports collection where created_by_uid = uid_0.
Here's the code with an object to hold the report data: documentId the report_info (the report data) and a field to track who created the report. Suppose user with uid_0 is logged in and wants to edit a report. We'll load all of the reports uid_0 created and store them in an array:
class ReportClass {
var id = ""
var report_info = ""
var created_by_uid = ""
}
var reportArray = [ReportClass]()
func readReports(forUid: String) { //pass in uid_0
let reportsCollection = self.db.collection("reports")
reportsCollection.whereField("created_by_uid", isEqualTo: forUid)
reportsCollection.getDocuments(completion: {documentSnapshot, error in
for doc in documentSnapshot!.documents {
//note we don't need the created_by_uid because it's not going to change
let report = ReportClass()
//keep track of the document Id
report.id = doc.documentID as! String
//the report info which the user is changing
report.report_info = doc.get("report_info") as! String
self.reportArray.append(report)
}
})
}
now the reports are presented to the user so they can select one and edit it. Upon saving we update the ReportClass object report_info field with the new data the user input and pass the object to a function to update that report
func updateReport(theReport: ReportClass) {
let reportsCollection = self.db.collection("reports")
let thisReport = reportsCollection.document(theReport.id)
//the report document path is reports/documentId
thisReport.updateData(["report_info": theReport.info]) //only update the report_info field
}
The key here is we load in and store the users reports, keeping the report documentId in the object. That keeps track of the path to the report
reports/report id/id, report_info and created_by_uid fields
then when the report is ready to be written, we derive the path from the documentId stored in the object so the path is the same.
I did a lot of this long-hand, but you should be using Codable objects as it makes populating them a snap
See Mapping Firestore Data in Swift

Delete a specific document, not the all Firestrore collection

There is a table view that displays a collection of user flowers.
When user goes to detail VC, he can see information about the selected flower, also there is a "Delete" button, the problem is that I only found how to delete all flowers (all collection MyFlowers),
db.collection("users").document(Auth.auth().currentUser!.uid).collection("MyFlowers").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
document.reference.delete()
}
}
}
but I want delete only selected flower.
To delete a specific document, you'll need to know the ID of that document or something else that uniquely identifies the specific document.
So you'll need to take the document ID that you pass to the VC and use that in the call to delete the specific document:
db.collection("users").document(Auth.auth().currentUser!.uid)
.collection("MyFlowers").document("idOfTheDocumentToDelete")
.delete()

How to display specific data, and group it together in firestore

I'm extremely new to firebase and need to display all the data in my collection. Within my app there is an integrated quiz function, and when the 'submit score' button is pressed, the data is sent to Firestore as a new document based on the uid.
user collection in firebase, new document based on uid .
This is what I have so far:
func getData() {
let db = Firestore.firestore()
// Get data
db.collection("users").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
for document in querySnapshot!.documents {
self.studentlbl.text = ("\(document.documentID) => \(document.data())");
}
}
}
This displays the following: result
I'm trying to figure out how to display the first name, followed by the user's corresponding score.
Thanks
You can display the specific field by adding field name in document.data() or doc.data() or the example below:
document.data().firstname;
or in your case(swift) if I'm correct:
self.studentlbl.text = ("(document.data().firstname");
Regarding to the score of the users, I'll recommend creating a new collection to store the data of quiz scores for every users. You can use this answer for user and post as the reference and example that can help you how you can build the database structure of your application. The answer also include how you will query or group it together.

Firestore Security Rule to grant user read/write permissions on a document and its subcollection not working

I have a Firestore collection named UserProfiles which stores the profile information for each user in individual documents. Each document is named with the user id of the user it belongs to. Each document also contains a number of sub collections as well. I'd like to make rules giving each user the ability to read and write his own document and sub collections under his document ONLY. However when a user tries to read his profile document I keep getting the following error message:
Error getting documents: Error Domain=FIRFirestoreErrorDomain Code=7 "Missing or insufficient permissions." UserInfo={NSLocalizedDescription=Missing or insufficient permissions.}
Here is the rule I created:
service cloud.firestore {
match /databases/{database}/documents {
//grant users read and write access to anything under own profiles collection
match /UserProfiles/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
//grant users read and write access to their own profile document
match /UserProfiles/{userId} {
allow read, write: if request.auth.uid == userId;
}
}
}
Grateful if anyone could point out where I've gone wrong - Thanks!
EDIT: Sorry - here is the code on the client side in Swift:
let firebaseAuth = Auth.auth()
if firebaseAuth.currentUser != nil {
userUId = Auth.auth().currentUser!.uid
print("User id is \(String(describing: userUId))")
} else {
print("User is not currently logged in")
return
}
//Getting my user info
db.collection("UserProfiles").whereField("user_id", isEqualTo: self.userUId).getDocuments() { (snapshot, error) in
if let error = error {
print("Error getting documents in func viewWIllAppear with user id \(self.userUId): \(error)")
} else {
print("Successful db connection")
for document in snapshot!.documents {
self.myUserHandle = document["user_handle"]! as! String
print("Retrieved user handle is \(self.myUserHandle)")
}
}
}
And the Database structure is:
UserProfiles
|
|--FHEneuY3nron3Ns2Ndl1SGdg9Nsw
|
|--UserHistory
|
|--UserReports
|
|--UserLogs
Basically what I want to do with my rules is give a logged in user read and write permissions on his own profile document which is names with his user id (FHEneuY3nron3Ns2Ndl1SGdg9Nsw), and also on the subcollections beneath it (UserHistory, UserReports and UserLogs), but not allow users to read or write to other user's profile documents or their subcollections.
And then here is the result output to the console:
user id is FHEneuY3nron3Ns2Ndl1SGdg9Nsw
2020-05-26 18:53:58.560680-0400 MealFleet[2495:1086294] 6.2.0 - [Firebase/Firestore][I-FST000001] Listen for query at UserProfiles failed: Missing or insufficient permissions.
Error getting documents in func viewWIllAppear with user id FHEneuY3nron3Ns2Ndl1SGdg9Nsw: Error Domain=FIRFirestoreErrorDomain Code=7 "Missing or insufficient permissions." UserInfo={NSLocalizedDescription=Missing or insufficient permissions.}
UPDATE: Here is the corrected query thanks to Doug for pointing our my error:
db.collection("UserProfiles").document(self.userUId).getDocument() { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data()
self.myUserHandle = (dataDescription!["user_handle"] as? String)!
print("Retrieved user handle is \(self.myUserHandle)")
} else {
print("Error getting documents in func viewWIllAppear with user id \(self.userUId): \(error)")
}
}
Your query does not match your rules. Your query is attempting to filter for all documents where the document field user_id is equal to some string self.userUId. Your rules are requiring that a user may only access a single specific document in UserProfiles where the document ID matches their UID. These are not the same thing at all.
If you want your code to access the user's specific document, then it should not use whereField at all. It just needs to get the individual document using it ID.
db.collection("UserProfiles").document(self.userUId).getDocument()
See the documentation for getting a document.

User's relationship collection emptied afted user was updated

I have two collections: users and tradeOffers. Each user has a unique steamid field. Each tradeOffer has field recipient.steamid. On server I publish tradeOffers collection like this:
import { Meteor } from 'meteor/meteor';
import { TradeOffers } from './collection';
if (Meteor.isServer) {
Meteor.publish("tradeOffers", function () {
let user = Meteor.users.findOne({_id: this.userId });
return TradeOffers.find({
'recipient.steamid': user.services.steam.steamid
});
});
}
On client side I subscribe to this collection and display current user's trade offers. Everything works fine until I update the user. Whenever current user is updated, data disappears from the view. After page reload I can see trade offers again. Any help would be appreciated.