forest_admin_datasource_active_record 1.34.2 → 1.35.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 +4 -4
- data/lib/forest_admin_datasource_active_record/collection.rb +25 -14
- data/lib/forest_admin_datasource_active_record/utils/active_record_serializer.rb +37 -0
- data/lib/forest_admin_datasource_active_record/utils/query.rb +72 -15
- data/lib/forest_admin_datasource_active_record/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df0d3da32f3e5533efcd50c768ed24a103f7b442e956b48c7a359c07b3d3c721
|
|
4
|
+
data.tar.gz: 1af868e9ed17dc80de89648eb262c9fe960a2e35af5aa2515d7d802bd129557f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 415b589ca9918863682926543313e9c2917baf7736eb4333e876c9aafa9873580ddc6b4c15e9e9c64e7fdefbcea2a4617d9b9212479652d75d1855a277b82615
|
|
7
|
+
data.tar.gz: 7acfbd16b68262aca27a07e52cf2c76d1145f6e0fffc53d923a11a295475c171014d81c615469f6f738f79752475c7475a1fdc5cd36d16595b9beeb92ad58848
|
|
@@ -100,21 +100,32 @@ module ForestAdminDatasourceActiveRecord
|
|
|
100
100
|
source_polymorphic = association.source_reflection&.polymorphic? &&
|
|
101
101
|
association.options[:source_type].present?
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
)
|
|
116
118
|
)
|
|
117
|
-
|
|
119
|
+
else
|
|
120
|
+
add_field(
|
|
121
|
+
association.name.to_s,
|
|
122
|
+
ForestAdminDatasourceToolkit::Schema::Relations::OneToOneSchema.new(
|
|
123
|
+
foreign_collection: format_model_name(association.klass.name),
|
|
124
|
+
origin_key: association.klass.primary_key,
|
|
125
|
+
origin_key_target: @model.primary_key
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
end
|
|
118
129
|
elsif association.inverse_of&.polymorphic?
|
|
119
130
|
add_field(
|
|
120
131
|
association.name.to_s,
|
|
@@ -13,6 +13,7 @@ module ForestAdminDatasourceActiveRecord
|
|
|
13
13
|
# root keeps all its selected columns (attributes + FKs); a related record is restricted to
|
|
14
14
|
# its projected columns, matching the JOINed hydration
|
|
15
15
|
hash = path.empty? || projection.nil? ? base_attributes(object) : projected_columns(object, projection)
|
|
16
|
+
hash = normalize_polymorphic_types(object.class, hash)
|
|
16
17
|
|
|
17
18
|
serialize_associations(object, projection, hash, path) if projection
|
|
18
19
|
|
|
@@ -65,6 +66,7 @@ module ForestAdminDatasourceActiveRecord
|
|
|
65
66
|
|
|
66
67
|
hash = {}
|
|
67
68
|
projection.columns.each { |column| hash[column] = object[meta[:columns][column]] }
|
|
69
|
+
hash = normalize_polymorphic_types(target_model(relation_path), hash)
|
|
68
70
|
projection.relations.each_key do |nested_name|
|
|
69
71
|
hash[nested_name] = hash_joined_relation(projection.relations[nested_name], relation_path + [nested_name])
|
|
70
72
|
end
|
|
@@ -72,8 +74,43 @@ module ForestAdminDatasourceActiveRecord
|
|
|
72
74
|
hash
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
def normalize_polymorphic_types(model_class, hash)
|
|
78
|
+
return hash if model_class.nil?
|
|
79
|
+
|
|
80
|
+
polymorphic_belongs_to(model_class).each do |association|
|
|
81
|
+
stored = hash[association.foreign_type]
|
|
82
|
+
next if stored.nil?
|
|
83
|
+
|
|
84
|
+
hash = hash.merge(association.foreign_type => model_class.polymorphic_class_for(stored).name)
|
|
85
|
+
rescue NameError => e
|
|
86
|
+
warn_unable(association.name, model_class, e)
|
|
87
|
+
end
|
|
88
|
+
hash
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Target model of a JOINed relation path (only belongs_to / has_one :through are ever JOINed).
|
|
92
|
+
def target_model(relation_path)
|
|
93
|
+
relation_path.reduce(object.class) do |model, name|
|
|
94
|
+
model&.reflect_on_association(name.to_sym)&.klass
|
|
95
|
+
end
|
|
96
|
+
rescue NameError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
75
100
|
private
|
|
76
101
|
|
|
102
|
+
def polymorphic_belongs_to(model_class)
|
|
103
|
+
(@polymorphic_belongs_to ||= {})[model_class] ||=
|
|
104
|
+
model_class.reflect_on_all_associations(:belongs_to).select(&:polymorphic?)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def warn_unable(name, model_class, error)
|
|
108
|
+
ActiveSupport::Logger.new($stdout).warn(
|
|
109
|
+
"[ForestAdmin] Unable to normalize polymorphic type of '#{name}' " \
|
|
110
|
+
"in model '#{model_class.name}': #{error.message}. Keeping the stored value."
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
77
114
|
def joined_relation?(relation_path)
|
|
78
115
|
!joined_relations.nil? && joined_relations.key?(relation_path.join('.'))
|
|
79
116
|
end
|
|
@@ -196,6 +196,10 @@ module ForestAdminDatasourceActiveRecord
|
|
|
196
196
|
if collection.model.table_name == @collection.model.table_name
|
|
197
197
|
if one_to_one_relations.include?(relation_schema.type)
|
|
198
198
|
@select << "#{collection.model.table_name}.#{relation_schema.origin_key_target}"
|
|
199
|
+
# foreign_key is an array for a composite-key belongs_to through hop
|
|
200
|
+
Array(root_through_foreign_key(collection, relation_name)).each do |through_fk|
|
|
201
|
+
@select << "#{collection.model.table_name}.#{through_fk}"
|
|
202
|
+
end
|
|
199
203
|
elsif many_to_one_relations.include?(relation_schema.type)
|
|
200
204
|
@select << "#{collection.model.table_name}.#{relation_schema.foreign_key}"
|
|
201
205
|
end
|
|
@@ -225,12 +229,13 @@ module ForestAdminDatasourceActiveRecord
|
|
|
225
229
|
def split_relations
|
|
226
230
|
join_tree = {}
|
|
227
231
|
preload_tree = {}
|
|
228
|
-
|
|
232
|
+
used_joins = { @collection.model.table_name => :root }
|
|
233
|
+
@filter_joined_tables.each { |table| used_joins[table] ||= :filter } # never reuse a filter/sort join
|
|
229
234
|
|
|
230
235
|
@projection.relations.each do |relation_name, sub_projection|
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
|
|
236
|
+
joins = joinable_joins(@collection, relation_name, sub_projection, used_joins)
|
|
237
|
+
if joins
|
|
238
|
+
used_joins.merge!(joins)
|
|
234
239
|
join_tree[relation_name.to_sym] = format_relation_projection(sub_projection)
|
|
235
240
|
collect_joined_selects(@collection, relation_name, sub_projection, [relation_name])
|
|
236
241
|
else
|
|
@@ -271,40 +276,83 @@ module ForestAdminDatasourceActiveRecord
|
|
|
271
276
|
end
|
|
272
277
|
|
|
273
278
|
# Set of tables the subtree adds via JOIN, or nil if any relation in it can't be safely joined.
|
|
274
|
-
def
|
|
275
|
-
target = joinable_target(collection, relation_name
|
|
279
|
+
def joinable_joins(collection, relation_name, sub_projection, used_joins)
|
|
280
|
+
target = joinable_target(collection, relation_name)
|
|
276
281
|
return nil if target.nil?
|
|
277
282
|
|
|
278
|
-
|
|
283
|
+
joins = join_signatures(collection, relation_name)
|
|
284
|
+
return nil if joins.nil? || conflicting?(joins, used_joins)
|
|
285
|
+
|
|
279
286
|
sub_projection.relations.each do |nested_name, nested_projection|
|
|
280
|
-
nested =
|
|
287
|
+
nested = joinable_joins(target, nested_name, nested_projection, used_joins.merge(joins))
|
|
281
288
|
return nil if nested.nil?
|
|
282
289
|
|
|
283
|
-
|
|
290
|
+
joins = joins.merge(nested)
|
|
284
291
|
end
|
|
285
|
-
|
|
292
|
+
joins
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def conflicting?(new_joins, used_joins)
|
|
296
|
+
new_joins.any? { |table, signature| used_joins.key?(table) && used_joins[table] != signature }
|
|
286
297
|
end
|
|
287
298
|
|
|
288
299
|
# The target collection when this hop is safe to collapse into a JOIN, else nil (-> preload).
|
|
289
|
-
def joinable_target(collection, relation_name
|
|
300
|
+
def joinable_target(collection, relation_name)
|
|
290
301
|
relation_schema = collection.schema[:fields][relation_name]
|
|
291
|
-
|
|
292
|
-
# (a has_one child may not be unique)
|
|
293
|
-
return unless relation_schema.type == 'ManyToOne' && relation_schema.respond_to?(:foreign_collection)
|
|
302
|
+
return unless relation_schema.respond_to?(:foreign_collection)
|
|
294
303
|
|
|
295
304
|
# a scoped association applies its scope to the JOIN and may inject raw/unqualified SQL or
|
|
296
305
|
# extra joins (e.g. `belongs_to :x, -> { where('id > ?', 1) }`)
|
|
297
306
|
reflection = collection.model.reflect_on_association(relation_name.to_sym)
|
|
298
307
|
return if reflection.nil? || reflection.scope
|
|
299
308
|
|
|
309
|
+
case relation_schema.type
|
|
310
|
+
when 'ManyToOne' then nil
|
|
311
|
+
when 'OneToOne' then return unless belongs_to_chain_through?(reflection)
|
|
312
|
+
else return
|
|
313
|
+
end
|
|
314
|
+
|
|
300
315
|
target = local_ar_collection(collection.datasource, relation_schema.foreign_collection)
|
|
301
316
|
return if target.nil? || !target.model.default_scopes.empty? # same risk as a scoped association
|
|
302
317
|
return unless same_database?(collection.model, target.model)
|
|
303
|
-
return if used_tables.include?(target.model.table_name) # a table joined twice would be aliased by AR
|
|
304
318
|
|
|
305
319
|
target
|
|
306
320
|
end
|
|
307
321
|
|
|
322
|
+
def join_signatures(collection, relation_name)
|
|
323
|
+
reflection = collection.model.reflect_on_association(relation_name.to_sym)
|
|
324
|
+
if reflection.through_reflection?
|
|
325
|
+
{ reflection.through_reflection.klass.table_name => signature(reflection.through_reflection),
|
|
326
|
+
reflection.klass.table_name => signature(reflection.source_reflection) }
|
|
327
|
+
else
|
|
328
|
+
{ reflection.klass.table_name => signature(reflection) }
|
|
329
|
+
end
|
|
330
|
+
rescue StandardError
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def signature(reflection)
|
|
335
|
+
"#{reflection.active_record.table_name}.#{Array(reflection.foreign_key).join(",")}" \
|
|
336
|
+
"->#{reflection.klass.table_name}.#{Array(join_key(reflection)).join(",")}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def join_key(reflection)
|
|
340
|
+
reflection.respond_to?(:join_primary_key) ? reflection.join_primary_key : reflection.association_primary_key
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def belongs_to_chain_through?(reflection)
|
|
344
|
+
return false unless reflection.through_reflection?
|
|
345
|
+
|
|
346
|
+
through = reflection.through_reflection
|
|
347
|
+
source = reflection.source_reflection
|
|
348
|
+
|
|
349
|
+
through && source && through.belongs_to? && source.belongs_to? &&
|
|
350
|
+
through.scope.nil? && source.scope.nil? && through.klass.default_scopes.empty? &&
|
|
351
|
+
same_database?(reflection.active_record, through.klass)
|
|
352
|
+
rescue StandardError
|
|
353
|
+
false
|
|
354
|
+
end
|
|
355
|
+
|
|
308
356
|
def same_database?(model_a, model_b)
|
|
309
357
|
# compare the pools, not connection_specification_name (only an owner class name, shared across shards)
|
|
310
358
|
model_a.connection_pool == model_b.connection_pool
|
|
@@ -330,6 +378,15 @@ module ForestAdminDatasourceActiveRecord
|
|
|
330
378
|
@query
|
|
331
379
|
end
|
|
332
380
|
|
|
381
|
+
def root_through_foreign_key(collection, relation_name)
|
|
382
|
+
through = collection.model.reflect_on_association(relation_name.to_sym)&.through_reflection
|
|
383
|
+
return unless through&.belongs_to?
|
|
384
|
+
|
|
385
|
+
through.foreign_key
|
|
386
|
+
rescue StandardError
|
|
387
|
+
nil
|
|
388
|
+
end
|
|
389
|
+
|
|
333
390
|
def resolve_field(original_field)
|
|
334
391
|
if original_field.include?(':')
|
|
335
392
|
relation_name, column_name = original_field.split(':')
|