Fetch several ParseObjects with objectId - swift

I am updating an iOS app using ParseSwift. In general, every ParseUser of the app has several associated Userdata ParseObjects that need to be queried or fetched.
The current implementation uses individual queries to find every individual Userdata object one after the other. This works but is obviously not optimal.
What I already reworked is that every ParseUser now saves an Array of objectId's of the user-associated Userdata ParseObjects.
What I am trying to achieve now is to query for all Userdata objects that correspond to these saved objectId's in the ParseUser's userdataIds field.
This is my code:
func asyncAllUserdataFromServer() {
let userdataIdArray = User.current!.userdataIds
var objectsToBeFetched = [Userdata]()
userdataIdArray?.forEach({ objectId in
let stringObjectId = objectId as String
// create a dummy Userdata ParseObject with stringObjectId
let object = Userdata(objectId: stringObjectId)
// add that object to array of Userdata objects that need to be fetched
objectsToBeFetched.append(object)
})
// fetch all objects in one server request
objectsToBeFetched.fetchAll { result in
switch result {
case .success(let fetchedData):
// --- at this point, fetchedData is a ParseError ---
// --- here I want to loop over all fetched Userdata objects ---
case .failure(let error):
...
}
}
}
I read in the ParseSwift documentation that several objects can be fetched at the same time based on their objectIds, so my idea was to create an Array of dummy Userdata objects with the objectIds to fetch. This, however, does not work.
As indicated in the code block, I was able to narrow the error down, and while the query is executed, fetchedData comes back as a ParseError saying:
Error fetching individual object: ParseError code=101 error=objectId "y55wmfYTtT" was not found in className "Userdata"
This Userdata object with that objectId is the first (and only) objectId saved in the User.current.userdataIds array. This object does exist on my server, I know that for sure.
Any ideas why it cannot be fetched?
General infos:
Xcode 13.2.1
Swift 5
ParseSwift 2.5.0
Back4App backend
Edit:
User implementation:
struct User: ParseUser {
//: These are required by `ParseObject`.
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
//: These are required by `ParseUser`.
var username: String?
var email: String?
var emailVerified: Bool?
var password: String?
var authData: [String: [String: String]?]?
//: Custom keys.
var userdataIds: [String]? // --> array of Userdata objectId's
}
Userdata implementation:
struct Userdata: ParseObject, ParseObjectMutable {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
var firstname: String?
var lastSeen: Int64?
var isLoggedIn: Bool?
var profilepicture: ParseFile?
}

You should check your Class Level Permissions (CLP's) and here in Parse Dashboard, if you are using the default configuration, you can’t query the _User class unless you make the CLP’s public or authorized. In addition, you can probably shrink your code by using a query and finding the objects instead of building your user objects and fetching the way you are doing:
let userdataIdArray = User.current!.userdataIds
let query = Userdata.query(containedIn(key: "objectId", array: userdataIdArray))
//Completion block
query.find { result in
switch result {
case .success(let usersFound):
...
case .failure(let error):
...
}
}
// Async/await
do {
let usersFound = try await query.find()
...
} catch {
...
}
You should also look into your server settings to make sure this option is doing what you want it to do: https://github.com/parse-community/parse-server/blob/4c29d4d23b67e4abaf25803fe71cae47ce1b5957/src/Options/docs.js#L31

Are you sure that the Object Ids for UserData are the same as the User ids? while you might have user id in the class, it is probably not the objectId, but some other stored field.
doing
let query = PFQuery(className:"UserData")
query.whereKey("userId", containedIn:userdataIdArray)
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
// do stuff
}
May help.

Related

How to store field of a Firebase document in swift variable

I'm trying to access an array of references thats a field in a "post" document I have in Firebase and using the values in that array fetch posts that they reference.
However, if I call getDocument() on the document I'm interested in and try to return the array I get the following error. So how can I access the array and use its contents to perform other actions? Thanks!
Cannot convert value of type '[String]' to closure result type 'Void'
let array = usersReference.document(user.id).getDocument() { document, error in
return document["eventsAttending"] as? [String] ?? [""]
}
It is hard to say why this is failing without seeing your data model. When asking questions that relate to Firestore, I would recommend to also post a screenshot of one of your documents from the Firebase console.
That being said, I would recommend using Codable instead of mapping manually, as this will make your life easier.
In your case, this would look something like this:
Model
struct Event: Codable {
#DocumentID var id: String?
var eventsAttending: String?
}
Fetching data
func fetchEvents(documentId: String) {
let docRef = db.collection("events").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.book = try document.data(as: Event.self)
}
catch {
print(error)
}
}
}
}
}
Check out this blog post to learn more.

Swift + Firebase: Storing, retrieving, and using an array of document references

I'm relatively new to Swift and Firebase, so I'm not very familiar with the intricacies of how both work together. I'm building a chat app that has messages and threads. A user can send a message, struct Message, and if another user wants to directly reply to that message, a thread is created. For each message, I'm storing an array of Firebase document references to the other messages in the thread, threadBefore: [Message].
struct Message: Codable, Identifiable {
var id: String
var content: String
var name: String
var upvotes: Int
var likedByUser: Bool
var dontShow: Bool
var sentAt: Date
var threadsArray: [Message]
}
The following is my code for fetching all the chat messages from Firebase:
dontShow property: if dontShow == true means that the message is inside the thread and shouldn't be displayed like a regular message in the chat. However, the very last message in the thread is displayed and has dontShow = false.
func fetchMessages(docId: String, collectionType: String, isThreadMember: Bool) {
if (user != nil) {
db.collection("chatrooms").document(docId).collection(collectionType).order(by: "sentAt", descending: false).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No messages")
return
}
// let threadsTemp: [Message]()
let classroomId = docId
if !isThreadMember {
if collectionType == "messages" {
self.messages = documents.map { doc -> Message in
let data = doc.data()
let docId1 = doc.documentID
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let likedUsersArr = data["likedUsers"] as? Array ?? [""]
// if message is in thread (but not last message), then don't show as normal message, but in thread
let dontShow = data["dontShow"] as? Bool ?? false
let sentAt = data["sentAt"] as? Date ?? Date()
let threadBefore = data["threadBefore"] as? [DocumentReference] ?? [DocumentReference]()
// using reference array
if dontShow == false {
if (threadBefore.count > 0) {
// reset the temporary array that holds the threads to be added afterwards
self.threadsTemp = []
for docRef in threadBefore {
docRef.getDocument { (doc2, error) in
if let doc2 = doc2, doc2.exists {
let docId2 = doc2.documentID
self.fetchThreadMessages(classroomId: classroomId, parentMessageId: docId1, docId: docId2)
} else {
print("Document does not exist")
}
}
}
}
}
return Message(id: docId1,
content: content,
name: displayName,
upvotes: likedUsersArr.count,
likedByUser: likedUsersArr.contains(self.user!.uid) ? true : false,
dontShow: dontShow,
sentAt: sentAt,
threadsArray: self.threadsTemp)
}
}
Another function: fetchThreadMessages:
// fetch a specified message and then append to the temporary threads array, threadsTemp
func fetchThreadMessages(classroomId: String, parentMessageId: String, docId: String) -> Message {
if (user != nil) {
let docRef = db.collection("chatrooms").document(classroomId).collection("messages").document(docId)
docRef.getDocument { (doc, error) in
if let doc = doc, doc.exists {
if let data = doc.data(){
let docId = doc.documentID
print("docid")
print(docId)
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let likedUsersArr = data["likedUsers"] as? Array ?? [""]
// if message is in thread (but not last message), then don't show as normal message, but in thread
let dontShow = data["dontShow"] as? Bool ?? false
let sentAt = data["sentAt"] as? Date ?? Date()
self.threadsTemp.append(Message(id: docId,
content: content,
name: displayName,
upvotes: likedUsersArr.count,
likedByUser: likedUsersArr.contains(self.user!.uid) ? true : false,
dontShow: true,
sentAt: sentAt,
threadsArray: []))
}
}
}
}
}
I haven't implemented how the sendMessage() function updates the threadBefore array, but I'm currently updating this field directly on Firebase just for testing.
func sendMessage(messageContent: String, docId: String, collectionType: String, isReply: Bool, threadId: String) {
if (user != nil) {
if isReply {
let docRef = db.collection("chatrooms").document(docId).collection(collectionType).document(threadId)
self.threadRef = db.document(docRef.path)
}
db.collection("chatrooms").document(docId).collection(collectionType).addDocument(data: [
"sentAt": Date(),
"displayName": user!.email ?? "",
"content": messageContent,
"likedUsers": [String](),
"sender": user!.uid,
"threadBefore": isReply ? [threadRef] : [DocumentReference](),
"dontShow": false])
}
}
A little bit more on how I'm fetching and retrieving the document references from threadsBefore: For each message in the collection, I loop its threadsArray, which consists of DocumentReferences to other messesages that are in that thread. For each of those document references, I run self.fetchThreadMessages. This retrieves that message and stores a Message() instance in threadsTemp. Then, back in self.fetchMessage, when I'm done filling up the self.threadsTemp with all of the documents retrieved from threadsBefore, I store it in threadsArray property in the Message struct.
Now, look at the return state in self.fetchMessages above, the very last assignment inside Message() is threadsArray: self.threadsTemp. But the problem here is that this is just a reference? And it would change based on the last assignment to self.threadsTemp?
I've tried multiple ways to implement this entire storing and retrieving thing. But all came with several complicated errors. I tried using dictionaries, or storing just the document id's for the thread messages and then look them up in self.Messages (since it has all of the messages stored in it).
What's the best way to implement this? Or fix my errors?
I know my code is probably a mishmash of inefficient and confused coding practices. But I'm trying to learn.
With that being said (meaning, my comment above), I'm going to not as much address your code as I am going to propose what I think is a good way to organize your Firestore database for a chat app. Then, hopefully you will be able to apply it to your own situation.
First things first, we have to remember that Firestore charges based on the number of queries you make - it does not take into account the amount of data that is being fetched in a particular query - so fetching a single word is as expensive as fetching an entire novel. As such, we should structure our database in a way that is financially efficient.
What I would do personally is structure my database with two primary sections, (1) conversation threads and (2) users. Each user contains references to their threads they are a part of, and each thread contains a reference to the users that are in that thread. That way we can easily and efficiently fetch a user's conversations, or obtain the users in a particular conversation.
// COL = Collection
// DOC = Document
Threads (COL)
Thread1 (DOC)
id: String // id of the thread
participants: [String: String] = [
user1: [
"username": "Bob23",
"userID": IJ238KD... // the document id of the user
]
] // Keep this "participants" dictionary in the root of the thread
// document so we have easy access to fetch any user we want, if
// we so desire (like if the user clicks to view the profile of a
// user in the chat.
Messages (COL)
Message1 (DOC)
from: String // id of user that sent message
Message2 (DOC)
...
...
Thread 2 (DOC)
...
...
Users (COL)
User1 (DOC)
threadIDs: [String] // Contains the thread "id"s of all the threads
// the user is a part of. (You probably want to
// use a dictionary instead since array's can be
// difficult to work with in a database, but for
// simplicity, I'm going to use an array here.)
User2 (DOC)
...
...
Now, let's say the user opens the app and we need to fetch their conversation threads. It's as easy as
db.collection("Threads").whereField("id", in: user.threadIDs)
[EDIT] user.threads would be a local array of type String. You should have this data on hand since you would be fetching the current user's User document on app launch, which is why we include this data in that user document.
Basically, this query returns every document in the Threads collection whose "id" field exists in the array of the user's threadIDs (which is an array of type String). You can read more about Firestore queries in their docs here.
In other words, we get every thread document that the user has references to.
The great thing about this is that it takes only one query to return all of the conversations of a user.
Now let's say the user is scrolling through their conversation threads in the app, and they tap one of them to open up the messages. Now, all we do is fetch all the messages in that particular thread, again only requiring one query to do so.
Last but not least, if for some reason we have to get info about a particular user in a conversation, we have all the references we need within that thread document itself to fetch the user data, if needed.

Making a query on a mongo DB with ParseSwift

I have two collections in a Mongo DB.
Here is how a document looks in the first collection (MainCollection):
_id
:"mzWqPEDYRU"
TITLE
:"ZAZ: I want."
ownerID
:"lGutCBY52g"
accessKey
:"0kAd4TOmoK0"
_created_at
:2020-03-13T11:42:11.169+00:00
_updated_at
:2020-03-13T17:08:15.090+00:00
downloadCount
:2
And here is how it looks in the second collection (SecondCollection):
_id
:"07BOGA8bHG"
_p_unit
:"MainCollection$mzWqPEDYRU"
SENTENCE
:"I love nature peace and freedom."
Order
:5
ownerID
:"lGutCBY52g"
AUDIO
:"07067b5589d1edd1d907e96c1daf6da1_VOICE.bin"
_created_at
:2020-03-13T11:42:17.483+00:00
_updated_at
:2020-03-13T11:42:19.336+00:00
There is a parent children relationship between the first and the second collection. In the last document we can see the _p_unit field where the "mzWqPEDYRU" part points to the id of the parent in the first collection.
Though I finally get what I want, getting elements of the second collection with a given parent, it is not done how it should.
I have one problem making a selective query on SecondCollection. Here is the currently working code:
func theFunction(element: MainCollection) {
do {SecondCollection.query().find() {
result in
switch result {
case .success(let items):
print("items.count = \(items.count)")
var finalCount = 0
for item in items {
// Ignore useless elements:
if item.unit?.objectId != element.objectId! {continue}
finalCount += 1
/// .... Work with selected element.
}
print("finalCount = \(finalCount)")
case .failure(let error):
print("Error in \(#function): \(error)")
}
}
}
}
The way the above code is written works in the sense that I get the elements in SecondCollection I am interested in. But this trick inside the for loop to eliminate the non-needed element is not the way to go.
if item.unit?.objectId != element.objectId! {continue}
The filtering should happen in the query, with the line:
SecondCollection.query().find()
The problem is that everything I have tried failed. I did things like:
SecondCollection.query("unit" == element.objectId!).find()
with a zillion variations, but all with no luck.
Does anybody know the proper syntax?
In case this may be useful, here is how SecondCollection is declared:
struct SecondCollection: ParseObject,Identifiable,Equatable,Hashable {
// These fields are required for any Object.
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
// Local properties.
var id:UUID {return UUID()}
var SENTENCE: String?,
Order: Int?,
ownerID: String?,
AUDIO: ParseFile?,
unit: Pointer<MainCollection>?
.......
}
First, I do not think that bringing all elements and then "eliminating" the ones you don't need is a good approach. That way you are retrieving a lot more data than needed and it is very inneficient.
I tried out your code but you did not make clear (at least for me) if you are using Pointers or Relations between those classes. The approach would be different for each one.
Which one are you using?
UPDATE: Hey there! I think I could make it work kinda the way you need it.
I created two classes ParentClass (name: string, age: number, children: relation to ChildClass):
struct ParentClass: ParseObject {
//: These are required for any Object.
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
//: Your own properties.
var name: String?
var age: Int = 0
var children: ParseRelation<Self> {
ParseRelation(parent: self, key: "children", className: "ChildClass")
}
}
And ChildClass (name: string, age:number):
struct ChildClass: ParseObject {
//: These are required for any Object.
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
//: Your own properties.
var name: String?
var age: Int = 0
}
Then I did a simple query to find the first Parent. You could use the find() method and bring all the ones that you need, but I did it this way to keep it simpler to explain:
let parentQuery = ParentClass.query()
parentQuery.first { result in
switch result {
case .success(let found):
print ("FOUND A PARENT!")
print(found.name!)
print(found.age)
print(found.children)
case .failure(let error):
print(error)
}
}
Now that I got the Parent (that has two children in my case), I used the query() method to generate the query on the ChildClass containing all the ChildClass' objects with the found parent:
do {
let childrenObject = ChildClass()
try found.children.query(childrenObject).find()
{ result in
switch result {
case .success(let allChildrenFromParent):
print("The following Children are part of the \(found.name!):")
for child in allChildrenFromParent {
print("\(child.name!) is a child of \(found.name!)")
}
case .failure(let error):
print("Error finding Children: \(error)")
}
}
} catch {
print("Error: \(error)")
}
And the whole code ended up like this:
let parentQuery = ParentClass.query()
parentQuery.first { result in
switch result {
case .success(let found):
print ("FOUND A PARENT!")
print(found.name!)
print(found.age)
print(found.children)
do {
let childrenObject = ChildClass()
try found.children.query(childrenObject).find()
{ result in
switch result {
case .success(let allChildrenFromParent):
print("The following Children are part of the \(found.name!):")
for child in allChildrenFromParent {
print("\(child.name!) is a child of \(found.name!)")
}
case .failure(let error):
print("Error finding Children: \(error)")
}
}
} catch {
print("Error: \(error)")
}
case .failure(let error):
print(error)
}
}

Can the #ServerTimestamp be set once in Firestore, and skipped on update

I would like to have both createdAt and lastUpdatedAt fields, both of which are supposed to be set by the server.
To be consistent, I thought I'd create a wrapper that would always set these supporting properties to all documents:
struct FIRDocument<T: Codable>: Codable {
let data: T
#ServerTimestamp var createdAt: Timestamp? = nil
#ServerTimestamp var lastUpdatedAt: Timestamp? = nil
}
and to use it like so:
struct Message: Codable {
content: String
}
let ref = firestore.collection("messages").document()
// later
let newDoc = FIRDocument(data: Message(content: "hello"))
ref.addDocument(from: newDoc) { ... }
// later yet
let updatedDoc = FIRDocument(data: Message(content: "goodbye"))
ref.setData(from: updatedDoc) { ... }
Obviously, the above code just updates both of the timestamps. Is there a way to update the lastUpdatedAt without resetting the createdAt field in my set-up?
Unfortunately this is not directly possible on the server. While you can reject overwrites of createdAt, and can require the presence of lastUpdatedAt, in security rules, there is no way to ignore a value that was passed in a write instruction.
That means you have a case here where the data you want to write to a document depends on the existing data in that document, and you will need to use a transaction.

Decoding nested object with Swift and Firestore

I’m developing an app with Swift 5 and Firestore.
I created structs Identifiables and Codables and it works well but I have problem decoding DocumentID for nested documents.
I wasn’t able to find something similar online.
I would like to know if Firestore structure I implemented is right in my use case and how to solve id problem.
I have to show a list of activities with just some info about the User and the Sport (like name and image). That's why I'm trying to nest in the Activity object just the info I need to not query the entire User and the Sport for each activity of the list.
On Firestore:
The activity document contains:
info (String)
organizer:
- name (String)
- image (String)
- ref (Reference)
date (Date)
sport:
- name (String)
- image (String)
- ref (Reference)
Here is my code:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct User: Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let email: String?
let image: String
let birthday: Date?
let description: String?
}
struct Sport: Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let image: String
}
struct Activity: Codable {
#DocumentID var id: String? = UUID().uuidString
let organizer: User
let info: String
let date: Date
let sport: Sport
static func getActivities(completion: #escaping (_ activities: [Activity]?) -> Void) {
Activity.collectionReference.getDocuments { (querySnapshot, error) in
guard error == nil else {
print("Error: \(error?.localizedDescription ?? "Generic error")")
return
}
let activities = querySnapshot?.documents.compactMap({ (queryDocumentSnapshot) -> Activity? in
return try! queryDocumentSnapshot.data(as: Activity.self)
})
completion(activities)
}
}
}
Calling Activity.getActivities I'm able to get everything I need but decoding the user and sport directly from activity object they get the activity ID instead of their own. That's wrong because in this way I have the wrong reference to the object. The idea is to keep the reference to use it in the details page and fetching the complete object.
Can someone told me what's the best way to structure this kind of architecture?
Thank you in advance.