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

Update of model with required field fails #349

Closed pmmlo closed 2 years ago

pmmlo commented 2 years ago

New Issue Checklist

Issue Description

Very odd behavior. Initial edit of a nil-valued field saves without error, but subsequent changes/updates to the model returns an error 142 with the required field needed.

Steps to reproduce

  1. Create a model with a required field and other customized field(s) on the server-side (e.g. modelschema.addPointer('createdBy', '_User', { required: true }).
  2. Save an object in this class with the customized field as nil.
  3. Update a customized field as a mergeable object.
  4. Update the customized field again and try to save. Error occurs here consistently. Error 142 with 'createdBy' is a required field.

Additionally, when I make the require column unrequired, there is no error. In other words, a class with no required fields succeeds without issue.

Example:

struct GameScore: ParseObject {
    //: These are required by ParseObject
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    //: Your own properties.
    var shots: [ShotCodable]?
    var createdBy: Pointer<User>?

    init() {}

    //: Implement your own version of merge
    func merge(with object: Self) throws -> Self {
        var updated = try mergeParse(with: object)
        if updated.shouldRestoreKey(\.shots,
                                     original: object) {
            updated.shots = object.shots
        }
        if updated.shouldRestoreKey(\.createdBy,
                                     original: object) {
            updated.createdBy = object.createdBy
        }
        return updated
    }
}

struct ShotCodable: Codable, Hashable {
    var name: String?
    var shooter: Pointer<User>?
    var score: Int?

    init(){}

    init(name: String?, shooter: Pointer<User>?, score: Int?) {
        self.name = name
        self.shooter = shooter
        self.score = score
    }
}

var gameScore = GameScore()
gameScore.createdBy = try? User.current.toPointer()
gameScore.save { result in
    switch {
    case .success(let success):
        gameScore = success
    case .failure(let error):
        print("Failed to save \(error)")
    }
}

let shot = ShotCodable(name: "Curry", shooter: try? User.current.toPointer(), score: 3)
var mergeableGameScore = gameScore.mergeable()
mergeableGameScore.shots = [shot]
mergeableGameScore.save { result in
    switch {
    case .success(let success):
        print(success)
    case .failure(let error):
        print("Failed to save \(error)")
    }
}

let anotherOne = ShotCodable(name: "Green", shooter: try? User.current.toPointer(), score: 2)
mergeableGameScore.shots?.append(anotherOne)
mergeableGameScore.save { result in
    switch {
    case .success(let success):
        print(success)
    case .failure(let error):
        print("Failed to save \(error)") // <--- Error 142 occurs here.
    }
}

Actual Outcome

Error 142 with [required field] needed.

Expected Outcome

Save success.

Environment

Client

Server

Database

Logs

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

Thanks for opening this issue!

pmmlo commented 2 years ago

@cbaker6 Any chance you could take a look?

It's really odd behavior. I have no problem updating once, but on the second/subsequent updates I get an error about needing a required field. Even if I pass the required field, I get the error.

cbaker6 commented 2 years ago

Try changing: var createdBy: Pointer<User>? -> var createdBy: User? as this is what you probably really want anyways. The SDK will handle turning createdBy to the proper pointer during the first save. After the initial save, you may need to fetch or include; createdBy in your queries. ShotCodable should probably conform to ParseObject based on how you are using it and var shooter: Pointer<User>? should be var shooter: User?. Essentially, your objects should look similar to Author in the playgrounds: https://github.com/parse-community/Parse-Swift/blob/4d2d509fc946ee70fbfcd75d7b0520cc7164d72e/ParseSwift.playground/Pages/8%20-%20Pointers.xcplaygroundpage/Contents.swift#L54-L172

Also, the code below is asynchronous, so there's no guarantee the saved version of gameScore:

var gameScore = GameScore()
gameScore.createdBy = try? User.current.toPointer()
gameScore.save { result in
    switch {
    case .success(let success):
        gameScore = success
    case .failure(let error):
        print("Failed to save \(error)")
    }
}

will be available before gameScore.mergeable() the way your current code is structured:

let shot = ShotCodable(name: "Curry", shooter: try? User.current.toPointer(), score: 3)
var mergeableGameScore = gameScore.mergeable()
mergeableGameScore.shots = [shot]
mergeableGameScore.save { result in
    switch {
    case .success(let success):
        print(success)
    case .failure(let error):
        print("Failed to save \(error)")
    }
}

You can fix this by using async/await or moving the code into the first save() { result in... }

pmmlo commented 2 years ago

Yup, that was the issue. Thanks!

pmmlo commented 2 years ago

I should actually be clearer just in case someone has a similar issue. Your answer is great, but wasn't the issue. It was apparently using createdAt, updatedAt, and objectId as parameters within an array object. I suspect those are reserved, so those were not being saved. Not sure exactly for sure, but renaming these in my local project updates the object.

Thanks for the help!

cbaker6 commented 2 years ago

I still recommend the changes. The way your objects are setup in your question reduces the ability of Swift SDK and Parse and requires you to make extra calls to the server along with write extra code (your example). The coding footprint can be reduced significantly by following the suggested way in Playgrounds along with the ability to use include/exclude/fetch directly.