brick 1.0.8 → 1.0.12

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: 7e06e6aaa9b7fef7730d56404912ab186a29f6e698f6dbb0d9c551cf2008c0b1
4
- data.tar.gz: 7e546071063729d5b85b2f146bb401c7cbfc462f6dea0a701ed8c839cb883de5
3
+ metadata.gz: f7c3e4032e5e9f111839b7ff6e6378e4937edeabeb0edcf480cf6c50e79e599a
4
+ data.tar.gz: 1a1e5b096a3f6fff0f2c49c6be6abcfbe012f3e24fccf0ea6b307b58d9cbc076
5
5
  SHA512:
6
- metadata.gz: 83499e7959f98bb323f44593f6472ba1996815ed5f286a119f5adfbd2b690be3f9cd3556166cff3efda250fd609b6d06f4054aea599b0e9587191b4cb1d7a3cb
7
- data.tar.gz: 6ded71697d4cce4d931afb54869639d6c2d0b32462806c0c0c7826ceb46fd5f7d7c2476f29e5c9302c83482e0293916dd82e553fa847482de52c39e06dc2d210
6
+ metadata.gz: 4dbd885a2c4dc9666378a69423179ca2085c89b6b6120d21ef8d3274f229a88306f54868b8a18aad8e55a37a71bc9b78dff073263bb86fd89d5b982acf8d5210
7
+ data.tar.gz: 952f94f89445170f26fad10357ede0df7c9053d034ddb552b1e9cba884cbdf0325325cb29a2248fac4e9de3bd8c41619fc9612d44435e6ccb808a16fd018d8dc
data/lib/brick/config.rb CHANGED
@@ -65,13 +65,30 @@ module Brick
65
65
  @mutex.synchronize { @additional_references = references }
66
66
  end
67
67
 
68
+ # Skip creating a has_many association for these
69
+ def skip_hms
70
+ @mutex.synchronize { @skip_hms }
71
+ end
72
+
73
+ def skip_hms=(skips)
74
+ @mutex.synchronize { @skip_hms = skips }
75
+ end
76
+
68
77
  # Associations to treat as a has_one
69
78
  def has_ones
70
79
  @mutex.synchronize { @has_ones }
71
80
  end
72
81
 
73
- def has_ones=(references)
74
- @mutex.synchronize { @has_ones = references }
82
+ def has_ones=(hos)
83
+ @mutex.synchronize { @has_ones = hos }
84
+ end
85
+
86
+ def model_descrips
87
+ @mutex.synchronize { @model_descrips ||= {} }
88
+ end
89
+
90
+ def model_descrips=(descrips)
91
+ @mutex.synchronize { @model_descrips = descrips }
75
92
  end
76
93
 
77
94
  def skip_database_views
@@ -90,6 +107,14 @@ module Brick
90
107
  @mutex.synchronize { @exclude_tables = value }
91
108
  end
92
109
 
110
+ def table_name_prefixes
111
+ @mutex.synchronize { @table_name_prefixes }
112
+ end
113
+
114
+ def table_name_prefixes=(value)
115
+ @mutex.synchronize { @table_name_prefixes = value }
116
+ end
117
+
93
118
  def metadata_columns
94
119
  @mutex.synchronize { @metadata_columns }
95
120
  end
@@ -46,7 +46,56 @@ module ActiveRecord
46
46
  # Used to show a little prettier name for an object
47
47
  def brick_descrip
48
48
  klass = self.class
49
- klass.primary_key ? "#{klass.name} ##{send(klass.primary_key)}" : to_s
49
+ # If available, parse simple DSL attached to a model in order to provide a friendlier name.
50
+ # Object property names can be referenced in square brackets like this:
51
+ # { 'User' => '[profile.firstname] [profile.lastname]' }
52
+
53
+ # If there's no DSL yet specified, just try to find the first usable column on this model
54
+ unless ::Brick.config.model_descrips[klass.name]
55
+ descrip_col = (klass.columns.map(&:name) - klass._brick_get_fks -
56
+ (::Brick.config.metadata_columns || []) -
57
+ [klass.primary_key]).first
58
+ ::Brick.config.model_descrips[klass.name] = "[#{descrip_col}]" if descrip_col
59
+ end
60
+ if (dsl ||= ::Brick.config.model_descrips[klass.name])
61
+ caches = {}
62
+ output = +''
63
+ is_brackets_have_content = false
64
+ bracket_name = nil
65
+ dsl.each_char do |ch|
66
+ if bracket_name
67
+ if ch == ']' # Time to process a bracketed thing?
68
+ obj_name = +''
69
+ obj = self
70
+ bracket_name.split('.').each do |part|
71
+ obj_name += ".#{part}"
72
+ obj = if caches.key?(obj_name)
73
+ caches[obj_name]
74
+ else
75
+ (caches[obj_name] = obj&.send(part.to_sym))
76
+ end
77
+ end
78
+ is_brackets_have_content = true unless (obj&.to_s).blank?
79
+ output << (obj&.to_s || '')
80
+ bracket_name = nil
81
+ else
82
+ bracket_name << ch
83
+ end
84
+ elsif ch == '['
85
+ bracket_name = +''
86
+ else
87
+ output << ch
88
+ end
89
+ end
90
+ output += bracket_name if bracket_name
91
+ end
92
+ if is_brackets_have_content
93
+ output
94
+ elsif klass.primary_key
95
+ "#{klass.name} ##{send(klass.primary_key)}"
96
+ else
97
+ to_s
98
+ end
50
99
  end
51
100
 
52
101
  private
@@ -88,7 +137,26 @@ module ActiveRecord
88
137
  alias _brick_find_sti_class find_sti_class
89
138
  def find_sti_class(type_name)
90
139
  ::Brick.sti_models[type_name] = { base: self } unless type_name.blank?
91
- _brick_find_sti_class(type_name)
140
+ module_prefixes = type_name.split('::')
141
+ module_prefixes.unshift('') unless module_prefixes.first.blank?
142
+ candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
143
+ if File.exists?(candidate_file)
144
+ # Find this STI class normally
145
+ _brick_find_sti_class(type_name)
146
+ else
147
+ # Build missing prefix modules if they don't yet exist
148
+ this_module = Object
149
+ module_prefixes[1..-2].each do |module_name|
150
+ mod = if this_module.const_defined?(module_name)
151
+ this_module.const_get(module_name)
152
+ else
153
+ this_module.const_set(module_name.to_sym, Module.new)
154
+ end
155
+ end
156
+ # Build missing prefix modules if they don't yet exist
157
+ this_module.const_set(module_prefixes.last.to_sym, klass = Class.new(self))
158
+ klass
159
+ end
92
160
  end
93
161
  end
94
162
  end
@@ -112,7 +180,8 @@ class Object
112
180
  return Object._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
113
181
 
114
182
  relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
115
- result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
183
+ is_controllers_enabled = Rails.development? || ::Brick.enable_controllers?
184
+ result = if is_controllers_enabled && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
116
185
  # Otherwise now it's up to us to fill in the gaps
117
186
  if (model = ActiveSupport::Inflector.singularize(plural_class_name).constantize)
118
187
  # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
@@ -123,12 +192,13 @@ class Object
123
192
  # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
124
193
  plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
125
194
  singular_table_name = ActiveSupport::Inflector.underscore(model_name)
126
- table_name = ActiveSupport::Inflector.pluralize(singular_table_name)
127
195
 
128
196
  # Adjust for STI if we know of a base model for the requested model name
129
- if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
130
- table_name = base_model.table_name
131
- end
197
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
198
+ base_model.table_name
199
+ else
200
+ ActiveSupport::Inflector.pluralize(singular_table_name)
201
+ end
132
202
 
133
203
  # Maybe, just maybe there's a database table that will satisfy this need
134
204
  if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
@@ -155,7 +225,11 @@ class Object
155
225
  if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
156
226
  raise NameError.new("Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\".")
157
227
  end
158
- base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ActiveRecord::Base
228
+ if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
229
+ is_sti = true
230
+ else
231
+ base_model = ActiveRecord::Base
232
+ end
159
233
  code = +"class #{model_name} < #{base_model.name}\n"
160
234
  built_model = Class.new(base_model) do |new_model_class|
161
235
  Object.const_set(model_name.to_sym, new_model_class)
@@ -193,93 +267,96 @@ class Object
193
267
  code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
194
268
  end
195
269
 
196
- fks = relation[:fks] || {}
197
- # Do the bulk of the has_many / belongs_to processing, and store details about HMT so they can be done at the very last
198
- hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
199
- # The key in each hash entry (fk.first) is the constraint name
200
- assoc_name = (assoc = fk.last)[:assoc_name]
201
- inverse_assoc_name = assoc[:inverse][:assoc_name]
202
- options = {}
203
- singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
204
- macro = if assoc[:is_bt]
205
- need_class_name = singular_table_name.underscore != assoc_name
206
- need_fk = "#{assoc_name}_id" != assoc[:fk]
207
- inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], assoc[:inverse])
208
- if (has_ones = ::Brick.config.has_ones&.fetch(assoc[:inverse][:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
209
- inverse_assoc_name = if has_ones[singular_inv_assoc_name]
210
- need_inverse_of = true
211
- has_ones[singular_inv_assoc_name]
212
- else
213
- singular_inv_assoc_name
214
- end
215
- end
216
- :belongs_to
217
- else
218
- # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
219
- # Are there multiple foreign keys out to the same table?
220
- assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
221
- need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
222
- # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
223
- if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
224
- assoc_name = if has_ones[singular_assoc_name]
225
- need_class_name = true
226
- has_ones[singular_assoc_name]
227
- else
228
- singular_assoc_name
229
- end
230
- :has_one
270
+ unless is_sti
271
+ fks = relation[:fks] || {}
272
+ # Do the bulk of the has_many / belongs_to processing, and store details about HMT so they can be done at the very last
273
+ hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
274
+ # The key in each hash entry (fk.first) is the constraint name
275
+ assoc_name = (assoc = fk.last)[:assoc_name]
276
+ inverse_assoc_name = assoc[:inverse]&.fetch(:assoc_name, nil)
277
+ options = {}
278
+ singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
279
+ macro = if assoc[:is_bt]
280
+ need_class_name = singular_table_name.underscore != assoc_name
281
+ need_fk = "#{assoc_name}_id" != assoc[:fk]
282
+ if (inverse = assoc[:inverse])
283
+ inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
284
+ if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
285
+ inverse_assoc_name = if has_ones[singular_inv_assoc_name]
286
+ need_inverse_of = true
287
+ has_ones[singular_inv_assoc_name]
288
+ else
289
+ singular_inv_assoc_name
290
+ end
291
+ end
292
+ end
293
+ :belongs_to
231
294
  else
232
- :has_many
233
- end
234
- end
235
- # Figure out if we need to specially call out the class_name and/or foreign key
236
- # (and if either of those then definitely also a specific inverse_of)
237
- options[:class_name] = singular_table_name.camelize if need_class_name
238
- # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
239
- if need_fk # Funky foreign key?
240
- options[:foreign_key] = if assoc[:fk].is_a?(Array)
241
- assoc_fk = assoc[:fk].uniq
242
- assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
295
+ # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
296
+ # Are there multiple foreign keys out to the same table?
297
+ assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
298
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
299
+ # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
300
+ if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
301
+ assoc_name = if has_ones[singular_assoc_name]
302
+ need_class_name = true
303
+ has_ones[singular_assoc_name]
243
304
  else
244
- assoc[:fk].to_sym
305
+ singular_assoc_name
245
306
  end
246
- end
247
- options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk || need_inverse_of
307
+ :has_one
308
+ else
309
+ :has_many
310
+ end
311
+ end
312
+ # Figure out if we need to specially call out the class_name and/or foreign key
313
+ # (and if either of those then definitely also a specific inverse_of)
314
+ options[:class_name] = singular_table_name.camelize if need_class_name
315
+ # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
316
+ if need_fk # Funky foreign key?
317
+ options[:foreign_key] = if assoc[:fk].is_a?(Array)
318
+ assoc_fk = assoc[:fk].uniq
319
+ assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
320
+ else
321
+ assoc[:fk].to_sym
322
+ end
323
+ end
324
+ options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
248
325
 
249
- # Prepare a list of entries for "has_many :through"
250
- if macro == :has_many
251
- relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
252
- next if k == assoc[:fk]
326
+ # Prepare a list of entries for "has_many :through"
327
+ if macro == :has_many
328
+ relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
329
+ next if k == assoc[:fk]
253
330
 
254
- hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
331
+ hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
332
+ end
255
333
  end
256
- end
257
334
 
258
- # And finally create a has_one, has_many, or belongs_to for this association
259
- assoc_name = assoc_name.to_sym
260
- code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
261
- self.send(macro, assoc_name, **options)
262
- hmts
263
- end
264
- hmts.each do |hmt_fk, fks|
265
- fks.each do |fk|
266
- source = nil
267
- this_hmt_fk = if fks.length > 1
268
- singular_assoc_name = ActiveSupport::Inflector.singularize(fk.first[:inverse][:assoc_name])
269
- source = fk.last
270
- through = ActiveSupport::Inflector.pluralize(fk.first[:alternate_name])
271
- "#{singular_assoc_name}_#{hmt_fk}"
272
- else
273
- through = fk.first[:assoc_name]
274
- hmt_fk
275
- end
276
- code << " has_many :#{this_hmt_fk}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
277
- options = { through: assoc_name }
278
- options[:source] = source.to_sym if source
279
- self.send(:has_many, this_hmt_fk.to_sym, **options)
335
+ # And finally create a has_one, has_many, or belongs_to for this association
336
+ assoc_name = assoc_name.to_sym
337
+ code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
338
+ self.send(macro, assoc_name, **options)
339
+ hmts
340
+ end
341
+ hmts.each do |hmt_fk, fks|
342
+ fks.each do |fk|
343
+ source = nil
344
+ this_hmt_fk = if fks.length > 1
345
+ singular_assoc_name = ActiveSupport::Inflector.singularize(fk.first[:inverse][:assoc_name])
346
+ source = fk.last
347
+ through = ActiveSupport::Inflector.pluralize(fk.first[:alternate_name])
348
+ "#{singular_assoc_name}_#{hmt_fk}"
349
+ else
350
+ through = fk.first[:assoc_name]
351
+ hmt_fk
352
+ end
353
+ code << " has_many :#{this_hmt_fk}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
354
+ options = { through: assoc_name }
355
+ options[:source] = source.to_sym if source
356
+ self.send(:has_many, this_hmt_fk.to_sym, **options)
357
+ end
280
358
  end
281
359
  end
282
-
283
360
  code << "end # model #{model_name}\n\n"
284
361
  end # class definition
285
362
  [built_model, code]
@@ -298,6 +375,7 @@ class Object
298
375
  code << " @#{table_name}.brick_where(params)\n"
299
376
  code << " end\n"
300
377
  self.define_method :index do
378
+ ::Brick.set_db_schema(params)
301
379
  ar_relation = model.primary_key ? model.order(model.primary_key) : model.all
302
380
  instance_variable_set(:@_brick_params, ar_relation.brick_where(params))
303
381
  instance_variable_set("@#{table_name}".to_sym, ar_relation)
@@ -308,6 +386,7 @@ class Object
308
386
  code << " @#{singular_table_name} = #{model.name}.find(params[:id].split(','))\n"
309
387
  code << " end\n"
310
388
  self.define_method :show do
389
+ ::Brick.set_db_schema(params)
311
390
  instance_variable_set("@#{singular_table_name}".to_sym, model.find(params[:id].split(',')))
312
391
  end
313
392
  end
@@ -349,9 +428,11 @@ module ActiveRecord::ConnectionHandling
349
428
  # Only for Postgres? (Doesn't work in sqlite3)
350
429
  # puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
351
430
 
352
- case ActiveRecord::Base.connection.adapter_name
431
+ schema_sql = 'SELECT NULL AS table_schema;'
432
+ case ActiveRecord::Base.connection.adapter_name
353
433
  when 'PostgreSQL'
354
434
  schema = 'public'
435
+ schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
355
436
  when 'Mysql2'
356
437
  schema = ActiveRecord::Base.connection.current_database
357
438
  when 'SQLite'
@@ -459,6 +540,10 @@ module ActiveRecord::ConnectionHandling
459
540
  else
460
541
  end
461
542
  if sql
543
+ ::Brick.db_schemas = ActiveRecord::Base.connection.execute(schema_sql)
544
+ ::Brick.db_schemas = ::Brick.db_schemas.to_a unless ::Brick.db_schemas.is_a?(Array)
545
+ ::Brick.db_schemas.map! { |row| row['table_schema'] } unless ::Brick.db_schemas.empty? || ::Brick.db_schemas.first.is_a?(String)
546
+ ::Brick.db_schemas -= ['information_schema', 'pg_catalog']
462
547
  ActiveRecord::Base.connection.execute(sql).each do |fk|
463
548
  fk = fk.values unless fk.is_a?(Array)
464
549
  ::Brick._add_bt_and_hm(fk, relations)
@@ -466,10 +551,12 @@ module ActiveRecord::ConnectionHandling
466
551
  end
467
552
  end
468
553
 
469
- puts "Classes that can be built from tables:"
554
+ puts "\nClasses that can be built from tables:"
470
555
  relations.select { |_k, v| !v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
471
- puts "Classes that can be built from views:"
472
- relations.select { |_k, v| v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
556
+ unless (views = relations.select { |_k, v| v.key?(:isView) }).empty?
557
+ puts "\nClasses that can be built from views:"
558
+ views.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
559
+ end
473
560
  # pp relations; nil
474
561
 
475
562
  # relations.keys.each { |k| ActiveSupport::Inflector.singularize(k).camelize.constantize }
@@ -499,65 +586,69 @@ module Brick
499
586
  end # module Extensions
500
587
  # rubocop:enable Style/CommentedKeyword
501
588
 
502
- def self._add_bt_and_hm(fk, relations = nil)
503
- relations ||= ::Brick.relations
504
- bt_assoc_name = fk[1].underscore
505
- bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
506
-
507
- bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
508
- hms = (relation = relations.fetch(fk[2], nil))&.fetch(:fks) { relation[:fks] = {} }
509
-
510
- unless (cnstr_name = fk[3])
511
- # For any appended references (those that come from config), arrive upon a definitely unique constraint name
512
- cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{fk[2]}"
513
- cnstr_added_num = 1
514
- cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
515
- missing = []
516
- missing << fk[0] unless relations.key?(fk[0])
517
- missing << fk[2] unless relations.key?(fk[2])
518
- unless missing.empty?
519
- tables = relations.reject { |k, v| v.fetch(:isView, nil) }.keys.sort
520
- puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
521
- return
589
+ class << self
590
+ def _add_bt_and_hm(fk, relations = nil)
591
+ relations ||= ::Brick.relations
592
+ bt_assoc_name = fk[1].underscore
593
+ bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
594
+
595
+ bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
596
+ hms = (relation = relations.fetch(fk[2], nil))&.fetch(:fks) { relation[:fks] = {} }
597
+
598
+ unless (cnstr_name = fk[3])
599
+ # For any appended references (those that come from config), arrive upon a definitely unique constraint name
600
+ cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{fk[2]}"
601
+ cnstr_added_num = 1
602
+ cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
603
+ missing = []
604
+ missing << fk[0] unless relations.key?(fk[0])
605
+ missing << fk[2] unless relations.key?(fk[2])
606
+ unless missing.empty?
607
+ tables = relations.reject { |k, v| v.fetch(:isView, nil) }.keys.sort
608
+ puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
609
+ return
610
+ end
611
+ unless (cols = relations[fk[0]][:cols]).key?(fk[1])
612
+ columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
613
+ puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
614
+ return
615
+ end
616
+ if (redundant = bts.find { |k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == fk[2] })
617
+ puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed. (Already established by #{redundant.first}.)"
618
+ return
619
+ end
522
620
  end
523
- unless (cols = relations[fk[0]][:cols]).key?(fk[1])
524
- columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
525
- puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
526
- return
621
+ if (assoc_bt = bts[cnstr_name])
622
+ assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
623
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
624
+ else
625
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: fk[2] }
527
626
  end
528
- if (redundant = bts.find{|k, v| v[:inverse][:inverse_table] == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == fk[2] })
529
- puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed. (Already established by #{redundant.first}.)"
530
- return
627
+
628
+ unless ::Brick.config.skip_hms&.any? { |skip| fk[0] == skip[0] && fk[1] == skip[1] && fk[2] == skip[2] }
629
+ cnstr_name = "hm_#{cnstr_name}"
630
+ if (assoc_hm = hms.fetch(cnstr_name, nil))
631
+ assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
632
+ assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
633
+ assoc_hm[:inverse] = assoc_bt
634
+ else
635
+ assoc_hm = hms[cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
636
+ hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
637
+ hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
638
+ end
639
+ assoc_bt[:inverse] = assoc_hm
531
640
  end
641
+ # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
532
642
  end
533
643
 
534
- if (assoc_bt = bts[cnstr_name])
535
- assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
536
- assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
537
- else
538
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: fk[2] }
644
+ # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
645
+ ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
646
+ class NoUniqueColumnError < ar_not_unique_error
539
647
  end
540
648
 
541
- if (assoc_hm = hms[cnstr_name])
542
- assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
543
- assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
544
- assoc_hm[:inverse] = assoc_bt
545
- else
546
- assoc_hm = hms[cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
547
- hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
548
- hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
649
+ # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
650
+ ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
651
+ class LessThanHalfAreMatchingColumnsError < ar_invalid_error
549
652
  end
550
- assoc_bt[:inverse] = assoc_hm
551
- # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
552
- end
553
-
554
- # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
555
- ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
556
- class NoUniqueColumnError < ar_not_unique_error
557
- end
558
-
559
- # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
560
- ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
561
- class LessThanHalfAreMatchingColumnsError < ar_invalid_error
562
653
  end
563
654
  end
@@ -8,20 +8,26 @@ module Brick
8
8
  config.brick = ActiveSupport::OrderedOptions.new
9
9
  ActiveSupport.on_load(:before_initialize) do |app|
10
10
  ::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
11
- ::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, true)
12
- ::Brick.enable_views = app.config.brick.fetch(:enable_views, true)
13
- ::Brick.enable_routes = app.config.brick.fetch(:enable_routes, true)
11
+ ::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, false)
12
+ ::Brick.enable_views = app.config.brick.fetch(:enable_views, false)
13
+ ::Brick.enable_routes = app.config.brick.fetch(:enable_routes, false)
14
14
  ::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
15
15
 
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
+ # When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
20
+ ::Brick.table_name_prefixes = app.config.brick.fetch(:table_name_prefixes, [])
21
+
19
22
  # Columns to treat as being metadata for purposes of identifying associative tables for has_many :through
20
23
  ::Brick.metadata_columns = app.config.brick.fetch(:metadata_columns, ['created_at', 'updated_at', 'deleted_at'])
21
24
 
22
25
  # Additional references (virtual foreign keys)
23
26
  ::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
24
27
 
28
+ # Skip creating a has_many association for these
29
+ ::Brick.skip_hms = app.config.brick.fetch(:skip_hms, nil)
30
+
25
31
  # Has one relationships
26
32
  ::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
27
33
  end
@@ -31,7 +37,7 @@ module Brick
31
37
  # ====================================
32
38
  # Dynamically create generic templates
33
39
  # ====================================
34
- if ::Brick.enable_views?
40
+ if Rails.development? || ::Brick.enable_views?
35
41
  ActionView::LookupContext.class_exec do
36
42
  alias :_brick_template_exists? :template_exists?
37
43
  def template_exists?(*args, **options)
@@ -62,78 +68,152 @@ module Brick
62
68
  # This gets has_many as well as has_many :through
63
69
  # %%% weed out ones that don't have an available model to reference
64
70
  bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
65
- # Weed out has_manys that go to an associative table
66
- associatives = hms.select { |k, v| v.options[:through] }.each_with_object({}) do |hmt, s|
67
- s[hmt.first] = hms.delete(hmt.last.options[:through]) # End up with a hash of HMT names pointing to join-table associations
71
+ # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
72
+ # as well as any possible polymorphic associations
73
+ skip_hms = {}
74
+ associatives = hms.each_with_object({}) do |hmt, s|
75
+ if (through = hmt.last.options[:through])
76
+ skip_hms[through] = nil
77
+ s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
78
+ elsif hmt.last.inverse_of.nil?
79
+ puts "SKIPPING #{hmt.last.name.inspect}"
80
+ # %%% If we don't do this then below associative.name will find that associative is nil
81
+ skip_hms[hmt.last.name] = nil
82
+ end
68
83
  end
69
- hms_headers = hms.each_with_object(+'') { |hm, s| s << "<th>H#{hm.last.macro == :has_one ? 'O' : 'M'}#{'T' if hm.last.options[:through]} #{hm.first}</th>\n" }
70
- hms_columns = hms.each_with_object(+'') do |hm, s|
71
- hm_fk_name = if hm.last.options[:through]
72
- associative = associatives[hm.last.name]
73
- "'#{associative.name}.#{associative.foreign_key}'"
74
- else
75
- hm.last.foreign_key
76
- end
77
- s << if hm.last.macro == :has_many
84
+
85
+ schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
86
+ hms_columns = +'' # Used for 'index'
87
+ # puts skip_hms.inspect
88
+ hms_headers = hms.each_with_object([]) do |hm, s|
89
+ next if skip_hms.key?(hm.last.name)
90
+
91
+ if args.first == 'index'
92
+ hm_fk_name = if hm.last.options[:through]
93
+ associative = associatives[hm.last.name]
94
+ "'#{associative.name}.#{associative.foreign_key}'"
95
+ else
96
+ hm.last.foreign_key
97
+ end
98
+ hms_columns << if hm.last.macro == :has_many
78
99
  "<td>
79
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? %>
80
101
  </td>\n"
81
- else # has_one
102
+ else # has_one
82
103
  "<td>
83
104
  <%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
84
105
  </td>\n"
85
- end
106
+ end
107
+ end
108
+ s << [hm.last, "H#{hm.last.macro == :has_one ? 'O' : 'M'}#{'T' if hm.last.options[:through]} #{hm.first}"]
86
109
  end
87
110
 
111
+ css = "<style>
112
+ table {
113
+ border-collapse: collapse;
114
+ margin: 25px 0;
115
+ font-size: 0.9em;
116
+ font-family: sans-serif;
117
+ min-width: 400px;
118
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
119
+ }
120
+
121
+ table thead tr th, table tr th {
122
+ background-color: #009879;
123
+ color: #ffffff;
124
+ text-align: left;
125
+ }
126
+
127
+ table th, table td {
128
+ padding: 0.2em 0.5em;
129
+ }
130
+
131
+ .show-field {
132
+ background-color: #004998;
133
+ }
134
+
135
+ table tbody tr {
136
+ border-bottom: thin solid #dddddd;
137
+ }
138
+
139
+ table tbody tr:nth-of-type(even) {
140
+ background-color: #f3f3f3;
141
+ }
142
+
143
+ table tbody tr:last-of-type {
144
+ border-bottom: 2px solid #009879;
145
+ }
146
+
147
+ table tbody tr.active-row {
148
+ font-weight: bold;
149
+ color: #009879;
150
+ }
151
+
152
+ a.show-arrow {
153
+ font-size: 2.5em;
154
+ text-decoration: none;
155
+ }
156
+ </style>"
157
+
158
+ script = "<script>
159
+ var schemaSelect = document.getElementById(\"schema\");
160
+ if (schemaSelect) {
161
+ var brickSchema = changeout(location.href, \"_brick_schema\");
162
+ if (brickSchema) {
163
+ [... document.getElementsByTagName(\"A\")].forEach(function (a) { a.href = changeout(a.href, \"_brick_schema\", brickSchema); });
164
+ }
165
+ schemaSelect.value = brickSchema || \"public\";
166
+ schemaSelect.focus();
167
+ schemaSelect.addEventListener(\"change\", function () {
168
+ location.href = changeout(location.href, \"_brick_schema\", this.value);
169
+ });
170
+ }
171
+ function changeout(href, param, value) {
172
+ var hrefParts = href.split(\"?\");
173
+ var params = hrefParts.length > 1 ? hrefParts[1].split(\"&\") : [];
174
+ params = params.reduce(function (s, v) { var parts = v.split(\"=\"); s[parts[0]] = parts[1]; return s; }, {});
175
+ if (value === undefined) return params[param];
176
+ params[param] = value;
177
+ return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
178
+ }
179
+ </script>"
180
+
88
181
  inline = case args.first
89
182
  when 'index'
90
- "<p style=\"color: green\"><%= notice %></p>
91
-
183
+ "#{css}
184
+ <p style=\"color: green\"><%= notice %></p>#{"
185
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
92
186
  <h1>#{model_name.pluralize}</h1>
93
187
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
94
-
95
188
  <table id=\"#{table_name}\">
96
- <tr>
97
- <% is_first = true; is_need_id_col = nil
98
- 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(', ')} }
189
+ <thead><tr>#{"<th></th>" if pk }
190
+ <% 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(', ')} }
99
191
  @#{table_name}.columns.map(&:name).each do |col| %>
100
192
  <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
101
193
  <th>
102
- <% if bt = bts[col]
103
- if is_first
104
- is_first = false
105
- is_need_id_col = true %>
106
- </th><th>
107
- <% end %>
194
+ <% if (bt = bts[col]) %>
108
195
  BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
109
- <% else
110
- is_first = false %>
196
+ <% else %>
111
197
  <%= col %>
112
198
  <% end %>
113
199
  </th>
114
200
  <% end %>
115
- <% if is_first # STILL haven't been able to write a first non-key / non-metadata column?
116
- is_first = false
117
- is_need_id_col = true %>
118
- <th></th>
119
- <% end %>
120
- #{hms_headers}
121
- </tr>
201
+ #{hms_headers.map { |h| "<th>#{h.last}</th>\n" }.join}
202
+ </tr></thead>
122
203
 
204
+ <tbody>
123
205
  <% @#{table_name}.each do |#{obj_name}| %>
124
- <tr>
125
- <% is_first = true
126
- if is_need_id_col
127
- is_first = false %>
128
- <td><%= link_to \"#\{#{obj_name}.class.name\} ##\{#{obj_name}.id\}\", #{obj_name} %></td>
129
- <% end %>
206
+ <tr>#{"
207
+ <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'show-arrow' } %></td>" if pk }
130
208
  <% #{obj_name}.attributes.each do |k, val| %>
131
209
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
132
210
  <td>
133
211
  <% if (bt = bts[k]) %>
134
- <%= obj = bt[1].find_by(bt.last => val); link_to(obj.brick_descrip, obj) if obj %>
135
- <% elsif is_first %>
136
- <%= is_first = false; link_to val, #{obj_name}_path(#{obj_name}.#{pk}) %>
212
+ <%# Instead of just 'bt_obj we have to put in all of this junk:
213
+ # send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))
214
+ # Otherwise we get stuff like:
215
+ # 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 %>
137
217
  <% else %>
138
218
  <%= val %>
139
219
  <% end %>
@@ -142,13 +222,55 @@ module Brick
142
222
  #{hms_columns}
143
223
  <!-- td>X</td -->
144
224
  </tr>
225
+ </tbody>
145
226
  <% end %>
146
227
  </table>
147
228
 
148
229
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
149
- "
230
+ #{script}"
150
231
  when 'show'
151
- "<%= @#{@_brick_model.name.underscore}.inspect %>"
232
+ "#{css}
233
+ <p style=\"color: green\"><%= notice %></p>#{"
234
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
235
+ <h1>#{model_name}: <%= (obj = @#{obj_name}.first).brick_descrip %></h1>
236
+ <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
237
+ <table>
238
+ <% 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
+ @#{obj_name}.first.attributes.each do |k, val| %>
240
+ <tr>
241
+ <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
242
+ <th class=\"show-field\">
243
+ <% if (bt = bts[k]) %>
244
+ BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
245
+ <% else %>
246
+ <%= k %>
247
+ <% end %>
248
+ </th>
249
+ <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 %>
252
+ <% else %>
253
+ <%= val %>
254
+ <% end %>
255
+ </td>
256
+ </tr>
257
+ <% end %>
258
+ </table>
259
+
260
+ #{hms_headers.map do |hm|
261
+ next unless (pk = hm.first.klass.primary_key)
262
+ "<table id=\"#{hm_name = hm.first.name.to_s}\">
263
+ <tr><th>#{hm.last}</th></tr>
264
+ <% if (collection = @#{obj_name}.first.#{hm_name}).empty? %>
265
+ <tr><td>(none)</td></tr>
266
+ <% 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>
269
+ <% end %>
270
+ <% end %>
271
+ </table>" end.join}
272
+ #{script}"
273
+
152
274
  end
153
275
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
154
276
  keys = options.has_key?(:locals) ? options[:locals].keys : []
@@ -161,7 +283,7 @@ module Brick
161
283
  end
162
284
  end
163
285
 
164
- if ::Brick.enable_routes?
286
+ if Rails.development? || ::Brick.enable_routes?
165
287
  ActionDispatch::Routing::RouteSet.class_exec do
166
288
  alias _brick_finalize_routeset! finalize!
167
289
  def finalize!(*args, **options)
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 8
8
+ TINY = 12
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
@@ -86,6 +86,13 @@ module Brick
86
86
  end
87
87
 
88
88
  class << self
89
+ attr_accessor :db_schemas
90
+
91
+ def set_db_schema(params)
92
+ schema = params['_brick_schema'] || 'public'
93
+ ActiveRecord::Base.connection.execute("SET SEARCH_PATH='#{schema}';") if schema && ::Brick.db_schemas&.include?(schema)
94
+ end
95
+
89
96
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
90
97
  def relations
91
98
  connections = Brick.instance_variable_get(:@relations) ||
@@ -98,23 +105,10 @@ module Brick
98
105
  model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
99
106
  case a.macro
100
107
  when :belongs_to
101
- # Build #brick_descrip if needed
102
- if a.klass.instance_methods(false).exclude?(:brick_descrip)
103
- descrip_col = (a.klass.columns.map(&:name) - a.klass._brick_get_fks -
104
- (::Brick.config.metadata_columns || []) -
105
- [a.klass.primary_key]).first&.to_sym
106
- if descrip_col
107
- a.klass.define_method :brick_descrip do
108
- send(descrip_col)
109
- end
110
- end
111
- end
112
-
113
108
  s.first[a.foreign_key] = [a.name, a.klass]
114
109
  when :has_many, :has_one
115
110
  s.last[a.name] = a
116
111
  end
117
- s
118
112
  end
119
113
  end
120
114
 
@@ -180,6 +174,11 @@ module Brick
180
174
  Brick.config.exclude_tables = value
181
175
  end
182
176
 
177
+ # @api public
178
+ def table_name_prefixes=(value)
179
+ Brick.config.table_name_prefixes = value
180
+ end
181
+
183
182
  # @api public
184
183
  def metadata_columns=(value)
185
184
  Brick.config.metadata_columns = value
@@ -196,6 +195,18 @@ module Brick
196
195
  end
197
196
  end
198
197
 
198
+ # Skip creating a has_many association for these
199
+ # (Uses the same exact three-part format as would define an additional_reference)
200
+ # @api public
201
+ def skip_hms=(skips)
202
+ if skips
203
+ skips = skips.call if skips.is_a?(Proc)
204
+ skips = skips.to_a unless skips.is_a?(Array)
205
+ skips = [skips] unless skips.empty? || skips.first.is_a?(Array)
206
+ Brick.config.skip_hms = skips
207
+ end
208
+ end
209
+
199
210
  # Associations to treat as a has_one
200
211
  # @api public
201
212
  def has_ones=(hos)
@@ -211,6 +222,11 @@ module Brick
211
222
  end
212
223
  end
213
224
 
225
+ # DSL templates for individual models to provide prettier descriptions of objects
226
+ # @api public
227
+ def model_descrips=(descrips)
228
+ Brick.config.model_descrips = descrips
229
+ end
214
230
 
215
231
  # Returns Brick's `::Gem::Version`, convenient for comparisons. This is
216
232
  # recommended over `::Brick::VERSION::STRING`.
@@ -19,23 +19,84 @@ module Brick
19
19
 
20
20
  def create_initializer_file
21
21
  unless File.exists?(filename = 'config/initializers/brick.rb')
22
+ # See if we can make suggestions for additional_references
23
+ resembles_fks = []
24
+ possible_additional_references = (relations = ::Brick.relations).each_with_object([]) do |v, s|
25
+ v.last[:cols].each do |col, _type|
26
+ col_down = col.downcase
27
+ is_possible = true
28
+ if col_down.end_with?('_id')
29
+ col_down = col_down[0..-4]
30
+ elsif col_down.end_with?('id')
31
+ col_down = col_down[0..-3]
32
+ is_possible = false if col_down.length < 3 # Was it simply called "id" or something else really short?
33
+ elsif col_down.start_with?('id_')
34
+ col_down = col_down[3..-1]
35
+ elsif col_down.start_with?('id')
36
+ col_down = col_down[2..-1]
37
+ else
38
+ is_possible = false
39
+ end
40
+ if col_down.start_with?('fk_')
41
+ is_possible = true
42
+ col_down = col_down[3..-1]
43
+ end
44
+ # This possible key not really a primary key and not yet used as a foreign key?
45
+ if is_possible && !(relation = relations.fetch(v.first, {}))[:pkey].first&.last&.include?(col) &&
46
+ !relations.fetch(v.first, {})[:fks]&.any? { |_k, v| v[:is_bt] && v[:fk] == col }
47
+ if (relations.fetch(f_table = col_down, nil) ||
48
+ relations.fetch(f_table = ActiveSupport::Inflector.pluralize(col_down), nil)) &&
49
+ # Looks pretty promising ... just make sure a model file isn't present
50
+ !File.exists?("app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb")
51
+ s << "['#{v.first}', '#{col}', '#{f_table}']"
52
+ else
53
+ resembles_fks << "#{v.first}.#{col}"
54
+ end
55
+ end
56
+ end
57
+ s
58
+ end
59
+
60
+ bar = case possible_additional_references.length
61
+ when 0
62
+ +"# Brick.additional_references = [['orders', 'customer_id', 'customer'],
63
+ # ['customer', 'region_id', 'regions']]"
64
+ when 1
65
+ +"# # Here is a possible additional reference that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
66
+ # Brick.additional_references = [[#{possible_additional_references.first}]"
67
+ else
68
+ +"# # Here are possible additional references that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
69
+ # Brick.additional_references = [
70
+ # #{possible_additional_references.join(",\n# ")}
71
+ # ]"
72
+ end
73
+ if resembles_fks.length > 0
74
+ bar << "\n# # Columns named somewhat like a foreign key which you may want to consider:
75
+ # # #{resembles_fks.join(', ')}"
76
+ end
77
+
22
78
  create_file filename, "# frozen_string_literal: true
23
79
 
24
80
  # # Settings for the Brick gem
25
81
  # # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
26
82
 
27
- # # Normally these all start out as being enabled, but can be selectively disabled:
28
- # Brick.enable_routes = false
83
+ # # Normally all are enabled in development mode, and for security reasons only models are enabled in production
84
+ # # and test. This allows you to either (a) turn off models entirely, or (b) enable controllers, views, and routes
85
+ # # in production.
86
+ # Brick.enable_routes = true # Setting this to \"false\" will disable routes in development
29
87
  # Brick.enable_models = false
30
- # Brick.enable_controllers = false
31
- # Brick.enable_views = false
88
+ # Brick.enable_controllers = true # Setting this to \"false\" will disable controllers in development
89
+ # Brick.enable_views = true # Setting this to \"false\" will disable views in development
32
90
 
33
- # # By default models are auto-created from database views, and set to be read-only. This can be skipped.
91
+ # # By default models are auto-created for database views, and set to be read-only. This can be skipped.
34
92
  # Brick.skip_database_views = true
35
93
 
36
94
  # # Any tables or views you'd like to skip when auto-creating models
37
95
  # Brick.exclude_tables = ['custom_metadata', 'version_info']
38
96
 
97
+ # # When table names have specific prefixes automatically place them in their own module with a table_name_prefix.
98
+ # Brick.table_name_prefixes = { 'nav_' => 'Navigation' }
99
+
39
100
  # # Additional table references which are used to create has_many / belongs_to associations inside auto-created
40
101
  # # models. (You can consider these to be \"virtual foreign keys\" if you wish)... You only have to add these
41
102
  # # in cases where your database for some reason does not have foreign key constraints defined. Sometimes for
@@ -44,9 +105,13 @@ module Brick
44
105
  # # foreign table name / foreign key column / primary table name.
45
106
  # # (We boldly expect that the primary key identified by ActiveRecord on the primary table will be accurate,
46
107
  # # usually this is \"id\" but there are some good smarts that are used in case some other column has been set
47
- # # to be the primary key.
48
- # Brick.additional_references = [['orders', 'customer_id', 'customer'],
49
- # ['customer', 'region_id', 'regions']]
108
+ # # to be the primary key.)
109
+ #{bar}
110
+
111
+ # # Skip creating a has_many association for these
112
+ # # (Uses the same exact three-part format as would define an additional_reference)
113
+ # # 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']]
50
115
 
51
116
  # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
52
117
  # # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
@@ -56,11 +121,19 @@ module Brick
56
121
  # # instead of \"user_profile\", then apply that as a third parameter like this:
57
122
  # Brick.has_ones = [['User', 'user_profile', 'profile']]
58
123
 
59
- # # We normally don't consider the timestamp columns \"created_at\", \"updated_at\", and \"deleted_at\" to count when
60
- # # finding tables which can serve as associative tables in an N:M association. That is, ones that can be a
61
- # # part of a has_many :through association. If you want to use different exclusion columns than our defaults
62
- # # then this setting resets that list. For instance, here is the override for the Sakila sample database:
63
- # Brick.metadata_columns = ['last_updated']
124
+ # # We normally don't show the timestamp columns \"created_at\", \"updated_at\", and \"deleted_at\", and also do
125
+ # # not consider them when finding associative tables to support an N:M association. (That is, ones that can be a
126
+ # # part of a has_many :through association.) If you want to use different exclusion columns than our defaults
127
+ # # then this setting resets that list. For instance, here is an override that is useful in the Sakila sample
128
+ # # database:
129
+ # Brick.metadata_columns = ['last_update']
130
+
131
+ # # A simple DSL is available to allow more user-friendly display of objects. Normally a user object might be shown
132
+ # # as its first non-metadata column, or if that is not available then something like \"User #45\" where 45 is that
133
+ # # object's ID. If there is no primary key then even that is not possible, so the object's .to_s method is called.
134
+ # # To override these defaults and specify exactly what you want shown, such as first names and last names for a
135
+ # # user, then you can use model_descrips like this, putting expressions with property references in square brackets:
136
+ # Brick.model_descrips = { 'User' => '[profile.firstname] [profile.lastname]' }
64
137
 
65
138
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
66
139
  # # 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.8
4
+ version: 1.0.12
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-20 00:00:00.000000000 Z
11
+ date: 2022-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord