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.
## [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))
|
🎉 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