diff --git a/CHANGELOG.md b/CHANGELOG.md index ad64d3c..00d6ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.13.3 - 2026-08-18 + +- Stop loading `ActiveRecord::Base` when the gem is required. The engine now + loads `SolidObjects::Record` from an `ActiveSupport.on_load(:active_record)` + hook, so a host application keeps the normal timing of its own + `on_load(:active_record)` and `on_load(:active_record_encryption)` hooks. An + application that assigns its Active Record encryption keys in + `config/initializers` no longer loses them. +- Apply the configured `connects_to` in the record class body, so the + connection follows the class through a development reload. + ## 0.13.2 - 2026-08-17 - Accept a `key:` on `schedule`, naming a reminder for the item it is waiting diff --git a/Gemfile.lock b/Gemfile.lock index e4f2665..e7b922f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - solid_objects (0.13.2) + solid_objects (0.13.3) actioncable (>= 8.0) actionpack (>= 8.0) actionview (>= 8.0) @@ -384,7 +384,7 @@ CHECKSUMS rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 - solid_objects (0.13.2) + solid_objects (0.13.3) sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b diff --git a/app/models/solid_objects/record.rb b/app/models/solid_objects/record.rb index d080067..7a50867 100644 --- a/app/models/solid_objects/record.rb +++ b/app/models/solid_objects/record.rb @@ -13,5 +13,7 @@ def configure_connection connects_to(**connection_configuration) end end + + configure_connection end end diff --git a/lib/solid_objects/engine.rb b/lib/solid_objects/engine.rb index f0efca8..26ddaef 100644 --- a/lib/solid_objects/engine.rb +++ b/lib/solid_objects/engine.rb @@ -1,9 +1,9 @@ # rbs_inline: enabled -require_relative "../../app/models/solid_objects/record" - module SolidObjects class Engine < ::Rails::Engine + RECORD_PATH = File.expand_path("../../app/models/solid_objects/record.rb", __dir__) + isolate_namespace SolidObjects config.generators do |generators| @@ -16,7 +16,9 @@ class Engine < ::Rails::Engine end initializer "solid_objects.database", after: :load_config_initializers do - SolidObjects::Record.configure_connection + ActiveSupport.on_load(:active_record) do + require RECORD_PATH + end end initializer "solid_objects.helpers" do diff --git a/lib/solid_objects/version.rb b/lib/solid_objects/version.rb index 700f48f..0562095 100644 --- a/lib/solid_objects/version.rb +++ b/lib/solid_objects/version.rb @@ -1,5 +1,5 @@ # rbs_inline: enabled module SolidObjects - VERSION = "0.13.2" + VERSION = "0.13.3" end diff --git a/sig/generated/lib/solid_objects/engine.rbs b/sig/generated/lib/solid_objects/engine.rbs index 81f9cdd..bca97db 100644 --- a/sig/generated/lib/solid_objects/engine.rbs +++ b/sig/generated/lib/solid_objects/engine.rbs @@ -2,6 +2,8 @@ module SolidObjects class Engine < ::Rails::Engine + RECORD_PATH: untyped + include SolidObjects::ActorHelper end end diff --git a/test/dummy/config/initializers/encryption_keys.rb b/test/dummy/config/initializers/encryption_keys.rb new file mode 100644 index 0000000..042789a --- /dev/null +++ b/test/dummy/config/initializers/encryption_keys.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Rails.application.config.active_record.encryption.primary_key = + "solid-objects-dummy-primary-key" +Rails.application.config.active_record.encryption.deterministic_key = + "solid-objects-dummy-deterministic-key" +Rails.application.config.active_record.encryption.key_derivation_salt = + "solid-objects-dummy-key-derivation-salt" diff --git a/test/dummy/config/initializers/solid_objects_connects_to.rb b/test/dummy/config/initializers/solid_objects_connects_to.rb new file mode 100644 index 0000000..1923822 --- /dev/null +++ b/test/dummy/config/initializers/solid_objects_connects_to.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +if ENV["SOLID_OBJECTS_DUMMY_CONNECTS_TO"] + SolidObjects.configure do |config| + config.connects_to = { database: { writing: :primary } } + end +end diff --git a/test/dummy/connects_to_check.rb b/test/dummy/connects_to_check.rb new file mode 100644 index 0000000..555108d --- /dev/null +++ b/test/dummy/connects_to_check.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +ENV["RAILS_ENV"] = "test" +ENV["SOLID_OBJECTS_DUMMY_CONNECTS_TO"] = "1" + +require_relative "config/environment" + +Rails.application.eager_load! + +puts ActiveRecord::Base.connection_handler.connection_pool_names.sort.join(" ") diff --git a/test/dummy/encryption_configuration_check.rb b/test/dummy/encryption_configuration_check.rb new file mode 100644 index 0000000..6f04d24 --- /dev/null +++ b/test/dummy/encryption_configuration_check.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +ENV["RAILS_ENV"] = "test" + +require_relative "config/environment" + +puts ActiveRecord::Encryption.config.primary_key diff --git a/test/integration/active_record_load_timing_test.rb b/test/integration/active_record_load_timing_test.rb new file mode 100644 index 0000000..1121971 --- /dev/null +++ b/test/integration/active_record_load_timing_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "test_helper" +require "open3" + +# Requiring the gem must not load ActiveRecord::Base. A host application and +# other gems register `ActiveSupport.on_load(:active_record)` hooks from a +# railtie initializer. A hook that is registered after the constant is already +# loaded runs at once, before the `config/initializers` files that supply its +# configuration, so the timing of the load decides whether that configuration +# arrives at all. +class ActiveRecordLoadTimingTest < ActiveSupport::TestCase + test "requiring the gem leaves the active record load hooks deferred" do + assert_equal "deferred", probe_result, + "the gem loads ActiveRecord::Base at require time, so host application hooks run too early" + end + + # The reported breakage: an application that assigns its encryption keys in + # `config/initializers` lost them, because the railtie hook that reads them + # had already run. + test "an application initializer configures Active Record encryption" do + output, error_output, status = Open3.capture3( + Gem.ruby, + File.expand_path("../dummy/encryption_configuration_check.rb", __dir__) + ) + + assert status.success?, error_output + assert_equal "solid-objects-dummy-primary-key", output.strip + end + + private + + # Computed in a fresh process, because this process loaded Active Record long + # before the question was asked. + def probe_result + output, error_output, status = Open3.capture3( + { "BUNDLE_GEMFILE" => gem_root("Gemfile") }, + Gem.ruby, + "-e", + probe, + chdir: gem_root(".") + ) + assert status.success?, error_output + output.strip + end + + def probe + <<~RUBY + require "rails" + require "active_record/railtie" + require "solid_objects" + + fired = false + ActiveSupport.on_load(:active_record) { fired = true } + puts(fired ? "immediate" : "deferred") + RUBY + end + + def gem_root(path) + File.expand_path("../../#{path}", __dir__) + end +end diff --git a/test/integration/engine_test.rb b/test/integration/engine_test.rb index 0df49cc..f471a79 100644 --- a/test/integration/engine_test.rb +++ b/test/integration/engine_test.rb @@ -28,6 +28,20 @@ class EngineTest < ActiveSupport::TestCase assert_equal "/solid_objects/components", output.strip end + # The configuration arrives in `config/initializers`, and the record class + # reads it when it loads, which is after those files run. + test "connects the record class to the configured database" do + command = [ + Gem.ruby, + File.expand_path("../dummy/connects_to_check.rb", __dir__) + ] + + output, error_output, status = Open3.capture3(*command) + + assert status.success?, error_output + assert_equal "ActiveRecord::Base SolidObjects::Record", output.strip + end + test "packages the morph refresh browser module" do specification = Gem::Specification.load( File.expand_path("../../solid_objects.gemspec", __dir__)