-
-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Setting property to nil causes delete op on save #268
Conversation
Thanks for opening this pull request!
|
Codecov Report
@@ Coverage Diff @@
## main #268 +/- ##
==========================================
- Coverage 82.33% 82.12% -0.21%
==========================================
Files 99 100 +1
Lines 10512 10535 +23
==========================================
- Hits 8655 8652 -3
- Misses 1857 1883 +26
Continue to review full report at Codecov.
|
A few things before this would be ready to merge. I'm open to changing the name of the property wrapper from I updated the Playground examples where I saw them to use the wrapper, so hopefully, people viewing those examples will see the property wrapper is required for optional properties. If there is other documentation you'd like me to update just point me to it and I can take care of it. I briefly looked into modifying the ParseEncoder system to use the Delete op on nil values instead of using the wrapper, however, I was concerned this may have unintended consequences when trying to encode something that isn't just a Parse object. Finally, I tried to write a unit test to validate this on a Parse Object but was unable to recreate the problem in the test. The mock response was always correct, a server is required to recreate this issue as far as I could tell. I can write some test cases that just cover the property wrapper outside of the Parse environment, just making sure it is encoding as expected, if that works. |
This doesn't seem like it needs to be in the SDK as it won't be default behavior because it requires the wrapper to be applied manually by the developer. If the developer wants such behavior, it's easier to use any of the methods described here: #264 (comment) In addition, if a developer wants to choose a different wrapper to suit their needs or even a different way to encode, they can use that as well, providing greater flexibility than adding the wrapper internally. You could also write your wrapper in a separate repo and let developers know they can use it as a dependency if the linked wrapper doesn't suit current needs. |
You will need to install SwiftLint which will automatically fix or return errors. See here for more: https://github.com/parse-community/Parse-Swift/blob/main/CONTRIBUTING.md |
Testcases to replicate for your PR shouldn't need the mocker, but instead look something like: Parse-Swift/Tests/ParseSwiftTests/ParseObjectTests.swift Lines 558 to 584 in 5860abd
|
@@ -39,6 +39,9 @@ struct GameScore: ParseObject { | |||
|
|||
//: Your own properties. | |||
var score: Int = 0 | |||
|
|||
//: Optional custom properties need to be marked with @NullableProperty or setting properties to `nil` won't propagate to server | |||
@NullableProperty var gameEndDate: Date? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For playgrounds this probably only need a couple of examples. You can add a separate page that shows multiple different use-cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I wanted to make sure to cover any potential cases in case the user was only looking a one page of the Playground instead of looking through some of the earlier pages. I can change this to only add the changes to the first section, with maybe a little extra info there. I don't think there are really any other use-cases, if you're going to set a property to nil at some point then you'd need the wrapper.
I suppose I'll have to disagree on not needing it in the SDK. The current behavior was unexpected by all the developers on my team, if the saved value on return of save shows one thing and the value on the server shows another, that doesn't seem like it would be correct. If a developer isn't aware of this and they don't know they need to apply a custom operation, then they would run into bugs without realizing what the cause is. Having the documentation be clear about what needs to happen (add the included property wrapper to a property that may be set to nil) and giving the tool to actually perform it within the SDK seems like an appropriate measure. I'm afraid if we supply the property wrapper in another repository it won't be seen and people won't realize it's necessary. The NullCodable package in SPM wouldn't fix this problem, because it encodes values as null instead of removing them. As an aside I do have SwiftLint installed and running, however it was throwing errors all over the project, like it wasn't picking up the config file. I'll dig into that and fix any outstanding issues there. |
Got it. That makes sense, I'll add a case that works that way. I was starting with the |
The behavior is expected for Swift developers who have always used the native Swift JSON en/decoders since they arrived in Swift. The behavior you are mentioning isn't expected to a Swift developer. The SDK includes ways to handle this natively with respect to Parse in my comment here: #264 (comment)
If the developer doesn't know to use the property wrapper, even if's included, they will run into the same exact problem you mentioned. The readme can be updated to include recommended dependencies developers can choose to add if they need it. |
There are a lot of people who use the Parse Swift SDK who may not be super familiar with Swift Encoders, or their limitations. Or even understand that internally the Parse Swift SDK is using them in this way. I've used them quite a bit but didn't realize they didn't encode nil values by default. Let me give one more example. Here's some code using the Javascript SDK: var message...
message.set('league', league);
message.set('sender', requestingUser);
message.unset('unread');
message.set('messageBody', messageBody);
await message.save(); Here's the same code using the Objective C SDK: var message...
message.league = league
message.sender = requestingUser
message.unread = nil
message.messageBody = messageBody
try message.save() Here's the same code using the Android SDK: message.put("league", league);
message.put("sender", requestingUser);
message.remove("unread");
message.put("messageBody", messageBody);
message.saveInBackground(e -> {...}); The basic format is the same for every SDK. You set/unset values, then save the changed object. In the Swift SDK this isn't possible currently. The closest we could get would be: var message...
message.league = league
message.sender = requestingUser
message.messageBody = messageBody
message = try newLeague.save()
let removeOperation = message.operation.unset("unread")
message = try removeOperation.save() Similar but now we're running two separate operations and depending on code layout you may not always know at the time that save is being called that you might be setting a value to nil. You also have cloud functions getting called twice - there may be a way to combine those operations into a single transaction but that adds to the complexity even further. While I don't think the SDKs need to match exactly - this one should be as Swift-y as possible - the current SDK has a situation where setting a value to nil and saving it results in inconsistency between the local data and the server. This proposed solution keeps things Swift-y (a property wrapper is about as Swift-y as it gets) by telling the SDK/compiler you have a value that might drop to nil, but now has behavior that is consistent with the other SDKs and even the internal vs server state. While I'd love to fix this without the need for a property wrapper, this seems to be the next best solution and proper documentation can get us the rest of the way there. |
You probably need to upgrade to the latest SwiftLint as that's what the CI uses, type |
You should also add a new wrapped property here: Parse-Swift/Tests/ParseSwiftTests/ParseObjectTests.swift Lines 30 to 42 in 5860abd
to ensure the wrapper doesn't break the rest of the SDK. |
Also, can you highlight the differences of your added wrapper vs. the one it's based on? Specifically what's different? I couldn't tell from the original description. |
Yes, there are two differences (aside from the renaming). The first is adding extension NullableProperty: Hashable where Wrapped: Hashable { } The second is a change to the Here is the original: public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch wrappedValue {
case .some(let value): try container.encode(value)
case .none: try container.encodeNil()
}
} Here is the new copy: public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch wrappedValue {
case .some(let value):
try container.encode(value)
case .none:
try container.encode(Delete())
}
} Specifically, the change is in the |
Perfect that did the trick, thank you. |
Just for clarity:
Since version 1.11.0, you can use "set" along with any number of ParseOperations all within the same var message...
let operations = message.operation
.set(("league", \.league), league)
.set(("sender", \.sender), requestingUser)
.set(("messageBody", \.messageBody), messageBody)
.unset(("unread", \.unread))
do {
message = try await operations.save()
} catch {
// Handle error
} Which looks very similar to your JS and Android examples and won't make extra calls to Cloud Code nor does it add complexity. |
@Vortec4800 from your comments above, I'm assuming your DB is Mongo as there is no difference between Comments from 308 that directly related:
|
It seems #315 should help you carry out this PR, particularly checking if You will probably want two wrappers that almost do the same thing. 1) encodes null and 2) deletes the item. This will be important for mongo because of what I mentioned here: #268 (comment). Essentially, developers can choose which wrapper works best for them. We can then add this to the |
Closing this due to no activity and the previous answers providing the capability |
New Pull Request Checklist
Issue Description
Currently, saving a
nil
value to a property updates that value tonil
in the local SDK, but the value on the server does not get changed.Related issue: #264
Approach
This approach was inspired by the way the Javascript SDK and REST API handles unsetting values. Instead of updating a value to a new value, a custom Delete operation is sent instead. This property wrapper looks for a
nil
value and if found, codes the Delete operation as the value.TODOs before merging