Skip to content

[Sync] Validate the whole pulled changeset before applying any of it - #1985

Open
joker-777 wants to merge 1 commit into
Nozbe:masterfrom
kenhub:fix/sync-validate-changeset
Open

joker-777 wants to merge 1 commit into
Nozbe:masterfrom
kenhub:fix/sync-validate-changeset

Conversation

@joker-777

@joker-777 joker-777 commented Nov 2, 2026 •

Copy link
Copy Markdown

Validates a pulled changeset in full before any record is prepared, so a bad pull fails with nothing touched.

Before, an invalid record halfway through a pull left the records before it prepared but never batched. They stayed in the record cache in that state, and update() on them threw until the app restarted.

Changes

  • Checks before applying: every table change has created, updated and deleted arrays; every record has a string id and no _status, _changed or __proto__; deleted ids are strings
  • An id that appears more than once in one table's created, updated and deleted is rejected (for example, updated and deleted in the same pull)
  • Unknown tables are still skipped with a warning, for forward compatibility, so their contents aren't validated
  • Behaviour change: a record with a non-string id is now rejected. Before, it was silently given a random id

Tests

New tests in applyRemote.test.js cover a bad second table, duplicate ids, malformed tables and malformed ids. Each checks that a record from earlier in the pull is unchanged and can still be updated. On master they fail with record.prepareUpdate was called on mock_projects#pSynced but wasn't sent to batch().

Verification

🤖 Generated with Claude Code

* Validate the whole pulled changeset before applying any of it

applyRemoteChanges validated records one by one while preparing them, so a
malformed record (or one id listed in both updated and deleted) threw after
earlier records had already been prepared. Those records kept their pending
state and mutated raw in the cache, so later update() calls on them failed
until the app restarted.

The changeset is now checked up front: table shape, record shape (string id,
no _status/_changed, no __proto__ key), string deleted ids, and duplicate ids
across created/updated/deleted. Unknown tables are still skipped for
forward-compatibility.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Validate the same tables that are applied, and add a changelog entry

getAllRecordsToApply enumerates tables with for-in (through filterObj), which
includes inherited keys, while validation used Object.keys. Validation now
enumerates the same way, so no applied table skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Require own enumerable created/updated/deleted arrays

Table changes are copied with object spread when applied, which drops
inherited and non-enumerable keys. Changes whose arrays were only inherited
passed validation and then failed after earlier tables were prepared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@StasDoskalenko

Copy link
Copy Markdown

Hi @joker-777, thank you, that's a great catch on the half-prepared records!

I maintain NitromelonDB, a maintained fork of WatermelonDB (New Architecture, Nitro Modules). This applies there too: validateRemoteRaw still runs per record while preparing, so an invalid record halfway through a pull leaves the earlier ones prepared.

Our fork was rewritten in TypeScript (strict mode) to reduce the chance of bugs like this one slipping through, so I think the community would really benefit if you opened your PRs against it too. The code lives in src/sync/impl/applyRemote.ts there.

If you don't have the time or would rather not, no worries at all, I'm happy to port it myself. Just let me know. Thanks a lot!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants