brick 1.0.23 → 1.0.26
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 +17 -0
- data/lib/brick/extensions.rb +175 -119
- data/lib/brick/frameworks/rails/engine.rb +165 -118
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +54 -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: 922a55ec392e2ea9cef11c7297e4c2e4883e918130a23e49102b5a7e2442140e
|
4
|
+
data.tar.gz: 7cda213ec2e7cdccae096ef4ad8091a53a91359a1e6fabeb753cc2b579ac5c16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b817257f9c2aedd47bc2471b12e13edc8b370b71920258755f0f9a7594060e065d022e8d4b2b360cdc41f2a60def02cfc406889d526d80088670bd64acd58be
|
7
|
+
data.tar.gz: 1ea923301103dcc53853940770861ab5c2635f1280026e0740287274c0e9bb7d566ddcf96db0d3858467b788bc9ee4b73e7bcfc61145be1bbb43775cfb859b80
|
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,14 @@ 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
|
+
|
116
133
|
def skip_database_views
|
117
134
|
@mutex.synchronize { @skip_database_views }
|
118
135
|
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
|
@@ -115,8 +119,8 @@ module ActiveRecord
|
|
115
119
|
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
116
120
|
# Object property names can be referenced in square brackets like this:
|
117
121
|
# { 'User' => '[profile.firstname] [profile.lastname]' }
|
118
|
-
def brick_descrip
|
119
|
-
self.class.brick_descrip(self)
|
122
|
+
def brick_descrip(data = nil, pk_alias = nil)
|
123
|
+
self.class.brick_descrip(self, data, pk_alias)
|
120
124
|
end
|
121
125
|
|
122
126
|
def self.brick_descrip(obj, data = nil, pk_alias = nil)
|
@@ -136,11 +140,7 @@ module ActiveRecord
|
|
136
140
|
this_obj = obj
|
137
141
|
bracket_name.split('.').each do |part|
|
138
142
|
obj_name += ".#{part}"
|
139
|
-
this_obj =
|
140
|
-
caches[obj_name]
|
141
|
-
else
|
142
|
-
(caches[obj_name] = this_obj&.send(part.to_sym))
|
143
|
-
end
|
143
|
+
this_obj = caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
|
144
144
|
end
|
145
145
|
this_obj&.to_s || ''
|
146
146
|
end
|
@@ -160,7 +160,8 @@ module ActiveRecord
|
|
160
160
|
end
|
161
161
|
if is_brackets_have_content
|
162
162
|
output
|
163
|
-
elsif pk_alias
|
163
|
+
elsif (pk_alias ||= primary_key)
|
164
|
+
pk_alias = [pk_alias] unless pk_alias.is_a?(Array)
|
164
165
|
id = []
|
165
166
|
pk_alias.each do |pk_alias_part|
|
166
167
|
if (pk_part = obj.send(pk_alias_part))
|
@@ -194,7 +195,10 @@ module ActiveRecord
|
|
194
195
|
private
|
195
196
|
|
196
197
|
def self._brick_get_fks
|
197
|
-
@_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
|
198
202
|
end
|
199
203
|
end
|
200
204
|
|
@@ -295,8 +299,14 @@ module ActiveRecord
|
|
295
299
|
if is_add_bts || is_add_hms
|
296
300
|
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
297
301
|
bts.each do |_k, bt|
|
302
|
+
next if bt[2] # Polymorphic?
|
303
|
+
|
298
304
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
299
|
-
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
|
300
310
|
end
|
301
311
|
skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
|
302
312
|
hms.each do |k, hm|
|
@@ -314,7 +324,7 @@ module ActiveRecord
|
|
314
324
|
when 2
|
315
325
|
assoc_name = ks.first.to_sym
|
316
326
|
# Make sure it's a good association name and that the model has that column name
|
317
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.
|
327
|
+
next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
|
318
328
|
|
319
329
|
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
320
330
|
end
|
@@ -330,22 +340,31 @@ module ActiveRecord
|
|
330
340
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
331
341
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
332
342
|
bt_columns = bt_descrip.each_with_object([]) do |v, s|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
id_for_tables[v.first] << id_alias
|
339
|
-
end
|
340
|
-
v.last << id_for_tables[v.first]
|
341
|
-
end
|
342
|
-
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
|
343
348
|
field_tbl_name = nil
|
344
|
-
|
349
|
+
v1.map { |x|
|
350
|
+
[translations[x[0..-2].map(&:to_s).join('.')], x.last]
|
351
|
+
}.each_with_index do |sel_col, idx|
|
345
352
|
field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
|
346
|
-
|
353
|
+
|
347
354
|
selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
|
348
|
-
|
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
|
349
368
|
end
|
350
369
|
end
|
351
370
|
end
|
@@ -365,21 +384,26 @@ module ActiveRecord
|
|
365
384
|
hm.foreign_key
|
366
385
|
else
|
367
386
|
fk_col = hm.foreign_key
|
387
|
+
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
368
388
|
hm.klass.primary_key || '*'
|
369
389
|
end
|
370
390
|
tbl_alias = "_br_#{hm.name}"
|
371
391
|
pri_tbl = hm.active_record
|
392
|
+
on_clause = []
|
372
393
|
if fk_col.is_a?(Array) # Composite key?
|
373
|
-
on_clause = []
|
374
394
|
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]}" }
|
375
|
-
|
376
|
-
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}
|
377
|
-
ON #{on_clause.join(' AND ')}")
|
395
|
+
selects = fk_col.dup
|
378
396
|
else
|
379
|
-
|
380
|
-
|
381
|
-
|
397
|
+
selects = [fk_col]
|
398
|
+
on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
|
399
|
+
end
|
400
|
+
if poly_type
|
401
|
+
selects << poly_type
|
402
|
+
on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
|
382
403
|
end
|
404
|
+
join_clause = "LEFT OUTER
|
405
|
+
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}"
|
406
|
+
joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
|
383
407
|
end
|
384
408
|
where!(wheres) unless wheres.empty?
|
385
409
|
wheres unless wheres.empty? # Return the specific parameters that we did use
|
@@ -424,8 +448,7 @@ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table
|
|
424
448
|
this_module.const_get(class_name)
|
425
449
|
else
|
426
450
|
# Build STI subclass and place it into the namespace module
|
427
|
-
|
428
|
-
puts [this_module.const_set(class_name, klass = Class.new(self)).name, class_name].inspect
|
451
|
+
this_module.const_set(class_name, klass = Class.new(self))
|
429
452
|
klass
|
430
453
|
end
|
431
454
|
end
|
@@ -476,7 +499,7 @@ class Object
|
|
476
499
|
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
477
500
|
# return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
478
501
|
# If the file really exists, go and snag it:
|
479
|
-
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = self.name&.split('::'))
|
502
|
+
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
|
480
503
|
filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
|
481
504
|
end
|
482
505
|
if is_found
|
@@ -500,7 +523,7 @@ class Object
|
|
500
523
|
singular_table_name = ActiveSupport::Inflector.underscore(model_name)
|
501
524
|
|
502
525
|
# Adjust for STI if we know of a base model for the requested model name
|
503
|
-
table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base,
|
526
|
+
table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
|
504
527
|
base_model.table_name
|
505
528
|
else
|
506
529
|
ActiveSupport::Inflector.pluralize(singular_table_name)
|
@@ -541,7 +564,7 @@ class Object
|
|
541
564
|
return
|
542
565
|
end
|
543
566
|
|
544
|
-
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base,
|
567
|
+
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
|
545
568
|
is_sti = true
|
546
569
|
else
|
547
570
|
base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
|
@@ -589,77 +612,11 @@ class Object
|
|
589
612
|
hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
|
590
613
|
# The key in each hash entry (fk.first) is the constraint name
|
591
614
|
inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
597
|
-
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
598
|
-
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
599
|
-
end
|
600
|
-
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
601
|
-
else
|
602
|
-
assoc[:assoc_name]
|
603
|
-
end
|
604
|
-
need_class_name = singular_table_name.underscore != assoc_name
|
605
|
-
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
606
|
-
if (inverse = assoc[:inverse])
|
607
|
-
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
|
608
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
609
|
-
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
610
|
-
need_inverse_of = true
|
611
|
-
has_ones[singular_inv_assoc_name]
|
612
|
-
else
|
613
|
-
singular_inv_assoc_name
|
614
|
-
end
|
615
|
-
end
|
616
|
-
end
|
617
|
-
:belongs_to
|
618
|
-
else
|
619
|
-
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
620
|
-
# Are there multiple foreign keys out to the same table?
|
621
|
-
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
622
|
-
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
623
|
-
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
624
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
625
|
-
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
626
|
-
need_class_name = custom_assoc_name != singular_assoc_name
|
627
|
-
custom_assoc_name
|
628
|
-
else
|
629
|
-
singular_assoc_name
|
630
|
-
end
|
631
|
-
:has_one
|
632
|
-
else
|
633
|
-
:has_many
|
634
|
-
end
|
635
|
-
end
|
636
|
-
# Figure out if we need to specially call out the class_name and/or foreign key
|
637
|
-
# (and if either of those then definitely also a specific inverse_of)
|
638
|
-
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
639
|
-
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
640
|
-
if need_fk # Funky foreign key?
|
641
|
-
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
642
|
-
assoc_fk = assoc[:fk].uniq
|
643
|
-
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
644
|
-
else
|
645
|
-
assoc[:fk].to_sym
|
646
|
-
end
|
647
|
-
end
|
648
|
-
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
649
|
-
|
650
|
-
# Prepare a list of entries for "has_many :through"
|
651
|
-
if macro == :has_many
|
652
|
-
relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
|
653
|
-
next if k == assoc[:fk]
|
654
|
-
|
655
|
-
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
656
|
-
end
|
615
|
+
if (invs = assoc[:inverse_table]).is_a?(Array)
|
616
|
+
invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
|
617
|
+
else
|
618
|
+
build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
|
657
619
|
end
|
658
|
-
|
659
|
-
# And finally create a has_one, has_many, or belongs_to for this association
|
660
|
-
assoc_name = assoc_name.to_sym
|
661
|
-
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
662
|
-
self.send(macro, assoc_name, **options)
|
663
620
|
hmts
|
664
621
|
end
|
665
622
|
hmts.each do |hmt_fk, fks|
|
@@ -698,6 +655,88 @@ class Object
|
|
698
655
|
[built_model, code]
|
699
656
|
end
|
700
657
|
|
658
|
+
def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
|
659
|
+
singular_table_name = inverse_table&.singularize
|
660
|
+
options = {}
|
661
|
+
macro = if assoc[:is_bt]
|
662
|
+
# Try to take care of screwy names if this is a belongs_to going to an STI subclass
|
663
|
+
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
664
|
+
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
665
|
+
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
666
|
+
end
|
667
|
+
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
668
|
+
else
|
669
|
+
assoc[:assoc_name]
|
670
|
+
end
|
671
|
+
if assoc.key?(:polymorphic)
|
672
|
+
options[:polymorphic] = true
|
673
|
+
else
|
674
|
+
need_class_name = singular_table_name.underscore != assoc_name
|
675
|
+
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
676
|
+
end
|
677
|
+
if (inverse = assoc[:inverse])
|
678
|
+
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
|
679
|
+
has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
|
680
|
+
if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
681
|
+
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
682
|
+
need_inverse_of = true
|
683
|
+
has_ones[singular_inv_assoc_name]
|
684
|
+
else
|
685
|
+
singular_inv_assoc_name
|
686
|
+
end
|
687
|
+
end
|
688
|
+
end
|
689
|
+
:belongs_to
|
690
|
+
else
|
691
|
+
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
692
|
+
# Are there multiple foreign keys out to the same table?
|
693
|
+
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
694
|
+
if assoc.key?(:polymorphic)
|
695
|
+
options[:as] = assoc[:fk].to_sym
|
696
|
+
else
|
697
|
+
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
698
|
+
end
|
699
|
+
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
700
|
+
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
701
|
+
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
702
|
+
need_class_name = custom_assoc_name != singular_assoc_name
|
703
|
+
custom_assoc_name
|
704
|
+
else
|
705
|
+
singular_assoc_name
|
706
|
+
end
|
707
|
+
:has_one
|
708
|
+
else
|
709
|
+
:has_many
|
710
|
+
end
|
711
|
+
end
|
712
|
+
# Figure out if we need to specially call out the class_name and/or foreign key
|
713
|
+
# (and if either of those then definitely also a specific inverse_of)
|
714
|
+
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
715
|
+
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
716
|
+
if need_fk # Funky foreign key?
|
717
|
+
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
718
|
+
assoc_fk = assoc[:fk].uniq
|
719
|
+
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
720
|
+
else
|
721
|
+
assoc[:fk].to_sym
|
722
|
+
end
|
723
|
+
end
|
724
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
725
|
+
|
726
|
+
# Prepare a list of entries for "has_many :through"
|
727
|
+
if macro == :has_many
|
728
|
+
relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
|
729
|
+
next if k == assoc[:fk]
|
730
|
+
|
731
|
+
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
732
|
+
end
|
733
|
+
end
|
734
|
+
# And finally create a has_one, has_many, or belongs_to for this association
|
735
|
+
assoc_name = assoc_name.to_sym
|
736
|
+
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
737
|
+
self.send(macro, assoc_name, **options)
|
738
|
+
end
|
739
|
+
|
701
740
|
def build_controller(class_name, plural_class_name, model, relations)
|
702
741
|
table_name = ActiveSupport::Inflector.underscore(plural_class_name)
|
703
742
|
singular_table_name = ActiveSupport::Inflector.singularize(table_name)
|
@@ -1010,12 +1049,13 @@ module Brick
|
|
1010
1049
|
# rubocop:enable Style/CommentedKeyword
|
1011
1050
|
|
1012
1051
|
class << self
|
1013
|
-
def _add_bt_and_hm(fk, relations =
|
1014
|
-
relations ||= ::Brick.relations
|
1052
|
+
def _add_bt_and_hm(fk, relations, is_polymorphic = false)
|
1015
1053
|
bt_assoc_name = fk[1].underscore
|
1016
1054
|
bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
|
1017
1055
|
|
1018
1056
|
bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
|
1057
|
+
# %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
|
1058
|
+
# Maybe it's already gotten this info because we got as far as to say there was a unique class
|
1019
1059
|
primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
|
1020
1060
|
hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
|
1021
1061
|
|
@@ -1032,7 +1072,7 @@ module Brick
|
|
1032
1072
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
|
1033
1073
|
return
|
1034
1074
|
end
|
1035
|
-
unless (cols = relations[fk[0]][:cols]).key?(fk[1])
|
1075
|
+
unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
|
1036
1076
|
columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
|
1037
1077
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
|
1038
1078
|
return
|
@@ -1047,10 +1087,21 @@ module Brick
|
|
1047
1087
|
end
|
1048
1088
|
end
|
1049
1089
|
if (assoc_bt = bts[cnstr_name])
|
1050
|
-
|
1051
|
-
|
1090
|
+
if is_polymorphic
|
1091
|
+
# Assuming same fk (don't yet support composite keys for polymorphics)
|
1092
|
+
assoc_bt[:inverse_table] << fk[2]
|
1093
|
+
else # Expect we could have a composite key going
|
1094
|
+
if assoc_bt[:fk].is_a?(String)
|
1095
|
+
assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
|
1096
|
+
elsif assoc_bt[:fk].exclude?(fk[1])
|
1097
|
+
assoc_bt[:fk] << fk[1]
|
1098
|
+
end
|
1099
|
+
assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
|
1100
|
+
end
|
1052
1101
|
else
|
1053
|
-
|
1102
|
+
inverse_table = [primary_table] if is_polymorphic
|
1103
|
+
assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
|
1104
|
+
assoc_bt[:polymorphic] = true if is_polymorphic
|
1054
1105
|
end
|
1055
1106
|
if is_class
|
1056
1107
|
# For use in finding the proper :source for a HMT association that references an STI subclass
|
@@ -1062,11 +1113,16 @@ module Brick
|
|
1062
1113
|
return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
|
1063
1114
|
|
1064
1115
|
if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
|
1065
|
-
|
1116
|
+
if assoc_bt[:fk].is_a?(String)
|
1117
|
+
assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
|
1118
|
+
elsif assoc_bt[:fk].exclude?(fk[1])
|
1119
|
+
assoc_bt[:fk] << fk[1]
|
1120
|
+
end
|
1066
1121
|
assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
|
1067
1122
|
assoc_hm[:inverse] = assoc_bt
|
1068
1123
|
else
|
1069
1124
|
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 }
|
1125
|
+
assoc_hm[:polymorphic] = true if is_polymorphic
|
1070
1126
|
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
1071
1127
|
hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
|
1072
1128
|
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,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
|
@@ -274,108 +291,112 @@ function changeout(href, param, value) {
|
|
274
291
|
template_link = "
|
275
292
|
<%= link_to 'CSV', #{table_name}_path(format: :csv) %> <a href=\"#\" id=\"sheetsLink\">Sheets</a>
|
276
293
|
<div id=\"dropper\" contenteditable=\"true\"></div>
|
277
|
-
<input type=\"button\" id=\"btnImport\" value=\"Import\">
|
278
|
-
|
279
|
-
"#{css}
|
280
|
-
<p style=\"color: green\"><%= notice %></p>#{"
|
281
|
-
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
282
|
-
<select id=\"tbl\">#{table_options}</select>
|
283
|
-
<h1>#{model_name.pluralize}</h1>#{template_link}
|
294
|
+
<input type=\"button\" id=\"btnImport\" value=\"Import\">
|
295
|
+
|
284
296
|
<script>
|
285
|
-
var dropperDiv = document.getElementById(\"dropper\");
|
286
|
-
var btnImport = document.getElementById(\"btnImport\");
|
287
|
-
var droppedTSV;
|
288
|
-
if (dropperDiv) { // Other interesting events: blur keyup input
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
});
|
296
|
-
btnImport.addEventListener(\"click\", function () {
|
297
|
-
fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
|
298
|
-
method: 'PATCH',
|
299
|
-
headers: { 'Content-Type': 'text/tab-separated-values' },
|
300
|
-
body: droppedTSV
|
301
|
-
}).then(function (tsvResponse) {
|
302
|
-
btnImport.style.display = \"none\";
|
303
|
-
console.log(\"toaster\", tsvResponse);
|
297
|
+
var dropperDiv = document.getElementById(\"dropper\");
|
298
|
+
var btnImport = document.getElementById(\"btnImport\");
|
299
|
+
var droppedTSV;
|
300
|
+
if (dropperDiv) { // Other interesting events: blur keyup input
|
301
|
+
dropperDiv.addEventListener(\"paste\", function (evt) {
|
302
|
+
droppedTSV = evt.clipboardData.getData('text/plain');
|
303
|
+
var html = evt.clipboardData.getData('text/html');
|
304
|
+
var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
|
305
|
+
console.log(tbl);
|
306
|
+
btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
|
304
307
|
});
|
305
|
-
|
306
|
-
}
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
function
|
311
|
-
|
312
|
-
|
313
|
-
evt.preventDefault();
|
314
|
-
await gapi.load(\"client\", function () {
|
315
|
-
gapi.client.init({ // Load the discovery doc to initialize the API
|
316
|
-
clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
|
317
|
-
scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
|
318
|
-
discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
|
319
|
-
}).then(function () {
|
320
|
-
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
|
321
|
-
updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
|
308
|
+
btnImport.addEventListener(\"click\", function () {
|
309
|
+
fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
|
310
|
+
method: 'PATCH',
|
311
|
+
headers: { 'Content-Type': 'text/tab-separated-values' },
|
312
|
+
body: droppedTSV
|
313
|
+
}).then(function (tsvResponse) {
|
314
|
+
btnImport.style.display = \"none\";
|
315
|
+
console.log(\"toaster\", tsvResponse);
|
322
316
|
});
|
323
317
|
});
|
324
|
-
}
|
325
|
-
|
318
|
+
}
|
319
|
+
var sheetUrl;
|
320
|
+
var spreadsheetId;
|
321
|
+
var sheetsLink = document.getElementById(\"sheetsLink\");
|
322
|
+
function gapiLoaded() {
|
323
|
+
// Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
|
324
|
+
sheetsLink.addEventListener(\"click\", async function (evt) {
|
325
|
+
evt.preventDefault();
|
326
|
+
await gapi.load(\"client\", function () {
|
327
|
+
gapi.client.init({ // Load the discovery doc to initialize the API
|
328
|
+
clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
|
329
|
+
scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
|
330
|
+
discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
|
331
|
+
}).then(function () {
|
332
|
+
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
|
333
|
+
updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
|
334
|
+
});
|
335
|
+
});
|
336
|
+
});
|
337
|
+
}
|
326
338
|
|
327
|
-
async function updateSignInStatus(isSignedIn) {
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
// console.log(\"beefcake\", response2);
|
339
|
+
async function updateSignInStatus(isSignedIn) {
|
340
|
+
if (isSignedIn) {
|
341
|
+
console.log(\"turds!\");
|
342
|
+
await gapi.client.sheets.spreadsheets.create({
|
343
|
+
properties: {
|
344
|
+
title: #{table_name.inspect},
|
345
|
+
},
|
346
|
+
sheets: [
|
347
|
+
// sheet1, sheet2, sheet3
|
348
|
+
]
|
349
|
+
}).then(function (response) {
|
350
|
+
sheetUrl = response.result.spreadsheetUrl;
|
351
|
+
spreadsheetId = response.result.spreadsheetId;
|
352
|
+
sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
|
353
|
+
console.log(\"x1\", sheetUrl);
|
354
|
+
|
355
|
+
// Get JSON data
|
356
|
+
fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
|
357
|
+
response.json().then(function (data) {
|
358
|
+
gapi.client.sheets.spreadsheets.values.append({
|
359
|
+
spreadsheetId: spreadsheetId,
|
360
|
+
range: \"Sheet1\",
|
361
|
+
valueInputOption: \"RAW\",
|
362
|
+
insertDataOption: \"INSERT_ROWS\"
|
363
|
+
}, {
|
364
|
+
range: \"Sheet1\",
|
365
|
+
majorDimension: \"ROWS\",
|
366
|
+
values: data,
|
367
|
+
}).then(function (response2) {
|
368
|
+
// console.log(\"beefcake\", response2);
|
369
|
+
});
|
357
370
|
});
|
358
371
|
});
|
359
372
|
});
|
360
|
-
|
361
|
-
|
373
|
+
window.open(sheetUrl, '_blank');
|
374
|
+
}
|
362
375
|
}
|
363
|
-
}
|
364
376
|
</script>
|
365
377
|
<script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
|
366
|
-
|
378
|
+
"
|
379
|
+
end
|
380
|
+
"#{css}
|
381
|
+
<p style=\"color: green\"><%= notice %></p>#{"
|
382
|
+
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
383
|
+
<select id=\"tbl\">#{table_options}</select>
|
384
|
+
<h1>#{model_name.pluralize}</h1>#{template_link}
|
367
385
|
|
368
386
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
369
387
|
<table id=\"#{table_name}\">
|
370
388
|
<thead><tr>#{'<th></th>' if pk}
|
371
389
|
<% @#{table_name}.columns.map(&:name).each do |col| %>
|
372
|
-
<% 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) %>
|
373
391
|
<th>
|
374
392
|
<% if (bt = bts[col]) %>
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
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 %>
|
379
400
|
</th>
|
380
401
|
<% end %>
|
381
402
|
<%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
|
@@ -387,21 +408,30 @@ async function updateSignInStatus(isSignedIn) {
|
|
387
408
|
<tr>#{"
|
388
409
|
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
389
410
|
<% #{obj_name}.attributes.each do |k, val| %>
|
390
|
-
<% 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'))) %>
|
391
412
|
<td>
|
392
413
|
<% if (bt = bts[k]) %>
|
393
414
|
<%# binding.pry # Postgres column names are limited to 63 characters %>
|
394
|
-
<%
|
395
|
-
|
396
|
-
|
397
|
-
|
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 %>
|
398
429
|
<% else %>
|
399
430
|
<%= hide_bcrypt(val) %>
|
400
431
|
<% end %>
|
401
432
|
</td>
|
402
433
|
<% end %>
|
403
|
-
|
404
|
-
<!-- td>X</td -->
|
434
|
+
#{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
|
405
435
|
</tr>
|
406
436
|
</tbody>
|
407
437
|
<% end %>
|
@@ -418,35 +448,47 @@ async function updateSignInStatus(isSignedIn) {
|
|
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
|
456
|
+
@#{obj_name}.first.attributes.each do |k, val| %>
|
426
457
|
<tr>
|
427
458
|
<%# %%% Accommodate composite keys %>
|
428
459
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
429
460
|
<th class=\"show-field\">
|
430
|
-
<%
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
bt
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
461
|
+
<% has_fields = true
|
462
|
+
if (bt = bts[k])
|
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}.first.send(\"#\{bt.first\}_type\")
|
468
|
+
bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
|
469
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
470
|
+
poly_id = @#{obj_name}.first.send(\"#\{bt.first\}_id\")
|
471
|
+
# bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
472
|
+
else # No polymorphism, so just get the first one
|
473
|
+
bt_pair = bt[1].first
|
474
|
+
end
|
475
|
+
bt_class = bt_pair.first
|
476
|
+
if bt.length < 4
|
477
|
+
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
478
|
+
# %%% Accommodate composite keys for obj.pk at the end here
|
479
|
+
bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
480
|
+
end %>
|
481
|
+
BT <%= bt_class.bt_link(bt.first) %>
|
440
482
|
<% else %>
|
441
483
|
<%= k %>
|
442
484
|
<% end %>
|
443
485
|
</th>
|
444
486
|
<td>
|
445
|
-
<% if
|
487
|
+
<% if bt
|
446
488
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
447
489
|
html_options[:class] = 'dimmed' unless val %>
|
448
490
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
449
|
-
<%= bt_obj =
|
491
|
+
<%= 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 %>
|
450
492
|
<% else case #{model_name}.column_for_attribute(k).type
|
451
493
|
when :string, :text %>
|
452
494
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -467,8 +509,12 @@ async function updateSignInStatus(isSignedIn) {
|
|
467
509
|
<% end %>
|
468
510
|
</td>
|
469
511
|
</tr>
|
470
|
-
|
512
|
+
<% end
|
513
|
+
if has_fields %>
|
471
514
|
<tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
|
515
|
+
<% else %>
|
516
|
+
<tr><td colspan=\"2\">(No displayable fields)</td></tr>
|
517
|
+
<% end %>
|
472
518
|
</table>
|
473
519
|
<% end %>
|
474
520
|
|
@@ -482,6 +528,7 @@ async function updateSignInStatus(isSignedIn) {
|
|
482
528
|
<tr><td>(none)</td></tr>
|
483
529
|
<% else %>
|
484
530
|
<% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
|
531
|
+
<%# %%% accommodate composite primary key %>
|
485
532
|
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
|
486
533
|
<% end %>
|
487
534
|
<% end %>
|
@@ -510,7 +557,7 @@ async function updateSignInStatus(isSignedIn) {
|
|
510
557
|
end
|
511
558
|
|
512
559
|
# Just in case it hadn't been done previously when we tried to load the brick initialiser,
|
513
|
-
# go make sure we've loaded additional references (virtual foreign keys).
|
560
|
+
# go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
|
514
561
|
::Brick.load_additional_references
|
515
562
|
end
|
516
563
|
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,48 @@ 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
|
+
|
271
294
|
# Load additional references (virtual foreign keys)
|
272
295
|
# 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
296
|
# %%% Maybe look for differences the second time 'round and just add new stuff instead of entirely deferring
|
274
297
|
def load_additional_references
|
275
298
|
return if @_additional_references_loaded
|
276
299
|
|
277
|
-
|
278
|
-
|
300
|
+
relations = ::Brick.relations
|
301
|
+
if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
|
302
|
+
ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
|
303
|
+
if (polys = ::Brick.config.polymorphics)
|
304
|
+
if (schema = ::Brick.config.schema_to_analyse) && ::Brick.db_schemas&.include?(schema)
|
305
|
+
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
|
306
|
+
end
|
307
|
+
missing_stis = {}
|
308
|
+
polys.each do |k, v|
|
309
|
+
table_name, poly = k.split('.')
|
310
|
+
v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
|
311
|
+
v.each do |type|
|
312
|
+
if relations.key?(primary_table = type.underscore.pluralize)
|
313
|
+
::Brick._add_bt_and_hm([table_name, poly, primary_table, "(brick) #{table_name}_#{poly}"], relations, true)
|
314
|
+
else
|
315
|
+
missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
unless missing_stis.empty?
|
320
|
+
print "
|
321
|
+
You might be missing an STI namespace prefix entry for these tables: #{missing_stis.keys.join(', ')}.
|
322
|
+
In config/initializers/brick.rb appropriate entries would look something like:
|
323
|
+
Brick.sti_namespace_prefixes = {"
|
324
|
+
puts missing_stis.map { |_k, missing_sti| "\n '::#{missing_sti}' => 'YourParentModel'" }.join(',')
|
325
|
+
puts " }
|
326
|
+
(Just trade out YourParentModel with some more appropriate one.)"
|
327
|
+
end
|
328
|
+
end
|
279
329
|
@_additional_references_loaded = true
|
280
330
|
end
|
281
331
|
|
@@ -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.26
|
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
|