Skip to content

Handle deleted entities - #2

Merged
sammyapoex merged 12 commits into
mainfrom
handle-deleted-entities
Sep 25, 2026
Merged

sammyapoex merged 12 commits into
mainfrom
handle-deleted-entities

Conversation

@sammyapoex

@sammyapoex sammyapoex commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Why

An entity points at its record polymorphically (flippable_type / flippable_id), so there is no foreign key to remove it when the record is deleted. Such an entity then breaks the feature's entities page: display_entity(entity.flippable) gets nil and raises on the registered_entities.fetch("NilClass") lookup. The page cannot be opened, so the stale entity cannot be removed from the UI either.

What

The UI no longer raises on a deleted record

  • Flipside.display_entity also accepts a Flipside::Entity row. A deleted record is shown as User #42 (deleted), and a record whose class is no longer registered as User #42. Both keep their remove button.

Entities are cleaned up when a record is destroyed

  • New Flipside::Flippable concern with flipside_entity and flipside_role macros, so a model registers itself instead of being registered from an initializer.
  • flipside_entity adds a has_many :flipside_entities, dependent: :delete_all, so destroying a record removes its entities.
  • A class using the macros must be listed in Flipside.flippables. Flipside loads the listed classes on demand, so the UI finds them even when the app does not eager load. It raises if a class uses a macro without being listed, or is listed but calls neither macro.

Existing orphans can be removed

  • Flipside.prune_orphaned_entities deletes entities whose record, or whose class, no longer exists, and returns how many it deleted. This covers records deleted without callbacks (delete, delete_all, SQL) and anything left over from before this change.

Smaller changes

  • Registering the same role twice no longer lists it twice.
  • Flipside.register_entity and Flipside.register_role are deprecated in favour of the macros and will be removed in 1.0. They still work. In a Rails 7.1+ app the warnings follow the app's own deprecation settings (Rails.application.deprecators[:flipside]).
  • README: documents the macros, Flipside.flippables, registering a class from another gem through to_prepare, and upgrading from register_*. It also fixes some heading levels.
  • CHANGELOG: [Unreleased] entry.

Upgrading

Nothing breaks. register_entity and register_role keep working and only warn. To move over:

# config/initializers/flipside.rb
Flipside.flippables = %w[User]

# app/models/user.rb
class User < ApplicationRecord
  include Flipside::Flippable

  flipside_entity search_by: :name, display_as: :name
  flipside_role :admin?
end

After that, run Flipside.prune_orphaned_entities once to clear out entities that were orphaned before the upgrade.

Tests

  • spec/flipside/flippable_spec.rb: the macros register, destroying a record removes its entities, both flippables checks raise, and a listed class that is not loaded yet gets loaded.
  • spec/flipside/web_spec.rb: the entities page renders a deleted record, with its remove form.
  • spec/flipside_spec.rb: prune_orphaned_entities, the deprecation warnings, a role registered twice, and the new display_entity cases.

🤖 Generated with Claude Code

Sammy Henningsson and others added 7 commits September 25, 2026 10:44
A polymorphic flippable cannot carry a foreign key, so the host app can
delete the record behind an entity without Flipside noticing. The
entities page then passed nil to display_entity, which looked up the
registration by nil.class and raised KeyError (key not found:
"NilClass"), taking down the page that holds the Remove button.

display_entity now also accepts the Flipside::Entity join row, which
always knows the flippable type and id, and labels a missing record
(or a class that is no longer registered) instead of raising. Passing
a record directly still works.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entities point at their record polymorphically, so the database cannot
remove them when the record goes away. Host apps can now include
Flipside::Flippable in an entity model to delete its entity rows when a
record is destroyed.

It is an explicit include rather than something register_entity wires up:
registration runs in an initializer before models load, and patching a
class from outside would be lost on the next Zeitwerk reload.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Removes entities left behind by records deleted without callbacks, or
before Flipside::Flippable was included, and by entity classes that no
longer exist. Ids are plucked rather than joined so it also works when a
flippable model lives in another database.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
register_role appended to a list, so registering the same class and
method again listed the role twice in the UI. Key registered roles by
method name so a repeated registration replaces the previous one, which
lets registration run again whenever a class is reloaded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Models can now declare how they are used as entities and roles next to
the code they describe:

  class User < ApplicationRecord
    include Flipside::Flippable

    flipside_entity search_by: :name, display_as: :name
    flipside_role :admin?
  end

The macros register when the class loads, which in an app that does not
eager load is only once something references it. The initializer
therefore lists the class names in Flipside.flippables, and Flipside
constantizes them before the UI reads the registry. That loads them the
same way whether or not the app eager loads, keeps the gem free of Rails
hooks, and picks up a reloaded class. A macro in an unlisted class, or a
listed class that registers nothing, raises instead of appearing in one
environment only.

flipside_entity now also declares the association that removes a
record's entities on destroy, so including the concern no longer does it
on its own. register_entity and register_role remain for classes the app
cannot edit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The reported 500 came from rendering the entities page, which the
display_entity specs do not reach. Request the page through the Roda app
with an entity whose record is gone, and check that it renders with the
Remove form.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sammyapoex sammyapoex self-assigned this Sep 25, 2026
Flipside::Flippable is now the way to register entities and roles, so
the initializer API is deprecated for removal in 1.0.

Warnings go through a Flipside deprecator, which a Railtie hands to the
host app on Rails 7.1+ so its deprecation settings apply to them. The
macros register through add_registered_entity and add_registered_role
instead, so only the deprecated entry points warn, and removing them
later leaves the macros untouched.

The README now documents the macros, registering a class the app cannot
edit from to_prepare, and how to move off the deprecated methods.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sammy Henningsson and others added 4 commits September 25, 2026 14:32
Rails stores the base class in flippable_type, so an entity for an Admin
< User record was looked up as "User" and shown as "User #id" when only
Admin calls flipside_entity. Look up the record's class instead, the same
key display_flippable uses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A soft-deleting default scope hid records from the existence check, so
prune_orphaned_entities deleted their entities for good and a restored
record came back without its features.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
entity.flippable raises NameError when flippable_type no longer names a
class, so the entities page still failed for the rows
prune_orphaned_entities deletes for that reason. Show them as deleted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The macros raised unless their class was already listed, but a model
referenced from an initializer that runs before
config/initializers/flipside.rb loads while the list is still empty,
which failed boot for a correctly configured app. The macros now record
their class, and load_flippables raises for any that is not listed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sammyapoex
sammyapoex merged commit 4833ef5 into main Sep 25, 2026
1 check passed
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