parse-community / Parse-Swift

The Swift SDK for Parse Platform (iOS, macOS, watchOS, tvOS, Linux, Android, Windows)
https://parseplatform.org
MIT License
308 stars 69 forks source link

Updating a saved ParseObject using saveAll produces error #405

Closed vdkdamian closed 2 years ago

vdkdamian commented 2 years ago

New Issue Checklist

Issue Description

I am unable to upload objects using the saveAll() method. I get the error: "ParseError code=105 error=Invalid field name: __type."

This is my struct:

struct PGRListItem: NVMParseObject {
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    var id: String { nvmId ?? Novem.generate_nvmId() }

    var userId: String?
    var nvmId: String?
    var timeStamp: Date?

    var deviceId: String?
    var deviceName: String?

    var selected: Bool?
    var shops: [Shop]?
    var brand: String?
    var priority: Int?
    var amount: Float?
    var price: [String : String]?

    var checkedHistory: [Date]?
    var uncheckedHistory: [Date]?
    var checkedTimes: Int?
    var uncheckedTimes: Int?
    var lastUsed: Date?

    init() {
        self.nvmId = Novem.generate_nvmId()
    }
}

Here is my code that saves the objects.

if let synchronizableObject = synchronizableObjects.first, synchronizableObjects.count == 1 {
    let object = try await synchronizableObject.save()

    self.removeSynchronizingObject(object.nvmId)
    try self.storeParseItems([object],
                             storeGroup: self.storeGroup,
                             encryption: self.encrypted ?? false,
                             fetch: .none)

} else {
    let results = try await synchronizableObjects.saveAll()
    try self.storeResults(results,
                          storeGroup: self.storeGroup,
                          encryption: self.encrypted ?? false)
}

Also might be important, if only one object needs to be saved, my code makes use of .save(). Multiple objects use .saveAll(). It's good to know that the .save() function never throws this error.

Steps to reproduce

Using the saveAll() method

Actual Outcome

ParseError code=105 error=Invalid field name: __type.

Expected Outcome

No error

Environment

Client

Server

Database

Logs

parse-github-assistant[bot] commented 2 years ago

Thanks for opening this issue!

cbaker6 commented 2 years ago

I am unable to upload objects using the saveAll() method. I get the error: "ParseError code=105 error=Invalid field name: __type."

I'm not able to replicate your issue. Some notes:

  1. You are running on Xcode 14 beta, this isn't officially supported even though I've run the full test-suite on Xcode 14 Beta 5 and 6 and it passes without issues
  2. You never posted the code you are using that "actually" produces the saveAll error. Please update your issue with the code
  3. Please post the complete protocol of NVMParseObject. If you've customized a ParseObject, it's possible your customization is creating the issue
  4. Is Shop a ParseObject? If so, please post the code for it
  5. Is there a reason for var id: String { nvmId ?? Novem.generate_nvmId() }? Not sure if that's causing your current issue, but you are bound to run into issues with items that require Identifiable based on the code you've shown so far. A ParseObject comes with a default implementation of id, 90% of use-cases should use the default implementation shown here: https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/Sources/ParseSwift/Objects/ParseObject.swift#L175-L185
  6. I ran all of the code in Playgrounds that uses saveAll in Xcode 13.4.1 and Xcode 14 Beta 6 and all of it works as expected: https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/ParseSwift.playground/Pages/1%20-%20Your%20first%20Object.xcplaygroundpage/Contents.swift#L148-L252 https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/ParseSwift.playground/Pages/8%20-%20Pointers.xcplaygroundpage/Contents.swift#L269-L330 https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/ParseSwift.playground/Pages/12%20-%20Roles%20and%20Relations.xcplaygroundpage/Contents.swift#L282-L317

I recommend:

  1. Forking the repo and running all of the saveAll code in Playgrounds (step 6 above) connected to your server or development server on whatever service you use to host. If this code works as expected, proceed to step 2
  2. Modify the Playgrounds to a situation that represents yours. Are you able to replicate the error? If so, post all of the code
  3. Try the modified code in the previous step in Xcode 13.4.1, does it still throw an issue?
cbaker6 commented 2 years ago

In addition, I suspect:

init() {
        self.nvmId = Novem.generate_nvmId()
}

is related to your issue based on "ParseError code=105 error=Invalid field name: __type." The empty initialize should be "empty". If you want to create another initializer, place it in an extension and it should take at least one value. This is mentioned in the [Documentation](https://parseplatform.org/Parse-Swift/release/documentation/parseswift/parseobject/init()), in previous comments https://github.com/parse-community/Parse-Swift/pull/315#issuecomment-1014701003:

From a POP standpoint, if the ParseObject protocol doesn't require init(), developers can make non-optional values and it's impossible to initialize new versions of their ParseObject's internally in the SDK; preventing .mutable, merge, or anything else that needs a new copy inside the app. For value types with all optional properties, the compiler provides the init() automatically; assuming all other inits are defined in an extension. This is essential since this SDK doesn't use reference types and at times need to return a fresh copy of the value type to the developer.

and Playgrounds:

https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/ParseSwift.playground/Pages/1%20-%20Your%20first%20Object.xcplaygroundpage/Contents.swift#L55-L66

Your ParseObject should be:

struct PGRListItem: NVMParseObject {
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    /* 
       I highly doubt you need this here and you probably have to manage more 
       code in the long run because you have it here. If you really want it, it should look
       more like the following. Your nvmId will only get used when there's no objectId (basically
       when the ParseObject hasn't been saved to the server).
   */
    // var id: String { objectId ?? nvmId ?? Novem.generate_nvmId() }

    var userId: String?
    var nvmId: String?
    var timeStamp: Date?

    var deviceId: String?
    var deviceName: String?

    var selected: Bool?
    var shops: [Shop]?
    var brand: String?
    var priority: Int?
    var amount: Float?
    var price: [String : String]?

    var checkedHistory: [Date]?
    var uncheckedHistory: [Date]?
    var checkedTimes: Int?
    var uncheckedTimes: Int?
    var lastUsed: Date?
}

/*
  It is recommended to place custom initializers in an extension 
  to preserve the memberwise initializer. 
*/
extension PGRListItem {
  init(nvmId: String?) {
        self.nvmId = nvmId ?? Novem.generate_nvmId()
  }
}
vdkdamian commented 2 years ago

I totally forgot I can't use custom init().

But my custom ID is for the exact reason as you mention. I had an issue where I needed the ID on objects that where not saved yet.

I'll try to reform all the mess I made later today.

vdkdamian commented 2 years ago

I completely removed the custom init, and the custom id var. It still shows the error.

vdkdamian commented 2 years ago
  1. You never posted the code you are using that "actually" produces the saveAll error. Please update your issue with the code

I'll do.

  1. Please post the complete protocol of NVMParseObject. If you've customized a ParseObject, it's possible your customization is creating the issue
    
    // MARK: - NVMParseObject
    public protocol NVMParseObject: ParseObject, NVMUserable, NVMSynchronizable, Identifiable {
/**
 An ID for use by Novem instead of objectId. This id should be the same as nvmId.

 - warning: Changing this would be a breaking change and can result in various issues.
 */
var id: String { get }

}

extension NVMParseObject {

/**
 An ID for use by Novem instead of objectId. If no nvmId is found, it will generate a new one.
 */
 func nvmId() -> String { self.nvmId ?? Novem.generate_nvmId() }

}

// MARK: - NVMSynchronizable

/* Use this protocol to allow the full synchronization of Parse Object to the Novem server. / public protocol NVMSynchronizable {

/**
 A  `String` used to synchronize between local en cloud.

 - warning: This property is not intended to be set or modified by the developer.

 - note: **This value may not be nil.** It's optional because `ParseSwift` requires there variables to be optional, but all use by the Novem framework will force-unwrap this when used. If this value is nil, the object is not correctly set and shouldn't exist.
*/
var nvmId: String? { get }

/**
 A  `Date` used to synchronize between local en cloud.

 - warning: This property is not intended to be set or modified by the developer.

 - note: **This value may not be nil.** It's optional because `ParseSwift` requires there variables to be optional, but all use by the Novem framework will force-unwrap this when used. If this value is nil, the object is not correctly set and shouldn't exist.
*/
var timeStamp: Date? { get set }

}

// MARK: - NVMUserable public protocol NVMUserable: Codable {

/**
The Novem userId for this object.
*/
var userId: String? { get set }

}



> 4. Is `Shop` a `ParseObject`? If so, please post the code for it

It's not a `ParseObject`. It's just a struct conforming to Codable
vdkdamian commented 2 years ago

I recommend:

  1. Forking the repo and running all of the saveAll code in Playgrounds (step 6 above) connected to your server or development server on whatever service you use to host. If this code works as expected, proceed to step 2

I don't know how to connect the Playgrounds to my server. Is there a tutorial or something?

cbaker6 commented 2 years ago

I don't know how to connect the Playgrounds to my server. Is there a tutorial or something?

This is discussed In CONTRIBUTING.md and towards the top of README.md.

gemiren commented 2 years ago

I'm having the exact issue. saveAll() works for new objects (object never saved to Parse server yet) but doesn't work for modifying existing objects (object already exists on the Parse Sever).

Below is the log from Parse Sever when saveAll() is used to save existing objects. You can see that the body contents wrong encoding of the object. No changed keys of the object are sent to the server.

verbose: REQUEST for [POST] /parse//batch: {
  "requests": [
    {
      "body": {
        "__type": "Pointer",
        "className": "TestingParseObject",
        "objectId": "38vPLCcGKA"
      },
      "method": "PUT",
      "path": "/parse/classes/TestingParseObject/38vPLCcGKA"
    }
  ],
  "transaction": false
} {"body":{"requests":[{"body":{"__type":"Pointer","className":"TestingParseObject","objectId":"38vPLCcGKA"},"method":"PUT","path":"/parse/classes/TestingParseObject/38vPLCcGKA"}],"transaction":false},"headers":{"accept":"*/*","accept-encoding":"gzip, deflate","accept-language":"en-US,en;q=0.9","connection":"keep-alive","content-length":"204","content-type":"application/json","host":"147.182.187.161:1337","user-agent":"PodcastApp/1 CFNetwork/1390 Darwin/22.0.0","x-parse-application-id":"LUqHFAQFXnmfp6TN","x-parse-client-version":"swift4.14.1","x-parse-installation-id":"cecc6f78-72f6-49c0-8b44-2e7ce66b2b0e","x-parse-request-id":"d121c68b-49fc-488f-969b-ef75e2fa26b4","x-parse-session-token":"r:8b0b27669791b030b7d88ff289eab7a6"},"method":"POST","url":"/parse//batch"}
verbose: RESPONSE from [POST] /parse//batch: {
  "response": [
    {
      "error": {
        "code": 105,
        "error": "Invalid field name: __type."
      }
    }
  ]
} {"result":{"response":[{"error":{"code":105,"error":"Invalid field name: __type."}}]}}

When saving new objects with saveAll() the log from Parse Server looks like this:

verbose: REQUEST for [POST] /parse//batch: {
  "requests": [
    {
      "body": {
        "ACL": {
          "peYQ4RYdyp": {
            "read": true,
            "write": true
          }
        },
        "favorite": true,
        "hide": 0,
        "playedTime": 0,
      },
      "method": "POST",
      "path": "/parse/classes/TestingParseObject"
    }
  ],
  "transaction": false
}
verbose: RESPONSE from [POST] /parse//batch: {
  "response": [
    {
      "success": {
        "objectId": "38vPLCcGKA",
        "createdAt": "2022-09-30T00:50:20.327Z"
      }
    }
  ]
} {"result":{"response":[{"success":{"createdAt":"2022-09-30T00:50:20.327Z","objectId":"38vPLCcGKA"}}]}}

The body content is the object itself.

Digging into the source code, it looks the culprit is from this line https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/Sources/ParseSwift/Coding/ParseEncoder.swift#L345. For existing objects the pointer encoding is send to the Parse Sever.

For new objects, it goes to the line https://github.com/parse-community/Parse-Swift/blob/a94ac8c95d67c82cdeca870a9aeb977882d040aa/Sources/ParseSwift/Coding/ParseEncoder.swift#L353, which encodes the object itself correctly.

vdkdamian commented 2 years ago

Fix is working! Thanks

cbaker6 commented 2 years ago

@bcbeta can you provide the code for your objects and your call to save? It's best to edit the Playgrounds examples and make it produce the error as mentioned here: https://github.com/parse-community/Parse-Swift/issues/405#issuecomment-1238725358. From the example you described:

when I create 2 related parse objects and save one of them

The relevant playgrounds code seems to work in my testing of the latest version: https://github.com/parse-community/Parse-Swift/blob/dc7666f774755be333f48f25c4d8bead0a1a6877/ParseSwift.playground/Pages/8%20-%20Pointers.xcplaygroundpage/Contents.swift#L101-L140

Also, if you are using save() you have a different issue from this one. You should open a separate issue and link to not having issues until version 4.14.2

bcbeta commented 2 years ago

Yes sorry I deleted my comment because I need to spend a little more time describing the issue. I’ll try to get to it tonight. Something with the latest update broke my apps ability to save objects.

cbaker6 commented 2 years ago

Something with the latest update broke my apps ability to save objects.

This is unfortunate. I ran the whole Playgrounds suite along with the CI test suite and didn't see any breaks. Feel free to open a new issue if you discover how to replicate the problem you are facing.