brick 1.0.24 → 1.0.27
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/brick/config.rb +25 -0
- data/lib/brick/extensions.rb +178 -131
- data/lib/brick/frameworks/rails/engine.rb +93 -47
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +61 -4
- data/lib/generators/brick/install_generator.rb +79 -15
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '097fb194c2b1323cd051669f362d86492d9c768f1cbccf06c2fdc45a7f266e1e'
|
4
|
+
data.tar.gz: 72746f153d073256b8be063e90c28da85a50e11865250f2570d501ff320431b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83d0c40c0af17d314d6b4cd7ab0e312bce1c8d57bdd0992da1b54fa8a3e829852c6fd800c34f3bd35f3a3306bda736dc99d14af19009926bf65971a5d4904717
|
7
|
+
data.tar.gz: c82b9c31ffe3a00e701e22a3f69c85e7b60e0b3b6d24e95df7fe3f8afb85b00ab7515e0befab7dd37e8a25924274e663127b953dab8eb64041d834aa39de4bc1
|
data/lib/brick/config.rb
CHANGED
@@ -97,6 +97,15 @@ module Brick
|
|
97
97
|
@mutex.synchronize { @has_ones = hos }
|
98
98
|
end
|
99
99
|
|
100
|
+
# Polymorphic associations
|
101
|
+
def polymorphics
|
102
|
+
@mutex.synchronize { @polymorphics }
|
103
|
+
end
|
104
|
+
|
105
|
+
def polymorphics=(polys)
|
106
|
+
@mutex.synchronize { @polymorphics = polys }
|
107
|
+
end
|
108
|
+
|
100
109
|
def model_descrips
|
101
110
|
@mutex.synchronize { @model_descrips ||= {} }
|
102
111
|
end
|
@@ -113,6 +122,22 @@ module Brick
|
|
113
122
|
@mutex.synchronize { @sti_namespace_prefixes = prefixes }
|
114
123
|
end
|
115
124
|
|
125
|
+
def schema_to_analyse
|
126
|
+
@mutex.synchronize { @schema_to_analyse }
|
127
|
+
end
|
128
|
+
|
129
|
+
def schema_to_analyse=(schema)
|
130
|
+
@mutex.synchronize { @schema_to_analyse = schema }
|
131
|
+
end
|
132
|
+
|
133
|
+
def default_route_fallback
|
134
|
+
@mutex.synchronize { @default_route_fallback }
|
135
|
+
end
|
136
|
+
|
137
|
+
def default_route_fallback=(resource_name)
|
138
|
+
@mutex.synchronize { @default_route_fallback = resource_name }
|
139
|
+
end
|
140
|
+
|
116
141
|
def skip_database_views
|
117
142
|
@mutex.synchronize { @skip_database_views }
|
118
143
|
end
|
data/lib/brick/extensions.rb
CHANGED
@@ -74,8 +74,7 @@ module ActiveRecord
|
|
74
74
|
dsl
|
75
75
|
end
|
76
76
|
|
77
|
-
|
78
|
-
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
|
77
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false)
|
79
78
|
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
80
79
|
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
81
80
|
members = []
|
@@ -87,12 +86,17 @@ module ActiveRecord
|
|
87
86
|
if bracket_name
|
88
87
|
if ch == ']' # Time to process a bracketed thing?
|
89
88
|
parts = bracket_name.split('.')
|
90
|
-
first_parts = parts[0..-2].map
|
89
|
+
first_parts = parts[0..-2].map do |part|
|
90
|
+
klass = klass.reflect_on_association(part_sym = part.to_sym).klass
|
91
|
+
part_sym
|
92
|
+
end
|
91
93
|
parts = prefix + first_parts + [parts[-1]]
|
92
94
|
if parts.length > 1
|
93
|
-
|
94
|
-
|
95
|
-
|
95
|
+
unless is_polymorphic
|
96
|
+
s = build_array
|
97
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
98
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
99
|
+
end
|
96
100
|
translations[parts[0..-2].join('.')] = klass
|
97
101
|
end
|
98
102
|
members << parts
|
@@ -191,7 +195,10 @@ module ActiveRecord
|
|
191
195
|
private
|
192
196
|
|
193
197
|
def self._brick_get_fks
|
194
|
-
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.
|
198
|
+
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.each_with_object([]) do |bt, s|
|
199
|
+
s << bt.foreign_key
|
200
|
+
s << bt.foreign_type if bt.polymorphic?
|
201
|
+
end
|
195
202
|
end
|
196
203
|
end
|
197
204
|
|
@@ -279,23 +286,6 @@ module ActiveRecord
|
|
279
286
|
# , is_add_bts, is_add_hms
|
280
287
|
)
|
281
288
|
is_add_bts = is_add_hms = true
|
282
|
-
wheres = {}
|
283
|
-
has_hm = false
|
284
|
-
params.each do |k, v|
|
285
|
-
case (ks = k.split('.')).length
|
286
|
-
when 1
|
287
|
-
next unless klass._brick_get_fks.include?(k)
|
288
|
-
when 2
|
289
|
-
assoc_name = ks.first.to_sym
|
290
|
-
# Make sure it's a good association name and that the model has that column name
|
291
|
-
next unless (assoc = klass.reflect_on_association(assoc_name))&.klass&.columns&.map(&:name)&.include?(ks.last)
|
292
|
-
|
293
|
-
# There is some potential for duplicates when there is an HM-based where in play. De-duplicate if so.
|
294
|
-
has_hm ||= assoc.macro == :has_many
|
295
|
-
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
296
|
-
end
|
297
|
-
wheres[k] = v.split(',')
|
298
|
-
end
|
299
289
|
|
300
290
|
# %%% Skip the metadata columns
|
301
291
|
if selects&.empty? # Default to all columns
|
@@ -309,8 +299,14 @@ module ActiveRecord
|
|
309
299
|
if is_add_bts || is_add_hms
|
310
300
|
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
311
301
|
bts.each do |_k, bt|
|
302
|
+
next if bt[2] # Polymorphic?
|
303
|
+
|
312
304
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
313
|
-
bt_descrip[bt.first] =
|
305
|
+
bt_descrip[bt.first] = if bt[1].is_a?(Array)
|
306
|
+
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
|
307
|
+
else
|
308
|
+
{ bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
|
309
|
+
end
|
314
310
|
end
|
315
311
|
skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
|
316
312
|
hms.each do |k, hm|
|
@@ -328,7 +324,7 @@ module ActiveRecord
|
|
328
324
|
when 2
|
329
325
|
assoc_name = ks.first.to_sym
|
330
326
|
# Make sure it's a good association name and that the model has that column name
|
331
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.
|
327
|
+
next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
|
332
328
|
|
333
329
|
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
334
330
|
end
|
@@ -344,22 +340,31 @@ module ActiveRecord
|
|
344
340
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
345
341
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
346
342
|
bt_columns = bt_descrip.each_with_object([]) do |v, s|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
id_for_tables[v.first] << id_alias
|
353
|
-
end
|
354
|
-
v.last << id_for_tables[v.first]
|
355
|
-
end
|
356
|
-
if (col_name = v.last[1].last&.last)
|
343
|
+
v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
|
344
|
+
next if chains[k1].nil?
|
345
|
+
|
346
|
+
tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
|
347
|
+
# if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
|
357
348
|
field_tbl_name = nil
|
358
|
-
|
349
|
+
v1.map { |x|
|
350
|
+
[translations[x[0..-2].map(&:to_s).join('.')], x.last]
|
351
|
+
}.each_with_index do |sel_col, idx|
|
359
352
|
field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
|
360
|
-
|
353
|
+
|
361
354
|
selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
|
362
|
-
|
355
|
+
v1[idx] << col_alias
|
356
|
+
end
|
357
|
+
# end
|
358
|
+
|
359
|
+
unless id_for_tables.key?(v.first)
|
360
|
+
# Accommodate composite primary key by allowing id_col to come in as an array
|
361
|
+
((id_col = k1.primary_key).is_a?(Array) ? id_col : [id_col]).each do |id_part|
|
362
|
+
id_for_tables[v.first] << if id_part
|
363
|
+
selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
|
364
|
+
id_alias
|
365
|
+
end
|
366
|
+
end
|
367
|
+
v1 << id_for_tables[v.first].compact
|
363
368
|
end
|
364
369
|
end
|
365
370
|
end
|
@@ -379,21 +384,27 @@ module ActiveRecord
|
|
379
384
|
hm.foreign_key
|
380
385
|
else
|
381
386
|
fk_col = hm.foreign_key
|
382
|
-
hm.
|
387
|
+
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
388
|
+
pk = hm.klass.primary_key
|
389
|
+
(pk.is_a?(Array) ? pk.first : pk) || '*'
|
383
390
|
end
|
384
391
|
tbl_alias = "_br_#{hm.name}"
|
385
392
|
pri_tbl = hm.active_record
|
393
|
+
on_clause = []
|
386
394
|
if fk_col.is_a?(Array) # Composite key?
|
387
|
-
on_clause = []
|
388
395
|
fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_tbl.primary_key[idx]}" }
|
389
|
-
|
390
|
-
JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
|
391
|
-
ON #{on_clause.join(' AND ')}")
|
396
|
+
selects = fk_col.dup
|
392
397
|
else
|
393
|
-
|
394
|
-
|
395
|
-
|
398
|
+
selects = [fk_col]
|
399
|
+
on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
|
400
|
+
end
|
401
|
+
if poly_type
|
402
|
+
selects << poly_type
|
403
|
+
on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
|
396
404
|
end
|
405
|
+
join_clause = "LEFT OUTER
|
406
|
+
JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
|
407
|
+
joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
|
397
408
|
end
|
398
409
|
where!(wheres) unless wheres.empty?
|
399
410
|
wheres unless wheres.empty? # Return the specific parameters that we did use
|
@@ -438,8 +449,7 @@ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table
|
|
438
449
|
this_module.const_get(class_name)
|
439
450
|
else
|
440
451
|
# Build STI subclass and place it into the namespace module
|
441
|
-
|
442
|
-
puts [this_module.const_set(class_name, klass = Class.new(self)).name, class_name].inspect
|
452
|
+
this_module.const_set(class_name, klass = Class.new(self))
|
443
453
|
klass
|
444
454
|
end
|
445
455
|
end
|
@@ -490,7 +500,7 @@ class Object
|
|
490
500
|
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
491
501
|
# return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
492
502
|
# If the file really exists, go and snag it:
|
493
|
-
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = self.name&.split('::'))
|
503
|
+
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
|
494
504
|
filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
|
495
505
|
end
|
496
506
|
if is_found
|
@@ -514,7 +524,7 @@ class Object
|
|
514
524
|
singular_table_name = ActiveSupport::Inflector.underscore(model_name)
|
515
525
|
|
516
526
|
# Adjust for STI if we know of a base model for the requested model name
|
517
|
-
table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base,
|
527
|
+
table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
|
518
528
|
base_model.table_name
|
519
529
|
else
|
520
530
|
ActiveSupport::Inflector.pluralize(singular_table_name)
|
@@ -555,7 +565,7 @@ class Object
|
|
555
565
|
return
|
556
566
|
end
|
557
567
|
|
558
|
-
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base,
|
568
|
+
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
|
559
569
|
is_sti = true
|
560
570
|
else
|
561
571
|
base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
|
@@ -603,77 +613,11 @@ class Object
|
|
603
613
|
hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
|
604
614
|
# The key in each hash entry (fk.first) is the constraint name
|
605
615
|
inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
611
|
-
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
612
|
-
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
613
|
-
end
|
614
|
-
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
615
|
-
else
|
616
|
-
assoc[:assoc_name]
|
617
|
-
end
|
618
|
-
need_class_name = singular_table_name.underscore != assoc_name
|
619
|
-
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
620
|
-
if (inverse = assoc[:inverse])
|
621
|
-
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
|
622
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
623
|
-
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
624
|
-
need_inverse_of = true
|
625
|
-
has_ones[singular_inv_assoc_name]
|
626
|
-
else
|
627
|
-
singular_inv_assoc_name
|
628
|
-
end
|
629
|
-
end
|
630
|
-
end
|
631
|
-
:belongs_to
|
632
|
-
else
|
633
|
-
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
634
|
-
# Are there multiple foreign keys out to the same table?
|
635
|
-
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
636
|
-
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
637
|
-
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
638
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
639
|
-
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
640
|
-
need_class_name = custom_assoc_name != singular_assoc_name
|
641
|
-
custom_assoc_name
|
642
|
-
else
|
643
|
-
singular_assoc_name
|
644
|
-
end
|
645
|
-
:has_one
|
646
|
-
else
|
647
|
-
:has_many
|
648
|
-
end
|
649
|
-
end
|
650
|
-
# Figure out if we need to specially call out the class_name and/or foreign key
|
651
|
-
# (and if either of those then definitely also a specific inverse_of)
|
652
|
-
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
653
|
-
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
654
|
-
if need_fk # Funky foreign key?
|
655
|
-
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
656
|
-
assoc_fk = assoc[:fk].uniq
|
657
|
-
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
658
|
-
else
|
659
|
-
assoc[:fk].to_sym
|
660
|
-
end
|
661
|
-
end
|
662
|
-
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
663
|
-
|
664
|
-
# Prepare a list of entries for "has_many :through"
|
665
|
-
if macro == :has_many
|
666
|
-
relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
|
667
|
-
next if k == assoc[:fk]
|
668
|
-
|
669
|
-
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
670
|
-
end
|
616
|
+
if (invs = assoc[:inverse_table]).is_a?(Array)
|
617
|
+
invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
|
618
|
+
else
|
619
|
+
build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
|
671
620
|
end
|
672
|
-
|
673
|
-
# And finally create a has_one, has_many, or belongs_to for this association
|
674
|
-
assoc_name = assoc_name.to_sym
|
675
|
-
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
676
|
-
self.send(macro, assoc_name, **options)
|
677
621
|
hmts
|
678
622
|
end
|
679
623
|
hmts.each do |hmt_fk, fks|
|
@@ -712,6 +656,88 @@ class Object
|
|
712
656
|
[built_model, code]
|
713
657
|
end
|
714
658
|
|
659
|
+
def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
|
660
|
+
singular_table_name = inverse_table&.singularize
|
661
|
+
options = {}
|
662
|
+
macro = if assoc[:is_bt]
|
663
|
+
# Try to take care of screwy names if this is a belongs_to going to an STI subclass
|
664
|
+
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
665
|
+
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
666
|
+
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
667
|
+
end
|
668
|
+
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
669
|
+
else
|
670
|
+
assoc[:assoc_name]
|
671
|
+
end
|
672
|
+
if assoc.key?(:polymorphic)
|
673
|
+
options[:polymorphic] = true
|
674
|
+
else
|
675
|
+
need_class_name = singular_table_name.underscore != assoc_name
|
676
|
+
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
677
|
+
end
|
678
|
+
if (inverse = assoc[:inverse])
|
679
|
+
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
|
680
|
+
has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
|
681
|
+
if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
682
|
+
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
683
|
+
need_inverse_of = true
|
684
|
+
has_ones[singular_inv_assoc_name]
|
685
|
+
else
|
686
|
+
singular_inv_assoc_name
|
687
|
+
end
|
688
|
+
end
|
689
|
+
end
|
690
|
+
:belongs_to
|
691
|
+
else
|
692
|
+
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
693
|
+
# Are there multiple foreign keys out to the same table?
|
694
|
+
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
695
|
+
if assoc.key?(:polymorphic)
|
696
|
+
options[:as] = assoc[:fk].to_sym
|
697
|
+
else
|
698
|
+
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
699
|
+
end
|
700
|
+
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
701
|
+
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
702
|
+
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
703
|
+
need_class_name = custom_assoc_name != singular_assoc_name
|
704
|
+
custom_assoc_name
|
705
|
+
else
|
706
|
+
singular_assoc_name
|
707
|
+
end
|
708
|
+
:has_one
|
709
|
+
else
|
710
|
+
:has_many
|
711
|
+
end
|
712
|
+
end
|
713
|
+
# Figure out if we need to specially call out the class_name and/or foreign key
|
714
|
+
# (and if either of those then definitely also a specific inverse_of)
|
715
|
+
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
716
|
+
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
717
|
+
if need_fk # Funky foreign key?
|
718
|
+
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
719
|
+
assoc_fk = assoc[:fk].uniq
|
720
|
+
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
721
|
+
else
|
722
|
+
assoc[:fk].to_sym
|
723
|
+
end
|
724
|
+
end
|
725
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
726
|
+
|
727
|
+
# Prepare a list of entries for "has_many :through"
|
728
|
+
if macro == :has_many
|
729
|
+
relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
|
730
|
+
next if k == assoc[:fk]
|
731
|
+
|
732
|
+
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
733
|
+
end
|
734
|
+
end
|
735
|
+
# And finally create a has_one, has_many, or belongs_to for this association
|
736
|
+
assoc_name = assoc_name.to_sym
|
737
|
+
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
738
|
+
self.send(macro, assoc_name, **options)
|
739
|
+
end
|
740
|
+
|
715
741
|
def build_controller(class_name, plural_class_name, model, relations)
|
716
742
|
table_name = ActiveSupport::Inflector.underscore(plural_class_name)
|
717
743
|
singular_table_name = ActiveSupport::Inflector.singularize(table_name)
|
@@ -754,11 +780,15 @@ class Object
|
|
754
780
|
|
755
781
|
if model.primary_key
|
756
782
|
code << " def show\n"
|
757
|
-
code << (find_by_id = "
|
783
|
+
code << (find_by_id = " id = params[:id]&.split(/[\\/,_]/)
|
784
|
+
id = id.first if id.is_a?(Array) && id.length == 1
|
785
|
+
@#{singular_table_name} = #{model.name}.find(id)\n")
|
758
786
|
code << " end\n"
|
759
787
|
self.define_method :show do
|
760
788
|
::Brick.set_db_schema(params)
|
761
|
-
|
789
|
+
id = params[:id]&.split(/[\/,_]/)
|
790
|
+
id = id.first if id.is_a?(Array) && id.length == 1
|
791
|
+
instance_variable_set("@#{singular_table_name}".to_sym, model.find(id))
|
762
792
|
end
|
763
793
|
end
|
764
794
|
|
@@ -1024,12 +1054,13 @@ module Brick
|
|
1024
1054
|
# rubocop:enable Style/CommentedKeyword
|
1025
1055
|
|
1026
1056
|
class << self
|
1027
|
-
def _add_bt_and_hm(fk, relations =
|
1028
|
-
relations ||= ::Brick.relations
|
1057
|
+
def _add_bt_and_hm(fk, relations, is_polymorphic = false)
|
1029
1058
|
bt_assoc_name = fk[1].underscore
|
1030
1059
|
bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
|
1031
1060
|
|
1032
1061
|
bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
|
1062
|
+
# %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
|
1063
|
+
# Maybe it's already gotten this info because we got as far as to say there was a unique class
|
1033
1064
|
primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
|
1034
1065
|
hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
|
1035
1066
|
|
@@ -1046,7 +1077,7 @@ module Brick
|
|
1046
1077
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
|
1047
1078
|
return
|
1048
1079
|
end
|
1049
|
-
unless (cols = relations[fk[0]][:cols]).key?(fk[1])
|
1080
|
+
unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
|
1050
1081
|
columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
|
1051
1082
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
|
1052
1083
|
return
|
@@ -1061,10 +1092,21 @@ module Brick
|
|
1061
1092
|
end
|
1062
1093
|
end
|
1063
1094
|
if (assoc_bt = bts[cnstr_name])
|
1064
|
-
|
1065
|
-
|
1095
|
+
if is_polymorphic
|
1096
|
+
# Assuming same fk (don't yet support composite keys for polymorphics)
|
1097
|
+
assoc_bt[:inverse_table] << fk[2]
|
1098
|
+
else # Expect we could have a composite key going
|
1099
|
+
if assoc_bt[:fk].is_a?(String)
|
1100
|
+
assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
|
1101
|
+
elsif assoc_bt[:fk].exclude?(fk[1])
|
1102
|
+
assoc_bt[:fk] << fk[1]
|
1103
|
+
end
|
1104
|
+
assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
|
1105
|
+
end
|
1066
1106
|
else
|
1067
|
-
|
1107
|
+
inverse_table = [primary_table] if is_polymorphic
|
1108
|
+
assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
|
1109
|
+
assoc_bt[:polymorphic] = true if is_polymorphic
|
1068
1110
|
end
|
1069
1111
|
if is_class
|
1070
1112
|
# For use in finding the proper :source for a HMT association that references an STI subclass
|
@@ -1076,11 +1118,16 @@ module Brick
|
|
1076
1118
|
return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
|
1077
1119
|
|
1078
1120
|
if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
|
1079
|
-
|
1121
|
+
if assoc_bt[:fk].is_a?(String)
|
1122
|
+
assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
|
1123
|
+
elsif assoc_bt[:fk].exclude?(fk[1])
|
1124
|
+
assoc_bt[:fk] << fk[1]
|
1125
|
+
end
|
1080
1126
|
assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
|
1081
1127
|
assoc_hm[:inverse] = assoc_bt
|
1082
1128
|
else
|
1083
1129
|
assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
|
1130
|
+
assoc_hm[:polymorphic] = true if is_polymorphic
|
1084
1131
|
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
1085
1132
|
hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
|
1086
1133
|
end
|
@@ -38,6 +38,9 @@ module Brick
|
|
38
38
|
|
39
39
|
# Has one relationships
|
40
40
|
::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
|
41
|
+
|
42
|
+
# Polymorphic associations
|
43
|
+
::Brick.polymorphics = app.config.brick.fetch(:polymorphics, nil)
|
41
44
|
end
|
42
45
|
|
43
46
|
# After we're initialized and before running the rest of stuff, put our configuration in place
|
@@ -64,12 +67,14 @@ module Brick
|
|
64
67
|
is_template_exists
|
65
68
|
end
|
66
69
|
|
67
|
-
def path_keys(fk_name, obj_name, pk)
|
68
|
-
if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
def path_keys(hm_assoc, fk_name, obj_name, pk)
|
71
|
+
keys = if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
72
|
+
fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
|
73
|
+
else
|
74
|
+
[[fk_name, "#{obj_name}.#{pk}"]]
|
75
|
+
end
|
76
|
+
keys << [hm_assoc.inverse_of.foreign_type, "#{hm_assoc.active_record.name}"] if hm_assoc.options.key?(:as)
|
77
|
+
keys.map { |x| "#{x.first}: #{x.last}"}.join(', ')
|
73
78
|
end
|
74
79
|
|
75
80
|
alias :_brick_find_template :find_template
|
@@ -102,12 +107,12 @@ module Brick
|
|
102
107
|
"#{obj_name}.#{attrib_name} || 0"
|
103
108
|
end
|
104
109
|
"<%= ct = #{set_ct}
|
105
|
-
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
110
|
+
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
106
111
|
else # has_one
|
107
112
|
"<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
|
108
113
|
end
|
109
114
|
elsif args.first == 'show'
|
110
|
-
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}
|
115
|
+
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
|
111
116
|
end
|
112
117
|
s << hm_stuff
|
113
118
|
end
|
@@ -205,7 +210,19 @@ def hide_bcrypt(val)
|
|
205
210
|
end %>"
|
206
211
|
|
207
212
|
if ['index', 'show', 'update'].include?(args.first)
|
208
|
-
|
213
|
+
poly_cols = []
|
214
|
+
css << "<% bts = { #{
|
215
|
+
bts.each_with_object([]) do |v, s|
|
216
|
+
foreign_models = if v.last[2] # Polymorphic?
|
217
|
+
poly_cols << @_brick_model.reflect_on_association(v[1].first).foreign_type
|
218
|
+
v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
|
219
|
+
else
|
220
|
+
"[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
|
221
|
+
end
|
222
|
+
s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}], #{v.last[2].inspect}]"
|
223
|
+
end.join(', ')
|
224
|
+
} }
|
225
|
+
poly_cols = #{poly_cols.inspect} %>"
|
209
226
|
end
|
210
227
|
|
211
228
|
# %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
|
@@ -370,13 +387,16 @@ function changeout(href, param, value) {
|
|
370
387
|
<table id=\"#{table_name}\">
|
371
388
|
<thead><tr>#{'<th></th>' if pk}
|
372
389
|
<% @#{table_name}.columns.map(&:name).each do |col| %>
|
373
|
-
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
390
|
+
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) || poly_cols.include?(col) %>
|
374
391
|
<th>
|
375
392
|
<% if (bt = bts[col]) %>
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
393
|
+
BT <%
|
394
|
+
bt[1].each do |bt_pair| %><%=
|
395
|
+
bt_pair.first.bt_link(bt.first) %> <%
|
396
|
+
end %><%
|
397
|
+
else %><%=
|
398
|
+
col %><%
|
399
|
+
end %>
|
380
400
|
</th>
|
381
401
|
<% end %>
|
382
402
|
<%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
|
@@ -388,14 +408,24 @@ function changeout(href, param, value) {
|
|
388
408
|
<tr>#{"
|
389
409
|
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
390
410
|
<% #{obj_name}.attributes.each do |k, val| %>
|
391
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
411
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || poly_cols.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && (k.length == 63 || k.end_with?('_ct'))) %>
|
392
412
|
<td>
|
393
413
|
<% if (bt = bts[k]) %>
|
394
414
|
<%# binding.pry # Postgres column names are limited to 63 characters %>
|
395
|
-
<%
|
396
|
-
|
397
|
-
|
398
|
-
|
415
|
+
<% if bt[2] # Polymorphic?
|
416
|
+
bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
|
417
|
+
base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
|
418
|
+
poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
|
419
|
+
%><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
|
420
|
+
send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
|
421
|
+
else
|
422
|
+
bt_txt = (bt_class = bt[1].first.first).brick_descrip(
|
423
|
+
#{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |z| #{obj_name}.send(z.last[0..62]) }, (bt_id_col = descrips.last)
|
424
|
+
)
|
425
|
+
bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
|
426
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
427
|
+
<%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
|
428
|
+
<% end %>
|
399
429
|
<% else %>
|
400
430
|
<%= hide_bcrypt(val) %>
|
401
431
|
<% end %>
|
@@ -414,41 +444,56 @@ function changeout(href, param, value) {
|
|
414
444
|
<p style=\"color: green\"><%= notice %></p>#{"
|
415
445
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
416
446
|
<select id=\"tbl\">#{table_options}</select>
|
417
|
-
<h1>#{model_name}: <%= (obj = @#{obj_name}
|
447
|
+
<h1>#{model_name}: <%= (obj = @#{obj_name})&.brick_descrip || controller_name %></h1>
|
418
448
|
<%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
|
419
449
|
<% if obj %>
|
420
450
|
<%= # path_options = [obj.#{pk}]
|
421
|
-
|
422
|
-
|
423
|
-
|
451
|
+
# path_options << { '_brick_schema': } if
|
452
|
+
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
453
|
+
form_for(obj.becomes(#{model_name})) do |f| %>
|
424
454
|
<table>
|
425
455
|
<% has_fields = false
|
426
|
-
@#{obj_name}.
|
456
|
+
@#{obj_name}.attributes.each do |k, val| %>
|
427
457
|
<tr>
|
428
458
|
<%# %%% Accommodate composite keys %>
|
429
459
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
430
460
|
<th class=\"show-field\">
|
431
461
|
<% has_fields = true
|
432
462
|
if (bt = bts[k])
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
463
|
+
# Add a final member in this array with descriptive options to be used in <select> drop-downs
|
464
|
+
bt_name = bt[1].map { |x| x.first.name }.join('/')
|
465
|
+
# %%% Only do this if the user has permissions to edit this bt field
|
466
|
+
if bt[2] # Polymorphic?
|
467
|
+
poly_class_name = @#{obj_name}.send(\"#\{bt.first\}_type\")
|
468
|
+
bt_pair = nil
|
469
|
+
loop do
|
470
|
+
bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
|
471
|
+
# Acxommodate any valid STI by going up the chain of inheritance
|
472
|
+
break unless bt_pair.nil? && poly_class_name = ::Brick.existing_stis[poly_class_name]
|
473
|
+
end
|
474
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
475
|
+
poly_id = @#{obj_name}.send(\"#\{bt.first\}_id\")
|
476
|
+
# bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
477
|
+
else # No polymorphism, so just get the first one
|
478
|
+
bt_pair = bt[1].first
|
479
|
+
end
|
480
|
+
bt_class = bt_pair.first
|
481
|
+
if bt.length < 4
|
482
|
+
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
483
|
+
# %%% Accommodate composite keys for obj.pk at the end here
|
484
|
+
bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
485
|
+
end %>
|
486
|
+
BT <%= bt_class.bt_link(bt.first) %>
|
442
487
|
<% else %>
|
443
488
|
<%= k %>
|
444
489
|
<% end %>
|
445
490
|
</th>
|
446
491
|
<td>
|
447
|
-
<% if
|
492
|
+
<% if bt
|
448
493
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
449
494
|
html_options[:class] = 'dimmed' unless val %>
|
450
495
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
451
|
-
<%= bt_obj =
|
496
|
+
<%= bt_obj = bt_class.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.base_class.name.underscore\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
452
497
|
<% else case #{model_name}.column_for_attribute(k).type
|
453
498
|
when :string, :text %>
|
454
499
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -469,27 +514,28 @@ function changeout(href, param, value) {
|
|
469
514
|
<% end %>
|
470
515
|
</td>
|
471
516
|
</tr>
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
517
|
+
<% end
|
518
|
+
if has_fields %>
|
519
|
+
<tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
|
520
|
+
<% else %>
|
521
|
+
<tr><td colspan=\"2\">(No displayable fields)</td></tr>
|
522
|
+
<% end %>
|
478
523
|
</table>
|
479
524
|
<% end %>
|
480
525
|
|
481
526
|
#{hms_headers.each_with_object(+'') do |hm, s|
|
482
527
|
if (pk = hm.first.klass.primary_key)
|
483
|
-
|
528
|
+
hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
|
529
|
+
obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
|
530
|
+
s << "<table id=\"#{hm_name}\">
|
484
531
|
<tr><th>#{hm[3]}</th></tr>
|
485
|
-
<% collection = @#{obj_name}
|
532
|
+
<% collection = @#{obj_name}.#{hm_name}
|
486
533
|
collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
|
487
534
|
if collection.empty? %>
|
488
535
|
<tr><td>(none)</td></tr>
|
489
536
|
<% else %>
|
490
|
-
<% collection.uniq.each do |#{hm_singular_name
|
491
|
-
|
492
|
-
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
|
537
|
+
<% collection.uniq.each do |#{hm_singular_name}| %>
|
538
|
+
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path([#{obj_pk}])) %></td></tr>
|
493
539
|
<% end %>
|
494
540
|
<% end %>
|
495
541
|
</table>"
|
@@ -517,7 +563,7 @@ function changeout(href, param, value) {
|
|
517
563
|
end
|
518
564
|
|
519
565
|
# Just in case it hadn't been done previously when we tried to load the brick initialiser,
|
520
|
-
# go make sure we've loaded additional references (virtual foreign keys).
|
566
|
+
# go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
|
521
567
|
::Brick.load_additional_references
|
522
568
|
end
|
523
569
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -85,12 +85,16 @@ module Brick
|
|
85
85
|
@sti_models ||= {}
|
86
86
|
end
|
87
87
|
|
88
|
+
def self.existing_stis
|
89
|
+
@existing_stis ||= Brick.config.sti_namespace_prefixes.each_with_object({}) { |snp, s| s[snp.first[2..-1]] = snp.last unless snp.first.end_with?('::') }
|
90
|
+
end
|
91
|
+
|
88
92
|
class << self
|
89
93
|
attr_accessor :db_schemas
|
90
94
|
|
91
95
|
def set_db_schema(params)
|
92
96
|
schema = params['_brick_schema'] || 'public'
|
93
|
-
ActiveRecord::Base.
|
97
|
+
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema) if schema && ::Brick.db_schemas&.include?(schema)
|
94
98
|
end
|
95
99
|
|
96
100
|
# All tables and views (what Postgres calls "relations" including column and foreign key info)
|
@@ -107,7 +111,13 @@ module Brick
|
|
107
111
|
|
108
112
|
case a.macro
|
109
113
|
when :belongs_to
|
110
|
-
s.first[a.foreign_key] =
|
114
|
+
s.first[a.foreign_key] = if a.polymorphic?
|
115
|
+
primary_tables = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }&.last&.fetch(:inverse_table, [])
|
116
|
+
models = primary_tables&.map { |table| table.singularize.camelize.constantize }
|
117
|
+
[a.name, models, true]
|
118
|
+
else
|
119
|
+
[a.name, a.klass]
|
120
|
+
end
|
111
121
|
when :has_many, :has_one # This gets has_many as well as has_many :through
|
112
122
|
# %%% weed out ones that don't have an available model to reference
|
113
123
|
s.last[a.name] = a
|
@@ -256,6 +266,12 @@ module Brick
|
|
256
266
|
end
|
257
267
|
end
|
258
268
|
|
269
|
+
# Polymorphic associations
|
270
|
+
def polymorphics=(polys)
|
271
|
+
polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array)
|
272
|
+
Brick.config.polymorphics = polys || {}
|
273
|
+
end
|
274
|
+
|
259
275
|
# DSL templates for individual models to provide prettier descriptions of objects
|
260
276
|
# @api public
|
261
277
|
def model_descrips=(descrips)
|
@@ -268,14 +284,52 @@ module Brick
|
|
268
284
|
Brick.config.sti_namespace_prefixes = snp
|
269
285
|
end
|
270
286
|
|
287
|
+
# Database schema to use when analysing existing data, such as deriving a list of polymorphic classes
|
288
|
+
# for polymorphics in which it wasn't originally specified.
|
289
|
+
# @api public
|
290
|
+
def schema_to_analyse=(schema)
|
291
|
+
Brick.config.schema_to_analyse = schema
|
292
|
+
end
|
293
|
+
|
294
|
+
def default_route_fallback=(resource_name)
|
295
|
+
Brick.config.default_route_fallback = resource_name
|
296
|
+
end
|
297
|
+
|
271
298
|
# Load additional references (virtual foreign keys)
|
272
299
|
# This is attempted early if a brick initialiser file is found, and then again as a failsafe at the end of our engine's initialisation
|
273
300
|
# %%% Maybe look for differences the second time 'round and just add new stuff instead of entirely deferring
|
274
301
|
def load_additional_references
|
275
302
|
return if @_additional_references_loaded
|
276
303
|
|
277
|
-
|
278
|
-
|
304
|
+
relations = ::Brick.relations
|
305
|
+
if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
|
306
|
+
ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
|
307
|
+
if (polys = ::Brick.config.polymorphics)
|
308
|
+
if (schema = ::Brick.config.schema_to_analyse) && ::Brick.db_schemas&.include?(schema)
|
309
|
+
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
|
310
|
+
end
|
311
|
+
missing_stis = {}
|
312
|
+
polys.each do |k, v|
|
313
|
+
table_name, poly = k.split('.')
|
314
|
+
v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
|
315
|
+
v.each do |type|
|
316
|
+
if relations.key?(primary_table = type.underscore.pluralize)
|
317
|
+
::Brick._add_bt_and_hm([table_name, poly, primary_table, "(brick) #{table_name}_#{poly}"], relations, true)
|
318
|
+
else
|
319
|
+
missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
unless missing_stis.empty?
|
324
|
+
print "
|
325
|
+
You might be missing an STI namespace prefix entry for these tables: #{missing_stis.keys.join(', ')}.
|
326
|
+
In config/initializers/brick.rb appropriate entries would look something like:
|
327
|
+
Brick.sti_namespace_prefixes = {"
|
328
|
+
puts missing_stis.map { |_k, missing_sti| "\n '::#{missing_sti}' => 'YourParentModel'" }.join(',')
|
329
|
+
puts " }
|
330
|
+
(Just trade out YourParentModel with some more appropriate one.)"
|
331
|
+
end
|
332
|
+
end
|
279
333
|
@_additional_references_loaded = true
|
280
334
|
end
|
281
335
|
|
@@ -331,6 +385,9 @@ module Brick
|
|
331
385
|
def finalize!
|
332
386
|
existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
|
333
387
|
::Rails.application.routes.append do
|
388
|
+
unless ::Brick.config.default_route_fallback.blank? || ::Rails.application.routes.named_routes.send(:routes)[:root]
|
389
|
+
send(:root, "#{::Brick.config.default_route_fallback}#index")
|
390
|
+
end
|
334
391
|
# %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
|
335
392
|
# If auto-controllers and auto-models are both enabled then this makes sense:
|
336
393
|
::Brick.relations.each do |k, v|
|
@@ -19,11 +19,31 @@ module Brick
|
|
19
19
|
|
20
20
|
def create_initializer_file
|
21
21
|
unless File.exist?(filename = 'config/initializers/brick.rb')
|
22
|
-
# See if we can make suggestions for additional_references
|
23
|
-
resembles_fks = []
|
24
|
-
|
25
|
-
|
22
|
+
# See if we can make suggestions for additional_references and polymorphic associations
|
23
|
+
resembles_fks = Hash.new { |h, k| h[k] = [] }
|
24
|
+
possible_polymorphics = {}
|
25
|
+
possible_additional_references = (relations = ::Brick.relations).each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
|
26
|
+
model_filename = "app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb"
|
27
|
+
v.last[:cols].each do |col, type|
|
26
28
|
col_down = col.downcase
|
29
|
+
|
30
|
+
if (is_possible_poly = ['character varying', 'text'].include?(type.first))
|
31
|
+
if col_down.end_with?('_type') &&
|
32
|
+
poly_type_cut_length = -6
|
33
|
+
col_down = col_down[0..-6]
|
34
|
+
elsif col_down.end_with?('type')
|
35
|
+
poly_type_cut_length = -5
|
36
|
+
col_down = col_down[0..-5]
|
37
|
+
else
|
38
|
+
is_possible_poly = false
|
39
|
+
end
|
40
|
+
is_possible_poly = false if col_down.length < 6 # Was it simply called "type" or something else really short?
|
41
|
+
if is_possible_poly && !File.exist?(model_filename) # Make sure a model file isn't present
|
42
|
+
possible_polymorphics["#{v.first}.#{col_down}"] = "'#{v.first}.#{col[0..poly_type_cut_length]}'"
|
43
|
+
next
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
27
47
|
is_possible = true
|
28
48
|
if col_down.end_with?('_id')
|
29
49
|
col_down = col_down[0..-4]
|
@@ -40,30 +60,48 @@ module Brick
|
|
40
60
|
if col_down.start_with?('fk_')
|
41
61
|
is_possible = true
|
42
62
|
col_down = col_down[3..-1]
|
63
|
+
elsif col_down.start_with?('fk')
|
64
|
+
is_possible = true
|
65
|
+
col_down = col_down[2..-1]
|
43
66
|
end
|
44
67
|
# This possible key not really a primary key and not yet used as a foreign key?
|
45
68
|
if is_possible && !(relation = relations.fetch(v.first, {}))[:pkey].first&.last&.include?(col) &&
|
46
69
|
!relations.fetch(v.first, {})[:fks]&.any? { |_k, v| v[:is_bt] && v[:fk] == col }
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
70
|
+
# Starting to look promising ... make sure a model file isn't present
|
71
|
+
if !File.exist?(model_filename)
|
72
|
+
if (relations.fetch(f_table = col_down, nil) ||
|
73
|
+
relations.fetch(f_table = ActiveSupport::Inflector.pluralize(col_down), nil)) &&
|
74
|
+
s["#{v.first}.#{col_down}"] << "['#{v.first}', '#{col}', '#{f_table}']"
|
75
|
+
else
|
76
|
+
resembles_fks["#{v.first}.#{col_down}"] << "#{v.first}.#{col}"
|
77
|
+
end
|
54
78
|
end
|
55
79
|
end
|
56
80
|
end
|
57
|
-
s
|
58
81
|
end
|
59
82
|
|
60
|
-
|
83
|
+
possible_polymorphics.each_key do |k|
|
84
|
+
# Also matching one of the FK suggestions means it could be polymorphic,
|
85
|
+
# so delete any suggestions for a FK of the same name and only recommend
|
86
|
+
# the polymorphic association.
|
87
|
+
if resembles_fks.key?(k)
|
88
|
+
resembles_fks.delete(k)
|
89
|
+
elsif possible_additional_references.key?(k)
|
90
|
+
possible_additional_references.delete(k)
|
91
|
+
else
|
92
|
+
# While this one has a type, it's missing a corresponding ID column so it isn't polymorphic
|
93
|
+
possible_polymorphics.delete(k)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
resembles_fks = resembles_fks.values.flatten
|
97
|
+
|
98
|
+
bar = case (possible_additional_references = possible_additional_references.values.flatten).length
|
61
99
|
when 0
|
62
100
|
+"# Brick.additional_references = [['orders', 'customer_id', 'customer'],
|
63
101
|
# ['customer', 'region_id', 'regions']]"
|
64
102
|
when 1
|
65
103
|
+"# # Here is a possible additional reference that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
|
66
|
-
# Brick.additional_references = [
|
104
|
+
# Brick.additional_references = [#{possible_additional_references.first}]"
|
67
105
|
else
|
68
106
|
+"# # Here are possible additional references that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
|
69
107
|
# Brick.additional_references = [
|
@@ -75,6 +113,26 @@ module Brick
|
|
75
113
|
# # #{resembles_fks.join(', ')}"
|
76
114
|
end
|
77
115
|
|
116
|
+
poly = case (possible_polymorphics = possible_polymorphics.values.flatten).length
|
117
|
+
when 0
|
118
|
+
" like this:
|
119
|
+
# Brick.polymorphics = [
|
120
|
+
# 'comments.commentable',
|
121
|
+
# 'images.imageable'
|
122
|
+
# ]"
|
123
|
+
when 1
|
124
|
+
".
|
125
|
+
# # Here is a possible polymorphic association that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
|
126
|
+
# Brick.polymorphics = [#{possible_additional_references.first}]"
|
127
|
+
|
128
|
+
else
|
129
|
+
".
|
130
|
+
# # Here are possible polymorphic associations that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
|
131
|
+
# Brick.polymorphics = [
|
132
|
+
# #{possible_polymorphics.join(",\n# ")}
|
133
|
+
# ]"
|
134
|
+
end
|
135
|
+
|
78
136
|
create_file(filename, "# frozen_string_literal: true
|
79
137
|
|
80
138
|
# # Settings for the Brick gem
|
@@ -119,7 +177,7 @@ module Brick
|
|
119
177
|
# # Skip showing counts for these specific has_many associations when building auto-generated #index views.
|
120
178
|
# # When there are related tables with a significant number of records, this can lessen the load on the database
|
121
179
|
# # considerably, sometimes fixing what might appear to be an index page that just \"hangs\" for no apparent reason.
|
122
|
-
Brick.skip_index_hms = ['User.litany_of_woes']
|
180
|
+
# Brick.skip_index_hms = ['User.litany_of_woes']
|
123
181
|
|
124
182
|
# # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
|
125
183
|
# # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
|
@@ -157,6 +215,12 @@ Brick.skip_index_hms = ['User.litany_of_woes']
|
|
157
215
|
# Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
|
158
216
|
# '::Snake' => 'Reptile' }
|
159
217
|
|
218
|
+
# # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes in the case that
|
219
|
+
# # it wasn't originally specified.
|
220
|
+
# Brick.schema_to_analyse = 'engineering'
|
221
|
+
|
222
|
+
# # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
|
223
|
+
|
160
224
|
# # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
|
161
225
|
# # route to go to the :index action for what would be a controller for that table. You can specify any controller
|
162
226
|
# # name and action you wish in order to override this and have that be the default route when none other has been
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: brick
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.27
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-05-
|
11
|
+
date: 2022-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|