brick 1.0.14 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09600b354a3f70f8680f3dd7a9be9349e6666c013c09a7c9a0dc240e811344ee'
4
- data.tar.gz: 0f6ec90f3d38338797a10ec4fc338fae6fdea5b9fddb376c2044fe719afaaa8b
3
+ metadata.gz: 7f1a45a5262526e69cf49f16773049eb348e3971bddf09ecf05927002a265019
4
+ data.tar.gz: e9566a423a19ab55d44522b44e8b4abfcc39e92335b6093fe782c6344644bbbe
5
5
  SHA512:
6
- metadata.gz: 7189638a98c3899d49473a48638dce78da4ed109ed1d4a69185015f9b19428d658fa40ef0f70aa1652a478754b6c1b683a963424ec3e627c56e97b3c2528bba7
7
- data.tar.gz: a843f0ec11b7e40bc4cf0a7387ab33e87edd4d9a36de1e24d10c899d969e148015da346fe7b7915f9ffd5a769818e8948de36beb1a368d38a6bb3ab3a97ca7c5
6
+ metadata.gz: cb89dff96dccc7051bc854fe523b03f818255d81e4f2e2a65eb9808115e719b8bad98e38114f15ee79b32b1f035c95c364a74392948cd2e99f4030e6988e0217
7
+ data.tar.gz: cd5507fde2f1f23481696ab051d14f89e5e7e7d3fc2be97af947ff287313b7ada8ca7b4e3f9e0e54dc2ab7f14e7f39a3c18f78cf7fc995c58b3d60cf68487973
data/lib/brick/config.rb CHANGED
@@ -66,12 +66,12 @@ module Brick
66
66
  end
67
67
 
68
68
  # Skip creating a has_many association for these
69
- def skip_hms
70
- @mutex.synchronize { @skip_hms }
69
+ def exclude_hms
70
+ @mutex.synchronize { @exclude_hms }
71
71
  end
72
72
 
73
- def skip_hms=(skips)
74
- @mutex.synchronize { @skip_hms = skips }
73
+ def exclude_hms=(skips)
74
+ @mutex.synchronize { @exclude_hms = skips }
75
75
  end
76
76
 
77
77
  # Associations to treat as a has_one
@@ -115,6 +115,14 @@ module Brick
115
115
  @mutex.synchronize { @exclude_tables = value }
116
116
  end
117
117
 
118
+ def models_inherit_from
119
+ @mutex.synchronize { @models_inherit_from }
120
+ end
121
+
122
+ def models_inherit_from=(value)
123
+ @mutex.synchronize { @models_inherit_from = value }
124
+ end
125
+
118
126
  def table_name_prefixes
119
127
  @mutex.synchronize { @table_name_prefixes }
120
128
  end
@@ -130,5 +138,13 @@ module Brick
130
138
  def metadata_columns=(columns)
131
139
  @mutex.synchronize { @metadata_columns = columns }
132
140
  end
141
+
142
+ def not_nullables
143
+ @mutex.synchronize { @not_nullables }
144
+ end
145
+
146
+ def not_nullables=(columns)
147
+ @mutex.synchronize { @not_nullables = columns }
148
+ end
133
149
  end
134
150
  end
@@ -137,6 +137,7 @@ module ActiveRecord
137
137
  alias _brick_find_sti_class find_sti_class
138
138
  def find_sti_class(type_name)
139
139
  if ::Brick.sti_models.key?(type_name)
140
+ # puts ['X', self.name, type_name].inspect
140
141
  _brick_find_sti_class(type_name)
141
142
  else
142
143
  # This auto-STI is more of a brute-force approach, building modules where needed
@@ -145,8 +146,8 @@ module ActiveRecord
145
146
  module_prefixes = type_name.split('::')
146
147
  module_prefixes.unshift('') unless module_prefixes.first.blank?
147
148
  module_name = module_prefixes[0..-2].join('::')
148
- if ::Brick.config.sti_namespace_prefixes&.key?(module_name) ||
149
- ::Brick.config.sti_namespace_prefixes&.key?(module_name[2..-1]) # Take off the leading '::' and see if this matches
149
+ if ::Brick.config.sti_namespace_prefixes&.key?("::#{module_name}::") ||
150
+ ::Brick.config.sti_namespace_prefixes&.key?("#{module_name}::")
150
151
  _brick_find_sti_class(type_name)
151
152
  elsif File.exists?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
152
153
  _brick_find_sti_class(type_name) # Find this STI class normally
@@ -154,15 +155,20 @@ module ActiveRecord
154
155
  # Build missing prefix modules if they don't yet exist
155
156
  this_module = Object
156
157
  module_prefixes[1..-2].each do |module_name|
157
- mod = if this_module.const_defined?(module_name)
158
- this_module.const_get(module_name)
159
- else
160
- this_module.const_set(module_name.to_sym, Module.new)
161
- end
158
+ this_module = if this_module.const_defined?(module_name)
159
+ this_module.const_get(module_name)
160
+ else
161
+ this_module.const_set(module_name.to_sym, Module.new)
162
+ end
163
+ end
164
+ if this_module.const_defined?(class_name = module_prefixes.last.to_sym)
165
+ this_module.const_get(class_name)
166
+ else
167
+ # Build STI subclass and place it into the namespace module
168
+ # %%% Does this ever get used???
169
+ puts [this_module.const_set(class_name, klass = Class.new(self)).name, class_name].inspect
170
+ klass
162
171
  end
163
- # Build STI subclass and place it into the namespace module
164
- this_module.const_set(module_prefixes.last.to_sym, klass = Class.new(self))
165
- klass
166
172
  end
167
173
  end
168
174
  end
@@ -170,22 +176,27 @@ module ActiveRecord
170
176
  end
171
177
  end
172
178
 
173
- module ActiveSupport::Dependencies
174
- class << self
175
- # %%% Probably a little more targeted than other approaches we've taken thusfar
176
- # This happens before the whole parent check
177
- alias _brick_autoload_module! autoload_module!
178
- def autoload_module!(*args)
179
- into, const_name, qualified_name, path_suffix = args
180
- if (base_class = ::Brick.config.sti_namespace_prefixes&.fetch(into.name, nil)&.constantize)
181
- ::Brick.sti_models[qualified_name] = { base: base_class }
182
- # Build subclass and place it into the specially STI-namespaced module
183
- into.const_set(const_name.to_sym, klass = Class.new(base_class))
184
- # %%% used to also have: autoload_once_paths.include?(base_path) ||
185
- autoloaded_constants << qualified_name unless autoloaded_constants.include?(qualified_name)
186
- klass
187
- else
188
- _brick_autoload_module!(*args)
179
+ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works with previous non-zeitwerk auto-loading
180
+ module ActiveSupport::Dependencies
181
+ class << self
182
+ # %%% Probably a little more targeted than other approaches we've taken thusfar
183
+ # This happens before the whole parent check
184
+ alias _brick_autoload_module! autoload_module!
185
+ def autoload_module!(*args)
186
+ into, const_name, qualified_name, path_suffix = args
187
+ if (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)&.constantize)
188
+ ::Brick.sti_models[qualified_name] = { base: base_class }
189
+ # Build subclass and place it into the specially STI-namespaced module
190
+ into.const_set(const_name.to_sym, klass = Class.new(base_class))
191
+ # %%% used to also have: autoload_once_paths.include?(base_path) ||
192
+ autoloaded_constants << qualified_name unless autoloaded_constants.include?(qualified_name)
193
+ klass
194
+ elsif (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{const_name}", nil)&.constantize)
195
+ # Build subclass and place it into Object
196
+ Object.const_set(const_name.to_sym, klass = Class.new(base_class))
197
+ else
198
+ _brick_autoload_module!(*args)
199
+ end
189
200
  end
190
201
  end
191
202
  end
@@ -246,15 +257,14 @@ class Object
246
257
  built_class, code = result
247
258
  puts "\n#{code}"
248
259
  built_class
249
- elsif ::Brick.config.sti_namespace_prefixes&.key?(class_name)
250
- # binding.pry
260
+ elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
251
261
  # module_prefixes = type_name.split('::')
252
262
  # path = self.name.split('::')[0..-2] + []
253
263
  # module_prefixes.unshift('') unless module_prefixes.first.blank?
254
264
  # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
255
265
  self._brick_const_missing(*args)
256
266
  else
257
- puts "MISSING! #{args.inspect} #{table_name}"
267
+ puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
258
268
  self._brick_const_missing(*args)
259
269
  end
260
270
  end
@@ -267,13 +277,15 @@ class Object
267
277
 
268
278
  # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
269
279
  if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
270
- puts "Warning: Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\"."
280
+ unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.titleize}::")
281
+ puts "Warning: Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\"."
282
+ end
271
283
  return
272
284
  end
273
285
  if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
274
286
  is_sti = true
275
287
  else
276
- base_model = ActiveRecord::Base
288
+ base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
277
289
  end
278
290
  code = +"class #{model_name} < #{base_model.name}\n"
279
291
  built_model = Class.new(base_model) do |new_model_class|
@@ -322,6 +334,11 @@ class Object
322
334
  options = {}
323
335
  singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
324
336
  macro = if assoc[:is_bt]
337
+ # Try to take care of screwy names if this is a belongs_to going to an STI subclass
338
+ if (primary_class = assoc.fetch(:primary_class, nil)) &&
339
+ (sti_inverse_assoc = primary_class.reflect_on_all_associations.find { |a| a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key })
340
+ assoc_name = sti_inverse_assoc.options[:inverse_of].to_s || assoc_name
341
+ end
325
342
  need_class_name = singular_table_name.underscore != assoc_name
326
343
  need_fk = "#{assoc_name}_id" != assoc[:fk]
327
344
  if (inverse = assoc[:inverse])
@@ -356,7 +373,7 @@ class Object
356
373
  end
357
374
  # Figure out if we need to specially call out the class_name and/or foreign key
358
375
  # (and if either of those then definitely also a specific inverse_of)
359
- options[:class_name] = singular_table_name.camelize if need_class_name
376
+ options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
360
377
  # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
361
378
  if need_fk # Funky foreign key?
362
379
  options[:foreign_key] = if assoc[:fk].is_a?(Array)
@@ -387,12 +404,13 @@ class Object
387
404
  fks.each do |fk|
388
405
  source = nil
389
406
  this_hmt_fk = if fks.length > 1
390
- singular_assoc_name = ActiveSupport::Inflector.singularize(fk.first[:inverse][:assoc_name])
407
+ singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
391
408
  source = fk.last
392
- through = ActiveSupport::Inflector.pluralize(fk.first[:alternate_name])
409
+ through = fk.first[:alternate_name].pluralize
393
410
  "#{singular_assoc_name}_#{hmt_fk}"
394
411
  else
395
- through = fk.first[:assoc_name]
412
+ source = fk.last unless hmt_fk.singularize == fk.last
413
+ through = fk.first[:assoc_name].pluralize
396
414
  hmt_fk
397
415
  end
398
416
  code << " has_many :#{this_hmt_fk}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
@@ -401,6 +419,14 @@ class Object
401
419
  self.send(:has_many, this_hmt_fk.to_sym, **options)
402
420
  end
403
421
  end
422
+ # Not NULLables
423
+ relation[:cols].each do |col, datatype|
424
+ if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
425
+ ::Brick.config.not_nullables.include?("#{matching}.#{col}")
426
+ code << " validates :#{col}, presence: true\n"
427
+ self.send(:validates, col.to_sym, { presence: true })
428
+ end
429
+ end
404
430
  end
405
431
  code << "end # model #{model_name}\n\n"
406
432
  end # class definition
@@ -496,7 +522,8 @@ module ActiveRecord::ConnectionHandling
496
522
  "SELECT t.table_name AS relation_name, t.table_type,
497
523
  c.column_name, c.data_type,
498
524
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
499
- tc.constraint_type AS const, kcu.constraint_name AS \"key\"
525
+ tc.constraint_type AS const, kcu.constraint_name AS \"key\",
526
+ c.is_nullable
500
527
  FROM INFORMATION_SCHEMA.tables AS t
501
528
  LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
502
529
  AND t.table_name = c.table_name
@@ -534,7 +561,7 @@ module ActiveRecord::ConnectionHandling
534
561
  end
535
562
  key << col_name if key
536
563
  cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
537
- cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name)]
564
+ cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
538
565
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
539
566
  end
540
567
  else # MySQL2 acts a little differently, bringing back an array for each row
@@ -602,7 +629,11 @@ module ActiveRecord::ConnectionHandling
602
629
  puts "\nClasses that can be built from views:"
603
630
  views.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
604
631
  end
605
- # pp relations; nil
632
+ # Try to load the initializer pretty danged early
633
+ if File.exist?(brick_initialiser = Rails.root.join('config/initializers/brick.rb'))
634
+ load brick_initialiser
635
+ ::Brick.load_additional_references
636
+ end
606
637
 
607
638
  # relations.keys.each { |k| ActiveSupport::Inflector.singularize(k).camelize.constantize }
608
639
  # Layout table describes permissioned hierarchy throughout
@@ -638,16 +669,17 @@ module Brick
638
669
  bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
639
670
 
640
671
  bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
641
- hms = (relation = relations.fetch(fk[2], nil))&.fetch(:fks) { relation[:fks] = {} }
672
+ primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
673
+ hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
642
674
 
643
675
  unless (cnstr_name = fk[3])
644
676
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
645
- cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{fk[2]}"
677
+ cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{is_class ? fk[2][:class].underscore : fk[2]}"
646
678
  cnstr_added_num = 1
647
679
  cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
648
680
  missing = []
649
681
  missing << fk[0] unless relations.key?(fk[0])
650
- missing << fk[2] unless relations.key?(fk[2])
682
+ missing << primary_table unless is_class || relations.key?(primary_table)
651
683
  unless missing.empty?
652
684
  tables = relations.reject { |k, v| v.fetch(:isView, nil) }.keys.sort
653
685
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
@@ -658,8 +690,12 @@ module Brick
658
690
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
659
691
  return
660
692
  end
661
- if (redundant = bts.find { |k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == fk[2] })
662
- puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed. (Already established by #{redundant.first}.)"
693
+ if (redundant = bts.find { |k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == primary_table })
694
+ if is_class && !redundant.last.key?(:class)
695
+ redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
696
+ else
697
+ puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed. (Already established by #{redundant.first}.)"
698
+ end
663
699
  return
664
700
  end
665
701
  end
@@ -667,10 +703,16 @@ module Brick
667
703
  assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
668
704
  assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
669
705
  else
670
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: fk[2] }
706
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: primary_table }
707
+ end
708
+ if is_class
709
+ # For use in finding the proper :source for a HMT association that references an STI subclass
710
+ assoc_bt[:primary_class] = primary_class
711
+ # For use in finding the proper :inverse_of for a BT association that references an STI subclass
712
+ # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
671
713
  end
672
714
 
673
- unless ::Brick.config.skip_hms&.any? { |skip| fk[0] == skip[0] && fk[1] == skip[1] && fk[2] == skip[2] }
715
+ unless is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
674
716
  cnstr_name = "hm_#{cnstr_name}"
675
717
  if (assoc_hm = hms.fetch(cnstr_name, nil))
676
718
  assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
@@ -685,15 +727,5 @@ module Brick
685
727
  end
686
728
  # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
687
729
  end
688
-
689
- # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
690
- ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
691
- class NoUniqueColumnError < ar_not_unique_error
692
- end
693
-
694
- # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
695
- ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
696
- class LessThanHalfAreMatchingColumnsError < ar_invalid_error
697
- end
698
730
  end
699
731
  end
@@ -16,17 +16,23 @@ module Brick
16
16
  # Specific database tables and views to omit when auto-creating models
17
17
  ::Brick.exclude_tables = app.config.brick.fetch(:exclude_tables, [])
18
18
 
19
+ # Class for auto-generated models to inherit from
20
+ ::Brick.models_inherit_from = app.config.brick.fetch(:models_inherit_from, ActiveRecord::Base)
21
+
19
22
  # When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
20
23
  ::Brick.table_name_prefixes = app.config.brick.fetch(:table_name_prefixes, [])
21
24
 
22
25
  # Columns to treat as being metadata for purposes of identifying associative tables for has_many :through
23
26
  ::Brick.metadata_columns = app.config.brick.fetch(:metadata_columns, ['created_at', 'updated_at', 'deleted_at'])
24
27
 
28
+ # Columns for which to add a validate presence: true even though the database doesn't have them marked as NOT NULL
29
+ ::Brick.not_nullables = app.config.brick.fetch(:not_nullables, [])
30
+
25
31
  # Additional references (virtual foreign keys)
26
32
  ::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
27
33
 
28
34
  # Skip creating a has_many association for these
29
- ::Brick.skip_hms = app.config.brick.fetch(:skip_hms, nil)
35
+ ::Brick.exclude_hms = app.config.brick.fetch(:exclude_hms, nil)
30
36
 
31
37
  # Has one relationships
32
38
  ::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
@@ -70,34 +76,34 @@ module Brick
70
76
  bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
71
77
  # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
72
78
  # as well as any possible polymorphic associations
73
- skip_hms = {}
79
+ exclude_hms = {}
74
80
  associatives = hms.each_with_object({}) do |hmt, s|
75
81
  if (through = hmt.last.options[:through])
76
- skip_hms[through] = nil
82
+ exclude_hms[through] = nil
77
83
  s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
78
84
  elsif hmt.last.inverse_of.nil?
79
85
  puts "SKIPPING #{hmt.last.name.inspect}"
80
86
  # %%% If we don't do this then below associative.name will find that associative is nil
81
- skip_hms[hmt.last.name] = nil
87
+ exclude_hms[hmt.last.name] = nil
82
88
  end
83
89
  end
84
90
 
85
91
  schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
92
+ table_options = ::Brick.relations.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
86
93
  hms_columns = +'' # Used for 'index'
87
- # puts skip_hms.inspect
88
94
  hms_headers = hms.each_with_object([]) do |hm, s|
89
- next if skip_hms.key?(hm.last.name)
95
+ next if exclude_hms.key?((hm_assoc = hm.last).name)
90
96
 
91
97
  if args.first == 'index'
92
- hm_fk_name = if hm.last.options[:through]
93
- associative = associatives[hm.last.name]
98
+ hm_fk_name = if hm_assoc.options[:through]
99
+ associative = associatives[hm_assoc.name]
94
100
  "'#{associative.name}.#{associative.foreign_key}'"
95
101
  else
96
- hm.last.foreign_key
102
+ hm_assoc.foreign_key
97
103
  end
98
- hms_columns << if hm.last.macro == :has_many
104
+ hms_columns << if hm_assoc.macro == :has_many
99
105
  "<td>
100
- <%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm.last.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless #{obj_name}.#{hm.first}.count.zero? %>
106
+ <%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless #{obj_name}.#{hm.first}.count.zero? %>
101
107
  </td>\n"
102
108
  else # has_one
103
109
  "<td>
@@ -105,7 +111,7 @@ module Brick
105
111
  </td>\n"
106
112
  end
107
113
  end
108
- s << [hm.last, "H#{hm.last.macro == :has_one ? 'O' : 'M'}#{'T' if hm.last.options[:through]} #{hm.first}"]
114
+ s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
109
115
  end
110
116
 
111
117
  css = "<style>
@@ -150,10 +156,20 @@ table tbody tr.active-row {
150
156
  }
151
157
 
152
158
  a.show-arrow {
159
+ font-size: 1.5em;
160
+ text-decoration: none;
161
+ }
162
+ a.big-arrow {
153
163
  font-size: 2.5em;
154
164
  text-decoration: none;
155
165
  }
156
- </style>"
166
+ </style>
167
+ <% def is_bcrypt?(val)
168
+ val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
169
+ end
170
+ def hide_bcrypt(val)
171
+ is_bcrypt?(val) ? '(hidden)' : val
172
+ end %>"
157
173
 
158
174
  script = "<script>
159
175
  var schemaSelect = document.getElementById(\"schema\");
@@ -168,8 +184,28 @@ if (schemaSelect) {
168
184
  location.href = changeout(location.href, \"_brick_schema\", this.value);
169
185
  });
170
186
  }
187
+
188
+ var tblSelect = document.getElementById(\"tbl\");
189
+ if (tblSelect) {
190
+ tblSelect.value = changeout(location.href);
191
+ tblSelect.addEventListener(\"change\", function () {
192
+ var lhr = changeout(location.href, null, this.value);
193
+ if (brickSchema)
194
+ lhr = changeout(lhr, \"_brick_schema\", schemaSelect.value);
195
+ location.href = lhr;
196
+ });
197
+ }
198
+
171
199
  function changeout(href, param, value) {
172
200
  var hrefParts = href.split(\"?\");
201
+ if (param === undefined || param === null) {
202
+ hrefParts = hrefParts[0].split(\"://\");
203
+ var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
204
+ if (value === undefined)
205
+ return pathParts[1];
206
+ else
207
+ return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
208
+ }
173
209
  var params = hrefParts.length > 1 ? hrefParts[1].split(\"&\") : [];
174
210
  params = params.reduce(function (s, v) { var parts = v.split(\"=\"); s[parts[0]] = parts[1]; return s; }, {});
175
211
  if (value === undefined) return params[param];
@@ -183,6 +219,7 @@ function changeout(href, param, value) {
183
219
  "#{css}
184
220
  <p style=\"color: green\"><%= notice %></p>#{"
185
221
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
222
+ <select id=\"tbl\">#{table_options}</select>
186
223
  <h1>#{model_name.pluralize}</h1>
187
224
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
188
225
  <table id=\"#{table_name}\">
@@ -204,7 +241,7 @@ function changeout(href, param, value) {
204
241
  <tbody>
205
242
  <% @#{table_name}.each do |#{obj_name}| %>
206
243
  <tr>#{"
207
- <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'show-arrow' } %></td>" if pk }
244
+ <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk }
208
245
  <% #{obj_name}.attributes.each do |k, val| %>
209
246
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
210
247
  <td>
@@ -213,9 +250,9 @@ function changeout(href, param, value) {
213
250
  # send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))
214
251
  # Otherwise we get stuff like:
215
252
  # ActionView::Template::Error (undefined method `vehicle_path' for #<ActionView::Base:0x0000000033a888>) %>
216
- <%= bt_obj = bt[1].find_by(bt.last => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
253
+ <%= bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
217
254
  <% else %>
218
- <%= val %>
255
+ <%= hide_bcrypt(val) %>
219
256
  <% end %>
220
257
  </td>
221
258
  <% end %>
@@ -232,40 +269,51 @@ function changeout(href, param, value) {
232
269
  "#{css}
233
270
  <p style=\"color: green\"><%= notice %></p>#{"
234
271
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
272
+ <select id=\"tbl\">#{table_options}</select>
235
273
  <h1>#{model_name}: <%= (obj = @#{obj_name}.first).brick_descrip %></h1>
236
274
  <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
275
+ <%= form_for obj do |f| %>
237
276
  <table>
238
277
  <% bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} }
239
278
  @#{obj_name}.first.attributes.each do |k, val| %>
240
279
  <tr>
241
280
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
242
281
  <th class=\"show-field\">
243
- <% if (bt = bts[k]) %>
282
+ <% if (bt = bts[k])
283
+ # Add a final member in this array with descriptive options to be used in <select> drop-downs
284
+ # %%% Only do this if the user has permissions to edit this bt field
285
+ bt << bt[1].order(:#{pk}).map { |obj| [obj.brick_descrip, obj.#{pk}] } if bt.length < 4 %>
244
286
  BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
245
287
  <% else %>
246
288
  <%= k %>
247
289
  <% end %>
248
290
  </th>
249
291
  <td>
250
- <% if (bt = bts[k]) %>
251
- <%= bt_obj = bt[1].find_by(bt.last => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
292
+ <% if (bt = bts[k]) # bt_obj.brick_descrip %>
293
+ <%= f.select k.to_sym, bt[3], {}, prompt: 'Select #{model_name}' %>
294
+ <%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
295
+ <% elsif is_bcrypt?(val) %>
296
+ <%= hide_bcrypt(val) %>
252
297
  <% else %>
253
- <%= val %>
298
+ <%= f.text_field k.to_sym %>
254
299
  <% end %>
255
300
  </td>
256
301
  </tr>
257
302
  <% end %>
258
303
  </table>
304
+ <% end %>
259
305
 
260
306
  #{hms_headers.map do |hm|
261
307
  next unless (pk = hm.first.klass.primary_key)
262
308
  "<table id=\"#{hm_name = hm.first.name.to_s}\">
263
309
  <tr><th>#{hm.last}</th></tr>
264
- <% if (collection = @#{obj_name}.first.#{hm_name}).empty? %>
310
+ <% collection = @#{obj_name}.first.#{hm_name}
311
+ collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
312
+ if collection.empty? %>
265
313
  <tr><td>(none)</td></tr>
266
314
  <% else %>
267
- <% collection.order(#{pk.inspect}).uniq.each do |#{hm_singular_name = hm_name.singularize}| %>
268
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm_singular_name}_path(#{hm_singular_name}.#{pk})) %></td></tr>
315
+ <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
316
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
269
317
  <% end %>
270
318
  <% end %>
271
319
  </table>" end.join}
@@ -285,33 +333,14 @@ function changeout(href, param, value) {
285
333
 
286
334
  if ::Brick.enable_routes? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
287
335
  ActionDispatch::Routing::RouteSet.class_exec do
288
- alias _brick_finalize_routeset! finalize!
289
- def finalize!(*args, **options)
290
- unless @finalized
291
- existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
292
- ::Rails.application.routes.append do
293
- # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
294
- # If auto-controllers and auto-models are both enabled then this makes sense:
295
- ::Brick.relations.each do |k, v|
296
- unless existing_controllers.key?(controller_name = k.underscore.pluralize)
297
- options = {}
298
- options[:only] = [:index, :show] if v.key?(:isView)
299
- send(:resources, controller_name.to_sym, **options)
300
- end
301
- end
302
- end
303
- end
304
- _brick_finalize_routeset!(*args, **options)
305
- end
336
+ # In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
337
+ prepend ::Brick::RouteSet
306
338
  end
307
339
  end
308
340
 
309
- # Additional references (virtual foreign keys)
310
- if (ars = ::Brick.config.additional_references)
311
- ars.each do |fk|
312
- ::Brick._add_bt_and_hm(fk[0..2])
313
- end
314
- end
341
+ # Just in case it hadn't been done previously when we tried to load the brick initialiser,
342
+ # go make sure we've loaded additional references (virtual foreign keys).
343
+ ::Brick.load_additional_references
315
344
 
316
345
  # Find associative tables that can be set up for has_many :through
317
346
  ::Brick.relations.each do |_key, tbl|
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 14
8
+ TINY = 17
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -174,6 +174,11 @@ module Brick
174
174
  Brick.config.exclude_tables = value
175
175
  end
176
176
 
177
+ # @api public
178
+ def models_inherit_from=(value)
179
+ Brick.config.models_inherit_from = value
180
+ end
181
+
177
182
  # @api public
178
183
  def table_name_prefixes=(value)
179
184
  Brick.config.table_name_prefixes = value
@@ -184,6 +189,11 @@ module Brick
184
189
  Brick.config.metadata_columns = value
185
190
  end
186
191
 
192
+ # @api public
193
+ def not_nullables=(value)
194
+ Brick.config.not_nullables = value
195
+ end
196
+
187
197
  # Additional table associations to use (Think of these as virtual foreign keys perhaps)
188
198
  # @api public
189
199
  def additional_references=(ars)
@@ -198,12 +208,12 @@ module Brick
198
208
  # Skip creating a has_many association for these
199
209
  # (Uses the same exact three-part format as would define an additional_reference)
200
210
  # @api public
201
- def skip_hms=(skips)
211
+ def exclude_hms=(skips)
202
212
  if skips
203
213
  skips = skips.call if skips.is_a?(Proc)
204
214
  skips = skips.to_a unless skips.is_a?(Array)
205
215
  skips = [skips] unless skips.empty? || skips.first.is_a?(Array)
206
- Brick.config.skip_hms = skips
216
+ Brick.config.exclude_hms = skips
207
217
  end
208
218
  end
209
219
 
@@ -234,6 +244,18 @@ module Brick
234
244
  Brick.config.sti_namespace_prefixes = snp
235
245
  end
236
246
 
247
+ # Load additional references (virtual foreign keys)
248
+ # 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
249
+ # %%% Maybe look for differences the second time 'round and just add new stuff instead of entirely deferring
250
+ def load_additional_references
251
+ return if @_additional_references_loaded
252
+
253
+ if (ars = ::Brick.config.additional_references)
254
+ ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
255
+ @_additional_references_loaded = true
256
+ end
257
+ end
258
+
237
259
 
238
260
  # Returns Brick's `::Gem::Version`, convenient for comparisons. This is
239
261
  # recommended over `::Brick::VERSION::STRING`.
@@ -269,6 +291,25 @@ module Brick
269
291
  VERSION::STRING
270
292
  end
271
293
  end
294
+
295
+ module RouteSet
296
+ def finalize!
297
+ existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
298
+ ::Rails.application.routes.append do
299
+ # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
300
+ # If auto-controllers and auto-models are both enabled then this makes sense:
301
+ ::Brick.relations.each do |k, v|
302
+ unless existing_controllers.key?(controller_name = k.underscore.pluralize)
303
+ options = {}
304
+ options[:only] = [:index, :show] if v.key?(:isView)
305
+ send(:resources, controller_name.to_sym, **options)
306
+ end
307
+ end
308
+ end
309
+ super
310
+ end
311
+ end
312
+
272
313
  end
273
314
 
274
315
  require 'brick/version_number'
@@ -94,6 +94,9 @@ module Brick
94
94
  # # Any tables or views you'd like to skip when auto-creating models
95
95
  # Brick.exclude_tables = ['custom_metadata', 'version_info']
96
96
 
97
+ # # Class that auto-generated models should inherit from
98
+ # Brick.models_inherit_from = ApplicationRecord
99
+
97
100
  # # When table names have specific prefixes automatically place them in their own module with a table_name_prefix.
98
101
  # Brick.table_name_prefixes = { 'nav_' => 'Navigation' }
99
102
 
@@ -111,7 +114,7 @@ module Brick
111
114
  # # Skip creating a has_many association for these
112
115
  # # (Uses the same exact three-part format as would define an additional_reference)
113
116
  # # Say for instance that we didn't care to display the favourite colours that users have:
114
- # Brick.skip_hms = [['users', 'favourite_colour_id', 'colours']]
117
+ # Brick.exclude_hms = [['users', 'favourite_colour_id', 'colours']]
115
118
 
116
119
  # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
117
120
  # # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
@@ -128,6 +131,10 @@ module Brick
128
131
  # # database:
129
132
  # Brick.metadata_columns = ['last_update']
130
133
 
134
+ # # Columns for which to add a validate presence: true even though the database doesn't have them marked as NOT NULL.
135
+ # # Designated by <table name>.<column name>
136
+ # Brick.not_nullables = ['users.name']
137
+
131
138
  # # A simple DSL is available to allow more user-friendly display of objects. Normally a user object might be shown
132
139
  # # as its first non-metadata column, or if that is not available then something like \"User #45\" where 45 is that
133
140
  # # object's ID. If there is no primary key then even that is not possible, so the object's .to_s method is called.
@@ -135,10 +142,15 @@ module Brick
135
142
  # # user, then you can use model_descrips like this, putting expressions with property references in square brackets:
136
143
  # Brick.model_descrips = { 'User' => '[profile.firstname] [profile.lastname]' }
137
144
 
138
- # # Module prefixes to be built out and associated when new subclasses are requested for specific base STI models
139
- # # (If this setting exists then only these subclasses will be honoured, and other requested )
140
- # # The prefixed :: here on the sample module name LetterTemplates is optional. Used here for clarity.
141
- # Brick.sti_namespace_prefixes = { '::LetterTemplates' => 'LetterTemplate' }
145
+ # # Specify STI subclasses either directly by name or as a general module prefix that should always relate to a specific
146
+ # # parent STI class. The prefixed :: here for these examples is mandatory. Also having a suffixed :: means instead of
147
+ # # a class reference, this is for a general namespace reference. So in this case requests for, say, either of the
148
+ # # non-existant classes Animals::Cat or Animals::Goat (or anything else with the module prefix of \"Animals::\" would
149
+ # # build a model that inherits from Animal. And a request specifically for the class Snake would build a new model
150
+ # # that inherits from Reptile, and no other request would do this -- only specifically for Snake. The ending ::
151
+ # # indicates that it's a module prefix instead of a specific class name.
152
+ # Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
153
+ # '::Snake' => 'Reptile' }
142
154
 
143
155
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
144
156
  # # route to go to the :index action for what would be a controller for that table. You can specify any controller
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.14
4
+ version: 1.0.17
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-03-23 00:00:00.000000000 Z
11
+ date: 2022-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord