Skip to content

fix(forest_admin_agent): build PolymorphicManyToOne linkage id from foreign key (#332) - #333

Merged
bexchauveto merged 4 commits into
mainfrom
fix/polymorphic-many-to-one-empty-id
Jul 21, 2026
Merged

fix(forest_admin_agent): build PolymorphicManyToOne linkage id from foreign key (#332)#333
bexchauveto merged 4 commits into
mainfrom
fix/polymorphic-many-to-one-empty-id

Conversation

@bexchauveto

@bexchauveto bexchauveto commented Jul 21, 2026

Copy link
Copy Markdown
Member

Fixes #332

Problem

Opening the list view of a collection with a polymorphic belongs_to crashes the frontend:

UnknownClientRecordAgentError: Expected id to be a string or number, received null

The backend returns 200 OK but the payload is malformed: the relationship linkage id is an empty string.

Root cause

A PolymorphicManyToOne can't be SQL-joined, so query.rb:208 skips it while building the SELECT. The record therefore comes back with a phantom related object that has no primary key ({ "*" => nil }). ForestSerializer then reads the PK off that phantom object, producing an empty id in the JSON:API linkage.

The existing serializer specs didn't catch this because they hand-fed a fully-populated related hash, which the real datasource never produces.

Fix

Override has_one_relationship in ForestSerializer. For a PolymorphicManyToOne it rebuilds the linkage id from the foreign key (*_id + *_type) already loaded on the owner record — no extra query — and returns nil (omitting the data key) when the relation is unlinked. Both the linkage and the compound included document go through this method, so a single override covers both.

This is the workaround reported in #332, moved into the gem.

Tests

Added two specs reproducing the real datasource output (phantom related object) — both fail without the fix:

  • builds the linkage id from the foreign key when the related record is a phantom
  • omits the data key for an unlinked polymorphic relation

🤖 Generated with Claude Code

Note

Fix PolymorphicManyToOne linkage id serialization in ForestSerializer

When a polymorphic many-to-one related record is a phantom (no primary key), the serializer now rebuilds the linkage id from the owner's foreign key fields instead of returning an empty or broken relationship.

  • Overrides has_one_relationship in ForestSerializer to post-process PolymorphicManyToOne relationships, computing id from the foreign key value and resolving the type using foreign_key_targets.
  • Namespaced type values (e.g. Admin::User) are formatted to Admin__User to match collection naming conventions, and custom primary keys are respected.
  • Relationships with a nil foreign key or nil type omit the data member entirely.
  • Risk: any consumer relying on the previous (broken) phantom linkage behavior will see different output.

Changes since #333 opened

  • Added test verifying PolymorphicManyToOne relationship serialization [618ae03]
  • Clarified comments for PolymorphicManyToOne relationship handling [618ae03]

Macroscope summarized 870bad8.

bexchauveto and others added 2 commits July 21, 2026 14:37
…oreign key (#332)

A polymorphic belongs_to can't be SQL-joined, so the datasource returns a
phantom related object without a primary key. The serializer read the id off
that phantom, producing an empty id that crashes the frontend list view
("Expected id to be a string or number, received null").

Rebuild the linkage id from the foreign key already loaded on the owner record
(no extra query), and omit the data key for unlinked records.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation is loaded

The previous commit fabricated a relationship linkage even when the relation
was never projected (e.g. update/store responses expose only the FK columns,
not the related object). That made the serializer resolve the type-field value
as a collection name and raise "Collection not found".

Guard on super returning nil: rebuild the id only for the phantom object that
list projections return; otherwise keep the prior behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qltysh

qltysh Bot commented Jul 21, 2026

Copy link
Copy Markdown

1 new issue

Tool Category Rule Count
qlty Structure Function with high complexity (count = 5): has_one_relationship 1

return nil if foreign_key.nil? || foreign_type.nil?

primary_key = (field.foreign_key_targets || {})[foreign_type] || 'id'
(object.is_a?(Hash) ? object : {}).merge(primary_key => foreign_key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 5): has_one_relationship [qlty:function-complexity]

…ets lookup

foreign_key_targets is keyed by formatted collection names (Admin::User =>
Admin__User), but the type column stores the raw class name. For a namespaced
target with a custom primary key the lookup missed and fell back to 'id',
storing the FK under the wrong field and still emitting an empty linkage id.
Format the type value before the lookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: comments a bit verbose. The foreign_key_targets keying one justifies the gsub → keep. But # nil means… mostly restates return object, and no extra query is issued. reads as pre-empting a reviewer. Could trim both, wdyt?

The specs only assert the linkage, not the included doc (#332's 2nd symptom, the empty self-link). Could be good to add one with include: 'documentable' asserting included has id => '10'?

…333 review)

Add a spec asserting the compound `included` document builds its id and
self-link from the foreign key (#332's second symptom). Trim the verbose
comments per review, keeping only the non-obvious foreign_key_targets note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bexchauveto

Copy link
Copy Markdown
Member Author

Both done in 618ae03:

  • Trimmed the comments — dropped the nil means… block (now a one-line inline note) and No extra query is issued; kept the foreign_key_targets note since the gsub isn't obvious.
  • Added a spec that serializes with include: 'documentable' and asserts the included resource has id => '10' and self => /forest/Car/10 (the empty self-link symptom). Verified it fails without the fix (got nil).

Full suite green (724 examples).

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok for me 👍

@bexchauveto
bexchauveto merged commit 177b78f into main Jul 21, 2026
48 checks passed
@bexchauveto
bexchauveto deleted the fix/polymorphic-many-to-one-empty-id branch July 21, 2026 14:04
forest-bot added a commit that referenced this pull request Jul 21, 2026
## [1.35.4](v1.35.3...v1.35.4) (2026-07-21)

### Bug Fixes

* **forest_admin_agent:** build PolymorphicManyToOne linkage id from foreign key ([#332](#332)) ([#333](#333)) ([177b78f](177b78f))
@forest-bot

Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.35.4 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PolymorphicManyToOne serializes an empty id in list view → frontend crashes with "Expected id to be a string or number, received null"

3 participants