Making a query on a mongo DB with ParseSwift - swift

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)
}
}

Related

Fetch several ParseObjects with objectId

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.

Removing an array item from Firestore not working when array contains date

I've spent days researching this including various answers like: Firebase Firestore: Append/Remove items from document array and my previous question at: Removing an array item from Firestore
but can't work out how to actually get this working. Turns out the issue is when there is a date property in the object as shown below:
I have two structs:
struct TestList : Codable {
var title : String
var color: String
var number: Int
var date: Date
var asDict: [String: Any] {
return ["title" : self.title,
"color" : self.color,
"number" : self.number,
"date" : self.date]
}
}
struct TestGroup: Codable {
var items: [TestList]
}
I am able to add data using FieldValue.arrayUnion:
#objc func addAdditionalArray() {
let testList = TestList(title: "Testing", color: "blue", number: Int.random(in: 1..<999), date: Date())
let docRef = FirestoreReferenceManager.simTest.document("def")
docRef.updateData([
"items" : FieldValue.arrayUnion([["title":testList.title,
"color":testList.color,
"number":testList.number,
"date": testList.date]])
])
}
The above works as reflected in the Firestore dashboard:
But if I try and remove one of the items in the array, it just doesn't work.
#objc func deleteArray() {
let docRef = FirestoreReferenceManager.simTest.document("def")
docRef.getDocument { (document, error) in
do {
let retrievedTestGroup = try document?.data(as: TestGroup.self)
let retrievedTestItem = retrievedTestGroup?.items[1]
guard let itemToRemove = retrievedTestItem else { return }
docRef.updateData([
"items" : FieldValue.arrayRemove([itemToRemove.asDict])
]) { error in
if let error = error {
print("error: \(error)")
} else {
print("successfully deleted")
}
}
} catch {
}
}
}
I have printed the itemToRemove to the log to check that it is correct and it is. But it just doesn't remove it from Firestore. There is no error returned, yet the "successfully deleted" is logged.
I've tried different variations and this code works as long as I don't have a date property in the struct/object. The moment I add a date field, it breaks and stops working. Any ideas on what I'm doing wrong here?
Please note: I've tried passing in the field values as above in FieldValue.arrayUnion as well as the object as per FieldValue.arrayRemove and the same issue persists regardless of which method I use.
The problem is, as you noted, the Date field. And it's a problem because Firestore does not preserve the native Date object when it's stored in the database--they are converted into date objects native to Firestore. And the go-between these two data types is a token system. For example, when you write a date to Firestore from a Swift client, you actually send the database a token which is then redeemed by the server when it arrives which then creates the Firestore date object in the database. Conversely, when you read a date from Firestore on a Swift client, you actually receive a token which is then redeemed by the client which you then can convert into a Swift Date object. Therefore, the definition of "now" is not the same on the client as it is on the server, there is a discrepancy.
That said, in order to remove a specific item from a Firestore array, you must recreate that exact item to give to FieldValue.arrayRemove(), which as you can now imagine is tricky with dates. Unlike Swift, you cannot remove items from Firestore arrays by index. Therefore, if you want to keep your data architecture as is (because there is a workaround I will explain below), the safest way is to get the item itself from the server and pass that into FieldValue.arrayRemove(). You can do this with a regular read and then execute the remove in the completion handler or you can perform it atomically (safer) in a transaction.
let db = Firestore.firestore()
db.runTransaction { (trans, errorPointer) -> Any? in
let doc: DocumentSnapshot
let docRef = db.document("test/def")
// get the document
do {
try doc = trans.getDocument(docRef)
} catch let error as NSError {
errorPointer?.pointee = error
return nil
}
// get the items from the document
if let items = doc.get("items") as? [[String: Any]] {
// find the element to delete
if let toDelete = items.first(where: { (element) -> Bool in
// the predicate for finding the element
if let number = element["number"] as? Int,
number == 385 {
return true
} else {
return false
}
}) {
// element found, remove it
docRef.updateData([
"items": FieldValue.arrayRemove([toDelete])
])
}
} else {
// array itself not found
print("items not found")
}
return nil // you can return things out of transactions but not needed here so return nil
} completion: { (_, error) in
if let error = error {
print(error)
} else {
print("transaction done")
}
}
The workaround I mentioned earlier is to bypass the token system altogether. And the simplest way to do that is to express time as an integer, using the Unix timestamp. This way, the date is stored as an integer in the database which is almost how you'd expect it to be stored anyway. This makes locating array elements that contain dates simpler because time on the client is now equal to time on the server. This is not the case with tokens because the actual date that is stored in the database, for example, is when the token is redeemed and not when it was created.
You can extend Date to conveniently convert dates to timestamps and extend Int to conveniently convert timestamps to dates:
typealias UnixTimestamp = Int
extension Date {
var unixTimestamp: UnixTimestamp {
return UnixTimestamp(self.timeIntervalSince1970 * 1_000) // millisecond precision
}
}
extension UnixTimestamp {
var dateObject: Date {
return Date(timeIntervalSince1970: TimeInterval(self / 1_000)) // must take a millisecond-precision unix timestamp
}
}
One last thing is that in my example, I located the element to delete by its number field (I used your data), which I assumed to be a unique identifier. I don't know the nature of these elements and how they are uniquely identified so consider the filter predicate in my code to be purely an assumption.

How to merge two queries using Firestore - Swift

I need to merge two queries with firebase firestore and then order the results using the timestamp field of the documents.
Online I didn't find much information regarding Swift and Firestore.
This is what I did so far:
db.collection("Notes").whereField("fromUid", isEqualTo: currentUserUid as Any).whereField("toUid", isEqualTo: chatUserUid as Any).getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
return
}
db.collection("Notes").whereField("fromUid", isEqualTo: self.chatUserUid as Any).whereField("toUid", isEqualTo: self.currentUserUid as Any).getDocuments { (snaphot1, error1) in
if let err = error1{
print(err.localizedDescription)
return
}
}
}
I added the second query inside the first one on completion but now I don't know how to merge them and order them through the field of timestamp.
On this insightful question It is explained that it's recommended to use a Task object but I don't find anything similar with swift.
There are many ways to accomplish this; here's one option.
To provide an answer, we have to make a couple of additions; first, we need somewhere to store the data retrieved from firebase so here's a class to contains some chat information
class ChatClass {
var from = ""
var to = ""
var msg = ""
var timestamp = 0
convenience init(withDoc: DocumentSnapshot) {
self.init()
self.from = withDoc.get("from") as! String
self.to = withDoc.get("to") as! String
self.msg = withDoc.get("msg") as! String
self.timestamp = withDoc.get("timestamp") as! Int
}
}
then we need a class level array to store it so we can use it later - perhaps as a tableView dataSource
class ViewController: NSViewController {
var sortedChatArray = [ChatClass]()
The setup is we have two users, Jay and Cindy and we want to retrieve all of the chats between them and sort by timestamp (just an Int in this case).
Here's the code that reads in all of the chats from one user to another creates ChatClass objects and adds them to an array. When complete that array is passed back to the calling completion handler for further processing.
func chatQuery(from: String, to: String, completion: #escaping( [ChatClass] ) -> Void) {
let chatsColl = self.db.collection("chats") //self.db points to my Firestore
chatsColl.whereField("from", isEqualTo: from).whereField("to", isEqualTo: to).getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
var chatArray = [ChatClass]()
for doc in docs {
let chat = ChatClass(withDoc: doc)
chatArray.append(chat)
}
completion(chatArray)
})
}
Then the tricky bit. The code calls the above code which returns an array The above code is called again, returning another array. The arrays are combined, sorted and printed to console.
func buildChatArray() {
self.chatQuery(from: "Jay", to: "Cindy", completion: { jayCindyArray in
self.chatQuery(from: "Cindy", to: "Jay", completion: { cindyJayArray in
let unsortedArray = jayCindyArray + cindyJayArray
self.sortedChatArray = unsortedArray.sorted(by: { $0.timestamp < $1.timestamp })
for chat in self.sortedChatArray {
print(chat.timestamp, chat.from, chat.to, chat.msg)
}
})
})
}
and the output
ts: 2 from: Cindy to: Jay msg: Hey Jay, Sup.
ts: 3 from: Jay to: Cindy msg: Hi Cindy. Not much
ts: 9 from: Jay to: Cindy msg: Talk to you later

Swiftui 2.0 how can I access values in an api array and manipulate them

I am new to swift and swift . I have an api call that returns an array, right now I am getting the data and displaying it in a list . What I would like to do is access the individual fields in the array and change them before it shows on the list . This is my code . In the class MainViewModel I get the results from the API and I want to access the fullname string there so I can make it all uppercase . Any help would be great
struct MainModel: Decodable,Identifiable {
var id: Int?
var fullname: String?
var time: String?
}
class MainViewModel: ObservableObject {
func MainViewRequest(completion: #escaping([MainModel]) -> ())
{
let parameter = "id=1"
let request = RequestObject(AddToken: true, Url: "Home/MainView", Parameter: parameter)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let Data = data else { return }
do {
let value = try JSONDecoder().decode(Array<MainModel>.self, from: Data)
DispatchQueue.main.async {
// how can I get FullName property here
completion(value)
}
}
catch {
print(error)
}
}.resume()
}
}
struct MainView: View {
#StateObject var mainVM = MainViewModel()
#State var model: [MainModel] = []
var body: some View {
List {
ForEach(model) { value in
Text("\(value.fullname ?? "")")
Spacer()
Text("\(value.time ?? "")")
}
}.onAppear(){
mainVM.MainViewRequest() { data in
model = data
}
}
}
}
There are a few of ways you can achieve what you are trying to do.
Use map to transform the values
Any time you want to transform a collection of values map is worth considering.
The closure you supply is called on each element of the collection and you can return a transformed value (it can even be a different type). To keep things simple I will pack things back into one of your MainModel objects.
This is what this would look like:
let values = try JSONDecoder().decode(Array<MainModel>.self, from: Data)
// Process the values off the main thread
let modifiedValues = values.map { value in
MainModel(id: value.id,
fullname: value.fullname?.uppercased(),
time: value.time
)}
DispatchQueue.main.async {
// Use the processed values in the main thread
completion(modifiedValues)
}
But this may not be the ideal way of doing things. The reason I say this is because converting something to uppercase is not a reversible operation so,
after going to all the trouble of downloading the data, you are throwing away
information.
Transform in View
If you just want to uppercase it for presentation purposes, you might be better
off doing it in your view:
ForEach(model) { value in
Text("\(value.fullname?.uppercased() ?? "")")
Spacer()
Text("\(value.time ?? "")")
}
Computed property in an extension
This option is one I use a lot in my own code. In an extension I add computed display and format related computed properties.
extension MainModel {
var displayFullName: String { fullname?.uppercased() ?? "" }
}
You then modify the loop of your view code as follows:
ForEach(model) { value in
Text("\(value.displayFullName)")
Spacer()
Text("\(value.time ?? "")")
}
I like this approach because the original cased version is still available, you have not lost any information. You can use the extension throughout your code anywhere you need it... and it tidies up the view code.

Struggling To Query Using getDocuments() in Firestore Swift

This is the first time I am using a Firestore Query and I'm struggling to parse the data. I normally use the same setup when I get documents (which works), but when I attach it to a query it does not work.
I am trying to query the database for the shop most visited, so I can later set it as favourite.
My Code:
func findFavouriteShop(completed: #escaping ([String]) -> Void)
{
// Variables
let dispatch = DispatchGroup()
var dummyDetails = [String]()
// References
let db = Firestore.firestore()
let userID = Auth.auth().currentUser?.uid
let groupCollectionRef = String("visits-" + userID! )
// Query the database for the document with the most counts
dispatch.enter()
db.collectionGroup(groupCollectionRef).order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching documents: \(err)")
}
else {
print(snapshot)
guard let snap = snapshot else {return}
for document in snap.documents {
let data = document.data()
// Start Assignments
let shopName = data["shopName"] as? String
let count = data["count"] as? String
// Append the dummy array
dummyDetails.append(shopName!)
dummyDetails.append(count!)
}
dispatch.leave()
}
dispatch.notify(queue: .main, execute: {
print("USER number of documents appended: \(dummyDetails.count)")
completed(dummyDetails)}
)
}
Using Print statements it seems as if the guard statement kicks the function out. The processor does not reach the for-loop to do the assignments. When I print the snapshot it returns an empty array.
I am sure I have used the wrong notation, but I'm just not sure where.
There's a lot to comment on, such as your choice of collection groups over collections (maybe that's what you need), why you limit the results to one document but feel the need to query a collection, the naming of your collections (seems odd), the query to get multiple shops but creating a function that only returns a single shop, using a string for a count property that should probably be an integer, and using a string array to return multiple components of a single shop instead of using a custom type.
That said, I think this should get you in the right direction. I've created a custom type to show you how I'd start this process but there's a lot more work to be done to get this where you need it to be. But this is a good starting point. Also, there was no need for a dispatch group since you weren't doing any additional async work in the document parsing.
class Shop {
let name: String // constant
var count: Int // variable
init(name: String, count: Int) {
self.name = name
self.count = count
}
}
func findFavouriteShops(completion: #escaping (_ shops: [Shop]?) -> Void) {
guard let userID = Auth.auth().currentUser?.uid else {
completion(nil)
return
}
var temp = [Shop]()
Firestore.firestore().collection("visits-\(userID)").order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
completion(nil)
return
}
for doc in snapshot.documents {
if let name = doc.get("shopName") as? String,
let count = doc.get("count") as? String {
let shop = Shop(name: name, count: count)
temp.append(Shop)
}
}
completion(temp)
}
}
You can return a Result type in this completion handler but for this example I opted for an optional array of Shop types (just to demonstrate flexibility). If the method returns nil then there was an error, otherwise there are either shops in the array or there aren't. I also don't know if you're looking for a single shop or multiple shops because in some of your code it appeared you wanted one and in other parts of your code it appeared you wanted multiple.
findFavouriteShops { (shops) in
if let shops = shops {
if shops.isEmpty {
print("no error but no shops found")
} else {
print("shops found")
}
} else {
print("error")
}
}