forest_admin_datasource_active_record 1.39.4 → 1.40.1

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: b924a7fc6ab865fa83285d4dc554e0d2124b26c2b998d6c06c3dcfcd357d313d
4
+ data.tar.gz: dfd941d070ddb69f934b43db627d83ed0dac8af12fcf3dbf5f6594bdfaae617d
5
5
  SHA512:
6
- metadata.gz: 1167b2f625cbf6efe207d802cd722d2c66c412e086eab826b1ba0ccc41278cee545e60f44bcd0ccbf179ed8c4485f9634d527fa6eb5694dce21ba0ddac4f9cce
7
- data.tar.gz: a8878e3b5ca60b90fa06b6de164467ee6d3ba7beeff68523b60cb3fb86bb95e4290fde648311f60812362bd4727093ae440b9d19f13c0b16614f378df94ed982
6
+ metadata.gz: 13177d23d5702e9a93d282e09c213c8b99fb2d5ba47828712e3fc67b84a198e67bf65848af806ce58f084c8465bda80ad282c29da73b0413a3e50aac6b38bf0c
7
+ data.tar.gz: 297df24c7a0e70780552f7f0b4712860171930f57cce1c0a7a602732e73ace9ce187e41dde2e8e9048c5796c7674b8a23dd077895bcb05b64cdb96de6e4bc456
@@ -88,6 +88,69 @@ 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
+ # Same criterion as unrepresentable_many_to_many?: a composite primary key can arrive as a
118
+ # real Array or already flattened by Rails into a mangled String -- checking column
119
+ # membership catches both forms, plus a custom primary_key pointing at a non-column, all of
120
+ # which would otherwise hit nil.column_type in build_one_to_one_schema (#379).
121
+ def unrepresentable_one_to_one?(association)
122
+ !association.klass.column_names.include?(association.klass.primary_key) ||
123
+ !@model.column_names.include?(@model.primary_key)
124
+ end
125
+
126
+ def build_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
127
+ ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new(
128
+ foreign_collection: format_model_name(association.klass.name),
129
+ origin_key: through_reflection.foreign_key,
130
+ origin_key_target: through_reflection.join_foreign_key,
131
+ foreign_key: association.join_foreign_key,
132
+ foreign_key_target: association.association_primary_key,
133
+ through_collection: format_model_name(through_reflection.klass.name),
134
+ origin_type_field: is_polymorphic ? through_reflection.type : nil,
135
+ origin_type_value: is_polymorphic ? @model.name : nil,
136
+ foreign_type_field: source_polymorphic ? association.source_reflection.foreign_type : nil,
137
+ foreign_type_value: source_polymorphic ? association.options[:source_type] : nil
138
+ )
139
+ end
140
+
141
+ def add_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
142
+ if unrepresentable_many_to_many?(association, through_reflection)
143
+ warn_unrepresentable_many_to_many(association, through_reflection)
144
+ return
145
+ end
146
+
147
+ add_field(
148
+ association.name.to_s,
149
+ build_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
150
+ )
151
+ warn_identity_join(association, through_reflection) unless valid_many_to_many_source?(association)
152
+ end
153
+
91
154
  # rubocop:disable Metrics/BlockNesting
92
155
  def fetch_associations
93
156
  associations(@model, support_polymorphic_relations: @support_polymorphic_relations).each do |association|
@@ -99,32 +162,23 @@ module ForestAdminDatasourceActiveRecord
99
162
  is_polymorphic = through_reflection.options[:as].present?
100
163
  source_polymorphic = association.source_reflection&.polymorphic? &&
101
164
  association.options[:source_type].present?
165
+ many_to_many_shape = is_polymorphic || source_polymorphic
102
166
 
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
- )
167
+ if many_to_many_shape
168
+ add_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
169
+ elsif unrepresentable_one_to_one?(association)
170
+ warn_unrepresentable_one_to_one(association, through_reflection)
119
171
  else
120
172
  add_field(
121
173
  association.name.to_s,
122
174
  ForestAdminDatasourceToolkit::Schema::Relations::OneToOneSchema.new(
123
175
  foreign_collection: format_model_name(association.klass.name),
124
176
  origin_key: association.klass.primary_key,
125
- origin_key_target: @model.primary_key
177
+ origin_key_target: @model.primary_key,
178
+ is_read_only: true
126
179
  )
127
180
  )
181
+ warn_readonly_identity_join(association, through_reflection)
128
182
  end
129
183
  elsif association.inverse_of&.polymorphic?
130
184
  add_field(
@@ -186,21 +240,7 @@ module ForestAdminDatasourceActiveRecord
186
240
  source_polymorphic = association.source_reflection&.polymorphic? &&
187
241
  association.options[:source_type].present?
188
242
 
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
- )
243
+ add_many_to_many_field(association, through_reflection, is_polymorphic, source_polymorphic)
204
244
  elsif association.inverse_of&.polymorphic?
205
245
  add_field(
206
246
  association.name.to_s,
@@ -303,6 +343,65 @@ module ForestAdminDatasourceActiveRecord
303
343
  klass
304
344
  end
305
345
 
346
+ def warn_unrepresentable_many_to_many(association, through_reflection)
347
+ logger = ActiveSupport::Logger.new($stdout)
348
+ logger.warn(
349
+ "[ForestAdmin] ⚠️ Skipping association '#{association.name}' in model '#{@model.name}': " \
350
+ 'this relation cannot be represented as a Forest Admin many-to-many -- its join keys must ' \
351
+ 'each be a single existing column, on the through collection ' \
352
+ "'#{format_model_name(through_reflection.klass.name)}' or the foreign collection " \
353
+ "'#{format_model_name(association.klass.name)}' (composite/query_constraints keys are " \
354
+ 'not supported).'
355
+ )
356
+ end
357
+
358
+ # join_foreign_key/association_primary_key are guaranteed to be single, real, existing
359
+ # columns here: unrepresentable_many_to_many? above already checks column membership for
360
+ # both (and for origin_key) before this is ever called.
361
+ #
362
+ # Deliberately a diagnostic, not a deprecation notice: this shape is common (any
363
+ # has_many/has_one :through sourced from a plain has_one/has_many, not just polymorphic
364
+ # ones) and the relation is kept published to avoid narrowing the schema -- see #370.
365
+ # No removal is promised or planned.
366
+ def warn_identity_join(association, through_reflection)
367
+ logger = ActiveSupport::Logger.new($stdout)
368
+ logger.warn(
369
+ "[ForestAdmin] ⚠️ Association '#{association.name}' in model '#{@model.name}' is published " \
370
+ "as a many-to-many joining '#{format_model_name(through_reflection.klass.name)}'." \
371
+ "'#{association.join_foreign_key}' to " \
372
+ "'#{format_model_name(association.klass.name)}'.'#{association.association_primary_key}': " \
373
+ "its source association is not a belongs_to, so this join uses each side's own identifier " \
374
+ 'rather than a dedicated foreign key column -- see #370 for background.'
375
+ )
376
+ end
377
+
378
+ def warn_unrepresentable_one_to_one(association, through_reflection)
379
+ logger = ActiveSupport::Logger.new($stdout)
380
+ logger.warn(
381
+ "[ForestAdmin] ⚠️ Skipping association '#{association.name}' in model '#{@model.name}': " \
382
+ "this has_one :through (via '#{format_model_name(through_reflection.klass.name)}') can't be " \
383
+ 'represented as a Forest Admin one-to-one -- one of its endpoints has a composite primary key.'
384
+ )
385
+ end
386
+
387
+ # OneToOneSchema has no through_collection, so this publishes each side's own primary key
388
+ # as a placeholder join -- the columns exist on both sides, so GeneratorField's field
389
+ # lookups don't raise, but the join isn't meaningful data (it matches two unrelated
390
+ # sequences), which is exactly why it's marked read-only rather than left writable through
391
+ # what's really the foreign collection's own primary key (#379).
392
+ def warn_readonly_identity_join(association, through_reflection)
393
+ logger = ActiveSupport::Logger.new($stdout)
394
+ foreign_key = association.klass.primary_key
395
+ origin_key = @model.primary_key
396
+ logger.warn(
397
+ "[ForestAdmin] ⚠️ Association '#{association.name}' in model '#{@model.name}' is published " \
398
+ "as a read-only one-to-one joining '#{format_model_name(association.klass.name)}'.'#{foreign_key}' " \
399
+ "to this model's own '#{origin_key}': it's a has_one chained through " \
400
+ "'#{format_model_name(through_reflection.klass.name)}', which OneToOneSchema can't express as a " \
401
+ 'real two-hop join -- see #379 for background.'
402
+ )
403
+ end
404
+
306
405
  def warn_missing_polymorphic_columns(association)
307
406
  missing_columns = []
308
407
  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.1"
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.1
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-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord