Uh oh!
There was an error while loading. Please reload this page.
fix(agent): stop the update route from writing foreign keys from relationships - #363
Conversation
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (3)
🛟 Help
|
PMerlet
left a comment
There was a problem hiding this comment.
Adversarial review. I checked out the branch, replayed the suite, and probed both code paths in throwaway worktrees (origin/main vs this branch) to verify each claim rather than take it on trust.
Claims that hold up
| Claim | Verified |
|---|---|
1094 examples, 0 failures | ✅ replayed |
rubocop clean | ✅ 170 files, no offense |
Node parity (update.ts:56-58) | ✅ identical, in-place delete included — the Node comment even says "the frontend is making a second request to update relationships (not sure why) so we purposely ignore" |
store untouched | ✅ store_spec.rb still fences it with call create with polymorphic foreign key and type and clears both columns when the relationship carries no data |
The is_a?(Hash) guard works on the real payload | ✅ params.to_unsafe_h returns a HashWithIndifferentAccess, which subclasses Hash, and delete(:relationships) removes the string key |
Nothing else reads data[:relationships] on this path | ✅ only store.rb:57 and format_attributes, so the in-place mutation breaks nothing today |
One blocking finding (inline on update.rb): this removes the only working way to clear a PolymorphicManyToOne, because the dedicated route raises on data: null. Verified with probes, and a 2-line fix is in that comment.
Two smaller asks: the safety argument is contradicted by the history of #217 (inline on the spec), and the code deserves the why comment that Node has.
Out of diff: CHANGELOG.md:631 still announces "create and update operations" for #217 and becomes wrong with this PR. A fix: patch that removes a communicated capability deserves a line in the release notes.
Alternative I considered and rejected: ignoring only the relationships whose data is null (keeping explicit writes working) is strictly less breaking and preserves the update half of #217 — but it diverges from agent-nodejs, keeps alive a relation-writing path that covers 2 of the 7 relation types, and has the exact same polymorphic-clear hole. Dropping the whole block is the better call, provided that hole gets plugged.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
## [1.39.1](v1.39.0...v1.39.1) (2026-08-20) ### Bug Fixes * **agent:** stop the update route from writing foreign keys from relationships ([#363](#363)) ([6f91d9a](6f91d9a))
forest-bot
commented
Aug 20, 2026
🎉 This PR is included in version 1.39.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |

What
PUT /forest/:collection/:idno longer writes foreign keys from the JSON:APIrelationshipsblock.And
PUT /forest/:collection/:id/relationships/:namecan now clear aPolymorphicManyToOne— it could not before, on any payload shape.storeuntouched.Why
Fixes#361. Editing one field from a partial Summary panel wiped every
belongs_tothe panel did not show.The frontend sends all to-one relations on every update, with
data: nullfor the ones it is not editing.format_attributesturned those nulls intoforeign_key => nil. HTTP 200, no error, silent data loss.Measured, editing
nameonly:Every relation outside the panel, not just one.
The hole this had to plug first
Dropping the block alone would have killed the only working way to clear a polymorphic foreign key. The dedicated route was broken for that case:
abstract_related_route.rb:9didargs[:params]['data']['type']unguarded, sodata: nullraisedNoMethodError(500).update_polymorphic_many_to_onebuiltforeign_key_type_fieldfromchild_collection.nameunconditionally, so evendata: {id: nil, type: "user"}wroteaddressable_type => "user".Probed on
address.addressable:data: nulladdressable_id=nil, addressable_type=nilPUT /relationships/addressable,data: nullNoMethodError204, writes both nilTwo lines, plus a regression spec that raises without them. No spec covered
data: nullon that path, hence nobody saw it.Why dropping the block is safe
The frontend fires
PUT /forest/:collection/:id/relationships/:namebefore the record PUT, and that route handles set and clear for all four to-one types it dispatches. The generic PUT arrives second, sometimes withattributes: [].The Node agent drops the same block (
packages/agent/src/routes/modification/update.ts:56-58), same rationale in a comment.This path was never a coherent relation-writing API: 7 relation types exist,
format_attributesbranches on 2, the dedicated route dispatches 4 includingOneToOne.Not claimed: that no frontend flow ever reached the generic PUT for relations.
e2d20167(#217) added thePolymorphicManyToOnebranch toformat_attributesafter the dedicated route already handled polymorphic relations (80566a5b, #63), with no issue link explaining why. Either #217 was really aboutPOSTand the update half came for free from the shared method, or some flow did go through here. This PR does not need to settle it: the capability survives either way, on the route the frontend calls first.Removed spec
The
with polymorphic many to one relationexample from #217 asserted the generic PUT writesmemberable_id+memberable_type— the behavior removed here. Inverted rather than deleted: the new examples check no foreign key is written,ManyToOneandPolymorphicManyToOne.Scope
Does not change:
store/ create. Sharedformat_attributes, andstore_spec.rbcoversdata: nullwriting nil at creation on purpose.relationships/:nameroutes, beyond making the polymorphic clear work.basic-serializer.js:92(pruning only runs on create), but fixing that leaves every deployed Ruby agent broken.Create and update now disagree:
POSTwithmemberable: {data: null}writes nil,PUTwrites nothing. Deliberate — create has no prior value to destroy.Release notes
CHANGELOG.mdis generated by@semantic-release/changelogfrom commit messages, so it is not edited here. Two commits carry the change:fix(agent): let the relationships route clear a polymorphic many to onerefactor(agent): name the relationships drop and document whyThe 1.16.2 entry announcing "create and update operations" for #217 stays accurate on create. On update the capability moves to the dedicated route rather than disappearing.
How to test
On a collection with 3+
belongs_to:Verified in the browser against a local Rails 8 app (both
ManyToOneandPolymorphicManyToOne), with a Rack logger dumping every/forestwrite. What the wire shows:12 edge cases through the API:
relationshipsabsent,{}, all-null, explicit ids (ignored, as intended), polymorphic set to another type,dataabsent, unknown relation name. All as expected.Two pre-existing issues found, neither introduced here, both left out of scope:
PUT /relationships/attachablewithdata: {id: null, type: "X"}returns 204 and writestype="X", id=NULL— an incoherent row. Not reachable from the UI: the frontend only sendsdata: null.PUT /forest/:collection/:idwith nodatakey is a 500 onmainas well.Definition of Done
General
Security