Module: StandardId::AssociationStrictLoading

Defined in:
lib/standard_id/association_strict_loading.rb

Overview

Resolves the strict_loading: option the gem's own associations declare, from StandardId.config.association_strict_loading.

WHY THIS EXISTS

Apps that set strict_loading_by_default = true globally cannot fix the gem's associations by re-declaring them: they are declared inside StandardId::AccountAssociations, and credentials is a has_many :through, where re-declaration risks ordering breakage. So two consuming apps reached into Rails internals instead —

Account.reflect_on_association(assoc)&.options&.[]=(:strict_loading, false)

— both with a comment asking for exactly this hook. Passing strict_loading: at declaration time is the supported Rails API, and unlike re-declaration it works for the :through association.

WHY A HELPER RATHER THAN strict_loading: config.association_strict_loading

Because nil must mean OMITTED, not false. Rails checks reflection.options.key?(:strict_loading) before consulting the owner:

# ActiveRecord::Associations::Association#violates_strict_loading?
return reflection.strict_loading? if reflection.options.key?(:strict_loading)
owner.strict_loading? && !owner.strict_loading_n_plus_one_only?

so declaring strict_loading: nil would put the key in the options hash and make reflection.strict_loading? return !!nil == false — silently disabling strict loading on every gem association in every app that never asked for it. This returns an empty hash in that case instead, so the option is genuinely absent and the owner's setting still governs.

Constant Summary collapse

GEM_OWNED =

Associations declared with **option, as owner_class_name => [names]. Used by the boot-time consistency check.

Account's are keyed by the configured account class rather than listed here, since the host owns that constant.

{
  "StandardId::Session" => %i[refresh_tokens],
  "StandardId::Identifier" => %i[credentials]
}.freeze
ACCOUNT_ASSOCIATIONS =
%i[
  identifiers credentials sessions refresh_tokens client_applications
].freeze

Class Method Summary collapse

Class Method Details

.mismatched_associations(configured) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/standard_id/association_strict_loading.rb', line 100

def mismatched_associations(configured)
  pairs = GEM_OWNED.flat_map do |class_name, names|
    names.map { |name| [class_name.constantize, name] }
  end

   = begin
    StandardId.
  rescue StandardError
    nil
  end
  pairs += ACCOUNT_ASSOCIATIONS.map { |name| [, name] } if 

  pairs.filter_map do |klass, name|
    reflection = klass.reflect_on_association(name)
    next if reflection.nil?
    next if reflection.options[:strict_loading] == configured

    "#{klass.name}##{name}"
  end
end

.optionHash

Splat into a has_many / has_one / belongs_to declaration:

has_many :sessions, class_name: "StandardId::Session",
       **StandardId::AssociationStrictLoading.option

Returns:

  • (Hash)

    {} when unconfigured, else { strict_loading: <value> }



42
43
44
45
46
47
# File 'lib/standard_id/association_strict_loading.rb', line 42

def option
  value = StandardId.config.association_strict_loading
  return {} if value.nil?

  { strict_loading: value }
end

.verify_consistency!Object

Raise if a declared association disagrees with the configured value.

The included do block in AccountAssociations reads the config when the host's Account class body runs — during autoload, which can happen before an initializer that sets association_strict_loading. The result is silent: strict loading behaves as though the setting were never made, and the app either raises StrictLoadingViolationError deep in a request or quietly N+1s in production. This converts that into a boot failure that names the ordering problem.

Called from the Engine's after_initialize, when both the config and the host's model are settled.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/standard_id/association_strict_loading.rb', line 77

def verify_consistency!
  configured = StandardId.config.association_strict_loading
  return if configured.nil?

  mismatched = mismatched_associations(configured)
  return if mismatched.empty?

  raise StandardId::ConfigurationError, <<~MESSAGE.strip
    StandardId.config.association_strict_loading is #{configured.inspect}, but these
    associations were declared without it: #{mismatched.join(', ')}.

    This means the model class body ran BEFORE the config was set. StandardId's
    associations read the setting at declaration time, so it must be assigned in
    `config/initializers/standard_id.rb` — not in an `after_initialize` block, and
    not anywhere else that can run after your Account class is autoloaded.

    Left unchecked this fails silently: strict loading behaves as though you never
    made the setting, and the app either raises
    ActiveRecord::StrictLoadingViolationError inside a request or quietly N+1s in
    production.
  MESSAGE
end