forest_admin_datasource_active_record 1.39.4 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b449c8be1d6a6e9cc36ee341a071615ee75d85854d1c706608fcdbd98d48c58f
4
- data.tar.gz: 59d19e7d934051127d598120ceb879d1cd2e6b673f43f9c1c04ade23c60ca8e0
3
+ metadata.gz: 900ccccbc0c8a7ae95c5eee13a67dca16bbad7add14ac849381a95246878393d
4
+ data.tar.gz: 7fb49021e33e83ed9cb8e01a39a813b4e261fae73e8d6d32e936092f843e7aad
5
5
  SHA512:
6
- metadata.gz: 1167b2f625cbf6efe207d802cd722d2c66c412e086eab826b1ba0ccc41278cee545e60f44bcd0ccbf179ed8c4485f9634d527fa6eb5694dce21ba0ddac4f9cce
7
- data.tar.gz: a8878e3b5ca60b90fa06b6de164467ee6d3ba7beeff68523b60cb3fb86bb95e4290fde648311f60812362bd4727093ae440b9d19f13c0b16614f378df94ed982
6
+ metadata.gz: dcda7837f4a0d0b6658c10ae3245d9e31ab3933d7302b414e7560716f1cf6ccfb261f4ccc1e93dac131384cfb927cca9b5cb412c6ebace19cff563b24398d67b
7
+ data.tar.gz: 43d723551915aace679d91971d5264f70e91f2adbd4a5bde9bc153884a69364292d5fbb7d33e2584794ca5f7009358ee783cdf930b2521035e5ef12e31b70f63
@@ -88,6 +88,60 @@ module ForestAdminDatasourceActiveRecord
88
88
  %i[destroy destroy_async delete_all].include?(association.options[:dependent])
89
89
  end
90
90
 
91
+ # join_foreign_key can fall back to the through model's own PK (see
92
+ # valid_many_to_many_source? below); that PK usually still exists as a column, so
93
+ # "column missing" alone doesn't catch every unrepresentable case -- a composite key is
94
+ # unrepresentable too, even when every individual column exists: GeneratorField looks
95
+ # fields up by the raw key, so a composite key never resolves. A composite key can reach
96
+ # us either as a real Array, or (via an explicit primary_key: option on the source)
97
+ # already flattened by Rails into a mangled String like '["a", "b"]' -- neither is ever a
98
+ # real column name, so checking column membership catches both forms uniformly, on
99
+ # whichever collection each key is actually resolved against downstream (#370).
100
+ def unrepresentable_many_to_many?(association, through_reflection)
101
+ [
102
+ [association.join_foreign_key, through_reflection.klass],
103
+ [through_reflection.foreign_key, through_reflection.klass],
104
+ [association.association_primary_key, association.klass]
105
+ ].any? { |key, klass| !klass.column_names.include?(key) }
106
+ end
107
+
108
+ # True only for a belongs_to source: ThroughReflection#join_foreign_key delegates to
109
+ # the source reflection, and only belongs_to overrides it to return a real FK column.
110
+ # Any other source macro falls back to AssociationReflection#active_record_primary_key
111
+ # -- the through model's own primary key column (honoring a custom primary_key:, and
112
+ # possibly composite), not a genuine join column (#370).
113
+ def valid_many_to_many_source?(association)
114
+ association.source_reflection&.belongs_to?
115
+ end
116
+
117
+ def build_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
118
+ ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new(
119
+ foreign_collection: format_model_name(association.klass.name),
120
+ origin_key: through_reflection.foreign_key,
121
+ origin_key_target: through_reflection.join_foreign_key,
122
+ foreign_key: association.join_foreign_key,
123
+ foreign_key_target: association.association_primary_key,
124
+ through_collection: format_model_name(through_reflection.klass.name),
125
+ origin_type_field: is_polymorphic ? through_reflection.type : nil,
126
+ origin_type_value: is_polymorphic ? @model.name : nil,
127
+ foreign_type_field: source_polymorphic ? association.source_reflection.foreign_type : nil,
128
+ foreign_type_value: source_polymorphic ? association.options[:source_type] : nil
129
+ )
130
+ end
131
+
132
+ def add_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
133
+ if unrepresentable_many_to_many?(association, through_reflection)
134
+ warn_unrepresentable_many_to_many(association, through_reflection)
135
+ return
136
+ end
137
+
138
+ add_field(
139
+ association.name.to_s,
140
+ build_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
141
+ )
142
+ warn_identity_join(association, through_reflection) unless valid_many_to_many_source?(association)
143
+ end
144
+
91
145
  # rubocop:disable Metrics/BlockNesting
92
146
  def fetch_associations
93
147
  associations(@model, support_polymorphic_relations: @support_polymorphic_relations).each do |association|
@@ -99,23 +153,10 @@ module ForestAdminDatasourceActiveRecord
99
153
  is_polymorphic = through_reflection.options[:as].present?
100
154
  source_polymorphic = association.source_reflection&.polymorphic? &&
101
155
  association.options[:source_type].present?
156
+ many_to_many_shape = is_polymorphic || source_polymorphic
102
157
 
103
- if is_polymorphic || source_polymorphic
104
- add_field(
105
- association.name.to_s,
106
- ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new(
107
- foreign_collection: format_model_name(association.klass.name),
108
- origin_key: through_reflection.foreign_key,
109
- origin_key_target: through_reflection.join_foreign_key,
110
- foreign_key: association.join_foreign_key,
111
- foreign_key_target: association.association_primary_key,
112
- through_collection: format_model_name(through_reflection.klass.name),
113
- origin_type_field: is_polymorphic ? through_reflection.type : nil,
114
- origin_type_value: is_polymorphic ? @model.name : nil,
115
- foreign_type_field: source_polymorphic ? association.source_reflection.foreign_type : nil,
116
- foreign_type_value: source_polymorphic ? association.options[:source_type] : nil
117
- )
118
- )
158
+ if many_to_many_shape
159
+ add_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
119
160
  else
120
161
  add_field(
121
162
  association.name.to_s,
@@ -186,21 +227,7 @@ module ForestAdminDatasourceActiveRecord
186
227
  source_polymorphic = association.source_reflection&.polymorphic? &&
187
228
  association.options[:source_type].present?
188
229
 
189
- add_field(
190
- association.name.to_s,
191
- ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new(
192
- foreign_collection: format_model_name(association.klass.name),
193
- origin_key: through_reflection.foreign_key,
194
- origin_key_target: through_reflection.join_foreign_key,
195
- foreign_key: association.join_foreign_key,
196
- foreign_key_target: association.association_primary_key,
197
- through_collection: format_model_name(through_reflection.klass.name),
198
- origin_type_field: is_polymorphic ? through_reflection.type : nil,
199
- origin_type_value: is_polymorphic ? @model.name : nil,
200
- foreign_type_field: source_polymorphic ? association.source_reflection.foreign_type : nil,
201
- foreign_type_value: source_polymorphic ? association.options[:source_type] : nil
202
- )
203
- )
230
+ add_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
204
231
  elsif association.inverse_of&.polymorphic?
205
232
  add_field(
206
233
  association.name.to_s,
@@ -303,6 +330,38 @@ module ForestAdminDatasourceActiveRecord
303
330
  klass
304
331
  end
305
332
 
333
+ def warn_unrepresentable_many_to_many(association, through_reflection)
334
+ logger = ActiveSupport::Logger.new($stdout)
335
+ logger.warn(
336
+ "[ForestAdmin] ⚠️ Skipping association '#{association.name}' in model '#{@model.name}': " \
337
+ 'this relation cannot be represented as a Forest Admin many-to-many -- its join keys must ' \
338
+ 'each be a single existing column, on the through collection ' \
339
+ "'#{format_model_name(through_reflection.klass.name)}' or the foreign collection " \
340
+ "'#{format_model_name(association.klass.name)}' (composite/query_constraints keys are " \
341
+ 'not supported).'
342
+ )
343
+ end
344
+
345
+ # join_foreign_key/association_primary_key are guaranteed to be single, real, existing
346
+ # columns here: unrepresentable_many_to_many? above already checks column membership for
347
+ # both (and for origin_key) before this is ever called.
348
+ #
349
+ # Deliberately a diagnostic, not a deprecation notice: this shape is common (any
350
+ # has_many/has_one :through sourced from a plain has_one/has_many, not just polymorphic
351
+ # ones) and the relation is kept published to avoid narrowing the schema -- see #370.
352
+ # No removal is promised or planned.
353
+ def warn_identity_join(association, through_reflection)
354
+ logger = ActiveSupport::Logger.new($stdout)
355
+ logger.warn(
356
+ "[ForestAdmin] ⚠️ Association '#{association.name}' in model '#{@model.name}' is published " \
357
+ "as a many-to-many joining '#{format_model_name(through_reflection.klass.name)}'." \
358
+ "'#{association.join_foreign_key}' to " \
359
+ "'#{format_model_name(association.klass.name)}'.'#{association.association_primary_key}': " \
360
+ "its source association is not a belongs_to, so this join uses each side's own identifier " \
361
+ 'rather than a dedicated foreign key column -- see #370 for background.'
362
+ )
363
+ end
364
+
306
365
  def warn_missing_polymorphic_columns(association)
307
366
  missing_columns = []
308
367
  missing_columns << association.foreign_key unless schema[:fields][association.foreign_key]
@@ -1,3 +1,3 @@
1
1
  module ForestAdminDatasourceActiveRecord
2
- VERSION = "1.39.4"
2
+ VERSION = "1.40.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_datasource_active_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.39.4
4
+ version: 1.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-08-26 00:00:00.000000000 Z
12
+ date: 2026-08-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord