Allow MERGE operations into empty array values#754
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 479bc113d0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
lib/utils.ts
Outdated
| if (Array.isArray(existingValue) && existingValue.length === 0 && !Array.isArray(value)) { | ||
| return {isCompatible: true}; |
There was a problem hiding this comment.
Restrict empty-array compatibility to merge-only updates
checkCompatibilityWithExistingValue() is shared by setWithRetry() and mergeCollectionWithPatches(), not only Onyx.merge(), so this new early return makes all non-array writes compatible whenever the current value is []. That means a key that is legitimately array-typed (and currently empty) can now be silently overwritten by an object/primitive via set/mergeCollection, whereas the previous array-vs-non-array guard would log and skip it. Please scope this empty-array exception to the merge-object path instead of globally in the compatibility helper.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
I think this is right, and we should be checking explicitly for object here, rather than non-array
rafecolton
left a comment
There was a problem hiding this comment.
I tested this locally and it worked well.
I think the AI reviewer's comments are valid, and this will allow overwriting arrays with scalar values in addition to objects. Is that intended?
lib/utils.ts
Outdated
| if (Array.isArray(existingValue) && existingValue.length === 0 && !Array.isArray(value)) { | ||
| return {isCompatible: true}; |
There was a problem hiding this comment.
I think this is right, and we should be checking explicitly for object here, rather than non-array
lib/Onyx.ts
Outdated
| if (Array.isArray(existingValue) && existingValue.length === 0) { | ||
| Logger.logAlert(`[ENSURE_BUGBOT] Onyx merge called on key "${key}" whose existing value is an empty array. Will coerce to object.`); | ||
| } |
There was a problem hiding this comment.
Will this fire for any merge attempt into an existing array or only if attempting to merge an object?
There was a problem hiding this comment.
I'm pretty sure merging into an array is unsupported anyway. Checking
rafecolton
left a comment
There was a problem hiding this comment.
Tested on dev via red/green, worked great!
| const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(change, existingValue); | ||
| const {isCompatible, existingValueType, newValueType, isEmptyArrayCoercion} = utils.checkCompatibilityWithExistingValue(change, existingValue); | ||
| if (isEmptyArrayCoercion) { | ||
| Logger.logAlert(`[ENSURE_BUGBOT] Onyx merge called on key "${key}" whose existing value is an empty array. Will coerce to object.`); |
There was a problem hiding this comment.
NAB: Is this enough data for someone to tell what went wrong and fix it? Having the key is nice, but I feel like having the value would almost be necessary. Though, I don't love the idea of logging the value since there is not a good way to know if it's sensitive or not. Is there any other data that we can include that would be helpful? I think probably the most useful thing would be the requestID (if one exists), but that sounds difficult to grab.
There was a problem hiding this comment.
Yeah, I don't see a great way to get the requestID that delivered the update. We could maybe log in SaveResponseInOnyx for any SET [] operation, but that would be potentially noisier than we want, and that wouldn't cover pusher updates. I also don't want to log the value for the reason you stated
There was a problem hiding this comment.
The log statements that make it to the backend will have a user email and a datetime. It'll take a bit of legwork but it's probably not impossible to work backwards from that and the onyx key and figure out what the user was doing
lib/OnyxUtils.ts
Outdated
| }, | ||
| { | ||
| result: (existingValue ?? {}) as TChange, | ||
| result: ((Array.isArray(existingValue) && existingValue.length === 0 ? {} : existingValue) ?? {}) as TChange, |
There was a problem hiding this comment.
Would you mind adding a code comment explaining why this is being done?
There was a problem hiding this comment.
Well, I thought it was necessary because I was misinterpreting this comment in fastMerge, but I was mixing up source and target. Turns out it works fine without this line, so I reverted it.
lib/utils.ts
Outdated
| isCompatible: true, | ||
| }; | ||
| } | ||
| // An empty array existing value is compatible with an object update. |
There was a problem hiding this comment.
Hm, this comment is kind of odd. It is making a statement, but it's not obvious to me why this statement would be true or not. I think the explanation about PHP below is helpful, but this comment could probably be more verbose to help understand why we chose to do this.
rafecolton
left a comment
There was a problem hiding this comment.
Re-tested, confirmed it still works
tgolen
left a comment
There was a problem hiding this comment.
Cool, that looks great. Thanks for the improved comments!
Update react-native-onyx to include Expensify/react-native-onyx#754
Details
An alternative solution to #753. Rather than treating empty arrays as null at set-time, we should allow merge updates to clobber empty array values. Also logs an alert with ENSURE_BUGBOT so we can know when it happens.
See #expert-contributors
Related Issues
https://github.com/Expensify/Expensify/issues/611769
Automated Tests
Add
should merge an object into an empty arrayandshould still reject merging an object into a non-empty arrayManual Tests
Onyx.set('someKey', [])in the browser console to create or set a key to[]Onyx.get('someKey').then(data => console.log('someKey', data))to log the value and make sure it's[]Onyx.merge('someKey', {asdf: {}})to merge into the empty arrayOnyx.get('someKey').then(data => console.log('someKey', data))to log the value and make sure it's{asdf: {}}Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
onyx.patch.working.android.mp4
iOS: Native
Onyx.patch.working.ios.mp4
MacOS: Chrome / Safari
onyx.patch.working.mp4