Repository: parse-community/Parse-Swift (swift)

Issue:
.fetch() with custom objectId
I am not sure if I misunderstood `fetch` and `find` or I am hitting some bug. 

* `fetch` I should use when I want to get objects based on `objectId` field
* `find` I should use when I have some query criteria and want to find one or more objects

I believe I can use find to get one single object with given `objectId` also, but I guess it is more performant to use fetch on unique objects?

What I got stuck is a `fetch` response returning empty object. I use custom `objectId` on this object and the save and update is working well. Before the first `fetch` I don't know the createAt date, so I init the struct with any `Date()`:

```
        init(objectId: String) {
                self.objectId = objectId
                self.createdAt = Date()
        }
```

then I call the fetch like this:

```
        let profileToFetch = PrsProfile(objectId: objectId)
        print("profileToFetch: \(profileToFetch)")
        profileToFetch.fetch { results in
            switch results {
            case .success(let profile):
                print("fetched profile: \(profile)")
            case .failure(let error):
                print("error: \(error)")
            }
        }
```

So struct being passed to fetch is:

> profileToFetch: PrsProfile ({"createdAt":{"__type":"Date","iso":"2021-05-11T09:28:21.877Z"},"objectId":"Uo9eYl2rkB"})

But the response gives back empty object:

> fetched profile: PrsProfile ({})

When I do not specify `createdAt` in init, the fetch function throws error due to `isSaved` check in SDK:

```
guard object.isSaved else {
            throw ParseError(code: .unknownError, message: "Cannot Fetch an object without id")
        }
```

`inkludekemys: ["*"]` is also not helping here. There is also no code in `beforeSave` trigger. How should I fetch an object with customId?

Thank you!

Pull request:
Fixes issue where Swift SDK prevents fetching of Parse objects when custom objectId is enabled. This is due to checking for `createdAt` for a saved object. Close #138 

- [x] Only check objectId for fetchCommand
- [x] Add testcase
- [x] Add playground example
- [x] Add changelog

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7f31c1a5..da98b3bd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.7.1...main)
 * _Contributing to this repo? Add info about your change here to be included in the next release_
 
+__Fixes__
+- Fixed issue where Swift SDK prevented fetching of Parse objects when custom objectId was enabled ([#139](https://github.com/parse-community/Parse-Swift/pull/139)), thanks to [Corey Baker](https://github.com/cbaker6).
+
 __Improvements__
 - Add playground example of saving Parse objects with a custom objectId ([#137](https://github.com/parse-community/Parse-Swift/pull/137)), thanks to [Corey Baker](https://github.com/cbaker6).
 
diff --git a/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift
index cd57728ea..3b421b5ab 100644
--- a/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift
+++ b/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift
@@ -80,4 +80,36 @@ score.save { result in
     }
 }
 
+//: Fetch object
+score.fetch { result in
+    switch result {
+    case .success(let fetchedScore):
+        print("Successfully fetched: \(fetchedScore)")
+    case .failure(let error):
+        assertionFailure("Error fetching: \(error)")
+    }
+}
+
+//: Query object
+let query = GameScore.query("objectId" == "myObjectId")
+query.first { result in
+    switch result {
+    case .success(let found):
+        print(found)
+    case .failure(let error):
+        print(error)
+    }
+}
+
+//: Query object
+let query = GameScore.query("objectId" == "myObjectId")
+query.find { result in
+    switch result {
+    case .success(let found):
+        print(found)
+    case .failure(let error):
+        print(error)
+    }
+}
+
 //: [Next](@next)
diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground
index 46a182367..8d387d1c6 100644
--- a/ParseSwift.playground/contents.xcplayground
+++ b/ParseSwift.playground/contents.xcplayground
@@ -17,4 +17,4 @@
         <page name='14 - Config'/>
         <page name='15 - Custom ObjectId'/>
     </pages>
-</playground>
\ No newline at end of file
+</playground>
diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift
index 8f8b31582..e10771902 100644
--- a/Sources/ParseSwift/API/API+Commands.swift
+++ b/Sources/ParseSwift/API/API+Commands.swift
@@ -392,7 +392,7 @@ internal extension API.Command {
 
     // MARK: Fetching
     static func fetchCommand<T>(_ object: T, include: [String]?) throws -> API.Command<T, T> where T: ParseObject {
-        guard object.isSaved else {
+        guard object.objectId != nil else {
             throw ParseError(code: .unknownError, message: "Cannot Fetch an object without id")
         }
 
diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift
index 7e5864d33..958bef757 100644
--- a/Sources/ParseSwift/Objects/ParseInstallation.swift
+++ b/Sources/ParseSwift/Objects/ParseInstallation.swift
@@ -402,7 +402,7 @@ extension ParseInstallation {
     }
 
     func fetchCommand(include: [String]?) throws -> API.Command<Self, Self> {
-        guard isSaved else {
+        guard objectId != nil else {
             throw ParseError(code: .unknownError, message: "Cannot fetch an object without id")
         }
 
diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift
index 6457fc718..4a7c6a96c 100644
--- a/Sources/ParseSwift/Objects/ParseUser.swift
+++ b/Sources/ParseSwift/Objects/ParseUser.swift
@@ -755,7 +755,7 @@ extension ParseUser {
     }
 
     func fetchCommand(include: [String]?) throws -> API.Command<Self, Self> {
-        guard isSaved else {
+        guard objectId != nil else {
             throw ParseError(code: .unknownError, message: "Cannot fetch an object without id")
         }
 
diff --git a/Sources/ParseSwift/Protocols/Objectable.swift b/Sources/ParseSwift/Protocols/Objectable.swift
index 98c0d9a40..7c974d04f 100644
--- a/Sources/ParseSwift/Protocols/Objectable.swift
+++ b/Sources/ParseSwift/Protocols/Objectable.swift
@@ -80,7 +80,7 @@ extension Objectable {
         if !ParseSwift.configuration.allowCustomObjectId {
             return objectId != nil
         } else {
-            return createdAt != nil
+            return objectId != nil && createdAt != nil
         }
     }
 


Test patch:
diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift
index adc44c915..390fff13a 100644
--- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift
+++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift
@@ -1657,4 +1657,91 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ
         }
         wait(for: [expectation1], timeout: 20.0)
     }
+
+    // swiftlint:disable:next function_body_length
+    func testFetch() {
+        var score = GameScore(score: 10)
+        let objectId = "yarr"
+        score.objectId = objectId
+
+        var scoreOnServer = score
+        scoreOnServer.createdAt = Date()
+        scoreOnServer.updatedAt = scoreOnServer.createdAt
+        scoreOnServer.ACL = nil
+        let encoded: Data!
+        do {
+            encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer)
+            //Get dates in correct format from ParseDecoding strategy
+            scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded)
+        } catch {
+            XCTFail("Should encode/decode. Error \(error)")
+            return
+        }
+
+        MockURLProtocol.mockRequests { _ in
+            return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+        }
+        do {
+            let fetched = try score.fetch(options: [])
+            XCTAssert(fetched.hasSameObjectId(as: scoreOnServer))
+            guard let fetchedCreatedAt = fetched.createdAt,
+                let fetchedUpdatedAt = fetched.updatedAt else {
+                    XCTFail("Should unwrap dates")
+                    return
+            }
+            guard let originalCreatedAt = scoreOnServer.createdAt,
+                let originalUpdatedAt = scoreOnServer.updatedAt else {
+                    XCTFail("Should unwrap dates")
+                    return
+            }
+            XCTAssertEqual(fetchedCreatedAt, originalCreatedAt)
+            XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt)
+            XCTAssertNil(fetched.ACL)
+        } catch {
+            XCTFail(error.localizedDescription)
+        }
+    }
+
+    func testFetchUser() { // swiftlint:disable:this function_body_length
+        var user = User()
+        let objectId = "yarr"
+        user.objectId = objectId
+
+        var userOnServer = user
+        userOnServer.createdAt = Date()
+        userOnServer.updatedAt = userOnServer.createdAt
+        userOnServer.ACL = nil
+        let encoded: Data!
+        do {
+            encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none)
+            //Get dates in correct format from ParseDecoding strategy
+            userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded)
+        } catch {
+            XCTFail("Should encode/decode. Error \(error)")
+            return
+        }
+        MockURLProtocol.mockRequests { _ in
+            return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+        }
+
+        do {
+            let fetched = try user.fetch()
+            XCTAssert(fetched.hasSameObjectId(as: userOnServer))
+            guard let fetchedCreatedAt = fetched.createdAt,
+                let fetchedUpdatedAt = fetched.updatedAt else {
+                    XCTFail("Should unwrap dates")
+                    return
+            }
+            guard let originalCreatedAt = userOnServer.createdAt,
+                let originalUpdatedAt = userOnServer.updatedAt else {
+                    XCTFail("Should unwrap dates")
+                    return
+            }
+            XCTAssertEqual(fetchedCreatedAt, originalCreatedAt)
+            XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt)
+            XCTAssertNil(fetched.ACL)
+        } catch {
+            XCTFail(error.localizedDescription)
+        }
+    }
 }