brick 1.0.24 → 1.0.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/brick/config.rb +9 -0
- data/lib/brick/extensions.rb +171 -125
- data/lib/brick/frameworks/rails/engine.rb +78 -40
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +25 -4
- data/lib/generators/brick/install_generator.rb +4 -0
- 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: b0dfa64d7a8a148b4c2ad990f5af1604d5855b80c6e95f39cd371a43a591b7f1
|
4
|
+
data.tar.gz: bc7644a08678136d91696a68e0ad670f4f04d41772f36ba1bd806ebe45f23b04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a39a8adc0c72288db5bd08e2483cefb701db9c2122fe1b4012c19bef07c7d048fed7f6e051473a9b4b6c80e321e4a781a16543d40f3e569f23c31f420a5117b6
|
7
|
+
data.tar.gz: 3ddcc914e3143a02a4582d6f525417c3012ae4eafc9ae889d5935223f6173afb4fb5ff3a40dc6c9caf95dfbf7a3c5be8e7bc4890efb385867ef5fa6f3ccbfb9e
|
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
|
data/lib/brick/extensions.rb
CHANGED
@@ -52,6 +52,14 @@ module Arel
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
# module ActiveModel
|
56
|
+
# class NotNullValidator < EachValidator
|
57
|
+
# def validate_each(record, attribute, value)
|
58
|
+
# record.errors[attribute] << "must not be null" if value.nil?
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
|
55
63
|
module ActiveRecord
|
56
64
|
class Base
|
57
65
|
def self._assoc_names
|
@@ -74,8 +82,8 @@ module ActiveRecord
|
|
74
82
|
dsl
|
75
83
|
end
|
76
84
|
|
77
|
-
# Pass in true or a JoinArray
|
78
|
-
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
|
85
|
+
# Pass in true for build_array, or just pass in a JoinArray
|
86
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false)
|
79
87
|
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
80
88
|
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
81
89
|
members = []
|
@@ -87,12 +95,17 @@ module ActiveRecord
|
|
87
95
|
if bracket_name
|
88
96
|
if ch == ']' # Time to process a bracketed thing?
|
89
97
|
parts = bracket_name.split('.')
|
90
|
-
first_parts = parts[0..-2].map
|
98
|
+
first_parts = parts[0..-2].map do |part|
|
99
|
+
klass = klass.reflect_on_association(part_sym = part.to_sym).klass
|
100
|
+
part_sym
|
101
|
+
end
|
91
102
|
parts = prefix + first_parts + [parts[-1]]
|
92
103
|
if parts.length > 1
|
93
|
-
|
94
|
-
|
95
|
-
|
104
|
+
unless is_polymorphic
|
105
|
+
s = build_array
|
106
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
107
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
108
|
+
end
|
96
109
|
translations[parts[0..-2].join('.')] = klass
|
97
110
|
end
|
98
111
|
members << parts
|
@@ -191,7 +204,10 @@ module ActiveRecord
|
|
191
204
|
private
|
192
205
|
|
193
206
|
def self._brick_get_fks
|
194
|
-
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.
|
207
|
+
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.each_with_object([]) do |bt, s|
|
208
|
+
s << bt.foreign_key
|
209
|
+
s << bt.foreign_type if bt.polymorphic?
|
210
|
+
end
|
195
211
|
end
|
196
212
|
end
|
197
213
|
|
@@ -279,23 +295,6 @@ module ActiveRecord
|
|
279
295
|
# , is_add_bts, is_add_hms
|
280
296
|
)
|
281
297
|
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
298
|
|
300
299
|
# %%% Skip the metadata columns
|
301
300
|
if selects&.empty? # Default to all columns
|
@@ -310,7 +309,11 @@ module ActiveRecord
|
|
310
309
|
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
311
310
|
bts.each do |_k, bt|
|
312
311
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
313
|
-
bt_descrip[bt.first] =
|
312
|
+
bt_descrip[bt.first] = if bt[1].is_a?(Array)
|
313
|
+
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
|
314
|
+
else
|
315
|
+
{ bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
|
316
|
+
end
|
314
317
|
end
|
315
318
|
skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
|
316
319
|
hms.each do |k, hm|
|
@@ -328,7 +331,7 @@ module ActiveRecord
|
|
328
331
|
when 2
|
329
332
|
assoc_name = ks.first.to_sym
|
330
333
|
# 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&.
|
334
|
+
next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
|
332
335
|
|
333
336
|
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
334
337
|
end
|
@@ -344,23 +347,31 @@ module ActiveRecord
|
|
344
347
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
345
348
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
346
349
|
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)
|
350
|
+
v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
|
351
|
+
next if chains[k1].nil?
|
352
|
+
|
353
|
+
tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
|
354
|
+
# if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
|
357
355
|
field_tbl_name = nil
|
358
|
-
|
356
|
+
v1.map { |x|
|
357
|
+
[translations[x[0..-2].map(&:to_s).join('.')], x.last]
|
358
|
+
}.each_with_index do |sel_col, idx|
|
359
359
|
field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
|
360
|
-
|
360
|
+
|
361
361
|
selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
|
362
|
-
|
362
|
+
v1[idx] << col_alias
|
363
|
+
end
|
364
|
+
# end
|
365
|
+
|
366
|
+
if (id_col = k1.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
|
367
|
+
# Accommodate composite primary key by allowing id_col to come in as an array
|
368
|
+
(id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
|
369
|
+
selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
|
370
|
+
id_for_tables[v.first] << id_alias
|
371
|
+
end
|
372
|
+
v1 << id_for_tables[v.first]
|
363
373
|
end
|
374
|
+
|
364
375
|
end
|
365
376
|
end
|
366
377
|
join_array.each do |assoc_name|
|
@@ -379,21 +390,26 @@ module ActiveRecord
|
|
379
390
|
hm.foreign_key
|
380
391
|
else
|
381
392
|
fk_col = hm.foreign_key
|
393
|
+
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
382
394
|
hm.klass.primary_key || '*'
|
383
395
|
end
|
384
396
|
tbl_alias = "_br_#{hm.name}"
|
385
397
|
pri_tbl = hm.active_record
|
398
|
+
on_clause = []
|
386
399
|
if fk_col.is_a?(Array) # Composite key?
|
387
|
-
on_clause = []
|
388
400
|
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 ')}")
|
401
|
+
selects = fk_col.dup
|
392
402
|
else
|
393
|
-
|
394
|
-
|
395
|
-
ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
|
403
|
+
selects = [fk_col]
|
404
|
+
on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
|
396
405
|
end
|
406
|
+
if poly_type
|
407
|
+
selects << poly_type
|
408
|
+
on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
|
409
|
+
end
|
410
|
+
join_clause = "LEFT OUTER
|
411
|
+
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}"
|
412
|
+
joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
|
397
413
|
end
|
398
414
|
where!(wheres) unless wheres.empty?
|
399
415
|
wheres unless wheres.empty? # Return the specific parameters that we did use
|
@@ -490,7 +506,7 @@ class Object
|
|
490
506
|
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
491
507
|
# return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
492
508
|
# 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('::'))
|
509
|
+
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
|
494
510
|
filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
|
495
511
|
end
|
496
512
|
if is_found
|
@@ -603,77 +619,11 @@ class Object
|
|
603
619
|
hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
|
604
620
|
# The key in each hash entry (fk.first) is the constraint name
|
605
621
|
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
|
622
|
+
if (invs = assoc[:inverse_table]).is_a?(Array)
|
623
|
+
invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
|
624
|
+
else
|
625
|
+
build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
|
671
626
|
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
627
|
hmts
|
678
628
|
end
|
679
629
|
hmts.each do |hmt_fk, fks|
|
@@ -712,6 +662,89 @@ class Object
|
|
712
662
|
[built_model, code]
|
713
663
|
end
|
714
664
|
|
665
|
+
def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
|
666
|
+
singular_table_name = inverse_table&.singularize
|
667
|
+
options = {}
|
668
|
+
macro = if assoc[:is_bt]
|
669
|
+
# Try to take care of screwy names if this is a belongs_to going to an STI subclass
|
670
|
+
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
671
|
+
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
672
|
+
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
673
|
+
end
|
674
|
+
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
675
|
+
else
|
676
|
+
assoc[:assoc_name]
|
677
|
+
end
|
678
|
+
if assoc.key?(:polymorphic)
|
679
|
+
options[:polymorphic] = true
|
680
|
+
else
|
681
|
+
need_class_name = singular_table_name.underscore != assoc_name
|
682
|
+
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
683
|
+
end
|
684
|
+
if (inverse = assoc[:inverse])
|
685
|
+
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
|
686
|
+
has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
|
687
|
+
if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
688
|
+
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
689
|
+
need_inverse_of = true
|
690
|
+
has_ones[singular_inv_assoc_name]
|
691
|
+
else
|
692
|
+
singular_inv_assoc_name
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
:belongs_to
|
697
|
+
else
|
698
|
+
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
699
|
+
# Are there multiple foreign keys out to the same table?
|
700
|
+
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
701
|
+
# binding.pry if assoc.key?(:polymorphic)
|
702
|
+
if assoc.key?(:polymorphic)
|
703
|
+
options[:as] = assoc[:fk].to_sym if assoc.key?(:polymorphic)
|
704
|
+
else
|
705
|
+
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
706
|
+
end
|
707
|
+
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
708
|
+
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
709
|
+
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
710
|
+
need_class_name = custom_assoc_name != singular_assoc_name
|
711
|
+
custom_assoc_name
|
712
|
+
else
|
713
|
+
singular_assoc_name
|
714
|
+
end
|
715
|
+
:has_one
|
716
|
+
else
|
717
|
+
:has_many
|
718
|
+
end
|
719
|
+
end
|
720
|
+
# Figure out if we need to specially call out the class_name and/or foreign key
|
721
|
+
# (and if either of those then definitely also a specific inverse_of)
|
722
|
+
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
723
|
+
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
724
|
+
if need_fk # Funky foreign key?
|
725
|
+
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
726
|
+
assoc_fk = assoc[:fk].uniq
|
727
|
+
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
728
|
+
else
|
729
|
+
assoc[:fk].to_sym
|
730
|
+
end
|
731
|
+
end
|
732
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
733
|
+
|
734
|
+
# Prepare a list of entries for "has_many :through"
|
735
|
+
if macro == :has_many
|
736
|
+
relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
|
737
|
+
next if k == assoc[:fk]
|
738
|
+
|
739
|
+
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
740
|
+
end
|
741
|
+
end
|
742
|
+
# And finally create a has_one, has_many, or belongs_to for this association
|
743
|
+
assoc_name = assoc_name.to_sym
|
744
|
+
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
745
|
+
self.send(macro, assoc_name, **options)
|
746
|
+
end
|
747
|
+
|
715
748
|
def build_controller(class_name, plural_class_name, model, relations)
|
716
749
|
table_name = ActiveSupport::Inflector.underscore(plural_class_name)
|
717
750
|
singular_table_name = ActiveSupport::Inflector.singularize(table_name)
|
@@ -1024,14 +1057,19 @@ module Brick
|
|
1024
1057
|
# rubocop:enable Style/CommentedKeyword
|
1025
1058
|
|
1026
1059
|
class << self
|
1027
|
-
def _add_bt_and_hm(fk, relations =
|
1028
|
-
relations ||= ::Brick.relations
|
1060
|
+
def _add_bt_and_hm(fk, relations, is_polymorphic = false)
|
1029
1061
|
bt_assoc_name = fk[1].underscore
|
1030
1062
|
bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
|
1031
1063
|
|
1032
1064
|
bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
|
1033
|
-
|
1034
|
-
|
1065
|
+
# %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
|
1066
|
+
# Maybe it's already gotten this info because we got as far as to say there was a unique class
|
1067
|
+
# if is_polymorphic
|
1068
|
+
# primary_table = fk[]
|
1069
|
+
# else
|
1070
|
+
primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
|
1071
|
+
hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
|
1072
|
+
# end
|
1035
1073
|
|
1036
1074
|
unless (cnstr_name = fk[3])
|
1037
1075
|
# For any appended references (those that come from config), arrive upon a definitely unique constraint name
|
@@ -1046,7 +1084,7 @@ module Brick
|
|
1046
1084
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
|
1047
1085
|
return
|
1048
1086
|
end
|
1049
|
-
unless (cols = relations[fk[0]][:cols]).key?(fk[1])
|
1087
|
+
unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
|
1050
1088
|
columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
|
1051
1089
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
|
1052
1090
|
return
|
@@ -1061,10 +1099,17 @@ module Brick
|
|
1061
1099
|
end
|
1062
1100
|
end
|
1063
1101
|
if (assoc_bt = bts[cnstr_name])
|
1064
|
-
|
1065
|
-
|
1102
|
+
if is_polymorphic
|
1103
|
+
# Assuming same fk (don't yet support composite keys for polymorphics)
|
1104
|
+
assoc_bt[:inverse_table] << fk[2]
|
1105
|
+
else # Expect we've got a composite key going
|
1106
|
+
assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
|
1107
|
+
assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
|
1108
|
+
end
|
1066
1109
|
else
|
1067
|
-
|
1110
|
+
inverse_table = [primary_table] if is_polymorphic
|
1111
|
+
assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
|
1112
|
+
assoc_bt[:polymorphic] = true if is_polymorphic
|
1068
1113
|
end
|
1069
1114
|
if is_class
|
1070
1115
|
# For use in finding the proper :source for a HMT association that references an STI subclass
|
@@ -1081,6 +1126,7 @@ module Brick
|
|
1081
1126
|
assoc_hm[:inverse] = assoc_bt
|
1082
1127
|
else
|
1083
1128
|
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 }
|
1129
|
+
assoc_hm[:polymorphic] = true if is_polymorphic
|
1084
1130
|
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
1085
1131
|
hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
|
1086
1132
|
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}&.first&", pk)} }) %>\n"
|
115
|
+
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
|
111
116
|
end
|
112
117
|
s << hm_stuff
|
113
118
|
end
|
@@ -205,7 +210,16 @@ def hide_bcrypt(val)
|
|
205
210
|
end %>"
|
206
211
|
|
207
212
|
if ['index', 'show', 'update'].include?(args.first)
|
208
|
-
css << "<% bts = { #{
|
213
|
+
css << "<% bts = { #{
|
214
|
+
bts.each_with_object([]) do |v, s|
|
215
|
+
foreign_models = if v.last[1].is_a?(Array)
|
216
|
+
v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
|
217
|
+
else
|
218
|
+
"[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
|
219
|
+
end
|
220
|
+
s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}]]"
|
221
|
+
end.join(', ')
|
222
|
+
} } %>"
|
209
223
|
end
|
210
224
|
|
211
225
|
# %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
|
@@ -373,10 +387,13 @@ function changeout(href, param, value) {
|
|
373
387
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
374
388
|
<th>
|
375
389
|
<% if (bt = bts[col]) %>
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
390
|
+
BT <%
|
391
|
+
bt[1].each do |bt_pair| %><%=
|
392
|
+
bt_pair.first.bt_link(bt.first) %> <%
|
393
|
+
end %><%
|
394
|
+
else %><%=
|
395
|
+
col %><%
|
396
|
+
end %>
|
380
397
|
</th>
|
381
398
|
<% end %>
|
382
399
|
<%# 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 +405,24 @@ function changeout(href, param, value) {
|
|
388
405
|
<tr>#{"
|
389
406
|
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
390
407
|
<% #{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')) %>
|
408
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && (k.length == 63 || k.end_with?('_ct'))) %>
|
392
409
|
<td>
|
393
410
|
<% if (bt = bts[k]) %>
|
394
411
|
<%# binding.pry # Postgres column names are limited to 63 characters %>
|
395
|
-
<%
|
396
|
-
|
397
|
-
|
398
|
-
|
412
|
+
<% if (pairs = bt[1].length > 1)
|
413
|
+
bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
|
414
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
415
|
+
poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
|
416
|
+
%><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
|
417
|
+
send(\"#\{bt_class.underscore\}_path\".to_sym, poly_id)) if poly_id %><%
|
418
|
+
else # We should do something other than [0..-2] for when there is no primary key (or maybe have an empty final array there in that case?)
|
419
|
+
bt_txt = (bt_class = bt[1].first.first).brick_descrip(
|
420
|
+
#{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)
|
421
|
+
)
|
422
|
+
bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
|
423
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
424
|
+
<%#= 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 %>
|
425
|
+
<% end %>
|
399
426
|
<% else %>
|
400
427
|
<%= hide_bcrypt(val) %>
|
401
428
|
<% end %>
|
@@ -418,9 +445,9 @@ function changeout(href, param, value) {
|
|
418
445
|
<%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
|
419
446
|
<% if obj %>
|
420
447
|
<%= # path_options = [obj.#{pk}]
|
421
|
-
|
422
|
-
|
423
|
-
|
448
|
+
# path_options << { '_brick_schema': } if
|
449
|
+
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
450
|
+
form_for(obj.becomes(#{model_name})) do |f| %>
|
424
451
|
<table>
|
425
452
|
<% has_fields = false
|
426
453
|
@#{obj_name}.first.attributes.each do |k, val| %>
|
@@ -430,25 +457,35 @@ function changeout(href, param, value) {
|
|
430
457
|
<th class=\"show-field\">
|
431
458
|
<% has_fields = true
|
432
459
|
if (bt = bts[k])
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
460
|
+
# Add a final member in this array with descriptive options to be used in <select> drop-downs
|
461
|
+
bt_name = bt[1].map { |x| x.first.name }.join('/')
|
462
|
+
# %%% Only do this if the user has permissions to edit this bt field
|
463
|
+
if (pairs = bt[1]).length > 1
|
464
|
+
poly_class_name = @#{obj_name}.first.send(\"#\{bt.first\}_type\")
|
465
|
+
bt_pair = pairs.find { |pair| pair.first.name == poly_class_name }
|
466
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
467
|
+
poly_id = @#{obj_name}.first.send(\"#\{bt.first\}_id\")
|
468
|
+
# bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
469
|
+
else # No polymorphism, so just get the first one
|
470
|
+
bt_pair = bt[1].first
|
471
|
+
end
|
472
|
+
bt_class = bt_pair.first
|
473
|
+
if bt.length < 3
|
474
|
+
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
475
|
+
# %%% Accommodate composite keys for obj.pk at the end here
|
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
|
+
end %>
|
478
|
+
BT <%= bt_class.bt_link(bt.first) %>
|
442
479
|
<% else %>
|
443
480
|
<%= k %>
|
444
481
|
<% end %>
|
445
482
|
</th>
|
446
483
|
<td>
|
447
|
-
<% if
|
484
|
+
<% if bt
|
448
485
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
449
486
|
html_options[:class] = 'dimmed' unless val %>
|
450
|
-
<%= f.select k.to_sym, bt[
|
451
|
-
<%= bt_obj =
|
487
|
+
<%= f.select k.to_sym, bt[2], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
488
|
+
<%= bt_obj = bt_class.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
452
489
|
<% else case #{model_name}.column_for_attribute(k).type
|
453
490
|
when :string, :text %>
|
454
491
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -469,12 +506,12 @@ function changeout(href, param, value) {
|
|
469
506
|
<% end %>
|
470
507
|
</td>
|
471
508
|
</tr>
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
509
|
+
<% end
|
510
|
+
if has_fields %>
|
511
|
+
<tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
|
512
|
+
<% else %>
|
513
|
+
<tr><td colspan=\"2\">(No displayable fields)</td></tr>
|
514
|
+
<% end %>
|
478
515
|
</table>
|
479
516
|
<% end %>
|
480
517
|
|
@@ -501,6 +538,7 @@ function changeout(href, param, value) {
|
|
501
538
|
#{script}"
|
502
539
|
|
503
540
|
end
|
541
|
+
puts inline
|
504
542
|
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
505
543
|
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
506
544
|
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
@@ -517,7 +555,7 @@ function changeout(href, param, value) {
|
|
517
555
|
end
|
518
556
|
|
519
557
|
# 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).
|
558
|
+
# go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
|
521
559
|
::Brick.load_additional_references
|
522
560
|
end
|
523
561
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -103,11 +103,17 @@ module Brick
|
|
103
103
|
|
104
104
|
def get_bts_and_hms(model)
|
105
105
|
bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
|
106
|
-
next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
|
106
|
+
next if (!const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name))
|
107
107
|
|
108
108
|
case a.macro
|
109
109
|
when :belongs_to
|
110
|
-
s.first[a.foreign_key] =
|
110
|
+
s.first[a.foreign_key] = if a.polymorphic?
|
111
|
+
primary_tables = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }&.last&.fetch(:inverse_table, [])
|
112
|
+
models = primary_tables&.map { |table| table.singularize.camelize.constantize }
|
113
|
+
[a.name, models]
|
114
|
+
else
|
115
|
+
[a.name, a.klass]
|
116
|
+
end
|
111
117
|
when :has_many, :has_one # This gets has_many as well as has_many :through
|
112
118
|
# %%% weed out ones that don't have an available model to reference
|
113
119
|
s.last[a.name] = a
|
@@ -256,6 +262,11 @@ module Brick
|
|
256
262
|
end
|
257
263
|
end
|
258
264
|
|
265
|
+
# Polymorphic associations
|
266
|
+
def polymorphics=(polys)
|
267
|
+
Brick.config.polymorphics = polys || {}
|
268
|
+
end
|
269
|
+
|
259
270
|
# DSL templates for individual models to provide prettier descriptions of objects
|
260
271
|
# @api public
|
261
272
|
def model_descrips=(descrips)
|
@@ -274,8 +285,18 @@ module Brick
|
|
274
285
|
def load_additional_references
|
275
286
|
return if @_additional_references_loaded
|
276
287
|
|
277
|
-
|
278
|
-
|
288
|
+
relations = ::Brick.relations
|
289
|
+
if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
|
290
|
+
ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
|
291
|
+
if (polys = ::Brick.config.polymorphics)
|
292
|
+
polys.each do |k, v|
|
293
|
+
table_name, poly = k.split('.')
|
294
|
+
v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
|
295
|
+
v.each do |type|
|
296
|
+
::Brick._add_bt_and_hm([table_name, poly, type.underscore.pluralize, "(brick) #{table_name}_#{poly}"], relations, true)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
279
300
|
@_additional_references_loaded = true
|
280
301
|
end
|
281
302
|
|
@@ -157,6 +157,10 @@ Brick.skip_index_hms = ['User.litany_of_woes']
|
|
157
157
|
# Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
|
158
158
|
# '::Snake' => 'Reptile' }
|
159
159
|
|
160
|
+
# # Polymorphic associations must be explicitly specified, which is as easy as providing a model name and polymorphic
|
161
|
+
# # association name like this:
|
162
|
+
# Brick.polymorphics = ['Comment.commentable', 'Image.imageable']
|
163
|
+
|
160
164
|
# # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
|
161
165
|
# # route to go to the :index action for what would be a controller for that table. You can specify any controller
|
162
166
|
# # 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.25
|
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-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|